@itsl-solutions/npm-registry-shield 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 +21 -0
- package/README.md +230 -0
- package/package.json +38 -0
- package/src/cli.ts +346 -0
- package/src/config.ts +88 -0
- package/src/daemon.ts +206 -0
- package/src/dashboard.ts +230 -0
- package/src/filter.ts +186 -0
- package/src/origin.ts +130 -0
- package/src/pm-config.ts +124 -0
- package/src/registry.ts +128 -0
- package/src/server.ts +366 -0
- package/src/stats.ts +235 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ITSL Solutions
|
|
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,230 @@
|
|
|
1
|
+
# npm-registry-shield
|
|
2
|
+
|
|
3
|
+
Registry proxy that quarantines recently published npm package versions. Protects against supply chain attacks by ensuring you only install versions that have been on the registry for a configurable number of days.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
npm-registry-shield sits between your package manager and the npm registry. When you run `npm install`, it intercepts the package metadata and strips out versions published within the quarantine window (default: 3 days). Your package manager never sees the risky versions - it resolves to the newest safe version automatically.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm install express
|
|
11
|
+
|
|
|
12
|
+
v
|
|
13
|
+
localhost:4873 (npm-registry-shield)
|
|
14
|
+
|
|
|
15
|
+
|- fetch metadata from registry.npmjs.org
|
|
16
|
+
|- strip versions published < 3 days ago
|
|
17
|
+
|- return filtered metadata
|
|
18
|
+
|
|
|
19
|
+
v
|
|
20
|
+
npm resolves to newest safe version
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
All transitive dependencies are also protected - every package at every depth resolves through the proxy.
|
|
24
|
+
|
|
25
|
+
### What gets filtered, what passes through
|
|
26
|
+
|
|
27
|
+
| Request type | Behavior |
|
|
28
|
+
|--------------|----------|
|
|
29
|
+
| `GET /<pkg>` (full packument) | Versions inside the quarantine window are stripped. `dist-tags` are rewritten to point at the newest surviving version. If every version is quarantined, returns 404. |
|
|
30
|
+
| `GET /<pkg>/<version>` (single version metadata) | Passed through unchanged. If the version is quarantined, a warning is logged and counted in stats but the response is not blocked. |
|
|
31
|
+
| Tarball downloads (`/<pkg>/-/<file>.tgz`) | Forwarded upstream as-is. |
|
|
32
|
+
| Search, login, publish, anything else | Forwarded upstream as-is. |
|
|
33
|
+
|
|
34
|
+
Packuments are cached in memory for 10 minutes (configurable via `cacheTtlMinutes`). The cache is cleared automatically when you run `allow` or `remove`.
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- [bun](https://bun.sh) >= 1.0
|
|
39
|
+
|
|
40
|
+
`bun` is the only runtime. The proxy itself runs under bun, and the published package ships TypeScript sources executed directly by bun.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bun add -g npm-registry-shield
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm-registry-shield start
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
That's it. On macOS this installs a launchd service and starts it. On Linux it installs a systemd user service. The proxy runs in the background, survives reboots, and rewrites your `~/.npmrc` (and `~/.yarnrc.yml` if present) to point at it.
|
|
55
|
+
|
|
56
|
+
Your `npm`, `pnpm`, `yarn`, and `bun install` commands are now protected.
|
|
57
|
+
|
|
58
|
+
To stop:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm-registry-shield stop
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This unloads the service and restores your original `~/.npmrc`.
|
|
65
|
+
|
|
66
|
+
To run in the foreground (for debugging or one-off use):
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm-registry-shield start --foreground
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Logs are written to `~/.npm-shield/daemon.log`. Configuration is at `~/.npm-shield/config.json`.
|
|
73
|
+
|
|
74
|
+
## Manual daemon install
|
|
75
|
+
|
|
76
|
+
If you want to write the service file yourself (custom flags, custom path, etc.):
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# macOS
|
|
80
|
+
npm-registry-shield daemon-template launchd > ~/Library/LaunchAgents/com.npm-registry-shield.plist
|
|
81
|
+
launchctl load ~/Library/LaunchAgents/com.npm-registry-shield.plist
|
|
82
|
+
|
|
83
|
+
# Linux
|
|
84
|
+
mkdir -p ~/.config/systemd/user
|
|
85
|
+
npm-registry-shield daemon-template systemd > ~/.config/systemd/user/npm-registry-shield.service
|
|
86
|
+
systemctl --user daemon-reload && systemctl --user enable --now npm-registry-shield
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Commands
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
npm-registry-shield start [options] Install + start the background daemon (survives reboots)
|
|
93
|
+
npm-registry-shield stop Stop the daemon and restore package manager configs
|
|
94
|
+
npm-registry-shield status Show running state and config
|
|
95
|
+
npm-registry-shield allow <pkg> Skip quarantine for a package
|
|
96
|
+
npm-registry-shield allow <pkg>@<ver> Allow specific version (with warning)
|
|
97
|
+
npm-registry-shield remove <pattern> Remove from passthrough list
|
|
98
|
+
npm-registry-shield list Show passthrough entries
|
|
99
|
+
npm-registry-shield config get <key> Show config value
|
|
100
|
+
npm-registry-shield config set <k> <v> Update config value
|
|
101
|
+
npm-registry-shield stats Show statistics summary
|
|
102
|
+
npm-registry-shield daemon-template <launchd|systemd>
|
|
103
|
+
Print a service unit you can install for persistence
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Start options
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
--foreground Run in this terminal instead of installing the daemon
|
|
110
|
+
--quarantine=<days> Override quarantine period (default: 3)
|
|
111
|
+
--port=<port> Override proxy port (default: 4873)
|
|
112
|
+
--upstream=<url> Override upstream registry URL
|
|
113
|
+
--no-dashboard Disable the web dashboard
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Package manager support
|
|
117
|
+
|
|
118
|
+
| Manager | Supported | How |
|
|
119
|
+
|---------|-----------|-----|
|
|
120
|
+
| npm | Yes | `.npmrc` registry setting |
|
|
121
|
+
| pnpm | Yes | `.npmrc` registry setting |
|
|
122
|
+
| yarn v1 | Yes | `.npmrc` registry setting |
|
|
123
|
+
| yarn v2+ | Yes | `.yarnrc.yml` npmRegistryServer |
|
|
124
|
+
| bun | Yes | `.npmrc` registry setting |
|
|
125
|
+
| deno | Manual | `export DENO_NPM_REGISTRY=http://localhost:4873` |
|
|
126
|
+
|
|
127
|
+
## Dashboard
|
|
128
|
+
|
|
129
|
+
When the proxy is running, open `http://localhost:4873/` for a live stats dashboard showing total requests, hidden (quarantined) versions, blocked installs, and a per-package breakdown.
|
|
130
|
+
|
|
131
|
+
Each package row also shows:
|
|
132
|
+
|
|
133
|
+
- **Tool** that fetched it (`npm`, `pnpm`, `yarn`, `bun`, `deno`, `browser`, ...) detected from the request `User-Agent`.
|
|
134
|
+
- **Origin path** of the process that made the request (working directory) plus the **launcher app** that started the chain (e.g. `iTerm2`, `Claude`, `Code Helper`), so you can tell apart an explicit `npm install` from a tool that quietly spawned `npm` in the background.
|
|
135
|
+
|
|
136
|
+
By default only packages with more than 10 requests are shown; the "Show all" button in the Packages header reveals the long tail.
|
|
137
|
+
|
|
138
|
+
Origin attribution uses `lsof` + `ps` and works on macOS today. On Linux and other platforms the proxy still serves stats - the origin/tool columns are simply empty.
|
|
139
|
+
|
|
140
|
+
## Stats CLI
|
|
141
|
+
|
|
142
|
+
For a terminal-friendly snapshot:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm-registry-shield stats
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Prints the totals, the top 20 packages by request count with aligned columns, and the legend explaining what each column means. Reads from `~/.npm-shield/stats.json`, which the daemon flushes every 30 seconds.
|
|
149
|
+
|
|
150
|
+
## Security & networking
|
|
151
|
+
|
|
152
|
+
The proxy listens on `127.0.0.1` only, so nothing on your LAN can reach it. Upstream traffic still goes out over HTTPS to whatever registry you configured (`https://registry.npmjs.org` by default).
|
|
153
|
+
|
|
154
|
+
## Configuration
|
|
155
|
+
|
|
156
|
+
Config is stored at `~/.npm-shield/config.json`:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"port": 4873,
|
|
161
|
+
"upstream": "https://registry.npmjs.org",
|
|
162
|
+
"quarantineDays": 3,
|
|
163
|
+
"cacheTtlMinutes": 10,
|
|
164
|
+
"dashboard": true,
|
|
165
|
+
"passthrough": []
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Passthrough list
|
|
170
|
+
|
|
171
|
+
Skip quarantine for trusted packages:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Entire package
|
|
175
|
+
npm-registry-shield allow lodash
|
|
176
|
+
|
|
177
|
+
# Scoped packages
|
|
178
|
+
npm-registry-shield allow "@my-org/*"
|
|
179
|
+
|
|
180
|
+
# Specific version (allowed with a warning)
|
|
181
|
+
npm-registry-shield allow express@5.0.0
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## What happens when...
|
|
185
|
+
|
|
186
|
+
**`npm install express`** - Resolves to the newest version that is at least 3 days old.
|
|
187
|
+
|
|
188
|
+
**`npm install express@5.0.0` where 5.0.0 is too new** - npm fetches the full packument first to resolve, sees 5.0.0 missing, and fails. Run `npm-registry-shield allow express@5.0.0` to permit it.
|
|
189
|
+
|
|
190
|
+
**A brand-new package (all versions < 3 days old)** - The packument response is a 404 with an explanatory error. Add the package to the passthrough list to allow it.
|
|
191
|
+
|
|
192
|
+
**`npm ci` with a lockfile pointing at a quarantined version** - The packument resolution step strips the version, so npm errors. Catches dependencies your teammates added that haven't aged past the quarantine window yet. Add the offending versions to passthrough or wait them out.
|
|
193
|
+
|
|
194
|
+
**`npm view express@5.0.0`** - Hits the single-version endpoint, which passes through with a warning logged in `~/.npm-shield/daemon.log` and counted under "warnings" in stats. Useful for inspecting fresh versions without committing to install them.
|
|
195
|
+
|
|
196
|
+
**The proxy is down** - `.npmrc` points at a dead server, npm commands fail. Run `npm-registry-shield stop` to restore your config. If the CLI is unreachable, manually remove the `registry=` line from `~/.npmrc`.
|
|
197
|
+
|
|
198
|
+
## Recovery
|
|
199
|
+
|
|
200
|
+
If something goes wrong and npm commands fail:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# Restore configs from backup
|
|
204
|
+
npm-registry-shield stop
|
|
205
|
+
|
|
206
|
+
# If the CLI is unavailable, remove this line manually:
|
|
207
|
+
# registry=http://localhost:4873
|
|
208
|
+
nano ~/.npmrc
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Development
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
git clone https://github.com/maleta/npm-registry-shield.git
|
|
215
|
+
cd npm-registry-shield
|
|
216
|
+
bun install
|
|
217
|
+
bun link
|
|
218
|
+
exec $SHELL # reload shell so the new global bin is on PATH
|
|
219
|
+
npm-registry-shield start
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`bun link` symlinks the package into bun's global bin dir pointing at your clone. Edit `src/*.ts` and the change is live on the next run - no rebuild step.
|
|
223
|
+
|
|
224
|
+
Note: after `bun link`, the `npm-registry-shield` command is not available in the same shell session that ran the link - shells cache PATH lookups. Open a new terminal or run `exec $SHELL` (or `hash -r` in bash/zsh) to refresh.
|
|
225
|
+
|
|
226
|
+
Run tests with `bun test`.
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@itsl-solutions/npm-registry-shield",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Registry proxy that quarantines recently published npm package versions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"npm-registry-shield": "src/cli.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"npm",
|
|
16
|
+
"security",
|
|
17
|
+
"supply-chain",
|
|
18
|
+
"registry",
|
|
19
|
+
"proxy",
|
|
20
|
+
"quarantine"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "Aleksandar Maletic",
|
|
24
|
+
"engines": {
|
|
25
|
+
"bun": ">=1.0.0"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "bun test",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"prepublishOnly": "bun test && bunx tsc --noEmit"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"bun-types": "^1.3.14"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { loadConfig, saveConfig, getConfigValue, setConfigValue } from "./config.js";
|
|
3
|
+
import { startServer, stopServer } from "./server.js";
|
|
4
|
+
import { configurePackageManagers, restorePackageManagers, hasStaleState, forceRestore } from "./pm-config.js";
|
|
5
|
+
import { formatStatsForConsole, loadStats } from "./stats.js";
|
|
6
|
+
import { installDaemon, uninstallDaemon, isDaemonRunning, getDaemonType, printDaemonInstructions } from "./daemon.js";
|
|
7
|
+
|
|
8
|
+
interface ParsedArgs {
|
|
9
|
+
command: string | undefined;
|
|
10
|
+
positional: string[];
|
|
11
|
+
flags: Record<string, string | boolean>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseArgs(argv: string[]): ParsedArgs {
|
|
15
|
+
const positional: string[] = [];
|
|
16
|
+
const flags: Record<string, string | boolean> = {};
|
|
17
|
+
|
|
18
|
+
for (const arg of argv) {
|
|
19
|
+
if (arg.startsWith("--no-")) {
|
|
20
|
+
flags[arg.slice(5)] = false;
|
|
21
|
+
} else if (arg.startsWith("--")) {
|
|
22
|
+
const eqIdx = arg.indexOf("=");
|
|
23
|
+
if (eqIdx !== -1) {
|
|
24
|
+
flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
|
|
25
|
+
} else {
|
|
26
|
+
flags[arg.slice(2)] = true;
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
positional.push(arg);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
command: positional[0],
|
|
35
|
+
positional,
|
|
36
|
+
flags,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
41
|
+
const { command, positional, flags } = parsed;
|
|
42
|
+
const subcommand = positional[1];
|
|
43
|
+
|
|
44
|
+
function printUsage(): void {
|
|
45
|
+
console.log(`npm-registry-shield - Registry proxy that quarantines recent package versions
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
npm-registry-shield start [options] Install + start the background daemon (survives reboots)
|
|
49
|
+
npm-registry-shield stop Stop the daemon and restore package manager configs
|
|
50
|
+
npm-registry-shield status Show running state and config
|
|
51
|
+
npm-registry-shield allow <pkg> Skip quarantine for a package
|
|
52
|
+
npm-registry-shield allow <pkg>@<ver> Allow specific version (with warning)
|
|
53
|
+
npm-registry-shield remove <pattern> Remove from passthrough list
|
|
54
|
+
npm-registry-shield list Show passthrough entries
|
|
55
|
+
npm-registry-shield config get <key> Show config value
|
|
56
|
+
npm-registry-shield config set <k> <v> Update config value
|
|
57
|
+
npm-registry-shield stats Show statistics summary
|
|
58
|
+
npm-registry-shield daemon-template <launchd|systemd>
|
|
59
|
+
Print a service unit (manual install path)
|
|
60
|
+
|
|
61
|
+
Start options:
|
|
62
|
+
--foreground Run in this terminal instead of installing the daemon
|
|
63
|
+
--quarantine=<days> Override quarantine period (default: 3)
|
|
64
|
+
--port=<port> Override proxy port (default: 4873)
|
|
65
|
+
--upstream=<url> Override upstream registry URL
|
|
66
|
+
--no-dashboard Disable the web dashboard
|
|
67
|
+
--dashboard Enable the web dashboard (default)`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function applyStartFlags(config: ReturnType<typeof loadConfig>): void {
|
|
71
|
+
if (flags.quarantine !== undefined) {
|
|
72
|
+
const days = Number(flags.quarantine);
|
|
73
|
+
if (isNaN(days) || days <= 0) {
|
|
74
|
+
console.error("--quarantine must be a positive number");
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
config.quarantineDays = days;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (flags.port !== undefined) {
|
|
81
|
+
const port = Number(flags.port);
|
|
82
|
+
if (isNaN(port) || port <= 0 || port > 65535) {
|
|
83
|
+
console.error("--port must be a valid port number (1-65535)");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
config.port = port;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (flags.dashboard === false) {
|
|
90
|
+
config.dashboard = false;
|
|
91
|
+
} else if (flags.dashboard === true) {
|
|
92
|
+
config.dashboard = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (flags.upstream !== undefined) {
|
|
96
|
+
config.upstream = String(flags.upstream);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handleStartForeground(): void {
|
|
101
|
+
const config = loadConfig();
|
|
102
|
+
applyStartFlags(config);
|
|
103
|
+
|
|
104
|
+
if (hasStaleState()) {
|
|
105
|
+
console.log("[npm-registry-shield] Detected stale backup files from a previous crash. Cleaning up...");
|
|
106
|
+
forceRestore();
|
|
107
|
+
}
|
|
108
|
+
configurePackageManagers(config.port);
|
|
109
|
+
|
|
110
|
+
const server = startServer(config);
|
|
111
|
+
|
|
112
|
+
const cleanup = (): void => {
|
|
113
|
+
console.log("\n[npm-registry-shield] Shutting down...");
|
|
114
|
+
restorePackageManagers();
|
|
115
|
+
stopServer(server).then(() => {
|
|
116
|
+
console.log("[npm-registry-shield] Stopped.");
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}).catch(() => {
|
|
119
|
+
process.exit(1);
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
process.on("SIGINT", cleanup);
|
|
124
|
+
process.on("SIGTERM", cleanup);
|
|
125
|
+
process.on("SIGHUP", cleanup);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function handleStartDaemon(): void {
|
|
129
|
+
const config = loadConfig();
|
|
130
|
+
applyStartFlags(config);
|
|
131
|
+
saveConfig(config);
|
|
132
|
+
|
|
133
|
+
if (isDaemonRunning()) {
|
|
134
|
+
console.log("[npm-registry-shield] Daemon is already running. Use 'npm-registry-shield stop' first.");
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
installDaemon();
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if ((err as Error).message === "DAEMON_UNSUPPORTED") {
|
|
142
|
+
console.log("[npm-registry-shield] Auto-daemon is only supported on macOS and Linux.");
|
|
143
|
+
console.log("[npm-registry-shield] Falling back to foreground (Ctrl+C to stop).");
|
|
144
|
+
handleStartForeground();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(`[npm-registry-shield] Quarantine: ${config.quarantineDays} days`);
|
|
151
|
+
console.log(`[npm-registry-shield] Port: ${config.port}`);
|
|
152
|
+
if (config.dashboard) {
|
|
153
|
+
console.log(`[npm-registry-shield] Dashboard: http://localhost:${config.port}/`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function handleStart(): void {
|
|
158
|
+
if (flags.foreground) {
|
|
159
|
+
handleStartForeground();
|
|
160
|
+
} else {
|
|
161
|
+
handleStartDaemon();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function handleStop(): void {
|
|
166
|
+
const wasRunning = isDaemonRunning();
|
|
167
|
+
uninstallDaemon();
|
|
168
|
+
|
|
169
|
+
if (hasStaleState()) {
|
|
170
|
+
forceRestore();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (wasRunning) {
|
|
174
|
+
console.log("[npm-registry-shield] Daemon stopped and package manager configs restored.");
|
|
175
|
+
} else if (hasStaleState()) {
|
|
176
|
+
console.log("[npm-registry-shield] Restored package manager configs (daemon was not running).");
|
|
177
|
+
} else {
|
|
178
|
+
console.log("[npm-registry-shield] Nothing to stop.");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function handleStatus(): void {
|
|
183
|
+
const config = loadConfig();
|
|
184
|
+
const running = isDaemonRunning();
|
|
185
|
+
const stale = hasStaleState();
|
|
186
|
+
const daemonType = getDaemonType();
|
|
187
|
+
|
|
188
|
+
console.log(`npm-registry-shield status:`);
|
|
189
|
+
console.log(` Running: ${running ? `yes (${daemonType})` : "no"}`);
|
|
190
|
+
console.log(` Port: ${config.port}`);
|
|
191
|
+
console.log(` Upstream: ${config.upstream}`);
|
|
192
|
+
console.log(` Quarantine: ${config.quarantineDays} days`);
|
|
193
|
+
console.log(` Cache TTL: ${config.cacheTtlMinutes} minutes`);
|
|
194
|
+
console.log(` Dashboard: ${config.dashboard ? "on" : "off"}`);
|
|
195
|
+
console.log(` Passthrough: ${config.passthrough.length === 0 ? "(none)" : config.passthrough.join(", ")}`);
|
|
196
|
+
|
|
197
|
+
if (stale && !running) {
|
|
198
|
+
console.log(` WARNING: Stale config detected - run 'npm-registry-shield stop' to clean up`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function handleAllow(): void {
|
|
203
|
+
const pattern = positional[1];
|
|
204
|
+
if (!pattern) {
|
|
205
|
+
console.error("Usage: npm-registry-shield allow <pkg> or npm-registry-shield allow <pkg>@<version>");
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const config = loadConfig();
|
|
210
|
+
if (config.passthrough.includes(pattern)) {
|
|
211
|
+
console.log(`[npm-registry-shield] "${pattern}" is already in the passthrough list`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
config.passthrough.push(pattern);
|
|
216
|
+
saveConfig(config);
|
|
217
|
+
console.log(`[npm-registry-shield] Added "${pattern}" to passthrough list`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function handleRemove(): void {
|
|
221
|
+
const pattern = positional[1];
|
|
222
|
+
if (!pattern) {
|
|
223
|
+
console.error("Usage: npm-registry-shield remove <pattern>");
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const config = loadConfig();
|
|
228
|
+
const idx = config.passthrough.indexOf(pattern);
|
|
229
|
+
if (idx === -1) {
|
|
230
|
+
console.error(`[npm-registry-shield] "${pattern}" not found in passthrough list`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
config.passthrough.splice(idx, 1);
|
|
235
|
+
saveConfig(config);
|
|
236
|
+
console.log(`[npm-registry-shield] Removed "${pattern}" from passthrough list`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function handleList(): void {
|
|
240
|
+
const config = loadConfig();
|
|
241
|
+
if (config.passthrough.length === 0) {
|
|
242
|
+
console.log("Passthrough list is empty");
|
|
243
|
+
} else {
|
|
244
|
+
console.log("Passthrough list:");
|
|
245
|
+
for (const entry of config.passthrough) {
|
|
246
|
+
console.log(` ${entry}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function handleConfig(): void {
|
|
252
|
+
const action = subcommand;
|
|
253
|
+
const key = positional[2];
|
|
254
|
+
|
|
255
|
+
if (action === "get") {
|
|
256
|
+
if (!key) {
|
|
257
|
+
console.error("Usage: npm-registry-shield config get <key>");
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const value = getConfigValue(key);
|
|
262
|
+
console.log(Array.isArray(value) ? value.join(", ") : String(value));
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.error((err as Error).message);
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (action === "set") {
|
|
271
|
+
const value = positional[3];
|
|
272
|
+
if (!key || value === undefined) {
|
|
273
|
+
console.error("Usage: npm-registry-shield config set <key> <value>");
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
setConfigValue(key, value);
|
|
278
|
+
console.log(`[npm-registry-shield] Set ${key} = ${value}`);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.error((err as Error).message);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.error("Usage: npm-registry-shield config <get|set> <key> [value]");
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function handleStats(): void {
|
|
291
|
+
loadStats();
|
|
292
|
+
console.log(formatStatsForConsole());
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function handleDaemonTemplate(): void {
|
|
296
|
+
const target = subcommand;
|
|
297
|
+
if (target !== "launchd" && target !== "systemd") {
|
|
298
|
+
console.error("Usage: npm-registry-shield daemon-template <launchd|systemd>");
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
printDaemonInstructions(target);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
console.error((err as Error).message);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
switch (command) {
|
|
310
|
+
case "start":
|
|
311
|
+
handleStart();
|
|
312
|
+
break;
|
|
313
|
+
case "stop":
|
|
314
|
+
handleStop();
|
|
315
|
+
break;
|
|
316
|
+
case "status":
|
|
317
|
+
handleStatus();
|
|
318
|
+
break;
|
|
319
|
+
case "allow":
|
|
320
|
+
handleAllow();
|
|
321
|
+
break;
|
|
322
|
+
case "remove":
|
|
323
|
+
handleRemove();
|
|
324
|
+
break;
|
|
325
|
+
case "list":
|
|
326
|
+
handleList();
|
|
327
|
+
break;
|
|
328
|
+
case "config":
|
|
329
|
+
handleConfig();
|
|
330
|
+
break;
|
|
331
|
+
case "stats":
|
|
332
|
+
handleStats();
|
|
333
|
+
break;
|
|
334
|
+
case "daemon-template":
|
|
335
|
+
handleDaemonTemplate();
|
|
336
|
+
break;
|
|
337
|
+
case "--help":
|
|
338
|
+
case "-h":
|
|
339
|
+
case undefined:
|
|
340
|
+
printUsage();
|
|
341
|
+
break;
|
|
342
|
+
default:
|
|
343
|
+
console.error(`Unknown command: ${command}`);
|
|
344
|
+
printUsage();
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|