Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/mobility-core/mobility-core.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,12 @@ library
Kernel.External.Verification.Tten.Flow
Kernel.External.Verification.Tten.Types
Kernel.External.Verification.Types
Kernel.External.VoiceSdk.BreezeBuddy.Flow
Kernel.External.VoiceSdk.BreezeBuddy.Types
Kernel.External.VoiceSdk.Interface
Kernel.External.VoiceSdk.Interface.BreezeBuddy
Kernel.External.VoiceSdk.Interface.Types
Kernel.External.VoiceSdk.Types
Kernel.External.Wallet
Kernel.External.Wallet.Interface
Kernel.External.Wallet.Interface.Juspay
Expand Down
54 changes: 54 additions & 0 deletions lib/mobility-core/src/Kernel/External/VoiceSdk/BreezeBuddy/Flow.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module Kernel.External.VoiceSdk.BreezeBuddy.Flow where

import EulerHS.Types as Euler
import Kernel.External.VoiceSdk.BreezeBuddy.Types
import Kernel.Prelude
import qualified Kernel.Tools.Metrics.CoreMetrics as Metrics
import Kernel.Types.Error
import Kernel.Utils.Common
import Servant hiding (throwError)

type CreateLeadAPI =
"agent"
:> "voice"
:> "breeze-buddy"
:> "leads"
:> Header "Authorization" Text
:> ReqBody '[JSON] BreezeBuddyLeadRequest
:> Post '[JSON] BreezeBuddyLeadResponse

type ConnectAPI =
"agent"
:> "voice"
:> "breeze-buddy"
:> "connect"
:> Header "Authorization" Text
:> ReqBody '[JSON] BreezeBuddyConnectRequest
:> Post '[JSON] BreezeBuddyConnectResponse

createLead ::
(Metrics.CoreMetrics m, MonadFlow m, HasRequestId r, MonadReader r m) =>
BaseUrl ->
Text ->
BreezeBuddyLeadRequest ->
m BreezeBuddyLeadResponse
createLead url apiKey request = do
let proxy = Proxy @CreateLeadAPI
eulerClient = Euler.client proxy (Just ("Bearer " <> apiKey)) request
callBreezeBuddyAPI url eulerClient "breeze-buddy-create-lead" proxy

connect ::
(Metrics.CoreMetrics m, MonadFlow m, HasRequestId r, MonadReader r m) =>
BaseUrl ->
Text ->
BreezeBuddyConnectRequest ->
m BreezeBuddyConnectResponse
connect url apiKey request = do
let proxy = Proxy @ConnectAPI
eulerClient = Euler.client proxy (Just ("Bearer " <> apiKey)) request
callBreezeBuddyAPI url eulerClient "breeze-buddy-connect" proxy

callBreezeBuddyAPI :: (MonadFlow m, HasRequestId r, MonadReader r m) => CallAPI' m r api res res
callBreezeBuddyAPI url eulerClient description proxy = do
callAPI url eulerClient description proxy
>>= fromEitherM (\err -> InternalError $ "Failed to call " <> description <> " API: " <> show err)
Comment on lines +51 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Don’t surface raw ClientError details via InternalError.

callAPI already redacts the underlying show err before logging, but Line 54 puts the raw error text back into InternalError. Since InternalError is exposed as the API error message, this can leak third-party response bodies or request details to callers.

Proposed fix
 callBreezeBuddyAPI url eulerClient description proxy = do
   callAPI url eulerClient description proxy
-    >>= fromEitherM (\err -> InternalError $ "Failed to call " <> description <> " API: " <> show err)
+    >>= fromEitherM (\_ -> InternalError $ "Failed to call " <> description <> " API")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
callBreezeBuddyAPI :: (MonadFlow m, HasRequestId r, MonadReader r m) => CallAPI' m r api res res
callBreezeBuddyAPI url eulerClient description proxy = do
callAPI url eulerClient description proxy
>>= fromEitherM (\err -> InternalError $ "Failed to call " <> description <> " API: " <> show err)
callBreezeBuddyAPI :: (MonadFlow m, HasRequestId r, MonadReader r m) => CallAPI' m r api res res
callBreezeBuddyAPI url eulerClient description proxy = do
callAPI url eulerClient description proxy
>>= fromEitherM (\_ -> InternalError $ "Failed to call " <> description <> " API")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/mobility-core/src/Kernel/External/VoiceSdk/BreezeBuddy/Flow.hs` around
lines 51 - 54, The issue is that callBreezeBuddyAPI re-exposes raw ClientError
details through InternalError, which can leak sensitive upstream
response/request data to callers. Update the error handling in
callBreezeBuddyAPI so it preserves the existing callAPI redaction behavior and
does not concatenate show err into the InternalError message; keep the returned
API error generic while still allowing internal logging to retain the detailed
error elsewhere.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
module Kernel.External.VoiceSdk.BreezeBuddy.Types where

import qualified Data.Aeson as A
import Kernel.External.Encryption
import Kernel.Prelude

data BreezeBuddySdkConfig = BreezeBuddySdkConfig
{ url :: BaseUrl,
-- | Bearer token used to authenticate against the BreezeBuddy voice agent APIs.
apiKey :: EncryptedField 'AsEncrypted Text
}
deriving (Show, Eq, Generic, ToJSON, FromJSON)

-- | Request body for @POST /agent/voice/breeze-buddy/leads@.
-- Optional fields are omitted from the JSON when 'Nothing'.
data BreezeBuddyLeadRequest = BreezeBuddyLeadRequest
{ request_id :: Text,
template :: Text,
-- | Template specific payload. @customer_mobile_number@ is mandatory and must be E.164 (e.g. "+14155551234").
payload :: A.Value,
reseller_id :: Text,
merchant_id :: Maybe Text,
reporting_webhook_url :: Maybe Text,
execution_mode :: Maybe Text,
is_playground :: Maybe Bool,
configurations_override :: Maybe A.Value
}
deriving (Show, Eq, Generic)

instance FromJSON BreezeBuddyLeadRequest where
parseJSON = A.genericParseJSON A.defaultOptions {A.omitNothingFields = True}

instance ToJSON BreezeBuddyLeadRequest where
toJSON = A.genericToJSON A.defaultOptions {A.omitNothingFields = True}

-- | Success (201) response for @POST .../leads@.
data BreezeBuddyLeadResponse = BreezeBuddyLeadResponse
{ -- | Always "queued" on success.
status :: Text,
-- | Canonical lead identifier; pass this as @lead_id@ to the connect endpoint.
lead_call_tracker_id :: Text,
-- | Echoes the submitted @request_id@.
order_id :: Text,
message :: Text
}
deriving (Show, Eq, Generic, ToJSON, FromJSON)

newtype BreezeBuddyConnectRequest = BreezeBuddyConnectRequest
{ lead_id :: Text
}
deriving (Show, Eq, Generic, ToJSON, FromJSON)

-- | Success response for @POST .../connect@.
data BreezeBuddyConnectResponse = BreezeBuddyConnectResponse
{ room_url :: Text,
token :: Text,
session_id :: Text,
lead_id :: Text
}
deriving (Show, Eq, Generic, ToJSON, FromJSON)
33 changes: 33 additions & 0 deletions lib/mobility-core/src/Kernel/External/VoiceSdk/Interface.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module Kernel.External.VoiceSdk.Interface
( module Reexport,
module Kernel.External.VoiceSdk.Interface,
)
where

import qualified Kernel.External.VoiceSdk.Interface.BreezeBuddy as BreezeBuddy
import Kernel.External.VoiceSdk.Interface.Types
import Kernel.External.VoiceSdk.Types as Reexport
import Kernel.Tools.Metrics.CoreMetrics (CoreMetrics)
import Kernel.Utils.Common
Comment on lines +7 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Import Kernel.Prelude in this module.

This is the only new Haskell module here that skips the required Prelude import. Please add Kernel.Prelude (or EulerHS.Prelude) to stay aligned with the repo’s NoImplicitPrelude convention.

Proposed fix
 import qualified Kernel.External.VoiceSdk.Interface.BreezeBuddy as BreezeBuddy
 import Kernel.External.VoiceSdk.Interface.Types
 import Kernel.External.VoiceSdk.Types as Reexport
+import Kernel.Prelude
 import Kernel.Tools.Metrics.CoreMetrics (CoreMetrics)
 import Kernel.Utils.Common

As per coding guidelines, "**/*.hs: All modules must import Kernel.Prelude or EulerHS.Prelude instead of the standard Prelude due to NoImplicitPrelude language pragma`."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import qualified Kernel.External.VoiceSdk.Interface.BreezeBuddy as BreezeBuddy
import Kernel.External.VoiceSdk.Interface.Types
import Kernel.External.VoiceSdk.Types as Reexport
import Kernel.Tools.Metrics.CoreMetrics (CoreMetrics)
import Kernel.Utils.Common
import qualified Kernel.External.VoiceSdk.Interface.BreezeBuddy as BreezeBuddy
import Kernel.External.VoiceSdk.Interface.Types
import Kernel.External.VoiceSdk.Types as Reexport
import Kernel.Prelude
import Kernel.Tools.Metrics.CoreMetrics (CoreMetrics)
import Kernel.Utils.Common
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/mobility-core/src/Kernel/External/VoiceSdk/Interface.hs` around lines 7 -
11, This Haskell module is missing the required Prelude import under the repo’s
NoImplicitPrelude convention. Add an explicit import of Kernel.Prelude (or
EulerHS.Prelude) to Kernel.External.VoiceSdk.Interface alongside the existing
imports, keeping the module consistent with the rest of the codebase and
ensuring standard Prelude symbols are available through the project’s preferred
prelude.

Source: Coding guidelines


createLead ::
( EncFlow m r,
CoreMetrics m,
HasRequestId r
) =>
VoiceSdkConfig ->
CreateLeadReq ->
m CreateLeadResp
createLead serviceConfig req = case serviceConfig of
BreezeBuddyVoiceSdkConfig cfg -> BreezeBuddy.createLead cfg req

connect ::
( EncFlow m r,
CoreMetrics m,
HasRequestId r
) =>
VoiceSdkConfig ->
ConnectReq ->
m ConnectResp
connect serviceConfig req = case serviceConfig of
BreezeBuddyVoiceSdkConfig cfg -> BreezeBuddy.connect cfg req
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
module Kernel.External.VoiceSdk.Interface.BreezeBuddy where

import Kernel.External.Encryption
import qualified Kernel.External.VoiceSdk.BreezeBuddy.Flow as BreezeBuddy
import qualified Kernel.External.VoiceSdk.BreezeBuddy.Types as BreezeBuddyTypes
import Kernel.External.VoiceSdk.Interface.Types
import Kernel.Prelude
import qualified Kernel.Tools.Metrics.CoreMetrics as Metrics
import Kernel.Utils.Common

createLead ::
( Metrics.CoreMetrics m,
EncFlow m r,
HasRequestId r,
MonadReader r m
) =>
BreezeBuddyTypes.BreezeBuddySdkConfig ->
CreateLeadReq ->
m CreateLeadResp
createLead config req = do
apiKey <- decrypt config.apiKey
resp <- BreezeBuddy.createLead config.url apiKey (toBreezeBuddyLeadRequest req)
pure $ fromBreezeBuddyLeadResponse resp

connect ::
( Metrics.CoreMetrics m,
EncFlow m r,
HasRequestId r,
MonadReader r m
) =>
BreezeBuddyTypes.BreezeBuddySdkConfig ->
ConnectReq ->
m ConnectResp
connect config req = do
apiKey <- decrypt config.apiKey
resp <- BreezeBuddy.connect config.url apiKey (toBreezeBuddyConnectRequest req)
pure $ fromBreezeBuddyConnectResponse resp

toBreezeBuddyLeadRequest :: CreateLeadReq -> BreezeBuddyTypes.BreezeBuddyLeadRequest
toBreezeBuddyLeadRequest req =
BreezeBuddyTypes.BreezeBuddyLeadRequest
{ request_id = req.requestId,
template = req.template,
payload = req.payload,
reseller_id = req.resellerId,
merchant_id = req.merchantId,
reporting_webhook_url = req.reportingWebhookUrl,
execution_mode = req.executionMode,
is_playground = req.isPlayground,
configurations_override = req.configurationsOverride
}

fromBreezeBuddyLeadResponse :: BreezeBuddyTypes.BreezeBuddyLeadResponse -> CreateLeadResp
fromBreezeBuddyLeadResponse resp =
CreateLeadResp
{ status = resp.status,
leadCallTrackerId = resp.lead_call_tracker_id,
orderId = resp.order_id,
message = resp.message
}

toBreezeBuddyConnectRequest :: ConnectReq -> BreezeBuddyTypes.BreezeBuddyConnectRequest
toBreezeBuddyConnectRequest req =
BreezeBuddyTypes.BreezeBuddyConnectRequest
{ lead_id = req.leadId
}

fromBreezeBuddyConnectResponse :: BreezeBuddyTypes.BreezeBuddyConnectResponse -> ConnectResp
fromBreezeBuddyConnectResponse resp =
ConnectResp
{ roomUrl = resp.room_url,
token = resp.token,
sessionId = resp.session_id,
leadId = resp.lead_id
}
44 changes: 44 additions & 0 deletions lib/mobility-core/src/Kernel/External/VoiceSdk/Interface/Types.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module Kernel.External.VoiceSdk.Interface.Types where

import qualified Data.Aeson as A
import qualified Kernel.External.VoiceSdk.BreezeBuddy.Types as BreezeBuddy
import Kernel.Prelude

data CreateLeadReq = CreateLeadReq
{ requestId :: Text,
template :: Text,
payload :: A.Value,
resellerId :: Text,
merchantId :: Maybe Text,
reportingWebhookUrl :: Maybe Text,
executionMode :: Maybe Text,
isPlayground :: Maybe Bool,
configurationsOverride :: Maybe A.Value
}
deriving (Show, Eq, Generic, ToJSON, FromJSON)

data CreateLeadResp = CreateLeadResp
{ status :: Text,
-- | Canonical lead identifier; feed this into 'ConnectReq.leadId'.
leadCallTrackerId :: Text,
orderId :: Text,
message :: Text
}
deriving (Show, Eq, Generic, ToJSON, FromJSON)

newtype ConnectReq = ConnectReq
{ leadId :: Text
}
deriving (Show, Eq, Generic, ToJSON, FromJSON)

data ConnectResp = ConnectResp
{ roomUrl :: Text,
token :: Text,
sessionId :: Text,
leadId :: Text
}
deriving (Show, Eq, Generic, ToJSON, FromJSON)

data VoiceSdkConfig
= BreezeBuddyVoiceSdkConfig BreezeBuddy.BreezeBuddySdkConfig
deriving (Show, Eq, Generic, ToJSON, FromJSON)
22 changes: 22 additions & 0 deletions lib/mobility-core/src/Kernel/External/VoiceSdk/Types.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{-# LANGUAGE TemplateHaskell #-}

module Kernel.External.VoiceSdk.Types where

import Data.Aeson.Types
import EulerHS.Prelude
import Kernel.Beam.Lib.UtilsTH (mkBeamInstancesForEnumAndList)
import Kernel.Storage.Esqueleto (derivePersistField)

data VoiceSdkService = BreezeBuddy
deriving (Show, Read, Eq, Ord, Generic)

$(mkBeamInstancesForEnumAndList ''VoiceSdkService)
derivePersistField "VoiceSdkService"

instance FromJSON VoiceSdkService where
parseJSON (String "BreezeBuddy") = pure BreezeBuddy
parseJSON (String _) = parseFail "Expected \"BreezeBuddy\""
parseJSON e = typeMismatch "String" e

instance ToJSON VoiceSdkService where
toJSON = String . show
Loading