ecluse:ecluse-core
Safe HaskellNone
LanguageGHC2021

Ecluse.Core.Server.Pipeline.Origin

Description

Resolving a packument's upstream origins: the per-origin fetch with its credential posture, the read-handle construction over the mount's dependencies, and the typed outcome the merge consumes.

The credential-authority invariant lives here (see docs/architecture/access-model.md): the private (trusted) origin is fetched uncached with the client's own forwarded credential, so the upstream re-authorises every client itself, while the public origin is fetched anonymous (the client's credential is stripped before any public-upstream fetch) and resolved through the shared metadata cache, one shared document serving every client. A fetch that fails degrades to no contribution rather than an error; a self-reported different package name is kept distinct (OriginNameMismatch) so the no-valid-origin terminal can render a 502 apart from a transient outage.

Ecluse.Core.Server.Pipeline.Packument gates, merges, and serves what resolves here; Ecluse.Core.Server.Pipeline.Tarball shares the public read handle (withPublicMetadataClient) so its single-version gate and the packument fetch collapse onto one cache entry.

Synopsis

A resolved contribution

data Contribution Source #

A successfully resolved upstream contribution: the parsed packument used to decide, alongside the raw document (CachedDoc) the served body is rebuilt from, and the origin body's ContentDigest for the derived validator. Pairing the views is the decision-surface/served-surface contract -- every stage carries the raw document next to the typed view so losslessness survives the pipeline. The raw document is threaded opaquely: the pipeline hands it to the injected adapter capabilities and never reads it.

fingerprintPiece :: Contribution -> (Provenance, ContentDigest, [Text]) Source #

One source's slice of the derived validator: its provenance, its origin body's digest, and the version keys that actually survived its gate -- together with the mount base URL and package name, exactly the inputs the assembled document is a deterministic function of (the plan itself derives from these).

The per-origin outcome

data OriginResult Source #

The outcome of resolving one upstream origin for a packument, beyond the plain "resolved or not" the merge consumes: a name mismatch is kept distinct from a plain non-resolution so the no-valid-origin terminal status can render a 502 (a responding upstream returned a packument for a different package) apart from a transient outage or a genuine absence.

Constructors

OriginResolved Manifest

A packument that decoded and whose self-reported name matched the request.

OriginNameMismatch

The origin answered, but its packument self-reported a name for a different package -- dropped as untrusted for this request, and a 502 signal when no origin is valid.

OriginUnresolved

The origin did not yield a usable packument -- unreachable, undecodable, or a genuine absence -- the existing degrade (no contribution).

OriginAbsent

The origin is not configured on this mount (a serve-only mount with no private upstream): structurally absent, kept distinct from OriginUnresolved so an unconfigured leg never contributes the degraded-availability signal a failed fetch rightly does.

originManifest :: OriginResult -> Maybe Manifest Source #

The resolved manifest an origin contributed, if any. A name mismatch, a plain non-resolution, and an absent origin alike contribute no document to the merge.

Fetching the two origins

fetchPrivateOrigin :: PackumentDeps -> ServeRuntime -> Maybe Secret -> PackageName -> Handler OriginResult Source #

Resolve the private (trusted) upstream origin, uncached, forwarding the client's own credential (the default passthrough posture). Returns its coherent (parsed packument, raw Value) pair -- or Nothing when the origin is unavailable or its body does not parse. A failed fetch is a degraded contribution, not an error: the merge serves the best-effort union of whatever resolved (partial-upstream availability).

Under passthrough the private upstream is the per-client authority for who may read what, so its metadata is not shared across clients: it is fetched and parsed on every request with that client's own forwarded token, so the upstream re-authorises each client itself. Caching it would key on the base URL alone (no credential dimension), so within the TTL one client's cache hit would skip the fetch and serve another client's private document -- bypassing the upstream's authorisation. The private origin is therefore deliberately kept out of the metadata cache; only the anonymous public origin is cached. (How a non-passthrough strategy can instead share the private origin safely is the serve-time authorisation it adds -- see docs/architecture/access-model.md.)

fetchPublicOrigin :: PackumentDeps -> ServeRuntime -> PackageName -> Handler OriginResult Source #

Resolve the public (gated, anonymous) upstream origin through the metadata cache, keyed by the origin's base URL as its Source, returning its coherent (parsed packument, raw Value) pair -- or Nothing when the origin is unavailable or its body does not parse. A failed fetch is a degraded contribution, not an error.

The public origin is anonymous (no client credential), so a single cached entry serves every client without crossing any trust boundary -- there is no per-client authority to preserve, only one shared anonymous document. A hit returns the cached pair (typed view and the exact bytes it was decoded from), so the served document and the decision over it stay coherent across the TTL, and concurrent resolutions of a popular package collapse to one upstream call -- as does the tarball gate's single-version read, which shares this very cache entry (fetchVersionMetadata).

withPublicMetadataClient :: ServeRuntime -> PackumentDeps -> Text -> (MetadataClient -> IO a) -> Handler a Source #

The public origin's read handle: anonymous (no token), resolved through the shared metadata cache under the base URL's Source. Both the packument fetch (fetchFullManifest) and the tarball gate's single-version read (fetchVersionMetadata) go through this handle, so they share one cache entry.