What Écluse is

Écluse is a proxy you put in front of public package registries to protect the builds that install from them. Point your CI and developer tooling at Écluse instead of at a public registry. Écluse fetches from that registry on their behalf and decides which versions a build may install. npm supports reads, mirroring, and first-party publication. PyPI supports project-index and distribution reads, but not mirroring or publication.

A new public version waits in a quarantine, seven days by default, before a build can install it. Most malicious publishes are found and pulled within days, so the wait alone sidesteps them, with no attempt to detect malice. A security patch overrides the quarantine as soon as the advisory data is available: a version that a synced advisory names as the exact fix for a vulnerability skips the wait. A fix still waits when no advisory database has synced, when the advisory data is too old, when another advisory still affects the version, or when the advisory spells the version differently. Pin such a fix with AllowByIdentity. Everything else is deny by default and opt-in by name.

If you run a private registry, Écluse reads it first and passes your own packages through untouched. Any https registry that speaks the ecosystem's protocol serves in that role. Écluse can also mirror each admitted public version into a registry you nominate, so a mirrored version survives a public outage or yank. You declare each registry under a tag that names the store behind it. A mirror target under the codeArtifact tag mints its own short-lived write token, and the other tags take a static token you supply. Écluse hosts no packages itself.

Écluse ships as one container image with four roles:

Deploying Écluse covers all four.

How it works#

The registry roles#

Écluse sees registries by role, and each role is a URL on a mount. Each ecosystem gets its own path prefix: /npm/ or /pypi/.

RoleWhat it doesTrustRequired?
Public upstreamThe registry Écluse gatesSubject to admissionDefaults to registry.npmjs.org for npm and pypi.org for PyPI
Private upstreamServes your own packages firstTrustedNo. Without it, a pure public gate
Mirror targetReceives admitted npm versions and answers presence probesOperator-controlledNo. Declaring it makes the npm mount a mirror
Publication targetReceives your publish requestsTrustedOpt-in

These are roles, not necessarily separate servers. The recommended topology gives each one its own store and explains what you lose when two share one.

A request, step by step#

For npm, a client requests a version listing and then a tarball. PyPI reads use project indexes and distribution files instead, with no mirror or publication step.

npm clientÉclusePrivate upstream(trusted)Public registry(gated by policy)QueueMirror target listing,then tarball merged listing,admitted stream parallel fetch parallel fetch mirror job write
  1. The listing. Écluse fetches the private and the public registry in parallel. It trusts every private version that meets the trusted integrity floor, gates every public version through the policy, and serves the merged listing. A public version the policy did not admit is absent from it, so a resolver never picks it. The served dist-tags.latest follows the public registry's own tag when that version is admitted. Otherwise it names the highest admitted version, stable before prerelease. A private registry's or a mirror store's own latest tag is never served.
  2. The tarball. A private hit streams through unfiltered, and a private miss is gated on its public metadata. When admitted, Écluse streams the tarball from the public registry while a mirror job copies it into the mirror target in the background. Mirroring is demand-driven, so only a version a client pulls gets mirrored.
  3. A publish. Publishing stays off until you configure a publication target. With one, Écluse refuses any name outside the mount's first-party namespaces before it writes upstream.

Private versions do not re-enter public admission. Private metadata passes the trusted integrity floor, but a conventional private npm artifact hit bypasses that metadata path. Those reads rely on the client's integrity checks. A public/private digest conflict logs a WARNING with both digests and increments the divergence counter. Comparison uses complete digest sets for each shared filename and algorithm. Hexadecimal case does not affect agreement. Diagnostics keep the original digest spelling. The private version and its tags stay listed, and direct private artifact reads still work. The alarm remains when a public rule denies the conflicting copy. The denied public version stays excluded. Only metadata that passes identity, artifact-location, and integrity checks supplies conflict evidence. A disagreement proves neither copy correct. A damaged private copy can still be served because the private repository is trusted to hold previously admitted or first-party content. Divergence alone never authorises Dredger to delete a version. Removing an allow does not revoke a mirrored version unless a named deny that applies at revocation becomes decisive. See Revoking a mirrored version.

Listings omit files whose URLs lack a filename, name . or .., or have a backslash or control character in the filename. The check also applies after one percent decode and refuses encoded separators or invalid UTF-8. A release disappears when no file survives.

The mirror write republishes the version object the public registry served when the worker admitted the version, so an install from the mirror resolves the same dependencies, executable mappings, engines, scripts, and deprecation notice as an install from the public registry. The worker reads that object at admission time, never from the queued job.

Écluse never edits a version the mirror already holds, so a copy that an earlier release mirrored with the shorter manifest keeps it. To repair one, delete that version from the mirror target with the store's own tooling (DeletePackageVersions on CodeArtifact, an unpublish on Verdaccio), then request the version through the proxy again. The next tarball request enqueues a fresh mirror job, which republishes the version with its full metadata. A separate private cache that retained the old copy keeps serving it until you remove that copy too.

Each mirror write sets dist-tags.latest on the mirror target. Écluse reads the public registry's own latest and the versions the mirror holds after the write, then names the public target when the mirror holds it. Otherwise it names the highest stable version present, or the highest prerelease when no stable version is present. Mirroring one version never makes that version latest by itself, so back-filling an old version does not change what an unqualified install resolves. Écluse owns latest on every public package it mirrors and overwrites a value it did not set. That stored tag governs a client pointed straight at the mirror, never the listing Écluse serves. First-party names never reach the mirror worker, and their release tags are untouched.

A mirror job whose target metadata cannot be read is not published, because the tag cannot be chosen without the current inventory. It follows the worker's usual fault handling instead, which retries a transport failure and retires a fault no redelivery can clear. Two workers mirroring the same package at the same instant still race: an npm publish carries no compare-and-set, so the last write wins. The next job that publishes a new version of that package corrects the tag.

Before publication, the mirror worker verifies fetched bytes against the current admitted metadata. For npm dist.integrity, any matching SRI alternative at the strongest algorithm permits publication, regardless of token order. A matching weaker digest cannot rescue a stronger-algorithm mismatch. This alternative rule applies only to SRI components.

The policy#

The policy is deny by default: a public version reaches a build only when a rule admits it. Rules run in precedence order and the first decisive one wins. The revoke and the install-time deny sit above every allow by default.

RuleBandOn by defaultWhat it decides
min-ageAllowYesAdmits a public version older than seven days: the quarantine
remediation-fast-trackAllowYesAdmits a version a synced advisory names as the exact fix for a vulnerability, as long as no other advisory still affects it
AllowScopeAllowNoAllow-lists every package under a scope you name
AllowByIdentityAllowNoPins a package or a package@version by exact name
DenyByIdentityDenyNoRevokes a package or version
DenyInstallTimeExecutionDenynpm onlydeny-scripts denies npm install scripts at admission, leaving stored copies in place
DenyIfCveDenyNoDenies versions with a known vulnerability above a severity you choose
DenyIfEpssDenyNoDenies versions with a known vulnerability whose exploitation probability (EPSS) is at or above a threshold you choose

remediation-fast-track abstains until a first advisory database syncs, so without one only the quarantine supplies the shared allow. The npm mount also denies install scripts by default. Disable deny-scripts under mounts.npm.rules, or add an AllowByIdentity pin with precedence above 300. The off rules opt in by name, and Rule policy has their knobs.

Independent of the rules, Écluse serves a public file only if it carries a digest that meets the public integrity floor, sha256 by default. The check runs per file, so a release that ships several, as a Python release ships an sdist beside its wheels, keeps the files that clear the floor and loses the ones that do not. A release disappears only when no file of it survives. On a custom or off-spec public upstream this has a gotcha: files without such a digest silently disappear, and downloading one 403s. To serve such a source, give it the private upstream role and loosen the trusted floor below sha256. The mechanics are in Integrity floors.