| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Ecluse.Core.Server.Context
Description
The per-request context the serve path reads through, and the handler monad over it.
Mount dispatch matches a request to one MountBinding -- a mount's complete
ecosystem wiring -- then runs the route's handler in Handler, a reader over a
RequestCtx pairing that binding with the request runtime ServeRuntime. A handler
reads its per-mount dependencies (the classifier, the packument-serve dependencies,
the path prefix) and the shared runtime from that one context,
rather than taking them as explicit arguments threaded down the pipeline.
ServeRuntime is the runtime interface the serve path is closed over: the two
data-plane HTTP managers, the metadata cache, the mirror queue, and the abstract
metric- and tracing-recording ports. It holds precisely what the pipeline needs to
serve a request and nothing more; the application's composition root constructs it
(wiring the concrete OpenTelemetry-backed ports), and a test constructs it over
doubles. Logging is not a field: a handler logs through the ambient katip
context, which the dispatch boundary establishes (with the structured-log scribes and
the trace-correlation dd object) when it runs the handler.
RequestCtx is a concrete record with plain accessors (ctxRuntime, ctxMount). The
handler monad layers over katip's logging context, so a structured log call composes
uniformly across the serve path.
Synopsis
- data ServeRuntime = ServeRuntime {}
- data PackumentDeps = PackumentDeps {
- pdPrivateBaseUrl :: Maybe Text
- pdPublicBaseUrl :: Text
- pdMountBaseUrl :: Text
- pdMirror :: MirrorServePlan
- pdRules :: [PreparedRule]
- pdAdditionalBlockedRanges :: [IPRange]
- pdTarballHostGate :: TarballHostGate
- pdLimits :: Limits
- pdInboundToken :: Maybe Secret
- pdNow :: IO UTCTime
- pdAdvisoryEtag :: IO (Maybe DbEtag)
- pdHelp :: Maybe HelpMessage
- pdMinIntegrity :: MinIntegrity
- pdMinTrustedIntegrity :: MinTrustedIntegrity
- pdDivergencePolicy :: DivergencePolicy
- pdNewMetadataClient :: TracingPort -> MetricsPort -> Upstream -> ManifestCaching -> (PackageName -> MetadataError -> IO ()) -> (PackageName -> [InvalidEntry] -> IO ()) -> (PackageName -> IO ()) -> Limits -> Manager -> Text -> Maybe Secret -> MetadataClient
- pdBuildArtifactRequestByFile :: Limits -> Manager -> Text -> Maybe Secret -> PackageName -> Text -> Either UrlFormationError Request
- pdBuildArtifactRequestByUrl :: Limits -> Manager -> Text -> Maybe Secret -> Text -> Either UrlFormationError Request
- pdAssemble :: Text -> Map SourceId CachedDoc -> MergePlan -> Maybe CachedDoc -> CachedDoc
- pdSerialise :: CachedDoc -> LByteString
- pdEgressUrl :: Text -> Either Text RegistryUrl
- data MirrorServePlan
- tarballHostHonoured :: Origin -> PackumentDeps -> Maybe HostPort -> Maybe HostPort -> Bool
- data PublishDeps = PublishDeps {
- pubTargetUrl :: Text
- pubScopes :: [Scope]
- pubStaticToken :: Maybe Secret
- pubInboundToken :: Maybe Secret
- pubLimits :: Limits
- pubBodyBudget :: ByteAdmission
- pubMaxRequestBytes :: Int
- pubHelp :: Maybe HelpMessage
- pubRelayPublish :: Limits -> Manager -> Text -> Maybe Secret -> PackageName -> ByteString -> IO (Either PublishRelayFault PublishRelayResponse)
- pubCanonicaliseName :: Text -> Maybe PackageName
- pubDeclaredNames :: LByteString -> [Text]
- data RouteAction = RouteAction (ResponseContract response) (ResponseAction response)
- data ResponseAction response
- = AnswerLocally response
- | RunPipeline response (Request -> (response -> IO ResponseReceived) -> Handler ResponseReceived)
- type MountRouter = Method -> [Text] -> RouteAction
- data MountBinding = MountBinding {}
- data RequestCtx = RequestCtx {}
- data Handler a
- runHandler :: LogEnv -> SimpleLogPayload -> RequestCtx -> Handler a -> IO a
Request runtime
data ServeRuntime Source #
The runtime backends the serve path is closed over: exactly the effectful
capabilities a request needs to fetch, gate, serve, and record. A record of concrete
handles and abstract ports (the Handle pattern), assembled by the composition root and
read by every handler through the RequestCtx.
The two HTTP managers carry the per-origin split: the public manager serves the
untrusted public-upstream and artifact egress, the private manager the trusted
private-upstream path. Both are the validating TLS manager (registry egress is
https-only by construction; certificate validation authenticates the host), so the
split is in credential handling and the dist.tarball host gate's trust, not the
manager. The metadata cache and mirror queue are the shared data-plane handles. The
metric and tracing ports are the abstract recording interfaces
(Ecluse.Core.Telemetry.Record, Ecluse.Core.Telemetry.Span); the application supplies
their OpenTelemetry-backed implementations, so the serve path records without naming a
telemetry backend. There is no log field: handlers log through the ambient katip
context.
Constructors
| ServeRuntime | |
Fields
| |
Packument-serve dependencies
data PackumentDeps Source #
The per-mount inputs the serve handlers need beyond the request runtime
ServeRuntime: the upstream endpoints, the mount's externally-visible base URL,
the mirror serve plan, its resolved rule policy, the edge auth token, the
wall-clock source, and the operator help message.
These are a mount-level concern, resolved at the composition root (a separate
concern) and carried on the mount's MountBinding; a handler reads exactly what it
needs to decide and serve from the RequestCtx it runs in. Both the packument and
the tarball paths share these deps -- the tarball path additionally gates one
version and, under MirrorOnAdmit, enqueues a mirror job -- so the name is retained
for continuity rather than narrowed to one route.
Constructors
| PackumentDeps | |
Fields
| |
data MirrorServePlan Source #
Whether an admitted public artifact is enqueued for the demand-driven mirror, and where that write lands. The discriminant is an absent capability, not a no-op handle: a serve-only mount opens no mirror producer span and emits no enqueue metric, so the telemetry never claims work that cannot happen.
Constructors
| MirrorOnAdmit Text | Enqueue admitted public artifacts for publication to this mirror-target endpoint (the mount's declared destination; the worker resolves its publish capability from the same configuration). |
| NoMirrorWrite | Serve-only: admitted public artifacts stream to the client and are never mirrored anywhere. Every artifact stays on the gated public leg. |
Instances
| Show MirrorServePlan Source # | |
Defined in Ecluse.Core.Server.Context Methods showsPrec :: Int -> MirrorServePlan -> ShowS # show :: MirrorServePlan -> String # showList :: [MirrorServePlan] -> ShowS # | |
| Eq MirrorServePlan Source # | |
Defined in Ecluse.Core.Server.Context Methods (==) :: MirrorServePlan -> MirrorServePlan -> Bool # (/=) :: MirrorServePlan -> MirrorServePlan -> Bool # | |
tarballHostHonoured :: Origin -> PackumentDeps -> Maybe HostPort -> Maybe HostPort -> Bool Source #
Whether an artifact's dist.tarball authority may be fetched, given the
origin's trust and the authority that served the packument it came from. Connects
the pure tarballHostAllowed to a mount's precomputed gate:
the tarball's host:port pair must be on the upstream allowlist and equal to the
packument origin's pair, the ecosystem's own declared artifact hosts being the one
same-host equivalence.
The literal internal-range block is origin-aware: an
UntrustedOrigin (the public path) is gated against the fixed
range set plus the operator-configured additionalBlockedRanges, while an
TrustedOrigin (the operator-configured private upstream) is
exempt, since a private registry may legitimately live on an internal address
(security.md invariant 3). The allowlist and same-authority clauses still gate the
trusted origin identically.
This is the one composition of the host gate over a mount's inputs: the serve
pipeline applies it before its public artifact fetch, and the composition root
closes it (against the public upstream authority) into the mirror worker's
re-evaluation bundle, so the ingest-time host check can never drift from the
serve-time one. Both authorities are already extracted (Nothing meaning no
dialable authority, which the gate refuses): the mount-constant ones live in the
precomputed pdTarballHostGate, so the hot path parses no base URL and rebuilds
no allowlist per request; only the dynamic artifact authority is parsed at the
call site.
Publish-serve dependencies
data PublishDeps Source #
The per-mount inputs the first-party publish handler needs: the publication target endpoint, the publish-scope allow-list (the anti-shadowing guard), the optional static fallback credential, the edge token, the response-bound budget, and the operator help message.
The mere presence of these deps is the publish path's opt-in: a mount carries a
PublishDeps only when a publication target is configured, so the binding's
bindingPublishDeps being Nothing is exactly the "no publication target ⇒ a
PUT /{pkg} is 405 Method Not Allowed" rule, modelled in the type rather than
re-derived at the handler (see
docs/architecture/registry-model.md → "Publishing first-party packages").
The credential posture is passthrough, symmetric with the private-upstream read
under passthrough: the publisher's own forwarded token is what reaches the
publication target, the static pubStaticToken only a fallback for a client that
sends none. Écluse mints no token of its own here -- unlike the mirror target -- so this
record carries no CredentialProvider (see
docs/architecture/access-model.md → "Publishing: the publication target").
Constructors
| PublishDeps | |
Fields
| |
The serve action, and the router an adapter supplies
data RouteAction Source #
A matched route's response contract existentially paired with an action that can produce only that contract's response type.
The existential is the application boundary's proof: dispatch can render the action without knowing an ecosystem's response sum, while the action cannot be paired with a contract for another sum.
Constructors
| RouteAction (ResponseContract response) (ResponseAction response) |
data ResponseAction response Source #
How one matched request is served: by the proxy itself, or through the data plane.
This is the whole of what the web layer knows about a request. A route is an
ecosystem's own concern (npm's /{pkg}/-/{file}.tgz and RubyGems' /versions have
nothing in common but the fact that something must be done about them), so the mapping
from a request to an action is declared by the ecosystem's adapter, as its MountRouter.
What is shared is only the kind of thing an action can be:
- An
AnswerLocallyaction is a pure value admitted by the route's response contract, so the dispatcher simply responds with it: no upstream round-trip, no effects. - A
RunPipelineaction is a data-plane handler awaiting the request and its typed respond continuation, so the dispatcher discharges it toIOunder the request perimeter (the guard that answers an escaped fault with the route's declared neutral500). Those handlers (Ecluse.Core.Server.Pipeline) are themselves ecosystem-neutral: a registry's client, projection, and document assembly reach them as injected capabilities onPackumentDeps, never as imports. So two ecosystems whose URL grammars share nothing can still route onto the same handler, and one with a route the other lacks simply names a different action.
Being a closed sum of exactly these two is what lets the front door serve a request without knowing what the request is.
It lives here, beside MountBinding and Handler, because the three are one
mutually-recursive knot: a mount carries a router, the router names an action, and an
action is a handler that reads the mount.
Constructors
| AnswerLocally response | A pure value admitted by the route's response contract. |
| RunPipeline response (Request -> (response -> IO ResponseReceived) -> Handler ResponseReceived) | A data-plane handler and its pre-commit perimeter fallback. The handler receives
only the responder for this |
type MountRouter = Method -> [Text] -> RouteAction Source #
An ecosystem's whole routing decision: what to do with a mount-relative request.
The adapter supplies one (serveRouter), derived from
its own declarative route table, so the ecosystem owns both halves of the decision: which
of its paths a request names, and what action that names. A path the ecosystem does not
recognise yields its deny-by-default 404
(notFoundInMount).
The Method is part of the mapping because the same path names different actions by
method (npm's GET /{pkg} reads, PUT /{pkg} publishes), and because a HEAD is a
bodiless variation of its GET rather than a distinct action, which the router
resolves by selecting the head-mode handler. Segments arrive already mount-stripped and
percent-decoded.
Mount binding
data MountBinding Source #
A mount: a path prefix bound to a registry, carrying that registry's
complete ecosystem wiring. Dispatch matches a request's leading path segments
to bindingPrefix, strips them, and routes the remainder through the rest of the
binding.
The prefix is a NonEmpty list of segments ("npm" :| [] for a /npm mount):
every registry is path-mounted, so a root mount -- which would force a URL change
on every consumer the day a second ecosystem is added -- is unrepresentable
rather than merely discouraged. Bundling the classifier and serve dependencies into one
record means a mount cannot be half-wired: there is no default to fall back to.
Constructors
| MountBinding | |
Fields
| |
Per-request context
data RequestCtx Source #
The context one request is served through: the request runtime ServeRuntime
paired with the MountBinding the request matched. A concrete record with plain
accessors -- ctxRuntime and ctxMount -- so a handler reads the shared runtime and its
per-mount wiring from one place rather than as explicit arguments.
Dispatch builds it once per request; the handler reads it through the Handler reader.
Constructors
| RequestCtx | |
Fields
| |
Instances
| MonadReader RequestCtx Handler Source # | |
Defined in Ecluse.Core.Server.Context Methods ask :: Handler RequestCtx # local :: (RequestCtx -> RequestCtx) -> Handler a -> Handler a # reader :: (RequestCtx -> a) -> Handler a # | |
The handler monad
The request hot path's monad: a reader over the per-request RequestCtx
layered on katip's logging context.
A newtype over so its instances
are this module's to control and call sites name one concrete monad. The derived
instances give reader access to the context (ReaderT RequestCtx (KatipContextT IO)MonadReader RequestCtx), arbitrary
effects (MonadIO), the unlift capability (MonadUnliftIO) the serve path's
concurrently/bracket need, and the katip classes (Katip, KatipContext)
so a structured log call composes through the ambient context the dispatch boundary
establishes.
The katip base is a reader, never a StateT, so logging context behaves
correctly across the serve path's concurrent fetches (see
docs/architecture/technology-stack.md → "Key Decisions").
Instances
| MonadIO Handler Source # | |
Defined in Ecluse.Core.Server.Context | |
| Applicative Handler Source # | |
| Functor Handler Source # | |
| Monad Handler Source # | |
| Katip Handler Source # | |
| KatipContext Handler Source # | |
Defined in Ecluse.Core.Server.Context Methods getKatipContext :: Handler LogContexts Source # localKatipContext :: (LogContexts -> LogContexts) -> Handler a -> Handler a Source # getKatipNamespace :: Handler Namespace Source # localKatipNamespace :: (Namespace -> Namespace) -> Handler a -> Handler a Source # | |
| MonadUnliftIO Handler Source # | |
Defined in Ecluse.Core.Server.Context | |
| MonadReader RequestCtx Handler Source # | |
Defined in Ecluse.Core.Server.Context Methods ask :: Handler RequestCtx # local :: (RequestCtx -> RequestCtx) -> Handler a -> Handler a # reader :: (RequestCtx -> a) -> Handler a # | |
runHandler :: LogEnv -> SimpleLogPayload -> RequestCtx -> Handler a -> IO a Source #
Run a Handler against the RequestCtx dispatch built for the request and the
katip logging environment and initial context the dispatch boundary supplies,
yielding the underlying IO action the server's continuation runs in. This is the
boundary where the serve path's Handler code is discharged to IO.
The LogEnv (the structured-log scribes) and the initial context payload are passed
in rather than read from the runtime, so the application owns the log stream and the
trace-correlation dd enrichment: it resolves the dd object for the request and
hands it here as the initial context, so every line a handler emits carries dd for
trace-to-log correlation. A handler narrows the namespace or adds package/version/rule
context with katip's combinators on top as it logs.