packages feed

shomei-core-0.2.0.0: src/Shomei/Passkey/Store.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeFamilies #-}

-- | Store effect for registered passkeys (WebAuthn public-key credentials).
--
-- A passkey is the public half of a WebAuthn credential that an authenticator
-- created during a registration ceremony. This port persists those credentials and
-- offers the three lookups the workflows need: by owning user (enrollment listing),
-- by the authenticator-assigned credential id (the assertion key the browser returns),
-- and by the per-user 'UserHandle' (passwordless discovery in EP-4). It also bumps the
-- clone-detection signature counter, counts a user's passkeys, and deletes one.
--
-- The credential domain types are owned by EP-1 ('Shomei.Passkey.Domain'); this module
-- only references them. EP-2 supplies the in-memory and PostgreSQL interpreters.
module Shomei.Passkey.Store
  ( PasskeyStore (..),
    createPasskey,
    findPasskeysByUser,
    findPasskeyByCredentialId,
    findPasskeysByUserHandle,
    updatePasskeySignCounter,
    deletePasskey,
    countPasskeysByUser,
  )
where

import Effectful (Dispatch (Dynamic), DispatchOf, Eff, Effect, (:>))
import Effectful.Dispatch.Dynamic (send)
import Shomei.Id (PasskeyId, UserId)
import Shomei.Passkey.Domain
  ( NewPasskeyCredential,
    PasskeyCredential,
    SignatureCounter,
    UserHandle,
    WebAuthnCredentialId,
  )
import Shomei.Prelude

data PasskeyStore :: Effect where
  CreatePasskey :: NewPasskeyCredential -> PasskeyStore m PasskeyCredential
  FindPasskeysByUser :: UserId -> PasskeyStore m [PasskeyCredential]
  FindPasskeyByCredentialId :: WebAuthnCredentialId -> PasskeyStore m (Maybe PasskeyCredential)
  FindPasskeysByUserHandle :: UserHandle -> PasskeyStore m [PasskeyCredential]
  -- | Compare-and-swap the signature counter and @last_used_at@. 'False' means replay. A
  -- counterless authenticator's zero-to-zero update is accepted, matching WebAuthn clone checks.
  UpdatePasskeySignCounter :: PasskeyId -> SignatureCounter -> UTCTime -> PasskeyStore m Bool
  -- | Delete only when both the owning user and the passkey id match (a user action).
  DeletePasskey :: UserId -> PasskeyId -> PasskeyStore m ()
  CountPasskeysByUser :: UserId -> PasskeyStore m Int

type instance DispatchOf PasskeyStore = Dynamic

createPasskey :: (PasskeyStore :> es) => NewPasskeyCredential -> Eff es PasskeyCredential
createPasskey = send . CreatePasskey

findPasskeysByUser :: (PasskeyStore :> es) => UserId -> Eff es [PasskeyCredential]
findPasskeysByUser = send . FindPasskeysByUser

findPasskeyByCredentialId :: (PasskeyStore :> es) => WebAuthnCredentialId -> Eff es (Maybe PasskeyCredential)
findPasskeyByCredentialId = send . FindPasskeyByCredentialId

findPasskeysByUserHandle :: (PasskeyStore :> es) => UserHandle -> Eff es [PasskeyCredential]
findPasskeysByUserHandle = send . FindPasskeysByUserHandle

updatePasskeySignCounter :: (PasskeyStore :> es) => PasskeyId -> SignatureCounter -> UTCTime -> Eff es Bool
updatePasskeySignCounter i c t = send (UpdatePasskeySignCounter i c t)

deletePasskey :: (PasskeyStore :> es) => UserId -> PasskeyId -> Eff es ()
deletePasskey u p = send (DeletePasskey u p)

countPasskeysByUser :: (PasskeyStore :> es) => UserId -> Eff es Int
countPasskeysByUser = send . CountPasskeysByUser