From 259f0a9afc902a1739293ba71166d942a995b818 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 9 Jun 2026 12:43:23 +0200 Subject: [PATCH 1/3] WIP --- .../src/Wire/Options/Galley.hs | 2 +- services/brig/src/Brig/Options.hs | 158 +++++++++++++++++- 2 files changed, 156 insertions(+), 4 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/Options/Galley.hs b/libs/wire-subsystems/src/Wire/Options/Galley.hs index 6b30fff0976..76d25b5bc64 100644 --- a/libs/wire-subsystems/src/Wire/Options/Galley.hs +++ b/libs/wire-subsystems/src/Wire/Options/Galley.hs @@ -221,7 +221,7 @@ data Opts = Opts _federator :: !(Maybe Endpoint), -- | RabbitMQ settings, required when federation is enabled. _rabbitmq :: !(Maybe AmqpEndpoint), - -- | Disco URL + -- | Disco URL. TODO: Remove _discoUrl :: !(Maybe Text), -- | Other settings _settings :: !Settings, diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index da75b90138a..34ebb354406 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -24,6 +24,7 @@ module Brig.Options where +import Amazonka.Types (S3AddressingStyle) import Brig.Queue.Types (QueueOpts (..)) import Control.Applicative import Control.Lens hiding (Level, element, enum) @@ -355,6 +356,157 @@ instance ToSchema ListAllSFTServers where element "disabled" HideAllSFTServers ] +data WireConfig = WireConfig + { internalServices :: InternalServices, + externalServices :: ExternalServices, + settings :: WireSettings + } + +data InternalServices = InternalServices + { brig :: !Endpoint, + cargohold :: !Endpoint, + galley :: !Endpoint, + spar :: !Endpoint, + gundeck :: !Endpoint, + federatorInternal :: !(Maybe Endpoint), + wireServerEnterprise :: !(Maybe Endpoint) + } + +data ExternalServices = ExternalServices + { cassandraBrig :: !CassandraOpts, + cassandraGalley :: !CassandraOpts, + cassandraGundeck :: !CassandraOpts, + cassandraSpar :: !CassandraOpts, + elasticsearch :: !ElasticSearchOpts, + redis :: !RedisEndpoint, + redisAdditionalWrite :: !(Maybe RedisEndpoint), + -- | Postgresql settings, the key values must be in libpq format. + -- https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS + postgresql :: !(Map Text Text), + postgresqlPassword :: !(Maybe FilePathSecrets), + postgresqlPool :: !PoolConfig, + rabbitmq :: !AmqpEndpoint, + email :: !EmailOpts, + prekeySelection :: !PrekeySelectionOpts, + -- TODO: See if user and team journal can even be configured differently. If + -- everything is supposed to be used by ibis, we cannot actually configure + -- them seperately anyway. + userJournal :: !(Maybe SqsOpts), + teamJournal :: !(Maybe SqsOpts), + internalEvents :: !SqsOpts, + assets :: !AssetOpts + } + +data RedisConnectionMode + = Master + | Cluster + +data RedisEndpoint = RedisEndpoint + { _host :: !Text, + _port :: !Word16, + _connectionMode :: !RedisConnectionMode, + _enableTls :: !Bool, + -- | When not specified, use system CA bundle + _tlsCa :: !(Maybe FilePath), + -- | When 'True', uses TLS but does not verify hostname or CA or validity of + -- the cert. Not recommended to set to 'True'. + _insecureSkipVerifyTls :: !Bool + } + +data PrekeySelectionOpts + = RandomPrekeySelection + | DynamoDBPrekeySelection !DynamoDBPrekeySelectionOpts + +data DynamoDBPrekeySelectionOpts = DynamoDBPrekeySelectionOpts + { dynamoDBEndpoint :: !AWSEndpoint, + tableName :: !Text + } + +data SqsOpts = SqsOpts + { sqsEndpoint :: !AWSEndpoint, + queueName :: !Text + } + +data AssetOpts = AssetOpts + { s3Endpoint :: !AWSEndpoint, + -- | S3 can either by addressed in path style, i.e. + -- https:////, or vhost style, i.e. + -- https://./. AWS's S3 offering has + -- deprecated path style addressing for S3 and completely disabled it for + -- buckets created after 30 Sep 2020: + -- https://aws.amazon.com/blogs/aws/amazon-s3-path-deprecation-plan-the-rest-of-the-story/ + -- + -- However other object storage providers (specially self-deployed ones like + -- MinIO) may not support vhost style addressing yet (or ever?). Users of + -- such buckets should configure this option to "path". + -- + -- Installations using S3 service provided by AWS, should use "auto", this + -- option will ensure that vhost style is only used when it is possible to + -- construct a valid hostname from the bucket name and the bucket name + -- doesn't contain a '.'. Having a '.' in the bucket name causes TLS + -- validation to fail, hence it is not used by default. + -- + -- Using "virtual" as an option is only useful in situations where vhost + -- style addressing must be used even if it is not possible to construct a + -- valid hostname from the bucket name or the S3 service provider can ensure + -- correct certificate is issued for bucket which contain one or more '.'s + -- in the name. + -- + -- When this option is unspecified, we default to path style addressing to + -- ensure smooth transition for older deployments. + s3AddressingStyle :: !(Maybe OptS3AddressingStyle), + -- | S3 endpoint for generating download links. Useful if Cargohold is configured to use + -- an S3 replacement running inside the internal network (in which case internally we + -- would use one hostname for S3, and when generating an asset link for a client app, we + -- would use another hostname). + s3DownloadEndpoint :: !(Maybe AWSEndpoint), + s3Bucket :: !Text, + -- | Enable this option for compatibility with specific S3 backends. + s3Compatibility :: !(Maybe S3Compatibility), + cloudFront :: !(Maybe CloudFrontOpts), + -- | @Z-Host@ header to s3 download endpoint `Map` + -- + -- This logic is: If the @Z-Host@ header is provided and found in this map, + -- the map's values is taken as s3 download endpoint to redirect to; + -- otherwise a 404 is retuned. This option is only useful + -- in the context of multi-ingress setups where one backend / deployment is + -- reachable under several domains. + multiIngress :: !(Maybe (Map String AWSEndpoint)) + } + +newtype OptS3AddressingStyle = OptS3AddressingStyle + { unwrapS3AddressingStyle :: S3AddressingStyle + } + +data WireSettings = WireSettings + { turn :: !TurnOpts, + sft :: !(Maybe SFTOptions) + } + +data S3Compatibility + = -- | Scality RING, might also work for Zenko CloudServer + -- + S3CompatibilityScalityRing + +-- | AWS CloudFront settings. +data CloudFrontOpts = CloudFrontOpts + { -- | Domain + domain :: CFDomain, + -- | Keypair ID + keyPairId :: CFKeyPairId, + -- | Path to private key + privateKey :: FilePath + } + deriving (Show, Generic) + +-- TODO: This is copied from cargohold, dedupe +newtype CFKeyPairId = CFKeyPairId Text + deriving (Eq, Show, Generic) + +-- TODO: This is copied from cargohold, dedupe +newtype CFDomain = CFDomain Text + deriving (Eq, Show, Generic) + -- | Options that are consumed on startup data Opts = Opts -- services @@ -404,7 +556,7 @@ data Opts = Opts zauth :: !ZAuthOpts, -- Misc. - -- | Disco URL + -- | Disco URL, TODO: Remove. discoUrl :: !(Maybe Text), -- | Event queue for -- Brig-generated events (e.g. @@ -415,7 +567,7 @@ data Opts = Opts -- | Log level (Debug, Info, etc) logLevel :: !Level, -- | Use netstrings encoding (see - -- ) + -- ). TODO: Remove. logNetStrings :: !(Maybe (Last Bool)), -- | Logformat to use -- TURN @@ -440,7 +592,7 @@ data Settings = Settings teamInvitationTimeout :: !Timeout, -- | Check for expired users every so often, in seconds expiredUserCleanupTimeout :: !(Maybe Timeout), - -- | STOMP broker credentials + -- | STOMP broker credentials. TODO: Remove. stomp :: !(Maybe FilePathSecrets), -- | Whitelist of allowed emails/phones allowlistEmailDomains :: !(Maybe AllowlistEmailDomains), From e93bea46f3abbbbf40406cb3619349e0bd57ce1c Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 9 Jun 2026 16:20:46 +0200 Subject: [PATCH 2/3] More WIP --- services/brig/src/Brig/Options.hs | 325 ++++++++++++------------ services/gundeck/src/Gundeck/Options.hs | 1 + services/spar/src/Spar/Options.hs | 2 +- 3 files changed, 158 insertions(+), 170 deletions(-) diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 34ebb354406..c97b031d73c 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -24,6 +24,7 @@ module Brig.Options where +import Amazonka (Region) import Amazonka.Types (S3AddressingStyle) import Brig.Queue.Types (QueueOpts (..)) import Control.Applicative @@ -55,6 +56,7 @@ import Wire.API.Allowlists (AllowlistEmailDomains (..)) import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.Version import Wire.API.Team.Feature +import Wire.API.Team.FeatureFlags import Wire.API.User import Wire.AuthenticationSubsystem.Config (ZAuthSettings) import Wire.AuthenticationSubsystem.Cookie.Limit @@ -235,16 +237,15 @@ instance FromJSON EmailOpts where EmailAWS <$> parseJSON o <|> EmailSMTP <$> parseJSON o -data EmailSMSOpts = EmailSMSOpts - { email :: !EmailOpts, - general :: !EmailSMSGeneralOpts, +data EmailSettings = EmailSettings + { general :: !EmailSMSGeneralOpts, user :: !EmailUserOpts, provider :: !ProviderOpts, team :: !TeamOpts } deriving (Show, Generic) -instance FromJSON EmailSMSOpts +instance FromJSON EmailSettings -- | Login retry limit. In contrast to 'setUserCookieThrottle', this is not about mitigating -- DOS attacks, but about preventing dictionary attacks. This introduces the orthogonal risk @@ -369,6 +370,7 @@ data InternalServices = InternalServices spar :: !Endpoint, gundeck :: !Endpoint, federatorInternal :: !(Maybe Endpoint), + backgroundWorker :: !Endpoint, wireServerEnterprise :: !(Maybe Endpoint) } @@ -394,7 +396,8 @@ data ExternalServices = ExternalServices userJournal :: !(Maybe SqsOpts), teamJournal :: !(Maybe SqsOpts), internalEvents :: !SqsOpts, - assets :: !AssetOpts + assets :: !AssetOpts, + pushNotifications :: !PushNotifiactionOpts } data RedisConnectionMode @@ -479,121 +482,42 @@ newtype OptS3AddressingStyle = OptS3AddressingStyle } data WireSettings = WireSettings - { turn :: !TurnOpts, - sft :: !(Maybe SFTOptions) - } - -data S3Compatibility - = -- | Scality RING, might also work for Zenko CloudServer - -- - S3CompatibilityScalityRing - --- | AWS CloudFront settings. -data CloudFrontOpts = CloudFrontOpts - { -- | Domain - domain :: CFDomain, - -- | Keypair ID - keyPairId :: CFKeyPairId, - -- | Path to private key - privateKey :: FilePath + { users :: UserSettings, + search :: SearchSettings, + teams :: TeamSettings, + conversations :: ConversationSettings, + auth :: AuthSettings, + calling :: CallingSettings, + notifications :: NotificationSettings, + federation :: FederationSettings, + email :: EmailSettings, + featureFlags :: FeatureFlags, + assets :: AssetSettings, + bots :: BotSettings, + postgresMigration :: PostgresMigrationOpts, + logSettings :: LogSettings + } + +data SearchSettings = SearchSettings + { emailVisibility :: !EmailVisibilityConfig, + -- | When true, search only + -- returns users from the same team + searchSameTeamOnly :: !(Maybe Bool) } - deriving (Show, Generic) - --- TODO: This is copied from cargohold, dedupe -newtype CFKeyPairId = CFKeyPairId Text - deriving (Eq, Show, Generic) - --- TODO: This is copied from cargohold, dedupe -newtype CFDomain = CFDomain Text - deriving (Eq, Show, Generic) - --- | Options that are consumed on startup -data Opts = Opts - -- services - { -- | Host and port to bind to - brig :: !Endpoint, - -- | Cargohold address - cargohold :: !Endpoint, - -- | Galley address - galley :: !Endpoint, - -- | Spar address - spar :: !Endpoint, - -- | Gundeck address - gundeck :: !Endpoint, - -- | Federator address - federatorInternal :: !(Maybe Endpoint), - -- | Wire Server Enterprise address - wireServerEnterprise :: !(Maybe Endpoint), - -- external - -- | Cassandra settings - cassandra :: !CassandraOpts, - -- | ElasticSearch settings - elasticsearch :: !ElasticSearchOpts, - -- | Postgresql settings, the key values must be in libpq format. - -- https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS - postgresql :: !(Map Text Text), - postgresqlPassword :: !(Maybe FilePathSecrets), - postgresqlPool :: !PoolConfig, - postgresMigration :: !PostgresMigrationOpts, - -- | SFT Federation - multiSFT :: !(Maybe Bool), - -- | RabbitMQ settings, required when federation is enabled. - rabbitmq :: !AmqpEndpoint, - -- | AWS settings - aws :: !AWSOpts, - -- | Enable Random Prekey Strategy - randomPrekeys :: !(Maybe Bool), - -- | STOMP broker settings - stompOptions :: !(Maybe StompOpts), - -- Email & SMS - - -- | Email and SMS settings - emailSMS :: !EmailSMSOpts, - -- ZAuth - - -- | ZAuth settings - zauth :: !ZAuthOpts, - -- Misc. - - -- | Disco URL, TODO: Remove. - discoUrl :: !(Maybe Text), - -- | Event queue for - -- Brig-generated events (e.g. - -- user deletion) - internalEvents :: !InternalEventsOpts, - -- Logging - - -- | Log level (Debug, Info, etc) - logLevel :: !Level, - -- | Use netstrings encoding (see - -- ). TODO: Remove. - logNetStrings :: !(Maybe (Last Bool)), - -- | Logformat to use - -- TURN - logFormat :: !(Maybe (Last LogFormat)), - -- | TURN server settings - turn :: !TurnOpts, - -- | SFT Settings - sft :: !(Maybe SFTOptions), - -- | Runtime settings - settings :: !Settings +data LogSettings = LogSettings + { logLevel :: !Level, + logFormat :: !(Maybe LogFormat) } - deriving (Show, Generic) --- | Options that persist as runtime settings. -data Settings = Settings - { -- | Activation timeout, in seconds +data UserSettings = UserSettings + { -- \| Activation timeout, in seconds activationTimeout :: !Timeout, -- | Default verification code timeout, in seconds -- use `verificationTimeout` as the getter function which always provides a default value verificationCodeTimeoutInternal :: !(Maybe Code.Timeout), - -- | Team invitation timeout, in seconds - teamInvitationTimeout :: !Timeout, -- | Check for expired users every so often, in seconds expiredUserCleanupTimeout :: !(Maybe Timeout), - -- | STOMP broker credentials. TODO: Remove. - stomp :: !(Maybe FilePathSecrets), -- | Whitelist of allowed emails/phones allowlistEmailDomains :: !(Maybe AllowlistEmailDomains), -- | Max. number of sent/accepted @@ -601,55 +525,68 @@ data Settings = Settings userMaxConnections :: !Int64, -- | Max. number of permanent clients per user userMaxPermClients :: !(Maybe Int), - -- | Whether to allow plain HTTP transmission - -- of cookies (for testing purposes only) - cookieInsecure :: !Bool, - -- | Minimum age of a user cookie before - -- it is renewed during token refresh - userCookieRenewAge :: !Integer, - -- | Max. # of cookies per user and cookie type - userCookieLimit :: !Int, - -- | Throttling tings (not to be confused - -- with 'LoginRetryOpts') - userCookieThrottle :: !CookieThrottle, - -- | Block user from logging in - -- for m minutes after n failed - -- logins - limitFailedLogins :: !(Maybe LimitFailedLogins), - -- | If last cookie renewal is too long ago, - -- suspend the user. suspendInactiveUsers :: !(Maybe SuspendInactiveUsers), -- | Max size of rich info (number of chars in -- field names and values), should be in sync -- with Spar richInfoLimit :: !Int, - -- | Default locale to use when selecting templates - -- use `defaultTemplateLocale` as the getter function which always provides a default value + -- | Default locale to use when selecting templates use + -- `defaultTemplateLocale` as the getter function which always provides a + -- default value. TODO: Merge this and next. defaultTemplateLocaleInternal :: !(Maybe Locale), -- | Default locale to use for users -- use `defaultUserLocale` as the getter function which always provides a default value defaultUserLocaleInternal :: !(Maybe Locale), - -- | Max. # of members in a team. - -- NOTE: This must be in sync with galley - maxTeamSize :: !Word32, - -- | Max. # of members in a conversation. - -- NOTE: This must be in sync with galley - maxConvSize :: !Word16, - -- | Filter ONLY services with - -- the given provider id - providerSearchFilter :: !(Maybe ProviderId), - -- | Whether to expose user emails and to whom - emailVisibility :: !EmailVisibilityConfig, propertyMaxKeyLen :: !(Maybe Int64), propertyMaxValueLen :: !(Maybe Int64), - -- | How long, in milliseconds, to wait - -- in between processing delete events + -- | How long, in milliseconds, to wait in between processing delete events -- from the internal delete queue deleteThrottleMillis :: !(Maybe Int), - -- | When true, search only - -- returns users from the same team - searchSameTeamOnly :: !(Maybe Bool), - -- | FederationDomain is required, even when not wanting to federate with other backends + -- | The amount of time in milliseconds to wait after reading from an SQS queue + -- returns no message, before asking for messages from SQS again. + -- defaults to 'defSqsThrottleMillis'. + -- When using real SQS from AWS, throttling isn't needed as much, since using + -- >>> SQS.rmWaitTimeSeconds (Just 20) in Brig.AWS.listen + -- ensures that there is only one request every 20 seconds. + -- However, that parameter is not honoured when using fake-sqs + -- (where throttling can thus make sense) + sqsThrottleMillis :: !(Maybe Int), + -- | Do not allow certain user creation flows. + -- docs/reference/user/registration.md {#RefRestrictRegistration}. + restrictUserCreation :: !(Maybe Bool) + } + +data TeamSettings = TeamSettings + { -- \| Team invitation timeout, in seconds + teamInvitationTimeout :: !Timeout, + -- | Max. # of members in a team. + maxTeamSize :: !Word32 + } + +data ConversationSettings = ConversationSettings + { -- | Max. # of members in a conversation. + maxConvSize :: !Word16 + } + +data AuthSettings = AuthSettings + { zauth :: !ZAuthOpts, + -- | Whether to allow plain HTTP transmission of cookies (for testing + -- purposes only) + cookieInsecure :: !Bool, + -- | Minimum age of a user cookie before it is renewed during token refresh + userCookieRenewAge :: !Integer, + -- | Max. # of cookies per user and cookie type + userCookieLimit :: !Int, + -- | Throttling tings (not to be confused with 'LoginRetryOpts') + userCookieThrottle :: !CookieThrottle, + -- | Block user from logging in for m minutes after n failed logins + limitFailedLogins :: !(Maybe LimitFailedLogins) + } + +data NotificationSettings = NotificationSettings {} + +data FederationSettings = FederationSettings + { -- \| FederationDomain is required, even when not wanting to federate with other backends -- (in that case the 'federationStrategy' can be set to `allowNone` below, or to -- `allowDynamic` while keeping the list of allowed domains empty, see -- https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections) @@ -672,20 +609,70 @@ data Settings = Settings federationDomainConfigs :: !(Maybe [ImplicitNoFederationRestriction]), -- | In seconds. Default: 10 seconds. Values <1 are silently replaced by 1. See -- https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections - federationDomainConfigsUpdateFreq :: !(Maybe Int), - -- | The amount of time in milliseconds to wait after reading from an SQS queue - -- returns no message, before asking for messages from SQS again. - -- defaults to 'defSqsThrottleMillis'. - -- When using real SQS from AWS, throttling isn't needed as much, since using - -- >>> SQS.rmWaitTimeSeconds (Just 20) in Brig.AWS.listen - -- ensures that there is only one request every 20 seconds. - -- However, that parameter is not honoured when using fake-sqs - -- (where throttling can thus make sense) - sqsThrottleMillis :: !(Maybe Int), - -- | Do not allow certain user creation flows. - -- docs/reference/user/registration.md {#RefRestrictRegistration}. - restrictUserCreation :: !(Maybe Bool), - -- | The analog to `Galley.Options.featureFlags`. See 'AccountFeatureConfigs'. + federationDomainConfigsUpdateFreq :: !(Maybe Int) + } + +data AssetSettings = AssetSettings {} + +data CallingSettings = CallingSettings + { turn :: !TurnOpts, + sft :: !(Maybe SFTOptions), + multiSFT :: !(Maybe Bool) + } + +data BotSettings = BotSettings + { -- \| Filter ONLY services with + -- the given provider id + providerSearchFilter :: !(Maybe ProviderId) + } + +data S3Compatibility + = -- | Scality RING, might also work for Zenko CloudServer + -- + S3CompatibilityScalityRing + +-- | AWS CloudFront settings. +data CloudFrontOpts = CloudFrontOpts + { -- | Domain + domain :: CFDomain, + -- | Keypair ID + keyPairId :: CFKeyPairId, + -- | Path to private key + privateKey :: FilePath + } + deriving (Show, Generic) + +-- TODO: This is copied from cargohold, dedupe +newtype CFKeyPairId = CFKeyPairId Text + deriving (Eq, Show, Generic) + +-- TODO: This is copied from cargohold, dedupe +newtype CFDomain = CFDomain Text + deriving (Eq, Show, Generic) + +data PushNotifiactionOpts = PushNotifiactionOpts + { -- \| AWS account + _account :: !Text, + -- | AWS region name + _region :: !Region, + -- | Environment name to scope ARNs to. TODO: Add explanation for on-prem operators. + _arnEnv :: !Text, + -- | SQS queue name + _queueName :: !Text, + _sqsEndpoint :: !AWSEndpoint, + _snsEndpoint :: !AWSEndpoint + } + +-- | Options that are consumed on startup +data Opts = Opts + -- services + { settings :: !Settings + } + deriving (Show, Generic) + +-- | Options that persist as runtime settings. +data Settings = Settings + { -- | The analog to `Galley.Options.featureFlags`. See 'AccountFeatureConfigs'. featureFlags :: !(Maybe UserFeatureFlags), -- | Customer extensions. Read 'CustomerExtensions' docs carefully! customerExtensions :: !(Maybe CustomerExtensions), @@ -769,17 +756,17 @@ instance FromJSON ImplicitNoFederationRestriction where defaultLocale :: Locale defaultLocale = Locale (Language EN) Nothing -defaultUserLocale :: Settings -> Locale -defaultUserLocale = fromMaybe defaultLocale . defaultUserLocaleInternal +-- defaultUserLocale :: Settings -> Locale +-- defaultUserLocale = fromMaybe defaultLocale . defaultUserLocaleInternal -defaultTemplateLocale :: Settings -> Locale -defaultTemplateLocale = fromMaybe defaultLocale . defaultTemplateLocaleInternal +-- defaultTemplateLocale :: Settings -> Locale +-- defaultTemplateLocale = fromMaybe defaultLocale . defaultTemplateLocaleInternal -verificationTimeout :: Settings -> Code.Timeout -verificationTimeout = fromMaybe defVerificationTimeout . verificationCodeTimeoutInternal - where - defVerificationTimeout :: Code.Timeout - defVerificationTimeout = Code.Timeout (60 * 10) -- 10 minutes +-- verificationTimeout :: Settings -> Code.Timeout +-- verificationTimeout = fromMaybe defVerificationTimeout . verificationCodeTimeoutInternal +-- where +-- defVerificationTimeout :: Code.Timeout +-- defVerificationTimeout = Code.Timeout (60 * 10) -- 10 minutes twoFACodeGenerationDelaySecs :: Settings -> Int twoFACodeGenerationDelaySecs = fromMaybe def2FACodeGenerationDelaySecs . twoFACodeGenerationDelaySecsInternal diff --git a/services/gundeck/src/Gundeck/Options.hs b/services/gundeck/src/Gundeck/Options.hs index d70bbc4f91d..8c9b86728e1 100644 --- a/services/gundeck/src/Gundeck/Options.hs +++ b/services/gundeck/src/Gundeck/Options.hs @@ -139,6 +139,7 @@ data Opts = Opts _redisAdditionalWrite :: !(Maybe RedisEndpoint), _aws :: !AWSOpts, _rabbitmq :: !AmqpEndpoint, + -- TODO: Delete. _discoUrl :: !(Maybe Text), _settings :: !Settings, -- Logging diff --git a/services/spar/src/Spar/Options.hs b/services/spar/src/Spar/Options.hs index ee54660c2c6..6bd5830805d 100644 --- a/services/spar/src/Spar/Options.hs +++ b/services/spar/src/Spar/Options.hs @@ -53,7 +53,7 @@ data Opts = Opts -- | The maximum size of rich info. richInfoLimit :: !Int, -- | Wire/AWS specific; optional; used to discover Cassandra instance - -- IPs using describe-instances. + -- IPs using describe-instances. TODO Delete. discoUrl :: !(Maybe Text), logNetStrings :: !(Maybe (Last Bool)), logFormat :: !(Maybe (Last LogFormat)), From 0d84c8ea567093565706fa56e2f3987c55e7a048 Mon Sep 17 00:00:00 2001 From: sghosh23 Date: Mon, 15 Jun 2026 14:03:44 +0200 Subject: [PATCH 3/3] common configs in the wire-subsystem --- .../wire-subsystems/src/Wire/ConfigOptions.hs | 598 ++++++++++++++++++ libs/wire-subsystems/wire-subsystems.cabal | 2 + 2 files changed, 600 insertions(+) create mode 100644 libs/wire-subsystems/src/Wire/ConfigOptions.hs diff --git a/libs/wire-subsystems/src/Wire/ConfigOptions.hs b/libs/wire-subsystems/src/Wire/ConfigOptions.hs new file mode 100644 index 00000000000..bd6c6165443 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/ConfigOptions.hs @@ -0,0 +1,598 @@ +{-# LANGUAGE TemplateHaskell #-} + + +module Wire.ConfigOptions where + +import Amazonka (Region) +import Amazonka.Types (S3AddressingStyle) +import Data.Aeson (FromJSON (..), Value (..), parseJSON, withObject, (.:), (.:?)) +import Data.Aeson.TH (Options (..), defaultOptions, deriveFromJSON, deriveJSON) +import Data.Aeson.Types qualified as A +import Data.Char qualified as Char +import Data.Code qualified as Code +import Data.Domain (Domain (..)) +import Data.Id (ProviderId) +import Data.LanguageCodes (ISO639_1 (EN)) +import Data.Range (Range) +import Data.Text qualified as Text +import Data.Text.Encoding qualified as Text +import Data.Time (DiffTime, secondsToDiffTime) +import Database.Bloodhound.Types qualified as ES +import Hasql.Pool.Extended (PoolConfig) +import Imports +import Network.AMQP.Extended (AmqpEndpoint) +import Network.DNS qualified as DNS +import System.Logger.Extended (Level, LogFormat) +import Util.Options +import Util.Options.Common (toOptionFieldName) +import Util.Timeout (Timeout) +import Wire.API.Allowlists (AllowlistEmailDomains (..)) +import Wire.API.Routes.FederationDomainConfig (FederationDomainConfig (..), FederationRestriction (..), FederationStrategy) +import Wire.API.Team.FeatureFlags (FeatureFlags) +import Wire.API.User +import Wire.AuthenticationSubsystem.Config (ZAuthSettings) +import Wire.AuthenticationSubsystem.Cookie.Limit (CookieThrottle) +import Wire.EmailSending.SMTP (SMTPConnType (..)) +import Wire.EmailSubsystem.Template (TeamOpts) +import Wire.PostgresMigrationOpts (PostgresMigrationOpts) + +asciiOnly :: Text -> A.Parser ByteString +asciiOnly t = + if Text.all Char.isAscii t + then pure $ Text.encodeUtf8 t + else fail $ "Expected ascii string only, found: " <> Text.unpack t + +defaultLocale :: Locale +defaultLocale = Locale (Language EN) Nothing + +data WireConfig = WireConfig + { internalServices :: InternalServices, + externalServices :: ExternalServices, + settings :: WireSettings + } + +data InternalServices = InternalServices + { brig :: !Endpoint, + cargohold :: !Endpoint, + galley :: !Endpoint, + spar :: !Endpoint, + gundeck :: !Endpoint, + federatorInternal :: !(Maybe Endpoint), + backgroundWorker :: !Endpoint, + wireServerEnterprise :: !(Maybe Endpoint) + } + +data ExternalServices = ExternalServices + { cassandraBrig :: !CassandraOpts, + cassandraGalley :: !CassandraOpts, + cassandraGundeck :: !CassandraOpts, + cassandraSpar :: !CassandraOpts, + elasticsearch :: !ElasticSearchOpts, + redis :: !RedisEndpoint, + redisAdditionalWrite :: !(Maybe RedisEndpoint), + -- | Postgresql settings, the key values must be in libpq format. + -- https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS + postgresql :: !(Map Text Text), + postgresqlPassword :: !(Maybe FilePathSecrets), + postgresqlPool :: !PoolConfig, + rabbitmq :: !AmqpEndpoint, + email :: !EmailOpts, + prekeySelection :: !PrekeySelectionOpts, + -- TODO: See if user and team journal can even be configured differently. If + -- everything is supposed to be used by ibis, we cannot actually configure + -- them seperately anyway. + userJournal :: !(Maybe SqsOpts), + teamJournal :: !(Maybe SqsOpts), + internalEvents :: !SqsOpts, + assets :: !AssetOpts, + pushNotifications :: !PushNotifiactionOpts + } + +data RedisConnectionMode + = Master + | Cluster + deriving (Show, Generic) + +data RedisEndpoint = RedisEndpoint + { _host :: !Text, + _port :: !Word16, + _connectionMode :: !RedisConnectionMode, + _enableTls :: !Bool, + -- | When not specified, use system CA bundle + _tlsCa :: !(Maybe FilePath), + -- | When 'True', uses TLS but does not verify hostname or CA or validity of + -- the cert. Not recommended to set to 'True'. + _insecureSkipVerifyTls :: !Bool + } + deriving (Show, Generic) + +data PrekeySelectionOpts + = RandomPrekeySelection + | DynamoDBPrekeySelection !DynamoDBPrekeySelectionOpts + +data DynamoDBPrekeySelectionOpts = DynamoDBPrekeySelectionOpts + { dynamoDBEndpoint :: !AWSEndpoint, + tableName :: !Text + } + +data SqsOpts = SqsOpts + { sqsEndpoint :: !AWSEndpoint, + queueName :: !Text + } + +data AssetOpts = AssetOpts + { s3Endpoint :: !AWSEndpoint, + -- | S3 can either by addressed in path style, i.e. + -- https:////, or vhost style, i.e. + -- https://./. AWS's S3 offering has + -- deprecated path style addressing for S3 and completely disabled it for + -- buckets created after 30 Sep 2020: + -- https://aws.amazon.com/blogs/aws/amazon-s3-path-deprecation-plan-the-rest-of-the-story/ + -- + -- However other object storage providers (specially self-deployed ones like + -- MinIO) may not support vhost style addressing yet (or ever?). Users of + -- such buckets should configure this option to "path". + -- + -- Installations using S3 service provided by AWS, should use "auto", this + -- option will ensure that vhost style is only used when it is possible to + -- construct a valid hostname from the bucket name and the bucket name + -- doesn't contain a '.'. Having a '.' in the bucket name causes TLS + -- validation to fail, hence it is not used by default. + -- + -- Using "virtual" as an option is only useful in situations where vhost + -- style addressing must be used even if it is not possible to construct a + -- valid hostname from the bucket name or the S3 service provider can ensure + -- correct certificate is issued for bucket which contain one or more '.'s + -- in the name. + -- + -- When this option is unspecified, we default to path style addressing to + -- ensure smooth transition for older deployments. + s3AddressingStyle :: !(Maybe OptS3AddressingStyle), + -- | S3 endpoint for generating download links. Useful if Cargohold is configured to use + -- an S3 replacement running inside the internal network (in which case internally we + -- would use one hostname for S3, and when generating an asset link for a client app, we + -- would use another hostname). + s3DownloadEndpoint :: !(Maybe AWSEndpoint), + s3Bucket :: !Text, + -- | Enable this option for compatibility with specific S3 backends. + s3Compatibility :: !(Maybe S3Compatibility), + cloudFront :: !(Maybe CloudFrontOpts), + -- | @Z-Host@ header to s3 download endpoint `Map` + -- + -- This logic is: If the @Z-Host@ header is provided and found in this map, + -- the map's values is taken as s3 download endpoint to redirect to; + -- otherwise a 404 is retuned. This option is only useful + -- in the context of multi-ingress setups where one backend / deployment is + -- reachable under several domains. + multiIngress :: !(Maybe (Map String AWSEndpoint)) + } + +newtype OptS3AddressingStyle = OptS3AddressingStyle + { unwrapS3AddressingStyle :: S3AddressingStyle + } + +data WireSettings = WireSettings + { users :: UserSettings, + search :: SearchSettings, + teams :: TeamSettings, + conversations :: ConversationSettings, + auth :: AuthSettings, + calling :: CallingSettings, + notifications :: NotificationSettings, + federation :: FederationSettings, + email :: EmailSettings, + featureFlags :: FeatureFlags, + assets :: AssetSettings, + bots :: BotSettings, + postgresMigration :: PostgresMigrationOpts, + logSettings :: LogSettings + } + +data SearchSettings = SearchSettings + { emailVisibility :: !EmailVisibilityConfig, + -- | When true, search only + -- returns users from the same team + searchSameTeamOnly :: !(Maybe Bool) + } + +data LogSettings = LogSettings + { logLevel :: !Level, + logFormat :: !(Maybe LogFormat) + } + +data UserSettings = UserSettings + { -- \| Activation timeout, in seconds + activationTimeout :: !Timeout, + -- | Default verification code timeout, in seconds + -- use `verificationTimeout` as the getter function which always provides a default value + verificationCodeTimeoutInternal :: !(Maybe Code.Timeout), + -- | Check for expired users every so often, in seconds + expiredUserCleanupTimeout :: !(Maybe Timeout), + -- | Whitelist of allowed emails/phones + allowlistEmailDomains :: !(Maybe AllowlistEmailDomains), + -- | Max. number of sent/accepted + -- connections per user + userMaxConnections :: !Int64, + -- | Max. number of permanent clients per user + userMaxPermClients :: !(Maybe Int), + suspendInactiveUsers :: !(Maybe SuspendInactiveUsers), + -- | Max size of rich info (number of chars in + -- field names and values), should be in sync + -- with Spar + richInfoLimit :: !Int, + -- | Default locale to use when selecting templates use + -- `defaultTemplateLocale` as the getter function which always provides a + -- default value. TODO: Merge this and next. + defaultTemplateLocaleInternal :: !(Maybe Locale), + -- | Default locale to use for users + -- use `defaultUserLocale` as the getter function which always provides a default value + defaultUserLocaleInternal :: !(Maybe Locale), + propertyMaxKeyLen :: !(Maybe Int64), + propertyMaxValueLen :: !(Maybe Int64), + -- | How long, in milliseconds, to wait in between processing delete events + -- from the internal delete queue + deleteThrottleMillis :: !(Maybe Int), + -- | The amount of time in milliseconds to wait after reading from an SQS queue + -- returns no message, before asking for messages from SQS again. + -- defaults to 'defSqsThrottleMillis'. + -- When using real SQS from AWS, throttling isn't needed as much, since using + -- >>> SQS.rmWaitTimeSeconds (Just 20) in Brig.AWS.listen + -- ensures that there is only one request every 20 seconds. + -- However, that parameter is not honoured when using fake-sqs + -- (where throttling can thus make sense) + sqsThrottleMillis :: !(Maybe Int), + -- | Do not allow certain user creation flows. + -- docs/reference/user/registration.md {#RefRestrictRegistration}. + restrictUserCreation :: !(Maybe Bool) + } + +data TeamSettings = TeamSettings + { -- \| Team invitation timeout, in seconds + teamInvitationTimeout :: !Timeout, + -- | Max. # of members in a team. + maxTeamSize :: !Word32 + } + +data ConversationSettings = ConversationSettings + { -- | Max. # of members in a conversation. + maxConvSize :: !Word16 + } + +data AuthSettings = AuthSettings + { zauth :: !ZAuthOpts, + -- | Whether to allow plain HTTP transmission of cookies (for testing + -- purposes only) + cookieInsecure :: !Bool, + -- | Minimum age of a user cookie before it is renewed during token refresh + userCookieRenewAge :: !Integer, + -- | Max. # of cookies per user and cookie type + userCookieLimit :: !Int, + -- | Throttling tings (not to be confused with 'LoginRetryOpts') + userCookieThrottle :: !CookieThrottle, + -- | Block user from logging in for m minutes after n failed logins + limitFailedLogins :: !(Maybe LimitFailedLogins) + } + +data NotificationSettings = NotificationSettings {} + +data FederationSettings = FederationSettings + { -- \| FederationDomain is required, even when not wanting to federate with other backends + -- (in that case the 'federationStrategy' can be set to `allowNone` below, or to + -- `allowDynamic` while keeping the list of allowed domains empty, see + -- https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections) + -- Federation domain is used to qualify local IDs and handles, + -- e.g. 0c4d8944-70fa-480e-a8b7-9d929862d18c@wire.com and somehandle@wire.com. + -- It should also match the SRV DNS records under which other wire-server installations can find this backend: + -- >>> _wire-server-federator._tcp. + -- Once set, DO NOT change it: if you do, existing users may have a broken experience and/or stop working. + -- Remember to keep it the same in all services. + federationDomain :: !Domain, + -- | See https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections + -- default: AllowNone + federationStrategy :: !(Maybe FederationStrategy), + -- | 'federationDomainConfigs' is introduced in + -- https://github.com/wireapp/wire-server/pull/3260 for the sole purpose of transitioning + -- to dynamic federation remote configuration. See + -- https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections + -- for details. + -- default: [] + federationDomainConfigs :: !(Maybe [ImplicitNoFederationRestriction]), + -- | In seconds. Default: 10 seconds. Values <1 are silently replaced by 1. See + -- https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections + federationDomainConfigsUpdateFreq :: !(Maybe Int) + } + +data AssetSettings = AssetSettings {} + +data CallingSettings = CallingSettings + { turn :: !TurnOpts, + sft :: !(Maybe SFTOptions), + multiSFT :: !(Maybe Bool) + } + +data BotSettings = BotSettings + { -- \| Filter ONLY services with + -- the given provider id + providerSearchFilter :: !(Maybe ProviderId) + } + +data S3Compatibility + = -- | Scality RING, might also work for Zenko CloudServer + -- + S3CompatibilityScalityRing + +-- | AWS CloudFront settings. +data CloudFrontOpts = CloudFrontOpts + { -- | Domain + domain :: CFDomain, + -- | Keypair ID + keyPairId :: CFKeyPairId, + -- | Path to private key + privateKey :: FilePath + } + deriving (Show, Generic) + +-- TODO: This is copied from cargohold, dedupe +newtype CFKeyPairId = CFKeyPairId Text + deriving (Eq, Show, Generic) + +-- TODO: This is copied from cargohold, dedupe +newtype CFDomain = CFDomain Text + deriving (Eq, Show, Generic) + +data PushNotifiactionOpts = PushNotifiactionOpts + { -- \| AWS account + _account :: !Text, + -- | AWS region name + _region :: !Region, + -- | Environment name to scope ARNs to. TODO: Add explanation for on-prem operators. + _arnEnv :: !Text, + -- | SQS queue name + _queueName :: !Text, + _sqsEndpoint :: !AWSEndpoint, + _snsEndpoint :: !AWSEndpoint + } + +-- | Wraps 'FederationDomainConfig' with a 'FromJSON' instance that defaults +-- 'FederationRestriction' to 'FederationRestrictionAllowAll' when absent. +newtype ImplicitNoFederationRestriction = ImplicitNoFederationRestriction + {federationDomainConfig :: FederationDomainConfig} + deriving (Show, Eq, Generic) + +instance FromJSON ImplicitNoFederationRestriction where + parseJSON = + withObject + "ImplicitNoFederationRestriction" + ( \obj -> do + domain <- obj .: "domain" + searchPolicy <- obj .: "search_policy" + pure . ImplicitNoFederationRestriction $ + FederationDomainConfig domain searchPolicy FederationRestrictionAllowAll + ) + +-- --------------------------------------------------------------------------- +-- Types moved from Brig.Options +-- --------------------------------------------------------------------------- + +data ElasticSearchOpts = ElasticSearchOpts + { url :: !ES.Server, + index :: !ES.IndexName, + additionalWriteIndex :: !(Maybe ES.IndexName), + additionalWriteIndexUrl :: !(Maybe ES.Server), + credentials :: !(Maybe FilePathSecrets), + additionalCredentials :: !(Maybe FilePathSecrets), + insecureSkipVerifyTls :: Bool, + caCert :: Maybe FilePath, + additionalInsecureSkipVerifyTls :: Bool, + additionalCaCert :: Maybe FilePath + } + deriving (Show, Generic) + +instance FromJSON ElasticSearchOpts + +data EmailAWSOpts = EmailAWSOpts + { -- | Event feedback queue for SES (e.g. for email bounces and complaints) + sesQueue :: !Text, + -- | AWS SES endpoint + sesEndpoint :: !AWSEndpoint + } + deriving (Show, Generic) + +instance FromJSON EmailAWSOpts + +data EmailSMTPCredentials = EmailSMTPCredentials + { smtpUsername :: !Text, + smtpPassword :: !FilePathSecrets + } + deriving (Show, Generic) + +instance FromJSON EmailSMTPCredentials + +data EmailSMTPOpts = EmailSMTPOpts + { smtpEndpoint :: !Endpoint, + smtpCredentials :: !(Maybe EmailSMTPCredentials), + smtpConnType :: !SMTPConnType + } + deriving (Show, Generic) + +instance FromJSON EmailSMTPOpts + +data EmailOpts + = EmailAWS EmailAWSOpts + | EmailSMTP EmailSMTPOpts + deriving (Show, Generic) + +instance FromJSON EmailOpts where + parseJSON o = + EmailAWS <$> parseJSON o + <|> EmailSMTP <$> parseJSON o + +data BrandingOpts = BrandingOpts + { brand :: !Text, + brandUrl :: !Text, + brandLabelUrl :: !Text, + brandLogoUrl :: !Text, + brandService :: !Text, + copyright :: !Text, + misuse :: !Text, + legal :: !Text, + forgot :: !Text, + support :: !Text + } + deriving (Show, Generic) + +instance FromJSON BrandingOpts + +data EmailSMSGeneralOpts = EmailSMSGeneralOpts + { templateDir :: !FilePath, + emailSender :: !EmailAddress, + smsSender :: !Text, + templateBranding :: !BrandingOpts + } + deriving (Show, Generic) + +instance FromJSON EmailSMSGeneralOpts + +data EmailUserOpts = EmailUserOpts + { activationUrl :: !Text, + smsActivationUrl :: !Text, + passwordResetUrl :: !Text, + deletionUrl :: !Text + } + deriving (Show, Generic) + +instance FromJSON EmailUserOpts + +data ProviderOpts = ProviderOpts + { homeUrl :: !Text, + providerActivationUrl :: !Text, + approvalUrl :: !Text, + approvalTo :: !EmailAddress, + providerPwResetUrl :: !Text + } + deriving (Show, Generic) + +instance FromJSON ProviderOpts + +data EmailSettings = EmailSettings + { general :: !EmailSMSGeneralOpts, + user :: !EmailUserOpts, + provider :: !ProviderOpts, + team :: !TeamOpts + } + deriving (Show, Generic) + +instance FromJSON EmailSettings + +data LimitFailedLogins = LimitFailedLogins + { timeout :: !Timeout, + retryLimit :: !Int + } + deriving (Eq, Show, Generic) + +instance FromJSON LimitFailedLogins + +data SuspendInactiveUsers = SuspendInactiveUsers + { suspendTimeout :: !Timeout + } + deriving (Eq, Show, Generic) + +instance FromJSON SuspendInactiveUsers + +data ZAuthOpts = ZAuthOpts + { privateKeys :: !FilePath, + publicKeys :: !FilePath, + authSettings :: !ZAuthSettings + } + deriving (Show, Generic) + +instance FromJSON ZAuthOpts + +data TurnServersFiles = TurnServersFiles + { tsfServers :: !FilePath, + tsfServersV2 :: !FilePath + } + deriving (Show) + +instance FromJSON TurnServersFiles where + parseJSON = withObject "TurnServersFiles" $ \o -> + TurnServersFiles + <$> o .: "servers" + <*> o .: "serversV2" + +data TurnDnsOpts = TurnDnsOpts + { tdoBaseDomain :: DNS.Domain, + tdoDiscoveryIntervalSeconds :: !(Maybe DiffTime) + } + deriving (Show) + +instance FromJSON TurnDnsOpts where + parseJSON = withObject "TurnDnsOpts" $ \o -> + TurnDnsOpts + <$> (asciiOnly =<< o .: "baseDomain") + <*> o .:? "discoveryIntervalSeconds" + +data TurnServersSource + = TurnSourceDNS TurnDnsOpts + | TurnSourceFiles TurnServersFiles + deriving (Show) + +data TurnOpts = TurnOpts + { serversSource :: !TurnServersSource, + secret :: !FilePath, + tokenTTL :: !Word32, + configTTL :: !Word32 + } + deriving (Show) + +instance FromJSON TurnOpts where + parseJSON = withObject "TurnOpts" $ \o -> do + sourceName <- o .: "serversSource" + source <- + case sourceName of + "files" -> TurnSourceFiles <$> parseJSON (Object o) + "dns" -> TurnSourceDNS <$> parseJSON (Object o) + _ -> fail $ "TurnOpts: Invalid sourceType, expected one of [files, dns] but got: " <> Text.unpack sourceName + TurnOpts source + <$> o .: "secret" + <*> o .: "tokenTTL" + <*> o .: "configTTL" + +data SFTTokenOptions = SFTTokenOptions + { sttTTL :: !Word32, + sttSecret :: !FilePath + } + deriving (Show, Generic) + +instance FromJSON SFTTokenOptions where + parseJSON = withObject "SFTTokenOptions" $ \o -> + SFTTokenOptions + <$> o .: "ttl" + <*> o .: "secret" + +data SFTOptions = SFTOptions + { sftBaseDomain :: !DNS.Domain, + sftSRVServiceName :: !(Maybe ByteString), + sftDiscoveryIntervalSeconds :: !(Maybe DiffTime), + sftListLength :: !(Maybe (Range 1 100 Int)), + sftTokenOptions :: !(Maybe SFTTokenOptions) + } + deriving (Show, Generic) + +instance FromJSON SFTOptions where + parseJSON = withObject "SFTOptions" $ \o -> + SFTOptions + <$> (asciiOnly =<< o .: "sftBaseDomain") + <*> (mapM asciiOnly =<< o .:? "sftSRVServiceName") + <*> (fmap . fmap) secondsToDiffTime (o .:? "sftDiscoveryIntervalSeconds") + <*> o .:? "sftListLength" + <*> o .:? "sftToken" + +-- --------------------------------------------------------------------------- +-- TH splices — must come after all data declarations due to stage restrictions +-- --------------------------------------------------------------------------- + +deriveJSON defaultOptions {constructorTagModifier = map toLower} ''RedisConnectionMode + +deriveFromJSON toOptionFieldName ''RedisEndpoint + diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 0f4c739c004..42e3b92342c 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -116,6 +116,7 @@ common common-all , currency-codes , data-default , data-timeout + , dns , email-validate , errors , exceptions @@ -234,6 +235,7 @@ library Wire.BlockListStore Wire.BlockListStore.Cassandra Wire.BrigAPIAccess + Wire.ConfigOptions Wire.BrigAPIAccess.Rpc Wire.ClientStore Wire.ClientStore.Cassandra