@cdxoo/npm-lockdown-proxy 0.0.1 → 0.0.2

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/README.md CHANGED
@@ -4,27 +4,39 @@ A minimal npm registry proxy that blocks any package (or version) not on a white
4
4
 
5
5
  ## AI Disclosure
6
6
 
7
- This stuff was vibe coded with claude (pronounced "KLORT!!")
7
+ This stuff was vibe coded with claude (pronounced "KLORT!!"). I hope I never have to actually mantain this...
8
8
 
9
9
  ## Run
10
10
 
11
11
  ```sh
12
- node proxy.js
13
- ```
12
+ # env var defaults are PORT=4873 WHITELIST=whitelist.json
13
+ npx @cdxoo/npm-lockdown-proxy
14
14
 
15
- | Env var | Default | Description |
16
- |---|---|---|
17
- | `PORT` | `4873` | Port to listen on |
18
- | `WHITELIST` | `whitelist.json` | Path to whitelist file |
15
+ # or
16
+
17
+ npm install -g @cdxoo/npm-lockdown-proxy
18
+ npm-lockdown-proxy
19
+ npm-lockdown-proxy-create-whitelist-form-lockfile some-package-lock.json [--merge]
20
+
21
+ ```
19
22
 
20
23
  ## Use
21
24
 
22
25
  ```sh
23
26
  npm install <pkg> --registry http://localhost:4873
24
- # or set it globally
27
+ # or
28
+ echo "registry=http://localhost:4873" >> my-project/.npmrc # or ~/.npmrc
29
+ # or
25
30
  npm config set registry http://localhost:4873
26
31
  ```
27
32
 
33
+ ## Server Env Vars
34
+
35
+ | Env var | Default | Description |
36
+ |---|---|---|
37
+ | `PORT` | `4873` | Port to listen on |
38
+ | `WHITELIST` | `whitelist.json` | Path to whitelist file |
39
+
28
40
  ## Whitelist format
29
41
 
30
42
  `whitelist.json` is an object. The value controls which versions are allowed:
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@cdxoo/npm-lockdown-proxy",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Minimal npm registry proxy with package/version whitelisting",
5
5
  "bin": {
6
- "npm-lockdown-proxy": "./proxy.js"
6
+ "npm-lockdown-proxy": "./proxy.js",
7
+ "npm-lockdown-proxy-whitelist-from-lockfile": "./whitelist-from-lockfile.js"
7
8
  },
8
9
  "files": [
9
10
  "proxy.js"
@@ -24,5 +25,14 @@
24
25
  "publishConfig": {
25
26
  "access": "public"
26
27
  },
27
- "dependencies": {}
28
+ "dependencies": {},
29
+ "keywords": [
30
+ "npm",
31
+ "registry",
32
+ "proxy",
33
+ "whitelist",
34
+ "lockdown",
35
+ "security",
36
+ "supply-chain"
37
+ ]
28
38
  }
package/proxy.js CHANGED
@@ -15,19 +15,19 @@ function loadWhitelist() {
15
15
  try {
16
16
  raw = JSON.parse(fs.readFileSync(WHITELIST_FILE, 'utf8'));
17
17
  } catch {
18
- console.warn(`WARNING: could not load whitelist from '${WHITELIST_FILE}' all packages will be blocked`);
18
+ console.warn(`WARNING: could not load whitelist from '${WHITELIST_FILE}' - all packages will be blocked`);
19
19
  console.warn(` Set the WHITELIST env var or create a whitelist.json in the working directory.`);
20
- return {};
20
+ return new Map();
21
21
  }
22
- // Normalise each entry to an array of allowed versions, or '*' for any.
23
- const wl = {};
22
+ // Normalise each entry to a Set of allowed versions, or '*' for any.
23
+ const wl = new Map();
24
24
  for (const [pkg, versions] of Object.entries(raw)) {
25
25
  if (versions === '*') {
26
- wl[pkg] = '*';
26
+ wl.set(pkg, '*');
27
27
  } else if (Array.isArray(versions)) {
28
- wl[pkg] = versions;
28
+ wl.set(pkg, new Set(versions));
29
29
  } else {
30
- wl[pkg] = [versions];
30
+ wl.set(pkg, new Set([versions]));
31
31
  }
32
32
  }
33
33
  return wl;
@@ -83,15 +83,15 @@ const server = http.createServer((req, res) => {
83
83
  const { pkg, version } = parseRequest(req.url.split('?')[0]);
84
84
 
85
85
  if (pkg !== null) {
86
- if (!Object.keys(whitelist).includes(pkg)) {
86
+ if (!whitelist.has(pkg)) {
87
87
  console.log(`BLOCKED ${req.method} ${req.url} - '${pkg}' not whitelisted`);
88
88
  return deny(res, `Package '${pkg}' is not on the whitelist`);
89
89
  }
90
90
 
91
- const allowed = whitelist[pkg];
92
- if (version !== null && allowed !== '*' && !allowed.includes(version)) {
91
+ const allowed = whitelist.get(pkg);
92
+ if (version !== null && allowed !== '*' && !allowed.has(version)) {
93
93
  console.log(`BLOCKED ${req.method} ${req.url} - '${pkg}@${version}' not an allowed version`);
94
- return deny(res, `Version '${version}' of '${pkg}' is not on the whitelist (allowed: ${allowed.join(', ')})`);
94
+ return deny(res, `Version '${version}' of '${pkg}' is not on the whitelist (allowed: ${[...allowed].join(', ')})`);
95
95
  }
96
96
  }
97
97
 
@@ -122,5 +122,5 @@ const server = http.createServer((req, res) => {
122
122
 
123
123
  server.listen(PORT, () => {
124
124
  console.log(`npm proxy -> ${UPSTREAM} on http://localhost:${PORT}`);
125
- console.log(`whitelist ${WHITELIST_FILE} (${Object.keys(whitelist).length} packages)`);
125
+ console.log(`whitelist ${WHITELIST_FILE} (${whitelist.size} packages)`);
126
126
  });
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+
6
+ const args = process.argv.slice(2).filter(a => a !== '--merge');
7
+ const merge = process.argv.includes('--merge');
8
+ const lockfile = args[0];
9
+ const output = args[1] || 'whitelist.json';
10
+
11
+ if (!lockfile) {
12
+ console.error('usage: npm-lockdown-proxy-whitelist-from-lockfile <package-lock.json> [whitelist.json] [--merge]');
13
+ process.exit(1);
14
+ }
15
+
16
+ const lock = JSON.parse(fs.readFileSync(lockfile, 'utf8'));
17
+
18
+ if (lock.lockfileVersion < 2) {
19
+ console.error('error: lockfile version 2 or higher required (npm 7+)');
20
+ process.exit(1);
21
+ }
22
+
23
+ function confirm(prompt) {
24
+ process.stdout.write(prompt);
25
+ const fd = fs.openSync('/dev/tty', 'r');
26
+ const buf = Buffer.alloc(1);
27
+ let answer = '';
28
+ while (true) {
29
+ fs.readSync(fd, buf, 0, 1);
30
+ if (buf[0] === 0x0a) break; // newline
31
+ answer += buf.toString();
32
+ }
33
+ fs.closeSync(fd);
34
+ return answer.trim().toLowerCase() === 'y';
35
+ }
36
+
37
+ // Map<name, Set<version>>
38
+ function parseLockfile(lock) {
39
+ const pkgs = new Map();
40
+ for (const [key, pkg] of Object.entries(lock.packages || {})) {
41
+ if (key === '') continue; // root package
42
+ if (pkg.link) continue; // workspace symlinks
43
+ const name = key.replace(/^.*node_modules\//, '');
44
+ process.stdout.write(`\rparsing: ${name.padEnd(60)}`);
45
+ const versions = pkgs.get(name) ?? new Set();
46
+ versions.add(pkg.version);
47
+ pkgs.set(name, versions);
48
+ }
49
+ process.stdout.write('\r' + ' '.repeat(70) + '\r'); // clear line
50
+ return pkgs;
51
+ }
52
+
53
+ // Map<name, Set<version> | '*'>
54
+ function loadWhitelist(file) {
55
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
56
+ const wl = new Map();
57
+ for (const [name, versions] of Object.entries(raw)) {
58
+ wl.set(name, versions === '*' ? '*' : new Set(versions));
59
+ }
60
+ return wl;
61
+ }
62
+
63
+ function serialize(wl) {
64
+ const out = {};
65
+ for (const [name, versions] of [...wl.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
66
+ out[name] = versions === '*' ? '*' : [...versions];
67
+ }
68
+ return JSON.stringify(out, null, 2);
69
+ }
70
+
71
+ const parsed = parseLockfile(lock);
72
+
73
+ if (merge) {
74
+ if (!fs.existsSync(output)) {
75
+ console.error(`error: '${output}' does not exist, cannot merge`);
76
+ process.exit(1);
77
+ }
78
+ if (!confirm(`merge into '${output}'? [y/N] `)) {
79
+ console.log('aborted');
80
+ process.exit(0);
81
+ }
82
+ const existing = loadWhitelist(output);
83
+ for (const [name, versions] of parsed) {
84
+ if (!existing.has(name)) {
85
+ existing.set(name, versions);
86
+ } else if (existing.get(name) !== '*') {
87
+ const merged = existing.get(name);
88
+ for (const v of versions) merged.add(v);
89
+ }
90
+ }
91
+ fs.writeFileSync(output, serialize(existing));
92
+ console.log(`merged ${parsed.size} packages into ${output} (${existing.size} total)`);
93
+ } else {
94
+ if (fs.existsSync(output)) {
95
+ if (!confirm(`'${output}' already exists, overwrite? [y/N] `)) {
96
+ console.log('aborted');
97
+ process.exit(0);
98
+ }
99
+ }
100
+ fs.writeFileSync(output, serialize(parsed));
101
+ console.log(`wrote ${parsed.size} packages to ${output}`);
102
+ }