ecluse:ecluse-core
Safe HaskellNone
LanguageGHC2021

Ecluse.Core.Registry.Npm.Route

Description

npm's route table: the list of routes an npm mount serves.

Each entry is one Route record, carrying its method condition, its path template, what to do when it matches, its prose, and the ResponseContract that admits every response it can emit. npmRouter folds the list into the mount's router (first match wins; no match is the deny-by-default 404) and npmRouteSpecs projects the same list for the capability manifest, so the routed surface, the emitted responses, and the documented ones are all readings of one declaration.

Each response body is a codec (npmErrorCodec for a denial) or a named hand-authored schema (the merged packument, the publish document), so the wire body and the documented schema are one source. The package, artifact, and publish routes name the shared data-plane handlers (Ecluse.Core.Server.Pipeline); the meta-routes answer locally through their declared outcome.

A PUT /{pkg} is the npm publish request, so the method is part of the match: a PUT over a bare-package path publishes, while a read (GET, or its bodiless HEAD) over the same path fetches the packument. Those three methods are the only ones the front door answers; any other (POST, DELETE, …) matches no route and denies.

The model is deny by default. Three npm-specific facts shape the matching, all from the protocol research (see docs/research/reverse-engineering/npm.md §2 and §7):

  • Reserved meta-routes (/-/…) are matched first. A real package name can never begin with '-', so a leading "-" segment is unambiguously a meta-route.
  • Scoped names arrive in two encodings. The path is percent-decoded before it reaches us, so a scoped name arrives either as one decoded segment (@scope/pkg) or as two (@scope, pkg). Both are normalised to the same PackageName here.
  • A tarball path is /{pkg}/-/{file}.tgz. tarballCoordinate is the npm-side parse of the artifact coordinate; a basename that does not match the package is a path-confusion attempt and denies.

Mount dispatch, prefix-stripping, and the liveness/readiness routes are handled in the agnostic web layer (see docs/architecture/web-layer.md); this table only ever sees the npm-native request.

Synopsis

The mount's router and fallback action

npmRouter :: MountRouter Source #

npm's mount router: the route table folded into the whole routing decision. The first route that claims the request decides what is done with it; a request no route claims is the deny-by-default 404 (npmNotFound) in npm's own error surface.

npmNotFound :: RouteAction Source #

The deny-by-default 404 action for a path no route claims. Its local value and manifest entry are two interpretations of unsupportedContract.

Route-scoped pipeline contracts (exported for direct pipeline specs)

npmTarballContract :: ResponseContract PassthroughResponse Source #

The tarball is deliberately an open relay: any upstream status, headers, media type, and bytes can be forwarded. The one default document is therefore more accurate than a closed list that the upstream can escape.

The table, as data

npmRoutes :: [Route NpmCap] Source #

npm's routes, in matching order: one named value each, aggregated here. The structure of each is in its own definition; the security-critical leaf parsing stays in the named functions the captures and builders reference (takePackage, tarballCoordinate). Ordering follows npm's conventions: the reserved meta-routes are literal and tried first.

npmRouteSpecs :: NonEmpty RouteSpec Source #

npm's routes as data for the capability manifest: the specsOf projection of the same npmRoutes the router runs, plus the synthetic deny-by-default catch-all.

The leaf parsers (exported for their specs)

tarballCoordinate :: PackageName -> Text -> Maybe (Version, Filename) Source #

Parse an npm tarball-slot file into the artifact coordinate it names for name: the Version and the verbatim Filename. Nothing denies it.

The npm convention is {unscoped-name}-{version}.tgz, so the file must end in .tgz over a non-empty name and have a basename of exactly {unscoped-name}-{version}. A basename that does not begin with {unscoped-name}- is a path-confusion attempt and denies. On a match the version run is read by the total mkVersion, and the file is preserved verbatim.

Exported so the coordinate parse -- the security-critical half of the artifact route -- is asserted directly, rather than only through the router.