Skip to content

npm

NORA provides a full npm-compatible registry. Publish private packages and transparently proxy public packages from the official npm registry.

Point npm (or yarn/pnpm) at NORA:

Terminal window
# npm
npm config set registry http://nora.example.com:4000/npm/
# yarn
yarn config set registry http://nora.example.com:4000/npm/
# pnpm
pnpm config set registry http://nora.example.com:4000/npm/

Publish a package:

Terminal window
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:

Terminal window
npm config set //nora.example.com:4000/npm/:_authToken=<token>

Per-project .npmrc:

registry=http://nora.example.com:4000/npm/

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 = true
proxy = "https://registry.npmjs.org"
proxy_timeout = 30
metadata_ttl = 300 # seconds

Environment variables:

VariableDescriptionDefault
NORA_NPM_ENABLEDEnable npm registrytrue
NORA_NPM_PROXYUpstream registry URLhttps://registry.npmjs.org
NORA_NPM_PROXY_AUTHUpstream auth (user:pass)—
NORA_NPM_PROXY_TIMEOUTUpstream timeout in seconds30
NORA_NPM_METADATA_TTLMetadata cache TTL in seconds300

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.

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 gets 502: 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 omit scopes. If none does, packages outside the listed scopes are served from local storage only.
  • Each entry carries its own credential: auth = "user:pass" (Basic) or auth_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 proxies set, proxy and proxy_auth are ignored (startup logs a warning for proxy_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”:

RequestStatus
npm 6 audits/quick or pnpm audits (whole dependency tree)422
Body larger than 8 MB, compressed or after decompression413
Encoding other than gzip415
gzip that does not decompress, or a body that is not the expected JSON422

A registry with no upstream for unscoped packages answers audits with 200 {}, which clients report as no advisories.

FeatureStatusNotes
Package metadata (GET)FullJSON with all versions
Scoped packages (@scope/name)FullURL-encoded path
Tarball downloadFullSHA256 verified
Tarball URL rewritingFullPoints to NORA, not upstream
Publish (npm publish)FullImmutable versions
Upstream proxyFullConfigurable TTL; several upstreams by scope
Unpublish—Not implemented
Dist-tags (latest, next)PartialRead from metadata, no explicit management
Search (/-/v1/search)—Not implemented
Audit advisoriesProxiedForwarded upstream; owned-scope names stripped or the request refused
  • 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.tarball that 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).