| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
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 samePackageNamehere. - A tarball path is
/{pkg}/-/{file}.tgz.tarballCoordinateis 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
- npmRouter :: MountRouter
- npmNotFound :: RouteAction
- npmPackumentContract :: ResponseContract NpmPackumentResponse
- npmPackumentReplies :: PackumentReplies NpmPackumentResponse
- npmTarballContract :: ResponseContract PassthroughResponse
- npmTarballReplies :: TarballReplies PassthroughResponse
- npmPublishContract :: ResponseContract NpmPublishResponse
- npmPublishReplies :: PublishReplies NpmPublishResponse
- npmRoutes :: [Route NpmCap]
- npmRouteSpecs :: NonEmpty RouteSpec
- takePackage :: [Text] -> Maybe (PackageName, [Text])
- tarballCoordinate :: PackageName -> Text -> Maybe (Version, Filename)
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)
npmPackumentContract :: ResponseContract NpmPackumentResponse Source #
npmPackumentReplies :: PackumentReplies NpmPackumentResponse Source #
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.
npmPublishContract :: ResponseContract NpmPublishResponse Source #
npmPublishReplies :: PublishReplies NpmPublishResponse Source #
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.
The leaf parsers (exported for their specs)
takePackage :: [Text] -> Maybe (PackageName, [Text]) Source #
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.