trust-npm 0.1.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 trust-npm contributors
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,154 @@
1
+ # trust-npm
2
+
3
+ `trust-npm` is a production-oriented npm wrapper that blocks unknown dependencies by default and requires explicit approvals.
4
+
5
+ It uses your existing `package-lock.json` as a trust baseline and intercepts `npm install` before allowing package installation.
6
+
7
+ ## Features
8
+
9
+ - Default-deny installs for unknown packages
10
+ - Trust baseline bootstrapped from `package-lock.json`
11
+ - Risk scoring for unknown packages (age, downloads, repo, naming pattern)
12
+ - Explicit approvals via `trust-npm approve`
13
+ - Cross-platform npm passthrough using `child_process.spawn`
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install
19
+ npm run build
20
+ npm link
21
+ ```
22
+
23
+ Then use:
24
+
25
+ ```bash
26
+ trust-npm --help
27
+ ```
28
+
29
+ ## One-command project setup
30
+
31
+ If you want npm commands to go through `trust-npm` automatically:
32
+
33
+ ```bash
34
+ trust-npm init
35
+ ```
36
+
37
+ This will:
38
+
39
+ - create/update `.trust-npm.json` from `package-lock.json`
40
+ - persist alias `npm -> trust-npm` in your shell profile
41
+
42
+ You can force shell target:
43
+
44
+ ```bash
45
+ trust-npm init --shell powershell
46
+ trust-npm init --shell bash
47
+ trust-npm init --shell zsh
48
+ ```
49
+
50
+ ## Commands
51
+
52
+ ### `trust-npm init`
53
+
54
+ Initializes `.trust-npm.json` using lockfile dependencies and sets a persistent shell alias (`npm -> trust-npm`).
55
+
56
+ ```bash
57
+ trust-npm init
58
+ ```
59
+
60
+ Skip automatic alias setup:
61
+
62
+ ```bash
63
+ trust-npm init --skip-alias
64
+ ```
65
+
66
+ Optional alias instructions:
67
+
68
+ ```bash
69
+ trust-npm init --print-shell-alias
70
+ ```
71
+
72
+ ### `trust-npm install <package...>`
73
+
74
+ Intercepts install requests and blocks unknown packages until explicitly approved.
75
+
76
+ ```bash
77
+ trust-npm install lodash
78
+ trust-npm install react -D
79
+ ```
80
+
81
+ If blocked:
82
+
83
+ ```text
84
+ ❌ BLOCKED (high risk): fast-ultra-db-kit
85
+ Reason:
86
+ - Published 2 day(s) ago
87
+ - 12 weekly downloads
88
+ - Missing repository field
89
+ - Risk score: 90/50
90
+
91
+ Run:
92
+ trust-npm approve fast-ultra-db-kit
93
+ ```
94
+
95
+ ### `trust-npm approve <package...>`
96
+
97
+ Adds packages to trusted store:
98
+
99
+ ```bash
100
+ trust-npm approve lodash
101
+ trust-npm approve react react-dom
102
+ ```
103
+
104
+ ### `trust-npm status`
105
+
106
+ Shows high-level trust status for the current project:
107
+
108
+ ```bash
109
+ trust-npm status
110
+ ```
111
+
112
+ ## Trust Store Format
113
+
114
+ `.trust-npm.json`:
115
+
116
+ ```json
117
+ {
118
+ "version": 1,
119
+ "createdAt": "2026-04-04T10:00:00.000Z",
120
+ "updatedAt": "2026-04-04T10:05:00.000Z",
121
+ "trustedPackages": {
122
+ "lodash": {
123
+ "source": "lockfile",
124
+ "approvedAt": "2026-04-04T10:00:00.000Z"
125
+ },
126
+ "fast-ultra-db-kit": {
127
+ "source": "manual",
128
+ "approvedAt": "2026-04-04T10:05:00.000Z"
129
+ }
130
+ },
131
+ "riskThreshold": 50
132
+ }
133
+ ```
134
+
135
+ ## Security Model
136
+
137
+ - Unknown package install attempts are blocked.
138
+ - High-risk unknown packages are called out with detailed risk factors.
139
+ - Even low-risk unknown packages are still blocked until approved.
140
+ - No silent approvals.
141
+
142
+ ## Notes
143
+
144
+ - `trust-npm install` forwards to real `npm install` only when checks pass.
145
+ - If you run plain `npm install`, trust checks are bypassed. Use shell aliasing if desired.
146
+ - Network access to npm registry is required for risk analysis on unknown packages.
147
+
148
+ ## Future Extensions
149
+
150
+ The structure is prepared for:
151
+
152
+ - additional package manager adapters (for example, `pip`)
153
+ - runtime import/require protection
154
+ - CI/CD enforcement mode
package/dist/cli.js ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const init_1 = require("./commands/init");
6
+ const install_1 = require("./commands/install");
7
+ const approve_1 = require("./commands/approve");
8
+ const status_1 = require("./commands/status");
9
+ const program = new commander_1.Command();
10
+ program
11
+ .name("trust-npm")
12
+ .description("Secure wrapper around npm install with trust baseline checks")
13
+ .version("0.1.0");
14
+ program
15
+ .command("init")
16
+ .description("Create .trust-npm.json and auto-alias npm -> trust-npm")
17
+ .option("--print-shell-alias", "Print alias instructions")
18
+ .option("--skip-alias", "Skip automatic shell alias setup")
19
+ .option("--shell <shell>", "Alias target shell (powershell|bash|zsh)")
20
+ .action(async (options) => {
21
+ await (0, init_1.runInit)(options);
22
+ });
23
+ program
24
+ .command("install [packages...]")
25
+ .description("Intercept npm install and block unknown/untrusted packages")
26
+ .allowUnknownOption(true)
27
+ .action(async () => {
28
+ const rawInstallArgs = getRawInstallArgs(process.argv);
29
+ await (0, install_1.runInstall)(rawInstallArgs);
30
+ });
31
+ program
32
+ .command("approve <packages...>")
33
+ .description("Approve one or more packages into .trust-npm.json")
34
+ .action(async (packages) => {
35
+ await (0, approve_1.runApprove)(packages);
36
+ });
37
+ program
38
+ .command("status")
39
+ .description("Show trusted vs untrusted dependency status")
40
+ .action(async () => {
41
+ await (0, status_1.runStatus)();
42
+ });
43
+ program.parseAsync(process.argv).catch((error) => {
44
+ if (error instanceof Error) {
45
+ console.error(`trust-npm error: ${error.message}`);
46
+ }
47
+ else {
48
+ console.error("trust-npm error: unknown failure");
49
+ }
50
+ process.exitCode = 1;
51
+ });
52
+ function getRawInstallArgs(argv) {
53
+ const installIndex = argv.findIndex((token) => token === "install");
54
+ if (installIndex < 0) {
55
+ return [];
56
+ }
57
+ return argv.slice(installIndex + 1);
58
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runApprove = runApprove;
4
+ const project_1 = require("../core/project");
5
+ const trustStore_1 = require("../core/trustStore");
6
+ async function runApprove(packages) {
7
+ if (packages.length === 0) {
8
+ throw new Error("Please provide at least one package to approve.");
9
+ }
10
+ const projectRoot = (0, project_1.findProjectRoot)();
11
+ const store = (0, trustStore_1.readTrustStore)(projectRoot) ?? (0, trustStore_1.createEmptyTrustStore)();
12
+ (0, trustStore_1.addTrustedPackages)(store, dedupe(packages), "manual");
13
+ (0, trustStore_1.writeTrustStore)(projectRoot, store);
14
+ console.log(`Approved package(s): ${dedupe(packages).join(", ")}`);
15
+ }
16
+ function dedupe(items) {
17
+ return [...new Set(items)];
18
+ }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runInit = runInit;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const project_1 = require("../core/project");
11
+ const lockfile_1 = require("../core/lockfile");
12
+ const trustStore_1 = require("../core/trustStore");
13
+ async function runInit(options = {}) {
14
+ const projectRoot = (0, project_1.findProjectRoot)();
15
+ const packageJsonPath = path_1.default.join(projectRoot, "package.json");
16
+ if (!fs_1.default.existsSync(packageJsonPath)) {
17
+ throw new Error("package.json not found. Run this inside an npm project.");
18
+ }
19
+ const trustedFromLockfile = (0, lockfile_1.extractTrustedFromLockfile)(projectRoot);
20
+ const existing = (0, trustStore_1.readTrustStore)(projectRoot);
21
+ const store = existing ?? (0, trustStore_1.createEmptyTrustStore)();
22
+ (0, trustStore_1.addTrustedPackages)(store, [...trustedFromLockfile], "lockfile");
23
+ (0, trustStore_1.writeTrustStore)(projectRoot, store);
24
+ const storePath = (0, trustStore_1.trustStorePath)(projectRoot);
25
+ console.log(`Initialized trust store: ${storePath}`);
26
+ console.log(`Trusted packages imported: ${trustedFromLockfile.size}`);
27
+ if (!options.skipAlias) {
28
+ const shell = detectShell(options.shell);
29
+ const profilePath = ensureAliasInProfile(shell);
30
+ console.log(`Alias configured for ${shell} in: ${profilePath}`);
31
+ console.log("Restart your terminal or reload profile for alias to take effect.");
32
+ }
33
+ if (options.printShellAlias) {
34
+ console.log("");
35
+ console.log("Optional shell alias:");
36
+ console.log(" alias npm='trust-npm'");
37
+ console.log("Windows PowerShell:");
38
+ console.log(" Set-Alias npm trust-npm");
39
+ }
40
+ }
41
+ function detectShell(requested) {
42
+ if (requested) {
43
+ return requested;
44
+ }
45
+ if (process.platform === "win32") {
46
+ return "powershell";
47
+ }
48
+ const envShell = process.env.SHELL ?? "";
49
+ if (envShell.includes("zsh")) {
50
+ return "zsh";
51
+ }
52
+ return "bash";
53
+ }
54
+ function ensureAliasInProfile(shell) {
55
+ if (shell === "powershell") {
56
+ return ensurePowerShellAlias();
57
+ }
58
+ return ensurePosixAlias(shell);
59
+ }
60
+ function ensurePowerShellAlias() {
61
+ const home = os_1.default.homedir();
62
+ const ps7Path = path_1.default.join(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1");
63
+ const legacyPath = path_1.default.join(home, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
64
+ const profilePath = fs_1.default.existsSync(ps7Path) ? ps7Path : legacyPath;
65
+ ensureFile(profilePath);
66
+ const startMarker = "# trust-npm alias start";
67
+ const endMarker = "# trust-npm alias end";
68
+ const block = `${startMarker}
69
+ Set-Alias npm trust-npm
70
+ ${endMarker}
71
+ `;
72
+ appendBlockIfMissing(profilePath, startMarker, block);
73
+ return profilePath;
74
+ }
75
+ function ensurePosixAlias(shell) {
76
+ const home = os_1.default.homedir();
77
+ const filename = shell === "zsh" ? ".zshrc" : ".bashrc";
78
+ const profilePath = path_1.default.join(home, filename);
79
+ ensureFile(profilePath);
80
+ const startMarker = "# trust-npm alias start";
81
+ const endMarker = "# trust-npm alias end";
82
+ const block = `${startMarker}
83
+ alias npm='trust-npm'
84
+ ${endMarker}
85
+ `;
86
+ appendBlockIfMissing(profilePath, startMarker, block);
87
+ return profilePath;
88
+ }
89
+ function ensureFile(filePath) {
90
+ const dir = path_1.default.dirname(filePath);
91
+ if (!fs_1.default.existsSync(dir)) {
92
+ fs_1.default.mkdirSync(dir, { recursive: true });
93
+ }
94
+ if (!fs_1.default.existsSync(filePath)) {
95
+ fs_1.default.writeFileSync(filePath, "", "utf8");
96
+ }
97
+ }
98
+ function appendBlockIfMissing(filePath, marker, block) {
99
+ const existing = fs_1.default.readFileSync(filePath, "utf8");
100
+ if (existing.includes(marker)) {
101
+ return;
102
+ }
103
+ const prefix = existing.endsWith("\n") || existing.length === 0 ? "" : "\n";
104
+ fs_1.default.writeFileSync(filePath, `${existing}${prefix}${block}`, "utf8");
105
+ }
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runInstall = runInstall;
7
+ const npm_package_arg_1 = __importDefault(require("npm-package-arg"));
8
+ const project_1 = require("../core/project");
9
+ const npm_1 = require("../core/npm");
10
+ const risk_1 = require("../core/risk");
11
+ const trustStore_1 = require("../core/trustStore");
12
+ async function runInstall(rawInstallArgs) {
13
+ const projectRoot = (0, project_1.findProjectRoot)();
14
+ const store = (0, trustStore_1.readTrustStore)(projectRoot);
15
+ if (!store) {
16
+ throw new Error("No .trust-npm.json found. Run `trust-npm init` first.");
17
+ }
18
+ const requestedPackages = extractRequestedPackages(rawInstallArgs);
19
+ if (requestedPackages.length > 0) {
20
+ const unknown = requestedPackages.filter((name) => !store.trustedPackages[name]);
21
+ if (unknown.length > 0) {
22
+ const analyses = await Promise.all(unknown.map((pkg) => (0, risk_1.analyzePackageRisk)(pkg, store.riskThreshold)));
23
+ printBlockedPackages(analyses);
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+ }
28
+ const exitCode = await (0, npm_1.runNpm)(["install", ...rawInstallArgs]);
29
+ process.exitCode = exitCode;
30
+ }
31
+ function extractRequestedPackages(args) {
32
+ const names = new Set();
33
+ for (const token of args) {
34
+ if (!token || token.startsWith("-")) {
35
+ continue;
36
+ }
37
+ try {
38
+ const parsed = (0, npm_package_arg_1.default)(token);
39
+ if (parsed.name) {
40
+ names.add(parsed.name);
41
+ }
42
+ }
43
+ catch {
44
+ continue;
45
+ }
46
+ }
47
+ return [...names];
48
+ }
49
+ function printBlockedPackages(analyses) {
50
+ for (const analysis of analyses) {
51
+ const header = analysis.isHighRisk ? "BLOCKED (high risk)" : "BLOCKED (untrusted package)";
52
+ console.error(`\n❌ ${header}: ${analysis.packageName}`);
53
+ console.error("Reason:");
54
+ if (analysis.reasons.length > 0) {
55
+ for (const reason of analysis.reasons) {
56
+ console.error(`- ${reason}`);
57
+ }
58
+ }
59
+ else {
60
+ console.error("- New package is not in trusted baseline");
61
+ }
62
+ console.error(`- Risk score: ${analysis.score}/${analysis.threshold}`);
63
+ console.error("");
64
+ console.error(`Run:\ntrust-npm approve ${analysis.packageName}`);
65
+ }
66
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runStatus = runStatus;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const project_1 = require("../core/project");
10
+ const trustStore_1 = require("../core/trustStore");
11
+ async function runStatus() {
12
+ const projectRoot = (0, project_1.findProjectRoot)();
13
+ const store = (0, trustStore_1.readTrustStore)(projectRoot);
14
+ if (!store) {
15
+ console.log("No trust store found. Run `trust-npm init` first.");
16
+ return;
17
+ }
18
+ const pkg = readPackageJson(projectRoot);
19
+ const declared = new Set([
20
+ ...Object.keys(pkg.dependencies ?? {}),
21
+ ...Object.keys(pkg.devDependencies ?? {}),
22
+ ...Object.keys(pkg.peerDependencies ?? {}),
23
+ ...Object.keys(pkg.optionalDependencies ?? {})
24
+ ]);
25
+ const trustedDeclared = [];
26
+ const untrustedDeclared = [];
27
+ for (const name of declared) {
28
+ if (store.trustedPackages[name]) {
29
+ trustedDeclared.push(name);
30
+ }
31
+ else {
32
+ untrustedDeclared.push(name);
33
+ }
34
+ }
35
+ console.log(`Trust store path: ${path_1.default.join(projectRoot, ".trust-npm.json")}`);
36
+ console.log(`Total trusted packages in store: ${Object.keys(store.trustedPackages).length}`);
37
+ console.log(`Declared dependencies: ${declared.size}`);
38
+ console.log(`Trusted declared dependencies: ${trustedDeclared.length}`);
39
+ console.log(`Untrusted declared dependencies: ${untrustedDeclared.length}`);
40
+ if (untrustedDeclared.length > 0) {
41
+ console.log("");
42
+ console.log("Untrusted declared dependencies:");
43
+ for (const dep of untrustedDeclared.sort()) {
44
+ console.log(`- ${dep}`);
45
+ }
46
+ }
47
+ }
48
+ function readPackageJson(projectRoot) {
49
+ const filePath = path_1.default.join(projectRoot, "package.json");
50
+ if (!fs_1.default.existsSync(filePath)) {
51
+ throw new Error("package.json not found.");
52
+ }
53
+ return JSON.parse(fs_1.default.readFileSync(filePath, "utf8"));
54
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getLockfilePath = getLockfilePath;
7
+ exports.readLockfile = readLockfile;
8
+ exports.extractTrustedFromLockfile = extractTrustedFromLockfile;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ function getLockfilePath(projectRoot) {
12
+ return path_1.default.join(projectRoot, "package-lock.json");
13
+ }
14
+ function readLockfile(projectRoot) {
15
+ const lockfilePath = getLockfilePath(projectRoot);
16
+ if (!fs_1.default.existsSync(lockfilePath)) {
17
+ throw new Error(`Missing package-lock.json at ${lockfilePath}`);
18
+ }
19
+ const raw = fs_1.default.readFileSync(lockfilePath, "utf8");
20
+ return JSON.parse(raw);
21
+ }
22
+ function extractTrustedFromLockfile(projectRoot) {
23
+ const lockfile = readLockfile(projectRoot);
24
+ const trusted = new Set();
25
+ if (lockfile.packages) {
26
+ for (const key of Object.keys(lockfile.packages)) {
27
+ if (!key.startsWith("node_modules/")) {
28
+ continue;
29
+ }
30
+ const name = key.slice("node_modules/".length);
31
+ if (name) {
32
+ trusted.add(name);
33
+ }
34
+ }
35
+ }
36
+ if (lockfile.dependencies) {
37
+ walkDependencyTree(lockfile.dependencies, trusted);
38
+ }
39
+ return trusted;
40
+ }
41
+ function walkDependencyTree(deps, trusted) {
42
+ for (const [name, dep] of Object.entries(deps)) {
43
+ trusted.add(name);
44
+ if (dep.dependencies) {
45
+ walkDependencyTree(dep.dependencies, trusted);
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runNpm = runNpm;
4
+ const child_process_1 = require("child_process");
5
+ async function runNpm(args) {
6
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
7
+ return new Promise((resolve, reject) => {
8
+ const child = (0, child_process_1.spawn)(npmCommand, args, {
9
+ stdio: "inherit",
10
+ shell: false
11
+ });
12
+ child.on("error", (error) => reject(error));
13
+ child.on("close", (code) => resolve(code ?? 1));
14
+ });
15
+ }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.findProjectRoot = findProjectRoot;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ function findProjectRoot(startDir = process.cwd()) {
10
+ let current = path_1.default.resolve(startDir);
11
+ const root = path_1.default.parse(current).root;
12
+ while (true) {
13
+ const packageJsonPath = path_1.default.join(current, "package.json");
14
+ if (fs_1.default.existsSync(packageJsonPath)) {
15
+ return current;
16
+ }
17
+ if (current === root) {
18
+ throw new Error("Could not find project root (no package.json found).");
19
+ }
20
+ current = path_1.default.dirname(current);
21
+ }
22
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.fetchPackageMetadata = fetchPackageMetadata;
7
+ exports.fetchWeeklyDownloads = fetchWeeklyDownloads;
8
+ const axios_1 = __importDefault(require("axios"));
9
+ async function fetchPackageMetadata(packageName) {
10
+ const encoded = encodeURIComponent(packageName);
11
+ const url = `https://registry.npmjs.org/${encoded}`;
12
+ const response = await axios_1.default.get(url, { timeout: 5000 });
13
+ const data = response.data;
14
+ return {
15
+ name: data.name ?? packageName,
16
+ createdAt: data.time?.created,
17
+ repository: data.repository
18
+ };
19
+ }
20
+ async function fetchWeeklyDownloads(packageName) {
21
+ const encoded = encodeURIComponent(packageName);
22
+ const url = `https://api.npmjs.org/downloads/point/last-week/${encoded}`;
23
+ const response = await axios_1.default.get(url, { timeout: 5000 });
24
+ const data = response.data;
25
+ return {
26
+ downloads: typeof data.downloads === "number" ? data.downloads : 0
27
+ };
28
+ }
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzePackageRisk = analyzePackageRisk;
4
+ const registry_1 = require("./registry");
5
+ const NEW_PACKAGE_DAYS_THRESHOLD = 7;
6
+ const LOW_DOWNLOADS_THRESHOLD = 100;
7
+ async function analyzePackageRisk(packageName, threshold) {
8
+ let score = 0;
9
+ const reasons = [];
10
+ const metadata = {};
11
+ const [pkg, downloads] = await Promise.all([
12
+ (0, registry_1.fetchPackageMetadata)(packageName),
13
+ (0, registry_1.fetchWeeklyDownloads)(packageName)
14
+ ]);
15
+ const ageDays = getPackageAgeDays(pkg.createdAt);
16
+ metadata.publishedAt = pkg.createdAt;
17
+ metadata.ageDays = ageDays;
18
+ metadata.weeklyDownloads = downloads.downloads;
19
+ if (typeof ageDays === "number" && ageDays < NEW_PACKAGE_DAYS_THRESHOLD) {
20
+ score += 40;
21
+ reasons.push(`Published ${ageDays} day(s) ago`);
22
+ }
23
+ if (downloads.downloads < LOW_DOWNLOADS_THRESHOLD) {
24
+ score += 30;
25
+ reasons.push(`${downloads.downloads} weekly downloads`);
26
+ }
27
+ const hasRepo = hasRepository(pkg.repository);
28
+ metadata.hasRepository = hasRepo;
29
+ if (!hasRepo) {
30
+ score += 20;
31
+ reasons.push("Missing repository field");
32
+ }
33
+ const suspiciousName = hasSuspiciousName(packageName);
34
+ metadata.suspiciousName = suspiciousName;
35
+ if (suspiciousName) {
36
+ score += 10;
37
+ reasons.push("Suspicious naming pattern");
38
+ }
39
+ return {
40
+ packageName,
41
+ score,
42
+ threshold,
43
+ isHighRisk: score > threshold,
44
+ reasons,
45
+ metadata
46
+ };
47
+ }
48
+ function getPackageAgeDays(createdAt) {
49
+ if (!createdAt) {
50
+ return undefined;
51
+ }
52
+ const created = new Date(createdAt);
53
+ if (Number.isNaN(created.valueOf())) {
54
+ return undefined;
55
+ }
56
+ const diffMs = Date.now() - created.getTime();
57
+ return Math.max(0, Math.floor(diffMs / (1000 * 60 * 60 * 24)));
58
+ }
59
+ function hasRepository(repository) {
60
+ if (!repository) {
61
+ return false;
62
+ }
63
+ if (typeof repository === "string") {
64
+ return repository.trim().length > 0;
65
+ }
66
+ if (typeof repository === "object" && repository !== null) {
67
+ const maybeUrl = repository.url;
68
+ return typeof maybeUrl === "string" && maybeUrl.trim().length > 0;
69
+ }
70
+ return false;
71
+ }
72
+ function hasSuspiciousName(name) {
73
+ const normalized = name.toLowerCase();
74
+ const tokens = normalized.split(/[-_.]/g).filter(Boolean);
75
+ if (tokens.length >= 4) {
76
+ return true;
77
+ }
78
+ const suspiciousTokens = ["fast", "ultra", "super", "pro", "best", "safe"];
79
+ let suspiciousHits = 0;
80
+ for (const token of tokens) {
81
+ if (suspiciousTokens.includes(token)) {
82
+ suspiciousHits += 1;
83
+ }
84
+ }
85
+ return suspiciousHits >= 2;
86
+ }
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TRUST_STORE_FILE = void 0;
7
+ exports.trustStorePath = trustStorePath;
8
+ exports.createEmptyTrustStore = createEmptyTrustStore;
9
+ exports.readTrustStore = readTrustStore;
10
+ exports.writeTrustStore = writeTrustStore;
11
+ exports.addTrustedPackages = addTrustedPackages;
12
+ exports.isTrusted = isTrusted;
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const path_1 = __importDefault(require("path"));
15
+ exports.TRUST_STORE_FILE = ".trust-npm.json";
16
+ const DEFAULT_THRESHOLD = 50;
17
+ function trustStorePath(projectRoot) {
18
+ return path_1.default.join(projectRoot, exports.TRUST_STORE_FILE);
19
+ }
20
+ function createEmptyTrustStore() {
21
+ const now = new Date().toISOString();
22
+ return {
23
+ version: 1,
24
+ createdAt: now,
25
+ updatedAt: now,
26
+ trustedPackages: {},
27
+ riskThreshold: DEFAULT_THRESHOLD
28
+ };
29
+ }
30
+ function readTrustStore(projectRoot) {
31
+ const filePath = trustStorePath(projectRoot);
32
+ if (!fs_1.default.existsSync(filePath)) {
33
+ return null;
34
+ }
35
+ const raw = fs_1.default.readFileSync(filePath, "utf8");
36
+ const parsed = JSON.parse(raw);
37
+ parsed.riskThreshold = parsed.riskThreshold ?? DEFAULT_THRESHOLD;
38
+ return parsed;
39
+ }
40
+ function writeTrustStore(projectRoot, store) {
41
+ const filePath = trustStorePath(projectRoot);
42
+ store.updatedAt = new Date().toISOString();
43
+ fs_1.default.writeFileSync(filePath, `${JSON.stringify(store, null, 2)}\n`, "utf8");
44
+ }
45
+ function addTrustedPackages(store, packageNames, source) {
46
+ const now = new Date().toISOString();
47
+ for (const name of packageNames) {
48
+ store.trustedPackages[name] = {
49
+ source,
50
+ approvedAt: now
51
+ };
52
+ }
53
+ return store;
54
+ }
55
+ function isTrusted(store, packageName) {
56
+ return Boolean(store.trustedPackages[packageName]);
57
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "trust-npm",
3
+ "version": "0.1.0",
4
+ "description": "A safe npm wrapper that blocks untrusted dependencies by default.",
5
+ "type": "commonjs",
6
+ "main": "dist/cli.js",
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "bin": {
13
+ "trust-npm": "dist/cli.js"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "prepublishOnly": "npm run build",
18
+ "start": "node dist/cli.js",
19
+ "dev": "ts-node src/cli.ts"
20
+ },
21
+ "keywords": [
22
+ "npm",
23
+ "security",
24
+ "supply-chain",
25
+ "cli",
26
+ "lockfile"
27
+ ],
28
+ "author": "trust-npm contributors",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "axios": "^1.8.4",
32
+ "commander": "^12.1.0",
33
+ "npm-package-arg": "^12.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.14.1",
37
+ "ts-node": "^10.9.2",
38
+ "typescript": "^5.8.2"
39
+ }
40
+ }