fix: honor v2 interaction event contract

This commit is contained in:
2026-05-16 04:32:58 +02:00
parent 301a7b96d0
commit 0a4646bf44
3 changed files with 83 additions and 7 deletions

View File

@@ -3,6 +3,11 @@ module Main where
import Test.Hspec
import IHP.Prelude
import qualified Test.Architecture.LayerBoundarySpec as LayerBoundary
import Data.Aeson (object, toJSON, (.=))
import Web.Controller.Api.V2.InteractionEvents
( declaredEventTypeNames, manifestAllowsEvent, metadataFromJsonBody
, metadataParamOrEmpty
)
main :: IO ()
main = hspec do
@@ -10,4 +15,33 @@ main = hspec do
it "should pass" do
1 + 1 `shouldBe` (2 :: Int)
describe "API v2 interaction-event manifest validation" do
let opsEventTypes = toJSON
( [ "ops-endpoint-verified"
, "ops-workflow-started"
] :: [Text]
)
it "decodes manifest-declared event types from JSON arrays" do
declaredEventTypeNames opsEventTypes
`shouldBe` ["ops-endpoint-verified", "ops-workflow-started"]
it "allows manifest-declared ops-owned domain events" do
manifestAllowsEvent "ops-endpoint-verified" opsEventTypes
`shouldBe` True
it "rejects events absent from an active manifest declaration" do
manifestAllowsEvent "clicked" opsEventTypes
`shouldBe` False
it "keeps empty declarations unrestricted for legacy manifests" do
manifestAllowsEvent "clicked" (toJSON ([] :: [Text]))
`shouldBe` True
it "preserves submitted metadata values and defaults missing metadata" do
let metadata = object ["source" .= ("ops-hub" :: Text)]
metadataFromJsonBody (object ["metadata" .= metadata]) `shouldBe` Just metadata
metadataParamOrEmpty (Just metadata) `shouldBe` metadata
metadataParamOrEmpty Nothing `shouldBe` object []
LayerBoundary.spec

View File

@@ -4,8 +4,8 @@ import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ControllerPrelude
import Data.Aeson (object, (.=))
import qualified Data.Text as T
import Data.Aeson (Value(..), object, (.=))
import IHP.ControllerSupport (ControllerContext, getHeader, requestBodyJSON)
import Web.Controller.Api.V2.Auth
( requireApiConsumer, paginatedResponse, getPageParams
, respondWithStatus )
@@ -13,8 +13,13 @@ import Application.Helper.TypeRegistry (validateEventType)
import Web.Job.WebhookDeliveryJob (dispatchWebhooks)
import Control.Concurrent (forkIO)
import Control.Monad (void)
import Data.Maybe (fromMaybe, mapMaybe)
import qualified Data.Aeson.KeyMap as KM
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy.Char8 as LBSC
import qualified Data.UUID as UUID
import qualified Data.Aeson as A
import qualified Data.Vector as V
instance Controller ApiV2InteractionEventsController where
@@ -47,6 +52,7 @@ instance Controller ApiV2InteractionEventsController where
let widgetIdText = paramOrNothing @Text "widgetId"
eventType = paramOrNothing @Text "eventType"
viewContext = paramOrNothing @Text "viewContext"
metadata = metadataFromRequest
let missing = catMaybes
[ if isNothing widgetIdText then Just ("widgetId" :: Text) else Nothing
@@ -76,9 +82,7 @@ instance Controller ApiV2InteractionEventsController where
forM_ consumer.hubCapabilityManifestId $ \manifestId -> do
manifest <- fetch manifestId
when (manifest.status == "active") do
let declared = case manifest.declaredEventTypes of
_ -> [] :: [Text] -- JSONB array decoded via aeson
unless (null declared || evType `elem` declared) do
unless (manifestAllowsEvent evType manifest.declaredEventTypes) do
respondWithStatus 422 $ object
[ "error" .= ("Event type not declared in hub manifest" :: Text)
, "code" .= ("event_type_not_in_manifest" :: Text)
@@ -100,6 +104,7 @@ instance Controller ApiV2InteractionEventsController where
|> set #eventType evType
|> set #actorType "api"
|> set #viewContextRef viewContext
|> set #metadata metadata
|> createRecord
-- Dispatch webhooks fire-and-forget
let webhookPayload = object
@@ -109,7 +114,7 @@ instance Controller ApiV2InteractionEventsController where
, "eventType" .= event.eventType
, "occurredAt" .= event.occurredAt
]
liftIO $ void $ forkIO $ dispatchWebhooks "clicked" webhookPayload
liftIO $ void $ forkIO $ dispatchWebhooks evType webhookPayload
renderJson (eventToJson event)
eventToJson :: InteractionEvent -> Value
@@ -123,3 +128,33 @@ eventToJson e = object
, "metadata" .= e.metadata
, "occurredAt" .= e.occurredAt
]
declaredEventTypeNames :: A.Value -> [Text]
declaredEventTypeNames (Array values) = mapMaybe extractText (V.toList values)
where
extractText (String value) = Just value
extractText _ = Nothing
declaredEventTypeNames _ = []
manifestAllowsEvent :: Text -> A.Value -> Bool
manifestAllowsEvent eventType declaredEventTypes =
let declared = declaredEventTypeNames declaredEventTypes
in null declared || eventType `elem` declared
metadataParamOrEmpty :: Maybe A.Value -> A.Value
metadataParamOrEmpty = fromMaybe (object [])
metadataFromRequest :: (?context :: ControllerContext) => A.Value
metadataFromRequest =
case getHeader "Content-Type" of
Just contentType | "application/json" `BS.isPrefixOf` contentType ->
metadataParamOrEmpty (metadataFromJsonBody requestBodyJSON)
_ ->
metadataParamOrEmpty (metadataFromText =<< paramOrNothing @Text "metadata")
metadataFromJsonBody :: A.Value -> Maybe A.Value
metadataFromJsonBody (Object body) = KM.lookup "metadata" body
metadataFromJsonBody _ = Nothing
metadataFromText :: Text -> Maybe A.Value
metadataFromText raw = A.decode (LBSC.pack (cs raw))

View File

@@ -177,7 +177,7 @@ command while preserving the one-time secret display invariant.
```task
id: IHUB-WP-0019-T05
status: todo
status: done
priority: high
state_hub_task_id: "1febfdb6-757b-420a-b4bd-709ce3cd1252"
```
@@ -193,6 +193,13 @@ Fix the current v2 interaction event create behavior:
Done when: `ops-endpoint-verified` can be submitted with metadata and routed
as an ops-owned event.
Implementation note (2026-05-16): v2 interaction-event creation now validates
against active manifest-declared event types, persists submitted metadata from
JSON request bodies, dispatches webhooks with the submitted event type, and has
focused Hspec coverage for manifest-declared ops domain events. Local
`git diff --check` passed; `scripts/compile-check` could not run because this
shell does not have `IHP_LIB`/the IHP dev environment loaded.
---
### T06 — Update OpenAPI request schemas and hub quickstart docs