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#
| Registry | Status |
|---|---|
| npm | Served, mirrored, and published |
| PyPI | Served |
| RubyGems | Planned |
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
| URL | Description |
|---|---|
https://registry.ecluse.example | The 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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
package | path | yes | string | The package name, URL-encoded; a scoped name is @scope%2Fname. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
501 | Not implemented: dist-tags are not supported. | application/json | object |
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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
package | path | yes | string | The package name, URL-encoded; a scoped name is @scope%2Fname. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
501 | Not 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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
package | path | yes | string | The package name, URL-encoded; a scoped name is @scope%2Fname. |
tag | path | yes | string | The dist-tag name, e.g. latest. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
501 | Not implemented: dist-tags are not supported. | application/json | object |
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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
package | path | yes | string | The package name, URL-encoded; a scoped name is @scope%2Fname. |
tag | path | yes | string | The dist-tag name, e.g. latest. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
501 | Not implemented: dist-tags are not supported. | application/json | object |
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
| Status | Description | Media type | Schema |
|---|---|---|---|
200 | An empty object. | application/json | any |
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
| Status | Description | Media type | Schema |
|---|---|---|---|
200 | An empty object. | - | - |
GET /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
| Status | Description | Media type | Schema |
|---|---|---|---|
501 | Not implemented: search is not supported. | application/json | object |
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
| Status | Description | Media type | Schema |
|---|---|---|---|
501 | Not 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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
package | path | yes | string | The package name, URL-encoded; a scoped name is @scope%2Fname. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
200 | The synthesized packument. | application/json | SynthesizedPackument |
304 | The client's validator matched the synthesized packument. | - | - |
401 | Edge authentication failed. | application/json | object |
403 | Private access was refused, or no version survived policy and admission. | application/json | object |
404 | A first-party name the private upstream does not have. It is never fetched from the public upstream. | application/json | object |
500 | A permanent or internal inability to decide. | application/json | object |
502 | A responding upstream returned a packument for a different package. | application/json | object |
503 | A transient upstream or advisory condition; retry (see Retry-After). | application/json | object |
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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
package | path | yes | string | The 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 type | Schema |
|---|---|
application/json | PublishDocument |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
default | The 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/json | string (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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
package | path | yes | string | The package name, URL-encoded; a scoped name is @scope%2Fname. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
200 | The synthesized packument. | - | - |
304 | The client's validator matched the synthesized packument. | - | - |
401 | Edge authentication failed. | - | - |
403 | Private access was refused, or no version survived policy and admission. | - | - |
404 | A first-party name the private upstream does not have. It is never fetched from the public upstream. | - | - |
500 | A permanent or internal inability to decide. | - | - |
502 | A responding upstream returned a packument for a different package. | - | - |
503 | A 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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
package | path | yes | string | The package name, URL-encoded; a scoped name is @scope%2Fname. |
filename | path | yes | string | The artifact's on-the-wire file name, e.g. lodash-4.17.21.tgz. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
default | An 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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
package | path | yes | string | The package name, URL-encoded; a scoped name is @scope%2Fname. |
filename | path | yes | string | The artifact's on-the-wire file name, e.g. lodash-4.17.21.tgz. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
default | An 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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
unsupportedPath | path | yes | string | Any path under this mount matched by none of the routes above. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
404 | Unrecognised path; deny by default. | application/json | object |
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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
unsupportedPath | path | yes | string | Any path under this mount matched by none of the routes above. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
404 | Unrecognised 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
| Status | Description | Media type | Schema |
|---|---|---|---|
405 | Publishing is not enabled on this mount. The body is empty unless server.helpMessage is configured. | text/plain | string |
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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
project | path | yes | string | The project name in PEP 503 canonical form, e.g. zope-interface. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
200 | The filtered Simple index. | application/vnd.pypi.simple.v1+json | PyPISimpleIndex |
304 | The client's validator matched the assembled index. | - | - |
401 | Edge authentication failed. The body is empty unless server.helpMessage is configured. | text/plain | string |
403 | Private access was refused, or no release survived policy and admission. The body is empty unless server.helpMessage is configured. | text/plain | string |
404 | A 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/plain | string |
406 | The 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/plain | string |
500 | A permanent or internal inability to decide. The body is empty unless server.helpMessage is configured. | text/plain | string |
502 | A responding upstream returned an index for a different project. The body is empty unless server.helpMessage is configured. | text/plain | string |
503 | A transient upstream or advisory condition; retry (see Retry-After). The body is empty unless server.helpMessage is configured. | text/plain | string |
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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
project | path | yes | string | The project name in PEP 503 canonical form, e.g. zope-interface. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
200 | The filtered Simple index. | - | - |
304 | The client's validator matched the assembled index. | - | - |
401 | Edge authentication failed. The body is empty unless server.helpMessage is configured. | - | - |
403 | Private access was refused, or no release survived policy and admission. The body is empty unless server.helpMessage is configured. | - | - |
404 | A 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. | - | - |
406 | The 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. | - | - |
500 | A permanent or internal inability to decide. The body is empty unless server.helpMessage is configured. | - | - |
502 | A responding upstream returned an index for a different project. The body is empty unless server.helpMessage is configured. | - | - |
503 | A 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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
project | path | yes | string | The project name in PEP 503 canonical form, e.g. zope-interface. |
file | path | yes | string | The distribution file's on-the-wire name, e.g. requests-2.34.2-py3-none-any.whl. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
default | An 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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
project | path | yes | string | The project name in PEP 503 canonical form, e.g. zope-interface. |
file | path | yes | string | The distribution file's on-the-wire name, e.g. requests-2.34.2-py3-none-any.whl. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
default | An 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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
unsupportedPath | path | yes | string | Any path under this mount matched by none of the routes above. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
404 | Unrecognised path; deny by default. The body is empty unless server.helpMessage is configured. | text/plain | string |
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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
unsupportedPath | path | yes | string | Any path under this mount matched by none of the routes above. |
Responses
| Status | Description | Media type | Schema |
|---|---|---|---|
404 | Unrecognised 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.
| Property | Type | Required | Description |
|---|---|---|---|
files | array of object | yes | The 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. |
meta | object | no | The index's own metadata, relayed from the contributing upstream so _last-serial still lets a mirror revalidate cheaply. |
name | string | yes | The project name in PEP 503 canonical form. |
versions | array of string | no | The 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).
| Property | Type | Required | Description |
|---|---|---|---|
dist-tags | object | no | Tag to version string. latest is repointed to the newest surviving version after the gate. |
name | string | yes | The package name. |
time | object | no | Publish timestamps: created, modified, and one entry per version. |
versions | object | yes | Surviving 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:
| Use | Fields |
|---|---|
| Identity and policy | name, version, dist, deprecated, hasInstallScript, scripts, license, _npmUser |
| Dependency resolution | dependencies, dependenciesMeta, acceptDependencies, devDependencies, optionalDependencies, peerDependencies, peerDependenciesMeta, bundleDependencies, bundledDependencies |
| Runtime and type resolution | main, module, browser, exports, imports, type, types, typings, typesVersions, sideEffects |
| Installation and platforms | engines, engineStrict, os, cpu, libc, bin, man, directories, gypfile, preferGlobal, _hasShrinkwrap |
| Package configuration | files, 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.