module Ecluse.Core.Registry.Npm (
NpmClientConfig (..),
defaultNpmConfig,
publicRegistryBaseUrl,
publicRegistryUrl,
newNpmClient,
newNpmPublishClient,
fetchMetadataForm,
relayPublishDocument,
ResponseBoundExceeded (..),
) where
import Data.ByteString.Lazy qualified as LBS
import Data.List.NonEmpty qualified as NE
import Network.HTTP.Client (
BodyReader,
Manager,
Request,
Response (responseStatus),
brRead,
httpLbs,
responseBody,
withResponse,
)
import Network.HTTP.Types.Status (statusCode)
import UnliftIO (throwIO)
import Ecluse.Core.Credential (Secret)
import Ecluse.Core.Package (Hash (hashAlg, hashValue), HashAlg (SHA1, SRI), PackageName)
import Ecluse.Core.Queue (MirrorArtifact (maFilename, maHashes))
import Ecluse.Core.Registry (
ParseError (ParseError),
PublishError (..),
PublishFault (PublishRejected, PublishUrlUnformable),
PublishRelayResponse (..),
RegistryClient (..),
RegistryResponse (RegistryResponse),
UrlFormationError,
)
import Ecluse.Core.Registry.Npm.Project qualified as Project
import Ecluse.Core.Registry.Npm.Publish (npmPublishDocument, publishRequest)
import Ecluse.Core.Registry.Npm.Request (
MetadataForm (Abbreviated),
Validators,
artifactRequest,
metadataRequest,
noValidators,
)
import Ecluse.Core.Security (
LimitError,
Limits,
boundedRead,
defaultLimits,
)
import Ecluse.Core.Security.Egress.Internal (RegistryUrl (RegistryUrl))
import Ecluse.Core.Version (Version)
data NpmClientConfig = NpmClientConfig
{ NpmClientConfig -> Text
npmBaseUrl :: Text
, NpmClientConfig -> Manager
npmManager :: Manager
, NpmClientConfig -> Maybe Secret
npmToken :: Maybe Secret
, NpmClientConfig -> Limits
npmLimits :: Limits
}
publicRegistryBaseUrl :: Text
publicRegistryBaseUrl :: Text
publicRegistryBaseUrl = Text
"https://registry.npmjs.org"
publicRegistryUrl :: RegistryUrl
publicRegistryUrl :: RegistryUrl
publicRegistryUrl = Text -> RegistryUrl
RegistryUrl Text
publicRegistryBaseUrl
defaultNpmConfig :: Manager -> NpmClientConfig
defaultNpmConfig :: Manager -> NpmClientConfig
defaultNpmConfig Manager
manager =
NpmClientConfig
{ npmBaseUrl :: Text
npmBaseUrl = Text
publicRegistryBaseUrl
, npmManager :: Manager
npmManager = Manager
manager
, npmToken :: Maybe Secret
npmToken = Maybe Secret
forall a. Maybe a
Nothing
, npmLimits :: Limits
npmLimits = Limits
defaultLimits
}
newNpmClient :: NpmClientConfig -> IO RegistryClient
newNpmClient :: NpmClientConfig -> IO RegistryClient
newNpmClient NpmClientConfig
config = NpmClientConfig -> IO (Maybe Secret) -> IO RegistryClient
newNpmPublishClient NpmClientConfig
config (Maybe Secret -> IO (Maybe Secret)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (NpmClientConfig -> Maybe Secret
npmToken NpmClientConfig
config))
newNpmPublishClient :: NpmClientConfig -> IO (Maybe Secret) -> IO RegistryClient
newNpmPublishClient :: NpmClientConfig -> IO (Maybe Secret) -> IO RegistryClient
newNpmPublishClient NpmClientConfig
config IO (Maybe Secret)
mintToken =
RegistryClient -> IO RegistryClient
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure
RegistryClient
{ fetchMetadata :: PackageName -> IO RegistryResponse
fetchMetadata = \PackageName
name -> do
token <- IO (Maybe Secret)
mintToken
fetchMetadataForm config{npmToken = token} Abbreviated noValidators name
, fetchArtifact :: PackageName -> Version -> IO RegistryResponse
fetchArtifact = NpmClientConfig -> PackageName -> Version -> IO RegistryResponse
fetchArtifact' NpmClientConfig
config
, publishArtifact :: PackageName
-> Version
-> MirrorArtifact
-> ByteString
-> IO (Either PublishFault ())
publishArtifact = NpmClientConfig
-> IO (Maybe Secret)
-> PackageName
-> Version
-> MirrorArtifact
-> ByteString
-> IO (Either PublishFault ())
publishArtifact' NpmClientConfig
config IO (Maybe Secret)
mintToken
,
parsePackageInfo :: PackageName -> RegistryResponse -> Either ParseError PackageInfo
parsePackageInfo = \PackageName
name RegistryResponse
resp -> Text -> PackageInfo -> PackageInfo
Project.enforceTarballScheme Text
upstreamBaseUrl (PackageInfo -> PackageInfo)
-> Either ParseError PackageInfo -> Either ParseError PackageInfo
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> PackageName -> RegistryResponse -> Either ParseError PackageInfo
Project.parsePackageInfo PackageName
name RegistryResponse
resp
, parseVersionDetails :: RegistryResponse -> Version -> Either ParseError PackageDetails
parseVersionDetails = \RegistryResponse
resp Version
version ->
RegistryResponse -> Version -> Either ParseError PackageDetails
Project.parseVersionDetails RegistryResponse
resp Version
version
Either ParseError PackageDetails
-> (PackageDetails -> Either ParseError PackageDetails)
-> Either ParseError PackageDetails
forall a b.
Either ParseError a
-> (a -> Either ParseError b) -> Either ParseError b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Either ParseError PackageDetails
-> (PackageDetails -> Either ParseError PackageDetails)
-> Maybe PackageDetails
-> Either ParseError PackageDetails
forall b a. b -> (a -> b) -> Maybe a -> b
maybe (ParseError -> Either ParseError PackageDetails
forall a b. a -> Either a b
Left ParseError
tarballNotHttps) PackageDetails -> Either ParseError PackageDetails
forall a b. b -> Either a b
Right (Maybe PackageDetails -> Either ParseError PackageDetails)
-> (PackageDetails -> Maybe PackageDetails)
-> PackageDetails
-> Either ParseError PackageDetails
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> PackageDetails -> Maybe PackageDetails
Project.enforceTarballSchemeDetails Text
upstreamBaseUrl
, parseVersionList :: RegistryResponse -> Either ParseError [Version]
parseVersionList = RegistryResponse -> Either ParseError [Version]
Project.parseVersionList
}
where
upstreamBaseUrl :: Text
upstreamBaseUrl = NpmClientConfig -> Text
npmBaseUrl NpmClientConfig
config
tarballNotHttps :: ParseError
tarballNotHttps =
Text -> ParseError
ParseError Text
"the requested version's dist.tarball is not an https URL on the upstream host"
fetchMetadataForm ::
NpmClientConfig ->
MetadataForm ->
Validators ->
PackageName ->
IO RegistryResponse
fetchMetadataForm :: NpmClientConfig
-> MetadataForm -> Validators -> PackageName -> IO RegistryResponse
fetchMetadataForm NpmClientConfig
config MetadataForm
form Validators
validators PackageName
name = do
request <- Either UrlFormationError Request -> IO Request
orThrow (Text
-> Maybe Secret
-> MetadataForm
-> Validators
-> PackageName
-> Either UrlFormationError Request
metadataRequest (NpmClientConfig -> Text
npmBaseUrl NpmClientConfig
config) (NpmClientConfig -> Maybe Secret
npmToken NpmClientConfig
config) MetadataForm
form Validators
validators PackageName
name)
withResponse request (npmManager config) $ \Response BodyReader
response ->
Limits -> BodyReader -> IO RegistryResponse
readBoundedBody (NpmClientConfig -> Limits
npmLimits NpmClientConfig
config) (Response BodyReader -> BodyReader
forall body. Response body -> body
responseBody Response BodyReader
response)
newtype ResponseBoundExceeded = ResponseBoundExceeded LimitError
deriving stock (ResponseBoundExceeded -> ResponseBoundExceeded -> Bool
(ResponseBoundExceeded -> ResponseBoundExceeded -> Bool)
-> (ResponseBoundExceeded -> ResponseBoundExceeded -> Bool)
-> Eq ResponseBoundExceeded
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ResponseBoundExceeded -> ResponseBoundExceeded -> Bool
== :: ResponseBoundExceeded -> ResponseBoundExceeded -> Bool
$c/= :: ResponseBoundExceeded -> ResponseBoundExceeded -> Bool
/= :: ResponseBoundExceeded -> ResponseBoundExceeded -> Bool
Eq, Int -> ResponseBoundExceeded -> ShowS
[ResponseBoundExceeded] -> ShowS
ResponseBoundExceeded -> String
(Int -> ResponseBoundExceeded -> ShowS)
-> (ResponseBoundExceeded -> String)
-> ([ResponseBoundExceeded] -> ShowS)
-> Show ResponseBoundExceeded
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ResponseBoundExceeded -> ShowS
showsPrec :: Int -> ResponseBoundExceeded -> ShowS
$cshow :: ResponseBoundExceeded -> String
show :: ResponseBoundExceeded -> String
$cshowList :: [ResponseBoundExceeded] -> ShowS
showList :: [ResponseBoundExceeded] -> ShowS
Show)
instance Exception ResponseBoundExceeded
readBoundedBody :: Limits -> BodyReader -> IO RegistryResponse
readBoundedBody :: Limits -> BodyReader -> IO RegistryResponse
readBoundedBody Limits
limits BodyReader
bodyReader =
Limits -> BodyReader -> IO (Either LimitError ByteString)
forall (m :: * -> *).
Monad m =>
Limits -> m ByteString -> m (Either LimitError ByteString)
boundedRead Limits
limits (BodyReader -> BodyReader
brRead BodyReader
bodyReader) IO (Either LimitError ByteString)
-> (Either LimitError ByteString -> IO RegistryResponse)
-> IO RegistryResponse
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
Right ByteString
body -> RegistryResponse -> IO RegistryResponse
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (ByteString -> RegistryResponse
RegistryResponse ByteString
body)
Left LimitError
err -> ResponseBoundExceeded -> IO RegistryResponse
forall (m :: * -> *) e a. (MonadIO m, Exception e) => e -> m a
throwIO (LimitError -> ResponseBoundExceeded
ResponseBoundExceeded LimitError
err)
fetchArtifact' :: NpmClientConfig -> PackageName -> Version -> IO RegistryResponse
fetchArtifact' :: NpmClientConfig -> PackageName -> Version -> IO RegistryResponse
fetchArtifact' NpmClientConfig
config PackageName
name Version
version = do
request <- Either UrlFormationError Request -> IO Request
orThrow (Text
-> Maybe Secret
-> PackageName
-> Version
-> Either UrlFormationError Request
artifactRequest (NpmClientConfig -> Text
npmBaseUrl NpmClientConfig
config) (NpmClientConfig -> Maybe Secret
npmToken NpmClientConfig
config) PackageName
name Version
version)
response <- httpLbs request (npmManager config)
pure (RegistryResponse (toStrict (responseBody response)))
publishArtifact' ::
NpmClientConfig ->
IO (Maybe Secret) ->
PackageName ->
Version ->
MirrorArtifact ->
ByteString ->
IO (Either PublishFault ())
publishArtifact' :: NpmClientConfig
-> IO (Maybe Secret)
-> PackageName
-> Version
-> MirrorArtifact
-> ByteString
-> IO (Either PublishFault ())
publishArtifact' NpmClientConfig
config IO (Maybe Secret)
mintToken PackageName
name Version
version MirrorArtifact
artifact ByteString
tarball = do
token <- IO (Maybe Secret)
mintToken
let document = PackageName
-> Version
-> Text
-> Maybe Text
-> Maybe Text
-> ByteString
-> ByteString
npmPublishDocument PackageName
name Version
version (MirrorArtifact -> Text
maFilename MirrorArtifact
artifact) (MirrorArtifact -> Maybe Text
sriOf MirrorArtifact
artifact) (MirrorArtifact -> Maybe Text
sha1Of MirrorArtifact
artifact) ByteString
tarball
case publishRequest (npmBaseUrl config) token name document of
Left UrlFormationError
urlErr -> Either PublishFault () -> IO (Either PublishFault ())
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (PublishFault -> Either PublishFault ()
forall a b. a -> Either a b
Left (UrlFormationError -> PublishFault
PublishUrlUnformable UrlFormationError
urlErr))
Right Request
request -> do
response <- Request -> Manager -> IO (Response ByteString)
httpLbs Request
request (NpmClientConfig -> Manager
npmManager NpmClientConfig
config)
let code = Status -> Int
statusCode (Response ByteString -> Status
forall body. Response body -> Status
responseStatus Response ByteString
response)
pure (classifyPublish code)
classifyPublish :: Int -> Either PublishFault ()
classifyPublish :: Int -> Either PublishFault ()
classifyPublish Int
code
| Int
code Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
200 Bool -> Bool -> Bool
&& Int
code Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
< Int
300 = () -> Either PublishFault ()
forall a b. b -> Either a b
Right ()
| Int
code Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
409 = () -> Either PublishFault ()
forall a b. b -> Either a b
Right ()
| Bool
otherwise =
PublishFault -> Either PublishFault ()
forall a b. a -> Either a b
Left (PublishError -> PublishFault
PublishRejected (Text -> PublishError
PublishError (Text
"publish failed with HTTP status " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Int -> Text
forall b a. (Show a, IsString b) => a -> b
show Int
code)))
relayPublishDocument ::
NpmClientConfig ->
PackageName ->
ByteString ->
IO (Either UrlFormationError PublishRelayResponse)
relayPublishDocument :: NpmClientConfig
-> PackageName
-> ByteString
-> IO (Either UrlFormationError PublishRelayResponse)
relayPublishDocument NpmClientConfig
config PackageName
name ByteString
document =
case Text
-> Maybe Secret
-> PackageName
-> ByteString
-> Either UrlFormationError Request
publishRequest (NpmClientConfig -> Text
npmBaseUrl NpmClientConfig
config) (NpmClientConfig -> Maybe Secret
npmToken NpmClientConfig
config) PackageName
name ByteString
document of
Left UrlFormationError
urlErr -> Either UrlFormationError PublishRelayResponse
-> IO (Either UrlFormationError PublishRelayResponse)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (UrlFormationError -> Either UrlFormationError PublishRelayResponse
forall a b. a -> Either a b
Left UrlFormationError
urlErr)
Right Request
request ->
Request
-> Manager
-> (Response BodyReader
-> IO (Either UrlFormationError PublishRelayResponse))
-> IO (Either UrlFormationError PublishRelayResponse)
forall a.
Request -> Manager -> (Response BodyReader -> IO a) -> IO a
withResponse Request
request (NpmClientConfig -> Manager
npmManager NpmClientConfig
config) ((Response BodyReader
-> IO (Either UrlFormationError PublishRelayResponse))
-> IO (Either UrlFormationError PublishRelayResponse))
-> (Response BodyReader
-> IO (Either UrlFormationError PublishRelayResponse))
-> IO (Either UrlFormationError PublishRelayResponse)
forall a b. (a -> b) -> a -> b
$
(PublishRelayResponse
-> Either UrlFormationError PublishRelayResponse)
-> IO PublishRelayResponse
-> IO (Either UrlFormationError PublishRelayResponse)
forall a b. (a -> b) -> IO a -> IO b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap PublishRelayResponse
-> Either UrlFormationError PublishRelayResponse
forall a b. b -> Either a b
Right (IO PublishRelayResponse
-> IO (Either UrlFormationError PublishRelayResponse))
-> (Response BodyReader -> IO PublishRelayResponse)
-> Response BodyReader
-> IO (Either UrlFormationError PublishRelayResponse)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Limits -> Response BodyReader -> IO PublishRelayResponse
readRelayResponse (NpmClientConfig -> Limits
npmLimits NpmClientConfig
config)
readRelayResponse :: Limits -> Response BodyReader -> IO PublishRelayResponse
readRelayResponse :: Limits -> Response BodyReader -> IO PublishRelayResponse
readRelayResponse Limits
limits Response BodyReader
response = do
RegistryResponse body <- Limits -> BodyReader -> IO RegistryResponse
readBoundedBody Limits
limits (Response BodyReader -> BodyReader
forall body. Response body -> body
responseBody Response BodyReader
response)
pure
PublishRelayResponse
{ relayStatus = statusCode (responseStatus response)
, relayBody = LBS.fromStrict body
}
orThrow :: Either UrlFormationError Request -> IO Request
orThrow :: Either UrlFormationError Request -> IO Request
orThrow = \case
Left UrlFormationError
err -> UrlFormationError -> IO Request
forall (m :: * -> *) e a. (MonadIO m, Exception e) => e -> m a
throwIO UrlFormationError
err
Right Request
request -> Request -> IO Request
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Request
request
sriOf :: MirrorArtifact -> Maybe Text
sriOf :: MirrorArtifact -> Maybe Text
sriOf = HashAlg -> MirrorArtifact -> Maybe Text
firstHashValue HashAlg
SRI
sha1Of :: MirrorArtifact -> Maybe Text
sha1Of :: MirrorArtifact -> Maybe Text
sha1Of = HashAlg -> MirrorArtifact -> Maybe Text
firstHashValue HashAlg
SHA1
firstHashValue :: HashAlg -> MirrorArtifact -> Maybe Text
firstHashValue :: HashAlg -> MirrorArtifact -> Maybe Text
firstHashValue HashAlg
alg MirrorArtifact
artifact =
(Hash -> Text) -> Maybe Hash -> Maybe Text
forall a b. (a -> b) -> Maybe a -> Maybe b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap Hash -> Text
hashValue ((Hash -> Bool) -> [Hash] -> Maybe Hash
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find ((HashAlg -> HashAlg -> Bool
forall a. Eq a => a -> a -> Bool
== HashAlg
alg) (HashAlg -> Bool) -> (Hash -> HashAlg) -> Hash -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Hash -> HashAlg
hashAlg) (NonEmpty Hash -> [Hash]
forall a. NonEmpty a -> [a]
NE.toList (MirrorArtifact -> NonEmpty Hash
maHashes MirrorArtifact
artifact)))