Skip to content

Commit 9ac5967

Browse files
authored
feat: add npm support (#11)
1 parent 2737931 commit 9ac5967

16 files changed

Lines changed: 948 additions & 63 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ COPY --from=builder /build/target/release/vein /usr/local/bin/vein
4040

4141
# Set working directory and permissions
4242
WORKDIR /data
43-
RUN chown vein:vein /data
43+
RUN chown -R vein:vein /data
4444

4545
USER vein
4646

README.md

Lines changed: 57 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
# Vein 💎
22

3-
A fast, intelligent RubyGems proxy/mirror server. Part of the ore ecosystem alongside [ore-light](https://github.com/contriboss/ore-light).
3+
A fast, intelligent multi-ecosystem package proxy/mirror server (RubyGems, crates.io, npm; Python next).
4+
5+
**Single endpoint:** Bundler and Ore point to the same Vein URL. The same base URL can also serve Cargo (sparse index + crate downloads) and npm (metadata + tarballs) via path/header detection. Upstream configuration applies to RubyGems only; crates.io and npm use fixed upstreams.
46

57
## What is Vein?
68

7-
Vein is a **smart caching proxy** for RubyGems that:
9+
Vein is a **smart caching proxy** for multiple package ecosystems that:
810

9-
- Proxies rubygems.org with local caching
10-
- Serves gems from local cache when available
11-
- Only fetches from upstream on cache miss
11+
- Proxies RubyGems via a configurable upstream
12+
- Mirrors crates.io (sparse index + crate downloads) with caching
13+
- Proxies npm registry metadata and tarballs with caching
14+
- Serves cached artifacts from local storage on repeat requests
15+
- Runs in cache-only mode when no RubyGems upstream is configured
1216
- Built on Rama (modular service framework)
13-
- Zero configuration - just works
17+
- Minimal configuration - just works for common setups
1418

1519
## Why Vein?
1620

1721
- **Blazing Fast**: High-performance proxy with Rama
18-
- **Smart Caching**: Proxy once, serve forever from local cache
19-
- **Supply Chain Protection**: Optional quarantine delays for new gem versions
20-
- **Minimal Config**: Works out of the box, configure only what you need
22+
- **Smart Caching**: Cache artifacts once and serve locally thereafter
23+
- **Supply Chain Protection**: Quarantine system (RubyGems today; expanding across ecosystems)
24+
- **Minimal Config**: Cache-only by default; enable RubyGems upstream when needed
2125
- **Simple Deployment**: Single binary, no complex dependencies
26+
- **Multi-registry endpoint**: RubyGems + crates.io + npm on one base URL
2227
- **Ore Integration**: Works seamlessly with ore-light's fallback mechanism
2328

2429
## Quick Start
@@ -41,31 +46,32 @@ cargo build --release
4146
```
4247
Client Request → Vein → Local Cache?
4348
├─ Hit → Serve from filesystem
44-
└─ Miss → Fetch from rubygems.org
49+
└─ Miss → Fetch from upstream (RubyGems / crates.io / npm)
4550
├─ Cache locally
4651
└─ Serve to client
4752
```
4853

49-
**Permanent Caching**: Once a gem is cached, it's served locally forever. No re-fetching.
54+
**Permanent Caching**: Artifact files (gems, crates, npm tarballs) are cached on first fetch and served locally thereafter. Index/metadata endpoints are cached with TTL + revalidation.
5055

51-
**Simple Architecture**: SQLite for metadata + filesystem for gem files.
56+
**Simple Architecture**: SQLite (default) or PostgreSQL for metadata + filesystem for cached artifacts (default storage root: `./cache`).
5257

5358
## Features
5459

5560
- [x] Rama-based HTTP proxy
56-
- [x] SQLite gem inventory (persistent metadata)
57-
- [x] Filesystem storage (`./gems/`)
61+
- [x] SQLite/PostgreSQL inventory (persistent metadata)
62+
- [x] Filesystem storage (default `./cache/` with per-ecosystem subfolders)
5863
- [x] Smart cache resolver
5964
- [x] Stream-through caching (cache while serving)
6065
- [x] SHA256 verification
6166
- [x] Minimal configuration
6267
- [x] Docker image
63-
- [x] Gem name/version/platform parsing
68+
- [x] Package name/version/platform parsing
6469
- [x] Request logging with metrics
6570
- [x] Cache revalidation on corruption
66-
- [x] Legacy API rejection (with monitoring)
67-
- [x] CycloneDX SBOM extraction with admin preview & download API
68-
- [x] Quarantine system (supply chain attack protection)
71+
- [x] crates.io sparse index + crate download caching
72+
- [x] npm registry metadata + tarball caching
73+
- [x] CycloneDX SBOM extraction with admin preview & download API (RubyGems today; expanding)
74+
- [x] Quarantine system (supply chain attack protection, RubyGems today; expanding)
6975

7076
### Usage
7177

@@ -77,16 +83,16 @@ host = "0.0.0.0"
7783
port = 8346
7884
7985
[upstream]
80-
url = "https://rubygems.org"
86+
url = "https://rubygems.org" # RubyGems only (crates.io + npm are fixed upstreams)
8187
8288
[storage]
83-
path = "./gems"
89+
path = "./cache"
8490
8591
[database]
8692
path = "./vein.db"
8793
TOML
8894

89-
# Start the proxy (streams uncached gems through Rama)
95+
# Start the proxy (streams uncached artifacts through Rama)
9096
cargo run -- serve --config vein.toml
9197

9298
# Inspect cache statistics
@@ -95,12 +101,16 @@ cargo run -- stats --config vein.toml
95101

96102
### CycloneDX SBOM access
97103

104+
**Scope:** SBOMs are currently generated for cached RubyGems. The roadmap is to extend SBOM generation to all supported ecosystems (crates.io, npm, and Python).
105+
98106
- **Admin dashboard**: start `make admin` then browse to `http://127.0.0.1:9400/catalog/<gem>?version=<version>` to preview the generated SBOM and download the JSON directly from the UI.
99107
- **Proxy endpoint**: any client can fetch the SBOM without the admin UI by calling `GET /.well-known/vein/sbom?name=<gem>&version=<version>[&platform=<platform>]` against the running Vein proxy. The response is a CycloneDX 1.5 document with `Content-Type: application/json` and a download-friendly filename. Omit the `platform` query for default `ruby` builds; supply it for native variants (e.g. `arm64-darwin`).
100108
- SBOMs are generated automatically the first time a gem is cached and refreshed whenever the gem is re-fetched.
101109

102110
### Quarantine (Supply Chain Protection)
103111

112+
**Scope:** Quarantine is currently applied to RubyGems metadata/index responses. The roadmap is to apply the same protection to crates.io, npm, and Python.
113+
104114
Vein can delay new gem versions from appearing in Bundler's index, giving the community time to catch malicious packages before they reach your CI/CD.
105115

106116
**How it works:**
@@ -121,15 +131,18 @@ Vein can delay new gem versions from appearing in Bundler's index, giving the co
121131
enabled = true
122132
default_delay_days = 3
123133
skip_weekends = true # Don't release on Sat/Sun
134+
business_hours_only = true # Only release during business hours
124135
release_hour_utc = 10 # Release at 10:00 UTC
125136

126137
# Per-gem overrides (glob patterns supported)
127138
[[delay_policy.gems]]
128-
pattern = "rails*"
139+
name = "rails*"
140+
pattern = true
129141
delay_days = 7 # Extra scrutiny for Rails ecosystem
130142

131143
[[delay_policy.gems]]
132-
pattern = "internal-*"
144+
name = "internal-*"
145+
pattern = true
133146
delay_days = 0 # Trust internal gems
134147

135148
# Pin specific versions for immediate availability
@@ -170,10 +183,10 @@ Minimal config (most settings have sensible defaults):
170183
port = 8346 # default
171184

172185
[upstream]
173-
url = "https://rubygems.org" # default
186+
url = "https://rubygems.org" # RubyGems only (optional)
174187

175188
[storage]
176-
path = "./gems" # default
189+
path = "./cache" # default
177190
```
178191

179192
Full config options:
@@ -182,52 +195,47 @@ Full config options:
182195
[server]
183196
host = "0.0.0.0"
184197
port = 8346
185-
threads = 4 # Rama worker threads
198+
workers = 4 # Rama worker threads
186199

187200
[upstream]
188-
url = "https://rubygems.org"
189-
timeout_secs = 30
190-
connection_pool_size = 100
201+
url = "https://rubygems.org" # RubyGems only (optional)
202+
fallback_urls = []
191203

192204
[storage]
193-
path = "./gems"
205+
path = "./cache"
194206

195207
[database]
196208
path = "vein.db" # SQLite inventory
197209

198210
[logging]
199211
level = "info" # debug, info, warn, error
200-
201-
[hotcache]
202-
# Automatic refresh schedule (cron format: "sec min hour day month weekday year")
203-
refresh_schedule = "0 0 * * * *" # Every hour (default)
204-
# refresh_schedule = "0 */30 * * * *" # Every 30 minutes
205-
# refresh_schedule = "" # Disabled
212+
json = false
206213

207214
[delay_policy]
208215
enabled = false # Enable quarantine system
209216
default_delay_days = 3 # Default quarantine period
210217
skip_weekends = true # Don't release on weekends
211-
release_hour_utc = 10 # Hour to release (0-23)
218+
business_hours_only = true # Only release during business hours
219+
release_hour_utc = 9 # Hour to release (0-23)
212220
```
213221

214222
### Storage Architecture
215223

216-
Vein uses a **dual-database architecture** for optimal performance:
224+
Vein uses a **database + filesystem** architecture for optimal performance:
217225

218-
#### SQLite (`vein.db`) - Persistent Metadata Store
219-
**Purpose**: Authoritative source of truth for all cached gems
226+
#### SQLite (`vein.db`) or PostgreSQL - Persistent Metadata Store
227+
**Purpose**: Authoritative source of truth for all cached packages
220228

221229
**Stores**:
222-
- Full gem metadata (name, version, platform)
230+
- Full package metadata (name, version, platform)
223231
- Filesystem paths
224232
- SHA256 checksums
225233
- File sizes
226234
- Last accessed timestamps
227235

228236
**When Used**:
229237
- On cache misses to verify if gem needs fetching
230-
- On gem cache to store metadata
238+
- On cache writes to store metadata
231239

232240
## Development
233241

@@ -259,8 +267,7 @@ docker pull ghcr.io/contriboss/vein:latest
259267
docker run -d \
260268
--name vein \
261269
-p 8346:8346 \
262-
-v vein-gems:/data/gems \
263-
-v vein-db:/data/db \
270+
-v vein-data:/data \
264271
-e RUST_LOG=info \
265272
ghcr.io/contriboss/vein:latest
266273

@@ -294,7 +301,7 @@ docker compose down
294301
- Health checks for both services
295302
- Preconfigured networking
296303

297-
Point your Bundler at `http://localhost:8346` and you're done.
304+
Point Bundler, Cargo, or npm at `http://localhost:8346` and you're done.
298305

299306
### Custom Configuration
300307

@@ -308,8 +315,7 @@ docker run -d \
308315
--name vein \
309316
-p 8346:8346 \
310317
-v $(pwd)/vein.toml:/data/vein.toml:ro \
311-
-v vein-gems:/data/gems \
312-
-v vein-db:/data/db \
318+
-v vein-data:/data \
313319
vein:latest serve --config /data/vein.toml
314320
```
315321

@@ -319,7 +325,7 @@ docker run -d \
319325

320326
```ini
321327
[Unit]
322-
Description=Vein RubyGems Proxy
328+
Description=Vein Package Proxy
323329
After=network.target
324330

325331
[Service]
@@ -341,7 +347,7 @@ upstream vein {
341347
342348
server {
343349
listen 443 ssl http2;
344-
server_name gems.company.com;
350+
server_name packages.company.com;
345351
346352
location / {
347353
proxy_pass http://vein;
@@ -354,7 +360,7 @@ server {
354360
## Relationship to ore-light
355361

356362
- **ore-light**: Go-based Bundler alternative (client-side gem management)
357-
- **Vein**: RubyGems proxy/server (server-side gem hosting)
363+
- **Vein**: Multi-ecosystem proxy/server (server-side package hosting)
358364

359365
Together they provide a complete, modern Ruby dependency management ecosystem:
360366
- ore-light handles dependency resolution and installation
@@ -380,7 +386,7 @@ Vein is built on [Rama](https://github.com/plabayo/rama), a modular service fram
380386

381387
**Project Status**: Vein is a **side project** and will remain free and open source. It is not commercialized.
382388

383-
**HTTP Features**: Intentionally basic. Vein does what it needs to do: proxy, cache, serve gems. No plans to add complex HTTP features or enterprise-grade capabilities.
389+
**HTTP Features**: Intentionally basic. Vein does what it needs to do: proxy, cache, serve packages. No plans to add complex HTTP features or enterprise-grade capabilities.
384390

385391
**Need More?** Companies requiring additional features (advanced routing, auth, monitoring, protocol extensions) should **hire Plabayo directly** to extend Vein:
386392
- Extensions can be public (contributed upstream) or private (internal forks)

crates/vein-adapter/src/cache/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub enum AssetKind {
66
Gem,
77
Spec,
88
Crate,
9+
NpmPackage,
910
}
1011

1112
impl AssetKind {
@@ -14,6 +15,7 @@ impl AssetKind {
1415
AssetKind::Gem => "gem",
1516
AssetKind::Spec => "gemspec",
1617
AssetKind::Crate => "crate",
18+
AssetKind::NpmPackage => "npm",
1719
}
1820
}
1921
}

docker-compose.yml

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ services:
2929
environment:
3030
RUST_LOG: ${RUST_LOG:-info}
3131
volumes:
32-
- vein-gems:/data/gems
32+
- vein-data:/data
3333
- ./vein.docker.toml:/data/vein.toml:ro
3434
healthcheck:
3535
test: ["CMD", "curl", "-fsS", "http://localhost:8346/up"]
@@ -41,8 +41,27 @@ services:
4141
- vein-network
4242
restart: unless-stopped
4343

44+
test:
45+
build:
46+
context: .
47+
dockerfile: Dockerfile.test
48+
container_name: vein-test
49+
depends_on:
50+
vein:
51+
condition: service_healthy
52+
environment:
53+
# Point package managers to vein proxy
54+
GEM_HOST: http://vein:8346
55+
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
56+
CARGO_REGISTRIES_CRATES_IO_INDEX: http://vein:8346/index/
57+
NPM_CONFIG_REGISTRY: http://vein:8346/
58+
networks:
59+
- vein-network
60+
stdin_open: true
61+
tty: true
62+
4463
volumes:
45-
vein-gems:
64+
vein-data:
4665
driver: local
4766
vein-pgdata:
4867
driver: local

src/config/storage.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ impl Default for StorageConfig {
2929
}
3030

3131
fn default_storage_path() -> PathBuf {
32-
PathBuf::from("./gems")
32+
PathBuf::from("./cache")
3333
}

src/config/tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ fn test_default_config() {
1313
assert_eq!(config.server.port, 8346);
1414
assert_eq!(config.server.workers, num_cpus::get());
1515
assert!(config.upstream.is_none());
16-
assert_eq!(config.storage.path, PathBuf::from("./gems"));
16+
assert_eq!(config.storage.path, PathBuf::from("./cache"));
1717
#[cfg(feature = "sqlite")]
1818
assert_eq!(config.database.path, PathBuf::from("./vein.db"));
1919
assert!(config.database.url.is_none());
@@ -38,7 +38,7 @@ fn test_default_upstream_config() {
3838
#[test]
3939
fn test_default_storage_config() {
4040
let storage = StorageConfig::default();
41-
assert_eq!(storage.path, PathBuf::from("./gems"));
41+
assert_eq!(storage.path, PathBuf::from("./cache"));
4242
}
4343

4444
#[test]

src/crates/handlers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use rama::http::{
1111
use rama::Service;
1212
use vein_adapter::{CacheBackend, FilesystemStorage};
1313

14-
use crate::http_cache::{fetch_cached_text, CachedFetchResult, CacheOutcome, MetaStoreMode};
14+
use crate::http_cache::{fetch_cached_text, CacheOutcome, MetaStoreMode};
1515
use super::types::{index_path, IndexConfig};
1616

1717
const UA: &str = concat!("vein/", env!("CARGO_PKG_VERSION"));

src/http_cache.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub enum MetaStoreMode {
3434

3535
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3636
pub enum CacheOutcome {
37+
Hit,
3738
Miss,
3839
Revalidated,
3940
Pass,

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub mod crates;
1313
pub mod db;
1414
pub mod gem_metadata;
1515
pub mod http_cache;
16+
pub mod npm;
1617
pub mod proxy;
1718
pub mod quarantine;
1819
pub mod upstream;

0 commit comments

Comments
 (0)