| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Ecluse.Core.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.Core.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 (§6 manifest, §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 five 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). - Advisory
distsub-fields degrade rather than deny.fileCount,unpackedSize, andsignaturesare advisory -- they decide no rule and no serve -- so a hostile value in one (a fractional/huge/Int-overflowing number, a wrong-typed field, or a malformed/non-arraysignatures) reads as absent/empty rather than failing the version. One poisoned value therefore cannot deny the whole packument (Dist).
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), and the
Dist integrity triple (tarball/shasum/integrity).
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 {}
- data ErrorResponse
- data ErrorBody = ErrorBody {}
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.Core.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.Core.Registry.Npm.Wire Methods parseJSON :: Value -> Parser Repository Source # parseJSONList :: Value -> Parser [Repository] Source # | |
| Show Repository Source # | |
Defined in Ecluse.Core.Registry.Npm.Wire Methods showsPrec :: Int -> Repository -> ShowS # show :: Repository -> String # showList :: [Repository] -> ShowS # | |
| Eq Repository Source # | |
Defined in Ecluse.Core.Registry.Npm.Wire | |
| Ord Repository Source # | |
Defined in Ecluse.Core.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.
The remaining sub-fields (distFileCount, distUnpackedSize,
distSignatures) are advisory -- they inform reporting but decide no rule and
no serve -- and so are decoded leniently: a present-but-undecodable number
(fractional, huge, or Int-overflowing) reads as absent (Nothing), a malformed
signature element is skipped rather than failing the array, and a
signatures value that is not even an array reads as empty. A hostile value in
one version therefore degrades that field alone, never denying the whole
packument.
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's versions[v] object, embedded in an abbreviated packument's
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 time map,
not the manifest (see §8 of the protocol reference).
Constructors
| VersionManifest | |
Fields
| |
Instances
| FromJSON VersionManifest Source # | |
Defined in Ecluse.Core.Registry.Npm.Wire Methods parseJSON :: Value -> Parser VersionManifest Source # parseJSONList :: Value -> Parser [VersionManifest] Source # | |
| Show VersionManifest Source # | |
Defined in Ecluse.Core.Registry.Npm.Wire Methods showsPrec :: Int -> VersionManifest -> ShowS # show :: VersionManifest -> String # showList :: [VersionManifest] -> ShowS # | |
| Eq VersionManifest Source # | |
Defined in Ecluse.Core.Registry.Npm.Wire Methods (==) :: VersionManifest -> VersionManifest -> Bool # (/=) :: VersionManifest -> VersionManifest -> 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.
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.Core.Registry.Npm.Wire Methods parseJSON :: Value -> Parser ErrorResponse Source # parseJSONList :: Value -> Parser [ErrorResponse] Source # | |
| Show ErrorResponse Source # | |
Defined in Ecluse.Core.Registry.Npm.Wire Methods showsPrec :: Int -> ErrorResponse -> ShowS # show :: ErrorResponse -> String # showList :: [ErrorResponse] -> ShowS # | |
| Eq ErrorResponse Source # | |
Defined in Ecluse.Core.Registry.Npm.Wire Methods (==) :: ErrorResponse -> ErrorResponse -> Bool # (/=) :: ErrorResponse -> ErrorResponse -> Bool # | |