| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Ecluse.Registry.Npm.Wire
Description
The npm registry wire JSON types and their lenient decoders.
This module is the npm protocol boundary: it models the JSON the registry
actually sends and parses it with deliberately forgiving FromJSON instances.
It is the raw-wire layer of "parse, don't validate" — it captures /what the
registry said/ as faithfully as the rules and serving need, and __nothing
more__. Projecting these wire types into the ecosystem-agnostic domain model
(Ecluse.Package: PackageDetails et al.) is a separate concern; keeping the
two apart is what keeps the lenient/faithful handle clean.
The shapes here are reverse-engineered from live captures of
registry.npmjs.org; the authoritative reference (with real bodies) is
docs/research/reverse-engineering/npm.md (§4 full packument, §5 abbreviated,
§7 dist, §11 type model, §3 errors).
Lenient on input
The public registry has drifted from its own spec and is inconsistent across endpoints, so every decoder here is forgiving in four specific ways, matching the documented reality:
- Unknown keys are ignored. Manifests carry arbitrary author keys
(
gitHead,exports, tool-config blocks likeis-odd'sverb) and registry bookkeeping (_npmOperationalInternal); a decoder must not choke on them. aeson's record decoders already ignore extra keys, so this falls out of using(.:?)/(.:)rather than enumerating the whole object. - String-or-object scalars.
license,bugs,repository, and theauthor/maintainer person fields each arrive as either a bare string or an object, depending on the package's age and tooling. Each corresponding type (License,Bugs,Repository,Person) therefore parses both shapes. - The bare-string error body. npm's per-version 404 is a bare JSON
string (
"version not found: ^3.0.0"), not the documented{error|message}object.ErrorResponsetolerates both. - The string-or-boolean
deprecatedflag.deprecatedis conventionally the deprecation message string, but some published versions carry a boolean instead (true= deprecated without a message,false= not deprecated).vmDeprecatedreads every form, so a boolean never fails the whole packument decode (a real packument such as react's mixes the string and boolean forms across versions).
Faithful on the rule-decisive fields
The fields the rules engine and the serving path actually need are captured
precisely: the abbreviated-only vmHasInstallScript flag, the vmDeprecated
notice, the whole vmScripts map (so the full form's install-script presence
can be derived — the full manifest has no hasInstallScript key), the
Dist integrity triple (tarball/shasum/integrity), and the full
packument's pkmtTime map (the source of truth for publish age, which the
abbreviated form drops).
Only the decode path (FromJSON) is modelled here.
Synopsis
- data Person = Person {
- personName :: Text
- personEmail :: Maybe Text
- personUrl :: Maybe Text
- data Repository = Repository {}
- data Bugs = Bugs {}
- data License
- data Dist = Dist {}
- data Signature = Signature {}
- data VersionManifest = VersionManifest {
- vmName :: Text
- vmVersion :: Text
- vmDist :: Dist
- vmDeprecated :: Maybe Text
- vmHasInstallScript :: Maybe Bool
- vmScripts :: Map Text Text
- vmLicense :: Maybe License
- vmMaintainers :: [Person]
- vmDependencies :: Map Text Text
- vmDevDependencies :: Map Text Text
- vmPeerDependencies :: Map Text Text
- vmOptionalDependencies :: Map Text Text
- data Packument = Packument {
- pkmtName :: Text
- pkmtDistTags :: Map Text Text
- pkmtVersions :: Map Text VersionManifest
- pkmtTime :: Map Text UTCTime
- pkmtMaintainers :: [Person]
- pkmtDescription :: Maybe Text
- pkmtHomepage :: Maybe Text
- pkmtRepository :: Maybe Repository
- pkmtBugs :: Maybe Bugs
- pkmtLicense :: Maybe License
- pkmtKeywords :: [Text]
- data AbbreviatedPackument = AbbreviatedPackument {}
- data ErrorResponse
- data ErrorBody = ErrorBody {}
- errorMessage :: ErrorResponse -> Maybe Text
Shared scalars
A person associated with a package — an author, maintainer, contributor, or
the per-version publisher (_npmUser).
Lenient: npm sends a person as either an object {name, email?, url?} or
a single packed string of the conventional form
"Name <email> (url)". The packed form is captured verbatim in
personName (with personEmail/personUrl left Nothing); this wire layer
does not attempt to split it, leaving that to the domain projection if it is ever
needed. Distinct from Ecluse.Package's domain Person — this is the raw wire
shape.
Constructors
| Person | |
Fields
| |
data Repository Source #
An SCM location for a package.
Lenient: npm sends repository as either an object {type?, url} or a
bare string (a shorthand URL such as "github:user/repo"). Both are captured;
the bare-string form fills repoUrl and leaves repoType Nothing.
Constructors
| Repository | |
Instances
| FromJSON Repository Source # | |
Defined in Ecluse.Registry.Npm.Wire Methods parseJSON :: Value -> Parser Repository Source # parseJSONList :: Value -> Parser [Repository] Source # | |
| Show Repository Source # | |
Defined in Ecluse.Registry.Npm.Wire Methods showsPrec :: Int -> Repository -> ShowS # show :: Repository -> String # showList :: [Repository] -> ShowS # | |
| Eq Repository Source # | |
Defined in Ecluse.Registry.Npm.Wire | |
| Ord Repository Source # | |
Defined in Ecluse.Registry.Npm.Wire Methods compare :: Repository -> Repository -> Ordering # (<) :: Repository -> Repository -> Bool # (<=) :: Repository -> Repository -> Bool # (>) :: Repository -> Repository -> Bool # (>=) :: Repository -> Repository -> Bool # max :: Repository -> Repository -> Repository # min :: Repository -> Repository -> Repository # | |
The issue tracker for a package.
Lenient: npm sends bugs as either an object {url?, email?} or a bare
string (just the tracker URL). The bare-string form fills bugsUrl.
Constructors
| Bugs | |
A declared license.
Lenient: modern packages send a bare SPDX string (MIT); legacy
packages send an object {type, url?}. Both are preserved as a sum so the
distinction is not lost: LicenseSpdx for the string, LicenseObject for the
legacy object.
Constructors
| LicenseSpdx Text | An SPDX expression or identifier, sent as a bare string ( |
| LicenseObject Text (Maybe Text) | The legacy object form |
The dist object
The dist object: the artifact descriptor carried by every version
manifest (full and abbreviated). It is the gateway to the tarball bytes and the
integrity guarantee.
The integrity triple (distTarball, distShasum, distIntegrity) is
rule-decisive and serving-decisive — a client fails the install if the
downloaded bytes do not match integrity/shasum, so any mirror or URL rewrite
must preserve these byte-for-byte. Prefer distIntegrity (SRI) over the legacy
SHA-1 distShasum.
Constructors
| Dist | |
Fields
| |
One registry signature over a published artifact: an ECDSA signature and
the id of the key that produced it. Verifiable against npm's published public
keys (GET /-/npm/v1/keys) — the basis of npm audit signatures.
Constructors
| Signature | |
Instances
| FromJSON Signature Source # | |
| Show Signature Source # | |
| Eq Signature Source # | |
| Ord Signature Source # | |
Per-version manifest
data VersionManifest Source #
A single version's manifest — the per-version object that is essentially
the package's package.json at publish time plus registry-injected fields. It
appears three ways on the wire and this one type decodes all of them: embedded in
a full Packument (versions[v]), embedded in an AbbreviatedPackument (a
trimmed subset of the same shape), and standalone (GET /{pkg}/{version}).
Only the fields Écluse's rules and serving need are modelled; everything else is ignored (see the module header). The two rule-decisive optionals deserve note:
vmHasInstallScriptis abbreviated-only — the registry sets it when the version declarespreinstall/install/postinstallscripts. It is the cleanest install-script signal, but it is absent from the full manifest.vmScriptsis therefore captured whole so that, when only the full form is available, install-script presence can be derived (scriptshas any ofpreinstall/install/postinstall). That derivation is a domain-projection concern, not this layer's.
The publish timestamp is not here — it lives in the packument's
pkmtTime map, not the manifest (see §8 of the protocol reference).
Constructors
| VersionManifest | |
Fields
| |
Instances
| FromJSON VersionManifest Source # | |
Defined in Ecluse.Registry.Npm.Wire Methods parseJSON :: Value -> Parser VersionManifest Source # parseJSONList :: Value -> Parser [VersionManifest] Source # | |
| Show VersionManifest Source # | |
Defined in Ecluse.Registry.Npm.Wire Methods showsPrec :: Int -> VersionManifest -> ShowS # show :: VersionManifest -> String # showList :: [VersionManifest] -> ShowS # | |
| Eq VersionManifest Source # | |
Defined in Ecluse.Registry.Npm.Wire Methods (==) :: VersionManifest -> VersionManifest -> Bool # (/=) :: VersionManifest -> VersionManifest -> Bool # | |
Packuments
The full packument: GET /{pkg} with Accept: application/json (or
no Accept). One document describing the package and every published
version.
The field that earns the full form its place in the pipeline is pkmtTime: the
map of publish timestamps (created, modified, and one per version), the
source of truth for publish age that age-based rules need. The abbreviated form
(§5) drops it, keeping only a top-level modified. Package-level
description/license/author are hoisted from the latest version for
convenience; the authoritative copy is the per-version one in pkmtVersions.
_attachments is intentionally not modelled — it is populated only on the
publish document, not on reads.
Constructors
| Packument | |
Fields
| |
data AbbreviatedPackument Source #
The abbreviated packument: GET /{pkg} with
Accept: application/vnd.npm.install-v1+json. The install-optimised view and
the one the proxy treats as primary.
It carries exactly four top-level fields. Notably the full time map is dropped
(only a top-level apkmtModified remains), so publish-age rules need the full
Packument. Its apkmtVersions manifests are the trimmed subset of
VersionManifest — the same type, with the install-only fields populated
(including the abbreviated-only vmHasInstallScript).
Constructors
| AbbreviatedPackument | |
Fields
| |
Instances
| FromJSON AbbreviatedPackument Source # | |
Defined in Ecluse.Registry.Npm.Wire Methods parseJSON :: Value -> Parser AbbreviatedPackument Source # parseJSONList :: Value -> Parser [AbbreviatedPackument] Source # | |
| Show AbbreviatedPackument Source # | |
Defined in Ecluse.Registry.Npm.Wire Methods showsPrec :: Int -> AbbreviatedPackument -> ShowS # show :: AbbreviatedPackument -> String # showList :: [AbbreviatedPackument] -> ShowS # | |
| Eq AbbreviatedPackument Source # | |
Defined in Ecluse.Registry.Npm.Wire Methods (==) :: AbbreviatedPackument -> AbbreviatedPackument -> Bool # (/=) :: AbbreviatedPackument -> AbbreviatedPackument -> Bool # | |
Errors
data ErrorResponse Source #
An npm error body.
Lenient: the documented shape is an object { message?, error?, ok?: false
} and clients "should check for message, then error". But the registry is
inconsistent — its per-version 404 is a bare JSON string
("version not found: ^3.0.0"), not an object. This type tolerates both: the
object form keeps its fields in an ErrorBody, and a bare string is captured
whole as ErrorString. Read the human-facing reason via errorMessage, which
applies npm's "message, then error" precedence across both shapes.
Constructors
| ErrorObject ErrorBody | The documented object form |
| ErrorString Text | A bare JSON string body (npm's per-version 404), captured whole. |
Instances
| FromJSON ErrorResponse Source # | |
Defined in Ecluse.Registry.Npm.Wire Methods parseJSON :: Value -> Parser ErrorResponse Source # parseJSONList :: Value -> Parser [ErrorResponse] Source # | |
| Show ErrorResponse Source # | |
Defined in Ecluse.Registry.Npm.Wire Methods showsPrec :: Int -> ErrorResponse -> ShowS # show :: ErrorResponse -> String # showList :: [ErrorResponse] -> ShowS # | |
| Eq ErrorResponse Source # | |
Defined in Ecluse.Registry.Npm.Wire Methods (==) :: ErrorResponse -> ErrorResponse -> Bool # (/=) :: ErrorResponse -> ErrorResponse -> Bool # | |
The fields of npm's object-form error body. A product type (not inline
constructor fields on ErrorResponse) so its selectors are total — there is
no ErrorString case for them to be partial over.
Constructors
| ErrorBody | |
errorMessage :: ErrorResponse -> Maybe Text Source #
The human-facing reason carried by an error body, applying npm's documented
precedence: prefer message, then error, and for the bare-string form the
string itself. Nothing only when an object form carried neither field. Total.