Protocol support

The reference below is what an Écluse server speaks: every endpoint it answers, per ecosystem, with the responses each one returns. Écluse renders the page from the OpenAPI document it publishes as JSON, so the page and the document always agree.

Supported registries#

RegistryStatus
npmServed, mirrored, and published
PyPIServed
RubyGemsPlanned

A served registry answers reads: the metadata a client resolves against and the artifact bytes it installs, both gated by the same rules, integrity floors, and egress controls.

A pypi mount serves reads and nothing else. It writes nothing, so a publicationTarget or a mirrorTarget on it refuses the boot naming the ecosystem and the key, and its upload endpoint answers 405. Mirroring and first-party publishing for PyPI land in later releases.

The age rule measures a PyPI release from its newest file upload. Every offered file must have a usable upload timestamp. If any timestamp is absent or malformed, the release age stays unknown and the age rule cannot admit it. An explicit policy exception can still admit the release, subject to the other rules and integrity floors.

PyPI indexes retain the supported installation fields: filename, URL, hashes, interpreter constraint, size, upload time, yank status and provenance URL. The proxy also preserves the project name, API version, serial, tracking declarations, alternate locations and project status. It derives the served version list from the admitted releases.

The proxy omits unknown fields and metadata sidecar declarations while reading the index. Installers read distribution metadata from the wheel or source archive instead. Distribution bytes stay unchanged. The proxy hashes the complete upstream index for response validators, including fields omitted from the served response.

A planned registry is already a valid mounts key, but no adapter answers its routes yet, so activating one refuses the boot.

Which registry protocols this Écluse server speaks, and exactly what is and is not supported, per ecosystem. An OpenAPI spec for operators and contributors -- not a client-integration contract: registry clients hardcode the protocol and never read this document. Generated statically from the closed serve-route enumeration; it is not served.

API version: 0.1.0

Servers

URLDescription
https://registry.ecluse.exampleThe proxy's externally-reachable base URL; served artifact URLs resolve against it.

Endpoints

GET /npm/-/package/{package}/dist-tags

List a package's dist-tags (not supported)

A dist-tag is a mutable named pointer, which Écluse does not implement, so it returns 501 rather than the 404 an unrouted path takes. The merged packument already carries the reconciled dist-tags map, so a client reads a package's tags from its metadata.

Parameters

NameInRequiredTypeDescription
packagepathyesstringThe package name, URL-encoded; a scoped name is @scope%2Fname.

Responses

StatusDescriptionMedia typeSchema
501Not implemented: dist-tags are not supported.application/jsonobject

HEAD /npm/-/package/{package}/dist-tags

List a package's dist-tags (not supported)

A dist-tag is a mutable named pointer, which Écluse does not implement, so it returns 501 rather than the 404 an unrouted path takes. The merged packument already carries the reconciled dist-tags map, so a client reads a package's tags from its metadata.

Parameters

NameInRequiredTypeDescription
packagepathyesstringThe package name, URL-encoded; a scoped name is @scope%2Fname.

Responses

StatusDescriptionMedia typeSchema
501Not implemented: dist-tags are not supported.--

PUT /npm/-/package/{package}/dist-tags/{tag}

Set a package's dist-tag (not supported)

Écluse writes no mutable named pointer, so it returns 501 rather than the 404 an unrouted path takes. The publication target owns a package's tags, and a publisher sets them there.

Parameters

NameInRequiredTypeDescription
packagepathyesstringThe package name, URL-encoded; a scoped name is @scope%2Fname.
tagpathyesstringThe dist-tag name, e.g. latest.

Responses

StatusDescriptionMedia typeSchema
501Not implemented: dist-tags are not supported.application/jsonobject

DELETE /npm/-/package/{package}/dist-tags/{tag}

Remove a package's dist-tag (not supported)

Écluse holds no mutable named pointer to remove, so it returns 501 rather than the 404 an unrouted path takes. The publication target owns a package's tags, and a publisher removes them there.

Parameters

NameInRequiredTypeDescription
packagepathyesstringThe package name, URL-encoded; a scoped name is @scope%2Fname.
tagpathyesstringThe dist-tag name, e.g. latest.

Responses

StatusDescriptionMedia typeSchema
501Not implemented: dist-tags are not supported.application/jsonobject

GET /npm/-/ping

Liveness probe

Answered locally with 200 and an empty object; npm ping checks the endpoint it talks to is up, so there is no reason to round-trip upstream.

Responses

StatusDescriptionMedia typeSchema
200An empty object.application/jsonany

HEAD /npm/-/ping

Liveness probe

Answered locally with 200 and an empty object; npm ping checks the endpoint it talks to is up, so there is no reason to round-trip upstream.

Responses

StatusDescriptionMedia typeSchema
200An empty object.--

Package search (not supported)

Search is a first-class documented boundary: a discovery convenience, not an install path, so Écluse returns 501 and points to the public registry's website.

Responses

StatusDescriptionMedia typeSchema
501Not implemented: search is not supported.application/jsonobject

HEAD /npm/-/v1/search

Package search (not supported)

Search is a first-class documented boundary: a discovery convenience, not an install path, so Écluse returns 501 and points to the public registry's website.

Responses

StatusDescriptionMedia typeSchema
501Not implemented: search is not supported.--

GET /npm/{package}

Fetch a package's metadata (packument)

Returns Écluse's merged-and-filtered packument: versions merged across upstreams and gated, each dist.tarball rewritten to resolve back through this proxy. With no surviving version the status follows the most recoverable cause.

Parameters

NameInRequiredTypeDescription
packagepathyesstringThe package name, URL-encoded; a scoped name is @scope%2Fname.

Responses

StatusDescriptionMedia typeSchema
200The synthesized packument.application/jsonSynthesizedPackument
304The client's validator matched the synthesized packument.--
401Edge authentication failed.application/jsonobject
403Private access was refused, or no version survived policy and admission.application/jsonobject
404A first-party name the private upstream does not have. It is never fetched from the public upstream.application/jsonobject
500A permanent or internal inability to decide.application/jsonobject
502A responding upstream returned a packument for a different package.application/jsonobject
503A transient upstream or advisory condition; retry (see Retry-After).application/jsonobject

PUT /npm/{package}

Publish a first-party package

Relays the publish document to the configured publication target after the anti-shadowing scope guard. Écluse keys the write on the route's package name, never the document's self-reported name. The target's status and JSON-labelled bytes are relayed transparently.

Parameters

NameInRequiredTypeDescription
packagepathyesstringThe package name, URL-encoded; a scoped name is @scope%2Fname.

Request body (required)

The npm publish document (the version manifest plus the base64-encoded tarball in _attachments).

Media typeSchema
application/jsonPublishDocument

Responses

StatusDescriptionMedia typeSchema
defaultThe publication target's status and JSON-labelled response bytes are relayed. Local authentication, scope, configuration, transport, and internal failures use npm's JSON error body.application/jsonstring (binary)

HEAD /npm/{package}

Fetch a package's metadata (packument)

Returns Écluse's merged-and-filtered packument: versions merged across upstreams and gated, each dist.tarball rewritten to resolve back through this proxy. With no surviving version the status follows the most recoverable cause.

Parameters

NameInRequiredTypeDescription
packagepathyesstringThe package name, URL-encoded; a scoped name is @scope%2Fname.

Responses

StatusDescriptionMedia typeSchema
200The synthesized packument.--
304The client's validator matched the synthesized packument.--
401Edge authentication failed.--
403Private access was refused, or no version survived policy and admission.--
404A first-party name the private upstream does not have. It is never fetched from the public upstream.--
500A permanent or internal inability to decide.--
502A responding upstream returned a packument for a different package.--
503A transient upstream or advisory condition; retry (see Retry-After).--

GET /npm/{package}/-/{filename}

Stream a package artifact (tarball)

The artifact bytes are streamed verbatim with bounded memory; the client verifies the bytes against the packument's preserved integrity digest. Upstream statuses, headers, and media types are relayed transparently; locally generated refusals use npm's JSON error shape.

Parameters

NameInRequiredTypeDescription
packagepathyesstringThe package name, URL-encoded; a scoped name is @scope%2Fname.
filenamepathyesstringThe artifact's on-the-wire file name, e.g. lodash-4.17.21.tgz.

Responses

StatusDescriptionMedia typeSchema
defaultAn upstream-controlled artifact response is relayed transparently. Local authentication, policy, availability, and internal failures use npm's JSON error body under their corresponding status.*/*string (binary)

HEAD /npm/{package}/-/{filename}

Stream a package artifact (tarball)

The artifact bytes are streamed verbatim with bounded memory; the client verifies the bytes against the packument's preserved integrity digest. Upstream statuses, headers, and media types are relayed transparently; locally generated refusals use npm's JSON error shape.

Parameters

NameInRequiredTypeDescription
packagepathyesstringThe package name, URL-encoded; a scoped name is @scope%2Fname.
filenamepathyesstringThe artifact's on-the-wire file name, e.g. lodash-4.17.21.tgz.

Responses

StatusDescriptionMedia typeSchema
defaultAn upstream-controlled artifact response is relayed transparently. Local authentication, policy, availability, and internal failures use npm's JSON error body under their corresponding status.--

GET /npm/{unsupportedPath}

Deny by default (unsupported path)

Any request under this mount matched by none of the routes above is denied with 404 -- deny by default at the routing layer.

Parameters

NameInRequiredTypeDescription
unsupportedPathpathyesstringAny path under this mount matched by none of the routes above.

Responses

StatusDescriptionMedia typeSchema
404Unrecognised path; deny by default.application/jsonobject

HEAD /npm/{unsupportedPath}

Deny by default (unsupported path)

Any HEAD request under this mount matched by none of the routes above is denied with 404 and no response body.

Parameters

NameInRequiredTypeDescription
unsupportedPathpathyesstringAny path under this mount matched by none of the routes above.

Responses

StatusDescriptionMedia typeSchema
404Unrecognised path; deny by default.--

POST /pypi/legacy

Upload a first-party distribution (not supported)

A PyPI mount registers no publish capability, so an upload is answered 405 rather than relayed. The route is declared so the refusal is the documented one rather than the deny-by-default 404 an unrouted path takes.

Responses

StatusDescriptionMedia typeSchema
405Publishing is not enabled on this mount. The body is empty unless server.helpMessage is configured.text/plainstring

GET /pypi/simple/{project}

Fetch a project's Simple index

Returns Écluse's merged-and-filtered PEP 691 JSON index: files merged across upstreams and gated, each file URL rewritten to resolve back through this proxy. /simple/{project}/ and /simple/{project} both match, because the router strips a trailing empty segment. A non-canonical (non-PEP-503) project name matches no route and takes the 404, with no redirect. A client that requires HTML and admits no JSON gets 406: Écluse serves the JSON form alone.

Parameters

NameInRequiredTypeDescription
projectpathyesstringThe project name in PEP 503 canonical form, e.g. zope-interface.

Responses

StatusDescriptionMedia typeSchema
200The filtered Simple index.application/vnd.pypi.simple.v1+jsonPyPISimpleIndex
304The client's validator matched the assembled index.--
401Edge authentication failed. The body is empty unless server.helpMessage is configured.text/plainstring
403Private access was refused, or no release survived policy and admission. The body is empty unless server.helpMessage is configured.text/plainstring
404A first-party project the private upstream does not have. It is never fetched from the public upstream. The body is empty unless server.helpMessage is configured.text/plainstring
406The request requires a representation this index does not serve; only the PEP 691 JSON form exists. The body is empty unless server.helpMessage is configured.text/plainstring
500A permanent or internal inability to decide. The body is empty unless server.helpMessage is configured.text/plainstring
502A responding upstream returned an index for a different project. The body is empty unless server.helpMessage is configured.text/plainstring
503A transient upstream or advisory condition; retry (see Retry-After). The body is empty unless server.helpMessage is configured.text/plainstring

HEAD /pypi/simple/{project}

Fetch a project's Simple index

Returns Écluse's merged-and-filtered PEP 691 JSON index: files merged across upstreams and gated, each file URL rewritten to resolve back through this proxy. /simple/{project}/ and /simple/{project} both match, because the router strips a trailing empty segment. A non-canonical (non-PEP-503) project name matches no route and takes the 404, with no redirect. A client that requires HTML and admits no JSON gets 406: Écluse serves the JSON form alone.

Parameters

NameInRequiredTypeDescription
projectpathyesstringThe project name in PEP 503 canonical form, e.g. zope-interface.

Responses

StatusDescriptionMedia typeSchema
200The filtered Simple index.--
304The client's validator matched the assembled index.--
401Edge authentication failed. The body is empty unless server.helpMessage is configured.--
403Private access was refused, or no release survived policy and admission. The body is empty unless server.helpMessage is configured.--
404A first-party project the private upstream does not have. It is never fetched from the public upstream. The body is empty unless server.helpMessage is configured.--
406The request requires a representation this index does not serve; only the PEP 691 JSON form exists. The body is empty unless server.helpMessage is configured.--
500A permanent or internal inability to decide. The body is empty unless server.helpMessage is configured.--
502A responding upstream returned an index for a different project. The body is empty unless server.helpMessage is configured.--
503A transient upstream or advisory condition; retry (see Retry-After). The body is empty unless server.helpMessage is configured.--

GET /pypi/simple/{project}/{file}

Stream a distribution file (wheel or sdist)

The file's bytes are streamed verbatim with bounded memory, so the client verifies them against the sha256 the served index advertised. /simple/{project}/{file} is the one spelling every rewritten file URL agrees with. Upstream statuses, headers, and media types are relayed transparently; a locally generated refusal is a bare status.

Parameters

NameInRequiredTypeDescription
projectpathyesstringThe project name in PEP 503 canonical form, e.g. zope-interface.
filepathyesstringThe distribution file's on-the-wire name, e.g. requests-2.34.2-py3-none-any.whl.

Responses

StatusDescriptionMedia typeSchema
defaultAn upstream-controlled distribution response is relayed transparently. Local authentication, policy, availability, and internal failures answer a bare status under their own code.*/*string (binary)

HEAD /pypi/simple/{project}/{file}

Stream a distribution file (wheel or sdist)

The file's bytes are streamed verbatim with bounded memory, so the client verifies them against the sha256 the served index advertised. /simple/{project}/{file} is the one spelling every rewritten file URL agrees with. Upstream statuses, headers, and media types are relayed transparently; a locally generated refusal is a bare status.

Parameters

NameInRequiredTypeDescription
projectpathyesstringThe project name in PEP 503 canonical form, e.g. zope-interface.
filepathyesstringThe distribution file's on-the-wire name, e.g. requests-2.34.2-py3-none-any.whl.

Responses

StatusDescriptionMedia typeSchema
defaultAn upstream-controlled distribution response is relayed transparently. Local authentication, policy, availability, and internal failures answer a bare status under their own code.--

GET /pypi/{unsupportedPath}

Deny by default (unsupported path)

Any request under this mount matched by none of the routes above is denied with 404 -- deny by default at the routing layer.

Parameters

NameInRequiredTypeDescription
unsupportedPathpathyesstringAny path under this mount matched by none of the routes above.

Responses

StatusDescriptionMedia typeSchema
404Unrecognised path; deny by default. The body is empty unless server.helpMessage is configured.text/plainstring

HEAD /pypi/{unsupportedPath}

Deny by default (unsupported path)

Any HEAD request under this mount matched by none of the routes above is denied with 404 and no response body.

Parameters

NameInRequiredTypeDescription
unsupportedPathpathyesstringAny path under this mount matched by none of the routes above.

Responses

StatusDescriptionMedia typeSchema
404Unrecognised path; deny by default. The body is empty unless server.helpMessage is configured.--

Schemas

PublishDocument

The npm publish document, relayed to the publication target (its full shape is npm's, not re-specified here).

Additional properties: yes

PyPISimpleIndex

Écluse's merged-and-filtered view of a project's distribution files (PEP 691). Releases are merged across upstreams and gated (private releases trusted, public releases admitted only by policy), files that do not clear the integrity floor or that name an authority this mount does not honour are dropped, and each surviving file's url is rewritten to resolve back through this proxy. The PEP 658 core-metadata and data-dist-info-metadata keys are omitted, because Écluse serves no .metadata companion.

PropertyTypeRequiredDescription
filesarray of objectyesThe surviving distribution files. A file the integrity floor or the artifact-host policy refused does not appear, and the served listing and the download gate therefore agree file by file.
metaobjectnoThe index's own metadata, relayed from the contributing upstream so _last-serial still lets a mirror revalidate cheaply.
namestringyesThe project name in PEP 503 canonical form.
versionsarray of stringnoThe surviving releases (PEP 700), in canonical PEP 440 form. A release the gate withheld does not appear.

Additional properties: yes

SynthesizedPackument

Écluse's merged-and-filtered view of a package's metadata. Versions are merged across upstreams and gated (private versions trusted, public versions admitted only by policy), and each version's dist.tarball is rewritten to resolve back through this proxy. Only the fields Écluse reads and transforms are modelled; every other field is relayed unchanged from the contributing upstream (the private upstream wins on a collision).

PropertyTypeRequiredDescription
dist-tagsobjectnoTag to version string. latest is repointed to the newest surviving version after the gate.
namestringyesThe package name.
timeobjectnoPublish timestamps: created, modified, and one entry per version.
versionsobjectyesSurviving versions, keyed by version string.

Additional properties: yes

The raw OpenAPI document is published at /api/openapi.json.

npm metadata fields#

Écluse retains metadata needed for installation, runtime resolution, policy evaluation and mirroring. Unknown fields are skipped during extraction. Full npm view fidelity is not a supported contract. The source tarball is unchanged. Served and mirrored metadata replace author lists with a short pointer to the source registry's package document.

The version representation retains these supported fields:

UseFields
Identity and policyname, version, dist, deprecated, hasInstallScript, scripts, license, _npmUser
Dependency resolutiondependencies, dependenciesMeta, acceptDependencies, devDependencies, optionalDependencies, peerDependencies, peerDependenciesMeta, bundleDependencies, bundledDependencies
Runtime and type resolutionmain, module, browser, exports, imports, type, types, typings, typesVersions, sideEffects
Installation and platformsengines, engineStrict, os, cpu, libc, bin, man, directories, gypfile, preferGlobal, _hasShrinkwrap
Package configurationfiles, config, workspaces, packageManager, devEngines, publishConfig

The top-level document retains name, versions, time and dist-tags. Dependency names, script names, engine names, export conditions, import mappings and type mappings are protocol data, so their map entries remain available. Fixed schemas do not retain arbitrary keys: publisher records retain name, email and url, and legacy licence objects retain type and url. dependenciesMeta and peerDependenciesMeta retain each entry's optional flag and preserve its key. Yarn uses dependency optionality from registry metadata throughout the dependency tree. Other dependency metadata flags are not retained.

dist retains tarball, shasum, integrity, unpackedSize, fileCount, signatures and attestations. Signatures retain keyid and sig. Attestations retain url and provenance.predicateType. Mirror publication still replaces artifact coordinates and verified hashes, and removes signatures that belong to the source registry.

directories retains lib, bin, man, doc, example and test. devEngines retains cpu, os, libc, runtime and packageManager, with name, version and onFail in each entry. The object form of workspaces retains packages and nohoist. publishConfig retains registry, tag, access, provenance, ignore-scripts, directory, linkDirectory, executableFiles, main, module, types, typings, exports, imports, bin and browser.

The installation fields follow npm's package metadata contract. Adding support for another field requires an explicit compatibility change.

The registry metadata specification identifies acceptDependencies and _hasShrinkwrap as installation inputs. Mirroring preserves the shrinkwrap marker while removing other source-registry bookkeeping.