| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Ecluse.Core.Package
Description
The package domain model -- ecosystem-agnostic vocabulary for the rules engine.
These types capture everything the proxy needs to reason about a package version while staying decoupled from any registry's wire format. Registry adapters (npm, PyPI, RubyGems) are responsible for projecting their responses into these types; nothing above the registry layer sees registry-specific structures.
Two pieces of this vocabulary earn their own sibling module: the Ecosystem tag
lives in Ecluse.Core.Ecosystem (shared with the version engine and the registry
adapters), and version identity and ordering live in Ecluse.Core.Version (a
Version is embedded here in PackageDetails). Import those modules directly
when you need to name or build their types.
The design follows two principles synthesised from the protocol research (see
docs/research/synthesis.md):
- Rules consume normalised signals, not raw fields. The risky behaviours
differ on the wire (npm install scripts, PyPI sdist builds, RubyGems native
extensions) but collapse to one signal --
CodeExecSignal. Trust likewise collapses toTrust. A rule never learns which ecosystem it is looking at. - Signal availability is explicit. A signal the adapter has not (or cannot
cheaply) determine is
CodeExecUnknown/TrustUnknown/Nothing, so a pure rule abstains rather than guessing and the effectful tier can resolve it later (seedocs/architecture.md→ "Rules Engine").
Synopsis
- data Scope
- mkScope :: Text -> Scope
- unScope :: Scope -> Text
- renderScope :: Scope -> Text
- data PackageName
- mkPackageName :: Ecosystem -> Maybe Scope -> Text -> PackageName
- pkgEcosystem :: PackageName -> Ecosystem
- pkgNamespace :: PackageName -> Maybe Scope
- pkgCanonical :: PackageName -> ShortText
- pkgDisplay :: PackageName -> ShortText
- renderPackageName :: PackageName -> Text
- unscopedName :: PackageName -> Text
- data CodeExecSignal
- data Trust
- data TrustEvidence
- data Availability
- data Artifact = Artifact {
- artFilename :: Text
- artUrl :: Text
- artKind :: ArtifactKind
- artHashes :: [Hash]
- artSize :: Maybe Int
- artInterpreter :: Maybe Text
- artYanked :: Bool
- artProvenance :: Maybe Text
- data ArtifactKind
- data Hash
- hashAlg :: Hash -> HashAlg
- hashValue :: Hash -> Text
- mkHash :: HashAlg -> Text -> Either Text Hash
- data HashAlg
- renderHashAlg :: HashAlg -> Text
- parseHashAlg :: Text -> Either Text HashAlg
- sriPrefix :: Text -> Text
- sriBody :: Text -> Text
- sriAlgorithm :: Text -> Maybe HashAlg
- computeDigest :: HashAlg -> Maybe (LByteString -> ByteString)
- isComputable :: HashAlg -> Bool
- data Person = Person {
- personName :: Text
- personEmail :: Maybe Text
- personUrl :: Maybe Text
- data PackageDetails = PackageDetails {}
- data PackageInfo = PackageInfo {}
- data InvalidEntry = InvalidEntry {}
- data InvalidEntryKind
Scopes
An npm scope, stored without its leading '@' (the scope of
@myorg/pkg is "myorg"). Construct via mkScope, which normalises away
a leading '@' so equality is independent of how the scope was written.
A scope is a bulk-stored, equality-only identifier (an allow-list key and part
of PackageName identity), so it is held as ShortText: the
conversion happens once in Text -> ShortTextmkScope and the reverse once in unScope/renderScope,
never in a hot loop (see STYLE.md §6).
renderScope :: Scope -> Text Source #
Render a scope in npm wire form, with the leading '@'.
Package identity
data PackageName Source #
A package identity, decoupled from any registry's wire format.
Identity differs by ecosystem -- npm has scopes and is case-sensitive, PyPI
normalises per PEP 503, RubyGems is verbatim -- so the type is opaque:
build it with mkPackageName, which records the ecosystem, computes a
pkgCanonical key used for equality/matching, and keeps a pkgDisplay form
for faithful rendering. Equality and ordering are on
( only -- never the display
form -- so pkgEcosystem, pkgNamespace, pkgCanonical)Flask and flask are the same PyPI package but different npm ones.
Instances
| Show PackageName Source # | |
Defined in Ecluse.Core.Package Methods showsPrec :: Int -> PackageName -> ShowS # show :: PackageName -> String # showList :: [PackageName] -> ShowS # | |
| Eq PackageName Source # | |
Defined in Ecluse.Core.Package | |
| Ord PackageName Source # | |
Defined in Ecluse.Core.Package Methods compare :: PackageName -> PackageName -> Ordering # (<) :: PackageName -> PackageName -> Bool # (<=) :: PackageName -> PackageName -> Bool # (>) :: PackageName -> PackageName -> Bool # (>=) :: PackageName -> PackageName -> Bool # max :: PackageName -> PackageName -> PackageName # min :: PackageName -> PackageName -> PackageName # | |
mkPackageName :: Ecosystem -> Maybe Scope -> Text -> PackageName Source #
Build a PackageName, normalising the canonical key for the ecosystem.
The display form is the scope-joined raw name (@scope/name when scoped);
the canonical key is that form normalised: PEP 503 lower-casing and
[-_.]+→- collapsing for PyPI, verbatim for npm and RubyGems.
pkgEcosystem :: PackageName -> Ecosystem Source #
The ecosystem this name belongs to.
pkgNamespace :: PackageName -> Maybe Scope Source #
The scope, if scoped (npm @scope/name). Nothing for PyPI/RubyGems.
pkgCanonical :: PackageName -> ShortText Source #
The normalised key for equality and matching (PEP 503 for PyPI;
verbatim for npm/RubyGems). Held as ShortText: it is an equality/Ord key
that is normalised once at mkPackageName and never sliced afterwards.
pkgDisplay :: PackageName -> ShortText Source #
The name as published, for rendering and round-tripping. Held as
ShortText; read it back as Text through renderPackageName.
renderPackageName :: PackageName -> Text Source #
Render a package name in its native wire form (the display name).
unscopedName :: PackageName -> Text Source #
The unscoped (base) name: the display name with any @scope/ prefix dropped
(@babel/code-frame → code-frame). The single home for the bare-name derivation
the npm tarball/path layer and the mirror queue all need -- they previously each
reconstructed it by rendering then string-stripping the scope.
Normalised signals
data CodeExecSignal Source #
Whether installing a version executes code (the cross-ecosystem unification of npm install scripts, PyPI sdist builds, and RubyGems native extensions).
Constructors
| NoCodeOnInstall | Determined: installation runs no code. |
| RunsCodeOnInstall Text | Determined: installation runs code; the text says how (audit trail). |
| CodeExecUnknown | Not yet determined (e.g. the RubyGems gemspec has not been fetched). Pure rules abstain; the effectful tier may resolve it. |
Instances
| Show CodeExecSignal Source # | |
Defined in Ecluse.Core.Package Methods showsPrec :: Int -> CodeExecSignal -> ShowS # show :: CodeExecSignal -> String # showList :: [CodeExecSignal] -> ShowS # | |
| Eq CodeExecSignal Source # | |
Defined in Ecluse.Core.Package Methods (==) :: CodeExecSignal -> CodeExecSignal -> Bool # (/=) :: CodeExecSignal -> CodeExecSignal -> Bool # | |
The trust/provenance signal for a version. The how of trust differs by
ecosystem (npm dist.signatures, PyPI PEP 740 attestations, RubyGems signed
gems/MFA) but is captured as TrustEvidence so rules stay ecosystem-blind.
Constructors
| Trusted (NonEmpty TrustEvidence) | Determined trusted, with the evidence supporting it. |
| Untrusted | Determined: no trust signal established. |
| TrustUnknown | Not yet determined (e.g. signature verification needs a fetch). |
data TrustEvidence Source #
A normalised reason a version is trusted; the adapter maps its ecosystem's mechanism onto this vocabulary.
Constructors
| Signed | The artifact is cryptographically signed. |
| Attested | The artifact carries a provenance attestation (e.g. Sigstore). |
| MfaPublished | The version was published under enforced multi-factor auth. |
| OtherEvidence Text | An ecosystem mechanism not yet in this vocabulary (escape hatch). |
Instances
| Show TrustEvidence Source # | |
Defined in Ecluse.Core.Package Methods showsPrec :: Int -> TrustEvidence -> ShowS # show :: TrustEvidence -> String # showList :: [TrustEvidence] -> ShowS # | |
| Eq TrustEvidence Source # | |
Defined in Ecluse.Core.Package Methods (==) :: TrustEvidence -> TrustEvidence -> Bool # (/=) :: TrustEvidence -> TrustEvidence -> Bool # | |
data Availability Source #
Whether a version is offered, advisory-deprecated, or withdrawn.
Constructors
| Available | Offered normally. |
| Deprecated Text | Advisory deprecation (npm); still resolvable. Carries the message. |
| Yanked (Maybe Text) | Withdrawn from resolution (PyPI yank keeps the file; RubyGems yank removes it). Carries the reason, if given. |
Instances
| Show Availability Source # | |
Defined in Ecluse.Core.Package Methods showsPrec :: Int -> Availability -> ShowS # show :: Availability -> String # showList :: [Availability] -> ShowS # | |
| Eq Availability Source # | |
Defined in Ecluse.Core.Package | |
Artifacts
One distribution file for a version. A version owns a NonEmpty list of
these: npm has exactly one, PyPI has an sdist plus many wheels, RubyGems has one
per platform.
Constructors
| Artifact | |
Fields
| |
Instances
data ArtifactKind Source #
What kind of distribution file an artifact is.
Constructors
| Tarball | An npm tarball. |
| Sdist | A PyPI source distribution (building it may execute code). |
| Wheel Text | A PyPI wheel; carries its compatibility tag (e.g. |
| Gem Text | A RubyGems gem; carries its platform ( |
Instances
| Show ArtifactKind Source # | |
Defined in Ecluse.Core.Package Methods showsPrec :: Int -> ArtifactKind -> ShowS # show :: ArtifactKind -> String # showList :: [ArtifactKind] -> ShowS # | |
| Eq ArtifactKind Source # | |
Defined in Ecluse.Core.Package | |
hashValue :: Hash -> Text Source #
The digest itself, in the algorithm's wire encoding (e.g. hex, or the
whole sha512-… string for SRI).
mkHash :: HashAlg -> Text -> Either Text Hash Source #
Build a Hash, validating that the digest is structurally well-formed:
cleanly encoded and exactly the byte length its algorithm specifies. This is the only
way to construct a Hash, so the type itself is the proof that the digest could be a
real digest of that algorithm -- an empty, truncated, over-long, non-hex, or bad-base64
value is unconstructable and so can never reach an integrity gate as a degenerate
digest (the fail-open this closes is docs/architecture/security.md invariant 5).
Well-formedness is not admissibility: a well-formed but weak SHA-1 digest builds
fine; whether it clears the public-integrity floor is the separate decision of
Ecluse.Core.Package.Integrity. mkHash rejects a malformed digest, never a merely weak one.
A hex-tagged algorithm (everything but SRI) takes lower- or upper-case hex of the
algorithm's digest length. An SRI takes one or more whitespace-separated
<alg>-<base64> components, each naming a Subresource-Integrity algorithm
(sha256, sha384, sha512) whose base64 body decodes to that algorithm's digest
length; every component must be well-formed.
>>>import Ecluse.Core.Package (HashAlg (SHA1))>>>fmap hashAlg (mkHash SHA1 "0a4d55a8d778e5022fab701977c5d840bbc486d0")Right SHA1
>>>mkHash SHA1 "deadbeef"Left "malformed sha1 digest"
A hash algorithm an integrity digest is computed with.
Constructors
| SHA1 | |
| SHA256 | |
| SHA384 | |
| SHA512 | |
| MD5 | |
| Blake2b | |
| SRI | A Subresource-Integrity string (npm |
Instances
| Bounded HashAlg Source # | |
| Enum HashAlg Source # | |
| Show HashAlg Source # | |
| Eq HashAlg Source # | |
| Ord HashAlg Source # | |
Algorithm vocabulary
renderHashAlg :: HashAlg -> Text Source #
The lower-case wire name of an algorithm -- the canonical spelling parseHashAlg
reads back. Total and injective, so it doubles as config rendering and error text.
>>>renderHashAlg SHA256"sha256"
parseHashAlg :: Text -> Either Text HashAlg Source #
Parse an algorithm name, tolerating case and an optional internal '-' (so
"SHA-256" and "sha256" both parse). An unrecognised name is reported as such,
distinct from a recognised-but-too-weak floor. This admits only the named hash
algorithms; the sri wrapper is not a config-selectable algorithm and is rejected.
>>>parseHashAlg "SHA-256"Right SHA256
>>>parseHashAlg "frobnicate"Left "unknown integrity algorithm: frobnicate"
sriPrefix :: Text -> Text Source #
The algorithm-name token of a Subresource-Integrity string -- the <alg> before
the first '-' in <alg>-<base64>. A string with no '-' is all prefix.
>>>sriPrefix "sha512-Zm9vYmFy""sha512"
sriBody :: Text -> Text Source #
The base64 digest body of a Subresource-Integrity string -- the <base64> after
the first '-' in <alg>-<base64>. A string with no '-' has an empty body.
>>>sriBody "sha512-Zm9vYmFy""Zm9vYmFy"
sriAlgorithm :: Text -> Maybe HashAlg Source #
The HashAlg a Subresource-Integrity string names, read from its <alg> prefix.
The prefixes resolved are the Subresource-Integrity set sha256, sha384 and sha512
(every long digest the model represents and a registry serves); an unrecognised or
malformed prefix yields Nothing, so the string asserts no algorithm and clears no
floor (the fail-closed reading).
>>>sriAlgorithm "sha512-Zm9vYmFy"Just SHA512
>>>sriAlgorithm "sha384-Zm9vYmFy"Just SHA384
Digest computation
computeDigest :: HashAlg -> Maybe (LByteString -> ByteString) Source #
Compute the digest of bytes in a given algorithm, as the raw digest bytes, or
Nothing for an algorithm Écluse will not verify against. The computable algorithms are
exactly the collision-resistant ones: SHA1, SHA256, SHA384, SHA512, and
Blake2b-512. MD5 is deliberately uncomputable here (a match on a broken hash cannot prove
the bytes were not substituted, so the tamper gate never verifies against it), as is the
bare SRI wrapper, which names no algorithm of its own (resolve it with sriAlgorithm
first).
This is the sibling of hexDigestOk: both dispatch on the same per-algorithm crypto type,
so they live together and a new HashAlg must be given an arm in each (the 'case' is total,
and the package builds with -Wincomplete-patterns as an error). It is the one place that
defines which algorithms the worker can verify; the integrity floor admits by strength
(Ecluse.Core.Package.Integrity), and the invariant that every floor-clearing algorithm is
computable here keeps the worker able to verify whatever the floor admits.
isComputable :: HashAlg -> Bool Source #
Whether the worker can compute (and so verify a digest in) the given algorithm: the
predicate form of computeDigest, taken from the same single definition so the computable
set cannot drift from what computeDigest actually computes.
>>>isComputable SHA256True
>>>isComputable MD5False
Dependencies
People
A person associated with a package (author, maintainer, or publisher).
Constructors
| Person | |
Fields
| |
Per-version details
data PackageDetails Source #
The ecosystem-agnostic snapshot of a single package version that the rules engine evaluates. A registry adapter projects its wire format into this; the rules engine never sees anything else, and never branches on the ecosystem.
Constructors
| PackageDetails | |
Fields
| |
Instances
| Show PackageDetails Source # | |
Defined in Ecluse.Core.Package Methods showsPrec :: Int -> PackageDetails -> ShowS # show :: PackageDetails -> String # showList :: [PackageDetails] -> ShowS # | |
| Eq PackageDetails Source # | |
Defined in Ecluse.Core.Package Methods (==) :: PackageDetails -> PackageDetails -> Bool # (/=) :: PackageDetails -> PackageDetails -> Bool # | |
Packument-level view
data PackageInfo Source #
The packument-level view of a package: the whole-package metadata document
(PackageDetails is the per-version snapshot embedded within it). A registry
adapter projects a registry's packument (the npm full-metadata document) into
this; the proxy core reasons over it without ever seeing the wire format.
Constructors
| PackageInfo | |
Fields
| |
Instances
| Show PackageInfo Source # | |
Defined in Ecluse.Core.Package Methods showsPrec :: Int -> PackageInfo -> ShowS # show :: PackageInfo -> String # showList :: [PackageInfo] -> ShowS # | |
| Eq PackageInfo Source # | |
Defined in Ecluse.Core.Package | |
data InvalidEntry Source #
A single packument entry a registry projection dropped as malformed rather than failing the entire document, kept so the drop is observable rather than silent (an operator can see that an upstream served a malformed entry, and which). Each ecosystem's projection populates this from its own wire shape, so the drop-and-track contract is the same across npm, PyPI, and RubyGems.
Constructors
| InvalidEntry | |
Fields
| |
Instances
| Show InvalidEntry Source # | |
Defined in Ecluse.Core.Package Methods showsPrec :: Int -> InvalidEntry -> ShowS # show :: InvalidEntry -> String # showList :: [InvalidEntry] -> ShowS # | |
| Eq InvalidEntry Source # | |
Defined in Ecluse.Core.Package | |
data InvalidEntryKind Source #
Which part of a packument a dropped InvalidEntry came from. A version
manifest drop removes a serve candidate (fail-closed for that one version); a
dist-tag or publish-time drop loses only that advisory datum while the version it
referred to still resolves.
Constructors
| InvalidVersionManifest | A |
| InvalidDistTag | A |
| InvalidPublishTime | A |
Instances
| Show InvalidEntryKind Source # | |
Defined in Ecluse.Core.Package Methods showsPrec :: Int -> InvalidEntryKind -> ShowS # show :: InvalidEntryKind -> String # showList :: [InvalidEntryKind] -> ShowS # | |
| Eq InvalidEntryKind Source # | |
Defined in Ecluse.Core.Package Methods (==) :: InvalidEntryKind -> InvalidEntryKind -> Bool # (/=) :: InvalidEntryKind -> InvalidEntryKind -> Bool # | |