| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Ecluse.Core.Registry.Npm.Filter
Description
The two pure transforms an npm packument needs before Écluse serves it:
rewrite the embedded artifact URLs under the mount's prefix, and assemble the
served document from a cross-upstream MergePlan and the raw source documents.
Both transforms operate structurally over the raw aeson Value, never by
re-serialising a typed model. This is load-bearing: the served packument is an
open document -- its schema is additionalProperties: true (see
docs/architecture/api-surface.md → "The synthesized-packument schema") -- so
any field Écluse does not model (author keys, registry bookkeeping, per-version
extras) must be relayed unchanged. Building the served body from the raw
Values keeps every unmodelled key; rebuilding it from Ecluse.Core.Package
would silently drop them.
The decision/replay split
Which versions survive, which source wins each one, where dist-tags.latest
resolves, and each surviving version's publish instant are the ecosystem-agnostic
decisions, taken over the typed PackageInfo by
Ecluse.Core.Package.Filter and Ecluse.Core.Package.Merge and handed here as a
MergePlan. This module owns the npm wire-shape assembly: rebuilding
versions/dist-tags/time onto the base document from the plan, and the
tarball-URL rewrite over the raw upstream bytes. The npm wire knowledge lives
here; the decision logic does not (it is reused by every ecosystem). See
docs/architecture/registry-model.md → "Decision surface vs served surface".
URL rewriting
rewriteVersion rewrites one version object's dist.tarball to
{mount-base}/{pkg}/-/{file}, so a client resolving metadata through the
proxy also downloads the bytes through it rather than going straight to upstream
and bypassing the gate (see docs/architecture/web-layer.md → "Multi-ecosystem
mounts", whose URL rewriting is load-bearing). Keeping artifacts same-host also keeps npm's auth
flowing, which a separate artifact host would silently drop. The
{mount-base}/{pkg} prefix is supplied by the caller;
assembleMergedPackument derives it from the mount base and the document's own
safety-gated name as it places each surviving version. The transform performs
no IO. It is idempotent: re-deriving {file} from an already-rewritten URL
yields the same URL, so applying it more than once is safe.
Assembling the served document
assembleMergedPackument replays a MergePlan onto the raw source Values in
one pass: each surviving version's object is taken from the raw document of the
source that won it (so the served bytes are the winning upstream's, unmodelled keys
and all) with its dist.tarball rewritten under the mount base as it is placed;
dist-tags and time are rebuilt from the plan's reconciled decisions (the times
as normalised ISO-8601, with the base document's created/modified bookkeeping
retained); every other top-level key is relayed from the base document. A version
not in the plan's survivors is simply never taken, so a client's resolver only ever
sees admitted versions (presence in the packument is availability -- see
docs/research/reverse-engineering/npm.md §8).
The fused single pass is deliberate: restricting, assembling, and rewriting as
separate whole-document edits would rebuild a many-version packument several times
per request, and this transform sits on the serve path's hot loop (see
docs/architecture/performance.md). The rewrite gates the interpolated name:
the base document's own name is validated component-wise (safeName) before it
is interpolated, and a document with no usable name has no URLs rewritten.
Synopsis
- rewriteVersion :: Text -> Value -> Value
- assembleMergedPackument :: Text -> Map SourceId Value -> MergePlan -> Value -> Value
- assembleMergedDocument :: Text -> Map SourceId CachedDoc -> MergePlan -> Maybe CachedDoc -> CachedDoc
- serialiseMergedDocument :: CachedDoc -> LByteString
URL rewriting
rewriteVersion :: Text -> Value -> Value Source #
Rewrite one version object's dist.tarball to {prefix}/-/{file}, so the
artifact is fetched back through this mount rather than directly from upstream.
prefix is the mount's {base}/{pkg} -- the externally-visible base URL joined
with the package's URL form -- supplied by the caller. {file} is the existing
tarball URL's last path segment (the artifact filename), preserved verbatim so
the bytes a client integrity-checks are unchanged.
Total and lossless: a version with no dist object, no tarball string, or a
tarball with no filename segment is left untouched; every unmodelled key is
relayed unchanged. Rewriting is idempotent -- a second pass derives the same
{file} and so produces the same URL.
A {pkg} read from a document's own name is upstream-controlled, so it
must be gated component-wise through "Ecluse.Core.Server.Route.isSafeComponent"
before it reaches the prefix: assembleMergedPackument performs that gate as it
places each surviving version, and a caller building its own prefix owns it.
Assembling the served document
assembleMergedPackument :: Text -> Map SourceId Value -> MergePlan -> Value -> Value Source #
Assemble the served packument from a MergePlan and the raw source documents:
rebuild versions, dist-tags, and time from the plan onto the base document,
rewriting each surviving version's dist.tarball under mountBase in the same
pass. Other top-level keys are inherited from the base document.
The plan was decided over the projected PackageInfos (the
typed views of the same documents), but the assembly reads the raw Values, so
unmodelled fields survive (see the module header). Each surviving version's object
is taken from the source that won its key (mpSurvivors); a survivor whose source
object is missing is dropped rather than fabricated, so coherence with the plan is
preserved by construction. dist-tags is the plan's reconciled map (mpDistTags:
latest resolved, absent-target tags dropped); time is the plan's
surviving-version instants (mpTime, rendered as normalised ISO-8601) plus the
base document's non-version created/modified bookkeeping.
The tarball rewrite applies rewriteVersion to each surviving version as it is
placed, so the versions object is built once rather than rebuilt by a second
whole-document pass; the interpolated prefix is gated on the base document's own
name (validated by safeName), with no rewrite when the name is unusable.
The caller decides what to do with an empty plan; an empty mpSurvivors simply
assembles an empty versions object. A non-object base document contributes no
top-level keys and no bookkeeping (the plan-owned keys are still assembled), so the
result is always an object.
The served-document boundary (npm's CachedDoc capabilities)
assembleMergedDocument :: Text -> Map SourceId CachedDoc -> MergePlan -> Maybe CachedDoc -> CachedDoc Source #
npm's served-document assemble capability
(metadataAssemble): project each per-source
CachedDoc and the precedence-winning base document into npm's Value, replay the plan
through assembleMergedPackument, and inject the assembled Value back. The neutral
pipeline threads the documents opaquely; the projection/injection is npm's boundary.
serialiseMergedDocument :: CachedDoc -> LByteString Source #
npm's served-document serialise capability
(metadataSerialise): project the assembled
CachedDoc to npm's Value and encode it compactly to the wire bytes.