cryptoserve 0.3.2 → 0.3.3

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.
@@ -6,7 +6,7 @@
6
6
  * Includes project-level transparency stats when available.
7
7
  */
8
8
 
9
- import { TIERS, getCatalogSize } from './package-catalog.mjs';
9
+ import { TIERS, CATEGORIES, getCatalogSize } from './package-catalog.mjs';
10
10
 
11
11
  // NIST Post-Quantum Cryptography deadlines
12
12
  const NIST_2030 = new Date('2030-01-01T00:00:00Z');
@@ -78,6 +78,50 @@ function buildEcosystemBreakdown(pkgs, period) {
78
78
  };
79
79
  }
80
80
 
81
+ /**
82
+ * Build per-category breakdown across all packages.
83
+ * Groups packages by category and computes weak/modern/pqc totals + weak percentage.
84
+ */
85
+ function buildCategoryBreakdown(allPkgs) {
86
+ const categoryMap = {};
87
+ for (const cat of CATEGORIES) {
88
+ categoryMap[cat] = { category: cat, weak: 0, modern: 0, pqc: 0, total: 0, weakPercentage: 0, topPackages: [] };
89
+ }
90
+
91
+ for (const pkg of allPkgs) {
92
+ const cat = pkg.category || 'general';
93
+ const entry = categoryMap[cat];
94
+ if (!entry) continue;
95
+
96
+ const dl = pkg.downloads || 0;
97
+ if (pkg.tier === TIERS.WEAK) entry.weak += dl;
98
+ else if (pkg.tier === TIERS.PQC) entry.pqc += dl;
99
+ else entry.modern += dl;
100
+
101
+ entry.total += dl;
102
+ entry.topPackages.push(pkg);
103
+ }
104
+
105
+ // Compute weak percentage and sort top packages
106
+ const result = [];
107
+ for (const cat of CATEGORIES) {
108
+ const entry = categoryMap[cat];
109
+ entry.weakPercentage = entry.total > 0
110
+ ? Math.round((entry.weak / entry.total) * 1000) / 10
111
+ : 0;
112
+ entry.topPackages = entry.topPackages
113
+ .sort((a, b) => b.downloads - a.downloads)
114
+ .slice(0, 10);
115
+ if (entry.total > 0) {
116
+ result.push(entry);
117
+ }
118
+ }
119
+
120
+ // Sort by total downloads descending
121
+ result.sort((a, b) => b.total - a.total);
122
+ return result;
123
+ }
124
+
81
125
  /**
82
126
  * Aggregate all census data into headline metrics.
83
127
  *
@@ -178,6 +222,9 @@ export function aggregate(data) {
178
222
  return d?.packages?.length > 0;
179
223
  });
180
224
 
225
+ // Category breakdown
226
+ const categoryBreakdown = buildCategoryBreakdown(allPkgs);
227
+
181
228
  return {
182
229
  // Headline numbers
183
230
  totalDownloads,
@@ -189,6 +236,9 @@ export function aggregate(data) {
189
236
  pqcPercentage,
190
237
  weakToPqcRatio,
191
238
 
239
+ // Category breakdown
240
+ categoryBreakdown,
241
+
192
242
  // Per-ecosystem
193
243
  npm,
194
244
  pypi,
@@ -46,17 +46,17 @@ export async function collectCocoapodsDownloads(packages, options = {}) {
46
46
 
47
47
  if (!res.ok) {
48
48
  if (verbose) process.stderr.write(` cocoapods ${pkg.name}: ${res.status}\n`);
49
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
49
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
50
50
  } else {
51
51
  // Trunk API confirms pod exists but has no download stats.
52
52
  // Use conservative estimate: CocoaPods ecosystem is smaller,
53
53
  // most crypto pods get 1K-50K installs/month based on GitHub activity.
54
54
  // We set 0 and rely on scanner data if available.
55
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
55
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
56
56
  }
57
57
  } catch (err) {
58
58
  if (verbose) process.stderr.write(` cocoapods ${pkg.name} error: ${err.message}\n`);
59
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
59
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
60
60
  }
61
61
 
62
62
  if (i < packages.length - 1) {
@@ -45,17 +45,17 @@ export async function collectCratesDownloads(packages, options = {}) {
45
45
 
46
46
  if (!res.ok) {
47
47
  if (verbose) process.stderr.write(` crates ${pkg.name}: ${res.status}\n`);
48
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
48
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
49
49
  } else {
50
50
  const data = await res.json();
51
51
  // recent_downloads = last 90 days, divide by 3 for monthly estimate
52
52
  const recentDownloads = data?.crate?.recent_downloads || 0;
53
53
  const monthlyEstimate = Math.round(recentDownloads / 3);
54
- results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier });
54
+ results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
55
55
  }
56
56
  } catch (err) {
57
57
  if (verbose) process.stderr.write(` crates ${pkg.name} error: ${err.message}\n`);
58
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
58
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
59
59
  }
60
60
 
61
61
  if (i < packages.length - 1) {
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Enrich top packages with GitHub repository metadata.
3
+ *
4
+ * Fetches stars, forks, last push date, and archived status from the GitHub API
5
+ * for the top packages by download count. Uses unauthenticated requests
6
+ * (60 req/hr limit), so we limit to 30 packages with 1s delays.
7
+ */
8
+
9
+ const REQUEST_DELAY_MS = 1000;
10
+ const MAX_ENRICHMENTS = 30;
11
+
12
+ /**
13
+ * Static mapping of package name to GitHub owner/repo.
14
+ * Many registries don't expose repo URLs in download APIs,
15
+ * so we maintain this mapping for top packages.
16
+ */
17
+ const KNOWN_REPOS = {
18
+ // npm
19
+ 'crypto-js': 'brix/crypto-js',
20
+ '@noble/hashes': 'paulmillr/noble-hashes',
21
+ '@noble/curves': 'paulmillr/noble-curves',
22
+ '@noble/ciphers': 'paulmillr/noble-ciphers',
23
+ '@noble/post-quantum': 'paulmillr/noble-post-quantum',
24
+ 'node-forge': 'digitalbazaar/forge',
25
+ 'jose': 'panva/jose',
26
+ 'elliptic': 'indutny/elliptic',
27
+ 'hash.js': 'indutny/hash.js',
28
+ 'tweetnacl': 'nicola/tweetnacl-js',
29
+ 'bcryptjs': 'nicola/bcrypt.js',
30
+ 'jsonwebtoken': 'auth0/node-jsonwebtoken',
31
+ 'sodium-native': 'nicola/sodium-native',
32
+ 'md5': 'pvorb/node-md5',
33
+ 'scrypt-js': 'nicola/scrypt-js',
34
+
35
+ // PyPI
36
+ 'cryptography': 'pyca/cryptography',
37
+ 'pycryptodome': 'Legrandin/pycryptodome',
38
+ 'bcrypt': 'pyca/bcrypt',
39
+ 'pynacl': 'pyca/pynacl',
40
+ 'argon2-cffi': 'hynek/argon2-cffi',
41
+ 'PyJWT': 'jpadilla/pyjwt',
42
+ 'liboqs-python': 'open-quantum-safe/liboqs-python',
43
+
44
+ // Rust crates
45
+ 'ring': 'briansmith/ring',
46
+ 'rustls': 'rustls/rustls',
47
+ 'ed25519-dalek': 'dalek-cryptography/curve25519-dalek',
48
+ 'sha2': 'RustCrypto/hashes',
49
+ 'aes-gcm': 'RustCrypto/AEADs',
50
+ 'chacha20poly1305': 'RustCrypto/AEADs',
51
+ 'argon2': 'RustCrypto/password-hashes',
52
+
53
+ // Go
54
+ 'github.com/cloudflare/circl': 'cloudflare/circl',
55
+ 'golang-jwt/jwt/v5': 'golang-jwt/jwt',
56
+
57
+ // Maven
58
+ 'org.bouncycastle:bcprov-jdk18on': 'bcgit/bc-java',
59
+ 'com.google.crypto.tink:tink': 'google/tink',
60
+ 'io.jsonwebtoken:jjwt-api': 'jwtk/jjwt',
61
+
62
+ // PHP
63
+ 'phpseclib/phpseclib': 'phpseclib/phpseclib',
64
+ 'defuse/php-encryption': 'defuse/php-encryption',
65
+ 'firebase/php-jwt': 'firebase/php-jwt',
66
+
67
+ // Ruby
68
+ 'rbnacl': 'crypto-rb/rbnacl',
69
+ 'jwt': 'jwt/ruby-jwt',
70
+
71
+ // Dart
72
+ 'pointycastle': 'nicola/pc-dart',
73
+
74
+ // Swift
75
+ 'CryptoSwift': 'nicola/CryptoSwift',
76
+ };
77
+
78
+ function sleep(ms) {
79
+ return new Promise(resolve => setTimeout(resolve, ms));
80
+ }
81
+
82
+ /**
83
+ * Enrich packages with GitHub metadata.
84
+ *
85
+ * @param {Array} allPackages - All package entries (with name + downloads)
86
+ * @param {Object} [options]
87
+ * @param {Function} [options.fetchFn] - Fetch implementation
88
+ * @param {boolean} [options.verbose] - Log progress
89
+ * @returns {Promise<Map<string, {stars: number, forks: number, lastPush: string, archived: boolean}>>}
90
+ */
91
+ export async function collectGithubEnrichment(allPackages, options = {}) {
92
+ const fetchFn = options.fetchFn || globalThis.fetch;
93
+ const verbose = options.verbose || false;
94
+
95
+ // Sort by downloads descending and pick top packages that have known repos
96
+ const sorted = [...allPackages]
97
+ .sort((a, b) => b.downloads - a.downloads);
98
+
99
+ const toEnrich = [];
100
+ const seen = new Set();
101
+ for (const pkg of sorted) {
102
+ const repo = KNOWN_REPOS[pkg.name];
103
+ if (!repo || seen.has(repo)) continue;
104
+ seen.add(repo);
105
+ toEnrich.push({ name: pkg.name, repo });
106
+ if (toEnrich.length >= MAX_ENRICHMENTS) break;
107
+ }
108
+
109
+ if (verbose) {
110
+ process.stderr.write(` github enrichment: ${toEnrich.length} packages to enrich\n`);
111
+ }
112
+
113
+ const results = new Map();
114
+
115
+ for (const { name, repo } of toEnrich) {
116
+ try {
117
+ const res = await fetchFn(`https://api.github.com/repos/${repo}`, {
118
+ headers: { 'Accept': 'application/vnd.github+json' },
119
+ });
120
+
121
+ if (!res.ok) {
122
+ if (verbose) process.stderr.write(` github ${repo}: HTTP ${res.status}\n`);
123
+ // Check rate limit
124
+ const remaining = res.headers?.get?.('x-ratelimit-remaining');
125
+ if (remaining === '0') {
126
+ if (verbose) process.stderr.write(' github rate limit hit, stopping\n');
127
+ break;
128
+ }
129
+ await sleep(REQUEST_DELAY_MS);
130
+ continue;
131
+ }
132
+
133
+ const data = await res.json();
134
+ results.set(name, {
135
+ stars: data.stargazers_count || 0,
136
+ forks: data.forks_count || 0,
137
+ lastPush: data.pushed_at || null,
138
+ archived: data.archived || false,
139
+ });
140
+
141
+ if (verbose) {
142
+ process.stderr.write(` github ${repo}: ${data.stargazers_count} stars\n`);
143
+ }
144
+
145
+ await sleep(REQUEST_DELAY_MS);
146
+ } catch (err) {
147
+ if (verbose) process.stderr.write(` github ${repo} error: ${err.message}\n`);
148
+ await sleep(REQUEST_DELAY_MS);
149
+ }
150
+ }
151
+
152
+ if (verbose) {
153
+ process.stderr.write(` github enrichment: ${results.size} packages enriched\n`);
154
+ }
155
+
156
+ return {
157
+ enrichments: Object.fromEntries(results),
158
+ collectedAt: new Date().toISOString(),
159
+ };
160
+ }
@@ -73,7 +73,7 @@ export async function collectGoDownloads(packages, options = {}) {
73
73
  // Stdlib packages: use hardcoded estimates
74
74
  if (pkg.name.startsWith('crypto/')) {
75
75
  const estimate = STDLIB_ESTIMATES[pkg.name] || 10_000;
76
- results.push({ name: pkg.name, downloads: estimate, tier: pkg.tier });
76
+ results.push({ name: pkg.name, downloads: estimate, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
77
77
  continue;
78
78
  }
79
79
 
@@ -84,7 +84,7 @@ export async function collectGoDownloads(packages, options = {}) {
84
84
 
85
85
  if (!res.ok) {
86
86
  if (verbose) process.stderr.write(` go ${pkg.name}: proxy ${res.status}\n`);
87
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
87
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
88
88
  continue;
89
89
  }
90
90
 
@@ -115,10 +115,10 @@ export async function collectGoDownloads(packages, options = {}) {
115
115
  : Math.max(downloads, 500_000);
116
116
  }
117
117
 
118
- results.push({ name: pkg.name, downloads, tier: pkg.tier });
118
+ results.push({ name: pkg.name, downloads, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
119
119
  } catch (err) {
120
120
  if (verbose) process.stderr.write(` go ${pkg.name} error: ${err.message}\n`);
121
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
121
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
122
122
  }
123
123
 
124
124
  if (i < packages.length - 1) await sleep(REQUEST_DELAY_MS);
@@ -42,18 +42,18 @@ export async function collectHexDownloads(packages, options = {}) {
42
42
 
43
43
  if (!res.ok) {
44
44
  if (verbose) process.stderr.write(` hex ${pkg.name}: ${res.status}\n`);
45
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
45
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
46
46
  } else {
47
47
  const data = await res.json();
48
48
  // Hex provides downloads.recent (last 90 days) and downloads.all
49
49
  const recentDownloads = data?.downloads?.recent || 0;
50
50
  // Estimate monthly from 90-day window
51
51
  const monthlyEstimate = Math.round(recentDownloads / 3);
52
- results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier });
52
+ results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
53
53
  }
54
54
  } catch (err) {
55
55
  if (verbose) process.stderr.write(` hex ${pkg.name} error: ${err.message}\n`);
56
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
56
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
57
57
  }
58
58
 
59
59
  if (i < packages.length - 1) {
@@ -57,7 +57,7 @@ export async function collectMavenDownloads(packages, options = {}) {
57
57
 
58
58
  if (!res.ok) {
59
59
  if (verbose) process.stderr.write(` maven ${pkg.name}: ${res.status}\n`);
60
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
60
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
61
61
  } else {
62
62
  const data = await res.json();
63
63
  const doc = data?.response?.docs?.[0];
@@ -72,11 +72,15 @@ export async function collectMavenDownloads(packages, options = {}) {
72
72
  name: pkg.name,
73
73
  downloads: estimatedDownloads,
74
74
  tier: pkg.tier,
75
+ category: pkg.category,
76
+ replacedBy: pkg.replacedBy,
77
+ algorithms: pkg.algorithms,
78
+ note: pkg.note,
75
79
  });
76
80
  }
77
81
  } catch (err) {
78
82
  if (verbose) process.stderr.write(` maven ${pkg.name} error: ${err.message}\n`);
79
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
83
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
80
84
  }
81
85
 
82
86
  if (i < packages.length - 1) {
@@ -28,13 +28,13 @@ async function fetchSingle(pkg, fetchFn, period, verbose) {
28
28
  period.start = data.start;
29
29
  period.end = data.end;
30
30
  }
31
- return { name: pkg.name, downloads: data.downloads || 0, tier: pkg.tier };
31
+ return { name: pkg.name, downloads: data.downloads || 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note };
32
32
  }
33
33
  if (verbose) process.stderr.write(` npm ${pkg.name}: HTTP ${res.status}\n`);
34
34
  } catch (err) {
35
35
  if (verbose) process.stderr.write(` npm ${pkg.name} error: ${err.message}\n`);
36
36
  }
37
- return { name: pkg.name, downloads: 0, tier: pkg.tier };
37
+ return { name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note };
38
38
  }
39
39
 
40
40
  /**
@@ -90,7 +90,7 @@ export async function collectNpmDownloads(packages, options = {}) {
90
90
  period.start = data.start;
91
91
  period.end = data.end;
92
92
  }
93
- results.push({ name: batch[0].name, downloads: data.downloads || 0, tier: batch[0].tier });
93
+ results.push({ name: batch[0].name, downloads: data.downloads || 0, tier: batch[0].tier, category: batch[0].category, replacedBy: batch[0].replacedBy, algorithms: batch[0].algorithms, note: batch[0].note });
94
94
  } else {
95
95
  for (const pkg of batch) {
96
96
  const entry = data[pkg.name];
@@ -99,16 +99,16 @@ export async function collectNpmDownloads(packages, options = {}) {
99
99
  period.start = entry.start;
100
100
  period.end = entry.end;
101
101
  }
102
- results.push({ name: pkg.name, downloads: entry.downloads || 0, tier: pkg.tier });
102
+ results.push({ name: pkg.name, downloads: entry.downloads || 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
103
103
  } else {
104
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
104
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
105
105
  }
106
106
  }
107
107
  }
108
108
  } catch (err) {
109
109
  if (verbose) process.stderr.write(` npm batch ${i + 1} error: ${err.message}\n`);
110
110
  for (const pkg of batch) {
111
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
111
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
112
112
  }
113
113
  }
114
114
 
@@ -45,18 +45,18 @@ export async function collectNugetDownloads(packages, options = {}) {
45
45
 
46
46
  if (!res.ok) {
47
47
  if (verbose) process.stderr.write(` nuget ${pkg.name}: ${res.status}\n`);
48
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
48
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
49
49
  } else {
50
50
  const data = await res.json();
51
51
  const entry = data?.data?.[0];
52
52
  // NuGet returns total downloads, estimate monthly as total / 36 (3 years average)
53
53
  const totalDownloads = entry?.totalDownloads || 0;
54
54
  const monthlyEstimate = Math.round(totalDownloads / 36);
55
- results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier });
55
+ results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
56
56
  }
57
57
  } catch (err) {
58
58
  if (verbose) process.stderr.write(` nuget ${pkg.name} error: ${err.message}\n`);
59
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
59
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
60
60
  }
61
61
 
62
62
  if (i < packages.length - 1) {
@@ -41,15 +41,15 @@ export async function collectPackagistDownloads(packages, options = {}) {
41
41
 
42
42
  if (!res.ok) {
43
43
  if (verbose) process.stderr.write(` packagist ${pkg.name}: ${res.status}\n`);
44
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
44
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
45
45
  } else {
46
46
  const data = await res.json();
47
47
  const monthlyDownloads = data?.package?.downloads?.monthly || 0;
48
- results.push({ name: pkg.name, downloads: monthlyDownloads, tier: pkg.tier });
48
+ results.push({ name: pkg.name, downloads: monthlyDownloads, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
49
49
  }
50
50
  } catch (err) {
51
51
  if (verbose) process.stderr.write(` packagist ${pkg.name} error: ${err.message}\n`);
52
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
52
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
53
53
  }
54
54
 
55
55
  if (i < packages.length - 1) {
@@ -40,16 +40,16 @@ export async function collectPubDownloads(packages, options = {}) {
40
40
 
41
41
  if (!res.ok) {
42
42
  if (verbose) process.stderr.write(` pub ${pkg.name}: ${res.status}\n`);
43
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
43
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
44
44
  } else {
45
45
  const data = await res.json();
46
46
  // pub.dev score endpoint has downloadCount30Days
47
47
  const downloads = data?.downloadCount30Days || 0;
48
- results.push({ name: pkg.name, downloads: downloads, tier: pkg.tier });
48
+ results.push({ name: pkg.name, downloads: downloads, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
49
49
  }
50
50
  } catch (err) {
51
51
  if (verbose) process.stderr.write(` pub ${pkg.name} error: ${err.message}\n`);
52
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
52
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
53
53
  }
54
54
 
55
55
  if (i < packages.length - 1) {
@@ -41,16 +41,16 @@ export async function collectPypiDownloads(packages, options = {}) {
41
41
  const res = await fetchFn(url);
42
42
  if (!res.ok) {
43
43
  if (verbose) process.stderr.write(` pypi ${pkg.name}: ${res.status}\n`);
44
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
44
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
45
45
  } else {
46
46
  const data = await res.json();
47
47
  // Response: { data: { last_month: N, last_week: N, last_day: N }, ... }
48
48
  const downloads = data?.data?.last_month || 0;
49
- results.push({ name: pkg.name, downloads, tier: pkg.tier });
49
+ results.push({ name: pkg.name, downloads, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
50
50
  }
51
51
  } catch (err) {
52
52
  if (verbose) process.stderr.write(` pypi ${pkg.name} error: ${err.message}\n`);
53
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
53
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
54
54
  }
55
55
 
56
56
  // Delay between requests
@@ -41,17 +41,17 @@ export async function collectRubygemsDownloads(packages, options = {}) {
41
41
 
42
42
  if (!res.ok) {
43
43
  if (verbose) process.stderr.write(` rubygems ${pkg.name}: ${res.status}\n`);
44
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
44
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
45
45
  } else {
46
46
  const data = await res.json();
47
47
  // RubyGems only provides total downloads; estimate monthly
48
48
  const totalDownloads = data?.downloads || 0;
49
49
  const monthlyEstimate = Math.round(totalDownloads / 120);
50
- results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier });
50
+ results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
51
51
  }
52
52
  } catch (err) {
53
53
  if (verbose) process.stderr.write(` rubygems ${pkg.name} error: ${err.message}\n`);
54
- results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
54
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
55
55
  }
56
56
 
57
57
  if (i < packages.length - 1) {