npm-time-machine-cli 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marco Lo Pinto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,213 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/MarcoLoPinto/npm-time-machine-cli/main/assets/logo.png" width="300"/>
3
+ </p>
4
+
5
+ <h1 align="center">NTM: NPM Time Machine</h1>
6
+
7
+ Reproduce your npm dependency tree as it existed at a specific point in time.
8
+
9
+ ## ๐Ÿš€ Why?
10
+
11
+ Supply chain attacks and breaking changes often come from newly published versions of dependencies.
12
+
13
+ `npm-time-machine-cli` lets you:
14
+
15
+ - Install dependencies as they existed in the past
16
+ - Avoid recently introduced malicious or unstable versions
17
+ - Reproduce old environments reliably
18
+
19
+ ## โšก Features
20
+
21
+ - ๐Ÿ”™ Time-based dependency resolution
22
+ - ๐Ÿ“ฆ Works with all dependencies (including sub-dependencies)
23
+ - ๐Ÿ›ก๏ธ Reduces exposure to recent supply chain attacks
24
+ - ๐Ÿ” Verify installed packages against a date
25
+ - ๐Ÿงน Reset project state easily
26
+
27
+ ## ๐Ÿ“ฆ Installation
28
+
29
+ ```bash
30
+ npm install -g npm-time-machine-cli
31
+ ```
32
+
33
+ Or use with `npx`:
34
+
35
+ ```bash
36
+ npx npm-time-machine-cli <command>
37
+ ```
38
+
39
+ ## ๐ŸŽฏ Usage
40
+
41
+ ### 1๏ธโƒฃ Set Target Date
42
+
43
+ First, specify the date you want to freeze dependencies to:
44
+
45
+ ```bash
46
+ ntm set 2024-01-15
47
+ ```
48
+
49
+ This saves your target date to `.npm-time-machine/config.json`.
50
+
51
+ ### 2๏ธโƒฃ Install Dependencies
52
+
53
+ Install packages using the frozen date:
54
+
55
+ **Install all dependencies from `package.json`:**
56
+ ```bash
57
+ ntm install
58
+ ```
59
+
60
+ **Install specific packages:**
61
+ ```bash
62
+ ntm install express lodash
63
+ ```
64
+
65
+ Only versions published **before or on** the target date will be installed.
66
+
67
+ **Options:**
68
+ - `--fallback` - If no version exists before the date, use the oldest available version
69
+ ```bash
70
+ ntm install --fallback
71
+ ```
72
+ - `--allow-prerelease` - Include pre-release versions (e.g., alpha, beta, rc) in version resolution
73
+ ```bash
74
+ ntm install --allow-prerelease
75
+ ```
76
+
77
+ ### 3๏ธโƒฃ Verify Packages
78
+
79
+ Check if your `package-lock.json` matches the target date:
80
+
81
+ ```bash
82
+ ntm verify
83
+ ```
84
+
85
+ This will warn you about any packages installed **after** your target date.
86
+
87
+ **Override date temporarily:**
88
+ ```bash
89
+ ntm verify 2023-06-01
90
+ ```
91
+
92
+ ### 4๏ธโƒฃ Reset Configuration
93
+
94
+ Remove ntm configuration:
95
+
96
+ ```bash
97
+ ntm reset
98
+ ```
99
+
100
+ ## ๐Ÿ“š Examples
101
+
102
+ ### Scenario 1: Install dependencies from 2 years ago
103
+
104
+ ```bash
105
+ ntm set 2024-01-01
106
+ ntm install
107
+ # Your project now has all dependencies as they existed on Jan 1, 2024
108
+ ```
109
+
110
+ ### Scenario 2: Use fallback for legacy packages
111
+
112
+ ```bash
113
+ ntm set 2020-05-15
114
+ ntm install --fallback
115
+ # If a package didn't exist by May 2020, uses its oldest version
116
+ ```
117
+
118
+ ### Scenario 3: Verify a historic lock file
119
+
120
+ ```bash
121
+ ntm verify 2023-12-31
122
+ # Checks if package-lock.json complies with Dec 31, 2023 timeline
123
+ ```
124
+
125
+ ## ๐Ÿ”ง How It Works
126
+
127
+ 1. **Proxy Server** - Starts a local npm registry proxy on a random port
128
+ 2. **Version Filtering** - Intercepts npm requests and filters available versions by publish date
129
+ 3. **Registry Redirect** - npm CLI uses the proxy instead of the real registry
130
+ 4. **Caching** - Responses are cached to reduce registry calls
131
+ 5. **Cleanup** - Proxy automatically closes after installation
132
+
133
+ ### Strict vs Fallback Mode
134
+
135
+ - **Strict (default)** - Fails if no version exists before the target date
136
+ - **Fallback** - Uses the oldest available version as a last resort
137
+
138
+ ## โš ๏ธ Important Notes
139
+
140
+ - **Requires Node 18+** - Uses ES modules
141
+ - **Local Proxy** - Creates a temporary local server (doesn't modify global npm config)
142
+ - **Package Lock** - Works best with existing `package-lock.json` (or `npm-shrinkwrap.json`)
143
+ - **Offline** - Still requires internet to fetch package metadata
144
+ - **Transitive Dependencies** - Automatically handles sub-dependencies
145
+
146
+ ## ๐Ÿ› Troubleshooting
147
+
148
+ ### "No config found. Run 'ntm set <date>' first"
149
+ You haven't set a target date yet. Run:
150
+ ```bash
151
+ ntm set YYYY-MM-DD
152
+ ```
153
+
154
+ ### "No versions available before selected date"
155
+ The package didn't exist on that date. Use:
156
+ ```bash
157
+ ntm install --fallback
158
+ ```
159
+
160
+ ### "npm install failed"
161
+ The proxy closed unexpectedly. Check:
162
+ - Internet connection
163
+ - npm is properly installed
164
+ - No processes hogging ports
165
+
166
+ ### Port-related errors
167
+ The proxy randomly selects ports. If you get port errors, try againโ€”it should work on retry.
168
+
169
+ ## ๐Ÿ“‹ Commands Reference
170
+
171
+ | Command | Purpose |
172
+ |---------|---------|
173
+ | `ntm set <date>` | Set target date (YYYY-MM-DD format) |
174
+ | `ntm install [packages...]` | Install with frozen timeline |
175
+ | `ntm install --fallback` | Install with fallback mode enabled |
176
+ | `ntm install --allow-prerelease` | Install including pre-release versions |
177
+ | `ntm verify [date]` | Verify packages match a date |
178
+ | `ntm reset` | Remove ntm configuration |
179
+
180
+ ## ๐Ÿ›ก๏ธ Security Considerations
181
+
182
+ - **Supply Chain Protection**: Lock dependencies to a known-safe date before attacks occurred
183
+ - **Audit Checking**: Always run `npm audit` after installation
184
+ - **Trust Verification**: Verify publication dates match expectations
185
+ - **Locked Dependencies**: Use `npm ci` instead of `npm install` in CI/CD
186
+
187
+ ## ๐Ÿ‘ค Author
188
+
189
+ Marco Lo Pinto
190
+
191
+ ## ๐Ÿค Contributing
192
+
193
+ Contributions welcome! Feel free to open issues or submit pull requests on GitHub.
194
+
195
+
196
+ ## โšก Quick Start
197
+
198
+ ```bash
199
+ # 1. Install globally
200
+ npm install -g npm-time-machine-cli
201
+
202
+ # 2. Set a date in your project
203
+ cd your-project
204
+ ntm set 2024-06-01
205
+
206
+ # 3. Install dependencies
207
+ ntm install
208
+
209
+ # 4. Verify everything is correct
210
+ ntm verify
211
+ ```
212
+
213
+ You now have a reproducible, time-locked dependency tree! ๐ŸŽ‰
package/bin/ntm.js ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import { createRequire } from "module";
5
+ import { saveConfig, loadConfig } from "../src/config.js";
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const { version } = require("../package.json");
9
+ const program = new Command();
10
+
11
+ program
12
+ .name("ntm")
13
+ .description("npm time machine")
14
+ .version(version);
15
+
16
+ // SET
17
+ program
18
+ .command("set")
19
+ .argument("<date>", "Target date (YYYY-MM-DD)")
20
+ .description("Set the target date for dependency resolution")
21
+ .action((date) => {
22
+ const targetDate = new Date(date);
23
+
24
+ if (isNaN(targetDate)) {
25
+ console.error("โŒ Invalid date");
26
+ process.exit(1);
27
+ }
28
+
29
+ saveConfig({ date });
30
+ console.log(`โœ” Date set to ${date}`);
31
+ });
32
+
33
+ // INSTALL
34
+ program
35
+ .command("install")
36
+ .argument("[packages...]", "Packages to install")
37
+ .option("--fallback", "Allow fallback to oldest version if none match date")
38
+ .option("--allow-prerelease", "Include pre-release versions in version resolution")
39
+ .description("Install dependencies using frozen time")
40
+ .action(async (packages = [], options) => {
41
+ try {
42
+ const { startProxy } = await import("../src/proxy.js");
43
+ const { runInstall } = await import("../src/installer.js");
44
+
45
+ const { date } = loadConfig();
46
+ const targetDate = new Date(date);
47
+
48
+ console.log(`โณ Installing with frozen date ${date}`);
49
+ console.log(`โš™๏ธ Mode: ${options.fallback ? "fallback" : "strict"}${options.allowPrerelease ? ", prerelease enabled" : ""}`);
50
+
51
+ const proxy = await startProxy(targetDate, {
52
+ allowFallback: options.fallback || false,
53
+ allowPrerelease: options.allowPrerelease || false
54
+ });
55
+
56
+ try {
57
+ const args = ["install"];
58
+
59
+ if (packages.length > 0) {
60
+ args.push(...packages);
61
+ }
62
+
63
+ await runInstall(proxy.port, args);
64
+
65
+ } finally {
66
+ proxy.close();
67
+ }
68
+
69
+ } catch (err) {
70
+ console.error(`โŒ ${err.message}`);
71
+ process.exit(1);
72
+ }
73
+ });
74
+
75
+ // VERIFY
76
+ program
77
+ .command("verify")
78
+ .argument("[date]", "Optional date override")
79
+ .description("Verify installed dependencies against a date")
80
+ .action(async (dateArg) => {
81
+ try {
82
+ const { verifyProject } = await import("../src/verify.js");
83
+
84
+ let date;
85
+
86
+ if (dateArg) {
87
+ date = new Date(dateArg);
88
+ } else {
89
+ const config = loadConfig();
90
+ date = new Date(config.date);
91
+ }
92
+
93
+ if (isNaN(date)) {
94
+ console.error("โŒ Invalid date");
95
+ process.exit(1);
96
+ }
97
+
98
+ await verifyProject(date);
99
+
100
+ } catch (err) {
101
+ console.error(`โŒ ${err.message}`);
102
+ process.exit(1);
103
+ }
104
+ });
105
+
106
+ // RESET
107
+ program
108
+ .command("reset")
109
+ .description("Remove ntm configuration")
110
+ .action(async () => {
111
+ const { resetProject } = await import("../src/reset.js");
112
+ await resetProject();
113
+ });
114
+
115
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "npm-time-machine-cli",
3
+ "version": "1.0.0",
4
+ "description": "Install npm dependencies as they existed at a given date",
5
+ "type": "module",
6
+ "bin": {
7
+ "ntm": "bin/ntm.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test tests/**/*.test.js"
11
+ },
12
+ "author": "Marco Lo Pinto",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/MarcoLoPinto/npm-time-machine-cli.git"
16
+ },
17
+ "homepage": "https://github.com/MarcoLoPinto/npm-time-machine-cli#readme",
18
+ "license": "MIT",
19
+ "dependencies": {
20
+ "commander": "^11.0.0",
21
+ "express": "^4.18.2",
22
+ "node-fetch": "^3.3.2",
23
+ "semver": "^7.6.0"
24
+ },
25
+ "preferGlobal": true,
26
+ "files": [
27
+ "bin",
28
+ "src",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "keywords": [
36
+ "npm",
37
+ "dependencies",
38
+ "time",
39
+ "reproducibility",
40
+ "deterministic",
41
+ "lockfile",
42
+ "versioning",
43
+ "supply-chain",
44
+ "security",
45
+ "cli",
46
+ "dev-tools"
47
+ ]
48
+ }
package/src/cache.js ADDED
@@ -0,0 +1,11 @@
1
+ // Cache module
2
+
3
+ const cache = new Map();
4
+
5
+ export function getCache(key) {
6
+ return cache.get(key);
7
+ }
8
+
9
+ export function setCache(key, value) {
10
+ cache.set(key, value);
11
+ }
package/src/config.js ADDED
@@ -0,0 +1,21 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ const dir = path.join(process.cwd(), ".npm-time-machine");
5
+ const file = path.join(dir, "config.json");
6
+
7
+ export function saveConfig(config) {
8
+ if (!fs.existsSync(dir)) {
9
+ fs.mkdirSync(dir);
10
+ }
11
+
12
+ fs.writeFileSync(file, JSON.stringify(config, null, 2));
13
+ }
14
+
15
+ export function loadConfig() {
16
+ if (!fs.existsSync(file)) {
17
+ throw new Error("No ntm config found. Run 'ntm set <date>' first.");
18
+ }
19
+
20
+ return JSON.parse(fs.readFileSync(file));
21
+ }
@@ -0,0 +1,25 @@
1
+ import { spawn } from "child_process";
2
+
3
+ export function getNpmCommand() {
4
+ return process.platform === "win32" ? "npm.cmd" : "npm";
5
+ }
6
+
7
+ export function runInstall(port, args = ["install"]) {
8
+ return new Promise((resolve, reject) => {
9
+ const command = getNpmCommand();
10
+ const child = spawn(
11
+ command,
12
+ [...args, "--registry", `http://localhost:${port}`],
13
+ { stdio: "inherit" }
14
+ );
15
+
16
+ child.on("error", (error) => {
17
+ reject(error);
18
+ });
19
+
20
+ child.on("close", (code) => {
21
+ if (code === 0) resolve();
22
+ else reject(new Error("npm install failed"));
23
+ });
24
+ });
25
+ }
package/src/proxy.js ADDED
@@ -0,0 +1,108 @@
1
+ import express from "express";
2
+ import fetch from "node-fetch";
3
+ import { getCache, setCache } from "./cache.js";
4
+ import { filterVersionsByDate, updateDistTags } from "./version-filter.js";
5
+ import { pipeline } from "stream";
6
+ import { promisify } from "util";
7
+
8
+ const streamPipeline = promisify(pipeline);
9
+
10
+ export async function startProxy(targetDate, options = {}) {
11
+ const { allowFallback = false, allowPrerelease = false } = options;
12
+
13
+ const app = express();
14
+
15
+ app.get("/*", async (req, res) => {
16
+ try {
17
+ const url = `https://registry.npmjs.org${req.url}`;
18
+
19
+ // Check if this is a tarball request
20
+ if (req.url.endsWith(".tgz")) {
21
+ const response = await fetch(url);
22
+
23
+ if (!response.ok) {
24
+ return res.status(response.status).send("Upstream error");
25
+ }
26
+
27
+ res.status(response.status);
28
+
29
+ const excludedHeaders = ["content-encoding", "transfer-encoding"];
30
+
31
+ response.headers.forEach((value, key) => {
32
+ if (!excludedHeaders.includes(key.toLowerCase())) {
33
+ res.setHeader(key, value);
34
+ }
35
+ });
36
+
37
+ try {
38
+ await streamPipeline(response.body, res);
39
+ } catch (err) {
40
+ console.error("Stream error:", err.message);
41
+ }
42
+
43
+ return;
44
+ }
45
+
46
+ // Metadata request (JSON)
47
+ const cached = getCache(req.url);
48
+ if (cached) {
49
+ return res.json(cached);
50
+ }
51
+
52
+ const response = await fetch(url);
53
+
54
+ if (!response.ok) {
55
+ return res.status(response.status).send("Upstream error");
56
+ }
57
+
58
+ const data = await response.json();
59
+
60
+ // filter versions
61
+ const filterResult = filterVersionsByDate(data, targetDate, {
62
+ allowFallback,
63
+ allowPrerelease
64
+ });
65
+
66
+ if (filterResult === null) {
67
+ console.error(
68
+ `โŒ No versions available before ${targetDate.toISOString()} for ${req.url}`
69
+ );
70
+ return res
71
+ .status(404)
72
+ .send("No valid versions before selected date");
73
+ }
74
+
75
+ if (filterResult.fallback) {
76
+ console.warn(`โš  No versions before date for ${req.url}`);
77
+ console.warn(`โš  Fallback to oldest version: ${filterResult.fallbackVersion}`);
78
+ }
79
+
80
+ // Update data with filtered versions
81
+ data.versions = filterResult.versions;
82
+
83
+ // Update dist-tags
84
+ updateDistTags(data, filterResult);
85
+
86
+ // set cache
87
+ setCache(req.url, data);
88
+
89
+ res.json(data);
90
+
91
+ } catch (err) {
92
+ console.error(`โŒ Proxy error for ${req.url}:`, err.message);
93
+
94
+ return res.status(502).send("Bad gateway");
95
+ }
96
+ });
97
+
98
+ const server = app.listen(0);
99
+ const port = server.address().port;
100
+
101
+ console.log(`โณ NTM proxy running on http://localhost:${port}`);
102
+ console.log(`โš™๏ธ Mode: ${allowFallback ? "fallback enabled" : "strict"}${allowPrerelease ? ", prerelease enabled" : ""}`);
103
+
104
+ return {
105
+ port,
106
+ close: () => server.close()
107
+ };
108
+ }
package/src/reset.js ADDED
@@ -0,0 +1,13 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export async function resetProject() {
5
+ const dir = path.join(process.cwd(), ".npm-time-machine");
6
+
7
+ if (fs.existsSync(dir)) {
8
+ fs.rmSync(dir, { recursive: true, force: true });
9
+ console.log("โœ” Removed ntm configuration");
10
+ } else {
11
+ console.log("Nothing to reset");
12
+ }
13
+ }
package/src/verify.js ADDED
@@ -0,0 +1,123 @@
1
+ import fs from "fs";
2
+ import fetch from "node-fetch";
3
+ import semver from "semver";
4
+
5
+ export function extractPackageName(packagePath) {
6
+ if (!packagePath || packagePath === "") {
7
+ return null;
8
+ }
9
+
10
+ const marker = "node_modules/";
11
+ const markerIndex = packagePath.lastIndexOf(marker);
12
+
13
+ if (markerIndex === -1) {
14
+ return packagePath;
15
+ }
16
+
17
+ return packagePath.slice(markerIndex + marker.length) || null;
18
+ }
19
+
20
+ function collectFromPackageEntries(packages) {
21
+ const collected = [];
22
+
23
+ for (const [packagePath, info] of Object.entries(packages)) {
24
+ if (!info?.version || !semver.valid(info.version)) continue;
25
+
26
+ const packageName = extractPackageName(packagePath);
27
+ if (!packageName) continue;
28
+
29
+ collected.push({ name: packageName, version: info.version });
30
+ }
31
+
32
+ return collected;
33
+ }
34
+
35
+ function collectFromDependencyTree(dependencies, collected = []) {
36
+ if (!dependencies) {
37
+ return collected;
38
+ }
39
+
40
+ for (const [name, info] of Object.entries(dependencies)) {
41
+ if (info?.version && semver.valid(info.version)) {
42
+ collected.push({ name, version: info.version });
43
+ }
44
+
45
+ collectFromDependencyTree(info?.dependencies, collected);
46
+ }
47
+
48
+ return collected;
49
+ }
50
+
51
+ export function collectLockfilePackages(lock) {
52
+ if (lock.packages && typeof lock.packages === "object") {
53
+ return collectFromPackageEntries(lock.packages);
54
+ }
55
+
56
+ if (lock.dependencies && typeof lock.dependencies === "object") {
57
+ return collectFromDependencyTree(lock.dependencies);
58
+ }
59
+
60
+ return [];
61
+ }
62
+
63
+ export async function verifyProject(date) {
64
+ if (!fs.existsSync("package-lock.json")) {
65
+ throw new Error("No package-lock.json found");
66
+ }
67
+
68
+ const lock = JSON.parse(fs.readFileSync("package-lock.json", "utf-8"));
69
+ const packages = collectLockfilePackages(lock);
70
+
71
+ let issues = 0;
72
+ const metadataCache = new Map();
73
+ const lookupFailures = [];
74
+ const seen = new Set();
75
+
76
+ for (const pkg of packages) {
77
+ const key = `${pkg.name}@${pkg.version}`;
78
+ if (seen.has(key)) continue;
79
+ seen.add(key);
80
+
81
+ let data = metadataCache.get(pkg.name);
82
+
83
+ try {
84
+ if (!data) {
85
+ const res = await fetch(`https://registry.npmjs.org/${pkg.name}`);
86
+ if (!res.ok) {
87
+ throw new Error(`Registry responded with ${res.status}`);
88
+ }
89
+
90
+ data = await res.json();
91
+ metadataCache.set(pkg.name, data);
92
+ }
93
+
94
+ const publishTime = data.time?.[pkg.version];
95
+
96
+ if (!publishTime) {
97
+ lookupFailures.push(`${pkg.name}@${pkg.version}`);
98
+ continue;
99
+ }
100
+
101
+ if (new Date(publishTime) > date) {
102
+ console.log(`โŒ ${pkg.name}@${pkg.version} โ†’ ${publishTime}`);
103
+ issues++;
104
+ }
105
+
106
+ } catch (error) {
107
+ lookupFailures.push(`${pkg.name}@${pkg.version} (${error.message})`);
108
+ }
109
+ }
110
+
111
+ if (lookupFailures.length > 0) {
112
+ const preview = lookupFailures.slice(0, 5).join(", ");
113
+ throw new Error(
114
+ `Failed to verify ${lookupFailures.length} package(s): ${preview}`
115
+ );
116
+ }
117
+
118
+ if (issues === 0) {
119
+ console.log("โœ” All dependencies are within the selected date");
120
+ } else {
121
+ console.log(`โš  Found ${issues} issues`);
122
+ }
123
+ }
@@ -0,0 +1,77 @@
1
+ import semver from "semver";
2
+
3
+ /**
4
+ * Filters package versions by a target date
5
+ * @param {Object} packageData - NPM registry response with versions and time metadata
6
+ * @param {Date} targetDate - Cutoff date for version filtering
7
+ * @param {Object} options - Filter options
8
+ * @param {boolean} options.allowFallback - If true, fallback to oldest version when none match
9
+ * @param {boolean} options.allowPrerelease - If true, include pre-release versions
10
+ * @returns {Object} Filtered versions object or null if no versions match
11
+ */
12
+ export function filterVersionsByDate(packageData, targetDate, options = {}) {
13
+ const { allowFallback = false, allowPrerelease = false } = options;
14
+ const { versions, time } = packageData;
15
+
16
+ if (!versions || !time) {
17
+ return null;
18
+ }
19
+
20
+ const filtered = {};
21
+
22
+ // Filter versions by publish date
23
+ for (const [version, publishTime] of Object.entries(time)) {
24
+ // Skip metadata entries
25
+ if (["created", "modified"].includes(version)) continue;
26
+
27
+ // Check if version exists and meets criteria
28
+ if (
29
+ new Date(publishTime) <= targetDate &&
30
+ versions[version] &&
31
+ (allowPrerelease || !semver.prerelease(version))
32
+ ) {
33
+ filtered[version] = versions[version];
34
+ }
35
+ }
36
+
37
+ // Handle no versions before date
38
+ if (Object.keys(filtered).length === 0) {
39
+ if (allowFallback) {
40
+ const allVersions = Object.keys(versions || {});
41
+ const oldest = allVersions.sort(semver.compare)[0];
42
+
43
+ if (oldest) {
44
+ return {
45
+ versions: { [oldest]: versions[oldest] },
46
+ fallback: true,
47
+ fallbackVersion: oldest
48
+ };
49
+ }
50
+ return null;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ return { versions: filtered, fallback: false };
56
+ }
57
+
58
+ /**
59
+ * Updates dist-tags in package metadata based on filtered versions
60
+ * @param {Object} packageData - NPM registry response
61
+ * @param {Object} filteredResult - Result from filterVersionsByDate
62
+ * @returns {Object} Updated package data
63
+ */
64
+ export function updateDistTags(packageData, filteredResult) {
65
+ if (!filteredResult || !filteredResult.versions) {
66
+ return packageData;
67
+ }
68
+
69
+ const versions = Object.keys(filteredResult.versions);
70
+ if (versions.length > 0) {
71
+ const latest = versions.sort(semver.rcompare)[0];
72
+ packageData["dist-tags"] = packageData["dist-tags"] || {};
73
+ packageData["dist-tags"].latest = latest;
74
+ }
75
+
76
+ return packageData;
77
+ }