- Deduplicate proxy_fetch/proxy_fetch_text into generic proxy_fetch_core
with response extractor closure (removes ~50 lines of copy-paste)
- GC now scans all registry prefixes, not just docker/
- Add tracing::warn to fire-and-forget cache writes in docker proxy
- Mark S3 credentials as skip_serializing to prevent accidental leaks
- Remove TOCTOU race in LocalStorage get/delete (redundant exists check)
Three subsystems were using std::fs (blocking) inside async context,
which stalls the tokio runtime thread during I/O:
- DashboardMetrics::save(): now uses tokio::fs::write + rename
- TokenStore::flush_last_used(): now uses tokio::fs for batch updates
- AuditLog::log(): moved file write to spawn_blocking (fire-and-forget)
The background task and shutdown handler now properly .await the
async save/flush methods. AuditLog writer wrapped in Arc for
cross-thread access from spawn_blocking.
Upload sessions were stored in a global LazyLock<RwLock<HashMap>>,
making them impossible to test in isolation and invisible to other
parts of the system. Multi-instance deployments would also lose
sessions started on a different node.
Changes:
- Move upload_sessions into AppState as Arc<RwLock<HashMap>>
- Add State extractor to start_upload, patch_blob and their _ns wrappers
- Expire sessions in the existing 30s background task (alongside metrics)
- Make UploadSession and cleanup_expired_sessions pub for AppState access
Token verification previously ran Argon2id + disk read on every
authenticated request. Under load this becomes the bottleneck
(~100ms per Argon2 verify on a single core).
Changes:
- Add in-memory cache (SHA256 -> user/role/expiry) with 5 minute TTL
- Defer last_used timestamp writes to batch flush every 30 seconds
- Invalidate cache entry on token revoke
- Background task flushes pending last_used alongside metrics persist
First verify_token call per token: full Argon2 + disk (unchanged).
Subsequent calls within TTL: HashMap lookup only (sub-microsecond).
* docs: add Go module proxy to README, update dashboard GIF
- Add Go Modules to supported registries table
- Add Go usage example (GOPROXY)
- Add Go config.toml example
- Add /go/ endpoint to endpoints table
- Update dashboard GIF with 6 registry cards in one row
- Fix registries count: 6 package registries
* feat(ui): add Raw storage to dashboard, sidebar, and list pages
- Raw Storage card on dashboard with file count and size
- Raw in sidebar navigation with file icon
- Raw list and detail pages (/ui/raw)
- Raw mount point in mount points table
- Grid updated to 7 columns for all registry cards
- README: 7 registries, add Go module proxy docs
* docs: add product badges (release, image size, downloads)
* feat: add Go module proxy (GOPROXY protocol) (#47)
Implements caching proxy for Go modules with 5 standard endpoints:
- GET /go/{module}/@v/list — list versions
- GET /go/{module}/@v/{version}.info — version metadata
- GET /go/{module}/@v/{version}.mod — go.mod file
- GET /go/{module}/@v/{version}.zip — module zip
- GET /go/{module}/@latest — latest version info
Features:
- Module path encoding/decoding per Go spec (!x → X)
- Immutable caching (.info/.mod/.zip never overwritten)
- Mutable endpoints (@v/list, @latest) refreshed from upstream
- Configurable upstream (default: proxy.golang.org)
- Separate timeout for .zip downloads (default: 120s)
- Size limit for zips (default: 100MB)
- Path traversal protection
- Dashboard integration (stats, mount points, index)
- 25 unit tests (encoding, path splitting, safety, content-type)
Closes#47
* style: cargo fmt
* feat(ui): add Go pages, compact cards, fix icons
- Go in sidebar + list/detail pages with go get command
- Dashboard: fix fallback icon (was Docker whale for Go)
- Compact registry cards: lg:grid-cols-6, all 6 in one row
- Cargo icon: crate boxes instead of truck
- Go icon: stylized Go text (sidebar + dashboard)
* fix(go): URL-decode path + send encoded paths to upstream
Go client sends %21 for ! in module paths. Axum wildcard does not
auto-decode, so we percent-decode manually. Upstream proxy.golang.org
expects encoded paths (with !), not decoded uppercase.
Tested: full Pusk build (22 modules, 135MB cached) including
SherClockHolmes/webpush-go with triple uppercase encoding.
* style: cargo fmt
* docs: add DCO, governance model, roles, vulnerability credit policy
* security: migrate token hashing from SHA256 to Argon2id
- Replace unsalted SHA256 with Argon2id (salted) for API token hashing
- Fix TOCTOU race: replace exists()+read() with read()+match on error
- Set chmod 600 on token files and 700 on token storage directory
- Auto-migrate legacy SHA256 tokens to Argon2id on first verification
- Add regression tests: argon2 format, legacy migration, file permissions
* feat: add retry with timeout for upstream proxy, mark Maven proxy-only
- Add shared proxy_fetch() and proxy_fetch_text() with 1 retry on 5xx/timeout
- Replace duplicated fetch_from_proxy in maven.rs, npm.rs, pypi.rs
- Mark Maven as proxy-only in README (no full repository manager support)
- Existing timeout config (30s maven/npm/pypi, 60s docker) preserved
- 4xx errors fail immediately without retry
* docs: add DCO, governance model, roles, vulnerability credit policy
* security: migrate token hashing from SHA256 to Argon2id
- Replace unsalted SHA256 with Argon2id (salted) for API token hashing
- Fix TOCTOU race: replace exists()+read() with read()+match on error
- Set chmod 600 on token files and 700 on token storage directory
- Auto-migrate legacy SHA256 tokens to Argon2id on first verification
- Add regression tests: argon2 format, legacy migration, file permissions
When auth is enabled with anonymous_read=true, GET/HEAD requests
are allowed without credentials (pull/download), while write
operations (PUT/POST/DELETE/PATCH) still require authentication.
Use case: public demo registries, read-only mirrors.
Config: NORA_AUTH_ANONYMOUS_READ=true or auth.anonymous_read=true
Add px-4 to all td cells in Mount Points and Activity tables
to match th header padding. Remove non-functional px-4 from
tbody elements (CSS padding does not apply to tbody).
- Remove nora-cli and nora-storage from workspace (stub crates, not used)
- Remove root install.sh (duplicate of dist/install.sh)
- Remove root logo.jpg (duplicate of ui/logo.jpg)
- Remove committed SBOM .cdx.json files (generated by CI in release)
- Remove stale .githooks/ (real hook is in .git/hooks/)
- Update version in docs-ru to 0.2.32
- Add *.cdx.json to .gitignore
Docker dashboard:
- build_docker_index now finds manifests segment by position, not fixed index
- Correctly indexes library/alpine, grafana/grafana, and other namespaced images
Docker proxy:
- Auto-prepend library/ for single-segment names when upstream returns 404
- Applies to both manifests and blobs
- nginx, alpine, node now work without explicit library/ prefix
- Cached under original name for future local hits
npm proxy:
- Rewrite tarball URLs in metadata to point to NORA (was broken — tarballs bypassed NORA)
- Scoped packages (@scope/package) full support in handler and repo index
- Metadata cache TTL (NORA_NPM_METADATA_TTL, default 300s) with stale-while-revalidate
- proxy_auth now wired into fetch_from_proxy (was configured but unused)
npm publish:
- PUT /npm/{package} — accepts standard npm publish payload
- Version immutability — 409 Conflict on duplicate version
- Tarball URL rewriting in published metadata
Security:
- SHA256 integrity verification on cached tarballs (immutable cache)
- Attachment filename validation (path traversal protection)
- Package name mismatch detection (URL vs payload)
Config:
- npm.metadata_ttl — configurable cache TTL (env: NORA_NPM_METADATA_TTL)
- basic_auth_header() in config.rs replaces 6 inline STANDARD.encode calls
- warn_plaintext_credentials() logs warning at startup if auth is in config.toml
- All protocol handlers use shared helper instead of duplicating base64 logic
Metrics (downloads, uploads, cache hits) were stored in-memory only
and reset to zero on every restart. Now they persist to metrics.json
in the storage directory with:
- Load on startup from {storage_path}/metrics.json
- Background save every 30 seconds
- Final save on graceful shutdown
- Atomic writes (tmp + rename) to prevent corruption
Artifact count on dashboard now shows total tags/versions across
all registries instead of just counting unique repository names.
This matches user expectations when pushing multiple tags to the
same image (e.g. myapp:v1, myapp:v2 now shows 2, not 1).
Distinguish OCI vs Docker manifests by checking config.mediaType
instead of assuming all schemaVersion 2 manifests are Docker.
Enables helm push/pull via OCI protocol.
DevITWay
- Add Role enum to tokens: Read, Write, Admin (default: Read)
- Enforce role-based access in auth middleware (read-only tokens blocked from PUT/POST/DELETE)
- Add role field to token create/list/verify API
- Add persistent audit log (append-only JSONL) for all registry operations
- Audit logging across all registries: docker, npm, maven, pypi, cargo, raw
DevITWay
- Add enabled field to RateLimitConfig (default: true, env: NORA_RATE_LIMIT_ENABLED)
- Skip rate limiter layers entirely when disabled
- Replace PeerIpKeyExtractor with SmartIpKeyExtractor for upload/general routes
to correctly identify clients behind reverse proxies and Docker bridge networks
- Keep PeerIpKeyExtractor for auth routes (stricter brute-force protection)
Root cause: PeerIpKeyExtractor saw all Docker bridge traffic as single IP (172.17.0.1),
exhausting GCRA bucket for all clients simultaneously. With burst=1M, recovery time
reached 84000+ seconds.
Add http_client field to AppState, initialized once at startup.
Replace per-request Client::builder() calls in npm, maven, pypi,
and docker registry handlers with the shared instance.
This reuses the connection pool across requests instead of
creating a new client on every proxy fetch.
Bump version to 0.2.20.