module Ecluse.Core.Registry.Npm.Metadata (
newNpmMetadataClient,
fetchNpmManifest,
fetchNpmVersion,
projectNpmManifest,
projectNpmVersion,
) where
import Data.Aeson (Value, eitherDecodeStrict, parseJSON)
import Data.Aeson.Types (parseMaybe)
import Data.Time (UTCTime)
import Ecluse.Core.Ecosystem (Ecosystem (Npm))
import Ecluse.Core.Package (
InvalidEntry,
PackageDetails,
PackageInfo,
PackageName,
renderPackageName,
)
import Ecluse.Core.Package.Filter (enforceArtifactScheme, enforceArtifactSchemeDetails)
import Ecluse.Core.Registry (RegistryResponse (responseBody))
import Ecluse.Core.Registry.CachedDocument (npmCached)
import Ecluse.Core.Registry.Metadata (
Manifest (Manifest, manifestDigest, manifestInfo, manifestRaw),
MetadataClient,
MetadataError (MetadataBoundExceeded, MetadataNameMismatch, MetadataUndecodable),
digestOf,
fetchFaultError,
)
import Ecluse.Core.Registry.Npm (
NpmClientConfig (npmBaseUrl, npmLimits),
fetchMetadataFormBounded,
)
import Ecluse.Core.Registry.Npm.Project (
Projection (NameMismatch, Projected),
parsePackageInfoFromValue,
projectName,
projectVersionEntry,
)
import Ecluse.Core.Registry.Npm.Request (
MetadataForm (Full),
noValidators,
)
import Ecluse.Core.Registry.Npm.SelectiveDecode (
SelectedVersion (svName, svTime, svVersion, svVersionCount),
SelectiveError (SelectiveTooDeeplyNested, SelectiveUndecodable),
selectVersionFromPackument,
)
import Ecluse.Core.Security (
LimitError (TooDeeplyNested, TooManyVersions),
Limits,
checkNestingDepth,
checkVersionCount,
maxNestingDepth,
maxVersionCount,
)
import Ecluse.Core.Server.Metadata (ManifestCaching, newMetadataClient)
import Ecluse.Core.Telemetry.Metrics qualified as Metric
import Ecluse.Core.Telemetry.Record (MetricsPort)
import Ecluse.Core.Telemetry.Span (TracingPort (spanMetadataDecode, spanMetadataFetch))
import Ecluse.Core.Version (Version, mkVersion, renderVersion)
newNpmMetadataClient ::
TracingPort ->
MetricsPort ->
Metric.Upstream ->
ManifestCaching ->
(PackageName -> MetadataError -> IO ()) ->
(PackageName -> [InvalidEntry] -> IO ()) ->
(PackageName -> IO ()) ->
NpmClientConfig ->
MetadataClient
newNpmMetadataClient :: TracingPort
-> MetricsPort
-> Upstream
-> ManifestCaching
-> (PackageName -> MetadataError -> IO ())
-> (PackageName -> [InvalidEntry] -> IO ())
-> (PackageName -> IO ())
-> NpmClientConfig
-> MetadataClient
newNpmMetadataClient TracingPort
tracing MetricsPort
metrics Upstream
upstream ManifestCaching
caching PackageName -> MetadataError -> IO ()
logFailure PackageName -> [InvalidEntry] -> IO ()
logInvalid PackageName -> IO ()
logFetch NpmClientConfig
config =
MetricsPort
-> Upstream
-> ManifestCaching
-> (PackageName -> MetadataError -> IO ())
-> (PackageName -> [InvalidEntry] -> IO ())
-> (PackageName -> IO ())
-> (PackageName -> IO (Either MetadataError Manifest))
-> (PackageName
-> Version -> IO (Either MetadataError (Maybe PackageDetails)))
-> MetadataClient
newMetadataClient MetricsPort
metrics Upstream
upstream ManifestCaching
caching PackageName -> MetadataError -> IO ()
logFailure PackageName -> [InvalidEntry] -> IO ()
logInvalid PackageName -> IO ()
logFetch (TracingPort
-> NpmClientConfig
-> PackageName
-> IO (Either MetadataError Manifest)
fetchNpmManifest TracingPort
tracing NpmClientConfig
config) (TracingPort
-> NpmClientConfig
-> PackageName
-> Version
-> IO (Either MetadataError (Maybe PackageDetails))
fetchNpmVersion TracingPort
tracing NpmClientConfig
config)
fetchNpmManifest :: TracingPort -> NpmClientConfig -> PackageName -> IO (Either MetadataError Manifest)
fetchNpmManifest :: TracingPort
-> NpmClientConfig
-> PackageName
-> IO (Either MetadataError Manifest)
fetchNpmManifest TracingPort
tracing NpmClientConfig
config PackageName
name =
TracingPort -> forall a. PackageName -> IO a -> IO a
spanMetadataFetch TracingPort
tracing PackageName
name (NpmClientConfig
-> MetadataForm
-> Validators
-> PackageName
-> IO (Either FetchFault RegistryResponse)
fetchMetadataFormBounded NpmClientConfig
config MetadataForm
Full Validators
noValidators PackageName
name) IO (Either FetchFault RegistryResponse)
-> (Either FetchFault RegistryResponse
-> IO (Either MetadataError Manifest))
-> IO (Either MetadataError Manifest)
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
Left FetchFault
fault -> Either MetadataError Manifest -> IO (Either MetadataError Manifest)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (MetadataError -> Either MetadataError Manifest
forall a b. a -> Either a b
Left (FetchFault -> MetadataError
fetchFaultError FetchFault
fault))
Right RegistryResponse
response ->
let body :: ByteString
body = RegistryResponse -> ByteString
responseBody RegistryResponse
response
in TracingPort -> forall a. PackageName -> IO a -> IO a
spanMetadataDecode TracingPort
tracing PackageName
name (IO (Either MetadataError Manifest)
-> IO (Either MetadataError Manifest))
-> IO (Either MetadataError Manifest)
-> IO (Either MetadataError Manifest)
forall a b. (a -> b) -> a -> b
$
Either MetadataError Manifest -> IO (Either MetadataError Manifest)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure
( ContentDigest -> (PackageInfo, Value) -> Manifest
manifestOf (ByteString -> ContentDigest
digestOf ByteString
body) ((PackageInfo, Value) -> Manifest)
-> ((PackageInfo, Value) -> (PackageInfo, Value))
-> (PackageInfo, Value)
-> Manifest
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (PackageInfo -> PackageInfo)
-> (PackageInfo, Value) -> (PackageInfo, Value)
forall a b c. (a -> b) -> (a, c) -> (b, c)
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first (Text -> PackageInfo -> PackageInfo
enforceArtifactScheme (NpmClientConfig -> Text
npmBaseUrl NpmClientConfig
config))
((PackageInfo, Value) -> Manifest)
-> Either MetadataError (PackageInfo, Value)
-> Either MetadataError Manifest
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Limits
-> PackageName
-> ByteString
-> Either MetadataError (PackageInfo, Value)
projectNpmManifest (NpmClientConfig -> Limits
npmLimits NpmClientConfig
config) PackageName
name ByteString
body
)
where
manifestOf :: ContentDigest -> (PackageInfo, Value) -> Manifest
manifestOf ContentDigest
digest (PackageInfo
info, Value
raw) = Manifest{manifestInfo :: PackageInfo
manifestInfo = PackageInfo
info, manifestRaw :: CachedDoc
manifestRaw = (Value -> CachedDoc, CachedDoc -> Maybe Value)
-> Value -> CachedDoc
forall a b. (a, b) -> a
fst (Value -> CachedDoc, CachedDoc -> Maybe Value)
npmCached Value
raw, manifestDigest :: ContentDigest
manifestDigest = ContentDigest
digest}
projectNpmManifest :: Limits -> PackageName -> ByteString -> Either MetadataError (PackageInfo, Value)
projectNpmManifest :: Limits
-> PackageName
-> ByteString
-> Either MetadataError (PackageInfo, Value)
projectNpmManifest Limits
limits PackageName
name ByteString
body = do
value <- (String -> MetadataError)
-> Either String Value -> Either MetadataError Value
forall a b c. (a -> b) -> Either a c -> Either b c
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first (MetadataError -> String -> MetadataError
forall a b. a -> b -> a
const MetadataError
MetadataUndecodable) (ByteString -> Either String Value
forall a. FromJSON a => ByteString -> Either String a
eitherDecodeStrict ByteString
body)
bounded <- first MetadataBoundExceeded (checkNestingDepth limits value)
info <- case parsePackageInfoFromValue name bounded of
Left ParseError
_ -> MetadataError -> Either MetadataError PackageInfo
forall a b. a -> Either a b
Left MetadataError
MetadataUndecodable
Right (NameMismatch Text
reported) -> MetadataError -> Either MetadataError PackageInfo
forall a b. a -> Either a b
Left (Text -> MetadataError
MetadataNameMismatch Text
reported)
Right (Projected PackageInfo
projected) -> PackageInfo -> Either MetadataError PackageInfo
forall a b. b -> Either a b
Right PackageInfo
projected
boundedInfo <- first MetadataBoundExceeded (checkVersionCount limits info)
pure (boundedInfo, bounded)
fetchNpmVersion :: TracingPort -> NpmClientConfig -> PackageName -> Version -> IO (Either MetadataError (Maybe PackageDetails))
fetchNpmVersion :: TracingPort
-> NpmClientConfig
-> PackageName
-> Version
-> IO (Either MetadataError (Maybe PackageDetails))
fetchNpmVersion TracingPort
tracing NpmClientConfig
config PackageName
name Version
version =
TracingPort -> forall a. PackageName -> IO a -> IO a
spanMetadataFetch TracingPort
tracing PackageName
name (NpmClientConfig
-> MetadataForm
-> Validators
-> PackageName
-> IO (Either FetchFault RegistryResponse)
fetchMetadataFormBounded NpmClientConfig
config MetadataForm
Full Validators
noValidators PackageName
name) IO (Either FetchFault RegistryResponse)
-> (Either FetchFault RegistryResponse
-> IO (Either MetadataError (Maybe PackageDetails)))
-> IO (Either MetadataError (Maybe PackageDetails))
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
Left FetchFault
fault -> Either MetadataError (Maybe PackageDetails)
-> IO (Either MetadataError (Maybe PackageDetails))
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (MetadataError -> Either MetadataError (Maybe PackageDetails)
forall a b. a -> Either a b
Left (FetchFault -> MetadataError
fetchFaultError FetchFault
fault))
Right RegistryResponse
response ->
TracingPort -> forall a. PackageName -> IO a -> IO a
spanMetadataDecode TracingPort
tracing PackageName
name (IO (Either MetadataError (Maybe PackageDetails))
-> IO (Either MetadataError (Maybe PackageDetails)))
-> IO (Either MetadataError (Maybe PackageDetails))
-> IO (Either MetadataError (Maybe PackageDetails))
forall a b. (a -> b) -> a -> b
$
Either MetadataError (Maybe PackageDetails)
-> IO (Either MetadataError (Maybe PackageDetails))
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ((Maybe PackageDetails
-> (PackageDetails -> Maybe PackageDetails) -> Maybe PackageDetails
forall a b. Maybe a -> (a -> Maybe b) -> Maybe b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Text -> PackageDetails -> Maybe PackageDetails
enforceArtifactSchemeDetails (NpmClientConfig -> Text
npmBaseUrl NpmClientConfig
config)) (Maybe PackageDetails -> Maybe PackageDetails)
-> Either MetadataError (Maybe PackageDetails)
-> Either MetadataError (Maybe PackageDetails)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Limits
-> PackageName
-> Version
-> ByteString
-> Either MetadataError (Maybe PackageDetails)
projectNpmVersion (NpmClientConfig -> Limits
npmLimits NpmClientConfig
config) PackageName
name Version
version (RegistryResponse -> ByteString
responseBody RegistryResponse
response))
projectNpmVersion :: Limits -> PackageName -> Version -> ByteString -> Either MetadataError (Maybe PackageDetails)
projectNpmVersion :: Limits
-> PackageName
-> Version
-> ByteString
-> Either MetadataError (Maybe PackageDetails)
projectNpmVersion Limits
limits PackageName
name Version
version ByteString
body = do
selected <- (SelectiveError -> MetadataError)
-> Either SelectiveError SelectedVersion
-> Either MetadataError SelectedVersion
forall a b c. (a -> b) -> Either a c -> Either b c
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first (Limits -> SelectiveError -> MetadataError
selectiveError Limits
limits) (Int
-> Version -> ByteString -> Either SelectiveError SelectedVersion
selectVersionFromPackument (Limits -> Int
maxNestingDepth Limits
limits) Version
version ByteString
body)
reported <- validateReportedName (svName selected)
when (reported /= name) (Left (MetadataNameMismatch (renderPackageName reported)))
when
(svVersionCount selected > maxVersionCount limits)
(Left (MetadataBoundExceeded (TooManyVersions (svVersionCount selected) (maxVersionCount limits))))
publishedAt <- parsePublishTime (svTime selected)
pure (svVersion selected >>= projectVersionEntry name (mkVersion Npm (renderVersion version)) publishedAt)
validateReportedName :: Maybe Value -> Either MetadataError PackageName
validateReportedName :: Maybe Value -> Either MetadataError PackageName
validateReportedName = \case
Maybe Value
Nothing -> MetadataError -> Either MetadataError PackageName
forall a b. a -> Either a b
Left MetadataError
MetadataUndecodable
Just Value
nameValue -> case (Value -> Parser Text) -> Value -> Maybe Text
forall a b. (a -> Parser b) -> a -> Maybe b
parseMaybe Value -> Parser Text
forall a. FromJSON a => Value -> Parser a
parseJSON Value
nameValue of
Maybe Text
Nothing -> MetadataError -> Either MetadataError PackageName
forall a b. a -> Either a b
Left MetadataError
MetadataUndecodable
Just Text
raw -> (ParseError -> MetadataError)
-> Either ParseError PackageName
-> Either MetadataError PackageName
forall a b c. (a -> b) -> Either a c -> Either b c
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first (MetadataError -> ParseError -> MetadataError
forall a b. a -> b -> a
const MetadataError
MetadataUndecodable) (Text -> Either ParseError PackageName
projectName Text
raw)
parsePublishTime :: Maybe Value -> Either MetadataError (Maybe UTCTime)
parsePublishTime :: Maybe Value -> Either MetadataError (Maybe UTCTime)
parsePublishTime = \case
Maybe Value
Nothing -> Maybe UTCTime -> Either MetadataError (Maybe UTCTime)
forall a b. b -> Either a b
Right Maybe UTCTime
forall a. Maybe a
Nothing
Just Value
timeValue -> Maybe UTCTime -> Either MetadataError (Maybe UTCTime)
forall a b. b -> Either a b
Right ((Value -> Parser UTCTime) -> Value -> Maybe UTCTime
forall a b. (a -> Parser b) -> a -> Maybe b
parseMaybe Value -> Parser UTCTime
forall a. FromJSON a => Value -> Parser a
parseJSON Value
timeValue)
selectiveError :: Limits -> SelectiveError -> MetadataError
selectiveError :: Limits -> SelectiveError -> MetadataError
selectiveError Limits
limits = \case
SelectiveError
SelectiveUndecodable -> MetadataError
MetadataUndecodable
SelectiveError
SelectiveTooDeeplyNested -> LimitError -> MetadataError
MetadataBoundExceeded (Int -> LimitError
TooDeeplyNested (Limits -> Int
maxNestingDepth Limits
limits))