npm
NORA provides a full npm-compatible registry. Publish private packages and transparently proxy public packages from the official npm registry.
Client Configuration
Section titled “Client Configuration”Point npm (or yarn/pnpm) at NORA:
# npmnpm config set registry http://nora.example.com:4000/npm/
# yarnyarn config set registry http://nora.example.com:4000/npm/
# pnpmpnpm config set registry http://nora.example.com:4000/npm/Publish a package:
npm publish --registry http://nora.example.com:4000/npm/npm publishes only with a token for the registry: any value while authentication is off, an API token when it is on:
npm config set //nora.example.com:4000/npm/:_authToken=<token>Per-project .npmrc:
registry=http://nora.example.com:4000/npm/Upstream Proxy
Section titled “Upstream Proxy”When a package is not found locally, NORA fetches it from the upstream registry. Tarball URLs in metadata are rewritten to point through NORA.
config.toml:
[npm]enabled = trueproxy = "https://registry.npmjs.org"proxy_timeout = 30metadata_ttl = 300 # secondsEnvironment variables:
| Variable | Description | Default |
|---|---|---|
NORA_NPM_ENABLED | Enable npm registry | true |
NORA_NPM_PROXY | Upstream registry URL | https://registry.npmjs.org |
NORA_NPM_PROXY_AUTH | Upstream auth (user:pass) | — |
NORA_NPM_PROXY_TIMEOUT | Upstream timeout in seconds | 30 |
NORA_NPM_METADATA_TTL | Metadata cache TTL in seconds | 300 |
Published Packages and the Upstream
Section titled “Published Packages and the Upstream”From 1.4.0 a package published to NORA owns its name: its packument, version manifests and dist-tags (/<name>, /<name>/<version>, /<name>/<dist-tag>, -/package/<name>/dist-tags) are answered from its own keys, whatever metadata_ttl says, and never ask an upstream. What an upstream serves for a package NORA has not published is cached separately, under npm/<name>/upstream/. npm search and other metadata paths are answered by the upstream and not cached.
Several Upstreams by Scope
Section titled “Several Upstreams by Scope”Use proxies instead of proxy when some scopes live on a private registry (GitLab, GitHub Packages, Verdaccio, a vendor’s registry) and everything else comes from the public one:
[npm]proxies = [ "https://registry.npmjs.org", { url = "https://npm.vendor.example", scopes = ["@vendor"], auth_bearer_env = "VENDOR_NPM_TOKEN" }, { url = "https://gitlab.example.com/api/v4/packages/npm", scopes = ["@team", "@infra"], auth = "deploy:secret" },]- A package in a listed scope (
@vendor/a) goes only to the entry that owns the scope, for metadata, tarballs and revalidation. If that upstream is down, the client gets502: NORA never asks another upstream for it. This is what keeps a private name off the public registry. - Every other package goes to the entry without
scopes. At most one entry may omitscopes. If none does, packages outside the listed scopes are served from local storage only. - Each entry carries its own credential:
auth = "user:pass"(Basic) orauth_bearer_env = "VAR"(Bearer token read from that environment variable at startup; the token is never written to config or logs). Use one or the other. - With
proxiesset,proxyandproxy_authare ignored (startup logs a warning forproxy_auth).
NORA refuses to start (exit code 78) when an entry has a URL without http:///https://, a scope that is not @name, a scope owned by two entries, both auth and auth_bearer_env, or an auth_bearer_env variable that is unset or holds a value an HTTP header cannot carry (for example, a trailing newline).
Cache provenance. NORA records which upstream delivered each cached object, together with the SHA-256 of those bytes. In an owned scope, a cached copy is served only if the owner delivered it and the bytes are unchanged, or if it was published to NORA. A copy cached earlier from another upstream — for example, a public package with the same name cached before the scope got its own entry — is fetched again from the owner. It is never served from cache or as a stale fallback. The records live under .nora-origins/ in the storage root; nora backup, nora restore and nora migrate carry them, and nora gc removes records whose object is gone.
NORA keeps no advisory database. npm audit, yarn npm audit and pnpm audit are forwarded to the upstream that serves unscoped packages, and its answer is returned as is.
When proxies owns scopes, or a curation internal namespace is set, NORA removes those package names from the request first. It can do that only for the npm 7+ bulk format (/-/npm/v1/security/advisories/bulk, plain or gzip). A request it cannot check is refused with a 4xx status and a JSON error naming the reason, never answered as “no vulnerabilities”:
| Request | Status |
|---|---|
npm 6 audits/quick or pnpm audits (whole dependency tree) | 422 |
| Body larger than 8 MB, compressed or after decompression | 413 |
| Encoding other than gzip | 415 |
| gzip that does not decompress, or a body that is not the expected JSON | 422 |
A registry with no upstream for unscoped packages answers audits with 200 {}, which clients report as no advisories.
Features
Section titled “Features”| Feature | Status | Notes |
|---|---|---|
| Package metadata (GET) | Full | JSON with all versions |
Scoped packages (@scope/name) | Full | URL-encoded path |
| Tarball download | Full | SHA256 verified |
| Tarball URL rewriting | Full | Points to NORA, not upstream |
Publish (npm publish) | Full | Immutable versions |
| Upstream proxy | Full | Configurable TTL; several upstreams by scope |
| Unpublish | — | Not implemented |
Dist-tags (latest, next) | Partial | Read from metadata, no explicit management |
Search (/-/v1/search) | — | Not implemented |
| Audit advisories | Proxied | Forwarded upstream; owned-scope names stripped or the request refused |
Known Limitations
Section titled “Known Limitations”- No unpublish support — published versions are immutable.
- Dist-tags are read from upstream metadata but cannot be explicitly managed via NORA.
- Package search is not implemented. Audit is answered by the upstream; NORA keeps no advisory database.
- A
dist.tarballthat points to a host other than its upstream’s URL is not rewritten, so the client fetches it from that host directly. - When an upstream cannot be reached on a cold miss, NORA answers
502(before 1.3.4:404).