From 01d80e8d168d4d0da7a6d3006d7068b36c33b18d Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Sat, 29 Aug 2026 09:50:41 +0200 Subject: [PATCH 01/29] [WPB-28089] Treat team collaborators like team members in contact search (second attempt). (#5488) * Include team collaborators in contact search. * Implement TeamCollaboratorsSubsystem interpreter with BrigAPIAccess. Was previously UserSubsystem, but since it TeamCollaboratorsSubsystem is also used outside of Brig, that is not always available. Further changes: - Support BrigAPIAccess locally in Brig. - Change collaborator field type in UserDoc to collapse `Nothing` and `Just []` (remove the Maybe). * Fix brig-index: do not bump index version in failure case. * Add release notes on required postgres setup steps. --------- Co-authored-by: Gautier DI FOLCO --- ...rators-like-team-members-in-contact-search | 29 +++ ...rators-like-team-members-in-contact-search | 1 + integration/test/Test/TeamCollaborators.hs | 106 ++++++++ .../src/Wire/BrigAPIAccess/Local.hs | 65 +++++ .../src/Wire/BrigAPIAccess/Rpc.hs | 234 ++++++++++-------- .../IndexedUserStore/Bulk/ElasticSearch.hs | 61 +++-- .../Wire/IndexedUserStore/ElasticSearch.hs | 19 +- .../src/Wire/TeamCollaboratorsStore.hs | 2 + .../Wire/TeamCollaboratorsStore/Postgres.hs | 17 ++ .../TeamCollaboratorsSubsystem/Interpreter.hs | 36 ++- .../src/Wire/UserSearch/Migration.hs | 1 + .../src/Wire/UserSearch/Types.hs | 9 +- .../src/Wire/UserStore/IndexUser.hs | 8 +- .../src/Wire/UserSubsystem/Interpreter.hs | 20 +- .../test/unit/Wire/MiniBackend.hs | 9 +- .../test/unit/Wire/MockInterpreters.hs | 1 + .../Wire/MockInterpreters/BrigAPIAccess.hs | 81 ++++++ .../TeamCollaboratorsStore.hs | 2 + .../Wire/MockInterpreters/UserSubsystem.hs | 2 +- .../Wire/ScimSubsystem/InterpreterSpec.hs | 2 +- .../test/unit/Wire/UserSearch/TypesSpec.hs | 4 +- .../Wire/UserSubsystem/InterpreterSpec.hs | 2 +- libs/wire-subsystems/wire-subsystems.cabal | 2 + .../background-worker/src/Wire/Effects.hs | 2 +- services/brig/src/Brig/App.hs | 6 + .../brig/src/Brig/CanonicalInterpreter.hs | 28 ++- services/brig/src/Brig/Index/Eval.hs | 20 +- services/brig/src/Brig/User/Search/Index.hs | 9 + services/galley/src/Galley/App.hs | 2 +- 29 files changed, 610 insertions(+), 170 deletions(-) create mode 100644 changelog.d/0-release-notes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search create mode 100644 changelog.d/1-api-changes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search create mode 100644 libs/wire-subsystems/src/Wire/BrigAPIAccess/Local.hs create mode 100644 libs/wire-subsystems/test/unit/Wire/MockInterpreters/BrigAPIAccess.hs diff --git a/changelog.d/0-release-notes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search b/changelog.d/0-release-notes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search new file mode 100644 index 00000000000..3caefeb19af --- /dev/null +++ b/changelog.d/0-release-notes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search @@ -0,0 +1,29 @@ +`GET /contacts/search` returns apps (and regular users) that collaborate with the searcher's team. + +This means that `brig-index-migrate-data` now requires you to +configure the `elasticsearch-index` chart's postgres setup, and have a +postgres instance reachable with that setup, eg., like this: + +``` +# in (charts/elasticsearch-index/)values.yaml +postgresql: + host: postgresql # DNS name without protocol + port: "5432" + user: wire-server + dbname: wire-server +postgresqlPool: + size: 100 + acquisitionTimeout: 10s + idlenessTimeout: 10m + +postgresMigration: + user: cassandra # (or postgresql, migration-to-postgresql, ...) +``` + +Notes: +- If you have experienced any elasticsearch index update issues since 2026-03-24 (Chart Release 5.29.0), this might be related. If you have not resolved them, consider updating your `values.yaml` now and running a full re-index. +- Note: `brig-index` understands `--user-storage-location`, but that is not relevant here, because collaborators are not technically user accounts, but pointers to user accounts, and they are stored in a different table. + +More info: +- [configuring postgres](https://docs.wire.com/latest/developer/reference/config-options.html?h=config#configure-postgresql) +- [maintaining elastic search](https://docs.wire.com/latest/developer/reference/elastic-search.html?h=elasticsearch) diff --git a/changelog.d/1-api-changes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search b/changelog.d/1-api-changes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search new file mode 100644 index 00000000000..d66924aaeea --- /dev/null +++ b/changelog.d/1-api-changes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search @@ -0,0 +1 @@ +Treat team collaborators like team members in contact search. diff --git a/integration/test/Test/TeamCollaborators.hs b/integration/test/Test/TeamCollaborators.hs index cf55c3a558e..642dad537e8 100644 --- a/integration/test/Test/TeamCollaborators.hs +++ b/integration/test/Test/TeamCollaborators.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2025 Wire Swiss GmbH @@ -17,6 +19,9 @@ module Test.TeamCollaborators where +import qualified API.Brig as BrigP +import qualified API.BrigInternal as BrigI +import API.Common (randomName) import API.Galley import qualified API.GalleyInternal as Internal import Data.Tuple.Extra @@ -317,3 +322,104 @@ testUpdateCollaborator = do [] >>= assertSuccess postOne2OneConversation bob alice team "chit-chat" >>= assertLabel 403 "operation-denied" + +-- | Collaborators are part of the search space of the team they +-- collaborate with: `GET /search/contacts` returns them to members of +-- that team, just like it returns the team's own members. We test +-- collaborators from other teams, personal user accounts that +-- collaborate, and app. +testSearchFindsCollaborator :: (HasCallStack) => App () +testSearchFindsCollaborator = do + (owner, team, [alice]) <- createTeam OwnDomain 2 + (otherOwner, otherTeam, [bob, collab1]) <- createTeam OwnDomain 3 + collab2 :: Value <- randomUser OwnDomain def + collab3 :: Value <- + BrigP.createApp otherOwner otherTeam def + `bindResponse` \resp -> resp.json %. "user" + + collab1Name <- collab1 %. "name" & asString + collab2Name <- collab2 %. "name" & asString + collab3Name <- collab3 %. "name" & asString + + collab1Name' <- randomName + collab2Name' <- randomName + collab3Name' <- randomName + + -- Find before any collaborations have been established. + let assertFinds :: + (HasCallStack, MakesValue expectFound, MakesValue searcher) => + String -> + expectFound -> + searcher -> + App () + assertFinds searchTerm expectFound searcher = do + BrigI.refreshIndex OwnDomain + BrigP.searchContacts searcher searchTerm OwnDomain `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + foundIds :: [String] <- resp.json %. "documents" >>= asList >>= mapM objId + expectedIds :: [String] <- (make >=> asList >=> mapM objId) expectFound + assertBool + ("found: " <> show foundIds <> "; expected: " <> show expectedIds) + (sort foundIds == sort expectedIds) + + for_ [owner, alice] $ assertFinds collab1Name ([] @Value) + for_ [otherOwner, bob] $ assertFinds collab1Name [collab1] + + for_ [owner, alice] $ assertFinds collab2Name [collab2] + for_ [otherOwner, bob] $ assertFinds collab2Name [collab2] + + for_ [owner, alice] $ assertFinds collab3Name ([] @Value) + for_ [otherOwner, bob] $ assertFinds collab3Name [collab3] + + -- Add collaborators to team + for_ [collab1, collab2, collab3] + $ \collab -> + addTeamCollaborator owner team collab ["implicit_connection"] >>= assertSuccess + + for_ [owner, alice] $ assertFinds collab1Name [collab1] + for_ [otherOwner, bob] $ assertFinds collab1Name [collab1] + + for_ [owner, alice] $ assertFinds collab2Name [collab2] + for_ [otherOwner, bob] $ assertFinds collab2Name [collab2] + + for_ [owner, alice] $ assertFinds collab3Name [collab3] + for_ [otherOwner, bob] $ assertFinds collab3Name [collab3] + + -- Check that updating name does not erase collaborating teams in index. + for_ [(collab1, collab1Name'), (collab2, collab2Name'), (collab3, collab3Name')] + $ \(collab, newName) -> do + let updateBody = (def :: BrigP.PutSelf) {BrigP.name = Just newName} + in BrigP.putSelf collab updateBody >>= assertSuccess + + for_ [owner, alice] $ assertFinds collab1Name' [collab1] + for_ [otherOwner, bob] $ assertFinds collab1Name' [collab1] + + for_ [owner, alice] $ assertFinds collab2Name' [collab2] + for_ [otherOwner, bob] $ assertFinds collab2Name' [collab2] + + for_ [owner, alice] $ assertFinds collab3Name' [collab3] + for_ [otherOwner, bob] $ assertFinds collab3Name' [collab3] + + -- Check that updating collaborating teams does not erase name in index. + for_ [collab1, collab2, collab3] + $ \collab -> do + removeTeamCollaborator owner team collab >>= assertSuccess + + for_ [owner, alice] $ assertFinds collab1Name' ([] @Value) + for_ [otherOwner, bob] $ assertFinds collab1Name' [collab1] + + for_ [owner, alice] $ assertFinds collab2Name' [collab2] + for_ [otherOwner, bob] $ assertFinds collab2Name' [collab2] + + for_ [owner, alice] $ assertFinds collab3Name' ([] @Value) + for_ [otherOwner, bob] $ assertFinds collab3Name' [collab3] + + -- Can one user collaborate in multiple teams without breaking search? + (_thirdOwner, _thirdTeam, [multiCollab]) <- createTeam OwnDomain 2 + multiCollabName <- multiCollab %. "name" & asString + + addTeamCollaborator owner team multiCollab ["implicit_connection"] >>= assertSuccess + addTeamCollaborator otherOwner otherTeam multiCollab ["implicit_connection"] >>= assertSuccess + + for_ [owner, alice] $ assertFinds multiCollabName [multiCollab] + for_ [otherOwner, bob] $ assertFinds multiCollabName [multiCollab] diff --git a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Local.hs b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Local.hs new file mode 100644 index 00000000000..d89904b0a8e --- /dev/null +++ b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Local.hs @@ -0,0 +1,65 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2026 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +-- | Interprets 'BrigAPIAccess' from within brig itself, by calling into the local +-- subsystems directly instead of round-tripping over HTTP to itself (as +-- 'Wire.BrigAPIAccess.Rpc.interpretBrigAccess' does for every other service). +-- +-- Only the operations needed by code shared with other services (e.g. +-- 'Wire.TeamCollaboratorsSubsystem') are implemented locally. Everything else +-- falls back to the RPC handler, pointed at brig itself: correct, but a wasteful +-- round-trip through our own listen socket, so it logs a warning and should be +-- given a local implementation once something actually relies on it. +module Wire.BrigAPIAccess.Local where + +import Imports +import Polysemy +import Polysemy.Error (Error) +import Polysemy.Input (runInputConst) +import Polysemy.TinyLog (TinyLog) +import Polysemy.TinyLog qualified as Log +import System.Logger.Message qualified as Log +import Util.Options (Endpoint) +import Wire.BrigAPIAccess +import Wire.BrigAPIAccess.Rpc (brigAccessRpcHandler) +import Wire.ParseException (ParseException) +import Wire.Rpc (Rpc) +import Wire.RpcException (RpcException) +import Wire.UserSubsystem (UserSubsystem) +import Wire.UserSubsystem qualified as UserSubsystem + +-- | The 'Endpoint' is brig's own; it is only used for the operations that have +-- no local implementation yet. +interpretBrigAPIAccessLocally :: + forall r. + ( Member TinyLog r, + Member Rpc r, + Member (Error ParseException) r, + Member (Error RpcException) r + ) => + Endpoint -> + InterpreterFor UserSubsystem r -> + InterpreterFor BrigAPIAccess r +interpretBrigAPIAccessLocally selfEndpoint runUser = interpret $ \case + UpdateSearchIndex uid -> runUser (UserSubsystem.internalUpdateSearchIndex uid) + other -> selfRpc other + where + selfRpc :: forall m x. BrigAPIAccess m x -> Sem r x + selfRpc action = do + Log.warn $ + Log.msg (Log.val "BrigAPIAccess.Local: no local implementation, calling brig over HTTP") + runInputConst selfEndpoint (brigAccessRpcHandler action) diff --git a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs index e42a4791392..b96d0abeea0 100644 --- a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs @@ -82,115 +82,131 @@ interpretBrigAccess :: Sem (BrigAPIAccess ': r) a -> Sem r a interpretBrigAccess brigEndpoint = - interpret $ - runInputConst brigEndpoint . \case - GetConnectionsUnqualified uids muids mrel -> do - getConnectionsUnqualified uids muids mrel - GetConnections uids mquids mrel -> do - getConnections uids mquids mrel - PutConnectionInternal uc -> do - putConnectionInternal uc - ReauthUser uid reauth -> do - reAuthUser uid reauth - LookupActivatedUsers uids -> do - lookupActivatedUsers uids - GetUsers uids -> do - getUsers uids - DeleteUser uid -> do - deleteUser uid - GetContactList uid -> do - getContactList uid - GetUserExportData uid -> do - getUserExportData uid - GetSize tid -> do - getSize tid - LookupClients uids -> do - lookupClients uids - LookupClientsFull uids -> do - lookupClientsFull uids - NotifyClientsAboutLegalHoldRequest self other pk -> do - notifyClientsAboutLegalHoldRequest self other pk - GetLegalHoldAuthToken uid mpwd -> do - getLegalHoldAuthToken uid mpwd - AddLegalHoldClientToUserEither uid conn pks lpk -> do - addLegalHoldClientToUser uid conn pks lpk - RemoveLegalHoldClientFromUser uid -> do - removeLegalHoldClientFromUser uid - GetAccountConferenceCallingConfigClient uid -> do - getAccountConferenceCallingConfigClient uid - GetLocalMLSClients qusr ss -> do - getLocalMLSClients qusr ss - GetLocalMLSClient qusr cid ss -> do - getLocalMLSClient qusr cid ss - UpdateSearchVisibilityInbound status -> do - updateSearchVisibilityInbound status - DeleteBot convId botId -> - deleteBot convId botId - UpdateSearchIndex uid -> updateSearchIndex uid - GetAccountsBy localGetBy -> - getAccountsBy localGetBy - GetUsersByVariousKeys uids handles emails includePendingInvitations -> - getUsersByVariousKeys uids handles emails includePendingInvitations - CreateGroupInternal managedBy teamId creatorUserId newGroup -> - createGroupInternal managedBy teamId creatorUserId newGroup - GetGroupsInternal tid mbFilter mbManagedBy startIndex mbCount -> - getGroupsInternal tid mbFilter mbManagedBy startIndex mbCount - GetGroupInternal tid gid includeChannels -> - getGroupInternal tid gid includeChannels - UpdateGroup req -> - updateGroup req - DeleteGroupInternal managedBy teamId groupId -> - deleteGroupInternal managedBy teamId groupId - GetAppIdsForTeam teamId -> - getAppIdsForTeam teamId - SetAccountStatus uid status -> - setAccountStatus uid status - DeleteApp teamId uid -> - deleteApp teamId uid - CreateSAML uref buid teamid name managedBy handle richInfo mLocale role -> - createSAML uref buid teamid name managedBy handle richInfo mLocale role - CreateNoSAML extId email uid teamid uname locale role -> - createNoSAML extId email uid teamid uname locale role - UpdateEmail uid email activation -> - updateEmail uid email activation - GetAccount havePending uid -> - getAccount havePending uid - GetAccountByHandle handle -> - getByHandle handle - GetByEmail email -> - getByEmail email - SetName uid name -> - setName uid name - SetHandle uid handle -> - setHandle uid handle - SetManagedBy uid managedBy -> - setManagedBy uid managedBy - DeletePendingEmailUpdate uid -> - deletePendingEmailUpdate uid - SetSSOId uid ssoId -> - setSSOId uid ssoId - SetRichInfo uid richInfo -> - setRichInfo uid richInfo - SetLocale uid mLocale -> - setLocale uid mLocale - GetRichInfo uid -> - getRichInfo uid - CheckHandleAvailable handle -> - checkHandleAvailable handle - SsoLogin uid mLabel -> - ssoLogin uid mLabel - GetStatus uid -> - getStatus uid - GetStatusMaybe uid -> - getStatusMaybe uid - SetStatus uid status -> - setStatus uid status - GetDefaultUserLocale -> - getDefaultUserLocale - CheckAdminGetTeamId uid -> - checkAdminGetTeamId uid - SendSAMLIdPChangedEmail notif -> - sendSAMLIdPChangedEmail notif + interpret $ runInputConst brigEndpoint . brigAccessRpcHandler + +-- | Handles a single 'BrigAPIAccess' action by calling brig over HTTP. +-- +-- Exposed separately from 'interpretBrigAccess' so that +-- 'Wire.BrigAPIAccess.Local.interpretBrigAPIAccessLocally' can delegate the +-- actions it does not implement itself. 'BrigAPIAccess' is a first-order +-- effect, so @m@ is unconstrained and any handler's action can be passed here. +brigAccessRpcHandler :: + ( Member TinyLog r, + Member Rpc r, + Member (Error ParseException) r, + Member (Error RpcException) r, + Member (Input Endpoint) r + ) => + BrigAPIAccess m a -> + Sem r a +brigAccessRpcHandler = \case + GetConnectionsUnqualified uids muids mrel -> do + getConnectionsUnqualified uids muids mrel + GetConnections uids mquids mrel -> do + getConnections uids mquids mrel + PutConnectionInternal uc -> do + putConnectionInternal uc + ReauthUser uid reauth -> do + reAuthUser uid reauth + LookupActivatedUsers uids -> do + lookupActivatedUsers uids + GetUsers uids -> do + getUsers uids + DeleteUser uid -> do + deleteUser uid + GetContactList uid -> do + getContactList uid + GetUserExportData uid -> do + getUserExportData uid + GetSize tid -> do + getSize tid + LookupClients uids -> do + lookupClients uids + LookupClientsFull uids -> do + lookupClientsFull uids + NotifyClientsAboutLegalHoldRequest self other pk -> do + notifyClientsAboutLegalHoldRequest self other pk + GetLegalHoldAuthToken uid mpwd -> do + getLegalHoldAuthToken uid mpwd + AddLegalHoldClientToUserEither uid conn pks lpk -> do + addLegalHoldClientToUser uid conn pks lpk + RemoveLegalHoldClientFromUser uid -> do + removeLegalHoldClientFromUser uid + GetAccountConferenceCallingConfigClient uid -> do + getAccountConferenceCallingConfigClient uid + GetLocalMLSClients qusr ss -> do + getLocalMLSClients qusr ss + GetLocalMLSClient qusr cid ss -> do + getLocalMLSClient qusr cid ss + UpdateSearchVisibilityInbound status -> do + updateSearchVisibilityInbound status + DeleteBot convId botId -> + deleteBot convId botId + UpdateSearchIndex uid -> updateSearchIndex uid + GetAccountsBy localGetBy -> + getAccountsBy localGetBy + GetUsersByVariousKeys uids handles emails includePendingInvitations -> + getUsersByVariousKeys uids handles emails includePendingInvitations + CreateGroupInternal managedBy teamId creatorUserId newGroup -> + createGroupInternal managedBy teamId creatorUserId newGroup + GetGroupsInternal tid mbFilter mbManagedBy startIndex mbCount -> + getGroupsInternal tid mbFilter mbManagedBy startIndex mbCount + GetGroupInternal tid gid includeChannels -> + getGroupInternal tid gid includeChannels + UpdateGroup req -> + updateGroup req + DeleteGroupInternal managedBy teamId groupId -> + deleteGroupInternal managedBy teamId groupId + GetAppIdsForTeam teamId -> + getAppIdsForTeam teamId + SetAccountStatus uid status -> + setAccountStatus uid status + DeleteApp teamId uid -> + deleteApp teamId uid + CreateSAML uref buid teamid name managedBy handle richInfo mLocale role -> + createSAML uref buid teamid name managedBy handle richInfo mLocale role + CreateNoSAML extId email uid teamid uname locale role -> + createNoSAML extId email uid teamid uname locale role + UpdateEmail uid email activation -> + updateEmail uid email activation + GetAccount havePending uid -> + getAccount havePending uid + GetAccountByHandle handle -> + getByHandle handle + GetByEmail email -> + getByEmail email + SetName uid name -> + setName uid name + SetHandle uid handle -> + setHandle uid handle + SetManagedBy uid managedBy -> + setManagedBy uid managedBy + DeletePendingEmailUpdate uid -> + deletePendingEmailUpdate uid + SetSSOId uid ssoId -> + setSSOId uid ssoId + SetRichInfo uid richInfo -> + setRichInfo uid richInfo + SetLocale uid mLocale -> + setLocale uid mLocale + GetRichInfo uid -> + getRichInfo uid + CheckHandleAvailable handle -> + checkHandleAvailable handle + SsoLogin uid mLabel -> + ssoLogin uid mLabel + GetStatus uid -> + getStatus uid + GetStatusMaybe uid -> + getStatusMaybe uid + SetStatus uid status -> + setStatus uid status + GetDefaultUserLocale -> + getDefaultUserLocale + CheckAdminGetTeamId uid -> + checkAdminGetTeamId uid + SendSAMLIdPChangedEmail notif -> + sendSAMLIdPChangedEmail notif brigRequest :: (Member Rpc r, Member (Input Endpoint) r) => (Request -> Request) -> Sem r (Response (Maybe LByteString)) brigRequest req = do diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs index 6317ed7ba2d..31d77779011 100644 --- a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs @@ -29,6 +29,7 @@ import Data.Conduit.List qualified as CL import Data.Id import Data.Json.Util (UTCTimeMillis (fromUTCTimeMillis)) import Data.Map qualified as Map +import Data.Set qualified as Set import Database.Bloodhound qualified as ES import Imports import Polysemy @@ -37,6 +38,7 @@ import Polysemy.TinyLog import Polysemy.TinyLog qualified as Log import System.Logger.Message qualified as Log import UnliftIO (pooledForConcurrentlyN) +import Wire.API.Team.Collaborator (gTeam, gUser) import Wire.API.Team.Feature import Wire.API.Team.Member.Info import Wire.API.Team.Role @@ -45,6 +47,7 @@ import Wire.IndexedUserStore (IndexedUserStore) import Wire.IndexedUserStore qualified as IndexedUserStore import Wire.IndexedUserStore.MigrationStore import Wire.IndexedUserStore.MigrationStore qualified as MigrationStore +import Wire.TeamCollaboratorsStore (TeamCollaboratorsStore, getTeamCollaborationsForUsers) import Wire.UserSearch.Migration import Wire.UserSearch.Types import Wire.UserStore @@ -54,22 +57,28 @@ type IOInterpreter r = forall a. Sem r a -> IO a -- | Increase this number any time you want to force reindexing. expectedMigrationVersion :: MigrationVersion -expectedMigrationVersion = MigrationVersion 6 +expectedMigrationVersion = MigrationVersion 7 -syncAllUsers :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r) => IOInterpreter r -> Int32 -> IO () +syncAllUsers :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r, Member TeamCollaboratorsStore r) => IOInterpreter r -> Int32 -> IO Int syncAllUsers interpreter pageSize = syncAllUsersWithVersion interpreter pageSize ES.ExternalGT -forceSyncAllUsers :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r) => IOInterpreter r -> Int32 -> IO () +forceSyncAllUsers :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r, Member TeamCollaboratorsStore r) => IOInterpreter r -> Int32 -> IO Int forceSyncAllUsers interpreter pageSize = syncAllUsersWithVersion interpreter pageSize ES.ExternalGTE -syncAllUsersWithVersion :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r) => IOInterpreter r -> Int32 -> (ES.ExternalDocVersion -> ES.VersionControl) -> IO () +-- | Returns the number of users that could not be indexed because some of the +-- data needed to build their document was unavailable. Those users have been +-- logged individually by 'logAndHush'. +syncAllUsersWithVersion :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r, Member TeamCollaboratorsStore r) => IOInterpreter r -> Int32 -> (ES.ExternalDocVersion -> ES.VersionControl) -> IO Int syncAllUsersWithVersion interpreter pageSize mkVersion = - runConduit $ + fmap getSum . runConduit $ zipSources (CL.sourceList [1 ..]) (paginateWithStateC (interpreter . getIndexUsersPaginated pageSize)) .| logPage .| mkUserDocs - .| Conduit.mapM_ (interpreter . IndexedUserStore.bulkUpsert) + .| Conduit.foldMapM upsertPage where + upsertPage :: (Int, [(ES.DocId, UserDoc, ES.VersionControl)]) -> IO (Sum Int) + upsertPage (skipped, docs) = Sum skipped <$ interpreter (IndexedUserStore.bulkUpsert docs) + logPage :: ConduitT (Int32, [IndexUser]) [IndexUser] IO () logPage = Conduit.mapM $ \(pageNumber, page) -> do interpreter $ @@ -79,7 +88,9 @@ syncAllUsersWithVersion interpreter pageSize mkVersion = . Log.field "firstUser" (maybe "N/A" (idToText . (.userId)) (headMay page)) pure page - mkUserDocs :: ConduitT [IndexUser] [(ES.DocId, UserDoc, ES.VersionControl)] IO () + -- Emits the documents to be indexed together with the number of users of + -- this page that had to be skipped. + mkUserDocs :: ConduitT [IndexUser] (Int, [(ES.DocId, UserDoc, ES.VersionControl)]) IO () mkUserDocs = Conduit.mapM $ \page -> do -- FUTUREWORK: extract team visibilities, roles and user type -- more efficiently sending one query per page @@ -88,10 +99,10 @@ syncAllUsersWithVersion interpreter pageSize mkVersion = -- contains User, Maybe Role, UserType, ..., and pass around -- ExtendedUser. this should make the code less convoluted. - let teams :: Map TeamId [IndexUser] = Map.fromListWith (<>) $ mapMaybe (\u -> (,[u]) <$> u.teamId) page - teamIds = Map.keys teams + let teams :: Map TeamId [IndexUser] + teams = Map.fromListWith (<>) $ mapMaybe (\u -> (,[u]) <$> u.teamId) page - visMap <- fmap Map.fromList . pooledForConcurrentlyN 16 teamIds $ \t -> do + visMap <- fmap Map.fromList . pooledForConcurrentlyN 16 (Map.keys teams) $ \t -> do x <- try $ interpreter $ teamSearchVisibilityInbound t pure (t, x) @@ -114,6 +125,12 @@ syncAllUsersWithVersion interpreter pageSize mkVersion = fmap Map.unions . pooledForConcurrentlyN 16 (Map.toList teams) $ \(t, us) -> getRoles t (fmap (.userId) us) + -- One query for the whole page. A failure here fails every document of the + -- page, which 'logAndHush' then logs and skips. + eithCollabTeams :: Either SomeException (Map UserId [TeamId]) <- + try . fmap (Map.fromListWith (<>) . map (\tc -> (gUser tc, [gTeam tc]))) . interpreter $ + getTeamCollaborationsForUsers (Set.fromList (map (.userId) page)) + let vis :: IndexUser -> Either SomeException SearchVisibilityInbound vis indexUser = fromMaybe (Right defaultSearchVisibilityInbound) $ flip Map.lookup visMap =<< indexUser.teamId @@ -122,15 +139,20 @@ syncAllUsersWithVersion interpreter pageSize mkVersion = mkUserDoc indexUser = do currentVis <- vis indexUser currentRole <- sequence $ Map.lookup indexUser.userId roles - pure $ indexUserToDoc currentVis ((.value) <$> currentRole) indexUser + currentCollabTeams <- Map.findWithDefault [] indexUser.userId <$> eithCollabTeams + pure $ indexUserToDoc currentVis ((.value) <$> currentRole) currentCollabTeams indexUser mkDocVersion :: IndexUser -> Either SomeException ES.VersionControl mkDocVersion u = do roleWithTime <- sequence (Map.lookup u.userId roles) pure . mkVersion . ES.ExternalDocVersion . docVersion $ indexUserToVersion roleWithTime u - let docsWithErrors = map (\u -> (userIdToDocId u.userId, mkUserDoc u, mkDocVersion u)) page - interpreter . flip mapMaybeM docsWithErrors $ logAndHush + docsWithErrors :: (e ~ Either SomeException) => [(ES.DocId, e UserDoc, e ES.VersionControl)] + docsWithErrors = map (\u -> (userIdToDocId u.userId, mkUserDoc u, mkDocVersion u)) page + + docs <- interpreter . flip mapMaybeM docsWithErrors $ logAndHush + let skipped = length docsWithErrors - length docs + pure (skipped, docs) rightSecond :: (a, b) -> (a, Either c b) rightSecond (a, b) = (a, Right b) @@ -159,7 +181,7 @@ syncAllUsersWithVersion interpreter pageSize mkVersion = <$> permissionsToRole tmi.permissions migrateData :: - (Member (Embed IO) r, Member IndexedUserStore r, Member (Error MigrationException) r, Member IndexedUserMigrationStore r, Member TinyLog r, Member UserStore r, Member GalleyAPIAccess r) => + (Member (Embed IO) r, Member IndexedUserStore r, Member (Error MigrationException) r, Member IndexedUserMigrationStore r, Member TinyLog r, Member UserStore r, Member GalleyAPIAccess r, Member TeamCollaboratorsStore r) => IOInterpreter r -> Int32 -> IO () @@ -174,8 +196,15 @@ migrateData interpreter pageSize = interpreter $ do Log.msg (Log.val "Migration necessary.") . Log.field "expectedVersion" expectedMigrationVersion . Log.field "foundVersion" foundVersion - embed $ forceSyncAllUsers interpreter pageSize - MigrationStore.persistMigrationVersion expectedMigrationVersion + skipped <- embed $ forceSyncAllUsers interpreter pageSize + if skipped == 0 + then MigrationStore.persistMigrationVersion expectedMigrationVersion + else do + Log.err $ + Log.msg (Log.val "Migration incomplete, not persisting migration version.") + . Log.field "expectedVersion" expectedMigrationVersion + . Log.field "skippedUsers" skipped + throw $ SyncIncomplete else do Log.info $ Log.msg (Log.val "No migration necessary.") diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs index 156f8f6e479..07572a29851 100644 --- a/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs @@ -526,13 +526,17 @@ matchSelf :: UserId -> Maybe ES.Query matchSelf searcher = Just (termQ "_id" (idToText searcher)) -- | Exclude apps from other teams. --- Apps should only be searchable within their own team. +-- Apps should only be searchable within their own team, or within a team they +-- collaborate with. matchAppsFromOtherTeams :: Maybe TeamId -> Maybe ES.Query matchAppsFromOtherTeams mSearcherTeamId = Just $ ES.QueryBoolQuery boolQuery - { ES.boolQueryMustMatch = + { -- Apps collaborating with the searcher's team are not excluded. + ES.boolQueryMustNotMatch = + maybeToList (termQ "collaborating_teams" . idToText <$> mSearcherTeamId), + ES.boolQueryMustMatch = [ -- Match apps (type = "app") termQ "type" "app", -- That are from a different team than the searcher @@ -640,7 +644,16 @@ restrictSearchSpaceByUserType = \case else ES.TermsQuery "type" (userTypeFilterToText <$> (utsH :| utsT)) matchTeamMembersOf :: TeamId -> ES.Query -matchTeamMembersOf team = ES.TermQuery (ES.Term "team" $ idToText team) Nothing +matchTeamMembersOf team = + ES.QueryBoolQuery + boolQuery + { ES.boolQueryShouldMatch = + [ -- Match users who are members of the team + ES.TermQuery (ES.Term "team" $ idToText team) Nothing, + -- Match users who are collaborators in the team + ES.TermQuery (ES.Term "collaborating_teams" $ idToText team) Nothing + ] + } matchTeamMembersSearchableByAllTeams :: ES.Query matchTeamMembersSearchableByAllTeams = diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs index fcdf0731b08..ebf79c96721 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs @@ -29,6 +29,8 @@ data TeamCollaboratorsStore m a where GetAllTeamCollaborators :: TeamId -> TeamCollaboratorsStore m [TeamCollaborator] GetTeamCollaborator :: TeamId -> UserId -> TeamCollaboratorsStore m (Maybe TeamCollaborator) GetTeamCollaborations :: UserId -> TeamCollaboratorsStore m ([TeamCollaborator]) + -- | Batched 'GetTeamCollaborations', for callers that process users in pages. + GetTeamCollaborationsForUsers :: Set UserId -> TeamCollaboratorsStore m [TeamCollaborator] GetTeamCollaboratorsWithIds :: Set TeamId -> Set UserId -> TeamCollaboratorsStore m [TeamCollaborator] UpdateTeamCollaborator :: UserId -> TeamId -> Set CollaboratorPermission -> TeamCollaboratorsStore m () RemoveTeamCollaborator :: UserId -> TeamId -> TeamCollaboratorsStore m () diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs index a6a1e968a72..b898ae69b51 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs @@ -49,6 +49,7 @@ interpretTeamCollaboratorsStoreToPostgres = GetAllTeamCollaborators teamId -> getAllTeamCollaboratorsImpl teamId GetTeamCollaborator teamId userId -> getTeamCollaboratorImpl teamId userId GetTeamCollaborations userId -> getTeamCollaborationsImpl userId + GetTeamCollaborationsForUsers userIds -> getTeamCollaborationsForUsersImpl userIds GetTeamCollaboratorsWithIds teamIds userIds -> getTeamCollaboratorsWithIdsImpl teamIds userIds UpdateTeamCollaborator userId teamId permissions -> updateTeamCollaboratorImpl userId teamId permissions RemoveTeamCollaborator userId teamId -> removeTeamCollaboratorImpl userId teamId @@ -181,6 +182,22 @@ getTeamCollaborationsImpl teamId = do select user_id :: uuid, team_id :: uuid, permissions :: int2[] from collaborators where user_id = ($1 :: uuid) |] +getTeamCollaborationsForUsersImpl :: + (PGConstraints r) => + Set UserId -> + Sem r [TeamCollaborator] +getTeamCollaborationsForUsersImpl userIds = do + runStatement (Data.Set.toList userIds) getAllCollaborationsByUsersStatement + where + getAllCollaborationsByUsersStatement :: Statement [UserId] [TeamCollaborator] + getAllCollaborationsByUsersStatement = + dimap + (Data.Vector.fromList . Imports.map toUUID) + (Data.Vector.toList . (toTeamCollaborator <$>)) + $ [vectorStatement| + select user_id :: uuid, team_id :: uuid, permissions :: int2[] from collaborators where user_id = ANY($1 :: uuid[]) + |] + getTeamCollaboratorsWithIdsImpl :: (PGConstraints r) => Set TeamId -> diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs index 30a970706e3..bb0541636b0 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs @@ -29,6 +29,8 @@ import Wire.API.Error.Brig qualified as E import Wire.API.Event.Team import Wire.API.Team.Collaborator import Wire.API.Team.Member qualified as TeamMember +import Wire.BrigAPIAccess (BrigAPIAccess) +import Wire.BrigAPIAccess qualified as BrigAPIAccess import Wire.Error import Wire.NotificationSubsystem import Wire.Sem.Now @@ -44,15 +46,18 @@ interpretTeamCollaboratorsSubsystem :: Member Now r, Member NotificationSubsystem r ) => + InterpreterFor BrigAPIAccess r -> InterpreterFor TeamCollaboratorsSubsystem r -interpretTeamCollaboratorsSubsystem = interpret $ \case - CreateTeamCollaborator zUser user team perms -> createTeamCollaboratorImpl zUser user team perms - GetAllTeamCollaborators zUser team -> getAllTeamCollaboratorsImpl zUser team - InternalGetTeamCollaborator team user -> internalGetTeamCollaboratorImpl team user - InternalGetTeamCollaborations userId -> internalGetTeamCollaborationsImpl userId - InternalGetTeamCollaboratorsWithIds teams userIds -> internalGetTeamCollaboratorsWithIdsImpl teams userIds - InternalUpdateTeamCollaborator user team perms -> internalUpdateTeamCollaboratorImpl user team perms - InternalRemoveTeamCollaborator user team -> internalRemoveTeamCollaboratorImpl user team +interpretTeamCollaboratorsSubsystem brigAPIAccess = + interpret $ + brigAPIAccess . \case + CreateTeamCollaborator zUser user team perms -> createTeamCollaboratorImpl zUser user team perms + GetAllTeamCollaborators zUser team -> getAllTeamCollaboratorsImpl zUser team + InternalGetTeamCollaborator team user -> internalGetTeamCollaboratorImpl team user + InternalGetTeamCollaborations userId -> internalGetTeamCollaborationsImpl userId + InternalGetTeamCollaboratorsWithIds teams userIds -> internalGetTeamCollaboratorsWithIdsImpl teams userIds + InternalUpdateTeamCollaborator user team perms -> internalUpdateTeamCollaboratorImpl user team perms + InternalRemoveTeamCollaborator user team -> internalRemoveTeamCollaboratorImpl user team internalGetTeamCollaboratorImpl :: (Member Store.TeamCollaboratorsStore r) => @@ -74,7 +79,8 @@ createTeamCollaboratorImpl :: Member (Error TeamCollaboratorsError) r, Member Store.TeamCollaboratorsStore r, Member Now r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member BrigAPIAccess r ) => Local UserId -> UserId -> @@ -85,9 +91,11 @@ createTeamCollaboratorImpl zUser user team perms = do guardPermission (tUnqualified zUser) team TeamMember.GetTeamCollaborators InsufficientRights Store.createTeamCollaborator user team perms - -- TODO: Review the event's values generateTeamEvents (tUnqualified zUser) team [EdCollaboratorAdd user (Set.toList perms)] + -- Reindex the collaborator with their new collaboration team + BrigAPIAccess.updateSearchIndex user + getAllTeamCollaboratorsImpl :: ( Member TeamSubsystem r, Member (Error TeamCollaboratorsError) r, @@ -109,21 +117,25 @@ internalGetTeamCollaboratorsWithIdsImpl = do Store.getTeamCollaboratorsWithIds internalUpdateTeamCollaboratorImpl :: - (Member Store.TeamCollaboratorsStore r) => + (Member Store.TeamCollaboratorsStore r, Member BrigAPIAccess r) => UserId -> TeamId -> Set CollaboratorPermission -> Sem r () internalUpdateTeamCollaboratorImpl user team perms = do Store.updateTeamCollaborator user team perms + -- Reindex collaborator when permissions change + BrigAPIAccess.updateSearchIndex user internalRemoveTeamCollaboratorImpl :: - (Member Store.TeamCollaboratorsStore r) => + (Member Store.TeamCollaboratorsStore r, Member BrigAPIAccess r) => UserId -> TeamId -> Sem r () internalRemoveTeamCollaboratorImpl user team = do Store.removeTeamCollaborator user team + -- Reindex collaborator when removed + BrigAPIAccess.updateSearchIndex user -- This is of general usefulness. However, we cannot move this to wire-api as -- this would lead to a cyclic dependency. diff --git a/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs b/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs index 817d10370e9..7cbbbcd734e 100644 --- a/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs +++ b/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs @@ -42,6 +42,7 @@ data MigrationException | PutMappingFailed String | TargetIndexAbsent | VersionSourceMissing (ES.SearchResult MigrationVersion) + | SyncIncomplete deriving (Show) instance Exception MigrationException diff --git a/libs/wire-subsystems/src/Wire/UserSearch/Types.hs b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs index 5e8dcac765e..5464dae2a8f 100644 --- a/libs/wire-subsystems/src/Wire/UserSearch/Types.hs +++ b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs @@ -79,7 +79,10 @@ data UserDoc = UserDoc udScimExternalId :: Maybe Text, udSso :: Maybe Sso, udEmailUnvalidated :: Maybe EmailAddress, - udSearchable :: Maybe Bool + udSearchable :: Maybe Bool, + -- | Teams that have added this user as a collaborator. + -- Updated separately via 'syncUserIndexCollaborations' when collaborator relationships change. + udCollaboratingTeams :: [TeamId] } deriving (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UserDoc) @@ -104,7 +107,8 @@ instance ToJSON UserDoc where "scim_external_id" .= udScimExternalId ud, "sso" .= udSso ud, "email_unvalidated" .= udEmailUnvalidated ud, - "searchable" .= udSearchable ud + "searchable" .= udSearchable ud, + "collaborating_teams" .= udCollaboratingTeams ud ] instance FromJSON UserDoc where @@ -128,6 +132,7 @@ instance FromJSON UserDoc where <*> o .:? "sso" <*> o .:? "email_unvalidated" <*> o .:? "searchable" + <*> o .:? "collaborating_teams" .!= [] searchVisibilityInboundFieldName :: Key searchVisibilityInboundFieldName = "search_visibility_inbound" diff --git a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs index 09ac630d191..b051132f1ed 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs @@ -126,8 +126,8 @@ indexUserToVersion :: Maybe (WithWritetime Role) -> IndexUser -> IndexVersion indexUserToVersion role iu = mkIndexVersion [Just $ Writetime iu.updatedAt, const () <$$> fmap writetime role] -indexUserToDoc :: SearchVisibilityInbound -> Maybe Role -> IndexUser -> UserDoc -indexUserToDoc searchVisInbound mRole IndexUser {..} = +indexUserToDoc :: SearchVisibilityInbound -> Maybe Role -> [TeamId] -> IndexUser -> UserDoc +indexUserToDoc searchVisInbound mRole collaboratingTeams IndexUser {..} = if shouldIndex then UserDoc @@ -148,7 +148,8 @@ indexUserToDoc searchVisInbound mRole IndexUser {..} = udHandle = handle, udNormalized = Just $ normalized name.fromName, udName = Just name, - udTeam = teamId + udTeam = teamId, + udCollaboratingTeams = collaboratingTeams } else -- We insert a tombstone-style user here, as it's easier than -- deleting the old one. It's mostly empty, but having the status here @@ -209,5 +210,6 @@ emptyUserDoc uid = udNormalized = Nothing, udName = Nothing, udTeam = Nothing, + udCollaboratingTeams = [], udId = uid } diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index d5cb2dfee62..f78029cebde 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -60,6 +60,7 @@ import Wire.API.Federation.Error import Wire.API.MLS.CipherSuite (CipherSuiteTag, csSignatureScheme) import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti (TeamStatus (..)) +import Wire.API.Team.Collaborator (gTeam) import Wire.API.Team.Export import Wire.API.Team.Feature import Wire.API.Team.Member @@ -102,6 +103,8 @@ import Wire.Sem.Metrics qualified as Metrics import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredUser +import Wire.TeamCollaboratorsStore (TeamCollaboratorsStore) +import Wire.TeamCollaboratorsStore qualified as TeamCollaboratorsStore import Wire.TeamSubsystem import Wire.UserGroupStore (UserGroupStore, getUserGroupIdsForUsers) import Wire.UserKeyStore @@ -141,6 +144,7 @@ runUserSubsystem :: Member TinyLog r, Member (Input UserSubsystemConfig) r, Member TeamSubsystem r, + Member TeamCollaboratorsStore r, Member UserGroupStore r, Member (Input (Local any)) r ) => @@ -711,6 +715,7 @@ updateUserProfileImpl :: Member Events r, Member GalleyAPIAccess r, Member IndexedUserStore r, + Member TeamCollaboratorsStore r, Member Metrics r ) => Local UserId -> @@ -772,6 +777,7 @@ updateHandleImpl :: Member Events r, Member UserStore r, Member IndexedUserStore r, + Member TeamCollaboratorsStore r, Member Metrics r ) => Local UserId -> @@ -839,7 +845,8 @@ syncUserIndex :: ( Member UserStore r, Member GalleyAPIAccess r, Member IndexedUserStore r, - Member Metrics r + Member Metrics r, + Member TeamCollaboratorsStore r ) => UserId -> Sem r () @@ -860,9 +867,13 @@ syncUserIndex uid = teamSearchVisibilityInbound indexUser.teamId tm <- maybe (pure Nothing) selectTeamMember indexUser.teamId + collabTeams <- map gTeam <$> TeamCollaboratorsStore.getTeamCollaborations uid let mRole = tm >>= mkRoleWithWriteTime - userDoc = indexUserToDoc vis (value <$> mRole) indexUser - version = ES.ExternalGT . ES.ExternalDocVersion . docVersion $ indexUserToVersion mRole indexUser + userDoc = indexUserToDoc vis (value <$> mRole) collabTeams indexUser + -- GTE, not GT: the version comes from the user row alone, but the document also + -- holds data that changes without touching that row (collaborations), and under + -- GT those updates would be dropped as version conflicts. Older writes still lose. + version = ES.ExternalGTE . ES.ExternalDocVersion . docVersion $ indexUserToVersion mRole indexUser Metrics.incCounter indexUpdateCounter IndexedUserStore.upsert (userIdToDocId uid) userDoc version @@ -1180,6 +1191,7 @@ acceptTeamInvitationImpl :: Member (Error UserSubsystemError) r, Member InvitationStore r, Member IndexedUserStore r, + Member TeamCollaboratorsStore r, Member Metrics r, Member Events r, Member AuthenticationSubsystem r, @@ -1244,6 +1256,7 @@ removeEmailEitherImpl :: Member UserStore r, Member Events r, Member IndexedUserStore r, + Member TeamCollaboratorsStore r, Member (Input UserSubsystemConfig) r, Member GalleyAPIAccess r, Member Metrics r @@ -1280,6 +1293,7 @@ setUserSearchableImpl :: Member TeamSubsystem r, Member GalleyAPIAccess r, Member IndexedUserStore r, + Member TeamCollaboratorsStore r, Member Metrics r ) => Local UserId -> diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index 1324d919db3..ff27a27fa7f 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -365,22 +365,21 @@ miniBackendLowerEffectsInterpreters mb@(MiniBackendParams {..}) = . runInputConst conversationCfg . runClientSubsystem undefined undefined where - -- Mock BrigAPIAccess interpreter for tests - mockBrigAPIAccess :: forall r'. InterpreterFor BrigAPIAccess r' - mockBrigAPIAccess = interpret $ \case - _ -> error "Unimplemented BrigAPIAccess operation in mock" -- Mock UserClientIndexStore interpreter for tests mockUserClientIndexStore :: forall r'. InterpreterFor UserClientIndexStore r' mockUserClientIndexStore = interpret $ \case _ -> error "Unimplemented UserClientIndexStore operation in mock" + -- Mock BackendNotificationQueueAccess interpreter for tests mockBackendNotificationQueueAccess :: forall r'. InterpreterFor BackendNotificationQueueAccess r' mockBackendNotificationQueueAccess = interpret $ \case _ -> error "Unimplemented BackendNotificationQueueAccess operation in mock" + -- Mock ConversationSubsystem interpreter for tests mockConversationSubsystem :: forall r'. InterpreterFor ConversationSubsystem r' mockConversationSubsystem = interpretH $ \case _ -> error "Unimplemented ConversationSubsystem operation in mock" + mockMlsKeyPackageSubsystem :: forall r'. InterpreterFor MlsKeyPackageSubsystem r' mockMlsKeyPackageSubsystem = interpret $ \case HasMlsKeyPackages {} -> pure False @@ -786,7 +785,7 @@ interpretMaybeFederationStackState :: Sem (MiniBackendEffects `Append` r) a -> Sem r (MiniBackend, a) interpretMaybeFederationStackState mb = - miniBackendLowerEffectsInterpreters mb . interpretTeamCollaboratorsSubsystem . runRecursiveAuthUserApp + miniBackendLowerEffectsInterpreters mb . interpretTeamCollaboratorsSubsystem subsume . runRecursiveAuthUserApp -- FUTUREWORK(fisx): it would be nice to have a definition of an -- interpreter of all the subsystems combined, but since the diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs index 4630c0c7f77..ea57140aad5 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs @@ -25,6 +25,7 @@ import Wire.MockInterpreters.AppStore as MockInterpreters import Wire.MockInterpreters.AuthenticationSubsystem as MockInterpreters import Wire.MockInterpreters.BackgroundJobPublisher as MockInterpreters import Wire.MockInterpreters.BlockListStore as MockInterpreters +import Wire.MockInterpreters.BrigAPIAccess as MockInterpreters import Wire.MockInterpreters.ClientStore as MockInterpreters import Wire.MockInterpreters.ConversationStore as MockInterpreters import Wire.MockInterpreters.ConversationSubsystem as MockInterpreters diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/BrigAPIAccess.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/BrigAPIAccess.hs new file mode 100644 index 00000000000..aeb76299093 --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/BrigAPIAccess.hs @@ -0,0 +1,81 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2026 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.MockInterpreters.BrigAPIAccess where + +import Imports +import Polysemy +import Wire.BrigAPIAccess + +-- | Errors out on everything except 'UpdateSearchIndex', which is a no-op. +mockBrigAPIAccess :: InterpreterFor BrigAPIAccess r +mockBrigAPIAccess = interpret $ \case + UpdateSearchIndex _ -> pure () + -- everything else is not implemented + GetConnectionsUnqualified {} -> error "GetConnectionsUnqualified: implement on demand (mockBrigAPIAccess)" + GetConnections {} -> error "GetConnections: implement on demand (mockBrigAPIAccess)" + PutConnectionInternal {} -> error "PutConnectionInternal: implement on demand (mockBrigAPIAccess)" + ReauthUser {} -> error "ReauthUser: implement on demand (mockBrigAPIAccess)" + LookupActivatedUsers {} -> error "LookupActivatedUsers: implement on demand (mockBrigAPIAccess)" + GetUsers {} -> error "GetUsers: implement on demand (mockBrigAPIAccess)" + DeleteUser {} -> error "DeleteUser: implement on demand (mockBrigAPIAccess)" + GetContactList {} -> error "GetContactList: implement on demand (mockBrigAPIAccess)" + GetSize {} -> error "GetSize: implement on demand (mockBrigAPIAccess)" + LookupClients {} -> error "LookupClients: implement on demand (mockBrigAPIAccess)" + LookupClientsFull {} -> error "LookupClientsFull: implement on demand (mockBrigAPIAccess)" + NotifyClientsAboutLegalHoldRequest {} -> error "NotifyClientsAboutLegalHoldRequest: implement on demand (mockBrigAPIAccess)" + GetLegalHoldAuthToken {} -> error "GetLegalHoldAuthToken: implement on demand (mockBrigAPIAccess)" + AddLegalHoldClientToUserEither {} -> error "AddLegalHoldClientToUserEither: implement on demand (mockBrigAPIAccess)" + RemoveLegalHoldClientFromUser {} -> error "RemoveLegalHoldClientFromUser: implement on demand (mockBrigAPIAccess)" + GetAccountConferenceCallingConfigClient {} -> error "GetAccountConferenceCallingConfigClient: implement on demand (mockBrigAPIAccess)" + GetLocalMLSClients {} -> error "GetLocalMLSClients: implement on demand (mockBrigAPIAccess)" + GetLocalMLSClient {} -> error "GetLocalMLSClient: implement on demand (mockBrigAPIAccess)" + UpdateSearchVisibilityInbound {} -> error "UpdateSearchVisibilityInbound: implement on demand (mockBrigAPIAccess)" + GetUserExportData {} -> error "GetUserExportData: implement on demand (mockBrigAPIAccess)" + DeleteBot {} -> error "DeleteBot: implement on demand (mockBrigAPIAccess)" + GetAccountsBy {} -> error "GetAccountsBy: implement on demand (mockBrigAPIAccess)" + GetUsersByVariousKeys {} -> error "GetUsersByVariousKeys: implement on demand (mockBrigAPIAccess)" + CreateGroupInternal {} -> error "CreateGroupInternal: implement on demand (mockBrigAPIAccess)" + GetGroupInternal {} -> error "GetGroupInternal: implement on demand (mockBrigAPIAccess)" + GetGroupsInternal {} -> error "GetGroupsInternal: implement on demand (mockBrigAPIAccess)" + UpdateGroup {} -> error "UpdateGroup: implement on demand (mockBrigAPIAccess)" + DeleteGroupInternal {} -> error "DeleteGroupInternal: implement on demand (mockBrigAPIAccess)" + DeleteApp {} -> error "DeleteApp: implement on demand (mockBrigAPIAccess)" + GetAppIdsForTeam {} -> error "GetAppIdsForTeam: implement on demand (mockBrigAPIAccess)" + SetAccountStatus {} -> error "SetAccountStatus: implement on demand (mockBrigAPIAccess)" + CreateSAML {} -> error "CreateSAML: implement on demand (mockBrigAPIAccess)" + CreateNoSAML {} -> error "CreateNoSAML: implement on demand (mockBrigAPIAccess)" + UpdateEmail {} -> error "UpdateEmail: implement on demand (mockBrigAPIAccess)" + GetAccount {} -> error "GetAccount: implement on demand (mockBrigAPIAccess)" + GetAccountByHandle {} -> error "GetAccountByHandle: implement on demand (mockBrigAPIAccess)" + GetByEmail {} -> error "GetByEmail: implement on demand (mockBrigAPIAccess)" + SetName {} -> error "SetName: implement on demand (mockBrigAPIAccess)" + SetHandle {} -> error "SetHandle: implement on demand (mockBrigAPIAccess)" + SetManagedBy {} -> error "SetManagedBy: implement on demand (mockBrigAPIAccess)" + DeletePendingEmailUpdate {} -> error "DeletePendingEmailUpdate: implement on demand (mockBrigAPIAccess)" + SetSSOId {} -> error "SetSSOId: implement on demand (mockBrigAPIAccess)" + SetRichInfo {} -> error "SetRichInfo: implement on demand (mockBrigAPIAccess)" + SetLocale {} -> error "SetLocale: implement on demand (mockBrigAPIAccess)" + GetRichInfo {} -> error "GetRichInfo: implement on demand (mockBrigAPIAccess)" + CheckHandleAvailable {} -> error "CheckHandleAvailable: implement on demand (mockBrigAPIAccess)" + SsoLogin {} -> error "SsoLogin: implement on demand (mockBrigAPIAccess)" + GetStatus {} -> error "GetStatus: implement on demand (mockBrigAPIAccess)" + GetStatusMaybe {} -> error "GetStatusMaybe: implement on demand (mockBrigAPIAccess)" + SetStatus {} -> error "SetStatus: implement on demand (mockBrigAPIAccess)" + GetDefaultUserLocale {} -> error "GetDefaultUserLocale: implement on demand (mockBrigAPIAccess)" + CheckAdminGetTeamId {} -> error "CheckAdminGetTeamId: implement on demand (mockBrigAPIAccess)" + SendSAMLIdPChangedEmail {} -> error "SendSAMLIdPChangedEmail: implement on demand (mockBrigAPIAccess)" diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs index 4def51eeef7..63a334527ab 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs @@ -41,6 +41,8 @@ inMemoryTeamCollaboratorsStoreInterpreter = gets $ \(s :: Map TeamId [TeamCollaborator]) -> find (\tc -> tc.gUser == userId) =<< Map.lookup teamId s GetTeamCollaborations userId -> gets $ \(s :: Map TeamId [TeamCollaborator]) -> concatMap (filter (\tc -> tc.gUser == userId)) (Map.elems s) + GetTeamCollaborationsForUsers userIds -> + gets $ \(s :: Map TeamId [TeamCollaborator]) -> concatMap (filter (\tc -> tc.gUser `elem` userIds)) (Map.elems s) GetTeamCollaboratorsWithIds teamIds userIds -> gets $ \(s :: Map TeamId [TeamCollaborator]) -> concatMap (concatMap (filter (\tc -> tc.gUser `elem` userIds)) . (\(tid :: TeamId) -> Map.lookup tid s)) teamIds diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs index 786f733240f..043f3a834db 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs @@ -87,7 +87,7 @@ inMemoryUserSubsystemInterpreter = BlockListInsert _ -> error "BlockListInsert: implement on demand (userSubsystemInterpreter)" UpdateTeamSearchVisibilityInbound _ -> error "UpdateTeamSearchVisibilityInbound: implement on demand (userSubsystemInterpreter)" AcceptTeamInvitation {} -> error "AcceptTeamInvitation: implement on demand (userSubsystemInterpreter)" - InternalUpdateSearchIndex _ -> error "InternalUpdateSearchIndex: implement on demand (userSubsystemInterpreter)" + InternalUpdateSearchIndex {} -> error "InternalUpdateSearchIndex: implement on demand (userSubsystemInterpreter)" InternalFindTeamInvitation {} -> error "InternalFindTeamInvitation: implement on demand (userSubsystemInterpreter)" GetUserExportData _ -> error "GetUserExportData: implement on demand (userSubsystemInterpreter)" RemoveEmailEither _ -> error "RemoveEmailEither: implement on demand (userSubsystemInterpreter)" diff --git a/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs index 6861f097797..f623e0012dd 100644 --- a/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs @@ -42,7 +42,7 @@ import Wire.API.User as User import Wire.API.User.Scim import Wire.API.UserGroup import Wire.BrigAPIAccess (BrigAPIAccess (..)) -import Wire.MockInterpreters +import Wire.MockInterpreters hiding (mockBrigAPIAccess) import Wire.ScimSubsystem import Wire.ScimSubsystem.Interpreter import Wire.StoredUser diff --git a/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs index a09d56bd8ff..ea721887393 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs @@ -50,6 +50,7 @@ userDoc1 = UserDoc { udId = fromJust . hush . parseIdFromText $ "0a96b396-57d6-11ea-a04b-7b93d1a5c19c", udTeam = hush . parseIdFromText $ "17c59b18-57d6-11ea-9220-8bbf5eee961a", + udCollaboratingTeams = either (error . show) (: []) . parseIdFromText $ "17c59b18-57d6-11ea-9220-8bbf5eee961a", udName = Just . Name $ "Carl Phoomp", udNormalized = Just $ "carl phoomp", udHandle = Just . fromJust . parseHandle $ "phoompy", @@ -68,6 +69,5 @@ userDoc1 = udType = Nothing } --- Dont touch this. This represents serialized legacy data. userDoc1ByteString :: LByteString -userDoc1ByteString = "{\"email\":\"phoompy@example.com\",\"account_status\":\"active\",\"handle\":\"phoompy\",\"managed_by\":\"scim\",\"role\":\"admin\",\"accent_id\":32,\"name\":\"Carl Phoomp\",\"created_at\":\"2020-08-29T21:50:00.000Z\",\"team\":\"17c59b18-57d6-11ea-9220-8bbf5eee961a\",\"id\":\"0a96b396-57d6-11ea-a04b-7b93d1a5c19c\",\"normalized\":\"carl phoomp\",\"saml_idp\":\"https://issuer.net/214234\"}" +userDoc1ByteString = "{\"collaborating_teams\":[\"17c59b18-57d6-11ea-9220-8bbf5eee961a\"],\"email\":\"phoompy@example.com\",\"account_status\":\"active\",\"handle\":\"phoompy\",\"managed_by\":\"scim\",\"role\":\"admin\",\"accent_id\":32,\"name\":\"Carl Phoomp\",\"created_at\":\"2020-08-29T21:50:00.000Z\",\"team\":\"17c59b18-57d6-11ea-9220-8bbf5eee961a\",\"id\":\"0a96b396-57d6-11ea-a04b-7b93d1a5c19c\",\"normalized\":\"carl phoomp\",\"saml_idp\":\"https://issuer.net/214234\"}" diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index 68942a77cb3..9e309d50dda 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -1110,7 +1110,7 @@ spec = describe "UserSubsystem.Interpreter" do searchee = searcheeNoHandle {handle = Just searcheeHandle} :: StoredUser storedUserToDoc :: StoredUser -> UserDoc - storedUserToDoc user = indexUserToDoc defaultSearchVisibilityInbound Nothing (storedUserToIndexUser user) + storedUserToDoc user = indexUserToDoc defaultSearchVisibilityInbound Nothing [] (storedUserToIndexUser user) indexFromStoredUsers :: [StoredUser] -> UserIndex indexFromStoredUsers storedUsers = do diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index f7627036f1c..42196d6e18c 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -244,6 +244,7 @@ library Wire.BoundedQueue Wire.BoundedQueue.STM Wire.BrigAPIAccess + Wire.BrigAPIAccess.Local Wire.BrigAPIAccess.Rpc Wire.BudgetStore Wire.BudgetStore.Cassandra @@ -650,6 +651,7 @@ test-suite wire-subsystems-tests Wire.MockInterpreters.AuthenticationSubsystem Wire.MockInterpreters.BackgroundJobPublisher Wire.MockInterpreters.BlockListStore + Wire.MockInterpreters.BrigAPIAccess Wire.MockInterpreters.ClientStore Wire.MockInterpreters.ConversationStore Wire.MockInterpreters.ConversationSubsystem diff --git a/services/background-worker/src/Wire/Effects.hs b/services/background-worker/src/Wire/Effects.hs index 0d367f19eed..66dbee09c56 100644 --- a/services/background-worker/src/Wire/Effects.hs +++ b/services/background-worker/src/Wire/Effects.hs @@ -367,7 +367,7 @@ runBackgroundWorkerEffects env extEnv requestId mJobId = ) . runFeaturesConfigSubsystem . runInputSem getAllTeamFeaturesForServer - . interpretTeamCollaboratorsSubsystem + . interpretTeamCollaboratorsSubsystem (interpretBrigAccess env.brigEndpoint) . discardMeetingNotifier . interpretConversationSubsystem where diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 6c2145ea8cd..27812d06b47 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -36,6 +36,7 @@ module Brig.App cargoholdLens, galleyLens, galleyEndpointLens, + brigEndpointLens, sparEndpointLens, gundeckEndpointLens, cargoholdEndpointLens, @@ -189,6 +190,10 @@ data Env = Env { cargohold :: RPC.Request, galley :: RPC.Request, galleyEndpoint :: Endpoint, + -- | Brig's own listen address. Used only to call ourselves over HTTP for + -- 'BrigAPIAccess' operations that have no local implementation yet; see + -- 'Wire.BrigAPIAccess.Local'. + brigEndpoint :: Endpoint, sparEndpoint :: Endpoint, gundeckEndpoint :: Endpoint, cargoholdEndpoint :: Endpoint, @@ -307,6 +312,7 @@ newEnv opts = do { cargohold = mkEndpoint $ opts.cargohold, galley = mkEndpoint $ opts.galley, galleyEndpoint = opts.galley, + brigEndpoint = opts.brig, sparEndpoint = opts.spar, gundeckEndpoint = opts.gundeck, cargoholdEndpoint = opts.cargohold, diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index f866fc5a9ca..4414567c910 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -47,6 +47,7 @@ import Polysemy.Input (Input, runInputConst) import Polysemy.Internal.Kind import Polysemy.Resource import Polysemy.TinyLog (TinyLog) +import Util.Options (Endpoint) import Wire.API.Error (ErrorS, errorToWai) import Wire.API.Error.Galley import Wire.API.Federation.Client qualified @@ -68,6 +69,8 @@ import Wire.BackgroundJobsPublisher (BackgroundJobPublisher) import Wire.BackgroundJobsPublisher.RabbitMQ (interpretBackgroundJobPublisherRabbitMQ) import Wire.BlockListStore import Wire.BlockListStore.Cassandra +import Wire.BrigAPIAccess (BrigAPIAccess) +import Wire.BrigAPIAccess.Local (interpretBrigAPIAccessLocally) import Wire.BudgetStore import Wire.BudgetStore.Cassandra import Wire.ClientStore (ClientStore) @@ -130,6 +133,7 @@ import Wire.PropertySubsystem.Interpreter import Wire.RateLimit import Wire.RateLimit.Interpreter import Wire.Rpc +import Wire.RpcException (RpcException) import Wire.SAMLEmailSubsystem import Wire.SAMLEmailSubsystem.Interpreter import Wire.SFT (SFT, interpretSFT) @@ -189,13 +193,12 @@ type RecursiveEffects = '[ AuthenticationSubsystem, UserSubsystem, AppSubsystem, - ClientSubsystem + ClientSubsystem, + BrigAPIAccess, + TeamCollaboratorsSubsystem ] -type NonRecursiveEffects2 = - '[ TeamCollaboratorsSubsystem - ] - `Append` BrigLowerLevelEffects +type NonRecursiveEffects2 = BrigLowerLevelEffects -- | These effects have interpreters which don't depend on each other type BrigLowerLevelEffects = @@ -276,6 +279,7 @@ type BrigLowerLevelEffects = Embed Cas.Client, Error ClientError, Error ParseException, + Error RpcException, Error ErrorCall, Error SomeException, Error HttpError, @@ -297,9 +301,11 @@ type BrigLowerLevelEffects = -- Cloned from "Wire.MiniBackend". runRecursiveEffects :: (Members NonRecursiveEffects2 r) => + -- | Brig's own endpoint; see 'interpretBrigAPIAccessLocally'. + Endpoint -> Sem (RecursiveEffects `Append` r) a -> Sem r a -runRecursiveEffects = runClient . runApp . runUser . runAuth +runRecursiveEffects selfEndpoint = runTeamCollaborators . runBrigAPIAccess . runClient . runApp . runUser . runAuth where runAuth :: forall r. (Members NonRecursiveEffects2 r) => InterpreterFor AuthenticationSubsystem r runAuth = interpretAuthenticationSubsystem runUser @@ -313,6 +319,12 @@ runRecursiveEffects = runClient . runApp . runUser . runAuth runClient :: forall r. (Members NonRecursiveEffects2 r) => InterpreterFor ClientSubsystem r runClient = runClientSubsystem runAuth runUser + runBrigAPIAccess :: forall r. (Members NonRecursiveEffects2 r) => InterpreterFor BrigAPIAccess r + runBrigAPIAccess = interpretBrigAPIAccessLocally selfEndpoint runUser + + runTeamCollaborators :: forall r. (Members NonRecursiveEffects2 r) => InterpreterFor TeamCollaboratorsSubsystem r + runTeamCollaborators = interpretTeamCollaboratorsSubsystem runBrigAPIAccess + runBrigToIO :: App.Env -> AppT BrigCanonicalEffects a -> IO a runBrigToIO e (AppT ma) = do let blockedDomains = @@ -433,6 +445,7 @@ runBrigToIO e (AppT ma) = do . rethrowHttpErrorIO . runError @SomeException . mapError @ErrorCall SomeException + . mapError @RpcException SomeException . mapError @ParseException SomeException . mapError clientErrorToHttpError . interpretClientToIO e.casClient @@ -510,8 +523,7 @@ runBrigToIO e (AppT ma) = do . interpretTeamCollaboratorsStoreToPostgres . interpretTeamSubsystemToGalleyAPI . samlEmailSubsystemInterpreter - . interpretTeamCollaboratorsSubsystem - . runRecursiveEffects + . runRecursiveEffects e.brigEndpoint . interpretUserGroupSubsystem . maybe runEnterpriseLoginSubsystemNoConfig diff --git a/services/brig/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs index ea72f9aeef5..695007e195e 100644 --- a/services/brig/src/Brig/Index/Eval.hs +++ b/services/brig/src/Brig/Index/Eval.hs @@ -51,6 +51,7 @@ import Polysemy.TinyLog (TinyLog) import System.Logger qualified as Log import System.Logger.Class (Logger) import Util.Options +import Wire.API.Team.Collaborator (TeamCollaboratorsError) import Wire.ClientSubsystem.Error (ClientError) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess.Rpc @@ -65,6 +66,8 @@ import Wire.Rpc import Wire.Sem.Logger.TinyLog import Wire.Sem.Metrics (Metrics) import Wire.Sem.Metrics.IO +import Wire.TeamCollaboratorsStore (TeamCollaboratorsStore) +import Wire.TeamCollaboratorsStore.Postgres (interpretTeamCollaboratorsStoreToPostgres) import Wire.UserKeyStore (UserKeyStore) import Wire.UserKeyStore.Cassandra import Wire.UserSearch.Migration (MigrationException) @@ -75,6 +78,7 @@ import Wire.UserStore.Postgres (interpretUserStorePostgres) type BrigIndexEffectStack = [ UserKeyStore, UserStore, + TeamCollaboratorsStore, IndexedUserStore, Error IndexedUserStoreError, IndexedUserMigrationStore, @@ -86,6 +90,7 @@ type BrigIndexEffectStack = TinyLog, Input Hasql.Pool, Error UsageError, + Error TeamCollaboratorsError, Error ClientError, Embed IO, Final IO @@ -132,6 +137,7 @@ runSem (mgr, casClient, pgPool, bhEnv, indexedUserStoreConfig, reqId, migrationI runFinal . embedToFinal . throwErrorToIOFinal @ClientError + . throwErrorToIOFinal @TeamCollaboratorsError . throwPostgresUsageErrorToIOFinal . runInputConst pgPool . loggerToTinyLogReqId reqId logger @@ -143,6 +149,7 @@ runSem (mgr, casClient, pgPool, bhEnv, indexedUserStoreConfig, reqId, migrationI . interpretIndexedUserMigrationStoreES bhEnv migrationIndexName . throwErrorToIOFinal @IndexedUserStoreError . interpretIndexedUserStoreES indexedUserStoreConfig + . interpretTeamCollaboratorsStoreToPostgres . userStoreInterpreter . interpretUserKeyStoreCassandra casClient $ action @@ -169,10 +176,14 @@ runCommand l = \case runIndexIO e $ resetIndex (mkCreateIndexSettings es) Reindex es cas pg userStorageLocation galley pageSize -> do semDeps <- mkSemDeps (es ^. esConnection) cas pg l - IndexedUserStoreBulk.syncAllUsers (runSem semDeps userStorageLocation galley l) pageSize + skipped <- IndexedUserStoreBulk.syncAllUsers (runSem semDeps userStorageLocation galley l) pageSize + when (skipped /= 0) do + throwM . IndexMigrationError $ "Reindex: failed to sync " <> show skipped <> " documents." ReindexSameOrNewer es cas pg userStorageLocation galley pageSize -> do semDeps <- mkSemDeps (es ^. esConnection) cas pg l - IndexedUserStoreBulk.forceSyncAllUsers (runSem semDeps userStorageLocation galley l) pageSize + skipped <- IndexedUserStoreBulk.forceSyncAllUsers (runSem semDeps userStorageLocation galley l) pageSize + when (skipped /= 0) do + throwM . IndexMigrationError $ "ReindexSameOrNewer: failed to sync " <> show skipped <> " documents." UpdateMapping esConn galley -> do e <- initIndex l esConn galley runIndexIO e updateMapping @@ -253,6 +264,11 @@ waitForTaskToComplete timeoutSeconds taskNodeId = do errTaskGet :: ES.EsError -> m x errTaskGet e = throwM $ ReindexFromAnotherIndexError $ "Error response while getting task: " <> show e +newtype IndexMigrationError = IndexMigrationError String + deriving (Show) + +instance Exception IndexMigrationError + newtype ReindexFromAnotherIndexError = ReindexFromAnotherIndexError String deriving (Show) diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index 4c4919729d9..68a17f07b1f 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -364,6 +364,15 @@ indexMapping = mpAnalyzer = Nothing, mpFields = mempty }, + -- teams this user collaborates with (without being a member of them) + "collaborating_teams" + .= MappingProperty + { mpType = MPKeyword, + mpStore = False, + mpIndex = True, + mpAnalyzer = Nothing, + mpFields = mempty + }, "accent_id" .= MappingProperty { mpType = MPByte, diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 4d755d8c612..f47dc7f3798 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -572,7 +572,7 @@ evalGalley e = . interpretTeamSubsystem teamSubsystemConfig . runFeaturesConfigSubsystem . runInputSem getAllTeamFeaturesForServer - . interpretTeamCollaboratorsSubsystem + . interpretTeamCollaboratorsSubsystem (interpretBrigAccess (e ^. brig)) . runFederationSubsystem conversationSubsystemConfig.federationProtocols . runInputConst (e ^. reqId) . interpretJobSubsystem From d63125f6ee21cc3ec3d1f4e58475548946c5ea9a Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Mon, 31 Aug 2026 09:29:30 +0200 Subject: [PATCH 02/29] Fix: active team invitation after SCIM user deletion (#5492) Deleted SCIM users could still register with their invitation code and be then part of the team - just with a new non-SCIM user. This does not reflect the intuition of deleting a user. Now, invitations are deleted when the user gets deleted. --- ...pending-invitations-for-deleted-SCIM-users | 2 ++ integration/test/Test/Spar.hs | 31 +++++++++++++++++++ libs/types-common/src/Data/Id.hs | 5 +++ services/brig/src/Brig/API/User.hs | 8 +++-- 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 changelog.d/3-bug-fixes/pending-invitations-for-deleted-SCIM-users diff --git a/changelog.d/3-bug-fixes/pending-invitations-for-deleted-SCIM-users b/changelog.d/3-bug-fixes/pending-invitations-for-deleted-SCIM-users new file mode 100644 index 00000000000..1f472f2c805 --- /dev/null +++ b/changelog.d/3-bug-fixes/pending-invitations-for-deleted-SCIM-users @@ -0,0 +1,2 @@ +Deleted SCIM users could still have pending team invitations. These are now +deleted (invalidated) with the SCIM user. diff --git a/integration/test/Test/Spar.hs b/integration/test/Test/Spar.hs index 7d45322967a..dbdaf7eb0a0 100644 --- a/integration/test/Test/Spar.hs +++ b/integration/test/Test/Spar.hs @@ -157,6 +157,37 @@ testTeamInvitationWhenScimAccountExists = do user %. "managed_by" `shouldMatch` "scim" user %. "status" `shouldMatch` "active" +-- | Ensure that unused team invitations are invalidated when the SCIM user +-- gets deleted. +testTeamInvitationUsedAfterScimUserDeleted :: (HasCallStack) => App () +testTeamInvitationUsedAfterScimUserDeleted = do + (owner, tid, _) <- createTeam OwnDomain 1 + token <- createScimToken owner def >>= getJSON 200 >>= (%. "token") >>= asString + + email <- randomEmail + externalId <- randomExternalId + scimUser <- randomScimUserWithEmail externalId email + scid <- createScimUser OwnDomain token scimUser >>= getJSON 201 >>= (%. "id") >>= asString + + code <- + getInvitationByEmail OwnDomain email + >>= getJSON 200 + >>= getInvitationCodeForTeam OwnDomain tid + >>= getJSON 200 + >>= (%. "code") + >>= asString + + deleteScimUser OwnDomain token scid >>= assertSuccess + + -- The invitation must no longer be usable: completing registration with + -- the previously-fetched code should fail, not resurrect the deleted + -- account. (This was bug WPB-28198) + registerUserWith OwnDomain email code "Eve" >>= assertStatus 400 + + getUsersByEmail OwnDomain [email] >>= getJSON 200 >>= asList >>= shouldBeEmpty + + getInvitationByEmail OwnDomain email >>= assertStatus 404 + testSparUserCreationInvitationTimeout :: (HasCallStack) => App () testSparUserCreationInvitationTimeout = do (owner, tid, _) <- createTeam OwnDomain 1 diff --git a/libs/types-common/src/Data/Id.hs b/libs/types-common/src/Data/Id.hs index d9d7156640e..7157f8f334c 100644 --- a/libs/types-common/src/Data/Id.hs +++ b/libs/types-common/src/Data/Id.hs @@ -42,6 +42,7 @@ module Data.Id idToText, idToString, invitationIdToUserId, + userIdToInvitationId, idObjectSchema, IdObject (..), @@ -107,6 +108,10 @@ import Test.QuickCheck.Instances () invitationIdToUserId :: InvitationId -> UserId invitationIdToUserId = Id . toUUID +-- | Pending invitation users reuse the invitation UUID as the user UUID. +userIdToInvitationId :: UserId -> InvitationId +userIdToInvitationId = Id . toUUID + data IdTag = Asset | Conversation diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 10c4a949c2b..d26a22d5fa6 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -1170,7 +1170,7 @@ ensureAccountDeleted luid@(tUnqualified -> uid) = do -- states. Please change this order only with care! -- -- FUTUREWORK(mangoiv): this uses 'UserStore', hence it must be moved to 'UserSubsystem' --- as an effet operation +-- as an effect operation -- FUTUREWORK: this does not need the whole User structure, only the User. deleteAccount :: ( Member (Embed HttpClientIO) r, @@ -1197,8 +1197,10 @@ deleteAccount user = do PropertySubsystem.onUserDeleted uid UserStore.deleteUser user - for_ (userEmail user) $ \email -> - for_ (userTeam user) $ \tid -> + for_ (userTeam user) $ \tid -> do + -- Delete potentially pending team invitations for SCIM users + InvitationStore.deleteInvitation tid (userIdToInvitationId uid) + for_ (userEmail user) $ \email -> InvitationStore.deletePendingScimUser tid email uid traverse_ (removeUserFromAllGroups uid) user.userTeam From 7cd837812ec1cbac303165b172c3f6a2bc8b9fb6 Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Mon, 31 Aug 2026 13:31:32 +0200 Subject: [PATCH 03/29] Fix: Fail on invalid Postgresql connection string combinations (#5494) This prevents us from running the app with unintended configurations and/or confusing assumptions about them. For `fromKeyValueParams` this implies being able to handle failures. For now, they are just propagated as there is little value in running a service with erroneous connection strings. --- ...postgresql-connection-string-parse-failure | 3 + flake.lock | 8 +-- flake.nix | 3 +- libs/extended/default.nix | 4 ++ libs/extended/extended.cabal | 4 ++ libs/extended/src/Hasql/Pool/Extended.hs | 26 ++++++- .../test/Test/Hasql/Pool/ExtendedSpec.hs | 70 +++++++++++++++++++ .../src/Wire/JobSubsystem/Migrations.hs | 5 +- 8 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 changelog.d/3-bug-fixes/postgresql-connection-string-parse-failure create mode 100644 libs/extended/test/Test/Hasql/Pool/ExtendedSpec.hs diff --git a/changelog.d/3-bug-fixes/postgresql-connection-string-parse-failure b/changelog.d/3-bug-fixes/postgresql-connection-string-parse-failure new file mode 100644 index 00000000000..c4f4e02752f --- /dev/null +++ b/changelog.d/3-bug-fixes/postgresql-connection-string-parse-failure @@ -0,0 +1,3 @@ +Postgresql connection strings with mismatched host/port counts in service +configurations now lead to immediate failure with a clear error message instead +of silently producing an erroneous connection. diff --git a/flake.lock b/flake.lock index bd8e74f6d3a..67d13b227d8 100644 --- a/flake.lock +++ b/flake.lock @@ -350,16 +350,16 @@ "postgresql-connection-string": { "flake": false, "locked": { - "lastModified": 1778144330, - "narHash": "sha256-w/TFQX7PIsLQTG+Es2yla584Y3T7Q6H1iIlqFap8qUk=", + "lastModified": 1787930090, + "narHash": "sha256-ti+XZdhoCiDxgyUilva7EpV8SOfMlmwuKr28XkJ62N0=", "owner": "wireapp", "repo": "postgresql-connection-string", - "rev": "ff98790cb3058545ea978ea143cd57b04360c48c", + "rev": "bd5a2d72c15fa4ffe561e3b3f441e5809050f918", "type": "github" }, "original": { "owner": "wireapp", - "ref": "expose-from-key-value-params", + "ref": "wire-patches", "repo": "postgresql-connection-string", "type": "github" } diff --git a/flake.nix b/flake.nix index 09cc084bba4..46255be3e45 100644 --- a/flake.nix +++ b/flake.nix @@ -87,7 +87,8 @@ }; postgresql-connection-string = { - url = "github:wireapp/postgresql-connection-string?ref=expose-from-key-value-params"; + url = + "github:wireapp/postgresql-connection-string?ref=wire-patches"; flake = false; }; diff --git a/libs/extended/default.nix b/libs/extended/default.nix index 1813326fc12..8880b875bb5 100644 --- a/libs/extended/default.nix +++ b/libs/extended/default.nix @@ -27,6 +27,7 @@ , http-types , imports , lib +, megaparsec , metrics-wai , monad-control , postgresql-connection-string @@ -76,6 +77,7 @@ mkDerivation { http-client-tls http-types imports + megaparsec metrics-wai monad-control postgresql-connection-string @@ -101,12 +103,14 @@ mkDerivation { aeson base bytestring + containers crypton crypton-asn1-types crypton-pem crypton-x509 hspec imports + postgresql-connection-string QuickCheck string-conversions temporary diff --git a/libs/extended/extended.cabal b/libs/extended/extended.cabal index eee2d884228..6b64767dfd5 100644 --- a/libs/extended/extended.cabal +++ b/libs/extended/extended.cabal @@ -108,6 +108,7 @@ library , http-client-tls , http-types , imports + , megaparsec , metrics-wai , monad-control , postgresql-connection-string @@ -138,6 +139,7 @@ test-suite extended-tests Paths_extended Test.Data.Hourglass.ConstSpec Test.Data.X509.ExtendedSpec + Test.Hasql.Pool.ExtendedSpec Test.System.Logger.ExtendedSpec hs-source-dirs: test @@ -196,6 +198,7 @@ test-suite extended-tests aeson , base , bytestring + , containers , crypton , crypton-asn1-types , crypton-pem @@ -203,6 +206,7 @@ test-suite extended-tests , extended , hspec , imports + , postgresql-connection-string , QuickCheck , string-conversions , temporary diff --git a/libs/extended/src/Hasql/Pool/Extended.hs b/libs/extended/src/Hasql/Pool/Extended.hs index cebbdca0442..515c70ce09e 100644 --- a/libs/extended/src/Hasql/Pool/Extended.hs +++ b/libs/extended/src/Hasql/Pool/Extended.hs @@ -18,6 +18,7 @@ module Hasql.Pool.Extended where import Data.Aeson +import Data.List.NonEmpty qualified as NonEmpty import Data.Misc import Hasql.Connection qualified import Hasql.Connection.Settings qualified as HasqlConnSettings @@ -25,6 +26,7 @@ import Hasql.Pool qualified as HasqlPool import Imports import PostgresqlConnectionString qualified import Prometheus +import Text.Megaparsec qualified as Megaparsec import UnliftIO.IO (getMonotonicTime) import Util.Options @@ -108,6 +110,27 @@ startHasqlPoolStatsReporter pool = void $ forkIO $ forever $ do recordHasqlPoolStats pool threadDelay (5 * 1_000_000) -- 5s +-- | Run a postgresql-connection-string parser, turning a parse failure into +-- an IO exception whose message is the parser's failure text, so the +-- reason is visible wherever this call site's crash output is captured. +-- +-- 'Megaparsec.errorBundlePretty' is avoided because 'fromKeyValueParams' +-- never consumes the (always-empty) input stream, so its source-position +-- pointer would be meaningless noise around the actual message. +runConnStrParser :: Megaparsec.Parsec Void Text a -> IO a +runConnStrParser p = + either + ( fail + . dropWhileEnd isSpace + . Megaparsec.parseErrorTextPretty + . NonEmpty.head + . Megaparsec.bundleErrors + ) + pure + $ let file = "" + input = "" + in Megaparsec.parse p file input + -- | Creates a pool from postgres config params. -- -- 'acquisitionTimeout' is mapped to the pool acquisition timeout, @@ -115,8 +138,9 @@ startHasqlPoolStatsReporter pool = void $ forkIO $ forever $ do initPostgresPool :: PoolConfig -> Map Text Text -> Maybe FilePathSecrets -> IO Pool initPostgresPool config pgConfig mFpSecrets = do mPw <- for mFpSecrets initCredentials + connStr <- runConnStrParser $ PostgresqlConnectionString.fromKeyValueParams pgConfig let pgSettings = - HasqlConnSettings.connectionString (PostgresqlConnectionString.toUrl $ PostgresqlConnectionString.fromKeyValueParams pgConfig) + HasqlConnSettings.connectionString (PostgresqlConnectionString.toUrl connStr) <> foldMap HasqlConnSettings.password mPw metrics <- mkHasqlPoolMetrics rawPool <- diff --git a/libs/extended/test/Test/Hasql/Pool/ExtendedSpec.hs b/libs/extended/test/Test/Hasql/Pool/ExtendedSpec.hs new file mode 100644 index 00000000000..048ef8c323c --- /dev/null +++ b/libs/extended/test/Test/Hasql/Pool/ExtendedSpec.hs @@ -0,0 +1,70 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2026 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Hasql.Pool.ExtendedSpec where + +import Control.Exception (try) +import Data.Map qualified as Map +import Hasql.Pool.Extended (runConnStrParser) +import Imports +import PostgresqlConnectionString qualified +import System.IO.Error (ioeGetErrorString) +import Test.Hspec + +spec :: Spec +spec = + describe "runConnStrParser / fromKeyValueParams" $ do + it "parses a valid key/value connection string" $ do + let params = + Map.fromList + [ ("host", "localhost"), + ("port", "5432"), + ("dbname", "wire-server"), + ("user", "wire") + ] + connStr <- runConnStrParser $ PostgresqlConnectionString.fromKeyValueParams params + PostgresqlConnectionString.toUrl connStr `shouldBe` "postgresql://wire@localhost:5432/wire-server" + + it "applies a single port to every host" $ do + let params = + Map.fromList + [ ("host", "IP1,IP2,IP3"), + ("port", "5000") + ] + connStr <- runConnStrParser $ PostgresqlConnectionString.fromKeyValueParams params + PostgresqlConnectionString.toUrl connStr `shouldBe` "postgresql://IP1:5000,IP2:5000,IP3:5000" + + it "applies a single port to every host and keeps the dbname" $ do + let params = + Map.fromList + [ ("host", "IP1,IP2,IP3"), + ("port", "5000"), + ("dbname", "wire-server") + ] + connStr <- runConnStrParser $ PostgresqlConnectionString.fromKeyValueParams params + PostgresqlConnectionString.toUrl connStr `shouldBe` "postgresql://IP1:5000,IP2:5000,IP3:5000/wire-server" + + it "surfaces a mismatched host/port count as an exception with the parse error" $ do + let params = + Map.fromList + [ ("host", "host1,host2,host3"), + ("port", "5432,5433") + ] + result <- try @IOException $ runConnStrParser (PostgresqlConnectionString.fromKeyValueParams params) + case result of + Left e -> ioeGetErrorString e `shouldBe` "could not match 2 port numbers to 3 hosts" + Right _ -> expectationFailure "expected a parse failure" diff --git a/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs b/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs index c39ac05fc71..8d8fe1242ea 100644 --- a/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs +++ b/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs @@ -36,6 +36,7 @@ import Hasql.Connection qualified as HasqlConnection import Hasql.Connection.Settings qualified as HasqlConnectionSettings import Hasql.Decoders qualified as HasqlDecoders import Hasql.Encoders qualified as HasqlEncoders +import Hasql.Pool.Extended (runConnStrParser) import Hasql.Session qualified as HasqlSession import Hasql.Statement qualified as HasqlStatement import Hasql.TH @@ -52,8 +53,8 @@ mkArbiterConnectionString :: Map Text Text -> Maybe FilePathSecrets -> IO Secret mkArbiterConnectionString pgConfig mFpSecrets = do mPw <- for mFpSecrets initCredentials let pgConfig' = maybe pgConfig (\pw -> Map.insert "password" pw pgConfig) mPw - pure . secretText . PostgresqlConnectionString.toKeyValueString $ - PostgresqlConnectionString.fromKeyValueParams pgConfig' + connStr <- runConnStrParser $ PostgresqlConnectionString.fromKeyValueParams pgConfig' + pure . secretText $ PostgresqlConnectionString.toKeyValueString connStr -- | Apply all migrations for the job registry before constructing any worker -- pools or accepting jobs. From 4cc294811e964da7c8108074b2c88351973146fa Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 31 Aug 2026 16:00:00 +0200 Subject: [PATCH 04/29] WPB-28237 unlock prevent adminless groups feature (#5496) --- changelog.d/0-release-notes/WPB-28237 | 1 + charts/wire-server/values.yaml | 3 +-- hack/helm_vars/wire-server/values.yaml.gotmpl | 11 ----------- 3 files changed, 2 insertions(+), 13 deletions(-) create mode 100644 changelog.d/0-release-notes/WPB-28237 diff --git a/changelog.d/0-release-notes/WPB-28237 b/changelog.d/0-release-notes/WPB-28237 new file mode 100644 index 00000000000..c30460e5cdd --- /dev/null +++ b/changelog.d/0-release-notes/WPB-28237 @@ -0,0 +1 @@ +`preventAdminlessGroups` is unlocked by default diff --git a/charts/wire-server/values.yaml b/charts/wire-server/values.yaml index 5bb0b276eb6..8bea454951f 100644 --- a/charts/wire-server/values.yaml +++ b/charts/wire-server/values.yaml @@ -260,10 +260,9 @@ galley: allowed_to_open_channels: team-members lockStatus: locked preventAdminlessGroups: - # This feature has known errors. Thus, it must stay disabled for now. defaults: status: disabled - lockStatus: locked + lockStatus: unlocked config: promotionStrategy: alphabetical deletionTimeoutDuration: 7d diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index b0309380b56..556e8e1a225 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -420,17 +420,6 @@ galley: defaults: status: enabled lockStatus: unlocked - preventAdminlessGroups: - # FUTUREWORK: This feature should be unlocked in the main - # `values.yaml` file. It is just disabled & locked there due to open - # bugs. - defaults: - status: disabled - lockStatus: unlocked - config: - promotionStrategy: alphabetical - deletionTimeoutDuration: 7d - reminderTimeoutDurations: [2d, 4d, 6d] journal: endpoint: http://fake-aws-sqs:4568 queueName: integration-team-events.fifo From 57bf3e185d93444d3fca79debd14f6367ff2b550 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Tue, 1 Sep 2026 15:12:15 +0200 Subject: [PATCH 05/29] [WPB-28448] Bump headroom to unreleased 0.5.0.0. (#5503) --- ...B-28448-bump-headroom-to-unreleased-0_5_0_0 | 1 + flake.lock | 18 ++++++++++++++++++ flake.nix | 7 +++++++ nix/haskell-pins.nix | 6 +++++- 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 changelog.d/5-internal/WPB-28448-bump-headroom-to-unreleased-0_5_0_0 diff --git a/changelog.d/5-internal/WPB-28448-bump-headroom-to-unreleased-0_5_0_0 b/changelog.d/5-internal/WPB-28448-bump-headroom-to-unreleased-0_5_0_0 new file mode 100644 index 00000000000..f9b030ba5ad --- /dev/null +++ b/changelog.d/5-internal/WPB-28448-bump-headroom-to-unreleased-0_5_0_0 @@ -0,0 +1 @@ +Bump headroom to unreleased 0.5.0.0. diff --git a/flake.lock b/flake.lock index 67d13b227d8..38e18bceec0 100644 --- a/flake.lock +++ b/flake.lock @@ -249,6 +249,23 @@ "type": "github" } }, + "headroom": { + "flake": false, + "locked": { + "lastModified": 1788175700, + "narHash": "sha256-7Pj8dP56vbFzx6SnjFpA2vRY/hDCNvx6yIyc2A2B7NU=", + "owner": "xwinus", + "repo": "headroom", + "rev": "010ed0ca68f0af9026bb613800cbeae80be6ebbd", + "type": "github" + }, + "original": { + "owner": "xwinus", + "repo": "headroom", + "rev": "010ed0ca68f0af9026bb613800cbeae80be6ebbd", + "type": "github" + } + }, "hsaml2": { "flake": false, "locked": { @@ -392,6 +409,7 @@ "flake-utils": "flake-utils", "hasql-migration": "hasql-migration", "hasql-resource-pool": "hasql-resource-pool", + "headroom": "headroom", "hsaml2": "hsaml2", "hspec-wai": "hspec-wai", "http-client": "http-client", diff --git a/flake.nix b/flake.nix index 46255be3e45..7e5b5c6ab99 100644 --- a/flake.nix +++ b/flake.nix @@ -114,6 +114,13 @@ url = "github:velveteer/arbiter?rev=b9c57eb1f8277d97616aa449bea471fe9ce14eda"; flake = false; }; + + headroom = { + # https://github.com/xwinus/headroom/issues/117 + # this pin should be removed once 0.5.0.0 has been released. + url = "github:xwinus/headroom?rev=010ed0ca68f0af9026bb613800cbeae80be6ebbd"; + flake = false; + }; }; outputs = inputs@{ nixpkgs, nixpkgs_24_11, flake-utils, tom-bombadil, sbomnix, ... }: diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 13ebf988279..05531592ddf 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -71,6 +71,10 @@ let }; }; + headroom = { + src = inputs.headroom; + }; + bloodhound = { src = inputs.bloodhound; }; @@ -149,7 +153,7 @@ let # N.B. only the listed packages work. If you want to use another: # - list it here # - patch it on the fork (if required) - # + # # Can't currently be removed because amazonka-dynamodb-attributevalue # does not exist on hackage amazonka = { From faaec6322bdc927a9982318c4c7ecc763bb3f4b6 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 1 Sep 2026 17:20:32 +0200 Subject: [PATCH 06/29] treefmt: Remove headroom (#5506) * Revert "[WPB-28448] Bump headroom to unreleased 0.5.0.0. (#5503)" This reverts commit 57bf3e185d93444d3fca79debd14f6367ff2b550. * treefmt: Remove headroom It fails intermittently, we should enable it on next release --- ...B-28448-bump-headroom-to-unreleased-0_5_0_0 | 1 - flake.lock | 18 ------------------ flake.nix | 7 ------- nix/haskell-pins.nix | 6 +----- treefmt.toml | 7 ------- 5 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 changelog.d/5-internal/WPB-28448-bump-headroom-to-unreleased-0_5_0_0 diff --git a/changelog.d/5-internal/WPB-28448-bump-headroom-to-unreleased-0_5_0_0 b/changelog.d/5-internal/WPB-28448-bump-headroom-to-unreleased-0_5_0_0 deleted file mode 100644 index f9b030ba5ad..00000000000 --- a/changelog.d/5-internal/WPB-28448-bump-headroom-to-unreleased-0_5_0_0 +++ /dev/null @@ -1 +0,0 @@ -Bump headroom to unreleased 0.5.0.0. diff --git a/flake.lock b/flake.lock index 38e18bceec0..67d13b227d8 100644 --- a/flake.lock +++ b/flake.lock @@ -249,23 +249,6 @@ "type": "github" } }, - "headroom": { - "flake": false, - "locked": { - "lastModified": 1788175700, - "narHash": "sha256-7Pj8dP56vbFzx6SnjFpA2vRY/hDCNvx6yIyc2A2B7NU=", - "owner": "xwinus", - "repo": "headroom", - "rev": "010ed0ca68f0af9026bb613800cbeae80be6ebbd", - "type": "github" - }, - "original": { - "owner": "xwinus", - "repo": "headroom", - "rev": "010ed0ca68f0af9026bb613800cbeae80be6ebbd", - "type": "github" - } - }, "hsaml2": { "flake": false, "locked": { @@ -409,7 +392,6 @@ "flake-utils": "flake-utils", "hasql-migration": "hasql-migration", "hasql-resource-pool": "hasql-resource-pool", - "headroom": "headroom", "hsaml2": "hsaml2", "hspec-wai": "hspec-wai", "http-client": "http-client", diff --git a/flake.nix b/flake.nix index 7e5b5c6ab99..46255be3e45 100644 --- a/flake.nix +++ b/flake.nix @@ -114,13 +114,6 @@ url = "github:velveteer/arbiter?rev=b9c57eb1f8277d97616aa449bea471fe9ce14eda"; flake = false; }; - - headroom = { - # https://github.com/xwinus/headroom/issues/117 - # this pin should be removed once 0.5.0.0 has been released. - url = "github:xwinus/headroom?rev=010ed0ca68f0af9026bb613800cbeae80be6ebbd"; - flake = false; - }; }; outputs = inputs@{ nixpkgs, nixpkgs_24_11, flake-utils, tom-bombadil, sbomnix, ... }: diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 05531592ddf..13ebf988279 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -71,10 +71,6 @@ let }; }; - headroom = { - src = inputs.headroom; - }; - bloodhound = { src = inputs.bloodhound; }; @@ -153,7 +149,7 @@ let # N.B. only the listed packages work. If you want to use another: # - list it here # - patch it on the fork (if required) - # + # # Can't currently be removed because amazonka-dynamodb-attributevalue # does not exist on hackage amazonka = { diff --git a/treefmt.toml b/treefmt.toml index 516cbd86fbb..cc49a82bf60 100644 --- a/treefmt.toml +++ b/treefmt.toml @@ -10,13 +10,6 @@ excludes = [ "dist-newstyle/" ] -[formatter.headroom] -command = "hack/bin/headroom-treefmt.sh" -includes = ["*.hs", "*.hsc", "*.rs"] -excludes = [ - "dist-newstyle/", - "services/wire-server-enterprise/*", -] [formatter.shellcheck] command = "shellcheck" From 6fb1308d30f62b49d9dcf3018756bf007dcba406 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 1 Sep 2026 18:20:16 +0200 Subject: [PATCH 07/29] UserStore: Add migration logic and migration interpreter (#5324) * brig: Reduce gc_grace_seconds on user tables * PostgresMarshall/Unmarshall instances for tuples of length 25 * UserStore.Migration: Implement migration * integration: Add test for user migration to pg * background-worker: Run user migration * integration: Wait max 30s for migrations * changelog and docs * integration: Add test case for inconsistent handle claims * UserStore: Deal with handle update during migration * integration: Test edge cases of migrating invalid users * integration: Test migration of user when handle is claimed in Cassandra and PG by different users * pg-migration: Use AtomicState to keep track of errors When using `State` sometimes the error count is not correct. * UserStore.Migration: Gracefully deal with duplicate handle claims These can happen due to race in the migration interpreter. The user who gets it in postgresql first, wins. * Wire.MigrationLock: Sort lock ids to avoid deadlocks --------- Co-authored-by: Gautier DI FOLCO --- cassandra-schema.cql | 10 +- changelog.d/2-features/user-pg-migration | 1 + .../background-worker/configmap.yaml | 1 + charts/wire-server/values.yaml | 4 + deploy/dockerephemeral/docker-compose.yaml | 2 +- .../src/developer/reference/config-options.md | 20 +- integration/default.nix | 2 + integration/integration.cabal | 2 + integration/test/API/Brig.hs | 16 +- integration/test/API/Common.hs | 39 +- integration/test/SetupHelpers.hs | 21 +- .../test/Test/Migration/Conversation.hs | 1 + integration/test/Test/Migration/User.hs | 956 ++++++++++++++++++ integration/test/Test/Migration/Util.hs | 34 +- integration/test/Test/Search.hs | 2 +- integration/test/Testlib/Env.hs | 8 +- integration/test/Testlib/JSON.hs | 6 + integration/test/Testlib/Types.hs | 19 +- .../wire-api/src/Wire/API/PostgresMarshall.hs | 32 + ...559-add-user-migration-pending-deletes.sql | 3 + .../src/Wire/CodeStore/Migration.hs | 8 +- .../src/Wire/ConversationStore/Migration.hs | 16 +- .../Wire/DomainRegistrationStore/Migration.hs | 32 +- libs/wire-subsystems/src/Wire/Migration.hs | 27 +- .../wire-subsystems/src/Wire/MigrationLock.hs | 5 +- .../src/Wire/TeamFeatureStore/Migration.hs | 14 +- .../src/Wire/UserStore/Cassandra.hs | 210 +++- .../src/Wire/UserStore/Migration.hs | 379 +++++++ .../src/Wire/UserStore/Migration/Types.hs | 111 ++ .../src/Wire/UserStore/Postgres.hs | 8 +- libs/wire-subsystems/wire-subsystems.cabal | 2 + postgres-schema.sql | 19 + .../background-worker.integration.yaml | 1 + .../src/Wire/BackgroundWorker.hs | 11 +- .../src/Wire/BackgroundWorker/Options.hs | 1 + .../src/Wire/PostgresMigrations.hs | 25 +- services/brig/brig.cabal | 1 + .../brig/src/Brig/CanonicalInterpreter.hs | 2 +- services/brig/src/Brig/Index/Eval.hs | 14 +- services/brig/src/Brig/Run.hs | 2 +- services/brig/src/Brig/Schema/Run.hs | 4 +- .../Schema/V94_ReduceUserGCGracePeriod.hs | 44 + 42 files changed, 2012 insertions(+), 103 deletions(-) create mode 100644 changelog.d/2-features/user-pg-migration create mode 100644 integration/test/Test/Migration/User.hs create mode 100644 libs/wire-subsystems/postgres-migrations/20260625104559-add-user-migration-pending-deletes.sql create mode 100644 libs/wire-subsystems/src/Wire/UserStore/Migration.hs create mode 100644 libs/wire-subsystems/src/Wire/UserStore/Migration/Types.hs create mode 100644 services/brig/src/Brig/Schema/V94_ReduceUserGCGracePeriod.hs diff --git a/cassandra-schema.cql b/cassandra-schema.cql index 9e5e1528992..160a310f39b 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -738,7 +738,7 @@ CREATE TABLE brig_test.rich_info ( AND crc_check_chance = 1.0 AND default_time_to_live = 0 AND extensions = {} - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -848,7 +848,7 @@ CREATE TABLE brig_test.service_team ( AND crc_check_chance = 1.0 AND default_time_to_live = 0 AND extensions = {} - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -874,7 +874,7 @@ CREATE TABLE brig_test.service_user ( AND crc_check_chance = 1.0 AND default_time_to_live = 0 AND extensions = {} - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -1088,7 +1088,7 @@ CREATE TABLE brig_test.user ( AND crc_check_chance = 1.0 AND default_time_to_live = 0 AND extensions = {} - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -1137,7 +1137,7 @@ CREATE TABLE brig_test.user_handle ( AND crc_check_chance = 1.0 AND default_time_to_live = 0 AND extensions = {} - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 diff --git a/changelog.d/2-features/user-pg-migration b/changelog.d/2-features/user-pg-migration new file mode 100644 index 00000000000..2abecf732fc --- /dev/null +++ b/changelog.d/2-features/user-pg-migration @@ -0,0 +1 @@ +Support migrating user data to postgresql from cassandra \ No newline at end of file diff --git a/charts/wire-server/templates/background-worker/configmap.yaml b/charts/wire-server/templates/background-worker/configmap.yaml index d4fe2a63202..299d0703d3a 100644 --- a/charts/wire-server/templates/background-worker/configmap.yaml +++ b/charts/wire-server/templates/background-worker/configmap.yaml @@ -84,6 +84,7 @@ data: migrateConversationCodes: {{ .migrateConversationCodes }} migrateTeamFeatures: {{ .migrateTeamFeatures }} migrateDomainRegistration: {{ .migrateDomainRegistration }} + migrateUsers: {{ .migrateUsers }} migrationOptions: {{ toYaml .migrationOptions | indent 6 }} diff --git a/charts/wire-server/values.yaml b/charts/wire-server/values.yaml index 8bea454951f..d3f3f7f4501 100644 --- a/charts/wire-server/values.yaml +++ b/charts/wire-server/values.yaml @@ -1025,6 +1025,10 @@ background-worker: # It's important to set `settings.postgresMigration.domainRegistration` to `migration-to-postgresql` # before starting the migration. migrateDomainRegistration: false + # This will start the migration of users + # It's important to set `settings.postgresMigration.users` to `migration-to-postgresql` + # before starting the migration. + migrateUsers: false backendNotificationPusher: pushBackoffMinWait: 10000 # in microseconds, so 10ms diff --git a/deploy/dockerephemeral/docker-compose.yaml b/deploy/dockerephemeral/docker-compose.yaml index 2081edddc21..fb2a4801ebc 100644 --- a/deploy/dockerephemeral/docker-compose.yaml +++ b/deploy/dockerephemeral/docker-compose.yaml @@ -290,7 +290,7 @@ services: POSTGRES_PASSWORD: "posty-the-gres" POSTGRES_USER: "wire-server" POSTGRES_DB: "backendA" - command: postgres -c max_connections=150 + command: postgres -c max_connections=1000 cassandra: container_name: demo_wire_cassandra diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index cf86ee7681c..570a4d355d1 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -2187,6 +2187,7 @@ The current settings and their background-worker flags are: - `conversationCodes` -> `migrateConversationCodes` - `teamFeatures` -> `migrateTeamFeatures` - `domainRegistration` -> `migrateDomainRegistration` +- `user` -> `migrateUsers` **Migration pattern per migration setting** @@ -2205,13 +2206,15 @@ The current settings and their background-worker flags are: conversation: migration-to-postgresql conversationCodes: migration-to-postgresql teamFeatures: migration-to-postgresql - domainRegistration: cassandra + domainRegistration: migration-to-postgresql + user: migration-to-postgresql background-worker: config: migrateConversations: false migrateConversationCodes: false migrateTeamFeatures: false migrateDomainRegistration: false + migrateUsers: false ``` This change should restart the affected pods, and new writes will follow the @@ -2226,6 +2229,7 @@ The current settings and their background-worker flags are: migrateConversationCodes: true migrateTeamFeatures: true migrateDomainRegistration: true + migrateUsers: true ``` During migration, Cassandra rows are not deleted. Writes and migration share @@ -2241,6 +2245,16 @@ The current settings and their background-worker flags are: - `conversationCodes`: `wire_conv_codes_migration_finished` - `teamFeatures`: `wire_team_features_migration_finished` - `domainRegistration`: `wire_domain_registration_migration_finished` + - `user`: `wire_user_migration_finished` + + > ⚠️ For user migrations please watch the logs for `Invalid user found, + > skipping`. This would be accompanied by an error which is either + > `UserHasNoName` or `UserHasNoActivated`. These users are invalid and all + > interactions with them were resulting in errors. If these warnings are + > ignored, these users will stop existing in the system. If these users are + > to be saved, the operator must insert some value as `name` and/or + > `activated` and then re-trigger the migration **after** the background + > worker finishes migrating the valid users. 3. Cut over reads and writes to PostgreSQL for the selected migration setting(s). This configuration must be used from now on for every new @@ -2253,13 +2267,15 @@ The current settings and their background-worker flags are: conversation: postgresql conversationCodes: postgresql teamFeatures: postgresql - domainRegistration: cassandra + domainRegistration: postgresql + user: postgresql background-worker: config: migrateConversations: false migrateConversationCodes: false migrateTeamFeatures: false migrateDomainRegistration: false + migrateUsers: false ``` **How to run migrations independently or in batches** diff --git a/integration/default.nix b/integration/default.nix index 8290943dee2..a1ec576d096 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -57,6 +57,7 @@ , optparse-applicative , process , proto-lens +, QuickCheck , ram , random , raw-strings-qq @@ -162,6 +163,7 @@ mkDerivation { optparse-applicative process proto-lens + QuickCheck ram random raw-strings-qq diff --git a/integration/integration.cabal b/integration/integration.cabal index d124256a118..8dd9466197c 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -181,6 +181,7 @@ library Test.Migration.ConversationCodes Test.Migration.DomainRegistration Test.Migration.TeamFeatures + Test.Migration.User Test.Migration.Util Test.MLS Test.MLS.Clients @@ -301,6 +302,7 @@ library , optparse-applicative , process , proto-lens + , QuickCheck ^>=2.15.0.1 , ram , random , raw-strings-qq diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 7614567b84f..480bb781a15 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -156,10 +156,13 @@ getSelfClients u = -- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/delete_self deleteUser :: (HasCallStack, MakesValue user) => user -> App Response -deleteUser user = do +deleteUser user = deleteUserWithPassword user (Just defPassword) + +deleteUserWithPassword :: (HasCallStack, MakesValue user) => user -> Maybe String -> App Response +deleteUserWithPassword user mPassword = do req <- baseRequest user Brig Versioned "/self" submit "DELETE" $ - req & addJSONObject ["password" .= defPassword] + req & addJSONObject ["password" .= mPassword] -- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_clients addClient :: @@ -824,6 +827,15 @@ addBot user providerId serviceId convId = do & zType "access" & addJSONObject ["provider" .= providerId, "service" .= serviceId] +rmBotSelf :: (HasCallStack, MakesValue domain) => domain -> String -> String -> App Response +rmBotSelf domain bid cid = do + req <- rawBaseRequest domain Brig Versioned $ joinHttpPath ["bot", "self"] + submit "DELETE" $ + req + & zType "bot" + & addHeader "Z-Bot" bid + & addHeader "Z-Conversation" cid + setProperty :: (MakesValue user, ToJSON val) => user -> String -> val -> App Response setProperty user propName val = do req <- baseRequest user Brig Versioned $ joinHttpPath ["properties", propName] diff --git a/integration/test/API/Common.hs b/integration/test/API/Common.hs index cd4b4b348ab..9b720a3b9bd 100644 --- a/integration/test/API/Common.hs +++ b/integration/test/API/Common.hs @@ -25,6 +25,7 @@ import qualified Data.ByteString as BS import Data.Scientific (scientific) import qualified Data.Vector as Vector import System.Random (randomIO, randomRIO) +import Test.QuickCheck import Testlib.Prelude -- | please don't use special shell characters like '!' here. it makes writing shell lines @@ -33,8 +34,11 @@ defPassword :: String defPassword = "hunter2." randomEmail :: App String -randomEmail = do - u <- randomName +randomEmail = liftIO $ generate arbitraryEmail + +arbitraryEmail :: Gen String +arbitraryEmail = do + u <- arbitraryName pure $ u <> "@example.com" randomDomain :: App String @@ -52,23 +56,32 @@ randomExternalId = liftIO $ do pick = (chars !) <$> randomRIO (Array.bounds chars) randomName :: App String -randomName = liftIO $ do - n <- randomRIO (8, 15) +randomName = liftIO $ generate arbitraryName + +arbitraryName :: Gen String +arbitraryName = do + n <- chooseInt (8, 15) replicateM n pick where chars = mkArray $ ['A' .. 'Z'] <> ['a' .. 'z'] <> ['0' .. '9'] - pick = (chars !) <$> randomRIO (Array.bounds chars) + pick = (chars !) <$> chooseInt (Array.bounds chars) randomHandle :: App String -randomHandle = randomHandleWithRange 50 256 +randomHandle = liftIO $ generate arbitraryHandle randomHandleWithRange :: Int -> Int -> App String -randomHandleWithRange min' max' = liftIO $ do - n <- randomRIO (min', max') +randomHandleWithRange min' max' = liftIO $ generate (arbitraryHandleWithRange min' max') + +arbitraryHandle :: Gen String +arbitraryHandle = arbitraryHandleWithRange 50 60 + +arbitraryHandleWithRange :: Int -> Int -> Gen String +arbitraryHandleWithRange min' max' = do + n <- chooseInt (min', max') replicateM n pick where chars = mkArray $ ['a' .. 'z'] <> ['0' .. '9'] <> "_-." - pick = (chars !) <$> randomRIO (Array.bounds chars) + pick = (chars !) <$> chooseInt (Array.bounds chars) randomBytes :: Int -> App ByteString randomBytes n = liftIO $ BS.pack <$> replicateM n randomIO @@ -85,6 +98,14 @@ randomAlphaString n = liftIO $ replicateM n pick chars = mkArray $ ['A' .. 'Z'] <> ['a' .. 'z'] <> ['0' .. '9'] pick = (chars !) <$> randomRIO (Array.bounds chars) +randomPassword :: App String +randomPassword = liftIO $ generate arbitraryPassword + +arbitraryPassword :: Gen String +arbitraryPassword = do + n <- chooseInt (8, 1024) + replicateM n arbitraryPrintableChar + randomJSON :: App Value randomJSON = do let maxThings = 5 diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index f4ed0509567..b5aed2c98a2 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -605,10 +605,14 @@ getCookieWithSamlLogin :: App (Maybe String, SAML.SignedAuthnResponse) getCookieWithSamlLogin mbZHost domain expectSuccess tid nameId mLabel (iid, (meta, privcreds)) = do let idpConfig = SAML.IdPConfig (SAML.IdPId (fromMaybe (error "invalid idp id") (UUID.fromString iid))) meta () - spmeta <- getSPMetadataWithZHost domain mbZHost tid - authnreq <- initiateSamlLoginWithZHostAndLabel domain mbZHost mLabel iid - let spMetaData = toSPMetaData spmeta.body - parsedAuthnReq = parseAuthnReqResp authnreq.body + spMetaData <- + getSPMetadataWithZHost domain mbZHost tid `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + pure $ toSPMetaData resp.body + parsedAuthnReq <- + initiateSamlLoginWithZHostAndLabel domain mbZHost mLabel iid `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + pure $ parseAuthnReqResp resp.body authnReqResp <- makeAuthnResponse nameId privcreds idpConfig spMetaData parsedAuthnReq mCookie <- finalizeSamlLoginWithZHost domain mbZHost tid authnReqResp `bindResponse` validateLoginResp pure (mCookie, authnReqResp) @@ -653,13 +657,14 @@ makeAuthnResponse nameId privcreds idpConfig spMetaData parsedAuthnReq = -- | extract an `AuthnRequest` from the html form in the http response from /sso/initiate-login parseAuthnReqResp :: + (HasCallStack) => ByteString -> SAML.AuthnRequest parseAuthnReqResp bs = reqBody where xml :: XML.Document xml = - fromRight (error "malformed html in response body") $ + fromRight (error $ "malformed html in response body: \n" <> show bs) $ XML.parseText XML.def (cs bs) reqBody :: SAML.AuthnRequest @@ -855,12 +860,12 @@ createNewIndex = do ExitFailure _ -> assertFailure $ prefix <> "failed to create index" ExitSuccess -> pure indexName -reindexUsers :: (HasCallStack) => BackendResource -> Int -> App () -reindexUsers ber pageSize = do +reindexUsers :: (HasCallStack) => BackendResource -> ServiceOverrides -> Int -> App () +reindexUsers ber serviceOverrides pageSize = do testName <- asks (fromMaybe "NoTest" . (.currentTestName)) let indexName = ber.berElasticsearchIndex let prefix = "[reindex-users:" <> indexName <> ":" <> testName <> "] " - getBrigConfig <- readAndUpdateConfig (defaultOverrides ber) ber Brig + getBrigConfig <- readAndUpdateConfig (defaultOverrides ber <> serviceOverrides) ber Brig brigConfig <- liftIO $ getBrigConfig esServer <- brigConfig %. "elasticsearch.url" & asString esCredentials <- brigConfig %. "elasticsearch.credentials" & asString diff --git a/integration/test/Test/Migration/Conversation.hs b/integration/test/Test/Migration/Conversation.hs index 88f78e059ea..1e712ba9094 100644 --- a/integration/test/Test/Migration/Conversation.hs +++ b/integration/test/Test/Migration/Conversation.hs @@ -99,6 +99,7 @@ testMigrationToPostgresMLS = do runPhase 5 where n = 1 + createTestConvs :: (HasCallStack) => ClientIdentity -> String -> ClientIdentity -> ClientIdentity -> [ClientIdentity] -> App TestConvList createTestConvs creatorC tid melC markC othersC = do unmodifiedConvs <- replicateM n $ do diff --git a/integration/test/Test/Migration/User.hs b/integration/test/Test/Migration/User.hs new file mode 100644 index 00000000000..573b3f46e7c --- /dev/null +++ b/integration/test/Test/Migration/User.hs @@ -0,0 +1,956 @@ +{-# LANGUAGE ApplicativeDo #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + +-- | The migration has these phases. +-- 1. Write to cassandra (before any migration activity) +-- 2. Galley is prepared for migrations (new things created in PG, old things are in Cassandra) +-- 3. Backgound worker starts migration +-- 4. Background worker finishes migration, galley is still configured to think migration is on going +-- 5. Background worker is configured to not do anything, galley is configured to only use PG +-- +-- The comments and variable names call these phases by number i.e. Phase1, Phase2, and so on. +-- +-- The tests are from the perspective of mel, a user on the dynamic backend, +-- called backendM (migrating backend). There are also users called mark and mia +-- on this backend. +module Test.Migration.User where + +import API.Brig +import qualified API.BrigInternal as I +import API.Common +import API.Galley +import qualified API.GalleyInternal as I +import API.Spar +import Control.Applicative +import Control.Monad.Codensity +import Control.Monad.Reader +import qualified Data.Aeson.KeyMap as KM +import qualified Data.Aeson.KeyMap as KeyMap +import Data.IntMap (IntMap) +import qualified Data.IntMap as IntMap +import qualified Data.IntSet as IntSet +import qualified Data.Map as Map +import Data.String.Conversions +import Data.Text (Text) +import qualified Data.Text as Text +import Data.Tuple.Extra +import Data.UUID (UUID) +import qualified Data.UUID as UUID +import qualified Data.Vector as Vector +import Database.CQL.IO +import GHC.Stack +import Notifications +import SetupHelpers hiding (deleteUser) +import Test.Bot (mkBotService) +import Test.Migration.Util +import Test.QuickCheck +import Test.Search +import Testlib.MockIntegrationService (MockServerSettings (..), withMockServer) +import Testlib.Prelude +import Testlib.ResourcePool +import UnliftIO + +testUserMigrationToPostgres :: App () +testUserMigrationToPostgres = withMockServer botServiceSettings mkBotService $ \(botHost, botPort) _botChan -> do + resourcePool <- asks (.resourcePool) + + runCodensity (acquireResources 1 resourcePool) $ \[migratingBackend] -> do + let domainM = migratingBackend.berDomain + (mel, pid, sid, seedUsers) <- runCodensity (startDynamicBackend migratingBackend phase1Overrides) $ \_ -> do + -- mel exists to connect with all the personal users, so we can wait for a + -- notification for their deletion + mel <- randomUser domainM def + + pid <- setupProvider domainM def {newProviderPassword = Just defPassword} %. "id" & asString + service <- + newService domainM pid + $ def + { newServiceUrl = "https://" <> botHost <> ":" <> show botPort, + newServiceKey = cs botServiceSettings.publicKey + } + sid <- service %. "id" & asString + updateServiceConn domainM pid sid (object ["password" .= defPassword, "enabled" .= True]) >>= assertSuccess + + seedUsers <- seedTestUsers domainM mel pid sid + pure (mel, pid, sid, seedUsers) + + newUsersRef <- newIORef mempty + updatedUsersRef <- newIORef mempty + updates <- fmap IntMap.fromList . for [1 .. 5] $ \phase -> do + (phase,) <$> liftIO (generate (arbitraryPhaseUpdates nUpdates)) + + addUsersToFailureContext [("mel", mel)] + $ addJSONToFailureContext "updates" updates + $ addJSONToFailureContext "seed users" seedUsers do + let runPhase :: (HasCallStack) => Int -> App () + runPhase phase = do + runCodensity (startDynamicBackend migratingBackend (phaseOverrides IntMap.! phase)) $ \_ -> do + let toBeUpdated = seedUsers.updates IntMap.! phase + phaseUpdates = updates IntMap.! phase + + updatedScimUsersWithRichInfo <- updateScimUsers domainM toBeUpdated.scimUsersWithRichInfo phaseUpdates.scimUsersWithRichInfo + updatedScimUsersWithoutRichInfo <- updateScimUsers domainM toBeUpdated.scimUsersWithoutRichInfo phaseUpdates.scimUsersWithoutRichInfo + updatedPendingScimUsers <- updatePendingScimUsers domainM toBeUpdated.pendingScimUsers phaseUpdates.pendingScimUsers + updatedSsoUsers <- checkUpdateUser toBeUpdated.ssoUsers.users phaseUpdates.ssoUsers + updatedPasswordTeamUsers <- checkUpdateUser toBeUpdated.passwordTeamUsers.users phaseUpdates.passwordTeamUsers + updatedPersonalUsersWithoutHandle <- checkUpdateUser toBeUpdated.personalUsersWithoutHandle phaseUpdates.personalUsersWithoutHandle + updatedPersonalUsersWithHandle <- checkUpdateUser toBeUpdated.personalUsersWithHandle phaseUpdates.personalUsersWithHandle + let updatedUsers = + TestUserList + { scimUsersWithRichInfo = updatedScimUsersWithRichInfo, + scimUsersWithoutRichInfo = updatedScimUsersWithoutRichInfo, + pendingScimUsers = updatedPendingScimUsers, + ssoUsers = toBeUpdated.ssoUsers {users = updatedSsoUsers} :: TestTeamUsers, + passwordTeamUsers = toBeUpdated.passwordTeamUsers {users = updatedPasswordTeamUsers} :: TestTeamUsers, + personalUsersWithoutHandle = updatedPersonalUsersWithoutHandle, + personalUsersWithHandle = updatedPersonalUsersWithHandle, + -- Bots don't have any updates + botsInTeamConvs = toBeUpdated.botsInTeamConvs, + botsInPersonalConvs = toBeUpdated.botsInPersonalConvs + } + + newUsers <- createTestUsers domainM mel pid sid nNew + modifyIORef newUsersRef (IntMap.insert phase newUsers) + modifyIORef updatedUsersRef (IntMap.insert phase updatedUsers) + + let toBeDeleted = seedUsers.deletes IntMap.! phase + + deleteScimUsers domainM False toBeDeleted.scimUsersWithRichInfo + deleteScimUsers domainM False toBeDeleted.scimUsersWithoutRichInfo + deleteScimUsers domainM True toBeDeleted.pendingScimUsers + deleteTeamUsers toBeDeleted.ssoUsers + deleteTeamUsers toBeDeleted.passwordTeamUsers + deletePersonalUsers mel toBeDeleted.personalUsersWithoutHandle + deletePersonalUsers mel toBeDeleted.personalUsersWithHandle + deleteBotsTeam toBeDeleted.botsInTeamConvs pid sid + deleteBotConvs mel toBeDeleted.botsInPersonalConvs + + checkAllDeletionsWorked domainM mel seedUsers.deletes phase + checkUnaffectedUsers domainM seedUsers.deletes seedUsers.updates phase + updatedSoFar <- readIORef updatedUsersRef + newSoFar <- readIORef newUsersRef + addJSONToFailureContext "newSoFar" newSoFar + $ checkNewAndUpdatedUsers domainM updatedSoFar newSoFar + + when (phase == 3) $ do + waitForMigration domainM userMigrationFinishedCounterName + runPhase 1 + runPhase 2 + runPhase 3 + runPhase 4 + runPhase 5 + where + parallelism = 64 + + -- Number of users of each type + nUpdates = 5 + nDeletes = 1 + nNew = 1 + + botServiceSettings = def + + seedTestUsers :: (HasCallStack, MakesValue mel) => String -> mel -> String -> String -> App TestUsersByOperations + seedTestUsers domain mel pid sid = + fmap mconcat . for [(1 :: Int) .. 5] $ \phase -> do + updates <- IntMap.singleton phase <$> createTestUsers domain mel pid sid nUpdates + deletes <- IntMap.singleton phase <$> createTestUsers domain mel pid sid nDeletes + pure TestUsersByOperations {..} + + tombstone :: String -> String -> Maybe String -> Value + tombstone domain uid mTid = + object + $ [ "accent_id" .= (0 :: Int), + "assets" .= (), + "deleted" .= True, + "id" .= uid, + "legalhold_status" .= "no_consent", + "name" .= "default", + "picture" .= (), + "qualified_id" .= object ["domain" .= domain, "id" .= uid], + "searchable" .= True, + "supported_protocols" .= ["proteus"], + "type" .= "regular" + ] + <> (maybe [] (\tid -> ["team" .= tid]) mTid) + + scimUserIdsWithGetter :: (HasCallStack) => IntMap TestUserList -> [(String, String)] + scimUserIdsWithGetter relevantSeedUsers = + foldMap IntMap.elems . for relevantSeedUsers $ \usersInPhase -> do + map (usersInPhase.scimUsersWithRichInfo.token,) (Map.keys usersInPhase.scimUsersWithRichInfo.users) + <> map (usersInPhase.scimUsersWithoutRichInfo.token,) (Map.keys usersInPhase.scimUsersWithoutRichInfo.users) + <> map (usersInPhase.pendingScimUsers.token,) (Map.keys usersInPhase.pendingScimUsers.users) + + nonScimUserIds :: (HasCallStack) => Value -> IntMap TestUserList -> [(Value, Value)] + nonScimUserIds mel relevantSeedUsers = foldMap IntMap.elems . for relevantSeedUsers $ \usersInPhase -> do + map (usersInPhase.scimUsersWithRichInfo.owner,) (thd3 <$> Map.elems usersInPhase.scimUsersWithRichInfo.users) + <> map (usersInPhase.scimUsersWithoutRichInfo.owner,) (thd3 <$> Map.elems usersInPhase.scimUsersWithoutRichInfo.users) + <> map (usersInPhase.passwordTeamUsers.owner,) (fst <$> Map.elems usersInPhase.passwordTeamUsers.users) + <> map (mel,) (fst <$> Map.elems usersInPhase.personalUsersWithHandle) + <> map (mel,) (fst <$> Map.elems usersInPhase.personalUsersWithoutHandle) + + checkAllDeletionsWorked :: (HasCallStack) => String -> Value -> IntMap TestUserList -> Int -> App () + checkAllDeletionsWorked domain mel seedUsers phase = do + let deletedSoFar = IntMap.restrictKeys seedUsers (IntSet.fromList $ [1 .. phase]) + pooledForConcurrentlyN_ parallelism (scimUserIdsWithGetter deletedSoFar) $ \(token, uid) -> + getScimUser domain token uid >>= assertStatus 404 + + pooledForConcurrentlyN_ parallelism (nonScimUserIds mel deletedSoFar) $ \(getter, user) -> do + uid <- user %. "qualified_id.id" & asString + mTid <- lookupField user "team" & asStringM + getUser getter (object ["domain" .= domain, "id" .= uid]) `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` tombstone domain uid mTid + + let bots = + concatMap + ( \testUsers -> + map fst (Map.elems testUsers.botsInTeamConvs.users) + <> map fst (Map.elems testUsers.botsInPersonalConvs) + ) + (IntMap.elems deletedSoFar) + pooledForConcurrentlyN_ parallelism bots $ \botUser -> do + botTombstone <- setField "status" "deleted" =<< setField "deleted" True botUser + getSelf botUser `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` botTombstone + + checkUnaffectedUsers :: (HasCallStack) => String -> IntMap TestUserList -> IntMap TestUserList -> Int -> App () + checkUnaffectedUsers domain seedUsersToBeUpdated seedUsersToBeDeleted phase = do + let upcomingPhases = IntSet.fromList [(phase + 1) .. 5] + usersNotYetDeleted = IntMap.restrictKeys seedUsersToBeDeleted upcomingPhases + usersNotYetUpdated = IntMap.restrictKeys seedUsersToBeUpdated upcomingPhases + existingUserLists = IntMap.elems usersNotYetDeleted <> IntMap.elems usersNotYetUpdated + existingScimUsers = concatMap extractScimUsers existingUserLists + existingUsers = concatMap extractTestUsers existingUserLists + + checkScimUsers domain existingScimUsers + checkUsers existingUsers + + checkNewAndUpdatedUsers :: (HasCallStack) => String -> IntMap TestUserList -> IntMap TestUserList -> App () + checkNewAndUpdatedUsers domain newUsers updatedUsers = do + let scimUsers = + concatMap extractScimUsers newUsers + <> concatMap extractScimUsers updatedUsers + users = + concatMap extractTestUsers newUsers + <> concatMap extractTestUsers updatedUsers + checkScimUsers domain scimUsers + checkUsers users + + checkScimUsers :: (HasCallStack) => String -> [(String, Value)] -> App () + checkScimUsers domain tokensAndUsers = do + pooledForConcurrentlyN_ parallelism tokensAndUsers $ \(token, scimUser) -> + addJSONToFailureContext "scimUser" scimUser $ do + uid <- scimUser %. "id" & asString + getScimUser domain token uid `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` scimUser + + checkUsers :: (HasCallStack) => [Value] -> App () + checkUsers users = + pooledForConcurrentlyN_ parallelism users $ \user -> + addJSONToFailureContext "user" user $ do + getSelf user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` user + + extractScimUsers :: TestUserList -> [(String, Value)] + extractScimUsers testUserList = + map (testUserList.scimUsersWithRichInfo.token,) (fst3 <$> Map.elems testUserList.scimUsersWithRichInfo.users) + <> map (testUserList.scimUsersWithoutRichInfo.token,) (fst3 <$> Map.elems testUserList.scimUsersWithoutRichInfo.users) + <> map (testUserList.pendingScimUsers.token,) (fst3 <$> Map.elems testUserList.pendingScimUsers.users) + + extractTestUsers :: TestUserList -> [Value] + extractTestUsers testUserList = + map thd3 (Map.elems testUserList.scimUsersWithRichInfo.users) + <> map thd3 (Map.elems testUserList.scimUsersWithoutRichInfo.users) + <> map fst (Map.elems testUserList.ssoUsers.users) + <> map fst (Map.elems testUserList.passwordTeamUsers.users) + <> map fst (Map.elems testUserList.personalUsersWithHandle) + <> map fst (Map.elems testUserList.personalUsersWithoutHandle) + <> extractBots testUserList + + extractBots :: TestUserList -> [Value] + extractBots testUserList = + map fst (Map.elems testUserList.botsInTeamConvs.users) + <> map fst (Map.elems testUserList.botsInPersonalConvs) + + createTestUsers :: (HasCallStack, MakesValue mel) => String -> mel -> String -> String -> Int -> App TestUserList + createTestUsers domain mel pid sid n = runConcurrently $ do + scimUsersWithRichInfo <- Concurrently $ createScimUsers domain n True True + scimUsersWithoutRichInfo <- Concurrently $ createScimUsers domain n False True + pendingScimUsers <- Concurrently $ createScimUsers domain n False False + ssoUsers <- Concurrently $ createSsoUsers domain n + passwordTeamUsers <- Concurrently $ createPasswordTeamUsers domain n + personalUsersWithoutHandle <- Concurrently $ createPersonalUsers domain mel n False + personalUsersWithHandle <- Concurrently $ createPersonalUsers domain mel n True + botsInTeamConvs <- Concurrently $ createTeamBots domain pid sid n + botsInPersonalConvs <- Concurrently $ createConvsAndAddBot domain mel Nothing pid sid n + pure TestUserList {..} + + getUnqualifiedUser :: String -> String -> App (Map String Value) + getUnqualifiedUser domain uid = do + let quid = object ["domain" .= domain, "id" .= uid] + Map.singleton uid <$> (getSelf quid >>= getJSON 200) + + createScimUsers :: (HasCallStack) => String -> Int -> Bool -> Bool -> App TestScimUsers + createScimUsers domain n shouldCreateRichInfo shouldAcceptInvite = do + (owner, tid, _) <- createTeam domain 1 + tok <- createScimToken owner def >>= \resp -> resp.json %. "token" >>= asString + users <- fmap Map.unions . pooledReplicateConcurrentlyN 16 n $ do + newScimUser0 <- randomScimUser + newScimUser <- + if shouldCreateRichInfo + then do + richInfoKey <- randomAlphaString 10 + richInfoValue <- randomString 10 + modifyObject (KeyMap.insert (fromString "urn:ietf:params:scim:schemas:extension:wire:1.0:User") (object [richInfoKey .= richInfoValue])) + =<< setField "schemas" ["urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:wire:1.0:User"] newScimUser0 + else pure newScimUser0 + email <- asString $ newScimUser %. "emails.0.value" + inactiveScimUser <- createScimUser domain tok newScimUser >>= getJSON 201 + uid <- inactiveScimUser %. "id" & asString + (scimUser, userOrInv) <- + if shouldAcceptInvite + then do + registerInvitedUser domain tid email + scimUser <- getScimUser owner tok uid >>= getJSON 200 + (scimUser,) <$> getUnqualifiedUser domain uid + else fmap (inactiveScimUser,) . fmap (Map.singleton uid) . getJSON 200 =<< I.getInvitationByEmail domain email + pure $ (scimUser,defPassword,) <$> userOrInv + pure $ TestScimUsers owner tok users + + deleteScimUsers :: (HasCallStack) => String -> Bool -> TestScimUsers -> App () + deleteScimUsers domain arePendingUsers testScimUsers = do + withWebSocket testScimUsers.owner $ \wsOwner -> do + pooledForConcurrentlyN_ parallelism testScimUsers.users $ \(scimUser, _, _) -> do + uid <- scimUser %. "id" & asString + deleteScimUser domain testScimUsers.token uid >>= assertSuccess + getScimUser domain testScimUsers.token uid >>= assertStatus 404 + + unless arePendingUsers $ do + void $ awaitNMatches (Map.size testScimUsers.users) isTeamMemberLeaveNotif wsOwner + + updatePendingScimUserAndCheck :: (HasCallStack) => String -> String -> (Value, String, Value) -> UserUpdate -> App (Value, String, Value) + updatePendingScimUserAndCheck domain token (scimUser, pw, inv) update = do + addJSONToFailureContext "update" update . addJSONToFailureContext "scimUser" scimUser $ do + uid <- scimUser %. "id" & asString + updatedScimUser <- case update of + UpdatePassword _ -> do + pure scimUser + _ -> do + let updateScimRecord = case update of + UpdateName newName -> setField "displayName" newName + UpdateEmail newEmail -> setField "emails" (Array (Vector.singleton (object ["value" .= newEmail]))) + UpdateHandle newHandle -> setField "userName" newHandle + updateScimReq <- setField "active" True =<< updateScimRecord scimUser + updateScimUser domain token uid updateScimReq `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + case update of + UpdateName newName -> resp.json %. "displayName" `shouldMatch` newName + UpdateEmail newEmail -> resp.json %. "emails.0.value" `shouldMatch` newEmail + UpdateHandle newHandle -> resp.json %. "userName" `shouldMatch` newHandle + assertJust "expected a updated scim user" resp.json + (updatedUserOrInv, newPassword) <- do + case update of + UpdatePassword newPassword -> pure (inv, newPassword) + _ -> + -- Changing email of a pending user doesn't generate a new + -- invitation, perhaps this is a bug? + -- Changing other things ofc doesn't generate a new invitation. + pure (inv, pw) + pure (updatedScimUser, newPassword, updatedUserOrInv) + + updateScimUserAndCheck :: (HasCallStack) => String -> String -> (Value, String, Value) -> UserUpdate -> App (Value, String, Value) + updateScimUserAndCheck domain token (scimUser, pw, user) update = do + uid <- scimUser %. "id" & asString + updatedScimUser <- case update of + UpdatePassword newPassword -> do + putPassword user pw newPassword >>= assertSuccess + pure scimUser + _ -> do + updateScimReq <- case update of + UpdateName newName -> setField "displayName" newName scimUser + UpdateEmail newEmail -> setField "emails" (Array (Vector.singleton (object ["value" .= newEmail]))) scimUser + UpdateHandle newHandle -> setField "userName" newHandle scimUser + updateScimUser domain token uid updateScimReq `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + case update of + UpdateName newName -> resp.json %. "displayName" `shouldMatch` newName + UpdateEmail newEmail -> resp.json %. "emails.0.value" `shouldMatch` newEmail + UpdateHandle newHandle -> resp.json %. "userName" `shouldMatch` newHandle + assertJust "expected a updated scim user" resp.json + (updatedUserOrInv, newPassword) <- case update of + UpdatePassword newPassword -> do + email <- scimUser %. "emails.0.value" & asString + login domain email newPassword >>= assertSuccess + pure (user, newPassword) + UpdateEmail newEmail -> do + activateEmail domain newEmail + getSelf user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "email" `shouldMatch` newEmail + (,pw) <$> assertJust "expected user data" resp.json + _ -> do + getSelf user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + case update of + UpdateName newName -> resp.json %. "name" `shouldMatch` newName + UpdateHandle newHandle -> resp.json %. "handle" `shouldMatch` newHandle + (,pw) <$> assertJust "expected user data" resp.json + pure (updatedScimUser, newPassword, updatedUserOrInv) + + updateScimUsers :: (HasCallStack) => String -> TestScimUsers -> [UserUpdate] -> App TestScimUsers + updateScimUsers domain testScimUsers updates = do + let usersWithUpdates = (zip (Map.elems testScimUsers.users) updates) + updatedUsers <- fmap Map.unions . pooledForConcurrentlyN parallelism usersWithUpdates $ \((scimUser, pw, user), update) -> do + uid <- scimUser %. "id" & asString + Map.singleton uid <$> updateScimUserAndCheck domain testScimUsers.token (scimUser, pw, user) update + + pure $ (testScimUsers {users = updatedUsers} :: TestScimUsers) + + updatePendingScimUsers :: (HasCallStack) => String -> TestScimUsers -> [PendingScimUpdate] -> App TestScimUsers + updatePendingScimUsers domain testScimUsers updates = do + let usersWithUpdates = (zip (Map.elems testScimUsers.users) updates) + updatedUsers <- fmap Map.unions . pooledForConcurrentlyN parallelism usersWithUpdates $ \((scimUser, pw, inv), update) -> do + uid <- scimUser %. "id" & asString + email <- scimUser %. "externalId" & asString + tid <- testScimUsers.owner %. "team" & asString + Map.singleton uid <$> case update of + RegisterPendingScimUser -> do + registerInvitedUser domain tid email + updatedScimUser <- getScimUser domain testScimUsers.token uid >>= getJSON 200 + let quid = object ["domain" .= domain, "id" .= uid] + fmap (updatedScimUser,pw,) . getJSON 200 =<< getSelf quid + UpdatePendingScimUser updateUser -> do + updatePendingScimUserAndCheck domain testScimUsers.token (scimUser, pw, inv) updateUser + pure (testScimUsers {users = updatedUsers} :: TestScimUsers) + + createSsoUsers :: (HasCallStack) => String -> Int -> App TestTeamUsers + createSsoUsers domain n = do + (owner, tid, _) <- createTeam domain 1 + I.setTeamFeatureStatus owner tid "sso" "enabled" >>= assertSuccess + (createIdpResp, (idpMeta, privcreds)) <- registerTestIdPWithMetaWithPrivateCreds owner + assertSuccess createIdpResp + idpId <- asString =<< (createIdpResp.json %. "id") + + users <- fmap Map.unions . pooledReplicateConcurrentlyN 16 n $ do + subject <- nextSubject + (mUid, _) <- loginWithSamlWithZHost Nothing domain True tid subject (idpId, (idpMeta, privcreds)) + uid <- assertJust "user id not created by logging in with SAML" mUid + (,Nothing) <$$> getUnqualifiedUser domain uid + pure $ TestTeamUsers {..} + + createPasswordTeamUsers :: (HasCallStack) => String -> Int -> App TestTeamUsers + createPasswordTeamUsers domain n = do + (owner, _tid, usersWithoutPassword) <- createTeam domain n + + users <- fmap Map.unions . pooledForConcurrentlyN parallelism usersWithoutPassword $ \user -> do + p <- randomPassword + putPassword user defPassword p >>= assertSuccess + uid <- user %. "qualified_id.id" & asString + pure $ Map.singleton uid (user, Just p) + + pure $ TestTeamUsers {..} + + deleteTeamUsers :: (HasCallStack) => TestTeamUsers -> App () + deleteTeamUsers team = do + withWebSocket team.owner $ \wsOwner -> do + tid <- team.owner %. "team" & asString + pooledForConcurrentlyN_ parallelism team.users $ \(user, _) -> do + uid <- user %. "qualified_id.id" & asString + deleteTeamMember tid team.owner uid >>= assertSuccess + + void $ awaitNMatches (Map.size team.users) isTeamMemberLeaveNotif wsOwner + + getSelfWithAssertion :: (HasCallStack, MakesValue user) => user -> ((HasCallStack) => Response -> App ()) -> App (Map String Value) + getSelfWithAssertion user assertion = do + getSelf user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + assertion resp + Map.singleton <$> (resp.json %. "qualified_id.id" & asString) <*> (assertJust "expected GET /self to return a JSON" resp.json) + + checkUpdateUser :: (HasCallStack) => Map String (Value, Maybe String) -> [UserUpdate] -> App (Map String (Value, Maybe String)) + checkUpdateUser users updates = do + fmap Map.unions . pooledForConcurrentlyN parallelism (zip (Map.elems users) updates) $ \((user, mPassword), update) -> + addJSONToFailureContext "user" user . addJSONToFailureContext "update" update $ do + updatedUser <- case (update, mPassword) of + (UpdateName newName, _) -> do + putSelf user def {name = Just newName} >>= assertSuccess + getSelfWithAssertion user $ \resp -> resp.json %. "name" `shouldMatch` newName + (UpdateEmail newEmail, Just pw) -> do + oldEmail <- user %. "email" & asString + (cookie, token) <- bindResponse (login user oldEmail pw) $ \resp -> do + resp.status `shouldMatchInt` 200 + token <- resp.json %. "access_token" & asString + let cookie = fromJust $ getCookie "zuid" resp + pure ("zuid=" <> cookie, token) + updateEmail user newEmail cookie token >>= assertSuccess + activateEmail user newEmail + getSelfWithAssertion user $ \resp -> resp.json %. "email" `shouldMatch` newEmail + (UpdateEmail {}, Nothing) -> do + uid <- user %. "qualified_id.id" & asString + pure $ Map.singleton uid user + (UpdateHandle newHandle, _) -> do + putHandle user newHandle >>= assertSuccess + getSelfWithAssertion user $ \resp -> resp.json %. "handle" `shouldMatch` newHandle + (UpdatePassword newPassword, Just oldPassword) -> do + email <- user %. "email" & asString + putPassword user oldPassword newPassword >>= assertSuccess + login user email oldPassword `bindResponse` \resp -> + resp.status `shouldMatchInt` 403 + login user email newPassword >>= assertSuccess + uid <- user %. "qualified_id.id" & asString + pure $ Map.singleton uid user + (UpdatePassword {}, Nothing) -> do + uid <- user %. "qualified_id.id" & asString + pure $ Map.singleton uid user + pure $ (,mPassword) <$> updatedUser + + createPersonalUsers :: (HasCallStack, MakesValue mel) => String -> mel -> Int -> Bool -> App (Map String (Value, Maybe String)) + createPersonalUsers domain mel n claimHandle = + fmap Map.unions . pooledReplicateConcurrentlyN parallelism n $ do + user <- randomUser domain def + connectTwoUsers mel user + uid <- user %. "qualified_id.id" & asString + if claimHandle + then do + hdl <- randomHandle + putHandle user hdl >>= assertSuccess + fmap (,Just defPassword) . Map.singleton uid <$> (setField "handle" hdl user) + else pure $ Map.singleton uid (user, Just defPassword) + + deletePersonalUsers :: (HasCallStack, MakesValue mel, ToWSConnect mel) => mel -> Map String (Value, Maybe String) -> App () + deletePersonalUsers mel users = + withWebSocket mel $ \wsMel -> do + pooledForConcurrentlyN_ parallelism users $ uncurry deleteUserWithPassword + void $ awaitNMatches (Map.size users) isDeleteUserNotif wsMel + + createConvsAndAddBot :: (HasCallStack, MakesValue user) => String -> user -> Maybe String -> String -> String -> Int -> App (Map String (Value, Value)) + createConvsAndAddBot domain user tid pid sid n = do + fmap Map.unions . pooledReplicateConcurrentlyN parallelism n $ do + conv <- postConversation user (defProteus {team = tid}) >>= getJSON 201 + convId <- conv %. "qualified_id" & objId + addBotResp <- addBot user pid sid convId >>= getJSON 201 + botId <- addBotResp %. "id" & asString + (,conv) <$$> getUnqualifiedUser domain botId + + createTeamBots :: (HasCallStack) => String -> String -> String -> Int -> App TestTeamUsers + createTeamBots domain pid sid n = do + (owner, tid, _) <- createTeam domain 1 + postServiceWhitelist owner tid (object ["id" .= sid, "provider" .= pid, "whitelisted" .= True]) + >>= assertSuccess + TestTeamUsers owner . fmap (\(x, _) -> (x, Nothing)) <$> createConvsAndAddBot domain owner (Just tid) pid sid n + + deleteBotsTeam :: (HasCallStack) => TestTeamUsers -> String -> String -> App () + deleteBotsTeam testTeam pid sid = do + tid <- testTeam.owner %. "team" & asString + withWebSocket testTeam.owner $ \ws -> do + postServiceWhitelist testTeam.owner tid (object ["id" .= sid, "provider" .= pid, "whitelisted" .= False]) >>= assertSuccess + void $ awaitNMatches (Map.size testTeam.users) isConvLeaveNotif ws + + deleteBotConvs :: (HasCallStack) => Value -> Map String (Value, Value) -> App () + deleteBotConvs mel botConvs = do + pooledForConcurrentlyN_ parallelism (Map.elems botConvs) $ \(bot, conv) -> do + cid <- conv %. "qualified_id.id" & asString + bid <- bot %. "qualified_id.id" & asString + rmBotSelf mel bid cid >>= assertSuccess + +-- | This test creates users in PG and Cassandra separately to simulate a +-- situation where there are users in both DBs. Then tries to index them into ES +-- to make sure the pagination over these users works. +testReindexingUsersDuringMigration :: (HasCallStack) => App () +testReindexingUsersDuringMigration = do + resourcePool <- asks (.resourcePool) + + runCodensity (acquireResources 1 resourcePool) $ \[backend] -> do + let domain = backend.berDomain + -- Create users in cassandra using 'phase1Overrides' + (casSearcher, casExistingUsers, casDeletedUsers) <- + runCodensity (startDynamicBackend backend phase1Overrides) + $ \_ -> setupUsers domain + + -- Create users in postgres using 'phase5Overrides' + (pgSearcher, pgExistingUsers, pgDeletedUsers) <- + runCodensity (startDynamicBackend backend phase5Overrides) + $ \_ -> setupUsers domain + + -- Test that searching in the already existing index works with in + -- 'phase2Overrides', which should work with data in cassandra and postgres + runCodensity (startDynamicBackend backend phase2Overrides) $ \_ -> do + I.refreshIndex domain + checkSearchWorks domain casSearcher casExistingUsers casDeletedUsers + checkSearchWorks domain pgSearcher pgExistingUsers pgDeletedUsers + + newIndex <- createNewIndex + let backendWithNewIndex = backend {berElasticsearchIndex = newIndex} + runCodensity (startDynamicBackend backendWithNewIndex phase2Overrides) $ \_ -> do + reindexUsers backendWithNewIndex phase2Overrides 5 + I.refreshIndex domain + checkSearchWorks domain casSearcher casExistingUsers casDeletedUsers + checkSearchWorks domain pgSearcher pgExistingUsers pgDeletedUsers + where + n = 5 + parallelism = 16 + + setupUsers :: (HasCallStack) => String -> App (Value, [Value], [Value]) + setupUsers domain = do + searcher <- randomUser domain def + existingUsers <- pooledReplicateConcurrentlyN parallelism n $ randomUser domain def + deletedUsers <- pooledReplicateConcurrentlyN parallelism n $ do + u <- randomUser domain def + connectTwoUsers searcher u + pure u + withWebSocket searcher $ \ws -> do + pooledForConcurrentlyN_ parallelism deletedUsers deleteUser + void $ awaitNMatches n isDeleteUserNotif ws + pure (searcher, existingUsers, deletedUsers) + + checkSearchWorks :: (HasCallStack) => String -> Value -> [Value] -> [Value] -> App () + checkSearchWorks domain searcher existingUsers deletedUsers = do + pooledForConcurrentlyN_ parallelism existingUsers $ \u -> + assertCanFind searcher u (u %. "name") domain + + pooledForConcurrentlyN_ parallelism deletedUsers $ \u -> + assertCannotFind searcher u (u %. "name") domain + +-- handleA: Alice and Anna have the same handle, but the handle claims table +-- supports Alice's claim. After the migration Bob loses their handle. +-- +-- handleB: Bob and Bill also have the same handle, but the handle claims table +-- doesn't support any of their claims. After the migration both of them will +-- loose the claim. +-- +-- handleC: Carl and Creed also have the same handle, Cassandra supports Carl's +-- claim, while Postgresql supports Creed's claim. In this case Creed gets to +-- keep their handle. +testMigrationOfUsersWithHandleDisputes :: (HasCallStack) => App () +testMigrationOfUsersWithHandleDisputes = do + resourcePool <- asks (.resourcePool) + -- Between Alice and Anna + handleA <- randomHandle + + -- In user record for Bob and Bill + handleB <- randomHandle + + -- Between Carl and Creed + handleC <- randomHandle + + runCodensity (acquireResources 1 resourcePool) $ \[backend] -> do + let domain = backend.berDomain + brigKeyspace = backend.berBrigKeyspace + (alice, anna, bob, bill, carl) <- runCodensity (startDynamicBackend backend phase1Overrides) $ \_ -> do + alice <- randomUser domain def + anna <- randomUser domain def + bob <- randomUser domain def + bill <- randomUser domain def + carl <- randomUser domain def + + Just annaId <- UUID.fromString <$> (anna %. "qualified_id.id" & asString) + Just bobId <- UUID.fromString <$> (bob %. "qualified_id.id" & asString) + Just billId <- UUID.fromString <$> (bill %. "qualified_id.id" & asString) + + -- Claim handle correctly for alice + putHandle alice handleA >>= assertSuccess + putHandle carl handleC >>= assertSuccess + + -- Claim handle by hacking into the DB for others. There seems to be no + -- other way of testing this edge case + let assignHandleQuery :: PrepQuery W (Text, UUID) () = fromString $ "UPDATE " <> brigKeyspace <> ".user SET handle = ? WHERE id = ?" + write assignHandleQuery $ defQueryParams LocalQuorum (Text.pack handleA, annaId) + write assignHandleQuery $ defQueryParams LocalQuorum (Text.pack handleB, bobId) + write assignHandleQuery $ defQueryParams LocalQuorum (Text.pack handleB, billId) + + assertHandle alice (Just handleA) + assertHandle anna (Just handleA) + assertHandle bob (Just handleB) + assertHandle bill (Just handleB) + assertHandle carl (Just handleC) + + pure (alice, anna, bob, bill, carl) + + -- Start Phase 5 here so that we can claim the same handle for Dan as Doug + -- but in Postgresql. The production scenario can only happen due to a race + -- condition. This is just a more precise way of causing the DB + -- inconsistency. + creed <- runCodensity (startDynamicBackend backend phase5Overrides) $ \_ -> do + creed <- randomUser domain def + putHandle creed handleC >>= assertSuccess + assertHandle creed (Just handleC) + pure creed + + runCodensity (startDynamicBackend backend phase3Overrides) $ \_ -> do + waitForMigration domain userMigrationFinishedCounterName + assertMigrationSuccessful domain "^wire_users_migration_failed" + + runCodensity (startDynamicBackend backend phase5Overrides) $ \_ -> do + assertHandle alice (Just handleA) + assertHandle anna Nothing + + assertHandle bob Nothing + assertHandle bill Nothing + + assertHandle carl Nothing + assertHandle creed (Just handleC) + + -- handleA cannot be claimed + putHandle anna handleA >>= assertStatus 409 + + -- handleB can be claimed + putHandle bob handleB >>= assertSuccess + + -- handleC cannot be claimed + putHandle carl handleC >>= assertStatus 409 + where + assertHandle :: (HasCallStack) => Value -> Maybe String -> App () + assertHandle user expectedHandle = do + getSelf user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + case expectedHandle of + Just h -> + resp.json %. "handle" `shouldMatch` h + Nothing -> + case resp.json of + Just (Object o) -> KM.keys o `shouldNotContain` [fromString "handle"] + _ -> assertFailure "Unexpected body for getSelf" + +testMigrationOfInvalidUsers :: (HasCallStack) => App () +testMigrationOfInvalidUsers = do + resourcePool <- asks (.resourcePool) + + runCodensity (acquireResources 1 resourcePool) $ \[backend] -> do + let domain = backend.berDomain + brigKeyspace = backend.berBrigKeyspace + (validUser, noName, noNameId, noActivated, noActivatedId) <- runCodensity (startDynamicBackend backend phase1Overrides) $ \_ -> do + validUser <- randomUser domain def + + noName <- randomUser domain def + Just noNameId <- UUID.fromString <$> (noName %. "qualified_id.id" & asString) + + noActivated <- randomUser domain def + Just noActivatedId <- UUID.fromString <$> (noActivated %. "qualified_id.id" & asString) + + -- Cause users to be invalid by poking into Cassandra + let removeName :: PrepQuery W (Identity UUID) () = fromString $ "UPDATE " <> brigKeyspace <> ".user SET name = NULL WHERE id = ?" + removeActivated :: PrepQuery W (Identity UUID) () = fromString $ "UPDATE " <> brigKeyspace <> ".user SET activated = NULL WHERE id = ?" + write removeName $ defQueryParams LocalQuorum (Identity noNameId) + write removeActivated $ defQueryParams LocalQuorum (Identity noActivatedId) + + getSelf validUser >>= assertStatus 200 + getSelf noName >>= assertStatus 500 + getSelf noActivated >>= assertStatus 500 + + pure (validUser, noName, noNameId, noActivated, noActivatedId) + + runCodensity (startDynamicBackend backend phase3Overrides) $ \_ -> do + waitForMigration domain userMigrationFinishedCounterName + + runCodensity (startDynamicBackend backend phase5Overrides) $ \_ -> do + getSelf validUser >>= assertStatus 200 + getSelf noName >>= assertStatus 404 + getSelf noActivated >>= assertStatus 404 + + -- Delete invalid users from cassandra so they don't trip other tests. These + -- other tests are usually reindexing the users, the reindex code doesn't + -- deal with invalid users so well. + let deleteUserRow :: PrepQuery W (Identity UUID) () = fromString $ "DELETE FROM " <> brigKeyspace <> ".user WHERE id = ?" + write deleteUserRow $ defQueryParams LocalQuorum (Identity noNameId) + write deleteUserRow $ defQueryParams LocalQuorum (Identity noActivatedId) + +-- * Test Helpers + +data TestUsersByOperations = TestUsersByOperations + { updates :: IntMap TestUserList, + deletes :: IntMap TestUserList + } + deriving (Show, Eq, Generic) + +instance Semigroup TestUsersByOperations where + users1 <> users2 = + TestUsersByOperations + { updates = users1.updates <> users2.updates, + deletes = users1.deletes <> users2.deletes + } + +instance Monoid TestUsersByOperations where + mempty = TestUsersByOperations {updates = mempty, deletes = mempty} + +instance ToJSON TestUsersByOperations + +data TestUserList = TestUserList + { scimUsersWithRichInfo :: TestScimUsers, + scimUsersWithoutRichInfo :: TestScimUsers, + pendingScimUsers :: TestScimUsers, + ssoUsers :: TestTeamUsers, + passwordTeamUsers :: TestTeamUsers, + personalUsersWithoutHandle :: Map String (Value, Maybe String), + personalUsersWithHandle :: Map String (Value, Maybe String), + botsInTeamConvs :: TestTeamUsers, + -- UserId -> (User, Conv) + botsInPersonalConvs :: Map String (Value, Value) + } + deriving (Show, Eq) + +data TestScimUsers = TestScimUsers + { owner :: Value, + token :: String, + -- | ScimUser, Password, UserOrInv + users :: Map String (Value, String, Value) + } + deriving (Show, Eq) + +data TestTeamUsers = TestTeamUsers + { owner :: Value, + -- | (user, maybe password) + users :: Map String (Value, Maybe String) + } + deriving (Show, Eq) + +instance ToJSON TestUserList where + toJSON userList = do + object + [ fromString "scimUsersWithRichInfo" .= Map.keys userList.scimUsersWithRichInfo.users, + fromString "scimUsersWithoutRichInfo" .= Map.keys userList.scimUsersWithoutRichInfo.users, + fromString "pendingScimUsers" .= Map.keys userList.pendingScimUsers.users, + fromString "ssoUsers" .= Map.keys userList.ssoUsers.users, + fromString "passwordTeamUsers" .= Map.keys userList.passwordTeamUsers.users, + fromString "personalUsersWithoutHandle" .= Map.keys userList.personalUsersWithoutHandle, + fromString "personalUsersWithHandle" .= Map.keys userList.personalUsersWithHandle, + fromString "botsInTeamConvs" .= Map.keys userList.botsInTeamConvs.users, + fromString "botsInPersonalConvs" .= Map.keys userList.botsInPersonalConvs + ] + +data UserUpdate + = UpdateName String + | UpdateHandle String + | UpdateEmail String + | UpdatePassword String + deriving (Show, Eq, Generic) + +instance Arbitrary UserUpdate where + arbitrary = + oneof + [ UpdateName <$> arbitraryName, + UpdateHandle <$> arbitraryHandle, + UpdateEmail <$> arbitraryEmail, + UpdatePassword <$> arbitraryPassword + ] + +instance ToJSON UserUpdate + +arbitraryNonPasswordUpdate :: Gen UserUpdate +arbitraryNonPasswordUpdate = + oneof + [ UpdateName <$> arbitraryName, + UpdateHandle <$> arbitraryHandle, + UpdateEmail <$> arbitraryEmail + ] + +data PendingScimUpdate + = RegisterPendingScimUser + | UpdatePendingScimUser UserUpdate + deriving (Show, Eq, Generic) + +instance Arbitrary PendingScimUpdate where + arbitrary = + oneof + [ pure RegisterPendingScimUser, + UpdatePendingScimUser <$> arbitraryNonPasswordUpdate + ] + +instance ToJSON PendingScimUpdate + +data PhaseUpdates = PhaseUpdates + { scimUsersWithRichInfo :: [UserUpdate], + scimUsersWithoutRichInfo :: [UserUpdate], + pendingScimUsers :: [PendingScimUpdate], + ssoUsers :: [UserUpdate], + passwordTeamUsers :: [UserUpdate], + personalUsersWithoutHandle :: [UserUpdate], + personalUsersWithHandle :: [UserUpdate] + } + deriving (Show, Eq, Generic) + +instance ToJSON PhaseUpdates + +arbitraryPhaseUpdates :: Int -> Gen PhaseUpdates +arbitraryPhaseUpdates n = do + scimUsersWithRichInfo <- replicateM n arbitrary + scimUsersWithoutRichInfo <- replicateM n arbitrary + pendingScimUsers <- replicateM n arbitrary + ssoUsers <- replicateM n arbitraryNonPasswordUpdate + passwordTeamUsers <- replicateM n arbitrary + personalUsersWithoutHandle <- replicateM n arbitrary + personalUsersWithHandle <- replicateM n arbitrary + pure PhaseUpdates {..} + +userMigrationFinishedCounterName :: String +userMigrationFinishedCounterName = "^wire_users_migration_finished" + +commonOverrides, phase1Overrides, phase2Overrides, phase3Overrides, phase4Overrides, phase5Overrides :: ServiceOverrides +commonOverrides = + def + { brigCfg = + setField @_ @Int "optSettings.setUserMaxConnections" 500 + >=> setField @_ @Int "optSettings.setActivationTimeout" 3600 + >=> setField @_ @Int "optSettings.setVerificationTimeout" 3600 + >=> setField @_ @Int "optSettings.setTeamInvitationTimeout" 3600 + >=> setField @_ @Int "optSettings.setUserCookieRenewAge" 1209600 + >=> setField @_ @Int "postgresqlPool.size" 200 + >=> removeField "optSettings.setSuspendInactiveUsers" + } +phase1Overrides = + commonOverrides + <> def + { brigCfg = setField "postgresMigration.user" "cassandra", + galleyCfg = setField "postgresMigration.user" "cassandra", + backgroundWorkerCfg = + setField "postgresMigration.user" "cassandra" + >=> setField "migrateUsers" False + } +phase2Overrides = + commonOverrides + <> def + { brigCfg = setField "postgresMigration.user" "migration-to-postgresql", + galleyCfg = setField "postgresMigration.user" "migration-to-postgresql", + backgroundWorkerCfg = + setField "postgresMigration.user" "migration-to-postgresql" + >=> setField "migrateUsers" False + } +phase3Overrides = + commonOverrides + <> def + { brigCfg = setField "postgresMigration.user" "migration-to-postgresql", + galleyCfg = setField "postgresMigration.user" "migration-to-postgresql", + backgroundWorkerCfg = + setField "postgresMigration.user" "migration-to-postgresql" + >=> setField "migrateUsers" True + } +phase4Overrides = + commonOverrides + <> def + { brigCfg = setField "postgresMigration.user" "migration-to-postgresql", + galleyCfg = setField "postgresMigration.user" "migration-to-postgresql", + backgroundWorkerCfg = + setField "postgresMigration.user" "migration-to-postgresql" + >=> setField "migrateUsers" False + } +phase5Overrides = + commonOverrides + <> def + { brigCfg = setField "postgresMigration.user" "postgresql", + galleyCfg = setField "postgresMigration.user" "postgresql", + backgroundWorkerCfg = + setField "postgresMigration.user" "postgresql" + >=> setField "migrateUsers" False + } + +phaseOverrides :: IntMap ServiceOverrides +phaseOverrides = + IntMap.fromList + [ (1, phase1Overrides), + (2, phase2Overrides), + (3, phase3Overrides), + (4, phase4Overrides), + (5, phase5Overrides) + ] diff --git a/integration/test/Test/Migration/Util.hs b/integration/test/Test/Migration/Util.hs index ba3a116b453..28d1d4712ac 100644 --- a/integration/test/Test/Migration/Util.hs +++ b/integration/test/Test/Migration/Util.hs @@ -27,14 +27,30 @@ import GHC.Stack import SetupHelpers hiding (deleteUser) import Testlib.Prelude import Text.Regex.TDFA ((=~)) +import UnliftIO waitForMigration :: (HasCallStack) => String -> String -> App () -waitForMigration domain name = do - metrics <- - getMetrics domain BackgroundWorker `bindResponse` \resp -> do - resp.status `shouldMatchInt` 200 - pure $ Text.decodeUtf8 resp.body - let (_, _, _, finishedMatches) :: (Text, Text, Text, [Text]) = (metrics =~ Text.pack (name <> "\\ ([0-9]+\\.[0-9]+)$")) - when (finishedMatches /= [Text.pack "1.0"]) $ do - liftIO $ threadDelay 100_000 - waitForMigration domain name +waitForMigration domain metricName = + maybe failWithContext pure =<< timeout 30_000_000 go + where + failWithContext = do + getMetrics domain BackgroundWorker `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + assertFailure "Timed out waiting for postgresql migration" + go = do + metrics <- + getMetrics domain BackgroundWorker `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + pure $ Text.decodeUtf8 resp.body + let (_, _, _, finishedMatches) :: (Text, Text, Text, [Text]) = (metrics =~ Text.pack (metricName <> "\\ ([0-9]+\\.[0-9]+)$")) + when (finishedMatches /= [Text.pack "1.0"]) $ do + liftIO $ threadDelay 100_000 + go + +assertMigrationSuccessful :: (HasCallStack) => String -> String -> App () +assertMigrationSuccessful domain failedMetricName = do + getMetrics domain BackgroundWorker `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + let metrics = Text.decodeUtf8 resp.body + (_, _, _, failedMatches) :: (Text, Text, Text, [Text]) = (metrics =~ Text.pack (failedMetricName <> "\\ ([0-9]+\\.[0-9]+)$")) + failedMatches `shouldMatch` [Text.pack "0.0"] diff --git a/integration/test/Test/Search.hs b/integration/test/Test/Search.hs index a0986828555..b4b3edb4b79 100644 --- a/integration/test/Test/Search.hs +++ b/integration/test/Test/Search.hs @@ -693,7 +693,7 @@ testReindexAllUsers = do assertCannotFind alice user (user %. "name") domain -- Reindex users using a small page size so pagination gets excersiced - reindexUsers testBackend 5 + reindexUsers testBackend def 5 BrigI.refreshIndex domain -- Now things should work as expected diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index dd37e9c8f56..6c0a774953c 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -96,14 +96,14 @@ mkGlobalEnv cfgFile = do & Cassandra.setContacts intConfig.cassandra.cassHost [] & Cassandra.setPortNumber (fromIntegral intConfig.cassandra.cassPort) cassSettings = maybe basicCassSettings (\sslCtx -> Cassandra.setSSLContext sslCtx basicCassSettings) mbSSLContext - cassClient <- Cassandra.init cassSettings + gCassClient <- Cassandra.init cassSettings let resources = backendResources (Map.elems intConfig.dynamicBackends) resourcePool <- liftIO $ createBackendResourcePool resources intConfig.rabbitmq - cassClient + gCassClient let sm = Map.fromList $ [ (intConfig.backendOne.originDomain, intConfig.backendOne.beServiceMap), @@ -146,7 +146,8 @@ mkGlobalEnv cfgFile = do gDNSMockServerConfig = intConfig.dnsMockServer, gCellsEventQueue = intConfig.cellsEventQueue, gCellsEventWatchersLock, - gCellsEventWatchers + gCellsEventWatchers, + gCassClient } where createSSLContext :: Maybe FilePath -> IO (Maybe OpenSSL.SSLContext) @@ -202,6 +203,7 @@ mkEnv currentTestName ge = do cellsEventQueue = ge.gCellsEventQueue, cellsEventWatchersLock = ge.gCellsEventWatchersLock, cellsEventWatchers = ge.gCellsEventWatchers, + cassClient = ge.gCassClient, curlTrace } diff --git a/integration/test/Testlib/JSON.hs b/integration/test/Testlib/JSON.hs index 04ae74ef192..b02402cc27a 100644 --- a/integration/test/Testlib/JSON.hs +++ b/integration/test/Testlib/JSON.hs @@ -26,6 +26,7 @@ import Data.Aeson hiding ((.=)) import qualified Data.Aeson as Aeson import qualified Data.Aeson.Encode.Pretty as Aeson import qualified Data.Aeson.Key as KM +import Data.Aeson.KeyMap (KeyMap) import qualified Data.Aeson.KeyMap as KM import qualified Data.Aeson.Types as Aeson import Data.ByteString (ByteString) @@ -320,6 +321,11 @@ modifyField selector up x = do ob <- asObject v pure $ Object $ KM.insert (KM.fromString k) newValue ob +modifyObject :: (HasCallStack, MakesValue a) => (KeyMap Value -> KeyMap Value) -> a -> App Value +modifyObject f x = do + ob <- asObject x + pure . Object $ f ob + -- | `removeField "a.b" {"a": {"b": 3}, "c": true} == {"a": {}, "c": true}` removeField :: (HasCallStack, MakesValue a) => String -> a -> App Value removeField selector x = do diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index 638c6a65279..2919803d459 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -55,6 +55,7 @@ import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Time import Data.Word +import qualified Database.CQL.IO as Cassandra import GHC.Generics (Generic) import GHC.Records import GHC.Stack @@ -147,7 +148,8 @@ data GlobalEnv = GlobalEnv gDNSMockServerConfig :: DNSMockServerConfig, gCellsEventQueue :: String, gCellsEventWatchersLock :: MVar (), - gCellsEventWatchers :: IORef (Map String QueueWatcher) + gCellsEventWatchers :: IORef (Map String QueueWatcher), + gCassClient :: Cassandra.ClientState } data IntegrationConfig = IntegrationConfig @@ -276,7 +278,8 @@ data Env = Env cellsEventQueue :: String, cellsEventWatchersLock :: MVar (), cellsEventWatchers :: IORef (Map String QueueWatcher), - curlTrace :: IORef [String] + curlTrace :: IORef [String], + cassClient :: Cassandra.ClientState } data Response = Response @@ -488,6 +491,18 @@ newtype App a = App {unApp :: ReaderT Env IO a} instance MonadRandom App where getRandomBytes n = liftIO (getRandomBytes n) +instance Cassandra.MonadClient App where + liftClient :: Cassandra.Client a -> App a + liftClient action = do + clientState <- asks (.cassClient) + liftIO $ Cassandra.runClient clientState action + + localState :: (Cassandra.ClientState -> Cassandra.ClientState) -> App a -> App a + localState f action = do + env <- ask + let newClientState = f env.cassClient + liftIO $ runAppWithEnv (env {cassClient = newClientState}) action + runAppWithEnv :: Env -> App a -> IO a runAppWithEnv e m = runReaderT (unApp m) e diff --git a/libs/wire-api/src/Wire/API/PostgresMarshall.hs b/libs/wire-api/src/Wire/API/PostgresMarshall.hs index e1a6f55f18d..24ff507ec27 100644 --- a/libs/wire-api/src/Wire/API/PostgresMarshall.hs +++ b/libs/wire-api/src/Wire/API/PostgresMarshall.hs @@ -521,6 +521,9 @@ instance (PostgresMarshall a1 b1, PostgresMarshall a2 b2, PostgresMarshall a3 b3 instance (PostgresMarshall a1 b1, PostgresMarshall a2 b2, PostgresMarshall a3 b3, PostgresMarshall a4 b4, PostgresMarshall a5 b5, PostgresMarshall a6 b6, PostgresMarshall a7 b7, PostgresMarshall a8 b8, PostgresMarshall a9 b9, PostgresMarshall a10 b10, PostgresMarshall a11 b11, PostgresMarshall a12 b12, PostgresMarshall a13 b13, PostgresMarshall a14 b14, PostgresMarshall a15 b15, PostgresMarshall a16 b16, PostgresMarshall a17 b17, PostgresMarshall a18 b18, PostgresMarshall a19 b19, PostgresMarshall a20 b20, PostgresMarshall a21 b21, PostgresMarshall a22 b22, PostgresMarshall a23 b23, PostgresMarshall a24 b24) => PostgresMarshall (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24) (b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19, b20, b21, b22, b23, b24) where postgresMarshall (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24) = (postgresMarshall a1, postgresMarshall a2, postgresMarshall a3, postgresMarshall a4, postgresMarshall a5, postgresMarshall a6, postgresMarshall a7, postgresMarshall a8, postgresMarshall a9, postgresMarshall a10, postgresMarshall a11, postgresMarshall a12, postgresMarshall a13, postgresMarshall a14, postgresMarshall a15, postgresMarshall a16, postgresMarshall a17, postgresMarshall a18, postgresMarshall a19, postgresMarshall a20, postgresMarshall a21, postgresMarshall a22, postgresMarshall a23, postgresMarshall a24) +instance (PostgresMarshall a1 b1, PostgresMarshall a2 b2, PostgresMarshall a3 b3, PostgresMarshall a4 b4, PostgresMarshall a5 b5, PostgresMarshall a6 b6, PostgresMarshall a7 b7, PostgresMarshall a8 b8, PostgresMarshall a9 b9, PostgresMarshall a10 b10, PostgresMarshall a11 b11, PostgresMarshall a12 b12, PostgresMarshall a13 b13, PostgresMarshall a14 b14, PostgresMarshall a15 b15, PostgresMarshall a16 b16, PostgresMarshall a17 b17, PostgresMarshall a18 b18, PostgresMarshall a19 b19, PostgresMarshall a20 b20, PostgresMarshall a21 b21, PostgresMarshall a22 b22, PostgresMarshall a23 b23, PostgresMarshall a24 b24, PostgresMarshall a25 b25) => PostgresMarshall (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25) (b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25) where + postgresMarshall (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25) = (postgresMarshall a1, postgresMarshall a2, postgresMarshall a3, postgresMarshall a4, postgresMarshall a5, postgresMarshall a6, postgresMarshall a7, postgresMarshall a8, postgresMarshall a9, postgresMarshall a10, postgresMarshall a11, postgresMarshall a12, postgresMarshall a13, postgresMarshall a14, postgresMarshall a15, postgresMarshall a16, postgresMarshall a17, postgresMarshall a18, postgresMarshall a19, postgresMarshall a20, postgresMarshall a21, postgresMarshall a22, postgresMarshall a23, postgresMarshall a24, postgresMarshall a25) + instance PostgresMarshall UUID (Id a) where postgresMarshall = toUUID @@ -989,6 +992,35 @@ instance (PostgresUnmarshall a1 b1, PostgresUnmarshall a2 b2, PostgresUnmarshall <*> postgresUnmarshall a23 <*> postgresUnmarshall a24 +instance (PostgresUnmarshall a1 b1, PostgresUnmarshall a2 b2, PostgresUnmarshall a3 b3, PostgresUnmarshall a4 b4, PostgresUnmarshall a5 b5, PostgresUnmarshall a6 b6, PostgresUnmarshall a7 b7, PostgresUnmarshall a8 b8, PostgresUnmarshall a9 b9, PostgresUnmarshall a10 b10, PostgresUnmarshall a11 b11, PostgresUnmarshall a12 b12, PostgresUnmarshall a13 b13, PostgresUnmarshall a14 b14, PostgresUnmarshall a15 b15, PostgresUnmarshall a16 b16, PostgresUnmarshall a17 b17, PostgresUnmarshall a18 b18, PostgresUnmarshall a19 b19, PostgresUnmarshall a20 b20, PostgresUnmarshall a21 b21, PostgresUnmarshall a22 b22, PostgresUnmarshall a23 b23, PostgresUnmarshall a24 b24, PostgresUnmarshall a25 b25) => PostgresUnmarshall (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25) (b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25) where + postgresUnmarshall (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25) = + (,,,,,,,,,,,,,,,,,,,,,,,,) + <$> postgresUnmarshall a1 + <*> postgresUnmarshall a2 + <*> postgresUnmarshall a3 + <*> postgresUnmarshall a4 + <*> postgresUnmarshall a5 + <*> postgresUnmarshall a6 + <*> postgresUnmarshall a7 + <*> postgresUnmarshall a8 + <*> postgresUnmarshall a9 + <*> postgresUnmarshall a10 + <*> postgresUnmarshall a11 + <*> postgresUnmarshall a12 + <*> postgresUnmarshall a13 + <*> postgresUnmarshall a14 + <*> postgresUnmarshall a15 + <*> postgresUnmarshall a16 + <*> postgresUnmarshall a17 + <*> postgresUnmarshall a18 + <*> postgresUnmarshall a19 + <*> postgresUnmarshall a20 + <*> postgresUnmarshall a21 + <*> postgresUnmarshall a22 + <*> postgresUnmarshall a23 + <*> postgresUnmarshall a24 + <*> postgresUnmarshall a25 + instance PostgresUnmarshall UUID (Id a) where postgresUnmarshall = Right . Id diff --git a/libs/wire-subsystems/postgres-migrations/20260625104559-add-user-migration-pending-deletes.sql b/libs/wire-subsystems/postgres-migrations/20260625104559-add-user-migration-pending-deletes.sql new file mode 100644 index 00000000000..21143117419 --- /dev/null +++ b/libs/wire-subsystems/postgres-migrations/20260625104559-add-user-migration-pending-deletes.sql @@ -0,0 +1,3 @@ +CREATE TABLE user_migration_pending_deletes ( + id uuid PRIMARY KEY + ); diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Migration.hs b/libs/wire-subsystems/src/Wire/CodeStore/Migration.hs index e652c5619a3..49062ebe50f 100644 --- a/libs/wire-subsystems/src/Wire/CodeStore/Migration.hs +++ b/libs/wire-subsystems/src/Wire/CodeStore/Migration.hs @@ -32,12 +32,12 @@ import Hasql.Pool.Extended qualified as Hasql import Imports import Polysemy import Polysemy.Async +import Polysemy.AtomicState import Polysemy.Conc (interpretRace) import Polysemy.Conc qualified as Conc import Polysemy.Conc.Effect.Race hiding (Timeout) import Polysemy.Input import Polysemy.Resource (Resource, bracket, resourceToIOFinal) -import Polysemy.State import Polysemy.TinyLog import Prometheus qualified import System.Logger qualified as Log @@ -53,7 +53,7 @@ import Wire.Sem.Logger (mapLogger) import Wire.Sem.Logger.TinyLog (loggerToTinyLog) type EffectStack = - [ State Int, + [ AtomicState Int, Input ClientState, Input Hasql.Pool, Input (Either HttpsUrl (Map Domain HttpsUrl)), @@ -97,7 +97,7 @@ interpreter cassClient pgPool logger name = . runInputConst (Right mempty) . runInputConst pgPool . runInputConst cassClient - . runState 0 + . atomicStateToIO 0 migrateAllCodes :: ( Member (Input Hasql.Pool) r, @@ -105,7 +105,7 @@ migrateAllCodes :: Member (Embed IO) r, Member (Input ClientState) r, Member TinyLog r, - Member (State Int) r, + Member (AtomicState Int) r, Member Resource r, Member Race r ) => diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs index 8c5ddc11bd7..60ab456464e 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs @@ -43,11 +43,11 @@ import Hasql.Transaction.Sessions import Imports import Polysemy import Polysemy.Async +import Polysemy.AtomicState import Polysemy.Conc hiding (timeout_) import Polysemy.Error import Polysemy.Input import Polysemy.Resource (Resource, resourceToIOFinal) -import Polysemy.State import Polysemy.TinyLog import Prometheus qualified import System.Logger qualified as Log @@ -81,7 +81,7 @@ import Wire.StoredConversation -- * Top level logic type EffectStack = - [ State Int, + [ AtomicState Int, Input ClientState, Input Hasql.Pool, Resource, @@ -144,7 +144,7 @@ interpreter cassClient pgPool logger name = . resourceToIOFinal . runInputConst pgPool . runInputConst cassClient - . runState 0 + . atomicStateToIO 0 migrateAllConversations :: ( Member (Input Hasql.Pool) r, @@ -154,7 +154,7 @@ migrateAllConversations :: Member Async r, Member Race r, Member Resource r, - Member (State Int) r, + Member (AtomicState Int) r, Member (Concurrency Unsafe) r ) => MigrationOptions -> @@ -181,7 +181,7 @@ migrateAllUsers :: Member Async r, Member Race r, Member Resource r, - Member (State Int) r, + Member (AtomicState Int) r, Member (Concurrency 'Unsafe) r ) => MigrationOptions -> @@ -197,11 +197,11 @@ migrateAllUsers migOpts migCounter migDuration = do select :: PrepQuery R () (Identity UserId) select = "select distinct user from user_remote_conv" -handleErrors :: (Member (State Int) r, Member TinyLog r) => (Id a -> Sem (Error MigrationLockError : Error UsageError : r) b) -> ByteString -> Id a -> Sem r (Maybe b) +handleErrors :: (Member (AtomicState Int) r, Member TinyLog r) => (Id a -> Sem (Error MigrationLockError : Error UsageError : r) b) -> ByteString -> Id a -> Sem r (Maybe b) handleErrors action lockType id_ = join <$> handleError (handleError action lockType) lockType id_ -handleError :: (Member (State Int) r, Member TinyLog r, Show e) => (Id a -> Sem (Error e : r) b) -> ByteString -> Id a -> Sem r (Maybe b) +handleError :: (Member (AtomicState Int) r, Member TinyLog r, Show e) => (Id a -> Sem (Error e : r) b) -> ByteString -> Id a -> Sem r (Maybe b) handleError action lockType id_ = do eithErr <- runError (action id_) case eithErr of @@ -211,7 +211,7 @@ handleError action lockType id_ = do Log.msg (Log.val "error occurred during migration") . Log.field lockType (idToText id_) . Log.field "error" (show e) - modify (+ 1) + atomicModify (+ 1) pure Nothing -- * Conversations diff --git a/libs/wire-subsystems/src/Wire/DomainRegistrationStore/Migration.hs b/libs/wire-subsystems/src/Wire/DomainRegistrationStore/Migration.hs index 5047d4b61e2..bd29da928f4 100644 --- a/libs/wire-subsystems/src/Wire/DomainRegistrationStore/Migration.hs +++ b/libs/wire-subsystems/src/Wire/DomainRegistrationStore/Migration.hs @@ -27,17 +27,16 @@ import Data.Conduit.List qualified as C import Data.Domain import Data.Id import Database.CQL.Protocol (Record (asRecord), TupleType) -import Hasql.Pool (UsageError) import Hasql.Pool.Extended qualified as Hasql import Imports hiding (lookup) import Polysemy import Polysemy.Async +import Polysemy.AtomicState import Polysemy.Conc (interpretRace) import Polysemy.Conc.Effect.Race hiding (Timeout) import Polysemy.Error import Polysemy.Input import Polysemy.Resource (Resource, resourceToIOFinal) -import Polysemy.State import Polysemy.TinyLog import Prometheus qualified import System.Logger qualified as Log @@ -55,7 +54,7 @@ import Wire.Sem.Logger (mapLogger) import Wire.Sem.Logger.TinyLog (loggerToTinyLog) type EffectStack = - [ State Int, + [ AtomicState Int, Input ClientState, Input Hasql.Pool, Resource, @@ -97,14 +96,14 @@ interpreter cassClient pgPool logger name = . resourceToIOFinal . runInputConst pgPool . runInputConst cassClient - . runState 0 + . atomicStateToIO 0 migrateAllDomainRegistrations :: ( Member (Input Hasql.Pool) r, Member (Embed IO) r, Member (Input ClientState) r, Member TinyLog r, - Member (State Int) r, + Member (AtomicState Int) r, Member Async r, Member Race r, Member Resource r @@ -122,7 +121,7 @@ migrateAllDomainRegistrations migOpts migCounter migDuration = do lift $ info $ Log.msg (Log.val "migrateAllDomainRegistrations") withCount (paginateSem selectAllRegistrations (paramsP LocalQuorum () migOpts.pageSize) x5) .| logRetrievedPage migOpts.pageSize asRecord - .| C.mapM_ (traverse_ (\row -> handleRegistrationErrors (toByteString' (show row.domain)) (migrateDomainRegistrationRow migOpts migCounter migDuration row))) + .| C.mapM_ (traverse_ (\row -> handleLockAndDBErrors (toByteString' (show row.domain)) (migrateDomainRegistrationRow migOpts migCounter migDuration row))) migrateDomainRegistrationRow :: ( PGConstraints r, @@ -175,24 +174,3 @@ selectAllRegistrations = selectAllChallenges :: PrepQuery R () (ChallengeId, Domain, Token, DnsVerificationToken, Int32) selectAllChallenges = "SELECT id, domain, challenge_token_hash, dns_verification_token, ttl(challenge_token_hash) FROM domain_registration_challenge" - -handleRegistrationErrors :: - ( Member (State Int) r, - Member TinyLog r - ) => - ByteString -> - (Sem (Error MigrationLockError : Error UsageError : r) ()) -> - Sem r () -handleRegistrationErrors key action = do - eithErr <- runError (runError action) - case eithErr of - Right (Right _) -> pure () - Right (Left e) -> logError (show e) - Left e -> logError (show e) - where - logError e = do - warn $ - Log.msg (Log.val "error occurred during migration") - . Log.field "key" (show key) - . Log.field "error" e - modify (+ 1) diff --git a/libs/wire-subsystems/src/Wire/Migration.hs b/libs/wire-subsystems/src/Wire/Migration.hs index 325910448ba..b15edbfeb47 100644 --- a/libs/wire-subsystems/src/Wire/Migration.hs +++ b/libs/wire-subsystems/src/Wire/Migration.hs @@ -32,12 +32,12 @@ import Hasql.Pool qualified as Hasql import Imports import Polysemy import Polysemy.Async +import Polysemy.AtomicState import Polysemy.Conc hiding (timeout_) import Polysemy.Conc qualified as Conc import Polysemy.Error import Polysemy.Input import Polysemy.Resource -import Polysemy.State import Polysemy.Time import Polysemy.TinyLog import Prometheus qualified @@ -152,7 +152,7 @@ paginateSem q p r = do handleErrors :: forall r. - ( Member (State Int) r, + ( Member (AtomicState Int) r, Member TinyLog r ) => ByteString -> @@ -167,7 +167,28 @@ handleErrors key action = do Log.msg (Log.val "error occurred during migration") . Log.field "key" (show key) . Log.field "error" (show e) - modify (+ 1) + atomicModify (+ 1) + +handleLockAndDBErrors :: + ( Member (AtomicState Int) r, + Member TinyLog r + ) => + ByteString -> + (Sem (Error MigrationLockError : Error Hasql.UsageError : r) ()) -> + Sem r () +handleLockAndDBErrors key action = do + eithErr <- runError (runError action) + case eithErr of + Right (Right _) -> pure () + Right (Left e) -> logError (show e) + Left e -> logError (show e) + where + logError e = do + warn $ + Log.msg (Log.val "error occurred during migration") + . Log.field "key" (show key) + . Log.field "error" e + atomicModify (+ 1) withExclusiveMigrationLockAndTimeout :: forall x r. diff --git a/libs/wire-subsystems/src/Wire/MigrationLock.hs b/libs/wire-subsystems/src/Wire/MigrationLock.hs index 8c97876170f..9befa82247c 100644 --- a/libs/wire-subsystems/src/Wire/MigrationLock.hs +++ b/libs/wire-subsystems/src/Wire/MigrationLock.hs @@ -86,6 +86,8 @@ data MigrationLockError = TimedOutAcquiringLock instance APIError MigrationLockError where toResponse = waiErrorToJSONResponse . migrationLockErrorToWai +instance Exception MigrationLockError + migrationLockErrorToHttpError :: MigrationLockError -> HttpError migrationLockErrorToHttpError = StdError . migrationLockErrorToWai @@ -117,7 +119,8 @@ withMigrationLocks lockType maxWait lockables action = do pool <- (.rawPool) <$> input @HasqlPoolExt.Pool lockThread <- async . embed . Hasql.use pool $ do - let lockIds = fmap lockKey lockables + -- Sort lockIds to avoid deadlocks + let lockIds = sort $ fmap lockKey lockables Session.statement lockIds acquireLocks liftIO $ putMVar lockAcquired () diff --git a/libs/wire-subsystems/src/Wire/TeamFeatureStore/Migration.hs b/libs/wire-subsystems/src/Wire/TeamFeatureStore/Migration.hs index 1edeb7daf2c..048193e3f16 100644 --- a/libs/wire-subsystems/src/Wire/TeamFeatureStore/Migration.hs +++ b/libs/wire-subsystems/src/Wire/TeamFeatureStore/Migration.hs @@ -27,11 +27,11 @@ import Hasql.Pool.Extended qualified as Hasql import Imports import Polysemy import Polysemy.Async +import Polysemy.AtomicState import Polysemy.Conc import Polysemy.Error import Polysemy.Input import Polysemy.Resource (Resource, resourceToIOFinal) -import Polysemy.State import Polysemy.TinyLog import Prometheus qualified import System.Logger qualified as Log @@ -49,7 +49,7 @@ migrateAllTeamFeatures :: Member (Embed IO) r, Member (Input ClientState) r, Member TinyLog r, - Member (State Int) r, + Member (AtomicState Int) r, Member Async r, Member Race r, Member Resource r @@ -65,7 +65,7 @@ migrateAllTeamFeatures migOpts migCounter migDuration = do .| C.mapM_ (traverse_ (\row@(tid, feat, _, _, _) -> handleErrors (toByteString' (idToText tid <> " - " <> feat)) (migrateTeamFeature migOpts migCounter migDuration row))) type EffectStack = - [ State Int, + [ AtomicState Int, Input ClientState, Input Hasql.Pool, Resource, @@ -107,7 +107,7 @@ interpreter cassClient pgPool logger name = . resourceToIOFinal . runInputConst pgPool . runInputConst cassClient - . runState 0 + . atomicStateToIO 0 migrateTeamFeature :: ( PGConstraints r, @@ -134,7 +134,7 @@ migrateTeamFeature migOpts migCounter migDuration (tid, name, status, lockStatus liftIO $ Prometheus.incCounter migCounter handleErrors :: - ( Member (State Int) r, + ( Member (AtomicState Int) r, Member TinyLog r ) => ByteString -> @@ -149,10 +149,10 @@ handleErrors key action = do Log.msg (Log.val "error occurred during migration") . Log.field "key" (show key) . Log.field "error" (show e) - modify (+ 1) + atomicModify (+ 1) Left e -> do warn $ Log.msg (Log.val "error occurred during migration") . Log.field "key" (show key) . Log.field "error" (show e) - modify (+ 1) + atomicModify (+ 1) diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index 6226347fece..54fefc2d485 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -15,27 +15,42 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.UserStore.Cassandra (interpretUserStoreCassandra) where +module Wire.UserStore.Cassandra + ( interpretUserStoreCassandra, + interpretUserStoreToCassandraAndPostgres, + ) +where import Cassandra import Cassandra.Exec (prepared) import Control.Lens ((^.)) import Data.Handle import Data.Id -import Database.CQL.Protocol +import Data.Map qualified as Map +import Data.UUID qualified as UUID +import Database.CQL.Protocol hiding (Error) import Imports import Polysemy +import Polysemy.Async (Async) +import Polysemy.Conc (Race) import Polysemy.Embed import Polysemy.Error +import Polysemy.Resource (Resource) +import Polysemy.Time +import Polysemy.TinyLog (TinyLog) import Wire.API.Password (Password) import Wire.API.Provider.Service import Wire.API.Team.Feature (FeatureStatus) import Wire.API.User hiding (DeleteUser) import Wire.API.User.RichInfo import Wire.API.User.Search (SetSearchable (SetSearchable)) +import Wire.MigrationLock +import Wire.Postgres (PGConstraints) import Wire.StoredUser import Wire.UserStore +import Wire.UserStore qualified as UserStore import Wire.UserStore.IndexUser hiding (userId) +import Wire.UserStore.Postgres (interpretUserStorePostgres) import Wire.UserStore.Unique interpretUserStoreCassandra :: (Member (Embed IO) r) => ClientState -> InterpreterFor UserStore r @@ -79,6 +94,197 @@ interpretUserStoreCassandra casClient = LookupServiceUsers pid sid mPagingState -> lookupServiceUsersImpl pid sid (paginationStateCassandra =<< mPagingState) LookupServiceUsersForTeam pid sid tid mPagingState -> lookupServiceUsersForTeamImpl pid sid tid (paginationStateCassandra =<< mPagingState) +interpretUserStoreToCassandraAndPostgres :: + ( PGConstraints r, + Member Async r, + Member TinyLog r, + Member Race r, + Member Resource r, + Member (Error MigrationLockError) r + ) => + ClientState -> InterpreterFor UserStore r +interpretUserStoreToCassandraAndPostgres casClient = + interpret $ \case + CreateUser new mbConv -> do + -- Store new users in postgresql + withMigrationLocks LockShared (MilliSeconds 500) [new.id] $ do + isUserInCass <- interpretUserStoreCassandra casClient $ UserStore.doesUserExist new.id + if isUserInCass + then interpretUserStoreCassandra casClient $ UserStore.createUser new mbConv + else interpretUserStorePostgres $ UserStore.createUser new mbConv + ActivateUser uid identity -> + runAppropriateInterpreter casClient uid $ UserStore.activateUser uid identity + DeactivateUser uid -> + runAppropriateInterpreter casClient uid $ UserStore.deactivateUser uid + GetUsers uids -> + withMigrationLocks LockShared (Seconds 2) uids $ do + let indexByUserId = foldr (\storedUser -> Map.insert storedUser.id storedUser) Map.empty + cassUsers <- indexByUserId <$> interpretUserStoreCassandra casClient (UserStore.getUsers uids) + pgUsers <- indexByUserId <$> interpretUserStorePostgres (UserStore.getUsers uids) + pure $ mapMaybe (\uid -> Map.lookup uid pgUsers <|> Map.lookup uid cassUsers) uids + DoesUserExist uid -> do + withMigrationLocks LockShared (MilliSeconds 500) [uid] $ do + isUserInPg <- interpretUserStorePostgres $ UserStore.doesUserExist uid + if isUserInPg + then pure True + else interpretUserStoreCassandra casClient $ UserStore.doesUserExist uid + GetIndexUser uid -> + runAppropriateInterpreter casClient uid $ UserStore.getIndexUser uid + GetIndexUsersPaginated pageSize mPagingState -> do + paginateOverCassandraAndPostgres + (\size state -> interpretUserStoreCassandra casClient $ UserStore.getIndexUsersPaginated size state) + (\size state -> interpretUserStorePostgres $ UserStore.getIndexUsersPaginated size state) + (PagingExitingUsers $ Id UUID.nil) + pageSize + mPagingState + UpdateUser uid update -> + runAppropriateInterpreter casClient uid $ UserStore.updateUser uid update + UpdateEmail uid email -> + runAppropriateInterpreter casClient uid $ UserStore.updateEmail uid email + DeleteEmail uid -> + runAppropriateInterpreter casClient uid $ UserStore.deleteEmail uid + UpdateEmailUnvalidated uid email -> + runAppropriateInterpreter casClient uid $ UserStore.updateEmailUnvalidated uid email + DeleteEmailUnvalidated uid -> + runAppropriateInterpreter casClient uid $ UserStore.deleteEmailUnvalidated uid + LookupName uid -> + runAppropriateInterpreter casClient uid $ UserStore.lookupName uid + LookupHandle hdl -> do + let action = UserStore.lookupHandle hdl + interpretUserStorePostgres action >>= \case + Nothing -> interpretUserStoreCassandra casClient action + Just user -> pure $ Just user + GlimpseHandle hdl -> do + let action = UserStore.glimpseHandle hdl + interpretUserStorePostgres action >>= \case + Nothing -> interpretUserStoreCassandra casClient action + Just uid -> pure $ Just uid + UpdateUserHandleEither uid update -> do + -- There is no easy way to handle the race condition that Alice in + -- Cassandra and Bob in Postgresql don't claim the same handle. If they + -- race to claim a handle, they _can_ both succeed. In this case, the + -- migration code _could_ fail to migrate the Alice user, so it has to be + -- careful about handling this case. + withMigrationLocks LockShared (MilliSeconds 500) [uid] $ do + let glimpseAction = UserStore.glimpseHandle update.new + cassGlimpse <- interpretUserStoreCassandra casClient glimpseAction + pgGlimpse <- interpretUserStorePostgres glimpseAction + case (cassGlimpse, pgGlimpse) of + (_, Just pgClaimer) + | pgClaimer == uid -> pure $ Right () + | otherwise -> pure $ Left StoredUserUpdateHandleExists + (Just casClaimer, Nothing) + | casClaimer == uid -> pure $ Right () + | otherwise -> pure $ Left StoredUserUpdateHandleExists + (Nothing, Nothing) -> do + isUserInPg <- interpretUserStorePostgres $ UserStore.doesUserExist uid + let action = UserStore.updateUserHandleEither uid update + if isUserInPg + then interpretUserStorePostgres action + else interpretUserStoreCassandra casClient action + UpdateSSOId uid ssoId -> + runAppropriateInterpreter casClient uid $ UserStore.updateSSOId uid ssoId + UpdateManagedBy uid managedBy -> + runAppropriateInterpreter casClient uid $ UserStore.updateManagedBy uid managedBy + UpdateAccountStatus uid accountStatus -> + runAppropriateInterpreter casClient uid $ UserStore.updateAccountStatus uid accountStatus + UpdateRichInfo uid richInfo -> + runAppropriateInterpreter casClient uid $ UserStore.updateRichInfo uid richInfo + UpdateFeatureConferenceCalling uid feat -> + runAppropriateInterpreter casClient uid $ UserStore.updateFeatureConferenceCalling uid feat + LookupFeatureConferenceCalling uid -> + runAppropriateInterpreter casClient uid $ UserStore.lookupFeatureConferenceCalling uid + DeleteUser user -> + runAppropriateInterpreter casClient (userId user) $ UserStore.deleteUser user + LookupStatus uid -> + runAppropriateInterpreter casClient uid $ UserStore.lookupStatus uid + IsActivated uid -> + runAppropriateInterpreter casClient uid $ UserStore.isActivated uid + LookupLocale uid -> + runAppropriateInterpreter casClient uid $ UserStore.lookupLocale uid + GetUserTeam uid -> + runAppropriateInterpreter casClient uid $ UserStore.getUserTeam uid + UpdateUserTeam uid tid -> + runAppropriateInterpreter casClient uid $ UserStore.updateUserTeam uid tid + GetRichInfo uid -> + runAppropriateInterpreter casClient uid $ UserStore.getRichInfo uid + UpsertHashedPassword uid pw -> + runAppropriateInterpreter casClient uid $ UserStore.upsertHashedPassword uid pw + LookupHashedPassword uid -> + runAppropriateInterpreter casClient uid $ UserStore.lookupHashedPassword uid + GetUserAuthenticationInfo uid -> + runAppropriateInterpreter casClient uid $ UserStore.getUserAuthenticationInfo uid + SetUserSearchable uid searchable -> + runAppropriateInterpreter casClient uid $ UserStore.setUserSearchable uid searchable + DeleteServiceUser pid sid bid -> + runAppropriateInterpreter casClient (botUserId bid) $ UserStore.deleteServiceUser pid sid bid + LookupServiceUsers pid sid mPagingState -> + -- Ignoring the size paramter here makes us potentially return upto 199 + -- bots instead of 100, but this is ok as this is temporary and the + -- callers are not doing anything wrong with a longer list. + paginateOverCassandraAndPostgres + (\_size state -> interpretUserStoreCassandra casClient $ UserStore.lookupServiceUsers pid sid state) + (\_size state -> interpretUserStorePostgres $ UserStore.lookupServiceUsers pid sid state) + (BotId $ Id UUID.nil) + 100 + mPagingState + LookupServiceUsersForTeam pid sid tid mPagingState -> + -- Ignoring the size paramter here makes us potentially return upto 199 + -- bots instead of 100, but this is ok as this is temporary and the + -- callers are not doing anything wrong with a longer list. + paginateOverCassandraAndPostgres + (\_size state -> interpretUserStoreCassandra casClient $ UserStore.lookupServiceUsersForTeam pid sid tid state) + (\_size state -> interpretUserStorePostgres $ UserStore.lookupServiceUsersForTeam pid sid tid state) + (BotId $ Id UUID.nil) + 100 + mPagingState + +runAppropriateInterpreter :: + ( PGConstraints r, + Member TinyLog r, + Member (Error MigrationLockError) r, + Member Async r, + Member Race r, + Member Resource r + ) => + ClientState -> UserId -> InterpreterFor UserStore r +runAppropriateInterpreter casClient uid action = + withMigrationLocks LockShared (MilliSeconds 500) [uid] $ do + isUserInPg <- interpretUserStorePostgres $ UserStore.doesUserExist uid + if isUserInPg + then interpretUserStorePostgres action + else interpretUserStoreCassandra casClient action + +paginateOverCassandraAndPostgres :: + (Int32 -> Maybe (GeneralPaginationState pgMarker) -> Sem r (PageWithState pgMarker pageItem)) -> + (Int32 -> Maybe (GeneralPaginationState pgMarker) -> Sem r (PageWithState pgMarker pageItem)) -> + pgMarker -> + Int32 -> + Maybe (GeneralPaginationState pgMarker) -> + Sem r (PageWithState pgMarker pageItem) +paginateOverCassandraAndPostgres getCasPage getPgPage pgStartingMarker pageSize mPagingState = do + let getPageFromCassandra = do + casPage <- getCasPage pageSize mPagingState + if pwsHasMore casPage + then pure casPage + else do + let casSize = fromIntegral (length casPage.pwsResults) + remainingSize = pageSize - casSize + if remainingSize > 0 + then do + pgPage <- getPageFromPostgres remainingSize Nothing + pure + PageWithState + { pwsResults = casPage.pwsResults <> pgPage.pwsResults, + pwsState = pgPage.pwsState + } + else pure $ casPage {pwsState = Just (PaginationStatePostgres pgStartingMarker)} + getPageFromPostgres remainingSize mPgMarker = + getPgPage remainingSize (PaginationStatePostgres <$> mPgMarker) + case mPagingState of + Just (PaginationStatePostgres pgMarker) -> getPageFromPostgres pageSize (Just pgMarker) + _ -> getPageFromCassandra + createUserImpl :: NewStoredUser -> Maybe (ConvId, Maybe TeamId) -> Client () createUserImpl new mbConv = retry x5 . batch $ do setType BatchLogged diff --git a/libs/wire-subsystems/src/Wire/UserStore/Migration.hs b/libs/wire-subsystems/src/Wire/UserStore/Migration.hs new file mode 100644 index 00000000000..ebfe29541d8 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/UserStore/Migration.hs @@ -0,0 +1,379 @@ +{-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2026 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.UserStore.Migration where + +import Cassandra hiding (Set) +import Cassandra.Util +import Conduit +import Data.Conduit.List qualified as C +import Data.Handle +import Data.Id +import Data.Json.Util (UTCTimeMillis) +import Data.Misc +import Data.Time +import Database.CQL.Protocol (Record (..), TupleType) +import Hasql.Pool.Extended +import Hasql.Statement qualified as Hasql +import Hasql.TH (resultlessStatement, singletonStatement) +import Hasql.Transaction qualified as Transaction +import Hasql.Transaction.Sessions (IsolationLevel (..), Mode (..)) +import Imports +import Polysemy +import Polysemy.Async +import Polysemy.AtomicState +import Polysemy.Conc +import Polysemy.Error +import Polysemy.Input +import Polysemy.Resource +import Polysemy.TinyLog +import Prometheus qualified +import System.Logger.Class qualified as Log +import Wire.API.Password +import Wire.API.PostgresMarshall +import Wire.API.User +import Wire.API.User.RichInfo +import Wire.Migration +import Wire.MigrationLock +import Wire.Postgres +import Wire.Sem.Concurrency +import Wire.Sem.Concurrency.IO (unsafelyPerformConcurrency) +import Wire.Sem.Logger +import Wire.Sem.Logger.TinyLog (loggerToTinyLog) +import Wire.UserStore.Migration.Types +import Wire.UserStore.Postgres + +migrateUsersLoop :: + MigrationOptions -> + ClientState -> + Pool -> + Log.Logger -> + Prometheus.Counter -> + Prometheus.Counter -> + Prometheus.Counter -> + Prometheus.Vector Text Prometheus.Histogram -> + IO () +migrateUsersLoop migOpts cassClient pgPool logger migCounter migFinished migFailed migDuration = + migrationLoop + logger + "users" + migFinished + migFailed + (interpreter cassClient pgPool logger "users") + (migrateAllUsers migOpts migCounter migDuration) + +type EffectStack = + [ AtomicState Int, + Input ClientState, + Input Pool, + Resource, + Async, + Race, + TinyLog, + Embed IO, + Concurrency 'Unsafe, + Final IO + ] + +interpreter :: ClientState -> Pool -> Log.Logger -> ByteString -> Sem EffectStack a -> IO (Int, a) +interpreter cassClient pgPool logger name = + runFinal + . unsafelyPerformConcurrency + . embedToFinal + . loggerToTinyLog logger + . mapLogger (Log.field "migration" name .) + . raiseUnder + . interpretRace + . asyncToIOFinal + . resourceToIOFinal + . runInputConst pgPool + . runInputConst cassClient + . atomicStateToIO 0 + +migrateAllUsers :: + ( Member TinyLog r, + Member (Input ClientState) r, + Member (Embed IO) r, + Member (AtomicState Int) r, + Member (Concurrency Unsafe) r, + Member (Input Pool) r, + Member Async r, + Member Race r, + Member Resource r + ) => + MigrationOptions -> Prometheus.Counter -> Prometheus.Vector Text Prometheus.Histogram -> ConduitM () Void (Sem r) () +migrateAllUsers migOpts migCounter migDuration = do + lift $ info $ Log.msg (Log.val "migrateAllUsers") + withCount (paginateSem select (paramsP LocalQuorum () migOpts.pageSize) x5) + .| logRetrievedPage migOpts.pageSize runIdentity + .| C.mapM_ (unsafePooledMapConcurrentlyN_ migOpts.parallelism (\uid -> handleLockAndDBErrors "user" (migrateUser migOpts.timeout migCounter migDuration uid))) + where + select :: PrepQuery R () (Identity UserId) + select = "select id from user" + +migrateUser :: + ( PGConstraints r, + Member TinyLog r, + Member (Error MigrationLockError) r, + Member Async r, + Member Race r, + Member Resource r, + Member (Input ClientState) r + ) => + Duration -> Prometheus.Counter -> Prometheus.Vector Text Prometheus.Histogram -> UserId -> Sem r () +migrateUser migTimeout migCounter migDuration uid = + withExclusiveMigrationLockAndTimeout migTimeout migDuration [uid] $ do + cState <- input + mCassData <- runClient cState $ getUserData uid + case mCassData of + Nothing -> pure () + Just cassData -> do + let eithPGRow = mkUserRowPG cassData.id cassData.user cassData.handleClaimValidity cassData.richInfo + case eithPGRow of + Left e -> + warn $ + Log.msg (Log.val "Invalid user found, skipping") + . Log.field "id" (idToText cassData.id) + . Log.field "error" (show e) + Right pgRow -> do + case cassData.handleClaimValidity of + HandleClaimValid -> pure () + HandleNotClaimed -> + info $ + Log.msg (Log.val "This user has a handle which is not 'claimed' by anyone, this user will lose their handle") + . Log.field "user" (idToText pgRow.id_) + . Log.field "handle" (show $ fromHandle <$> cassData.user.handle) + HandleClaimedByAnotherUser claimedBy -> do + warn $ + Log.msg (Log.val "This user has a handle claimed by someone else, this user will lose their handle") + . Log.field "user" (idToText pgRow.id_) + . Log.field "handle" (show $ fromHandle <$> cassData.user.handle) + . Log.field "legitimate_claim_by" (idToText claimedBy) + saveToPostgres pgRow cassData.serviceConv + let mServiceTeam = (.teamId) =<< cassData.serviceConv + runClient cState $ deleteFromCassandra pgRow.id_ pgRow.handle ((,,mServiceTeam) <$> pgRow.providerId <*> pgRow.serviceId) + markDeletionComplete pgRow.id_ + liftIO $ Prometheus.incCounter migCounter + +getUserData :: UserId -> Client (Maybe RawUserData) +getUserData uid = do + mUserRow <- asRecord <$$> query1 selectUserRow (params LocalQuorum (Identity uid)) + case mUserRow of + Nothing -> pure Nothing + Just user -> do + serviceConv <- case (,) <$> user.providerId <*> user.serviceId of + Nothing -> pure Nothing + Just (pid, sid) -> asRecord <$$> query1 selectServiceConv (params LocalQuorum (pid, sid, uid)) + handleClaimValidity <- case user.handle of + Nothing -> pure HandleClaimValid + Just h -> do + mClaimedBy <- runIdentity <$$> query1 selectHandleClaim (params LocalQuorum (Identity h)) + case mClaimedBy of + Nothing -> pure HandleNotClaimed + Just claimedBy + | claimedBy == uid -> pure HandleClaimValid + | otherwise -> pure $ HandleClaimedByAnotherUser claimedBy + richInfo <- runIdentity <$$> query1 selectRichInfo (params LocalQuorum (Identity uid)) + pure $ Just RawUserData {id = uid, ..} + where + selectUserRow :: PrepQuery R (Identity UserId) (TupleType UserRowCass) + selectUserRow = + "SELECT accent_id, activated, country, email, email_unvalidated,\ + \expires, feature_conference_calling, handle, language, managed_by, \ + \name, password, provider, searchable, service,\ + \sso_id, status, supported_protocols, team, text_status,\ + \user_type, assets, picture, writetime(activated)\ + \FROM user WHERE id = ?" + + selectServiceConv :: PrepQuery R (ProviderId, ServiceId, UserId) (TupleType ServiceConv) + selectServiceConv = "SELECT conv, team FROM service_user WHERE provider = ? AND service = ? AND user = ?" + + selectHandleClaim :: PrepQuery R (Identity Handle) (Identity UserId) + selectHandleClaim = "SELECT user FROM user_handle WHERE handle = ?" + + selectRichInfo :: PrepQuery R (Identity UserId) (Identity RichInfoAssocList) + selectRichInfo = "SELECT json FROM rich_info where user = ?" + +data InvalidUserError = UserHasNoName | UserHasNoActivated + deriving (Show) + +mkUserRowPG :: UserId -> UserRowCass -> HandleClaimValidity -> Maybe RichInfoAssocList -> Either InvalidUserError UserRowPG +mkUserRowPG id_ cass@UserRowCass {..} handleClaimValidity richInfo = run . runError $ do + pgName <- note UserHasNoName cass.name + pgActivated <- note UserHasNoActivated cass.activated + createdAt <- note UserHasNoActivated $ writetimeToUTC <$> cass.activatedWriteTime + pure $ + UserRowPG + { accentId = fromMaybe defaultAccentId cass.accentId, + userType = fromMaybe UserTypeRegular cass.userType, + name = pgName, + activated = pgActivated, + handle = case handleClaimValidity of + HandleClaimValid -> cass.handle + HandleNotClaimed -> + -- In this case if we just give this handle to the current user, + -- there could be other users with the same situation. We cannot tie + -- break here, so we just take away the handle from all users + Nothing + HandleClaimedByAnotherUser _ -> + -- Handle is claimed by someone else, so this user cannot get to + -- keep it. + Nothing, + .. + } + +{- ORMOLU_DISABLE -} +type UserTuplePG = + (UserId, ColourId, Bool, Maybe Country, Maybe EmailAddress, + Maybe EmailAddress, Maybe UTCTimeMillis, Maybe Int32, Maybe Handle, Maybe Language, + Maybe ManagedBy, Name, Maybe Password, Maybe ProviderId, Maybe ServiceId, + Maybe UserSSOId, Maybe AccountStatus, Maybe (Set BaseProtocolTag), Maybe TeamId, Maybe TextStatus, + UserType, Maybe Pict, Maybe RichInfoAssocList, Maybe Bool, UTCTime + ) + +userRowPGToTuple :: UserRowPG -> UserTuplePG +userRowPGToTuple user = + (user.id_, user.accentId, user.activated, user.country,user.email, + user.emailUnvalidated, user.expires, user.featureConferenceCalling, user.handle, user.language, + user.managedBy, user.name, user.password, user.providerId, user.serviceId, + user.ssoId, user.status, user.supportedProtocols, user.teamId, user.textStatus, + user.userType, user.pict, user.richInfo, user.searchable, user.createdAt) +{- ORMOLU_ENABLE -} + +saveToPostgres :: (PGConstraints r, Member TinyLog r) => UserRowPG -> Maybe ServiceConv -> Sem r () +saveToPostgres user mServiceConv = do + isHandleRemoved <- runTransactionWithRetry Serializable Write $ do + isHandleRemoved <- case user.status of + -- bots are deleted by just updating their status to deleted and deleting + -- the rows in service_user and service_team tables. + Just Deleted + | user.userType /= UserTypeBot -> do + Transaction.statement (user.id_, user.teamId, user.createdAt) insertDeleted + pure False + _ -> do + removeHandle <- + maybe + (pure False) + (\h -> Transaction.statement (user.id_, h) isHandleTaken) + user.handle + let userTuple = + userRowPGToTuple $ + if removeHandle + then user {handle = Nothing} + else user + Transaction.statement userTuple insertUser + for_ user.assets $ \assets -> do + Transaction.statement user.id_ deleteAssetsStatement + Transaction.statement (mkAssetRows user.id_ assets) insertAssetsStatement + when (user.status /= Just Deleted) $ do + for_ mServiceConv $ \serviceConv -> + Transaction.statement (user.id_, serviceConv.convId, serviceConv.teamId) insertBotConv + pure removeHandle + Transaction.statement user.id_ markPendingDelete + pure isHandleRemoved + + when isHandleRemoved . warn $ + Log.msg (Log.val "Duplicate handle claim found, this user doesn't have a handle anymore") + . Log.field "user" (idToText user.id_) + . Log.field "handle" (show $ fromHandle <$> user.handle) + where + isHandleTaken :: Hasql.Statement (UserId, Handle) Bool + isHandleTaken = + dimapPG + [singletonStatement| + SELECT EXISTS (SELECT 1 FROM wire_user where handle = $2 :: text AND id != $1 :: uuid) :: bool + |] + insertDeleted :: Hasql.Statement (UserId, Maybe TeamId, UTCTime) () + insertDeleted = + lmapPG + [resultlessStatement| + INSERT INTO deleted_user + (id, team, created_at) + VALUES ($1 :: uuid, $2 :: uuid?, $3 :: timestamptz) + ON CONFLICT (id) DO NOTHING + |] + insertUser :: Hasql.Statement UserTuplePG () + insertUser = + lmapPG + [resultlessStatement| + INSERT INTO wire_user + (id, accent_id, activated, country, email, + email_unvalidated, expires, feature_conference_calling, handle, language, + managed_by, name, password, provider, service, + sso_id, account_status, supported_protocols, team, text_status, + user_type, picture, rich_info, searchable, created_at + ) + VALUES + ($1 :: uuid, $2 :: integer, $3 :: boolean, $4 :: text?, $5 :: text?, + $6 :: text?, $7 :: timestamptz?, $8 :: integer?, $9 :: text?, $10 :: text?, + $11 :: integer?, $12 :: text, $13 :: text?, $14 :: uuid?, $15 :: uuid?, + $16 :: jsonb?, $17 :: integer?, $18 :: integer?, $19 :: uuid?, $20 :: text?, + $21 :: integer, $22 :: jsonb?, $23 :: jsonb?, $24 :: boolean?, $25 :: timestamptz + ) + ON CONFLICT (id) DO NOTHING + |] + insertBotConv :: Hasql.Statement (UserId, ConvId, Maybe TeamId) () + insertBotConv = + lmapPG + [resultlessStatement| + INSERT INTO bot_conv + (id, conv, conv_team) + VALUES ($1 :: uuid, $2 :: uuid, $3 :: uuid?) + |] + + markPendingDelete :: Hasql.Statement UserId () + markPendingDelete = + lmapPG + [resultlessStatement| + INSERT INTO user_migration_pending_deletes (id) + VALUES ($1 :: uuid) + ON CONFLICT (id) DO NOTHING + |] + +markDeletionComplete :: (PGConstraints r) => UserId -> Sem r () +markDeletionComplete uid = + runStatement uid stmt + where + stmt :: Hasql.Statement UserId () + stmt = lmapPG [resultlessStatement|DELETE FROM user_migration_pending_deletes WHERE id = $1 :: uuid|] + +deleteFromCassandra :: UserId -> Maybe Handle -> Maybe (ProviderId, ServiceId, Maybe TeamId) -> Client () +deleteFromCassandra uid mHandle mService = do + for_ mHandle $ \handle -> write deleteHandle (params LocalQuorum (Identity handle)) + for_ mService $ \(pid, sid, mTid) -> do + write deleteServiceUser (params LocalQuorum (pid, sid, uid)) + for_ mTid $ \tid -> write deleteServiceTeam (params LocalQuorum (pid, sid, tid, uid)) + write deleteRichInfo (params LocalQuorum (Identity uid)) + write deleteUser (params LocalQuorum (Identity uid)) + where + deleteUser :: PrepQuery W (Identity UserId) () + deleteUser = "DELETE FROM user WHERE id = ?" + + deleteHandle :: PrepQuery W (Identity Handle) () + deleteHandle = "DELETE FROM user_handle WHERE handle = ?" + + deleteServiceUser :: PrepQuery W (ProviderId, ServiceId, UserId) () + deleteServiceUser = "DELETE FROM service_user WHERE provider = ? AND service = ? AND user = ?" + + deleteServiceTeam :: PrepQuery W (ProviderId, ServiceId, TeamId, UserId) () + deleteServiceTeam = "DELETE FROM service_team WHERE provider = ? AND service = ? AND team = ? AND user = ?" + + deleteRichInfo :: PrepQuery W (Identity UserId) () + deleteRichInfo = "DELETE FROM rich_info WHERE user = ?" diff --git a/libs/wire-subsystems/src/Wire/UserStore/Migration/Types.hs b/libs/wire-subsystems/src/Wire/UserStore/Migration/Types.hs new file mode 100644 index 00000000000..07619adc6c5 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/UserStore/Migration/Types.hs @@ -0,0 +1,111 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2026 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.UserStore.Migration.Types where + +import Cassandra.Util +import Data.Handle +import Data.Id +import Data.Json.Util +import Data.Time +import Database.CQL.Protocol (Record (..), TupleType, recordInstance) +import Imports +import Wire.API.Password +import Wire.API.User +import Wire.API.User.RichInfo + +data RawUserData = RawUserData + { id :: UserId, + user :: UserRowCass, + richInfo :: Maybe RichInfoAssocList, + serviceConv :: Maybe ServiceConv, + handleClaimValidity :: HandleClaimValidity + } + +data HandleClaimValidity + = HandleClaimValid + | HandleNotClaimed + | HandleClaimedByAnotherUser UserId + +-- | Some fields are read as 'Maybe' even if they're supposed to always be +-- there. This is to deal with potential old data in the DB. +data UserRowCass = UserRowCass + { accentId :: Maybe ColourId, + activated :: Maybe Bool, + country :: Maybe Country, + email :: Maybe EmailAddress, + emailUnvalidated :: Maybe EmailAddress, + expires :: Maybe UTCTimeMillis, + featureConferenceCalling :: Maybe Int32, + handle :: Maybe Handle, + language :: Maybe Language, + managedBy :: Maybe ManagedBy, + name :: Maybe Name, + password :: Maybe Password, + providerId :: Maybe ProviderId, + searchable :: Maybe Bool, + serviceId :: Maybe ServiceId, + ssoId :: Maybe UserSSOId, + status :: Maybe AccountStatus, + supportedProtocols :: Maybe (Set BaseProtocolTag), + teamId :: Maybe TeamId, + textStatus :: Maybe TextStatus, + userType :: Maybe UserType, + assets :: Maybe [Asset], + pict :: Maybe Pict, + activatedWriteTime :: Maybe (Writetime ()) + } + +data ServiceConv = ServiceConv + { convId :: ConvId, + teamId :: Maybe TeamId + } + +data UserRowPG = UserRowPG + { id_ :: UserId, + accentId :: ColourId, + activated :: Bool, + country :: Maybe Country, + email :: Maybe EmailAddress, + emailUnvalidated :: Maybe EmailAddress, + expires :: Maybe UTCTimeMillis, + featureConferenceCalling :: Maybe Int32, + handle :: Maybe Handle, + language :: Maybe Language, + managedBy :: Maybe ManagedBy, + name :: Name, + password :: Maybe Password, + providerId :: Maybe ProviderId, + searchable :: Maybe Bool, + serviceId :: Maybe ServiceId, + ssoId :: Maybe UserSSOId, + status :: Maybe AccountStatus, + supportedProtocols :: Maybe (Set BaseProtocolTag), + teamId :: Maybe TeamId, + textStatus :: Maybe TextStatus, + userType :: UserType, + assets :: Maybe [Asset], + pict :: Maybe Pict, + richInfo :: Maybe RichInfoAssocList, + createdAt :: UTCTime + } + +recordInstance ''UserRowCass + +recordInstance ''ServiceConv diff --git a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs index 88177b95cde..ca9e002bfc5 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs @@ -18,7 +18,13 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.UserStore.Postgres (interpretUserStorePostgres) where +module Wire.UserStore.Postgres + ( interpretUserStorePostgres, + deleteAssetsStatement, + insertAssetsStatement, + mkAssetRows, + ) +where import Cassandra (GeneralPaginationState (PaginationStatePostgres), PageWithState (..), paginationStatePostgres) import Data.Handle diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 42196d6e18c..abfa57d8442 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -501,6 +501,8 @@ library Wire.UserStore Wire.UserStore.Cassandra Wire.UserStore.IndexUser + Wire.UserStore.Migration + Wire.UserStore.Migration.Types Wire.UserStore.Postgres Wire.UserStore.Unique Wire.UserSubsystem diff --git a/postgres-schema.sql b/postgres-schema.sql index b2d1587ab49..de4a57a0f2e 100644 --- a/postgres-schema.sql +++ b/postgres-schema.sql @@ -1614,6 +1614,17 @@ CREATE TABLE public.user_group_member ( ALTER TABLE public.user_group_member OWNER TO "wire-server"; +-- +-- Name: user_migration_pending_deletes; Type: TABLE; Schema: public; Owner: wire-server +-- + +CREATE TABLE public.user_migration_pending_deletes ( + id uuid NOT NULL +); + + +ALTER TABLE public.user_migration_pending_deletes OWNER TO "wire-server"; + -- -- Name: wire_user; Type: TABLE; Schema: public; Owner: wire-server -- @@ -2012,6 +2023,14 @@ ALTER TABLE ONLY public.user_group ADD CONSTRAINT user_group_pkey PRIMARY KEY (team_id, id); +-- +-- Name: user_migration_pending_deletes user_migration_pending_deletes_pkey; Type: CONSTRAINT; Schema: public; Owner: wire-server +-- + +ALTER TABLE ONLY public.user_migration_pending_deletes + ADD CONSTRAINT user_migration_pending_deletes_pkey PRIMARY KEY (id); + + -- -- Name: wire_user wire_user_handle_key; Type: CONSTRAINT; Schema: public; Owner: wire-server -- diff --git a/services/background-worker/background-worker.integration.yaml b/services/background-worker/background-worker.integration.yaml index e264ce14016..b0bd0d172e4 100644 --- a/services/background-worker/background-worker.integration.yaml +++ b/services/background-worker/background-worker.integration.yaml @@ -58,6 +58,7 @@ migrationOptions: migrateConversationCodes: false migrateTeamFeatures: false migrateDomainRegistration: false +migrateUsers: false # Background jobs consumer configuration for integration backgroundJobs: diff --git a/services/background-worker/src/Wire/BackgroundWorker.hs b/services/background-worker/src/Wire/BackgroundWorker.hs index b57ba12df40..6c12b02e816 100644 --- a/services/background-worker/src/Wire/BackgroundWorker.hs +++ b/services/background-worker/src/Wire/BackgroundWorker.hs @@ -78,6 +78,14 @@ run opts galleyOpts = do withNamedLogger "migrate-domain-registration" $ Migrations.domainRegistration opts.migrationOptions else pure $ pure () + cleanupUsersMigration <- + if opts.migrateUsers + then + runAppT env $ + withNamedLogger "migrate-users" $ + Migrations.users opts.migrationOptions + else pure $ pure () + cleanupJobs <- runAppT env $ withNamedLogger "background-job-consumer" $ @@ -89,13 +97,14 @@ run opts galleyOpts = do let cleanup = void $ runConcurrently $ - (,,,,,,,) + (,,,,,,,,) <$> Concurrently cleanupDeadUserNotifWatcher <*> Concurrently cleanupBackendNotifPusher <*> Concurrently cleanupConvMigration <*> Concurrently cleanUpConvCodesMigration <*> Concurrently cleanupTeamFeaturesMigration <*> Concurrently cleanupDomainRegistrationMigration + <*> Concurrently cleanupUsersMigration <*> Concurrently cleanupJobRunner <*> Concurrently cleanupJobs diff --git a/services/background-worker/src/Wire/BackgroundWorker/Options.hs b/services/background-worker/src/Wire/BackgroundWorker/Options.hs index 61df5d5d14f..035460cfc32 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Options.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Options.hs @@ -55,6 +55,7 @@ data Opts = Opts migrateConversationCodes :: !Bool, migrateTeamFeatures :: !Bool, migrateDomainRegistration :: !Bool, + migrateUsers :: !Bool, jobs :: JobConfig, meetingsCleanup :: MeetingsCleanupConfig, backgroundJobs :: BackgroundJobsConfig diff --git a/services/background-worker/src/Wire/PostgresMigrations.hs b/services/background-worker/src/Wire/PostgresMigrations.hs index 604cab0140c..28c6a789a4a 100644 --- a/services/background-worker/src/Wire/PostgresMigrations.hs +++ b/services/background-worker/src/Wire/PostgresMigrations.hs @@ -24,10 +24,11 @@ import UnliftIO import Wire.BackgroundWorker.Env import Wire.BackgroundWorker.Util import Wire.CodeStore.Migration -import Wire.ConversationStore.Migration +import Wire.ConversationStore.Migration qualified as ConversationStore import Wire.DomainRegistrationStore.Migration import Wire.Migration (MigrationOptions) import Wire.TeamFeatureStore.Migration +import Wire.UserStore.Migration qualified as UserStore conversations :: MigrationOptions -> AppT IO CleanupAction conversations migOpts = do @@ -45,8 +46,8 @@ conversations migOpts = do userMigFailed <- register $ counter $ Prometheus.Info "wire_user_remote_convs_migration_failed" "Whether the migration of remote conversation membership data to Postgresql has failed" userMigDuration <- register $ vector "outcome" $ histogram (Prometheus.Info "wire_user_remote_convs_migration_duration_seconds" "Duration of remote conversation membership migration attempts") defaultBuckets - convLoop <- async . lift $ migrateConvsLoop migOpts cassClient pgPool logger convMigCounter convMigFinished convMigFailed convMigDuration - userLoop <- async . lift $ migrateUsersLoop migOpts cassClient pgPool logger userMigCounter userMigFinished userMigFailed userMigDuration + convLoop <- async . lift $ ConversationStore.migrateConvsLoop migOpts cassClient pgPool logger convMigCounter convMigFinished convMigFailed convMigDuration + userLoop <- async . lift $ ConversationStore.migrateUsersLoop migOpts cassClient pgPool logger userMigCounter userMigFinished userMigFailed userMigDuration Log.info logger $ Log.msg (Log.val "started conversation migration") pure $ do @@ -107,3 +108,21 @@ domainRegistration migOpts = do pure $ do Log.info logger $ Log.msg (Log.val "cancelling domain registration migration") cancel migrationLoop + +users :: MigrationOptions -> AppT IO CleanupAction +users migOpts = do + cassClient <- asks (.cassandraBrig) + pgPool <- asks (.hasqlPool) + logger <- asks (.logger) + Log.info logger $ Log.msg (Log.val "starting user migration") + count <- register $ counter $ Prometheus.Info "wire_users_migrated_to_pg" "Number of user rows migrated to Postgresql" + finished <- register $ counter $ Prometheus.Info "wire_users_migration_finished" "Whether the user migration to Postgresql is finished successfully" + failed <- register $ counter $ Prometheus.Info "wire_users_migration_failed" "Whether the user migration to Postgresql has failed" + duration <- register $ vector "outcome" $ histogram (Prometheus.Info "wire_users_migration_duration_seconds" "Duration of user migration attempts") defaultBuckets + + migrationLoop <- async . lift $ UserStore.migrateUsersLoop migOpts cassClient pgPool logger count finished failed duration + + Log.info logger $ Log.msg (Log.val "started user migration") + pure $ do + Log.info logger $ Log.msg (Log.val "cancelling user migration") + cancel migrationLoop diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 89b1d88f7a7..794b137836c 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -183,6 +183,7 @@ library Brig.Schema.V91_UpdateDomainRegistrationSchema_AddWebappUrl Brig.Schema.V92_AddUserType Brig.Schema.V93_AddScimPendingUserEmail + Brig.Schema.V94_ReduceUserGCGracePeriod Brig.Team.API Brig.Team.Template Brig.Template diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 4414567c910..e6103cb4722 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -431,7 +431,7 @@ runBrigToIO e (AppT ma) = do case e.postgresMigration.user of CassandraStorage -> interpretUserStoreCassandra e.casClient PostgresqlStorage -> interpretUserStorePostgres - MigrationToPostgresql -> error "Migration not implemented for user" + MigrationToPostgresql -> interpretUserStoreToCassandraAndPostgres e.casClient ( either throwM pure <=< ( runFinal diff --git a/services/brig/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs index 695007e195e..827d2028309 100644 --- a/services/brig/src/Brig/Index/Eval.hs +++ b/services/brig/src/Brig/Index/Eval.hs @@ -45,8 +45,11 @@ import Hasql.Pool.Extended qualified as Hasql import Imports import Network.HTTP.Client (Manager) import Polysemy +import Polysemy.Async (Async, asyncToIOFinal) +import Polysemy.Conc (Race, interpretRace) import Polysemy.Error import Polysemy.Input +import Polysemy.Resource (Resource, runResource) import Polysemy.TinyLog (TinyLog) import System.Logger qualified as Log import System.Logger.Class (Logger) @@ -60,6 +63,7 @@ import Wire.IndexedUserStore.Bulk.ElasticSearch qualified as IndexedUserStoreBul import Wire.IndexedUserStore.ElasticSearch import Wire.IndexedUserStore.MigrationStore (IndexedUserMigrationStore) import Wire.IndexedUserStore.MigrationStore.ElasticSearch +import Wire.MigrationLock import Wire.ParseException import Wire.PostgresMigrationOpts import Wire.Rpc @@ -83,6 +87,7 @@ type BrigIndexEffectStack = Error IndexedUserStoreError, IndexedUserMigrationStore, Error MigrationException, + Error MigrationLockError, GalleyAPIAccess, Error ParseException, Rpc, @@ -92,6 +97,9 @@ type BrigIndexEffectStack = Error UsageError, Error TeamCollaboratorsError, Error ClientError, + Resource, + Race, + Async, Embed IO, Final IO ] @@ -132,10 +140,13 @@ runSem :: SemDeps -> UserStorageLocation -> Endpoint -> Logger -> Sem BrigIndexE runSem (mgr, casClient, pgPool, bhEnv, indexedUserStoreConfig, reqId, migrationIndexName) userStorage galleyEndpoint logger action = do let userStoreInterpreter = case userStorage.userStorageLocation of CassandraStorage -> interpretUserStoreCassandra casClient - MigrationToPostgresql -> error "Migration not implemented for user" + MigrationToPostgresql -> interpretUserStoreToCassandraAndPostgres casClient PostgresqlStorage -> interpretUserStorePostgres runFinal . embedToFinal + . asyncToIOFinal + . interpretRace + . runResource . throwErrorToIOFinal @ClientError . throwErrorToIOFinal @TeamCollaboratorsError . throwPostgresUsageErrorToIOFinal @@ -145,6 +156,7 @@ runSem (mgr, casClient, pgPool, bhEnv, indexedUserStoreConfig, reqId, migrationI . runRpcWithHttp mgr reqId . throwErrorToIOFinal @ParseException . interpretGalleyAPIAccessToRpc mempty galleyEndpoint + . throwErrorToIOFinal @MigrationLockError . throwErrorToIOFinal @MigrationException . interpretIndexedUserMigrationStoreES bhEnv migrationIndexName . throwErrorToIOFinal @IndexedUserStoreError diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 7cc07fc7f2d..fd170bdbaff 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -101,7 +101,7 @@ run opts = withTracer \tracer -> do authMetrics <- Async.async (runBrigToIO e collectAuthMetrics) pendingActivationCleanupAsync <- Async.async (runBrigToIO e pendingActivationCleanup) - inSpan tracer "brig" defaultSpanArguments {kind = Otel.Server} (runSettingsWithShutdown s app Nothing) `finally` do + inSpan tracer "brig" defaultSpanArguments {kind = Otel.Server} (runSettingsWithCleanup (flush e.appLogger) s app Nothing) `finally` do Async.cancelMany $ [internalEventListener, pendingActivationCleanupAsync, authMetrics] <> catMaybes [emailListener, sftDiscovery] diff --git a/services/brig/src/Brig/Schema/Run.hs b/services/brig/src/Brig/Schema/Run.hs index 560cf64f2e7..ca29a34b79d 100644 --- a/services/brig/src/Brig/Schema/Run.hs +++ b/services/brig/src/Brig/Schema/Run.hs @@ -68,6 +68,7 @@ import Brig.Schema.V90_DomainRegistrationTeamIndex qualified as V90_DomainRegist import Brig.Schema.V91_UpdateDomainRegistrationSchema_AddWebappUrl qualified as V91_UpdateDomainRegistrationSchema_AddWebappUrl import Brig.Schema.V92_AddUserType qualified as V92_AddUserType import Brig.Schema.V93_AddScimPendingUserEmail qualified as V93_AddScimPendingUserEmail +import Brig.Schema.V94_ReduceUserGCGracePeriod qualified as V94_ReduceUserGCGracePeriod import Cassandra.MigrateSchema (migrateSchema) import Cassandra.Schema import Control.Exception (finally) @@ -142,7 +143,8 @@ migrations = V90_DomainRegistrationTeamIndex.migration, V91_UpdateDomainRegistrationSchema_AddWebappUrl.migration, V92_AddUserType.migration, - V93_AddScimPendingUserEmail.migration + V93_AddScimPendingUserEmail.migration, + V94_ReduceUserGCGracePeriod.migration -- FUTUREWORK: undo V41 (searchable flag); we stopped using it in -- https://github.com/wireapp/wire-server/pull/964 ] diff --git a/services/brig/src/Brig/Schema/V94_ReduceUserGCGracePeriod.hs b/services/brig/src/Brig/Schema/V94_ReduceUserGCGracePeriod.hs new file mode 100644 index 00000000000..3761b2f7f0a --- /dev/null +++ b/services/brig/src/Brig/Schema/V94_ReduceUserGCGracePeriod.hs @@ -0,0 +1,44 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2026 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . +module Brig.Schema.V94_ReduceUserGCGracePeriod + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 94 "reduce user gc_grace_period" $ do + schema' + [r| ALTER TABLE user WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE user_handle WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE rich_info WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE service_user WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE service_team WITH gc_grace_seconds = 86400 |] From ef740c656617efc075aa1a68d051a57bc7d6207d Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 2 Sep 2026 13:29:07 +0200 Subject: [PATCH 08/29] WPB-18929 [fix] SCIM cannot re invite user if initial invitation was revoked or expired via teams UI (#5510) --- changelog.d/3-bug-fixes/WPB-18929 | 3 + integration/test/API/Brig.hs | 5 ++ integration/test/Test/Spar.hs | 30 +++++++++ .../src/Wire/API/Routes/Internal/Spar.hs | 1 + .../wire-subsystems/src/Wire/SparAPIAccess.hs | 1 + .../src/Wire/SparAPIAccess/Rpc.hs | 10 +++ .../Wire/MockInterpreters/SparAPIAccess.hs | 1 + services/brig/src/Brig/Team/API.hs | 62 ++++++++++++++++++- services/spar/src/Spar/API.hs | 26 ++++++++ services/spar/src/Spar/Scim/User.hs | 15 +++++ 10 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-18929 diff --git a/changelog.d/3-bug-fixes/WPB-18929 b/changelog.d/3-bug-fixes/WPB-18929 new file mode 100644 index 00000000000..29da72e51fd --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-18929 @@ -0,0 +1,3 @@ +Revoking a pending SCIM invitation now removes the associated Brig account and +Spar SCIM metadata synchronously, allowing the same SCIM user to be invited +again. diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 480bb781a15..3b115345841 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -990,6 +990,11 @@ getInvitationByCode user code = do req <- baseRequest user Brig Versioned $ joinHttpPath ["teams", "invitations", "info"] submit "GET" (req & addQueryParams [("code", code)]) +deleteTeamInvitation :: (HasCallStack, MakesValue user) => user -> String -> String -> App Response +deleteTeamInvitation user tid iid = do + req <- baseRequest user Brig Versioned (joinHttpPath ["teams", tid, "invitations", iid]) + submit "DELETE" req + passwordReset :: (HasCallStack, MakesValue domain) => domain -> String -> App Response passwordReset domain email = do req <- baseRequest domain Brig Versioned "password-reset" diff --git a/integration/test/Test/Spar.hs b/integration/test/Test/Spar.hs index dbdaf7eb0a0..d2a2f4770c5 100644 --- a/integration/test/Test/Spar.hs +++ b/integration/test/Test/Spar.hs @@ -131,6 +131,36 @@ testTeamInvitationWhenScimInvitationPending = do user %. "managed_by" `shouldMatch` "scim" user %. "status" `shouldMatch` "pending-invitation" +testScimReinviteAfterRevoke :: (HasCallStack) => App () +testScimReinviteAfterRevoke = do + let settings = + def + { brigCfg = + -- Controls when asynchronous cleanup removes expired SCIM pending accounts. + setField "optSettings.setExpiredUserCleanupTimeout" (3600 :: Int) + } + withModifiedBackend settings $ \domain -> do + (owner, tid, _) <- createTeam domain 1 + token <- createScimToken owner def >>= getJSON 200 >>= (%. "token") >>= asString + + email <- randomEmail + externalId <- randomExternalId + scimUser <- randomScimUserWithEmail externalId email + scid <- createScimUser domain token scimUser >>= getJSON 201 >>= (%. "id") >>= asString + handle <- scimUser %. "userName" >>= asString + + -- assert that the SCIM handle is claimed + putHandle owner handle >>= assertStatus 409 + + -- cancel the invitation + void $ Brig.listInvitations owner tid >>= getJSON 200 >>= (%. "invitations") >>= asList >>= assertOne + Brig.deleteTeamInvitation owner tid scid >>= assertSuccess + void $ Brig.listInvitations owner tid >>= getJSON 200 >>= (%. "invitations") >>= shouldBeEmpty + + -- retry the invite should work + createScimUser domain token scimUser `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + testTeamInvitationWhenScimAccountExists :: (HasCallStack) => App () testTeamInvitationWhenScimAccountExists = do (owner, tid, _) <- createTeam OwnDomain 1 diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs index e2a23c2d1c1..31233e07bda 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs @@ -32,6 +32,7 @@ type InternalAPI = "i" :> ( Named "i_status" ("status" :> Get '[JSON] NoContent) :<|> Named "i_delete_team" ("teams" :> Capture "team" TeamId :> DeleteNoContent) + :<|> Named "i_delete_scim_user" ("scim" :> "users" :> Capture "team" TeamId :> Capture "user" UserId :> DeleteNoContent) :<|> Named "i_put_sso_settings" ("sso" :> "settings" :> ReqBody '[JSON] SsoSettings :> Put '[JSON] NoContent) :<|> Named "i_post_scim_user_info" ("scim" :> "userinfo" :> Capture "user" UserId :> Post '[JSON] ScimUserInfo) :<|> Named "i_get_identity_providers" ("identity-providers" :> Capture "team" TeamId :> Get '[JSON] IdPList) diff --git a/libs/wire-subsystems/src/Wire/SparAPIAccess.hs b/libs/wire-subsystems/src/Wire/SparAPIAccess.hs index b2df76bd01a..0a6005162d7 100644 --- a/libs/wire-subsystems/src/Wire/SparAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/SparAPIAccess.hs @@ -27,6 +27,7 @@ import Wire.API.User.IdentityProvider data SparAPIAccess m a where GetIdentityProviders :: TeamId -> SparAPIAccess m IdPList DeleteTeam :: TeamId -> SparAPIAccess m () + DeleteScimUser :: TeamId -> UserId -> SparAPIAccess m () LookupScimUserInfo :: UserId -> SparAPIAccess m ScimUserInfo makeSem ''SparAPIAccess diff --git a/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs index fc08de1b81d..a76692c5ffd 100644 --- a/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs @@ -49,6 +49,7 @@ interpretSparAPIAccessToRpc sparEndpoint = runInputConst sparEndpoint . \case GetIdentityProviders tid -> getIdentityProvidersImpl tid DeleteTeam tid -> deleteTeamImpl tid + DeleteScimUser tid uid -> deleteScimUserImpl tid uid LookupScimUserInfo uid -> lookupScimUserInfoImpl uid sparRequest :: @@ -93,6 +94,15 @@ deleteTeamImpl tid = do . paths ["i", "teams", toByteString' tid] . expect2xx +deleteScimUserImpl :: (Member (Input Endpoint) r, Member Rpc r) => TeamId -> UserId -> Sem r () +deleteScimUserImpl tid uid = do + void $ sparRequest delReq + where + delReq = + method DELETE + . paths ["i", "scim", "users", toByteString' tid, toByteString' uid] + . expect2xx + -- | Get the SCIM user info for a user. lookupScimUserInfoImpl :: ( Member (Error ParseException) r, diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs index 4122706e412..48f314d0528 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs @@ -31,6 +31,7 @@ miniSparAPIAccess = interpret $ \case GetIdentityProviders tid -> Map.findWithDefault (IdPList []) tid <$> input DeleteTeam {} -> error "DeleteTeam not implemented in miniSparAPIAccess" + DeleteScimUser {} -> error "DeleteScimUser not implemented in miniSparAPIAccess" LookupScimUserInfo {} -> error "LookupScimUserInfo not implemented in miniSparAPIAccess" emptySparAPIAccess :: InterpreterFor SparAPIAccess r diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 48430fbebab..ce88b50a63f 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -69,6 +69,7 @@ import Wire.API.Team.Size import Wire.API.User hiding (fromEmail) import Wire.AuthenticationSubsystem import Wire.BlockListStore +import Wire.ClientStore (ClientStore) import Wire.EmailSubsystem.Interpreter (renderInvitationUrl) import Wire.Error import Wire.Events (Events) @@ -77,18 +78,25 @@ import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.IndexedUserStore (IndexedUserStore, getTeamSize) import Wire.InvitationStore (InvitationStore (..), PaginatedResult (..), StoredInvitation (..)) import Wire.InvitationStore qualified as Store +import Wire.NotificationSubsystem (NotificationSubsystem) +import Wire.PropertySubsystem (PropertySubsystem) import Wire.Sem.Concurrency +import Wire.SparAPIAccess (SparAPIAccess) +import Wire.SparAPIAccess qualified as SparAPIAccess import Wire.TeamInvitationSubsystem import Wire.TeamInvitationSubsystem.Interpreter (toInvitation) import Wire.TeamSubsystem (TeamSubsystem) import Wire.TeamSubsystem qualified as TeamSubsystem +import Wire.UserGroupSubsystem (UserGroupSubsystem) import Wire.UserKeyStore import Wire.UserPendingActivationStore (UserPendingActivationStore) +import Wire.UserPendingActivationStore qualified as UserPendingActivationStore import Wire.UserStore import Wire.UserSubsystem import Wire.UserSubsystem.Error servantAPI :: + forall p r. ( Member GalleyAPIAccess r, Member TeamInvitationSubsystem r, Member UserSubsystem r, @@ -98,7 +106,18 @@ servantAPI :: Member (Input (Local ())) r, Member (Error UserSubsystemError) r, Member IndexedUserStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member SparAPIAccess r, + Member (Embed App.HttpClientIO) r, + Member NotificationSubsystem r, + Member ClientStore r, + Member PropertySubsystem r, + Member UserGroupSubsystem r, + Member Events r, + Member AuthenticationSubsystem r, + Member UserStore r, + Member UserKeyStore r, + Member (UserPendingActivationStore p) r ) => ServerT TeamsAPI (Handler r) servantAPI = @@ -202,9 +221,24 @@ logInvitationRequest context action = pure (Right result) deleteInvitation :: + forall p r. ( Member InvitationStore r, Member (Error UserSubsystemError) r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member SparAPIAccess r, + Member TinyLog r, + Member (Embed App.HttpClientIO) r, + Member NotificationSubsystem r, + Member ClientStore r, + Member PropertySubsystem r, + Member UserGroupSubsystem r, + Member Events r, + Member AuthenticationSubsystem r, + Member UserSubsystem r, + Member UserStore r, + Member UserKeyStore r, + Member (UserPendingActivationStore p) r, + Member (Input (Local ())) r ) => UserId -> TeamId -> @@ -212,6 +246,30 @@ deleteInvitation :: Sem r () deleteInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] + mInvitation <- Store.lookupInvitation tid iid + let scimUid = invitationIdToUserId iid + mUser <- getAccountNoFilter =<< qualifyLocal' scimUid + for_ mUser $ \user -> + for_ (userEmail user) $ \email -> do + pendingScimUsers <- Store.lookupPendingScimUsers tid email + let invitationMatches = maybe True (\inv -> inv.email == email) mInvitation + when + ( userId user == scimUid + && user.userTeam == Just tid + && user.userManagedBy == ManagedByScim + && user.userStatus == PendingInvitation + && invitationMatches + && scimUid `elem` pendingScimUsers + ) + $ do + -- Remove Spar's external-id mapping before deleting the Brig account. + -- Otherwise a SCIM retry still sees the old external ID as owned. + SparAPIAccess.deleteScimUser tid scimUid + UserPendingActivationStore.remove scimUid + -- Use the same complete deletion logic as the asynchronous user + -- deletion worker, but run it synchronously before the invitation is + -- removed so a replacement SCIM invitation can be created safely. + API.deleteAccount user Store.deleteInvitation tid iid listInvitations :: diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index f18e882b496..6886106b97f 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -263,6 +263,7 @@ apiINTERNAL :: Member IdPConfigStore r, Member (Error SparError) r, Member SAMLUserStore r, + Member ScimExternalIdStore r, Member ScimUserMetaStore r, Member (Logger (Msg -> Msg)) r, Member Random r, @@ -273,6 +274,7 @@ apiINTERNAL :: apiINTERNAL = Named @"i_status" internalStatus :<|> Named @"i_delete_team" internalDeleteTeam + :<|> Named @"i_delete_scim_user" internalDeleteScimUser :<|> Named @"i_put_sso_settings" internalPutSsoSettings :<|> Named @"i_post_scim_user_info" internalGetScimUserInfo :<|> Named @"i_get_identity_providers" idpGetAllByTeamId @@ -1132,6 +1134,30 @@ internalDeleteTeam teamId = do deleteTeam teamId pure NoContent +internalDeleteScimUser :: + ( Member BrigAPIAccess r, + Member ScimExternalIdStore r, + Member ScimUserMetaStore r, + Member SAMLUserStore r, + Member (Logger (Msg -> Msg)) r + ) => + TeamId -> + UserId -> + Sem r NoContent +internalDeleteScimUser teamId uid = do + Logger.info $ + Log.msg ("Attempting to delete SCIM user data" :: String) + . Log.field "team" (idToText teamId) + . Log.field "user" (idToText uid) + BrigAPIAccess.getAccount WithPendingInvitations uid >>= \case + Just user + | userTeam user == Just teamId + && userManagedBy user == ManagedByScim + && userStatus user == PendingInvitation -> + deleteScimUserData teamId user + _ -> pure () + pure NoContent + internalPutSsoSettings :: ( Member DefaultSsoCode r, Member (Error SparError) r, diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index 95e040bc661..c9c6e3a5150 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -41,6 +41,7 @@ module Spar.Scim.User mkValidScimId, scimFindUserByExternalId, deleteScimUser, + deleteScimUserData, ) where @@ -899,6 +900,20 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = ScimExternalIdStore.delete stiTeam veid.validScimIdExternal lift $ ScimUserMetaStore.delete uid +deleteScimUserData :: + ( Member ScimExternalIdStore r, + Member ScimUserMetaStore r, + Member SAMLUserStore r + ) => + TeamId -> + User -> + Sem r () +deleteScimUserData teamId account = do + for_ (Intra.oldVeidFromBrigUser account) $ \veid -> do + for_ (justThere veid.validScimIdAuthInfo) (SAMLUserStore.delete (userId account)) + ScimExternalIdStore.delete teamId veid.validScimIdExternal + ScimUserMetaStore.delete (userId account) + ---------------------------------------------------------------------------- -- Utilities From 033a6549112595f93409ad7c62224d8e9c5f8591 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 2 Sep 2026 18:34:36 +0200 Subject: [PATCH 09/29] Revert "[WPB-28089] Treat team collaborators like team members in contact search (second attempt). (#5488)" (#5513) This reverts commit 01d80e8d168d4d0da7a6d3006d7068b36c33b18d. --- ...rators-like-team-members-in-contact-search | 29 --- ...rators-like-team-members-in-contact-search | 1 - integration/test/Test/TeamCollaborators.hs | 106 -------- .../src/Wire/BrigAPIAccess/Local.hs | 65 ----- .../src/Wire/BrigAPIAccess/Rpc.hs | 234 ++++++++---------- .../IndexedUserStore/Bulk/ElasticSearch.hs | 61 ++--- .../Wire/IndexedUserStore/ElasticSearch.hs | 19 +- .../src/Wire/TeamCollaboratorsStore.hs | 2 - .../Wire/TeamCollaboratorsStore/Postgres.hs | 17 -- .../TeamCollaboratorsSubsystem/Interpreter.hs | 36 +-- .../src/Wire/UserSearch/Migration.hs | 1 - .../src/Wire/UserSearch/Types.hs | 9 +- .../src/Wire/UserStore/IndexUser.hs | 8 +- .../src/Wire/UserSubsystem/Interpreter.hs | 20 +- .../test/unit/Wire/MiniBackend.hs | 9 +- .../test/unit/Wire/MockInterpreters.hs | 1 - .../Wire/MockInterpreters/BrigAPIAccess.hs | 81 ------ .../TeamCollaboratorsStore.hs | 2 - .../Wire/MockInterpreters/UserSubsystem.hs | 2 +- .../Wire/ScimSubsystem/InterpreterSpec.hs | 2 +- .../test/unit/Wire/UserSearch/TypesSpec.hs | 4 +- .../Wire/UserSubsystem/InterpreterSpec.hs | 2 +- libs/wire-subsystems/wire-subsystems.cabal | 2 - .../background-worker/src/Wire/Effects.hs | 2 +- services/brig/src/Brig/App.hs | 6 - .../brig/src/Brig/CanonicalInterpreter.hs | 28 +-- services/brig/src/Brig/Index/Eval.hs | 20 +- services/brig/src/Brig/User/Search/Index.hs | 9 - services/galley/src/Galley/App.hs | 2 +- 29 files changed, 170 insertions(+), 610 deletions(-) delete mode 100644 changelog.d/0-release-notes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search delete mode 100644 changelog.d/1-api-changes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search delete mode 100644 libs/wire-subsystems/src/Wire/BrigAPIAccess/Local.hs delete mode 100644 libs/wire-subsystems/test/unit/Wire/MockInterpreters/BrigAPIAccess.hs diff --git a/changelog.d/0-release-notes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search b/changelog.d/0-release-notes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search deleted file mode 100644 index 3caefeb19af..00000000000 --- a/changelog.d/0-release-notes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search +++ /dev/null @@ -1,29 +0,0 @@ -`GET /contacts/search` returns apps (and regular users) that collaborate with the searcher's team. - -This means that `brig-index-migrate-data` now requires you to -configure the `elasticsearch-index` chart's postgres setup, and have a -postgres instance reachable with that setup, eg., like this: - -``` -# in (charts/elasticsearch-index/)values.yaml -postgresql: - host: postgresql # DNS name without protocol - port: "5432" - user: wire-server - dbname: wire-server -postgresqlPool: - size: 100 - acquisitionTimeout: 10s - idlenessTimeout: 10m - -postgresMigration: - user: cassandra # (or postgresql, migration-to-postgresql, ...) -``` - -Notes: -- If you have experienced any elasticsearch index update issues since 2026-03-24 (Chart Release 5.29.0), this might be related. If you have not resolved them, consider updating your `values.yaml` now and running a full re-index. -- Note: `brig-index` understands `--user-storage-location`, but that is not relevant here, because collaborators are not technically user accounts, but pointers to user accounts, and they are stored in a different table. - -More info: -- [configuring postgres](https://docs.wire.com/latest/developer/reference/config-options.html?h=config#configure-postgresql) -- [maintaining elastic search](https://docs.wire.com/latest/developer/reference/elastic-search.html?h=elasticsearch) diff --git a/changelog.d/1-api-changes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search b/changelog.d/1-api-changes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search deleted file mode 100644 index d66924aaeea..00000000000 --- a/changelog.d/1-api-changes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search +++ /dev/null @@ -1 +0,0 @@ -Treat team collaborators like team members in contact search. diff --git a/integration/test/Test/TeamCollaborators.hs b/integration/test/Test/TeamCollaborators.hs index 642dad537e8..cf55c3a558e 100644 --- a/integration/test/Test/TeamCollaborators.hs +++ b/integration/test/Test/TeamCollaborators.hs @@ -1,5 +1,3 @@ -{-# OPTIONS_GHC -Wno-ambiguous-fields #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2025 Wire Swiss GmbH @@ -19,9 +17,6 @@ module Test.TeamCollaborators where -import qualified API.Brig as BrigP -import qualified API.BrigInternal as BrigI -import API.Common (randomName) import API.Galley import qualified API.GalleyInternal as Internal import Data.Tuple.Extra @@ -322,104 +317,3 @@ testUpdateCollaborator = do [] >>= assertSuccess postOne2OneConversation bob alice team "chit-chat" >>= assertLabel 403 "operation-denied" - --- | Collaborators are part of the search space of the team they --- collaborate with: `GET /search/contacts` returns them to members of --- that team, just like it returns the team's own members. We test --- collaborators from other teams, personal user accounts that --- collaborate, and app. -testSearchFindsCollaborator :: (HasCallStack) => App () -testSearchFindsCollaborator = do - (owner, team, [alice]) <- createTeam OwnDomain 2 - (otherOwner, otherTeam, [bob, collab1]) <- createTeam OwnDomain 3 - collab2 :: Value <- randomUser OwnDomain def - collab3 :: Value <- - BrigP.createApp otherOwner otherTeam def - `bindResponse` \resp -> resp.json %. "user" - - collab1Name <- collab1 %. "name" & asString - collab2Name <- collab2 %. "name" & asString - collab3Name <- collab3 %. "name" & asString - - collab1Name' <- randomName - collab2Name' <- randomName - collab3Name' <- randomName - - -- Find before any collaborations have been established. - let assertFinds :: - (HasCallStack, MakesValue expectFound, MakesValue searcher) => - String -> - expectFound -> - searcher -> - App () - assertFinds searchTerm expectFound searcher = do - BrigI.refreshIndex OwnDomain - BrigP.searchContacts searcher searchTerm OwnDomain `bindResponse` \resp -> do - resp.status `shouldMatchInt` 200 - foundIds :: [String] <- resp.json %. "documents" >>= asList >>= mapM objId - expectedIds :: [String] <- (make >=> asList >=> mapM objId) expectFound - assertBool - ("found: " <> show foundIds <> "; expected: " <> show expectedIds) - (sort foundIds == sort expectedIds) - - for_ [owner, alice] $ assertFinds collab1Name ([] @Value) - for_ [otherOwner, bob] $ assertFinds collab1Name [collab1] - - for_ [owner, alice] $ assertFinds collab2Name [collab2] - for_ [otherOwner, bob] $ assertFinds collab2Name [collab2] - - for_ [owner, alice] $ assertFinds collab3Name ([] @Value) - for_ [otherOwner, bob] $ assertFinds collab3Name [collab3] - - -- Add collaborators to team - for_ [collab1, collab2, collab3] - $ \collab -> - addTeamCollaborator owner team collab ["implicit_connection"] >>= assertSuccess - - for_ [owner, alice] $ assertFinds collab1Name [collab1] - for_ [otherOwner, bob] $ assertFinds collab1Name [collab1] - - for_ [owner, alice] $ assertFinds collab2Name [collab2] - for_ [otherOwner, bob] $ assertFinds collab2Name [collab2] - - for_ [owner, alice] $ assertFinds collab3Name [collab3] - for_ [otherOwner, bob] $ assertFinds collab3Name [collab3] - - -- Check that updating name does not erase collaborating teams in index. - for_ [(collab1, collab1Name'), (collab2, collab2Name'), (collab3, collab3Name')] - $ \(collab, newName) -> do - let updateBody = (def :: BrigP.PutSelf) {BrigP.name = Just newName} - in BrigP.putSelf collab updateBody >>= assertSuccess - - for_ [owner, alice] $ assertFinds collab1Name' [collab1] - for_ [otherOwner, bob] $ assertFinds collab1Name' [collab1] - - for_ [owner, alice] $ assertFinds collab2Name' [collab2] - for_ [otherOwner, bob] $ assertFinds collab2Name' [collab2] - - for_ [owner, alice] $ assertFinds collab3Name' [collab3] - for_ [otherOwner, bob] $ assertFinds collab3Name' [collab3] - - -- Check that updating collaborating teams does not erase name in index. - for_ [collab1, collab2, collab3] - $ \collab -> do - removeTeamCollaborator owner team collab >>= assertSuccess - - for_ [owner, alice] $ assertFinds collab1Name' ([] @Value) - for_ [otherOwner, bob] $ assertFinds collab1Name' [collab1] - - for_ [owner, alice] $ assertFinds collab2Name' [collab2] - for_ [otherOwner, bob] $ assertFinds collab2Name' [collab2] - - for_ [owner, alice] $ assertFinds collab3Name' ([] @Value) - for_ [otherOwner, bob] $ assertFinds collab3Name' [collab3] - - -- Can one user collaborate in multiple teams without breaking search? - (_thirdOwner, _thirdTeam, [multiCollab]) <- createTeam OwnDomain 2 - multiCollabName <- multiCollab %. "name" & asString - - addTeamCollaborator owner team multiCollab ["implicit_connection"] >>= assertSuccess - addTeamCollaborator otherOwner otherTeam multiCollab ["implicit_connection"] >>= assertSuccess - - for_ [owner, alice] $ assertFinds multiCollabName [multiCollab] - for_ [otherOwner, bob] $ assertFinds multiCollabName [multiCollab] diff --git a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Local.hs b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Local.hs deleted file mode 100644 index d89904b0a8e..00000000000 --- a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Local.hs +++ /dev/null @@ -1,65 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2026 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - --- | Interprets 'BrigAPIAccess' from within brig itself, by calling into the local --- subsystems directly instead of round-tripping over HTTP to itself (as --- 'Wire.BrigAPIAccess.Rpc.interpretBrigAccess' does for every other service). --- --- Only the operations needed by code shared with other services (e.g. --- 'Wire.TeamCollaboratorsSubsystem') are implemented locally. Everything else --- falls back to the RPC handler, pointed at brig itself: correct, but a wasteful --- round-trip through our own listen socket, so it logs a warning and should be --- given a local implementation once something actually relies on it. -module Wire.BrigAPIAccess.Local where - -import Imports -import Polysemy -import Polysemy.Error (Error) -import Polysemy.Input (runInputConst) -import Polysemy.TinyLog (TinyLog) -import Polysemy.TinyLog qualified as Log -import System.Logger.Message qualified as Log -import Util.Options (Endpoint) -import Wire.BrigAPIAccess -import Wire.BrigAPIAccess.Rpc (brigAccessRpcHandler) -import Wire.ParseException (ParseException) -import Wire.Rpc (Rpc) -import Wire.RpcException (RpcException) -import Wire.UserSubsystem (UserSubsystem) -import Wire.UserSubsystem qualified as UserSubsystem - --- | The 'Endpoint' is brig's own; it is only used for the operations that have --- no local implementation yet. -interpretBrigAPIAccessLocally :: - forall r. - ( Member TinyLog r, - Member Rpc r, - Member (Error ParseException) r, - Member (Error RpcException) r - ) => - Endpoint -> - InterpreterFor UserSubsystem r -> - InterpreterFor BrigAPIAccess r -interpretBrigAPIAccessLocally selfEndpoint runUser = interpret $ \case - UpdateSearchIndex uid -> runUser (UserSubsystem.internalUpdateSearchIndex uid) - other -> selfRpc other - where - selfRpc :: forall m x. BrigAPIAccess m x -> Sem r x - selfRpc action = do - Log.warn $ - Log.msg (Log.val "BrigAPIAccess.Local: no local implementation, calling brig over HTTP") - runInputConst selfEndpoint (brigAccessRpcHandler action) diff --git a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs index b96d0abeea0..e42a4791392 100644 --- a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs @@ -82,131 +82,115 @@ interpretBrigAccess :: Sem (BrigAPIAccess ': r) a -> Sem r a interpretBrigAccess brigEndpoint = - interpret $ runInputConst brigEndpoint . brigAccessRpcHandler - --- | Handles a single 'BrigAPIAccess' action by calling brig over HTTP. --- --- Exposed separately from 'interpretBrigAccess' so that --- 'Wire.BrigAPIAccess.Local.interpretBrigAPIAccessLocally' can delegate the --- actions it does not implement itself. 'BrigAPIAccess' is a first-order --- effect, so @m@ is unconstrained and any handler's action can be passed here. -brigAccessRpcHandler :: - ( Member TinyLog r, - Member Rpc r, - Member (Error ParseException) r, - Member (Error RpcException) r, - Member (Input Endpoint) r - ) => - BrigAPIAccess m a -> - Sem r a -brigAccessRpcHandler = \case - GetConnectionsUnqualified uids muids mrel -> do - getConnectionsUnqualified uids muids mrel - GetConnections uids mquids mrel -> do - getConnections uids mquids mrel - PutConnectionInternal uc -> do - putConnectionInternal uc - ReauthUser uid reauth -> do - reAuthUser uid reauth - LookupActivatedUsers uids -> do - lookupActivatedUsers uids - GetUsers uids -> do - getUsers uids - DeleteUser uid -> do - deleteUser uid - GetContactList uid -> do - getContactList uid - GetUserExportData uid -> do - getUserExportData uid - GetSize tid -> do - getSize tid - LookupClients uids -> do - lookupClients uids - LookupClientsFull uids -> do - lookupClientsFull uids - NotifyClientsAboutLegalHoldRequest self other pk -> do - notifyClientsAboutLegalHoldRequest self other pk - GetLegalHoldAuthToken uid mpwd -> do - getLegalHoldAuthToken uid mpwd - AddLegalHoldClientToUserEither uid conn pks lpk -> do - addLegalHoldClientToUser uid conn pks lpk - RemoveLegalHoldClientFromUser uid -> do - removeLegalHoldClientFromUser uid - GetAccountConferenceCallingConfigClient uid -> do - getAccountConferenceCallingConfigClient uid - GetLocalMLSClients qusr ss -> do - getLocalMLSClients qusr ss - GetLocalMLSClient qusr cid ss -> do - getLocalMLSClient qusr cid ss - UpdateSearchVisibilityInbound status -> do - updateSearchVisibilityInbound status - DeleteBot convId botId -> - deleteBot convId botId - UpdateSearchIndex uid -> updateSearchIndex uid - GetAccountsBy localGetBy -> - getAccountsBy localGetBy - GetUsersByVariousKeys uids handles emails includePendingInvitations -> - getUsersByVariousKeys uids handles emails includePendingInvitations - CreateGroupInternal managedBy teamId creatorUserId newGroup -> - createGroupInternal managedBy teamId creatorUserId newGroup - GetGroupsInternal tid mbFilter mbManagedBy startIndex mbCount -> - getGroupsInternal tid mbFilter mbManagedBy startIndex mbCount - GetGroupInternal tid gid includeChannels -> - getGroupInternal tid gid includeChannels - UpdateGroup req -> - updateGroup req - DeleteGroupInternal managedBy teamId groupId -> - deleteGroupInternal managedBy teamId groupId - GetAppIdsForTeam teamId -> - getAppIdsForTeam teamId - SetAccountStatus uid status -> - setAccountStatus uid status - DeleteApp teamId uid -> - deleteApp teamId uid - CreateSAML uref buid teamid name managedBy handle richInfo mLocale role -> - createSAML uref buid teamid name managedBy handle richInfo mLocale role - CreateNoSAML extId email uid teamid uname locale role -> - createNoSAML extId email uid teamid uname locale role - UpdateEmail uid email activation -> - updateEmail uid email activation - GetAccount havePending uid -> - getAccount havePending uid - GetAccountByHandle handle -> - getByHandle handle - GetByEmail email -> - getByEmail email - SetName uid name -> - setName uid name - SetHandle uid handle -> - setHandle uid handle - SetManagedBy uid managedBy -> - setManagedBy uid managedBy - DeletePendingEmailUpdate uid -> - deletePendingEmailUpdate uid - SetSSOId uid ssoId -> - setSSOId uid ssoId - SetRichInfo uid richInfo -> - setRichInfo uid richInfo - SetLocale uid mLocale -> - setLocale uid mLocale - GetRichInfo uid -> - getRichInfo uid - CheckHandleAvailable handle -> - checkHandleAvailable handle - SsoLogin uid mLabel -> - ssoLogin uid mLabel - GetStatus uid -> - getStatus uid - GetStatusMaybe uid -> - getStatusMaybe uid - SetStatus uid status -> - setStatus uid status - GetDefaultUserLocale -> - getDefaultUserLocale - CheckAdminGetTeamId uid -> - checkAdminGetTeamId uid - SendSAMLIdPChangedEmail notif -> - sendSAMLIdPChangedEmail notif + interpret $ + runInputConst brigEndpoint . \case + GetConnectionsUnqualified uids muids mrel -> do + getConnectionsUnqualified uids muids mrel + GetConnections uids mquids mrel -> do + getConnections uids mquids mrel + PutConnectionInternal uc -> do + putConnectionInternal uc + ReauthUser uid reauth -> do + reAuthUser uid reauth + LookupActivatedUsers uids -> do + lookupActivatedUsers uids + GetUsers uids -> do + getUsers uids + DeleteUser uid -> do + deleteUser uid + GetContactList uid -> do + getContactList uid + GetUserExportData uid -> do + getUserExportData uid + GetSize tid -> do + getSize tid + LookupClients uids -> do + lookupClients uids + LookupClientsFull uids -> do + lookupClientsFull uids + NotifyClientsAboutLegalHoldRequest self other pk -> do + notifyClientsAboutLegalHoldRequest self other pk + GetLegalHoldAuthToken uid mpwd -> do + getLegalHoldAuthToken uid mpwd + AddLegalHoldClientToUserEither uid conn pks lpk -> do + addLegalHoldClientToUser uid conn pks lpk + RemoveLegalHoldClientFromUser uid -> do + removeLegalHoldClientFromUser uid + GetAccountConferenceCallingConfigClient uid -> do + getAccountConferenceCallingConfigClient uid + GetLocalMLSClients qusr ss -> do + getLocalMLSClients qusr ss + GetLocalMLSClient qusr cid ss -> do + getLocalMLSClient qusr cid ss + UpdateSearchVisibilityInbound status -> do + updateSearchVisibilityInbound status + DeleteBot convId botId -> + deleteBot convId botId + UpdateSearchIndex uid -> updateSearchIndex uid + GetAccountsBy localGetBy -> + getAccountsBy localGetBy + GetUsersByVariousKeys uids handles emails includePendingInvitations -> + getUsersByVariousKeys uids handles emails includePendingInvitations + CreateGroupInternal managedBy teamId creatorUserId newGroup -> + createGroupInternal managedBy teamId creatorUserId newGroup + GetGroupsInternal tid mbFilter mbManagedBy startIndex mbCount -> + getGroupsInternal tid mbFilter mbManagedBy startIndex mbCount + GetGroupInternal tid gid includeChannels -> + getGroupInternal tid gid includeChannels + UpdateGroup req -> + updateGroup req + DeleteGroupInternal managedBy teamId groupId -> + deleteGroupInternal managedBy teamId groupId + GetAppIdsForTeam teamId -> + getAppIdsForTeam teamId + SetAccountStatus uid status -> + setAccountStatus uid status + DeleteApp teamId uid -> + deleteApp teamId uid + CreateSAML uref buid teamid name managedBy handle richInfo mLocale role -> + createSAML uref buid teamid name managedBy handle richInfo mLocale role + CreateNoSAML extId email uid teamid uname locale role -> + createNoSAML extId email uid teamid uname locale role + UpdateEmail uid email activation -> + updateEmail uid email activation + GetAccount havePending uid -> + getAccount havePending uid + GetAccountByHandle handle -> + getByHandle handle + GetByEmail email -> + getByEmail email + SetName uid name -> + setName uid name + SetHandle uid handle -> + setHandle uid handle + SetManagedBy uid managedBy -> + setManagedBy uid managedBy + DeletePendingEmailUpdate uid -> + deletePendingEmailUpdate uid + SetSSOId uid ssoId -> + setSSOId uid ssoId + SetRichInfo uid richInfo -> + setRichInfo uid richInfo + SetLocale uid mLocale -> + setLocale uid mLocale + GetRichInfo uid -> + getRichInfo uid + CheckHandleAvailable handle -> + checkHandleAvailable handle + SsoLogin uid mLabel -> + ssoLogin uid mLabel + GetStatus uid -> + getStatus uid + GetStatusMaybe uid -> + getStatusMaybe uid + SetStatus uid status -> + setStatus uid status + GetDefaultUserLocale -> + getDefaultUserLocale + CheckAdminGetTeamId uid -> + checkAdminGetTeamId uid + SendSAMLIdPChangedEmail notif -> + sendSAMLIdPChangedEmail notif brigRequest :: (Member Rpc r, Member (Input Endpoint) r) => (Request -> Request) -> Sem r (Response (Maybe LByteString)) brigRequest req = do diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs index 31d77779011..6317ed7ba2d 100644 --- a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs @@ -29,7 +29,6 @@ import Data.Conduit.List qualified as CL import Data.Id import Data.Json.Util (UTCTimeMillis (fromUTCTimeMillis)) import Data.Map qualified as Map -import Data.Set qualified as Set import Database.Bloodhound qualified as ES import Imports import Polysemy @@ -38,7 +37,6 @@ import Polysemy.TinyLog import Polysemy.TinyLog qualified as Log import System.Logger.Message qualified as Log import UnliftIO (pooledForConcurrentlyN) -import Wire.API.Team.Collaborator (gTeam, gUser) import Wire.API.Team.Feature import Wire.API.Team.Member.Info import Wire.API.Team.Role @@ -47,7 +45,6 @@ import Wire.IndexedUserStore (IndexedUserStore) import Wire.IndexedUserStore qualified as IndexedUserStore import Wire.IndexedUserStore.MigrationStore import Wire.IndexedUserStore.MigrationStore qualified as MigrationStore -import Wire.TeamCollaboratorsStore (TeamCollaboratorsStore, getTeamCollaborationsForUsers) import Wire.UserSearch.Migration import Wire.UserSearch.Types import Wire.UserStore @@ -57,28 +54,22 @@ type IOInterpreter r = forall a. Sem r a -> IO a -- | Increase this number any time you want to force reindexing. expectedMigrationVersion :: MigrationVersion -expectedMigrationVersion = MigrationVersion 7 +expectedMigrationVersion = MigrationVersion 6 -syncAllUsers :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r, Member TeamCollaboratorsStore r) => IOInterpreter r -> Int32 -> IO Int +syncAllUsers :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r) => IOInterpreter r -> Int32 -> IO () syncAllUsers interpreter pageSize = syncAllUsersWithVersion interpreter pageSize ES.ExternalGT -forceSyncAllUsers :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r, Member TeamCollaboratorsStore r) => IOInterpreter r -> Int32 -> IO Int +forceSyncAllUsers :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r) => IOInterpreter r -> Int32 -> IO () forceSyncAllUsers interpreter pageSize = syncAllUsersWithVersion interpreter pageSize ES.ExternalGTE --- | Returns the number of users that could not be indexed because some of the --- data needed to build their document was unavailable. Those users have been --- logged individually by 'logAndHush'. -syncAllUsersWithVersion :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r, Member TeamCollaboratorsStore r) => IOInterpreter r -> Int32 -> (ES.ExternalDocVersion -> ES.VersionControl) -> IO Int +syncAllUsersWithVersion :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r) => IOInterpreter r -> Int32 -> (ES.ExternalDocVersion -> ES.VersionControl) -> IO () syncAllUsersWithVersion interpreter pageSize mkVersion = - fmap getSum . runConduit $ + runConduit $ zipSources (CL.sourceList [1 ..]) (paginateWithStateC (interpreter . getIndexUsersPaginated pageSize)) .| logPage .| mkUserDocs - .| Conduit.foldMapM upsertPage + .| Conduit.mapM_ (interpreter . IndexedUserStore.bulkUpsert) where - upsertPage :: (Int, [(ES.DocId, UserDoc, ES.VersionControl)]) -> IO (Sum Int) - upsertPage (skipped, docs) = Sum skipped <$ interpreter (IndexedUserStore.bulkUpsert docs) - logPage :: ConduitT (Int32, [IndexUser]) [IndexUser] IO () logPage = Conduit.mapM $ \(pageNumber, page) -> do interpreter $ @@ -88,9 +79,7 @@ syncAllUsersWithVersion interpreter pageSize mkVersion = . Log.field "firstUser" (maybe "N/A" (idToText . (.userId)) (headMay page)) pure page - -- Emits the documents to be indexed together with the number of users of - -- this page that had to be skipped. - mkUserDocs :: ConduitT [IndexUser] (Int, [(ES.DocId, UserDoc, ES.VersionControl)]) IO () + mkUserDocs :: ConduitT [IndexUser] [(ES.DocId, UserDoc, ES.VersionControl)] IO () mkUserDocs = Conduit.mapM $ \page -> do -- FUTUREWORK: extract team visibilities, roles and user type -- more efficiently sending one query per page @@ -99,10 +88,10 @@ syncAllUsersWithVersion interpreter pageSize mkVersion = -- contains User, Maybe Role, UserType, ..., and pass around -- ExtendedUser. this should make the code less convoluted. - let teams :: Map TeamId [IndexUser] - teams = Map.fromListWith (<>) $ mapMaybe (\u -> (,[u]) <$> u.teamId) page + let teams :: Map TeamId [IndexUser] = Map.fromListWith (<>) $ mapMaybe (\u -> (,[u]) <$> u.teamId) page + teamIds = Map.keys teams - visMap <- fmap Map.fromList . pooledForConcurrentlyN 16 (Map.keys teams) $ \t -> do + visMap <- fmap Map.fromList . pooledForConcurrentlyN 16 teamIds $ \t -> do x <- try $ interpreter $ teamSearchVisibilityInbound t pure (t, x) @@ -125,12 +114,6 @@ syncAllUsersWithVersion interpreter pageSize mkVersion = fmap Map.unions . pooledForConcurrentlyN 16 (Map.toList teams) $ \(t, us) -> getRoles t (fmap (.userId) us) - -- One query for the whole page. A failure here fails every document of the - -- page, which 'logAndHush' then logs and skips. - eithCollabTeams :: Either SomeException (Map UserId [TeamId]) <- - try . fmap (Map.fromListWith (<>) . map (\tc -> (gUser tc, [gTeam tc]))) . interpreter $ - getTeamCollaborationsForUsers (Set.fromList (map (.userId) page)) - let vis :: IndexUser -> Either SomeException SearchVisibilityInbound vis indexUser = fromMaybe (Right defaultSearchVisibilityInbound) $ flip Map.lookup visMap =<< indexUser.teamId @@ -139,20 +122,15 @@ syncAllUsersWithVersion interpreter pageSize mkVersion = mkUserDoc indexUser = do currentVis <- vis indexUser currentRole <- sequence $ Map.lookup indexUser.userId roles - currentCollabTeams <- Map.findWithDefault [] indexUser.userId <$> eithCollabTeams - pure $ indexUserToDoc currentVis ((.value) <$> currentRole) currentCollabTeams indexUser + pure $ indexUserToDoc currentVis ((.value) <$> currentRole) indexUser mkDocVersion :: IndexUser -> Either SomeException ES.VersionControl mkDocVersion u = do roleWithTime <- sequence (Map.lookup u.userId roles) pure . mkVersion . ES.ExternalDocVersion . docVersion $ indexUserToVersion roleWithTime u - docsWithErrors :: (e ~ Either SomeException) => [(ES.DocId, e UserDoc, e ES.VersionControl)] - docsWithErrors = map (\u -> (userIdToDocId u.userId, mkUserDoc u, mkDocVersion u)) page - - docs <- interpreter . flip mapMaybeM docsWithErrors $ logAndHush - let skipped = length docsWithErrors - length docs - pure (skipped, docs) + let docsWithErrors = map (\u -> (userIdToDocId u.userId, mkUserDoc u, mkDocVersion u)) page + interpreter . flip mapMaybeM docsWithErrors $ logAndHush rightSecond :: (a, b) -> (a, Either c b) rightSecond (a, b) = (a, Right b) @@ -181,7 +159,7 @@ syncAllUsersWithVersion interpreter pageSize mkVersion = <$> permissionsToRole tmi.permissions migrateData :: - (Member (Embed IO) r, Member IndexedUserStore r, Member (Error MigrationException) r, Member IndexedUserMigrationStore r, Member TinyLog r, Member UserStore r, Member GalleyAPIAccess r, Member TeamCollaboratorsStore r) => + (Member (Embed IO) r, Member IndexedUserStore r, Member (Error MigrationException) r, Member IndexedUserMigrationStore r, Member TinyLog r, Member UserStore r, Member GalleyAPIAccess r) => IOInterpreter r -> Int32 -> IO () @@ -196,15 +174,8 @@ migrateData interpreter pageSize = interpreter $ do Log.msg (Log.val "Migration necessary.") . Log.field "expectedVersion" expectedMigrationVersion . Log.field "foundVersion" foundVersion - skipped <- embed $ forceSyncAllUsers interpreter pageSize - if skipped == 0 - then MigrationStore.persistMigrationVersion expectedMigrationVersion - else do - Log.err $ - Log.msg (Log.val "Migration incomplete, not persisting migration version.") - . Log.field "expectedVersion" expectedMigrationVersion - . Log.field "skippedUsers" skipped - throw $ SyncIncomplete + embed $ forceSyncAllUsers interpreter pageSize + MigrationStore.persistMigrationVersion expectedMigrationVersion else do Log.info $ Log.msg (Log.val "No migration necessary.") diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs index 07572a29851..156f8f6e479 100644 --- a/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs @@ -526,17 +526,13 @@ matchSelf :: UserId -> Maybe ES.Query matchSelf searcher = Just (termQ "_id" (idToText searcher)) -- | Exclude apps from other teams. --- Apps should only be searchable within their own team, or within a team they --- collaborate with. +-- Apps should only be searchable within their own team. matchAppsFromOtherTeams :: Maybe TeamId -> Maybe ES.Query matchAppsFromOtherTeams mSearcherTeamId = Just $ ES.QueryBoolQuery boolQuery - { -- Apps collaborating with the searcher's team are not excluded. - ES.boolQueryMustNotMatch = - maybeToList (termQ "collaborating_teams" . idToText <$> mSearcherTeamId), - ES.boolQueryMustMatch = + { ES.boolQueryMustMatch = [ -- Match apps (type = "app") termQ "type" "app", -- That are from a different team than the searcher @@ -644,16 +640,7 @@ restrictSearchSpaceByUserType = \case else ES.TermsQuery "type" (userTypeFilterToText <$> (utsH :| utsT)) matchTeamMembersOf :: TeamId -> ES.Query -matchTeamMembersOf team = - ES.QueryBoolQuery - boolQuery - { ES.boolQueryShouldMatch = - [ -- Match users who are members of the team - ES.TermQuery (ES.Term "team" $ idToText team) Nothing, - -- Match users who are collaborators in the team - ES.TermQuery (ES.Term "collaborating_teams" $ idToText team) Nothing - ] - } +matchTeamMembersOf team = ES.TermQuery (ES.Term "team" $ idToText team) Nothing matchTeamMembersSearchableByAllTeams :: ES.Query matchTeamMembersSearchableByAllTeams = diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs index ebf79c96721..fcdf0731b08 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs @@ -29,8 +29,6 @@ data TeamCollaboratorsStore m a where GetAllTeamCollaborators :: TeamId -> TeamCollaboratorsStore m [TeamCollaborator] GetTeamCollaborator :: TeamId -> UserId -> TeamCollaboratorsStore m (Maybe TeamCollaborator) GetTeamCollaborations :: UserId -> TeamCollaboratorsStore m ([TeamCollaborator]) - -- | Batched 'GetTeamCollaborations', for callers that process users in pages. - GetTeamCollaborationsForUsers :: Set UserId -> TeamCollaboratorsStore m [TeamCollaborator] GetTeamCollaboratorsWithIds :: Set TeamId -> Set UserId -> TeamCollaboratorsStore m [TeamCollaborator] UpdateTeamCollaborator :: UserId -> TeamId -> Set CollaboratorPermission -> TeamCollaboratorsStore m () RemoveTeamCollaborator :: UserId -> TeamId -> TeamCollaboratorsStore m () diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs index b898ae69b51..a6a1e968a72 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs @@ -49,7 +49,6 @@ interpretTeamCollaboratorsStoreToPostgres = GetAllTeamCollaborators teamId -> getAllTeamCollaboratorsImpl teamId GetTeamCollaborator teamId userId -> getTeamCollaboratorImpl teamId userId GetTeamCollaborations userId -> getTeamCollaborationsImpl userId - GetTeamCollaborationsForUsers userIds -> getTeamCollaborationsForUsersImpl userIds GetTeamCollaboratorsWithIds teamIds userIds -> getTeamCollaboratorsWithIdsImpl teamIds userIds UpdateTeamCollaborator userId teamId permissions -> updateTeamCollaboratorImpl userId teamId permissions RemoveTeamCollaborator userId teamId -> removeTeamCollaboratorImpl userId teamId @@ -182,22 +181,6 @@ getTeamCollaborationsImpl teamId = do select user_id :: uuid, team_id :: uuid, permissions :: int2[] from collaborators where user_id = ($1 :: uuid) |] -getTeamCollaborationsForUsersImpl :: - (PGConstraints r) => - Set UserId -> - Sem r [TeamCollaborator] -getTeamCollaborationsForUsersImpl userIds = do - runStatement (Data.Set.toList userIds) getAllCollaborationsByUsersStatement - where - getAllCollaborationsByUsersStatement :: Statement [UserId] [TeamCollaborator] - getAllCollaborationsByUsersStatement = - dimap - (Data.Vector.fromList . Imports.map toUUID) - (Data.Vector.toList . (toTeamCollaborator <$>)) - $ [vectorStatement| - select user_id :: uuid, team_id :: uuid, permissions :: int2[] from collaborators where user_id = ANY($1 :: uuid[]) - |] - getTeamCollaboratorsWithIdsImpl :: (PGConstraints r) => Set TeamId -> diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs index bb0541636b0..30a970706e3 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs @@ -29,8 +29,6 @@ import Wire.API.Error.Brig qualified as E import Wire.API.Event.Team import Wire.API.Team.Collaborator import Wire.API.Team.Member qualified as TeamMember -import Wire.BrigAPIAccess (BrigAPIAccess) -import Wire.BrigAPIAccess qualified as BrigAPIAccess import Wire.Error import Wire.NotificationSubsystem import Wire.Sem.Now @@ -46,18 +44,15 @@ interpretTeamCollaboratorsSubsystem :: Member Now r, Member NotificationSubsystem r ) => - InterpreterFor BrigAPIAccess r -> InterpreterFor TeamCollaboratorsSubsystem r -interpretTeamCollaboratorsSubsystem brigAPIAccess = - interpret $ - brigAPIAccess . \case - CreateTeamCollaborator zUser user team perms -> createTeamCollaboratorImpl zUser user team perms - GetAllTeamCollaborators zUser team -> getAllTeamCollaboratorsImpl zUser team - InternalGetTeamCollaborator team user -> internalGetTeamCollaboratorImpl team user - InternalGetTeamCollaborations userId -> internalGetTeamCollaborationsImpl userId - InternalGetTeamCollaboratorsWithIds teams userIds -> internalGetTeamCollaboratorsWithIdsImpl teams userIds - InternalUpdateTeamCollaborator user team perms -> internalUpdateTeamCollaboratorImpl user team perms - InternalRemoveTeamCollaborator user team -> internalRemoveTeamCollaboratorImpl user team +interpretTeamCollaboratorsSubsystem = interpret $ \case + CreateTeamCollaborator zUser user team perms -> createTeamCollaboratorImpl zUser user team perms + GetAllTeamCollaborators zUser team -> getAllTeamCollaboratorsImpl zUser team + InternalGetTeamCollaborator team user -> internalGetTeamCollaboratorImpl team user + InternalGetTeamCollaborations userId -> internalGetTeamCollaborationsImpl userId + InternalGetTeamCollaboratorsWithIds teams userIds -> internalGetTeamCollaboratorsWithIdsImpl teams userIds + InternalUpdateTeamCollaborator user team perms -> internalUpdateTeamCollaboratorImpl user team perms + InternalRemoveTeamCollaborator user team -> internalRemoveTeamCollaboratorImpl user team internalGetTeamCollaboratorImpl :: (Member Store.TeamCollaboratorsStore r) => @@ -79,8 +74,7 @@ createTeamCollaboratorImpl :: Member (Error TeamCollaboratorsError) r, Member Store.TeamCollaboratorsStore r, Member Now r, - Member NotificationSubsystem r, - Member BrigAPIAccess r + Member NotificationSubsystem r ) => Local UserId -> UserId -> @@ -91,11 +85,9 @@ createTeamCollaboratorImpl zUser user team perms = do guardPermission (tUnqualified zUser) team TeamMember.GetTeamCollaborators InsufficientRights Store.createTeamCollaborator user team perms + -- TODO: Review the event's values generateTeamEvents (tUnqualified zUser) team [EdCollaboratorAdd user (Set.toList perms)] - -- Reindex the collaborator with their new collaboration team - BrigAPIAccess.updateSearchIndex user - getAllTeamCollaboratorsImpl :: ( Member TeamSubsystem r, Member (Error TeamCollaboratorsError) r, @@ -117,25 +109,21 @@ internalGetTeamCollaboratorsWithIdsImpl = do Store.getTeamCollaboratorsWithIds internalUpdateTeamCollaboratorImpl :: - (Member Store.TeamCollaboratorsStore r, Member BrigAPIAccess r) => + (Member Store.TeamCollaboratorsStore r) => UserId -> TeamId -> Set CollaboratorPermission -> Sem r () internalUpdateTeamCollaboratorImpl user team perms = do Store.updateTeamCollaborator user team perms - -- Reindex collaborator when permissions change - BrigAPIAccess.updateSearchIndex user internalRemoveTeamCollaboratorImpl :: - (Member Store.TeamCollaboratorsStore r, Member BrigAPIAccess r) => + (Member Store.TeamCollaboratorsStore r) => UserId -> TeamId -> Sem r () internalRemoveTeamCollaboratorImpl user team = do Store.removeTeamCollaborator user team - -- Reindex collaborator when removed - BrigAPIAccess.updateSearchIndex user -- This is of general usefulness. However, we cannot move this to wire-api as -- this would lead to a cyclic dependency. diff --git a/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs b/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs index 7cbbbcd734e..817d10370e9 100644 --- a/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs +++ b/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs @@ -42,7 +42,6 @@ data MigrationException | PutMappingFailed String | TargetIndexAbsent | VersionSourceMissing (ES.SearchResult MigrationVersion) - | SyncIncomplete deriving (Show) instance Exception MigrationException diff --git a/libs/wire-subsystems/src/Wire/UserSearch/Types.hs b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs index 5464dae2a8f..5e8dcac765e 100644 --- a/libs/wire-subsystems/src/Wire/UserSearch/Types.hs +++ b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs @@ -79,10 +79,7 @@ data UserDoc = UserDoc udScimExternalId :: Maybe Text, udSso :: Maybe Sso, udEmailUnvalidated :: Maybe EmailAddress, - udSearchable :: Maybe Bool, - -- | Teams that have added this user as a collaborator. - -- Updated separately via 'syncUserIndexCollaborations' when collaborator relationships change. - udCollaboratingTeams :: [TeamId] + udSearchable :: Maybe Bool } deriving (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UserDoc) @@ -107,8 +104,7 @@ instance ToJSON UserDoc where "scim_external_id" .= udScimExternalId ud, "sso" .= udSso ud, "email_unvalidated" .= udEmailUnvalidated ud, - "searchable" .= udSearchable ud, - "collaborating_teams" .= udCollaboratingTeams ud + "searchable" .= udSearchable ud ] instance FromJSON UserDoc where @@ -132,7 +128,6 @@ instance FromJSON UserDoc where <*> o .:? "sso" <*> o .:? "email_unvalidated" <*> o .:? "searchable" - <*> o .:? "collaborating_teams" .!= [] searchVisibilityInboundFieldName :: Key searchVisibilityInboundFieldName = "search_visibility_inbound" diff --git a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs index b051132f1ed..09ac630d191 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs @@ -126,8 +126,8 @@ indexUserToVersion :: Maybe (WithWritetime Role) -> IndexUser -> IndexVersion indexUserToVersion role iu = mkIndexVersion [Just $ Writetime iu.updatedAt, const () <$$> fmap writetime role] -indexUserToDoc :: SearchVisibilityInbound -> Maybe Role -> [TeamId] -> IndexUser -> UserDoc -indexUserToDoc searchVisInbound mRole collaboratingTeams IndexUser {..} = +indexUserToDoc :: SearchVisibilityInbound -> Maybe Role -> IndexUser -> UserDoc +indexUserToDoc searchVisInbound mRole IndexUser {..} = if shouldIndex then UserDoc @@ -148,8 +148,7 @@ indexUserToDoc searchVisInbound mRole collaboratingTeams IndexUser {..} = udHandle = handle, udNormalized = Just $ normalized name.fromName, udName = Just name, - udTeam = teamId, - udCollaboratingTeams = collaboratingTeams + udTeam = teamId } else -- We insert a tombstone-style user here, as it's easier than -- deleting the old one. It's mostly empty, but having the status here @@ -210,6 +209,5 @@ emptyUserDoc uid = udNormalized = Nothing, udName = Nothing, udTeam = Nothing, - udCollaboratingTeams = [], udId = uid } diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index f78029cebde..d5cb2dfee62 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -60,7 +60,6 @@ import Wire.API.Federation.Error import Wire.API.MLS.CipherSuite (CipherSuiteTag, csSignatureScheme) import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti (TeamStatus (..)) -import Wire.API.Team.Collaborator (gTeam) import Wire.API.Team.Export import Wire.API.Team.Feature import Wire.API.Team.Member @@ -103,8 +102,6 @@ import Wire.Sem.Metrics qualified as Metrics import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredUser -import Wire.TeamCollaboratorsStore (TeamCollaboratorsStore) -import Wire.TeamCollaboratorsStore qualified as TeamCollaboratorsStore import Wire.TeamSubsystem import Wire.UserGroupStore (UserGroupStore, getUserGroupIdsForUsers) import Wire.UserKeyStore @@ -144,7 +141,6 @@ runUserSubsystem :: Member TinyLog r, Member (Input UserSubsystemConfig) r, Member TeamSubsystem r, - Member TeamCollaboratorsStore r, Member UserGroupStore r, Member (Input (Local any)) r ) => @@ -715,7 +711,6 @@ updateUserProfileImpl :: Member Events r, Member GalleyAPIAccess r, Member IndexedUserStore r, - Member TeamCollaboratorsStore r, Member Metrics r ) => Local UserId -> @@ -777,7 +772,6 @@ updateHandleImpl :: Member Events r, Member UserStore r, Member IndexedUserStore r, - Member TeamCollaboratorsStore r, Member Metrics r ) => Local UserId -> @@ -845,8 +839,7 @@ syncUserIndex :: ( Member UserStore r, Member GalleyAPIAccess r, Member IndexedUserStore r, - Member Metrics r, - Member TeamCollaboratorsStore r + Member Metrics r ) => UserId -> Sem r () @@ -867,13 +860,9 @@ syncUserIndex uid = teamSearchVisibilityInbound indexUser.teamId tm <- maybe (pure Nothing) selectTeamMember indexUser.teamId - collabTeams <- map gTeam <$> TeamCollaboratorsStore.getTeamCollaborations uid let mRole = tm >>= mkRoleWithWriteTime - userDoc = indexUserToDoc vis (value <$> mRole) collabTeams indexUser - -- GTE, not GT: the version comes from the user row alone, but the document also - -- holds data that changes without touching that row (collaborations), and under - -- GT those updates would be dropped as version conflicts. Older writes still lose. - version = ES.ExternalGTE . ES.ExternalDocVersion . docVersion $ indexUserToVersion mRole indexUser + userDoc = indexUserToDoc vis (value <$> mRole) indexUser + version = ES.ExternalGT . ES.ExternalDocVersion . docVersion $ indexUserToVersion mRole indexUser Metrics.incCounter indexUpdateCounter IndexedUserStore.upsert (userIdToDocId uid) userDoc version @@ -1191,7 +1180,6 @@ acceptTeamInvitationImpl :: Member (Error UserSubsystemError) r, Member InvitationStore r, Member IndexedUserStore r, - Member TeamCollaboratorsStore r, Member Metrics r, Member Events r, Member AuthenticationSubsystem r, @@ -1256,7 +1244,6 @@ removeEmailEitherImpl :: Member UserStore r, Member Events r, Member IndexedUserStore r, - Member TeamCollaboratorsStore r, Member (Input UserSubsystemConfig) r, Member GalleyAPIAccess r, Member Metrics r @@ -1293,7 +1280,6 @@ setUserSearchableImpl :: Member TeamSubsystem r, Member GalleyAPIAccess r, Member IndexedUserStore r, - Member TeamCollaboratorsStore r, Member Metrics r ) => Local UserId -> diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index ff27a27fa7f..1324d919db3 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -365,21 +365,22 @@ miniBackendLowerEffectsInterpreters mb@(MiniBackendParams {..}) = . runInputConst conversationCfg . runClientSubsystem undefined undefined where + -- Mock BrigAPIAccess interpreter for tests + mockBrigAPIAccess :: forall r'. InterpreterFor BrigAPIAccess r' + mockBrigAPIAccess = interpret $ \case + _ -> error "Unimplemented BrigAPIAccess operation in mock" -- Mock UserClientIndexStore interpreter for tests mockUserClientIndexStore :: forall r'. InterpreterFor UserClientIndexStore r' mockUserClientIndexStore = interpret $ \case _ -> error "Unimplemented UserClientIndexStore operation in mock" - -- Mock BackendNotificationQueueAccess interpreter for tests mockBackendNotificationQueueAccess :: forall r'. InterpreterFor BackendNotificationQueueAccess r' mockBackendNotificationQueueAccess = interpret $ \case _ -> error "Unimplemented BackendNotificationQueueAccess operation in mock" - -- Mock ConversationSubsystem interpreter for tests mockConversationSubsystem :: forall r'. InterpreterFor ConversationSubsystem r' mockConversationSubsystem = interpretH $ \case _ -> error "Unimplemented ConversationSubsystem operation in mock" - mockMlsKeyPackageSubsystem :: forall r'. InterpreterFor MlsKeyPackageSubsystem r' mockMlsKeyPackageSubsystem = interpret $ \case HasMlsKeyPackages {} -> pure False @@ -785,7 +786,7 @@ interpretMaybeFederationStackState :: Sem (MiniBackendEffects `Append` r) a -> Sem r (MiniBackend, a) interpretMaybeFederationStackState mb = - miniBackendLowerEffectsInterpreters mb . interpretTeamCollaboratorsSubsystem subsume . runRecursiveAuthUserApp + miniBackendLowerEffectsInterpreters mb . interpretTeamCollaboratorsSubsystem . runRecursiveAuthUserApp -- FUTUREWORK(fisx): it would be nice to have a definition of an -- interpreter of all the subsystems combined, but since the diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs index ea57140aad5..4630c0c7f77 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs @@ -25,7 +25,6 @@ import Wire.MockInterpreters.AppStore as MockInterpreters import Wire.MockInterpreters.AuthenticationSubsystem as MockInterpreters import Wire.MockInterpreters.BackgroundJobPublisher as MockInterpreters import Wire.MockInterpreters.BlockListStore as MockInterpreters -import Wire.MockInterpreters.BrigAPIAccess as MockInterpreters import Wire.MockInterpreters.ClientStore as MockInterpreters import Wire.MockInterpreters.ConversationStore as MockInterpreters import Wire.MockInterpreters.ConversationSubsystem as MockInterpreters diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/BrigAPIAccess.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/BrigAPIAccess.hs deleted file mode 100644 index aeb76299093..00000000000 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/BrigAPIAccess.hs +++ /dev/null @@ -1,81 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2026 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Wire.MockInterpreters.BrigAPIAccess where - -import Imports -import Polysemy -import Wire.BrigAPIAccess - --- | Errors out on everything except 'UpdateSearchIndex', which is a no-op. -mockBrigAPIAccess :: InterpreterFor BrigAPIAccess r -mockBrigAPIAccess = interpret $ \case - UpdateSearchIndex _ -> pure () - -- everything else is not implemented - GetConnectionsUnqualified {} -> error "GetConnectionsUnqualified: implement on demand (mockBrigAPIAccess)" - GetConnections {} -> error "GetConnections: implement on demand (mockBrigAPIAccess)" - PutConnectionInternal {} -> error "PutConnectionInternal: implement on demand (mockBrigAPIAccess)" - ReauthUser {} -> error "ReauthUser: implement on demand (mockBrigAPIAccess)" - LookupActivatedUsers {} -> error "LookupActivatedUsers: implement on demand (mockBrigAPIAccess)" - GetUsers {} -> error "GetUsers: implement on demand (mockBrigAPIAccess)" - DeleteUser {} -> error "DeleteUser: implement on demand (mockBrigAPIAccess)" - GetContactList {} -> error "GetContactList: implement on demand (mockBrigAPIAccess)" - GetSize {} -> error "GetSize: implement on demand (mockBrigAPIAccess)" - LookupClients {} -> error "LookupClients: implement on demand (mockBrigAPIAccess)" - LookupClientsFull {} -> error "LookupClientsFull: implement on demand (mockBrigAPIAccess)" - NotifyClientsAboutLegalHoldRequest {} -> error "NotifyClientsAboutLegalHoldRequest: implement on demand (mockBrigAPIAccess)" - GetLegalHoldAuthToken {} -> error "GetLegalHoldAuthToken: implement on demand (mockBrigAPIAccess)" - AddLegalHoldClientToUserEither {} -> error "AddLegalHoldClientToUserEither: implement on demand (mockBrigAPIAccess)" - RemoveLegalHoldClientFromUser {} -> error "RemoveLegalHoldClientFromUser: implement on demand (mockBrigAPIAccess)" - GetAccountConferenceCallingConfigClient {} -> error "GetAccountConferenceCallingConfigClient: implement on demand (mockBrigAPIAccess)" - GetLocalMLSClients {} -> error "GetLocalMLSClients: implement on demand (mockBrigAPIAccess)" - GetLocalMLSClient {} -> error "GetLocalMLSClient: implement on demand (mockBrigAPIAccess)" - UpdateSearchVisibilityInbound {} -> error "UpdateSearchVisibilityInbound: implement on demand (mockBrigAPIAccess)" - GetUserExportData {} -> error "GetUserExportData: implement on demand (mockBrigAPIAccess)" - DeleteBot {} -> error "DeleteBot: implement on demand (mockBrigAPIAccess)" - GetAccountsBy {} -> error "GetAccountsBy: implement on demand (mockBrigAPIAccess)" - GetUsersByVariousKeys {} -> error "GetUsersByVariousKeys: implement on demand (mockBrigAPIAccess)" - CreateGroupInternal {} -> error "CreateGroupInternal: implement on demand (mockBrigAPIAccess)" - GetGroupInternal {} -> error "GetGroupInternal: implement on demand (mockBrigAPIAccess)" - GetGroupsInternal {} -> error "GetGroupsInternal: implement on demand (mockBrigAPIAccess)" - UpdateGroup {} -> error "UpdateGroup: implement on demand (mockBrigAPIAccess)" - DeleteGroupInternal {} -> error "DeleteGroupInternal: implement on demand (mockBrigAPIAccess)" - DeleteApp {} -> error "DeleteApp: implement on demand (mockBrigAPIAccess)" - GetAppIdsForTeam {} -> error "GetAppIdsForTeam: implement on demand (mockBrigAPIAccess)" - SetAccountStatus {} -> error "SetAccountStatus: implement on demand (mockBrigAPIAccess)" - CreateSAML {} -> error "CreateSAML: implement on demand (mockBrigAPIAccess)" - CreateNoSAML {} -> error "CreateNoSAML: implement on demand (mockBrigAPIAccess)" - UpdateEmail {} -> error "UpdateEmail: implement on demand (mockBrigAPIAccess)" - GetAccount {} -> error "GetAccount: implement on demand (mockBrigAPIAccess)" - GetAccountByHandle {} -> error "GetAccountByHandle: implement on demand (mockBrigAPIAccess)" - GetByEmail {} -> error "GetByEmail: implement on demand (mockBrigAPIAccess)" - SetName {} -> error "SetName: implement on demand (mockBrigAPIAccess)" - SetHandle {} -> error "SetHandle: implement on demand (mockBrigAPIAccess)" - SetManagedBy {} -> error "SetManagedBy: implement on demand (mockBrigAPIAccess)" - DeletePendingEmailUpdate {} -> error "DeletePendingEmailUpdate: implement on demand (mockBrigAPIAccess)" - SetSSOId {} -> error "SetSSOId: implement on demand (mockBrigAPIAccess)" - SetRichInfo {} -> error "SetRichInfo: implement on demand (mockBrigAPIAccess)" - SetLocale {} -> error "SetLocale: implement on demand (mockBrigAPIAccess)" - GetRichInfo {} -> error "GetRichInfo: implement on demand (mockBrigAPIAccess)" - CheckHandleAvailable {} -> error "CheckHandleAvailable: implement on demand (mockBrigAPIAccess)" - SsoLogin {} -> error "SsoLogin: implement on demand (mockBrigAPIAccess)" - GetStatus {} -> error "GetStatus: implement on demand (mockBrigAPIAccess)" - GetStatusMaybe {} -> error "GetStatusMaybe: implement on demand (mockBrigAPIAccess)" - SetStatus {} -> error "SetStatus: implement on demand (mockBrigAPIAccess)" - GetDefaultUserLocale {} -> error "GetDefaultUserLocale: implement on demand (mockBrigAPIAccess)" - CheckAdminGetTeamId {} -> error "CheckAdminGetTeamId: implement on demand (mockBrigAPIAccess)" - SendSAMLIdPChangedEmail {} -> error "SendSAMLIdPChangedEmail: implement on demand (mockBrigAPIAccess)" diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs index 63a334527ab..4def51eeef7 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs @@ -41,8 +41,6 @@ inMemoryTeamCollaboratorsStoreInterpreter = gets $ \(s :: Map TeamId [TeamCollaborator]) -> find (\tc -> tc.gUser == userId) =<< Map.lookup teamId s GetTeamCollaborations userId -> gets $ \(s :: Map TeamId [TeamCollaborator]) -> concatMap (filter (\tc -> tc.gUser == userId)) (Map.elems s) - GetTeamCollaborationsForUsers userIds -> - gets $ \(s :: Map TeamId [TeamCollaborator]) -> concatMap (filter (\tc -> tc.gUser `elem` userIds)) (Map.elems s) GetTeamCollaboratorsWithIds teamIds userIds -> gets $ \(s :: Map TeamId [TeamCollaborator]) -> concatMap (concatMap (filter (\tc -> tc.gUser `elem` userIds)) . (\(tid :: TeamId) -> Map.lookup tid s)) teamIds diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs index 043f3a834db..786f733240f 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs @@ -87,7 +87,7 @@ inMemoryUserSubsystemInterpreter = BlockListInsert _ -> error "BlockListInsert: implement on demand (userSubsystemInterpreter)" UpdateTeamSearchVisibilityInbound _ -> error "UpdateTeamSearchVisibilityInbound: implement on demand (userSubsystemInterpreter)" AcceptTeamInvitation {} -> error "AcceptTeamInvitation: implement on demand (userSubsystemInterpreter)" - InternalUpdateSearchIndex {} -> error "InternalUpdateSearchIndex: implement on demand (userSubsystemInterpreter)" + InternalUpdateSearchIndex _ -> error "InternalUpdateSearchIndex: implement on demand (userSubsystemInterpreter)" InternalFindTeamInvitation {} -> error "InternalFindTeamInvitation: implement on demand (userSubsystemInterpreter)" GetUserExportData _ -> error "GetUserExportData: implement on demand (userSubsystemInterpreter)" RemoveEmailEither _ -> error "RemoveEmailEither: implement on demand (userSubsystemInterpreter)" diff --git a/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs index f623e0012dd..6861f097797 100644 --- a/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs @@ -42,7 +42,7 @@ import Wire.API.User as User import Wire.API.User.Scim import Wire.API.UserGroup import Wire.BrigAPIAccess (BrigAPIAccess (..)) -import Wire.MockInterpreters hiding (mockBrigAPIAccess) +import Wire.MockInterpreters import Wire.ScimSubsystem import Wire.ScimSubsystem.Interpreter import Wire.StoredUser diff --git a/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs index ea721887393..a09d56bd8ff 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs @@ -50,7 +50,6 @@ userDoc1 = UserDoc { udId = fromJust . hush . parseIdFromText $ "0a96b396-57d6-11ea-a04b-7b93d1a5c19c", udTeam = hush . parseIdFromText $ "17c59b18-57d6-11ea-9220-8bbf5eee961a", - udCollaboratingTeams = either (error . show) (: []) . parseIdFromText $ "17c59b18-57d6-11ea-9220-8bbf5eee961a", udName = Just . Name $ "Carl Phoomp", udNormalized = Just $ "carl phoomp", udHandle = Just . fromJust . parseHandle $ "phoompy", @@ -69,5 +68,6 @@ userDoc1 = udType = Nothing } +-- Dont touch this. This represents serialized legacy data. userDoc1ByteString :: LByteString -userDoc1ByteString = "{\"collaborating_teams\":[\"17c59b18-57d6-11ea-9220-8bbf5eee961a\"],\"email\":\"phoompy@example.com\",\"account_status\":\"active\",\"handle\":\"phoompy\",\"managed_by\":\"scim\",\"role\":\"admin\",\"accent_id\":32,\"name\":\"Carl Phoomp\",\"created_at\":\"2020-08-29T21:50:00.000Z\",\"team\":\"17c59b18-57d6-11ea-9220-8bbf5eee961a\",\"id\":\"0a96b396-57d6-11ea-a04b-7b93d1a5c19c\",\"normalized\":\"carl phoomp\",\"saml_idp\":\"https://issuer.net/214234\"}" +userDoc1ByteString = "{\"email\":\"phoompy@example.com\",\"account_status\":\"active\",\"handle\":\"phoompy\",\"managed_by\":\"scim\",\"role\":\"admin\",\"accent_id\":32,\"name\":\"Carl Phoomp\",\"created_at\":\"2020-08-29T21:50:00.000Z\",\"team\":\"17c59b18-57d6-11ea-9220-8bbf5eee961a\",\"id\":\"0a96b396-57d6-11ea-a04b-7b93d1a5c19c\",\"normalized\":\"carl phoomp\",\"saml_idp\":\"https://issuer.net/214234\"}" diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index 9e309d50dda..68942a77cb3 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -1110,7 +1110,7 @@ spec = describe "UserSubsystem.Interpreter" do searchee = searcheeNoHandle {handle = Just searcheeHandle} :: StoredUser storedUserToDoc :: StoredUser -> UserDoc - storedUserToDoc user = indexUserToDoc defaultSearchVisibilityInbound Nothing [] (storedUserToIndexUser user) + storedUserToDoc user = indexUserToDoc defaultSearchVisibilityInbound Nothing (storedUserToIndexUser user) indexFromStoredUsers :: [StoredUser] -> UserIndex indexFromStoredUsers storedUsers = do diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index abfa57d8442..220d4252fb5 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -244,7 +244,6 @@ library Wire.BoundedQueue Wire.BoundedQueue.STM Wire.BrigAPIAccess - Wire.BrigAPIAccess.Local Wire.BrigAPIAccess.Rpc Wire.BudgetStore Wire.BudgetStore.Cassandra @@ -653,7 +652,6 @@ test-suite wire-subsystems-tests Wire.MockInterpreters.AuthenticationSubsystem Wire.MockInterpreters.BackgroundJobPublisher Wire.MockInterpreters.BlockListStore - Wire.MockInterpreters.BrigAPIAccess Wire.MockInterpreters.ClientStore Wire.MockInterpreters.ConversationStore Wire.MockInterpreters.ConversationSubsystem diff --git a/services/background-worker/src/Wire/Effects.hs b/services/background-worker/src/Wire/Effects.hs index 66dbee09c56..0d367f19eed 100644 --- a/services/background-worker/src/Wire/Effects.hs +++ b/services/background-worker/src/Wire/Effects.hs @@ -367,7 +367,7 @@ runBackgroundWorkerEffects env extEnv requestId mJobId = ) . runFeaturesConfigSubsystem . runInputSem getAllTeamFeaturesForServer - . interpretTeamCollaboratorsSubsystem (interpretBrigAccess env.brigEndpoint) + . interpretTeamCollaboratorsSubsystem . discardMeetingNotifier . interpretConversationSubsystem where diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 27812d06b47..6c2145ea8cd 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -36,7 +36,6 @@ module Brig.App cargoholdLens, galleyLens, galleyEndpointLens, - brigEndpointLens, sparEndpointLens, gundeckEndpointLens, cargoholdEndpointLens, @@ -190,10 +189,6 @@ data Env = Env { cargohold :: RPC.Request, galley :: RPC.Request, galleyEndpoint :: Endpoint, - -- | Brig's own listen address. Used only to call ourselves over HTTP for - -- 'BrigAPIAccess' operations that have no local implementation yet; see - -- 'Wire.BrigAPIAccess.Local'. - brigEndpoint :: Endpoint, sparEndpoint :: Endpoint, gundeckEndpoint :: Endpoint, cargoholdEndpoint :: Endpoint, @@ -312,7 +307,6 @@ newEnv opts = do { cargohold = mkEndpoint $ opts.cargohold, galley = mkEndpoint $ opts.galley, galleyEndpoint = opts.galley, - brigEndpoint = opts.brig, sparEndpoint = opts.spar, gundeckEndpoint = opts.gundeck, cargoholdEndpoint = opts.cargohold, diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index e6103cb4722..d44c91648d3 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -47,7 +47,6 @@ import Polysemy.Input (Input, runInputConst) import Polysemy.Internal.Kind import Polysemy.Resource import Polysemy.TinyLog (TinyLog) -import Util.Options (Endpoint) import Wire.API.Error (ErrorS, errorToWai) import Wire.API.Error.Galley import Wire.API.Federation.Client qualified @@ -69,8 +68,6 @@ import Wire.BackgroundJobsPublisher (BackgroundJobPublisher) import Wire.BackgroundJobsPublisher.RabbitMQ (interpretBackgroundJobPublisherRabbitMQ) import Wire.BlockListStore import Wire.BlockListStore.Cassandra -import Wire.BrigAPIAccess (BrigAPIAccess) -import Wire.BrigAPIAccess.Local (interpretBrigAPIAccessLocally) import Wire.BudgetStore import Wire.BudgetStore.Cassandra import Wire.ClientStore (ClientStore) @@ -133,7 +130,6 @@ import Wire.PropertySubsystem.Interpreter import Wire.RateLimit import Wire.RateLimit.Interpreter import Wire.Rpc -import Wire.RpcException (RpcException) import Wire.SAMLEmailSubsystem import Wire.SAMLEmailSubsystem.Interpreter import Wire.SFT (SFT, interpretSFT) @@ -193,12 +189,13 @@ type RecursiveEffects = '[ AuthenticationSubsystem, UserSubsystem, AppSubsystem, - ClientSubsystem, - BrigAPIAccess, - TeamCollaboratorsSubsystem + ClientSubsystem ] -type NonRecursiveEffects2 = BrigLowerLevelEffects +type NonRecursiveEffects2 = + '[ TeamCollaboratorsSubsystem + ] + `Append` BrigLowerLevelEffects -- | These effects have interpreters which don't depend on each other type BrigLowerLevelEffects = @@ -279,7 +276,6 @@ type BrigLowerLevelEffects = Embed Cas.Client, Error ClientError, Error ParseException, - Error RpcException, Error ErrorCall, Error SomeException, Error HttpError, @@ -301,11 +297,9 @@ type BrigLowerLevelEffects = -- Cloned from "Wire.MiniBackend". runRecursiveEffects :: (Members NonRecursiveEffects2 r) => - -- | Brig's own endpoint; see 'interpretBrigAPIAccessLocally'. - Endpoint -> Sem (RecursiveEffects `Append` r) a -> Sem r a -runRecursiveEffects selfEndpoint = runTeamCollaborators . runBrigAPIAccess . runClient . runApp . runUser . runAuth +runRecursiveEffects = runClient . runApp . runUser . runAuth where runAuth :: forall r. (Members NonRecursiveEffects2 r) => InterpreterFor AuthenticationSubsystem r runAuth = interpretAuthenticationSubsystem runUser @@ -319,12 +313,6 @@ runRecursiveEffects selfEndpoint = runTeamCollaborators . runBrigAPIAccess . run runClient :: forall r. (Members NonRecursiveEffects2 r) => InterpreterFor ClientSubsystem r runClient = runClientSubsystem runAuth runUser - runBrigAPIAccess :: forall r. (Members NonRecursiveEffects2 r) => InterpreterFor BrigAPIAccess r - runBrigAPIAccess = interpretBrigAPIAccessLocally selfEndpoint runUser - - runTeamCollaborators :: forall r. (Members NonRecursiveEffects2 r) => InterpreterFor TeamCollaboratorsSubsystem r - runTeamCollaborators = interpretTeamCollaboratorsSubsystem runBrigAPIAccess - runBrigToIO :: App.Env -> AppT BrigCanonicalEffects a -> IO a runBrigToIO e (AppT ma) = do let blockedDomains = @@ -445,7 +433,6 @@ runBrigToIO e (AppT ma) = do . rethrowHttpErrorIO . runError @SomeException . mapError @ErrorCall SomeException - . mapError @RpcException SomeException . mapError @ParseException SomeException . mapError clientErrorToHttpError . interpretClientToIO e.casClient @@ -523,7 +510,8 @@ runBrigToIO e (AppT ma) = do . interpretTeamCollaboratorsStoreToPostgres . interpretTeamSubsystemToGalleyAPI . samlEmailSubsystemInterpreter - . runRecursiveEffects e.brigEndpoint + . interpretTeamCollaboratorsSubsystem + . runRecursiveEffects . interpretUserGroupSubsystem . maybe runEnterpriseLoginSubsystemNoConfig diff --git a/services/brig/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs index 827d2028309..f931a032769 100644 --- a/services/brig/src/Brig/Index/Eval.hs +++ b/services/brig/src/Brig/Index/Eval.hs @@ -54,7 +54,6 @@ import Polysemy.TinyLog (TinyLog) import System.Logger qualified as Log import System.Logger.Class (Logger) import Util.Options -import Wire.API.Team.Collaborator (TeamCollaboratorsError) import Wire.ClientSubsystem.Error (ClientError) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess.Rpc @@ -70,8 +69,6 @@ import Wire.Rpc import Wire.Sem.Logger.TinyLog import Wire.Sem.Metrics (Metrics) import Wire.Sem.Metrics.IO -import Wire.TeamCollaboratorsStore (TeamCollaboratorsStore) -import Wire.TeamCollaboratorsStore.Postgres (interpretTeamCollaboratorsStoreToPostgres) import Wire.UserKeyStore (UserKeyStore) import Wire.UserKeyStore.Cassandra import Wire.UserSearch.Migration (MigrationException) @@ -82,7 +79,6 @@ import Wire.UserStore.Postgres (interpretUserStorePostgres) type BrigIndexEffectStack = [ UserKeyStore, UserStore, - TeamCollaboratorsStore, IndexedUserStore, Error IndexedUserStoreError, IndexedUserMigrationStore, @@ -95,7 +91,6 @@ type BrigIndexEffectStack = TinyLog, Input Hasql.Pool, Error UsageError, - Error TeamCollaboratorsError, Error ClientError, Resource, Race, @@ -148,7 +143,6 @@ runSem (mgr, casClient, pgPool, bhEnv, indexedUserStoreConfig, reqId, migrationI . interpretRace . runResource . throwErrorToIOFinal @ClientError - . throwErrorToIOFinal @TeamCollaboratorsError . throwPostgresUsageErrorToIOFinal . runInputConst pgPool . loggerToTinyLogReqId reqId logger @@ -161,7 +155,6 @@ runSem (mgr, casClient, pgPool, bhEnv, indexedUserStoreConfig, reqId, migrationI . interpretIndexedUserMigrationStoreES bhEnv migrationIndexName . throwErrorToIOFinal @IndexedUserStoreError . interpretIndexedUserStoreES indexedUserStoreConfig - . interpretTeamCollaboratorsStoreToPostgres . userStoreInterpreter . interpretUserKeyStoreCassandra casClient $ action @@ -188,14 +181,10 @@ runCommand l = \case runIndexIO e $ resetIndex (mkCreateIndexSettings es) Reindex es cas pg userStorageLocation galley pageSize -> do semDeps <- mkSemDeps (es ^. esConnection) cas pg l - skipped <- IndexedUserStoreBulk.syncAllUsers (runSem semDeps userStorageLocation galley l) pageSize - when (skipped /= 0) do - throwM . IndexMigrationError $ "Reindex: failed to sync " <> show skipped <> " documents." + IndexedUserStoreBulk.syncAllUsers (runSem semDeps userStorageLocation galley l) pageSize ReindexSameOrNewer es cas pg userStorageLocation galley pageSize -> do semDeps <- mkSemDeps (es ^. esConnection) cas pg l - skipped <- IndexedUserStoreBulk.forceSyncAllUsers (runSem semDeps userStorageLocation galley l) pageSize - when (skipped /= 0) do - throwM . IndexMigrationError $ "ReindexSameOrNewer: failed to sync " <> show skipped <> " documents." + IndexedUserStoreBulk.forceSyncAllUsers (runSem semDeps userStorageLocation galley l) pageSize UpdateMapping esConn galley -> do e <- initIndex l esConn galley runIndexIO e updateMapping @@ -276,11 +265,6 @@ waitForTaskToComplete timeoutSeconds taskNodeId = do errTaskGet :: ES.EsError -> m x errTaskGet e = throwM $ ReindexFromAnotherIndexError $ "Error response while getting task: " <> show e -newtype IndexMigrationError = IndexMigrationError String - deriving (Show) - -instance Exception IndexMigrationError - newtype ReindexFromAnotherIndexError = ReindexFromAnotherIndexError String deriving (Show) diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index 68a17f07b1f..4c4919729d9 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -364,15 +364,6 @@ indexMapping = mpAnalyzer = Nothing, mpFields = mempty }, - -- teams this user collaborates with (without being a member of them) - "collaborating_teams" - .= MappingProperty - { mpType = MPKeyword, - mpStore = False, - mpIndex = True, - mpAnalyzer = Nothing, - mpFields = mempty - }, "accent_id" .= MappingProperty { mpType = MPByte, diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index f47dc7f3798..4d755d8c612 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -572,7 +572,7 @@ evalGalley e = . interpretTeamSubsystem teamSubsystemConfig . runFeaturesConfigSubsystem . runInputSem getAllTeamFeaturesForServer - . interpretTeamCollaboratorsSubsystem (interpretBrigAccess (e ^. brig)) + . interpretTeamCollaboratorsSubsystem . runFederationSubsystem conversationSubsystemConfig.federationProtocols . runInputConst (e ^. reqId) . interpretJobSubsystem From 9a39b9cb4a87f21de51d3e6bcd3eea5c6008aedf Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Thu, 3 Sep 2026 12:23:16 +0200 Subject: [PATCH 10/29] WPB-28377: migrate gundeck presence from redis to PostGreSQL (#5493) --- Makefile | 4 +- .../0-release-notes/WPB-28377-remove-redis | 17 ++ .../WPB-28377-gundeck-presence-postgres | 1 + charts/databases-ephemeral/requirements.yaml | 7 - .../databases-ephemeral/templates/NOTES.txt | 1 - .../templates/integration-integration.yaml | 27 +- charts/integration/templates/secret.yaml | 6 - charts/reaper/.helmignore | 21 -- charts/reaper/Chart.yaml | 10 - charts/reaper/README.md | 71 ----- charts/reaper/scripts/reaper.sh | 89 ------- charts/reaper/templates/_helpers.tpl | 26 -- charts/reaper/templates/configmap.yaml | 10 - charts/reaper/templates/deployment.yaml | 81 ------ charts/reaper/templates/rbac.yaml | 38 --- charts/reaper/values.yaml | 45 ---- charts/redis-ephemeral/Chart.yaml | 4 - charts/redis-ephemeral/requirements.yaml | 5 - charts/redis-ephemeral/values.yaml | 60 ----- charts/wire-server/templates/_helpers.tpl | 40 --- .../templates/cannon/statefulset.yaml | 2 +- .../templates/gundeck/configmap.yaml | 24 +- .../templates/gundeck/deployment.yaml | 49 +--- .../templates/gundeck/redis-ca-secret.yaml | 30 --- .../wire-server/templates/gundeck/secret.yaml | 13 +- .../templates/gundeck/tests/configmap.yaml | 7 - .../gundeck/tests/gundeck-integration.yaml | 37 --- .../templates/gundeck/tests/secret.yaml | 6 - charts/wire-server/values.yaml | 40 +-- deploy/dockerephemeral/docker-compose.yaml | 142 ---------- .../docker/redis-master-mode.conf | 1 - .../docker/redis-node-1-cert.pem | 19 -- .../docker/redis-node-1-key.pem | 28 -- .../dockerephemeral/docker/redis-node-1.conf | 17 -- .../docker/redis-node-2-cert.pem | 19 -- .../docker/redis-node-2-key.pem | 28 -- .../dockerephemeral/docker/redis-node-2.conf | 17 -- .../docker/redis-node-3-cert.pem | 19 -- .../docker/redis-node-3-key.pem | 28 -- .../dockerephemeral/docker/redis-node-3.conf | 17 -- .../docker/redis-node-4-cert.pem | 19 -- .../docker/redis-node-4-key.pem | 28 -- .../dockerephemeral/docker/redis-node-4.conf | 17 -- .../docker/redis-node-5-cert.pem | 19 -- .../docker/redis-node-5-key.pem | 28 -- .../dockerephemeral/docker/redis-node-5.conf | 17 -- .../docker/redis-node-6-cert.pem | 19 -- .../docker/redis-node-6-key.pem | 28 -- .../dockerephemeral/docker/redis-node-6.conf | 17 -- docs/src/developer/developer/building.md | 1 - .../src/developer/reference/config-options.md | 94 +------ .../install/infrastructure-configuration.md | 5 +- docs/src/how-to/install/troubleshooting.md | 4 +- hack/bin/gen-certs.sh | 13 - hack/helm_vars/certs/values.yaml.gotmpl | 53 ---- hack/helm_vars/redis-ephemeral/values.yaml | 47 ---- hack/helm_vars/wire-server/values.yaml.gotmpl | 20 +- hack/helmfile.yaml.gotmpl | 36 --- libs/wire-api/src/Wire/API/Presence.hs | 7 +- .../Test/Wire/API/Golden/Manual/Presence.hs | 3 - .../20260828093750-gundeck-presence.sql | 12 + .../src/Wire/JobSubsystem/Migrations.hs | 4 +- libs/wire-subsystems/src/Wire/Postgres.hs | 1 + postgres-schema.sql | 30 +++ services/brig/src/Brig/Run.hs | 5 + services/gundeck/default.nix | 17 +- services/gundeck/gundeck.cabal | 14 +- services/gundeck/gundeck.integration.yaml | 23 +- services/gundeck/src/Gundeck/Env.hs | 81 +----- services/gundeck/src/Gundeck/Monad.hs | 73 ----- services/gundeck/src/Gundeck/Options.hs | 32 +-- services/gundeck/src/Gundeck/Presence.hs | 4 +- services/gundeck/src/Gundeck/Presence/Data.hs | 252 ++++++++++-------- services/gundeck/src/Gundeck/Push.hs | 2 +- .../gundeck/src/Gundeck/Push/Websocket.hs | 6 +- services/gundeck/src/Gundeck/Redis.hs | 127 --------- services/gundeck/src/Gundeck/Run.hs | 32 ++- services/gundeck/src/Gundeck/Util/Redis.hs | 61 ----- services/gundeck/test/integration/API.hs | 67 +---- services/gundeck/test/integration/Main.hs | 9 +- .../gundeck/test/integration/TestSetup.hs | 8 +- services/gundeck/test/integration/Util.hs | 119 --------- services/gundeck/test/unit/MockGundeck.hs | 1 - services/integration.yaml | 6 - 84 files changed, 328 insertions(+), 2239 deletions(-) create mode 100644 changelog.d/0-release-notes/WPB-28377-remove-redis create mode 100644 changelog.d/5-internal/WPB-28377-gundeck-presence-postgres delete mode 100644 charts/reaper/.helmignore delete mode 100644 charts/reaper/Chart.yaml delete mode 100644 charts/reaper/README.md delete mode 100755 charts/reaper/scripts/reaper.sh delete mode 100644 charts/reaper/templates/_helpers.tpl delete mode 100644 charts/reaper/templates/configmap.yaml delete mode 100644 charts/reaper/templates/deployment.yaml delete mode 100644 charts/reaper/templates/rbac.yaml delete mode 100644 charts/reaper/values.yaml delete mode 100644 charts/redis-ephemeral/Chart.yaml delete mode 100644 charts/redis-ephemeral/requirements.yaml delete mode 100644 charts/redis-ephemeral/values.yaml delete mode 100644 charts/wire-server/templates/gundeck/redis-ca-secret.yaml delete mode 100644 deploy/dockerephemeral/docker/redis-master-mode.conf delete mode 100644 deploy/dockerephemeral/docker/redis-node-1-cert.pem delete mode 100644 deploy/dockerephemeral/docker/redis-node-1-key.pem delete mode 100644 deploy/dockerephemeral/docker/redis-node-1.conf delete mode 100644 deploy/dockerephemeral/docker/redis-node-2-cert.pem delete mode 100644 deploy/dockerephemeral/docker/redis-node-2-key.pem delete mode 100644 deploy/dockerephemeral/docker/redis-node-2.conf delete mode 100644 deploy/dockerephemeral/docker/redis-node-3-cert.pem delete mode 100644 deploy/dockerephemeral/docker/redis-node-3-key.pem delete mode 100644 deploy/dockerephemeral/docker/redis-node-3.conf delete mode 100644 deploy/dockerephemeral/docker/redis-node-4-cert.pem delete mode 100644 deploy/dockerephemeral/docker/redis-node-4-key.pem delete mode 100644 deploy/dockerephemeral/docker/redis-node-4.conf delete mode 100644 deploy/dockerephemeral/docker/redis-node-5-cert.pem delete mode 100644 deploy/dockerephemeral/docker/redis-node-5-key.pem delete mode 100644 deploy/dockerephemeral/docker/redis-node-5.conf delete mode 100644 deploy/dockerephemeral/docker/redis-node-6-cert.pem delete mode 100644 deploy/dockerephemeral/docker/redis-node-6-key.pem delete mode 100644 deploy/dockerephemeral/docker/redis-node-6.conf delete mode 100644 hack/helm_vars/redis-ephemeral/values.yaml create mode 100644 libs/wire-subsystems/postgres-migrations/20260828093750-gundeck-presence.sql delete mode 100644 services/gundeck/src/Gundeck/Redis.hs delete mode 100644 services/gundeck/src/Gundeck/Util/Redis.hs delete mode 100644 services/gundeck/test/integration/Util.hs diff --git a/Makefile b/Makefile index 2a3250275e6..0e2e771c983 100644 --- a/Makefile +++ b/Makefile @@ -13,11 +13,11 @@ CHARTS_INTEGRATION := wire-server databases-ephemeral rabbitmq fake-aws ingre # (e.g. move charts/brig to charts/wire-server/brig) # this list could be generated from the folder names under ./charts/ like so: # CHARTS_RELEASE := $(shell find charts/ -maxdepth 1 -type d | xargs -n 1 basename | grep -v charts) -CHARTS_RELEASE := wire-server redis-ephemeral rabbitmq rabbitmq-external databases-ephemeral \ +CHARTS_RELEASE := wire-server rabbitmq rabbitmq-external databases-ephemeral \ fake-aws fake-aws-s3 fake-aws-sqs aws-ingress fluent-bit kibana backoffice \ calling-test demo-smtp elasticsearch-curator elasticsearch-external \ elasticsearch-ephemeral minio-external cassandra-external \ -ingress-nginx-controller nginx-ingress-services reaper \ +ingress-nginx-controller nginx-ingress-services \ k8ssandra-test-cluster ldap-scim-bridge wire-server-enterprise \ wire-ingress KIND_CLUSTER_NAME := wire-server diff --git a/changelog.d/0-release-notes/WPB-28377-remove-redis b/changelog.d/0-release-notes/WPB-28377-remove-redis new file mode 100644 index 00000000000..b7f75b78e85 --- /dev/null +++ b/changelog.d/0-release-notes/WPB-28377-remove-redis @@ -0,0 +1,17 @@ +Gundeck no longer uses redis: presence tracking is stored in PostgreSQL. + +Operators must: + +- Remove redis deployments that were only used by gundeck, and the gundeck + `redis:` and `redisAdditionalWrite:` configuration, the `REDIS_USERNAME`, + `REDIS_PASSWORD`, `REDIS_ADDITIONAL_WRITE_USERNAME` and + `REDIS_ADDITIONAL_WRITE_PASSWORD` environment variables, and gundeck redis + TLS secrets (`redisUsername`/`redisPassword`/`redisAdditionalWrite*` secrets + and the redis CA certificates). +- Add the new required configuration `gundeck.config.postgresql` (plus + `postgresqlPool`, and optionally `secrets.pgPassword` for the password file), + following the same format as brig's postgresql settings. +- Restart all cannons after deployment is successful. Presence data does + not carry over, this will make sure all clients reconnect after the + presence data is being written to PostgreSQL. +- Stop deploying `redis-ephemeral` and `reaper`, these have been removed. diff --git a/changelog.d/5-internal/WPB-28377-gundeck-presence-postgres b/changelog.d/5-internal/WPB-28377-gundeck-presence-postgres new file mode 100644 index 00000000000..3862322c3a1 --- /dev/null +++ b/changelog.d/5-internal/WPB-28377-gundeck-presence-postgres @@ -0,0 +1 @@ +Gundeck presence tracking moved from redis to postgres; redis and the gundeck redis configuration options are gone. (WPB-28377) diff --git a/charts/databases-ephemeral/requirements.yaml b/charts/databases-ephemeral/requirements.yaml index 74dbd7594d8..fff1534c387 100644 --- a/charts/databases-ephemeral/requirements.yaml +++ b/charts/databases-ephemeral/requirements.yaml @@ -13,13 +13,6 @@ dependencies: # since cassandra-migrations did not yet run; but the cassandra-migrations hook # requires all pods to be in a 'Ready' state before starting (condition for post-install); this is impossible. ##################################################### -- name: redis-ephemeral - version: "0.0.42" - repository: "file://../redis-ephemeral" - tags: - - redis-ephemeral - - databases-ephemeral - - demo - name: elasticsearch-ephemeral version: "0.0.42" repository: "file://../elasticsearch-ephemeral" diff --git a/charts/databases-ephemeral/templates/NOTES.txt b/charts/databases-ephemeral/templates/NOTES.txt index 2e2ad5b0592..7f07b9ed7ca 100644 --- a/charts/databases-ephemeral/templates/NOTES.txt +++ b/charts/databases-ephemeral/templates/NOTES.txt @@ -2,7 +2,6 @@ You now have an in-memory, non-persistent, non-highly-available set of databases * cassandra-ephemeral * elasticsearch-ephemeral -* redis-ephemeral !! WARNING WARNING !! This is fine for testing and demo purposes, but NOT for a production use case. diff --git a/charts/integration/templates/integration-integration.yaml b/charts/integration/templates/integration-integration.yaml index 9fea9fbde3e..4841ef372b7 100644 --- a/charts/integration/templates/integration-integration.yaml +++ b/charts/integration/templates/integration-integration.yaml @@ -41,6 +41,10 @@ spec: configMap: name: "gundeck" + - name: "gundeck-secrets" + secret: + secretName: "gundeck" + - name: "cargohold-config" configMap: name: "cargohold" @@ -93,9 +97,6 @@ spec: secret: secretName: {{ .Values.config.elasticsearch.tlsCaSecretRef.name }} - - name: redis-ca - secret: - secretName: {{ .Values.config.redis.tlsCaSecretRef.name }} - name: rabbitmq-ca secret: @@ -237,6 +238,9 @@ spec: - name: gundeck-config mountPath: /etc/wire/gundeck/conf + - name: gundeck-secrets + mountPath: /etc/wire/gundeck/secrets + - name: cargohold-config mountPath: /etc/wire/cargohold/conf @@ -276,9 +280,6 @@ spec: - name: elasticsearch-ca mountPath: /etc/wire/brig/elasticsearch-ca - - name: redis-ca - mountPath: /etc/wire/gundeck/redis-ca - - name: rabbitmq-ca mountPath: /etc/wire/brig/rabbitmq-ca @@ -343,20 +344,6 @@ spec: - name: ENABLE_FEDERATION_V{{$version}} value: "1" {{- end }} - {{- if hasKey .Values.secrets "redisUsername" }} - - name: REDIS_USERNAME - valueFrom: - secretKeyRef: - name: integration - key: redisUsername - {{- end }} - {{- if hasKey .Values.secrets "redisPassword" }} - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: integration - key: redisPassword - {{- end }} - name: TEST_XML value: /tmp/result.xml {{- if .Values.config.uploadXml }} diff --git a/charts/integration/templates/secret.yaml b/charts/integration/templates/secret.yaml index 32f6085176e..34e6698ed2f 100644 --- a/charts/integration/templates/secret.yaml +++ b/charts/integration/templates/secret.yaml @@ -16,10 +16,4 @@ data: {{- if hasKey . "uploadXmlAwsSecretAccessKey" }} uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} {{- end }} - {{- if hasKey . "redisUsername" }} - redisUsername: {{ .redisUsername | b64enc | quote }} - {{- end }} - {{- if hasKey . "redisPassword" }} - redisPassword: {{ .redisPassword | b64enc | quote }} - {{- end }} {{- end }} diff --git a/charts/reaper/.helmignore b/charts/reaper/.helmignore deleted file mode 100644 index f0c13194444..00000000000 --- a/charts/reaper/.helmignore +++ /dev/null @@ -1,21 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*~ -# Various IDEs -.project -.idea/ -*.tmproj diff --git a/charts/reaper/Chart.yaml b/charts/reaper/Chart.yaml deleted file mode 100644 index 131654fa443..00000000000 --- a/charts/reaper/Chart.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -version: 0.0.42 -name: reaper -appVersion: 0.1.0 -description: A helm charts to restart cannons if redis-ephemeal has died -annotations: - # must conform to https://github.com/helm/community/blob/main/hips/hip-0015.md - helm.sh/images: | - - name: kubectl - image: docker.io/alpine/kubectl:1.36.3 diff --git a/charts/reaper/README.md b/charts/reaper/README.md deleted file mode 100644 index f4b73e4e670..00000000000 --- a/charts/reaper/README.md +++ /dev/null @@ -1,71 +0,0 @@ -Reaper ------- - -This pod is useful in the following scenario: You run wire-server alongside a single -redis-ephemeral (part of databases-ephemeral). If you have a different setup for redis, -do not use this chart. - -Due to the nature of pods and their ephemerality, there might be situations where a -redis-ephemeral pod is restarted. In such cases, wire clients will have stale -connections (they will have an active websocket connection, but gundeck (responsible for -sending messages) will be unaware of this (as the record of who is connected where is -gone with a redis-ephemeral restart). So these stale clients will not receive any -messages. Here, this reaper will check that the `redis-ephemeral` pod is older than any -other `cannon`; if that is not the case, it kills the `cannon`s forcing clients to -reconnect. - -Image ------ - -The reaper runs `scripts/reaper.sh` through `kubectl`, so `image` must point at a -kubectl image that **contains a POSIX shell** at `/bin/sh`. Distroless kubectl images -do not ship one and the pod will fail to start. The script itself is POSIX sh, so -busybox `ash` is enough, bash not required. - -The image is fully configurable: - -```yaml -image: - registry: docker.io # set to "" for an unqualified repository - repository: alpine/kubectl - tag: 1.36.3 - digest: "" # e.g. "sha256:..."; takes precedence over tag - pullPolicy: IfNotPresent -imagePullSecrets: - - name: my-pull-secret -``` - -RBAC ----- - -The chart creates a namespaced `Role`/`RoleBinding` granting `get`, `list`, `watch` and -`delete` on pods, bound to a `-reaper` ServiceAccount. - -`watch` is required even though the script never watches anything explicitly: -`kubectl delete pod` blocks until the pod is gone and opens a watch to do so. Without it -the reaper deletes the first cannon and then hangs, without crashing. - -Earlier versions bound the ServiceAccount to `cluster-admin` through a fixed-name -`ClusterRoleBinding`, which gave the pod read access to every Secret in the cluster. -`helm upgrade` removes that binding and the old `reaper-role` ServiceAccount. Because -nothing is cluster-scoped any more and all names are release-scoped, several reaper -releases can now coexist in one cluster; previously a second release failed to install -with a `ClusterRoleBinding` ownership conflict. - -Runtime -------- - -The container runs as uid/gid 65534 with a read-only root filesystem and has resource -requests and limits. `nodeSelector`, `tolerations` and `affinity` are honoured. - -`checkIntervalSeconds` (default `15`) controls how long the script waits between checks. -Earlier versions listed pods once per second. - -Logs distinguish a failure to reach the API from "there are no matching pods", and -include the underlying error: - - Failed to list pods: Error from server (Forbidden): ... Skipping this iteration... - No cannon pods found. Doing nothing... - -Both cases previously printed `Failed to list pods. Skipping this iteration...`, so a -reaper that could not list pods at all looked exactly like an idle one. diff --git a/charts/reaper/scripts/reaper.sh b/charts/reaper/scripts/reaper.sh deleted file mode 100755 index f67049e76a6..00000000000 --- a/charts/reaper/scripts/reaper.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/sh - -# See the readme of the reaper chart. -# -# This is POSIX sh on purpose: the only actively maintained kubectl images that -# ship busybox ash, not bash. - -# we loop forever, and on transient errors sleep and try again. -# setting -e would crash the pod on transient e.g. network errors, which isn't useful. -set -u -# shellcheck disable=SC3040 # busybox ash supports pipefail -set -o pipefail - -USAGE="$0 [INTERVAL_SECONDS]" -NAMESPACE="${1:?$USAGE}" -INTERVAL="${2:-15}" - -echo "Using namespace: $NAMESPACE, check interval: ${INTERVAL}s" - -kill_all_cannons() { - echo "Killing all cannons" - RAW_PODS=$(kubectl -n "$NAMESPACE" get pods 2>&1) || { - echo "Failed to list cannon pods: $RAW_PODS. Skipping this iteration..." - return - } - CANNON_PODS=$(echo "$RAW_PODS" | grep -e "cannon" | awk '{ print $1 }') || CANNON_PODS="" - - # A here-document rather than a pipeline, so the loop runs in the current - # shell and the `exit 1` below actually terminates the script. - while IFS= read -r cannon; do - if [ -n "$cannon" ]; then - echo "Deleting $cannon" - # If a single delete fails, we skip it but keep going. - kubectl -n "$NAMESPACE" delete pod "$cannon" || { - echo "Failed to delete pod $cannon, crash reaper and try again" - exit 1 - } - fi - done <&1) || { - echo "Failed to list pods: $RAW_PODS. Skipping this iteration..." - sleep "$INTERVAL" - continue - } - - # Gather all pods that contain "cannon" or "redis-ephemeral", sorted by creation time - ALL_PODS=$(echo "$RAW_PODS" | grep -e "cannon" -e "redis-ephemeral") || ALL_PODS="" - - # Check if we have any cannon pods at all - if ! echo "$ALL_PODS" | grep -q "cannon"; then - echo "No cannon pods found. Doing nothing..." - sleep "$INTERVAL" - continue - fi - - # Check if we have any redis-ephemeral pods at all - if ! echo "$ALL_PODS" | grep -q "redis-ephemeral"; then - echo "No redis-ephemeral pod found. Doing nothing..." - sleep "$INTERVAL" - continue - fi - - # At this point, we have both cannon and redis-ephemeral pods in ALL_PODS - # Check which is oldest - FIRST_POD=$(echo "$ALL_PODS" | head -n 1 | awk '{ print $1 }') - - if [ -z "$FIRST_POD" ]; then - echo "Could not determine the oldest pod from the list. Doing nothing..." - sleep "$INTERVAL" - continue - fi - - case "$FIRST_POD" in - *redis-ephemeral*) - echo "redis-ephemeral is the oldest pod, all good." - ;; - *) - kill_all_cannons - ;; - esac - - sleep "$INTERVAL" -done diff --git a/charts/reaper/templates/_helpers.tpl b/charts/reaper/templates/_helpers.tpl deleted file mode 100644 index 47fc05fa161..00000000000 --- a/charts/reaper/templates/_helpers.tpl +++ /dev/null @@ -1,26 +0,0 @@ -{{/* Allow KubeVersion to be overridden. */}} -{{- define "kubeVersion" -}} - {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} -{{- end -}} - -{{- define "includeSecurityContext" -}} - {{- (semverCompare ">= 1.24-0" (include "kubeVersion" .)) -}} -{{- end -}} - -{{/* Fully qualified image reference, digest taking precedence over tag. */}} -{{- define "reaper.image" -}} -{{- $repository := .Values.image.repository -}} -{{- if .Values.image.registry -}} -{{- $repository = printf "%s/%s" .Values.image.registry .Values.image.repository -}} -{{- end -}} -{{- if .Values.image.digest -}} -{{- printf "%s@%s" $repository .Values.image.digest -}} -{{- else -}} -{{- printf "%s:%s" $repository (.Values.image.tag | toString) -}} -{{- end -}} -{{- end -}} - -{{/* Release-scoped name for the ServiceAccount, Role and RoleBinding. */}} -{{- define "reaper.serviceAccountName" -}} -{{- printf "%s-reaper" .Release.Name | trunc 63 | trimSuffix "-" -}} -{{- end -}} diff --git a/charts/reaper/templates/configmap.yaml b/charts/reaper/templates/configmap.yaml deleted file mode 100644 index 571e81a1f4a..00000000000 --- a/charts/reaper/templates/configmap.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: reaper-script - labels: - app: reaper -data: - reaper.sh: |- - {{- .Files.Get "scripts/reaper.sh" | nindent 4 }} - diff --git a/charts/reaper/templates/deployment.yaml b/charts/reaper/templates/deployment.yaml deleted file mode 100644 index 9d50439dc5c..00000000000 --- a/charts/reaper/templates/deployment.yaml +++ /dev/null @@ -1,81 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: reaper - labels: - app: reaper - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -spec: - replicas: 1 - selector: - matchLabels: - app: reaper - release: {{ .Release.Name }} - template: - metadata: - labels: - app: reaper - release: {{ .Release.Name }} - annotations: - # Ensure changes to the script cause a redeployment upon `helm upgrade` - checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} - spec: - serviceAccountName: {{ include "reaper.serviceAccountName" . }} - automountServiceAccountToken: true - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - topologySpreadConstraints: - - maxSkew: 1 - topologyKey: "kubernetes.io/hostname" - whenUnsatisfiable: ScheduleAnyway - labelSelector: - matchLabels: - app: reaper - containers: - - name: reaper - image: {{ include "reaper.image" . | quote }} - imagePullPolicy: {{ default "" .Values.image.pullPolicy | quote }} - command: ["/bin/sh", "/app/reaper.sh", "{{ .Release.Namespace }}", "{{ .Values.checkIntervalSeconds }}"] - {{- if eq (include "includeSecurityContext" .) "true" }} - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 12 }} - {{- end }} - env: - # kubectl writes its discovery cache below $HOME; the root - # filesystem is read-only, so point it at the emptyDir. - - name: HOME - value: /tmp - volumeMounts: - - name: reaper-script - mountPath: /app - readOnly: true - - name: tmp - mountPath: /tmp - resources: -{{ toYaml .Values.resources | indent 12 }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - volumes: - - name: reaper-script - configMap: - name: reaper-script - defaultMode: 0755 - items: - - key: reaper.sh - path: reaper.sh - - name: tmp - emptyDir: {} diff --git a/charts/reaper/templates/rbac.yaml b/charts/reaper/templates/rbac.yaml deleted file mode 100644 index 5e4caafb6f2..00000000000 --- a/charts/reaper/templates/rbac.yaml +++ /dev/null @@ -1,38 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "reaper.serviceAccountName" . }} - labels: - app: reaper - release: {{ .Release.Name }} ---- -# The reaper only ever lists and deletes pods in its own namespace, so a -# namespaced Role is sufficient. -# `watch` is required even though the script never watches anything explicitly. -kind: Role -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: {{ include "reaper.serviceAccountName" . }} - labels: - app: reaper - release: {{ .Release.Name }} -rules: - - apiGroups: [""] - resources: ["pods"] - verbs: ["get", "list", "watch", "delete"] ---- -kind: RoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: {{ include "reaper.serviceAccountName" . }} - labels: - app: reaper - release: {{ .Release.Name }} -roleRef: - kind: Role - name: {{ include "reaper.serviceAccountName" . }} - apiGroup: rbac.authorization.k8s.io -subjects: - - kind: ServiceAccount - name: {{ include "reaper.serviceAccountName" . }} - namespace: {{ .Release.Namespace }} diff --git a/charts/reaper/values.yaml b/charts/reaper/values.yaml deleted file mode 100644 index cf2f56cf9e4..00000000000 --- a/charts/reaper/values.yaml +++ /dev/null @@ -1,45 +0,0 @@ -image: - # The reaper executes a shell script through kubectl, so this image must - # contain a POSIX shell at /bin/sh. Distroless kubectl images do not - # ship one and the pod will fail to start with them. - # - # Set `registry` to "" to use an unqualified repository (e.g. when mirroring - # into a registry configured as the daemon default). - registry: docker.io - repository: alpine/kubectl - tag: 1.36.3 - # Optional: pin by digest (e.g. "sha256:abc..."). Takes precedence over `tag`. - digest: "" - pullPolicy: IfNotPresent - -imagePullSecrets: [] - -# How long to wait between two checks, in seconds. The condition this chart -# watches for (a redis-ephemeral restart) is rare, so there is no reason to poll -# the API server aggressively. -checkIntervalSeconds: 15 - -resources: - requests: - memory: 32Mi - cpu: 10m - limits: - memory: 64Mi - -nodeSelector: {} -tolerations: [] -affinity: {} - -# Applied as the container securityContext. runAsUser/runAsGroup are set -# explicitly because alpine/kubectl runs as root by default. -podSecurityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - runAsNonRoot: true - runAsUser: 65534 - runAsGroup: 65534 - seccompProfile: - type: RuntimeDefault diff --git a/charts/redis-ephemeral/Chart.yaml b/charts/redis-ephemeral/Chart.yaml deleted file mode 100644 index c907a999576..00000000000 --- a/charts/redis-ephemeral/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -description: Wrapper chart for https://github.com/groundhog2k/helm-charts/tree/redis-1.3.8/charts/redis -name: redis-ephemeral -version: 0.0.42 diff --git a/charts/redis-ephemeral/requirements.yaml b/charts/redis-ephemeral/requirements.yaml deleted file mode 100644 index 0cdad0dfbc4..00000000000 --- a/charts/redis-ephemeral/requirements.yaml +++ /dev/null @@ -1,5 +0,0 @@ -dependencies: -- name: redis - version: 1.3.8 - repository: https://groundhog2k.github.io/helm-charts/ - alias: redis-ephemeral diff --git a/charts/redis-ephemeral/values.yaml b/charts/redis-ephemeral/values.yaml deleted file mode 100644 index aafcf440e40..00000000000 --- a/charts/redis-ephemeral/values.yaml +++ /dev/null @@ -1,60 +0,0 @@ -redis-ephemeral: - image: - tag: "7.4.6" - - haMode: - enabled: false - - redisConfig: | - # dont write rdb to emptyDir disk for an ephemeral setup - save "" - -# To add a password add the following to redisConfig: -# requirepass my-plaintext-password - - -# How to enable SSL connections: -# -# Add the following lines to redisConfig: -# -# port 0 -# tls-port 6379 -# tls-cert-file /data/ssl/tls.crt -# tls-key-file /data/ssl/tls.key -# tls-ca-cert-file /data/ssl/ca.crt -# tls-auth-clients no -# -# Mount the certificate and adjust probes to use SSL -# -# redis-ephemeral: -# extraRedisSecrets: -# - name: redis-certificate -# mountPath: /data/ssl -# -# livenessProbe: -# enabled: false -# customLivenessProbe: -# exec: -# command: -# - sh -# - -c -# - redis-cli --tls --cacert /data/ssl/ca.crt ping -# -# readinessProbe: -# enabled: false -# customReadinessProbe: -# exec: -# command: -# - sh -# - -c -# - redis-cli --tls --cacert /data/ssl/ca.crt ping -# -# startupProbe: -# enabled: false -# customStartupProbe: -# exec: -# command: -# - sh -# - -c -# - redis-cli --tls --cacert /data/ssl/ca.crt ping -# diff --git a/charts/wire-server/templates/_helpers.tpl b/charts/wire-server/templates/_helpers.tpl index 5edb0251456..94aca197dc7 100644 --- a/charts/wire-server/templates/_helpers.tpl +++ b/charts/wire-server/templates/_helpers.tpl @@ -106,46 +106,6 @@ {{- end -}} {{- end -}} -{{- define "gundeck.configureRedisCa" -}} -{{ or (hasKey .redis "tlsCa") (hasKey .redis "tlsCaSecretRef") }} -{{- end -}} - -{{- define "gundeck.redisTlsSecretName" -}} -{{- if .redis.tlsCaSecretRef -}} -{{ .redis.tlsCaSecretRef.name }} -{{- else }} -{{- print "gundeck-redis-ca" -}} -{{- end -}} -{{- end -}} - -{{- define "gundeck.redisTlsSecretKey" -}} -{{- if .redis.tlsCaSecretRef -}} -{{ .redis.tlsCaSecretRef.key }} -{{- else }} -{{- print "ca.pem" -}} -{{- end -}} -{{- end -}} - -{{- define "gundeck.configureAdditionalRedisCa" -}} -{{ and (hasKey . "redisAdditionalWrite") (or (hasKey .redis "additionalTlsCa") (hasKey .redis "additionalTlsCaSecretRef")) }} -{{- end -}} - -{{- define "gundeck.additionalRedisTlsSecretName" -}} -{{- if .redis.additionalTlsCaSecretRef -}} -{{ .redis.additionalTlsCaSecretRef.name }} -{{- else }} -{{- print "gundeck-additional-redis-ca" -}} -{{- end -}} -{{- end -}} - -{{- define "gundeck.additionalRedisTlsSecretKey" -}} -{{- if .redis.additionalTlsCaSecretRef -}} -{{ .redis.additionalTlsCaSecretRef.key }} -{{- else }} -{{- print "ca.pem" -}} -{{- end -}} -{{- end -}} - {{/* SPAR */}} {{- define "spar.tlsSecretRef" -}} {{- if .cassandra.tlsCaSecretRef -}} diff --git a/charts/wire-server/templates/cannon/statefulset.yaml b/charts/wire-server/templates/cannon/statefulset.yaml index 00103604bf8..f60d6586453 100644 --- a/charts/wire-server/templates/cannon/statefulset.yaml +++ b/charts/wire-server/templates/cannon/statefulset.yaml @@ -2,7 +2,7 @@ # Specific pods can be accessed within the cluster at cannon-.cannon. # (the second 'cannon' is the name of the headless service) # Note: In fact, cannon-.cannon can also be used to access the service but assuming -# that we can have multiple namespaces accessing the same redis cluster, appending `.` +# that we can have multiple namespaces accessing the same cannon cluster, appending `.` # makes the service unambiguous apiVersion: apps/v1 kind: StatefulSet diff --git a/charts/wire-server/templates/gundeck/configmap.yaml b/charts/wire-server/templates/gundeck/configmap.yaml index 10be21c34e1..6aae6aa47d8 100644 --- a/charts/wire-server/templates/gundeck/configmap.yaml +++ b/charts/wire-server/templates/gundeck/configmap.yaml @@ -41,26 +41,10 @@ data: {{- end }} {{- end }} - redis: - host: {{ .redis.host }} - port: {{ .redis.port }} - connectionMode: {{ .redis.connectionMode }} - enableTls: {{ .redis.enableTls }} - insecureSkipVerifyTls: {{ .redis.insecureSkipVerifyTls }} - {{- if eq (include "gundeck.configureRedisCa" .) "true" }} - tlsCa: /etc/wire/gundeck/redis-ca/{{ include "gundeck.redisTlsSecretKey" . }} - {{- end }} - - {{- if .redisAdditionalWrite }} - redisAdditionalWrite: - host: {{ .redisAdditionalWrite.host }} - port: {{ .redisAdditionalWrite.port }} - connectionMode: {{ .redisAdditionalWrite.connectionMode }} - enableTls: {{ .redisAdditionalWrite.enableTls }} - insecureSkipVerifyTls: {{ .redisAdditionalWrite.insecureSkipVerifyTls }} - {{- if eq (include "gundeck.configureAdditionalRedisCa" .) "true" }} - tlsCa: /etc/wire/gundeck/additional-redis-ca/{{ include "gundeck.additionalRedisTlsSecretKey" . }} - {{- end }} + postgresql: {{ toYaml .postgresql | nindent 6 }} + postgresqlPool: {{ toYaml .postgresqlPool | nindent 6 }} + {{- if hasKey $.Values.gundeck.secrets "pgPassword" }} + postgresqlPassword: /etc/wire/gundeck/secrets/pgPassword {{- end }} # Gundeck uses discovery for AWS access key / secrets diff --git a/charts/wire-server/templates/gundeck/deployment.yaml b/charts/wire-server/templates/gundeck/deployment.yaml index bc46a53ec0d..ff7a457fc6e 100644 --- a/charts/wire-server/templates/gundeck/deployment.yaml +++ b/charts/wire-server/templates/gundeck/deployment.yaml @@ -50,16 +50,9 @@ spec: secret: secretName: {{ (include "gundeck.tlsSecretRef" .Values.gundeck.config | fromYaml).name }} {{- end }} - {{- if eq (include "gundeck.configureRedisCa" .Values.gundeck.config) "true" }} - - name: "redis-ca" + - name: "gundeck-secrets" secret: - secretName: {{ include "gundeck.redisTlsSecretName" .Values.gundeck.config }} - {{- end }} - {{- if eq (include "gundeck.configureAdditionalRedisCa" .Values.gundeck.config) "true" }} - - name: "additional-redis-ca" - secret: - secretName: {{ include "gundeck.additionalRedisTlsSecretName" .Values.gundeck.config }} - {{- end }} + secretName: "gundeck" containers: - name: gundeck image: "{{ .Values.gundeck.image.repository }}:{{ .Values.gundeck.image.tag }}" @@ -75,14 +68,8 @@ spec: - name: "gundeck-cassandra" mountPath: "/etc/wire/gundeck/cassandra" {{- end }} - {{- if eq (include "gundeck.configureRedisCa" .Values.gundeck.config) "true" }} - - name: "redis-ca" - mountPath: "/etc/wire/gundeck/redis-ca/" - {{- end }} - {{- if eq (include "gundeck.configureAdditionalRedisCa" .Values.gundeck.config) "true" }} - - name: "additional-redis-ca" - mountPath: "/etc/wire/gundeck/additional-redis-ca/" - {{- end }} + - name: "gundeck-secrets" + mountPath: "/etc/wire/gundeck/secrets" {{- if and .Values.gundeck.config.rabbitmq .Values.gundeck.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" mountPath: "/etc/wire/gundeck/rabbitmq-ca/" @@ -110,34 +97,6 @@ spec: name: gundeck key: awsSecretKey {{- end }} - {{- if hasKey .Values.gundeck.secrets "redisUsername" }} - - name: REDIS_USERNAME - valueFrom: - secretKeyRef: - name: gundeck - key: redisUsername - {{- end }} - {{- if hasKey .Values.gundeck.secrets "redisPassword" }} - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: gundeck - key: redisPassword - {{- end }} - {{- if hasKey .Values.gundeck.secrets "redisAdditionalWriteUsername" }} - - name: REDIS_ADDITIONAL_WRITE_USERNAME - valueFrom: - secretKeyRef: - name: gundeck - key: redisAdditionalWriteUsername - {{- end }} - {{- if hasKey .Values.gundeck.secrets "redisAdditionalWritePassword" }} - - name: REDIS_ADDITIONAL_WRITE_PASSWORD - valueFrom: - secretKeyRef: - name: gundeck - key: redisAdditionalWritePassword - {{- end }} - name: AWS_REGION value: "{{ .Values.gundeck.config.aws.region }}" {{- with .Values.gundeck.config.proxy }} diff --git a/charts/wire-server/templates/gundeck/redis-ca-secret.yaml b/charts/wire-server/templates/gundeck/redis-ca-secret.yaml deleted file mode 100644 index a82eab555cb..00000000000 --- a/charts/wire-server/templates/gundeck/redis-ca-secret.yaml +++ /dev/null @@ -1,30 +0,0 @@ ---- -{{- if not (empty .Values.gundeck.config.redis.tlsCa) }} -apiVersion: v1 -kind: Secret -metadata: - name: "gundeck-redis-ca" - labels: - app: gundeck - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: "{{ .Release.Name }}" - heritage: "{{ .Release.Service }}" -type: Opaque -data: - ca.pem: {{ .Values.gundeck.config.redis.tlsCa | b64enc | quote }} -{{- end }} ---- -{{- if not (empty .Values.gundeck.config.redis.additionalTlsCa) }} -apiVersion: v1 -kind: Secret -metadata: - name: "gundeck-additional-redis-ca" - labels: - app: gundeck - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: "{{ .Release.Name }}" - heritage: "{{ .Release.Service }}" -type: Opaque -data: - ca.pem: {{ .Values.gundeck.config.redis.additionalTlsCa | b64enc | quote }} -{{- end }} diff --git a/charts/wire-server/templates/gundeck/secret.yaml b/charts/wire-server/templates/gundeck/secret.yaml index b1f744ff600..a17529f4c77 100644 --- a/charts/wire-server/templates/gundeck/secret.yaml +++ b/charts/wire-server/templates/gundeck/secret.yaml @@ -19,17 +19,8 @@ data: {{- if hasKey . "awsSecretKey" }} awsSecretKey: {{ .awsSecretKey | b64enc | quote }} {{- end }} - {{- if hasKey . "redisUsername" }} - redisUsername: {{ .redisUsername | b64enc | quote }} - {{- end }} - {{- if hasKey . "redisPassword" }} - redisPassword: {{ .redisPassword | b64enc | quote }} - {{- end }} - {{- if hasKey . "redisAdditionalWriteUsername" }} - redisAdditionalWriteUsername: {{ .redisAdditionalWriteUsername | b64enc | quote }} - {{- end }} - {{- if hasKey . "redisAdditionalWritePassword" }} - redisAdditionalWritePassword: {{ .redisAdditionalWritePassword | b64enc | quote }} + {{- if hasKey . "pgPassword" }} + pgPassword: {{ .pgPassword | b64enc | quote }} {{- end }} {{- end }} {{- end }} diff --git a/charts/wire-server/templates/gundeck/tests/configmap.yaml b/charts/wire-server/templates/gundeck/tests/configmap.yaml index c8c23ce5185..28d76773076 100644 --- a/charts/wire-server/templates/gundeck/tests/configmap.yaml +++ b/charts/wire-server/templates/gundeck/tests/configmap.yaml @@ -39,10 +39,3 @@ data: host: brig port: 8080 - # a "redis migration" test in gundeck makes use of a second (distinct) redis - redis2: - host: redis-ephemeral-2 - port: 6379 - connectionMode: master - enableTls: false - insecureSkipVerifyTls: false diff --git a/charts/wire-server/templates/gundeck/tests/gundeck-integration.yaml b/charts/wire-server/templates/gundeck/tests/gundeck-integration.yaml index f1a661b4a58..60b5d26e3d5 100644 --- a/charts/wire-server/templates/gundeck/tests/gundeck-integration.yaml +++ b/charts/wire-server/templates/gundeck/tests/gundeck-integration.yaml @@ -18,11 +18,6 @@ spec: secret: secretName: {{ (include "gundeck.tlsSecretRef" .Values.gundeck.config | fromYaml).name }} {{- end }} - {{- if eq (include "gundeck.configureRedisCa" .Values.gundeck.config) "true" }} - - name: "redis-ca" - secret: - secretName: {{ include "gundeck.redisTlsSecretName" .Values.gundeck.config }} - {{- end }} {{- if .Values.gundeck.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" secret: @@ -73,10 +68,6 @@ spec: - name: "gundeck-cassandra" mountPath: "/etc/wire/gundeck/cassandra" {{- end }} - {{- if eq (include "gundeck.configureRedisCa" .Values.gundeck.config) "true" }} - - name: "redis-ca" - mountPath: "/etc/wire/gundeck/redis-ca/" - {{- end }} {{- if .Values.gundeck.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" mountPath: "/etc/wire/gundeck/rabbitmq-ca/" @@ -96,34 +87,6 @@ spec: value: "guest" - name: RABBITMQ_PASSWORD value: "guest" - {{- if hasKey .Values.gundeck.secrets "redisUsername" }} - - name: REDIS_USERNAME - valueFrom: - secretKeyRef: - name: gundeck - key: redisUsername - {{- end }} - {{- if hasKey .Values.gundeck.secrets "redisPassword" }} - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: gundeck - key: redisPassword - {{- end }} - {{- if and (hasKey .Values.gundeck.tests "secrets") (hasKey .Values.gundeck.tests.secrets "redisAdditionalWriteUsername") }} - - name: REDIS_ADDITIONAL_WRITE_USERNAME - valueFrom: - secretKeyRef: - name: gundeck-integration - key: redisAdditionalWriteUsername - {{- end }} - {{- if and (hasKey .Values.gundeck.tests "secrets") (hasKey .Values.gundeck.tests.secrets "redisAdditionalWritePassword") }} - - name: REDIS_ADDITIONAL_WRITE_PASSWORD - valueFrom: - secretKeyRef: - name: gundeck-integration - key: redisAdditionalWritePassword - {{- end }} {{- if .Values.gundeck.tests.config.uploadXml }} - name: UPLOAD_XML_S3_BASE_URL value: {{ .Values.gundeck.tests.config.uploadXml.baseUrl }} diff --git a/charts/wire-server/templates/gundeck/tests/secret.yaml b/charts/wire-server/templates/gundeck/tests/secret.yaml index 60aed14a3a4..df7b82695fe 100644 --- a/charts/wire-server/templates/gundeck/tests/secret.yaml +++ b/charts/wire-server/templates/gundeck/tests/secret.yaml @@ -17,11 +17,5 @@ data: {{- if hasKey . "uploadXmlAwsSecretAccessKey" }} uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} {{- end }} - {{- if hasKey . "redisAdditionalWriteUsername" }} - redisAdditionalWriteUsername: {{ .redisAdditionalWriteUsername | b64enc | quote }} - {{- end }} - {{- if hasKey . "redisAdditionalWritePassword" }} - redisAdditionalWritePassword: {{ .redisAdditionalWritePassword | b64enc | quote }} - {{- end }} {{- end }} {{- end }} diff --git a/charts/wire-server/values.yaml b/charts/wire-server/values.yaml index d3f3f7f4501..c6dda2176a2 100644 --- a/charts/wire-server/values.yaml +++ b/charts/wire-server/values.yaml @@ -744,35 +744,19 @@ gundeck: # tlsCaSecretRef: # name: # key: - redis: - host: redis-ephemeral - port: 6379 - connectionMode: "master" # master | cluster - enableTls: false - insecureSkipVerifyTls: false - # To configure custom TLS CA, please provide one of these: - # tlsCa: - # - # Or refer to an existing secret (containing the CA): - # tlsCaSecretRef: - # name: - # key: - - # To enable additional writes during a migration: - # redisAdditionalWrite: - # host: redis-two - # port: 6379 - # connectionMode: master - # enableTls: false - # insecureSkipVerifyTls: false + # Postgres connection settings for presence tracking. # - # # To configure custom TLS CA, please provide one of these: - # # tlsCa: - # # - # # Or refer to an existing secret (containing the CA): - # # tlsCaSecretRef: - # # name: - # # key: + # Values are described in https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS + # To set the password via a gundeck secret see `secrets.pgPassword`. + postgresql: + host: postgresql # DNS name without protocol + port: "5432" + user: wire-server + dbname: wire-server + postgresqlPool: + size: 100 + acquisitionTimeout: 10s + idlenessTimeout: 10m aws: region: "eu-west-1" proxy: {} diff --git a/deploy/dockerephemeral/docker-compose.yaml b/deploy/dockerephemeral/docker-compose.yaml index fb2a4801ebc..a8a9ab66d5c 100644 --- a/deploy/dockerephemeral/docker-compose.yaml +++ b/deploy/dockerephemeral/docker-compose.yaml @@ -1,10 +1,4 @@ networks: - redis: - driver: bridge - ipam: - config: - - subnet: 172.20.0.0/24 - coredns: driver: bridge ipam: @@ -78,134 +72,6 @@ services: networks: - demo_wire - redis-master: - container_name: demo_wire_redis - image: redis:7.2-alpine - command: redis-server /usr/local/etc/redis/redis.conf - ports: - - "127.0.0.1:6379:6379" - volumes: - - ./docker/redis-master-mode.conf:/usr/local/etc/redis/redis.conf - networks: - - demo_wire - - redis-cluster: - image: "redis:7.2-alpine" - command: - - redis-cli - - --cluster - - create - - 172.20.0.31:6373 - - 172.20.0.32:6374 - - 172.20.0.33:6375 - - 172.20.0.34:6376 - - 172.20.0.35:6377 - - 172.20.0.36:6378 - - --cluster-replicas - - "1" - - --cluster-yes - - -a - - very-secure-redis-cluster-password - - --cacert - - /usr/local/etc/redis/ca.pem - - --tls - volumes: - - ./docker/redis-ca.pem:/usr/local/etc/redis/ca.pem - networks: - redis: - ipv4_address: 172.20.0.30 - depends_on: - - redis-node-1 - - redis-node-2 - - redis-node-3 - - redis-node-4 - - redis-node-5 - - redis-node-6 - redis-node-1: - image: "redis:7.2-alpine" - command: redis-server /usr/local/etc/redis/redis.conf - ports: - - "127.0.0.1:6373:6373" - volumes: - - redis-node-1-data:/var/lib/redis - - ./docker/redis-node-1.conf:/usr/local/etc/redis/redis.conf - - ./docker/redis-node-1-cert.pem:/usr/local/etc/redis/cert.pem - - ./docker/redis-node-1-key.pem:/usr/local/etc/redis/key.pem - - ./docker/redis-ca.pem:/usr/local/etc/redis/ca.pem - networks: - redis: - ipv4_address: 172.20.0.31 - redis-node-2: - image: "redis:7.2-alpine" - command: redis-server /usr/local/etc/redis/redis.conf - ports: - - "127.0.0.1:6374:6374" - volumes: - - redis-node-2-data:/var/lib/redis - - ./docker/redis-node-2.conf:/usr/local/etc/redis/redis.conf - - ./docker/redis-node-2-cert.pem:/usr/local/etc/redis/cert.pem - - ./docker/redis-node-2-key.pem:/usr/local/etc/redis/key.pem - - ./docker/redis-ca.pem:/usr/local/etc/redis/ca.pem - networks: - redis: - ipv4_address: 172.20.0.32 - redis-node-3: - image: "redis:7.2-alpine" - command: redis-server /usr/local/etc/redis/redis.conf - ports: - - "127.0.0.1:6375:6375" - volumes: - - redis-node-3-data:/var/lib/redis - - ./docker/redis-node-3.conf:/usr/local/etc/redis/redis.conf - - ./docker/redis-node-3-cert.pem:/usr/local/etc/redis/cert.pem - - ./docker/redis-node-3-key.pem:/usr/local/etc/redis/key.pem - - ./docker/redis-ca.pem:/usr/local/etc/redis/ca.pem - networks: - redis: - ipv4_address: 172.20.0.33 - redis-node-4: - image: "redis:7.2-alpine" - command: redis-server /usr/local/etc/redis/redis.conf - ports: - - "127.0.0.1:6376:6376" - volumes: - - redis-node-4-data:/var/lib/redis - - ./docker/redis-node-4.conf:/usr/local/etc/redis/redis.conf - - ./docker/redis-node-4-cert.pem:/usr/local/etc/redis/cert.pem - - ./docker/redis-node-4-key.pem:/usr/local/etc/redis/key.pem - - ./docker/redis-ca.pem:/usr/local/etc/redis/ca.pem - networks: - redis: - ipv4_address: 172.20.0.34 - redis-node-5: - image: "redis:7.2-alpine" - command: redis-server /usr/local/etc/redis/redis.conf - ports: - - "127.0.0.1:6377:6377" - volumes: - - redis-node-5-data:/var/lib/redis - - ./docker/redis-node-5.conf:/usr/local/etc/redis/redis.conf - - ./docker/redis-node-5-cert.pem:/usr/local/etc/redis/cert.pem - - ./docker/redis-node-5-key.pem:/usr/local/etc/redis/key.pem - - ./docker/redis-ca.pem:/usr/local/etc/redis/ca.pem - networks: - redis: - ipv4_address: 172.20.0.35 - redis-node-6: - image: "redis:7.2-alpine" - command: redis-server /usr/local/etc/redis/redis.conf - ports: - - "127.0.0.1:6378:6378" - volumes: - - redis-node-6-data:/var/lib/redis - - ./docker/redis-node-6.conf:/usr/local/etc/redis/redis.conf - - ./docker/redis-node-6-cert.pem:/usr/local/etc/redis/cert.pem - - ./docker/redis-node-6-key.pem:/usr/local/etc/redis/key.pem - - ./docker/redis-ca.pem:/usr/local/etc/redis/ca.pem - networks: - redis: - ipv4_address: 172.20.0.36 - elasticsearch: container_name: demo_wire_elasticsearch image: elasticsearch:6.8.23 @@ -418,11 +284,3 @@ services: # - DNS_SERVER_RECURSION_DENIED_NETWORKS=1.1.1.0/24 #Comma separated list of IP addresses or network addresses to deny recursion. Valid only for `UseSpecifiedNetworkACL` recursion option. This option is obsolete and DNS_SERVER_RECURSION_NETWORK_ACL should be used instead. # - DNS_SERVER_RECURSION_ALLOWED_NETWORKS=127.0.0.1, 192.168.1.0/24 #Comma separated list of IP addresses or network addresses to allow recursion. Valid only for `UseSpecifiedNetworkACL` recursion option. This option is obsolete and DNS_SERVER_RECURSION_NETWORK_ACL should be used instead. # - DNS_SERVER_ENABLE_BLOCKING=false #Sets the DNS server to block domain names using Blocked Zone and Block List Zone. - -volumes: - redis-node-1-data: - redis-node-2-data: - redis-node-3-data: - redis-node-4-data: - redis-node-5-data: - redis-node-6-data: diff --git a/deploy/dockerephemeral/docker/redis-master-mode.conf b/deploy/dockerephemeral/docker/redis-master-mode.conf deleted file mode 100644 index d71dbc51c97..00000000000 --- a/deploy/dockerephemeral/docker/redis-master-mode.conf +++ /dev/null @@ -1 +0,0 @@ -requirepass very-secure-redis-master-password \ No newline at end of file diff --git a/deploy/dockerephemeral/docker/redis-node-1-cert.pem b/deploy/dockerephemeral/docker/redis-node-1-cert.pem deleted file mode 100644 index 7756f82bbd0..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-1-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTtp3U0VPwBVJNULQF -S4BBlWBNf/8NMidOq23IsTcjkIFWO1XL+HFZoa1AArUSA/TaLBYyz9WmX7eLWvAU -ADM6mfAf2V6whmIs2H9ZRnY89bFWO2hzLWWp1qq3dXK1ywTLpw7DqU4OT0rtYZbp -QHeVY0mKKspF+YJTZzWB1hs8IX9355wXRlYBLPNQ5oHRb4/16J/UUFPIJjpUyHsq -T1LWmVREqisrq9u50FnNPeLXE6SDnHGRkYGQXzQOM/yAI75/QUOOqo5rt3Et52t5 -pkOT45R0PbAC2UpR1usew0zVjRoQfFk9n38tXUSHKw/tW+ZY1xJqEKEiLGfnhhza -t4kjAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy0xhwSsFAAfMB0GA1UdDgQWBBQsOxsq4X8dS/Ddl9l1 -TWDb8Q5KKzAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG -9w0BAQsFAAOCAQEAaByeD08xOZCV0ZKsx7lHtiem5/XG01rMcDxNVrVguSD+mqhR -/j8ciTW2CruJ2X8ReTjNrI4X1nWLbh4rsrA56q4xkjkgJIfWQAdKCibXTrHOWfk5 -dcYG1pqVdpD5bvsxAsY95jxqoVJHXHGN8ynC+lV39HbDJQFOdHLAP66NUrphp76a -OZKiuzUS6naeiHWoA9eIANFRz/JoQvyp109gdce5MH0iFwGFqNJU2rwilOpzQVc7 -qldx7MHMnW5UYSTqryTOr8PS+xo24TSdHjIXmnOO3Ov0Pw7iPpGVGj56dAKgEisG -yGOAWYto8UBWKLox1vSSlfdkhAoDXluvE8EwRw== ------END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-1-key.pem b/deploy/dockerephemeral/docker/redis-node-1-key.pem deleted file mode 100644 index 6d8b29bbdee..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-1-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDTtp3U0VPwBVJN -ULQFS4BBlWBNf/8NMidOq23IsTcjkIFWO1XL+HFZoa1AArUSA/TaLBYyz9WmX7eL -WvAUADM6mfAf2V6whmIs2H9ZRnY89bFWO2hzLWWp1qq3dXK1ywTLpw7DqU4OT0rt -YZbpQHeVY0mKKspF+YJTZzWB1hs8IX9355wXRlYBLPNQ5oHRb4/16J/UUFPIJjpU -yHsqT1LWmVREqisrq9u50FnNPeLXE6SDnHGRkYGQXzQOM/yAI75/QUOOqo5rt3Et -52t5pkOT45R0PbAC2UpR1usew0zVjRoQfFk9n38tXUSHKw/tW+ZY1xJqEKEiLGfn -hhzat4kjAgMBAAECggEAFqMmoixVxMrU34Z7ETve9WRC/VZrz53mvQ8weG6WfjuD -0NQcWuhwOkzCyR7g/JGmuzNOllVJu3Xtmr15ATJ6R9BQ8B7edJKR6cimaUXS+7ar -pRRKGVKn1a6p517sCoswMpRkzEAMpBQPZ21xZPRrNPJ+WQM1SKEiscdN3dmmZNng -MvroH1dPVbyZ49xkjMQ0NaOtk4rvopzdKKZea2qz41w/vXR9hShnfVDs/q86clmx -5mnEvXcEdguioAfUWz+qQ7dXlWsASKa/gAMjUN9GW9uOn4LclFsVCD2MW+IUJMxe -+JtFM0xiQ3HaK0Fem8+XR8mG3BB5a/06ZHBfcv/lsQKBgQD4WSJjZwMBG80uGidR -ls+VhhFjysxm5qrF34MWziLczi1nAStc/PzVcA7tHapKX5JiKYT6d8Ptngz6FLIo -/72OshmLzctxRprlpihWxMIYOqwb2PLB0//ghuUE81Zbxj1MQ6k9WbTGHBwUbaiv -PSzclhmMubypfLLcmMEnHeZFswKBgQDaPIWmyax3Eft8DzC3Om7X3WMN0NXE96z2 -6hUAon5tqinMuWUWa2cyWzPsdBgFM8mCynoiIu08YFpZQivoB6QSal4x2mLg4R+u -aLm3h9f6NS4/VvpWPL5wMUAqeCCbP/2PVKk///0mtQGixUOxeQftTncQeLtfXOXd -4gDJHjfW0QKBgQDND7xnW42Ngsk+wfWpVt981UDSp4dziA+GZ3I0iG0c6Vlv7fVC -SNrz2h1ZCN+tnZCfYS0eK3oqYBDTBfe+Br0ccE7Ls1fC5svLyBES5FBn9TpbnB2G -kmh7mqbMGak7CktfB5dcww+TbW56J7nbSKYcVgwuuMbhI8gEglUq2XNkJQKBgQDV -VojIzSmdlKSlWCwlUif9OdyVKutuizg4gAhcAH1bMxd9nFbnncLaBTIzGiJJI6EA -DHNsX3xOo1pvGzLUtnN71SOT1IsIjsprstCqS0+ktswo+xvppaP9BQhW++vUGLAE -p5x0hgixCA07U1+jZE+NekEGhx+UT7oeN8rQ0IuBoQKBgQC4PF4WwqashYHkYW2j -4LaMu5kWY/0OI9Vh/h1iOcKPzVUn61aabjsx1wF9rummIdxP03/bs7ZpkwPypcVR -v7XnNbi+hDZFEN6s/+Gl4S6RfAbWXs3sgnhVlctlkzzwG8UHCef4DWMPxFI1JQI8 -X+SdDfpmB/ayQb8TlYvke/s8cQ== ------END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-1.conf b/deploy/dockerephemeral/docker/redis-node-1.conf deleted file mode 100644 index aa772f502fe..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-1.conf +++ /dev/null @@ -1,17 +0,0 @@ -port 0 -tls-port 6373 -tls-cert-file /usr/local/etc/redis/cert.pem -tls-key-file /usr/local/etc/redis/key.pem -tls-ca-cert-file /usr/local/etc/redis/ca.pem -tls-auth-clients no -tls-cluster yes -tls-replication yes - -cluster-enabled yes -cluster-config-file nodes.conf -cluster-node-timeout 5000 - -appendonly yes - -requirepass very-secure-redis-cluster-password -masterauth very-secure-redis-cluster-password diff --git a/deploy/dockerephemeral/docker/redis-node-2-cert.pem b/deploy/dockerephemeral/docker/redis-node-2-cert.pem deleted file mode 100644 index ea4b4507d6b..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-2-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4mm9GWOVh0ttlzXaJ -11/rQQX3vYQ3zyeMdz/KGTKArOF+pxVCxbETlI3ZufO8Ht9zqa7Doh5R86iNtVMR -LoQZVWeXjQsMATwNUZT3lEOezpDE0ZI8d5JyU946Z+7s0VjIMbXOzjTSjTNSi57N -li59/1NTG5CW9EtgnnYoP5SOrYTpK+fzawXD18tD8kq/VBLt8OoG7xn6DIpGsFr9 -h1Ot/yrUejvrHg2KIi3av/cnqA8twzFpkdvGSEarjRuYG6fHGL67dgSpLvzh/v7h -QiJDFFB8fHnUc5ioZXFw88P4Oq7UlzBhnkC8nhUi1X1vWoF9Xz4FXXJ1P4WkZfWB -Vui7AgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy0yhwSsFAAgMB0GA1UdDgQWBBQQK2od431iWKznJEQz -zy5GXgt1DDAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG -9w0BAQsFAAOCAQEAhvNbLzlY4sS/xmn8alzIYjY/uIc5c0PaUaXc7SSjeoChRfNQ -tE5YLmOo86WYNThtaNmiRLFv3yNBXCcqdVgNdL78EIQlKvPxHwzZXxkKDmOcfIZS -nUa4w+OmKJLsdNjphBGmR94h8WycwoFMThw55vnTJ2+AnCFPsLDfjtHiKB8AsW8u -gtSTtVyu+QyvGTDxEFDgqFgyFjJpVp37bOakRuzuZZ8VUssQbb11YHyhnNGTcL3a -hLXeGVSRA7SyDXxxRs5PmmJVsUOWkgbIjguvZK5APpqaGEYwBYo036DFSgt6DTOu -8YsCTeSOmue0xNlPDiVPSP8HUGfq3tTBKMXbUQ== ------END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-2-key.pem b/deploy/dockerephemeral/docker/redis-node-2-key.pem deleted file mode 100644 index fba9118998e..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-2-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4mm9GWOVh0ttl -zXaJ11/rQQX3vYQ3zyeMdz/KGTKArOF+pxVCxbETlI3ZufO8Ht9zqa7Doh5R86iN -tVMRLoQZVWeXjQsMATwNUZT3lEOezpDE0ZI8d5JyU946Z+7s0VjIMbXOzjTSjTNS -i57Nli59/1NTG5CW9EtgnnYoP5SOrYTpK+fzawXD18tD8kq/VBLt8OoG7xn6DIpG -sFr9h1Ot/yrUejvrHg2KIi3av/cnqA8twzFpkdvGSEarjRuYG6fHGL67dgSpLvzh -/v7hQiJDFFB8fHnUc5ioZXFw88P4Oq7UlzBhnkC8nhUi1X1vWoF9Xz4FXXJ1P4Wk -ZfWBVui7AgMBAAECggEAONB+8r2lSygkEf7cPqwkfzjx5z9SlAKTf22sGj0LCAMt -G1e8+WHyj74msh3C3+D4kJZmjRs2Da7Z71MhD6arTUi1qzTjc3xlyQuUt2XQMe4N -LCX7xdRfJASf3oXiSMxdcK+r7swUAcEnTH5gD5HrGSgdsvRG2c6x7DiY0OZQiGBk -2rPHQDUKeb7Z0YLWc8nldzlnOe2OeWpFVEraAOzmANnV5FVJZP8RNoiKviZnB77O -qbA6Xtpg4ytVhMaymUmjkjdFuxm5XcMCOIz4W9SZVJ9uSzjZqATzgjsiOWYozB7q -2xb1yOyCVPgf+dZj32D8DvqSrwwRBR3LcNhnj2wUIQKBgQDqL4VNp54Lrf+oZ0ZF -h3s6lL2NquY0xHs91YvoO187VetyUlNjOcGXt8ROhSSAf6qTLQvrreVdjYHr52xr -smCohhQ9QDm3d+Inh3ARgr75O577aPwJHBmo0fnu9h6OkDr8nx05SthW4XenHqoE -iWQ9FnibAFz5KLBSYC7x9wfGaQKBgQDJzI3UC6AqQS8ILbcqHm7ZmnpUUjn7vPUm -lkB3/YtV7ewWJhFzdPdaKHKe2YO9WXQTCF7iPRK3+gWt8uh4DWCrSObBkmSUlF66 -wbRof3lsYiWDPed9OTgoDHRwbMPeYrJ3A0TMrGJQsbedljneaat+DM3kNgjgChfW -JiL0g9c5gwKBgQDBi8zMRT/lv0SQVepKBJLf85ZFw3zHF6wTiq46nPcz/uq8bTXl -yBIr5gEkM/3bBahgQtabTflGvHEoGvgMejxQi5+mj7Ij47zRlqoUjs5vBct7VWUX -0lWSpRe/W0Id6S4XIxnwA9+Qzn8pa7pwTWy+4BeFY2NzuSEgs8WYzOVsIQKBgHbI -IPOfpDc7ByQZRKdWIomTlE3t2JOFNgfwiSIX69w4n66p2bvMLYy0IkO+ZP0fmmNZ -mgAxUsNYN9+cC5oexbgMwUdPlESg0OG9AyQ/ZImXe900ov3ioFtyeVdzrhdIoSPM -mMKg9X3qHdp0gruYF4mqn8akx7SYPE+hQxIKSLVhAoGAJP+TshJj8xAeE1Uroyc/ -yIWThbp0Q/EFaXkpS6aJqBjdcLfh2U+Zo9ZaTn9OBlzXHk9WttzeWuMY9PrINodJ -8DSg5f0PslYxJ5DQuKnDWUeqX3zCnXkgnymlvh78t6wWp+BUAEjI8qH5IgKVwKd+ -VJbPX4mzhAl/0kIablU6SqM= ------END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-2.conf b/deploy/dockerephemeral/docker/redis-node-2.conf deleted file mode 100644 index de7687558b3..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-2.conf +++ /dev/null @@ -1,17 +0,0 @@ -port 0 -tls-port 6374 -tls-cert-file /usr/local/etc/redis/cert.pem -tls-key-file /usr/local/etc/redis/key.pem -tls-ca-cert-file /usr/local/etc/redis/ca.pem -tls-auth-clients no -tls-cluster yes -tls-replication yes - -cluster-enabled yes -cluster-config-file nodes.conf -cluster-node-timeout 5000 - -appendonly yes - -requirepass very-secure-redis-cluster-password -masterauth very-secure-redis-cluster-password diff --git a/deploy/dockerephemeral/docker/redis-node-3-cert.pem b/deploy/dockerephemeral/docker/redis-node-3-cert.pem deleted file mode 100644 index e550d0e30f9..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-3-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCd72omHFn1mEFw/GBp -gkPM5BkF7giGx7GOLyijCoi4NLNVKJn6mOJt9vX2PbBYedy1OcskObLbEwqUwcZr -7fVim34xrE4AmdJqBWTkcMFnhbjzYIynfvejej/05kWlzp3JuhTpi7i2W+nnZjqb -S6UHgeTwF/iENA1oysuq0jC4oaVGNa2ZCoz3W+uAEbpUYNjN7/uQeEwRyZjSEJUY -KyG69Wrl9KnzBX0mkltq8rJiCqaG+qOZwP+XH7TxjYM1SlAxLHrnjDQHWyZXJzPY -fikRk2Zf8nDobA5thXVR/2PicDxUs1VyGYSg/vK1EMwOIHIZdxalo0x75vFjBJ9T -l+HFAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy0zhwSsFAAhMB0GA1UdDgQWBBQyljx2OR3L7yZLVax4 -MLTDhj4xPjAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG -9w0BAQsFAAOCAQEAUv3JN0ip/LWmtWHyqzPuq9tbVFs2M5waRO2ZZtEp6Pzudr9x -JKrmtz7IlnwK2E3eqw1Hh3kZYiM5XT2GzqFjPn+Na32i3IsR/S1Y4ZDq6T1WOjht -u+3EjrUvpTXAcLfaO60gJ7DrfC4PsuNuaRr23BiF3lIb7A693hnESg3EnUqGvAvA -ikR/Cv48kAvxpFlXZfnGApFEP49svj676emodRUlk4aCOjIniPByLF318Dl+MwzW -KbnjynzjnOqfcXeD67axFqIBAhZPBDWIDOLNo/ASAROkPntycBGFPUL+Wgdq75vs -8WnftwfCzYtKcASNVSeoSFtJhVy2cAqHK1bd/g== ------END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-3-key.pem b/deploy/dockerephemeral/docker/redis-node-3-key.pem deleted file mode 100644 index d7be5cf147d..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-3-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCd72omHFn1mEFw -/GBpgkPM5BkF7giGx7GOLyijCoi4NLNVKJn6mOJt9vX2PbBYedy1OcskObLbEwqU -wcZr7fVim34xrE4AmdJqBWTkcMFnhbjzYIynfvejej/05kWlzp3JuhTpi7i2W+nn -ZjqbS6UHgeTwF/iENA1oysuq0jC4oaVGNa2ZCoz3W+uAEbpUYNjN7/uQeEwRyZjS -EJUYKyG69Wrl9KnzBX0mkltq8rJiCqaG+qOZwP+XH7TxjYM1SlAxLHrnjDQHWyZX -JzPYfikRk2Zf8nDobA5thXVR/2PicDxUs1VyGYSg/vK1EMwOIHIZdxalo0x75vFj -BJ9Tl+HFAgMBAAECggEABYejI9UiS+MaMiaOtE2x/16NMb6f4Hg600umFJoDJ3qm -PM5rIHHHRn+7JPVhU00RA+y+HB/uZJVKGDigsJloWhzaUkrs1ZXiiYEe2JDKH3cj -KVexamabrRxUA53RxSMdizlPZM4A7axSMvP1YV1IrfadBCW9Ydj2DzvqiFShDWst -asKPAa6MAU63zfZZaBQvicswd1nJUvc8ZNp1p0JiVcwWPWVTYH9d2c+0WZLlfCHm -GxUurHwyVc6b7T4OSrsiDaQN0kdLJDAYowp+T94JDBCH3m4e/NF9W6gkoO2UGXTH -6A9HVDI3FwUBzXdT9rL/Wmp4kKXB4xO2TU/yeZoYAQKBgQDK2Q2vG+BucY2aJxGw -7HNeXov2lLma2Vn4TRr+cyzcXH7Jmc8J/h9RMU7AEfg3CQwMbXE60P561/1q1e0Z -fD55x9ka3FZ2dG+a5CDzjkqnUgnLYOK1bxx5UUq+Sf6IeNjGPikejRPcPBmvFVuu -NvoPU0HwWLm67BnantJIpUFvRQKBgQDHUaPa6SIMGAWHasI6EvZMGgBAy5iJa4s3 -o+DuESF+6lD989ZnOltsPFeYhwbIzm14EzhK/y4MVR46gXLMZ9FwlGCGdXE7LWiN -VKCm9kRcxcH9Sak70LkZ9yv08Nl45f9vTOzBcKzu6bgZ2LOeSJ0oTmiVEb98pL7N -w6XxD2iQgQKBgAVVPYncBsOAksN5wXpQTRwvCij6cgLDMh1YEZyc9JH6kI7GT24o -0zP0QujD0C3KPBnbir2MHxSltxDm/OvNm2riOS/+mPtWRlThKIiethG+E2nYaz1v -5WS/IWLtWRbHbpOPsM8P0HTa06YJvrZO1bYvby1dd8yVRny77jVgut6tAoGAHpMK -ZHkgjORebMBWnNvtxgyy/z1735CMoXNU/I/KKJK+68WsnNcZ0QeMlEwaIVFw/1tL -Zk2wfZnM8kKLHonKWc+Y4uc/AEnd4NgbcKEUKXr4X+cdu5wv2KjOqFsNsPru7N7K -7n1fOaLGZ8iS/PO8j8M/TaaUTgVjc2LQoKKxcoECgYAtPzq1Y0yc22M+m1m6nK/W -L7rsUI0zDs0VZcJ5mrJg8nahOM/f+BsFYN5oAHYxuXPUyynZyD2nPtdsES75DGOH -PEqr9DhgSig4JmHS/6SEBnWql+zyNdn1/FaYOkKRHiY7jNhjTayiDObJrXg0g4OT -BmzY39BABb52ogQbjWslow== ------END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-3.conf b/deploy/dockerephemeral/docker/redis-node-3.conf deleted file mode 100644 index 7f406d72324..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-3.conf +++ /dev/null @@ -1,17 +0,0 @@ -port 0 -tls-port 6375 -tls-cert-file /usr/local/etc/redis/cert.pem -tls-key-file /usr/local/etc/redis/key.pem -tls-ca-cert-file /usr/local/etc/redis/ca.pem -tls-auth-clients no -tls-cluster yes -tls-replication yes - -cluster-enabled yes -cluster-config-file nodes.conf -cluster-node-timeout 5000 - -appendonly yes - -requirepass very-secure-redis-cluster-password -masterauth very-secure-redis-cluster-password diff --git a/deploy/dockerephemeral/docker/redis-node-4-cert.pem b/deploy/dockerephemeral/docker/redis-node-4-cert.pem deleted file mode 100644 index 185f8f97014..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-4-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKxOmFo5c4ae38qC3z -I89R1F6xjaMyR6jjd6k5qsW7eU/y8+trgY4HV/jbzD3CDOjMkj70la5EiV+GTA0i -GeJH/BkKjqEPsIy/vAPux9xt2ZpIO9ieO2BF75ojrcM7tAbeOLQNAgYA7zAyIpQk -J2P8IyOYSJ31ujLJCR7d0zudAbXJXfAAyPUWqUrmmRHIY7hRi1tUv74JARqnU2tH -ZhFgGyBCaLROK69S/Wy+xPKo5w9Ol5L9eIccrK2/JwNpfsFAxJqXawNm1l1M9gGk -2MpQXzZeTg/hlusqCtPieOPUQKoEDXAgYArQy8iYkLuZzOtg2WwcPOhtfsgVRLNE -wXihAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy00hwSsFAAiMB0GA1UdDgQWBBQrI/peejY55qjXOc6W -XUU+/q6R+TAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG -9w0BAQsFAAOCAQEAdf1N+gPpnkEHzDAMnK4kUCHq2ymLBBWJVAPDcmmtcMjEiEVC -/9BU+hcdqgLXxonEqiA4kEs9Mkj8AcUk0Dzl5Gfk2haZO6yzVEp97zwto+3Tgzya -0l6bvRv4OSfdVeSTYx8T48h23O8FBD/Gp9l5sFOZgc1TCWrb7ReJQS+XThAksIdW -DLvwbOU1I2qRL3ZbT49FAhmVcrMkHJjzkugXDoGG3Rgdzx/HePUjXWdWC1L+7/Kn -U/7w72ymW1mC5PbjoW9zzkVKesj++mhzSb5+sXa/is3hUJ17zy4Bqc71Mb2q8tqM -G/uMrdwfPeoad3qRVPRsK8QlVnJ0eIpiUDk+Ow== ------END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-4-key.pem b/deploy/dockerephemeral/docker/redis-node-4-key.pem deleted file mode 100644 index 355661d6a99..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-4-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDKxOmFo5c4ae38 -qC3zI89R1F6xjaMyR6jjd6k5qsW7eU/y8+trgY4HV/jbzD3CDOjMkj70la5EiV+G -TA0iGeJH/BkKjqEPsIy/vAPux9xt2ZpIO9ieO2BF75ojrcM7tAbeOLQNAgYA7zAy -IpQkJ2P8IyOYSJ31ujLJCR7d0zudAbXJXfAAyPUWqUrmmRHIY7hRi1tUv74JARqn -U2tHZhFgGyBCaLROK69S/Wy+xPKo5w9Ol5L9eIccrK2/JwNpfsFAxJqXawNm1l1M -9gGk2MpQXzZeTg/hlusqCtPieOPUQKoEDXAgYArQy8iYkLuZzOtg2WwcPOhtfsgV -RLNEwXihAgMBAAECggEABRxG5XEc0dVro9tKQy9DHaUcWN3Av/bp5QfCSluJPcMe -Nnma1JwQjBNVyJZidRZVtLg34Xq3SG9s6qnWh+Y+m4FZUTiMiyRwO7HdqII9hkA+ -gPUPLdfBwql6CU2rFsFgDfBAa3aCV7ovjQftk2axwKxTDJbB8mxFtObnsgANp9SU -c+MTlNTs1IQ4ev4u1i9ntR8SlFMcYQUA2AxvOiEDu7b4x/Ph9TEGuR6wLxdImRq/ -7hXcPtGAJKYgZLzAwCrZrjGjHILSskTxdii+Tr52Aq75SA3tLYGkJfSxHTJjFe0u -1k4Ot4uSEjRf4DIwohbSFFbK/ZXG2uscn36OphtbUQKBgQDwQY263RPJ/M5mKvME -15DK1JW3DOLWCBiV0XzwXsS+QpE8pKs2YLeyrY7sV/w1tdnfNdfINCknuzC4tG7Y -I+QzCQGhyKrP2nj4K3SsKUcFk6OWxgiPF5CRmlWySJ+H6+yITKcSJt/ZjUvvGQyQ -TV+IQ8s4RbKII9Pvifai6SLJ2QKBgQDYDn4bqIfZKR0I46//AycGXAUl55Yfgeog -8CR5MatNz26crrmDzjnDgsRbKUxK+UZLl/zEXY5Npn06sOG1G0bO/t7wQqcPsXZt -rZTx58lKvW7LQhEBAz48y9QeK3WUvT1E3JMJ6rt+6IfHvbvCLIu9DwyGJ7Zc7N+6 -k5GduC9gCQKBgQC4Zdfd3+hcUwgnKjezM7ARvO/buqwvEa+s7UgzRMlELdtC7C/s -YHcdUFAt3anZn2VFCBJBuqcLs4RFf1bD1WhEM1lpTparSUcnUlMN//Beu14HTp8r -FC8FUasMVuj6bXzxb8ObDvMoCmaJcHRQHNKBx2amHfhUvQrhAsalasIkoQKBgAFo -XsP5XiE5FlpXeW8U6y0sblAn6R99bjQWvHYZr78LCfJ1ZPoJ3vB6KqNZaojWhPG7 -JMd2wJWa7xfxzRar/dMdcABqvsHoaxgd2GmXFAWrpEwouwmhpscooNItgE+eyAZp -1X9sCxqxkyjnAJEsTyDFN1Ssb5C9blu92GYJrC1ZAoGASChIICMp0HWrDSRxRCen -Fddf993aEI4e46NTWY54u2p0Ga62XUcaw5eND9QX6craD8nd7mMhwvdvQ5vuORBk -m+dqt0oU5cloVp0srHDA861CO8topJFaNGWdF4wDgLU8YKRzd6hNX8X0/CCRl1vd -z/YmtxfgU56SaqExe0X65eA= ------END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-4.conf b/deploy/dockerephemeral/docker/redis-node-4.conf deleted file mode 100644 index 55b360f9f90..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-4.conf +++ /dev/null @@ -1,17 +0,0 @@ -port 0 -tls-port 6376 -tls-cert-file /usr/local/etc/redis/cert.pem -tls-key-file /usr/local/etc/redis/key.pem -tls-ca-cert-file /usr/local/etc/redis/ca.pem -tls-auth-clients no -tls-cluster yes -tls-replication yes - -cluster-enabled yes -cluster-config-file nodes.conf -cluster-node-timeout 5000 - -appendonly yes - -requirepass very-secure-redis-cluster-password -masterauth very-secure-redis-cluster-password diff --git a/deploy/dockerephemeral/docker/redis-node-5-cert.pem b/deploy/dockerephemeral/docker/redis-node-5-cert.pem deleted file mode 100644 index e1221b9df77..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-5-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCSXrnTzrNfHudXjC0A -h0CiFRe3yp4j6cN3Hfv4snS6tPWXM4MH9Dka8zMLZvzRVQZK3PxDh/R/DQYBZhpy -LEvT7wYCDsS+F+tie2sPjzSAbdM5dolD8fGwACOqobI4vPz0QrwDqHde/OdVWAZl -h5Pzw5rDUu84CdfPSWRN1pomCFWG7gVkpuFzIcBfz+smPodyw3BfU8969q6tFACE -pjGPF/RufmHoIaHe2q/c+3HBY06ro0oTqTtRe36v4Jp2HLE/jE8wc+YggTmHE670 -uEXIR9N3fF3AbPVnhimEwcQ5fpJtMonUvfj5Z/4KfKo/0Yrh0wljeRiz/tZgTwwb -h5ATAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy01hwSsFAAjMB0GA1UdDgQWBBRkbb1LScfQthztQJ3l -R+QFCKjXUTAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG -9w0BAQsFAAOCAQEAn8TOFqomU30SmIDIHYBKMRGq3bVDLkDDC2yy6LCCwwG2rpoO -UtnUMig2w3iNQ6nvqR4LJB1ha0hLK5FP3iX/JcqZiO0NaucOTe7aJlt9taCADgAw -4vRW/pDuxtq7H1hc2pOue6i05UtGqy2E12jYowQc8a/5hylfEO3b5t5Z7xoQzyAZ -1ov7sYatBinwhqyDI5qNvZCuyT7SMx7H10T7cPrEec4uq55AJ0ReXnAAy1MLhpGd -nW5FX3F4gnyJcK2xL/V+ScL4NTzA8qWT+qOK33KxU1qrGripAkFaF6Z110nuIDiP -Z2tneIovCKKChgFsmZjy2spRpDw6R3Am6rXjpA== ------END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-5-key.pem b/deploy/dockerephemeral/docker/redis-node-5-key.pem deleted file mode 100644 index 467778629cd..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-5-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCSXrnTzrNfHudX -jC0Ah0CiFRe3yp4j6cN3Hfv4snS6tPWXM4MH9Dka8zMLZvzRVQZK3PxDh/R/DQYB -ZhpyLEvT7wYCDsS+F+tie2sPjzSAbdM5dolD8fGwACOqobI4vPz0QrwDqHde/OdV -WAZlh5Pzw5rDUu84CdfPSWRN1pomCFWG7gVkpuFzIcBfz+smPodyw3BfU8969q6t -FACEpjGPF/RufmHoIaHe2q/c+3HBY06ro0oTqTtRe36v4Jp2HLE/jE8wc+YggTmH -E670uEXIR9N3fF3AbPVnhimEwcQ5fpJtMonUvfj5Z/4KfKo/0Yrh0wljeRiz/tZg -Twwbh5ATAgMBAAECggEAFEPciJsaseBueUGQItLsZkRzVzVMtdOHW4uhjOpFnRVc -LLCrbe4opeGRaf0P+HpEIm38LewPNDP9ETPYv4FV3PmVTwhKbGNAFLovtXocnmzA -4jjWLRESEaMYombmwlJFghq8kJPCNeWKsIHnyNDU8YVd0mM+JE0V6GjUjq5YA0x4 -co87wiNxAtjdNuAmI8elOqH3YhCwCQjYO1NeEJwIhWz5tgb2J9Rvn85LOh65cU17 -FaxiTBNSrMW44yk+uhEyj8IDbcZax0s8gLbCSLIj/MnuSbm74VkGKXme0U3mJSmn -dY2tpO3DnNZ+qvakpUk50e/LXYFofsJH9cs1BlZ7AQKBgQDJufGOp07KcIG4N3ei -YxH1IRZ8vThOHksbKVnzQLRcJcEY6SHrL3DgxS+kO3IfjkKZGw5vZgV4/jfTfWQP -eDXwIl0t/YVCEDECppfAIN7fyvIVI14quRogbIrn0jn5ijhVzPI8SWvi/viFbFvn -2O/8KUaHudv9yQ6zKItZ1zHAkwKBgQC5wBLKYdeQT5EfvfXT+rHoioUyywFxTpOF -em14JfNwKLdhqEVB99MzGEdRs6HNz88YbhKQpuEQjwJkbBXZUpAXyYPLDN5uQtV7 -Xw1MY7d8O7U5qNevos+Yti8rrv4w8Cb8ppOX0DJ2SD7J4OQjuyiRYx6sE+tQH6p+ -6N2Gt9YigQKBgQCqpnt7s3uK9Aw42+t/2xFo7lnIooYMR8I/swaeKsGpJmMpAKep -/pMeApHf/E359e3O+b2HbaX5ig2OAwhvscDnaRqsekiN74aWeHntlaEVbujGCwpx -V++LOGd13zkeKdiodN0DNRVojUuOC3HgO3whNIWu8gLxuXGPDCB+mvZCswKBgH+I -vh4QgZYG22iE37U0ylQUT5HpSktGnQknXuQAgp1+hzJY+3xosKzDPax9/lk2FkX6 -xWpl+d+JoSXcBEBbbK24YXHXmxzvbG4xfAr36DI3OJ2nLLfdvFVouQhwNPza1pnf -sTSp8Qu/XMT1UQ6rYRY5jQSvBIDVzRUnw3nM3QyBAoGAXs5Mg1jcQme6X56e0Db0 -zDCcEJuYL+nWXSkClsQCaDwafi4PQVP/V351Qruw0n98grD5vacz1HdXosvCaACJ -8P8e4sFJmSGu8SQt4zbReq8DHNTWZyPC8muurnMSKtfg3XulY8SFsoog7dlMzGGY -IMDiEb5jIb6DFcpNxjigXsM= ------END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-5.conf b/deploy/dockerephemeral/docker/redis-node-5.conf deleted file mode 100644 index ba2cfde9c65..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-5.conf +++ /dev/null @@ -1,17 +0,0 @@ -port 0 -tls-port 6377 -tls-cert-file /usr/local/etc/redis/cert.pem -tls-key-file /usr/local/etc/redis/key.pem -tls-ca-cert-file /usr/local/etc/redis/ca.pem -tls-auth-clients no -tls-cluster yes -tls-replication yes - -cluster-enabled yes -cluster-config-file nodes.conf -cluster-node-timeout 5000 - -appendonly yes - -requirepass very-secure-redis-cluster-password -masterauth very-secure-redis-cluster-password diff --git a/deploy/dockerephemeral/docker/redis-node-6-cert.pem b/deploy/dockerephemeral/docker/redis-node-6-cert.pem deleted file mode 100644 index c176eae043d..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-6-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmN9ktdBsuxTOPFUsU -qAjMnQSyBz/BpYDGMagy9e7PbtniVGTHHOvGgoq5VvPdtiVTerwefNAQaL3nLLvg -24hOEWBlQuBgK0gW48NPZJAbzYvNdF2jOzIzsDu8edEz4TcI8oKvw2WS5HQGl213 -06f2tMN1Ng0O07WoW8cxOYISsKVT9EyQJX4M/Oq5/nzHkXvS97ayFT0OvVdIRzPU -A6VsSyr/X1LgVmZEGfWcdv+cxJGBiXRsiWdW+Y+n6qvRBC2WpTEhCXomtbbDtuSH -e+8EXk9eKSc5QYFNCDWEMk25JuEQpXIMfdiHbMmK+9BgdRTUh8Pm94yD3hkMO6z5 -N5e3AgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy02hwSsFAAkMB0GA1UdDgQWBBRyt96xEM6o5VkG9JV5 -vLVxnBELSjAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG -9w0BAQsFAAOCAQEAmxFmsjenSgrrI1sE7DJahX1CaNVGodx4CwVc2etEq5PBWC6r -DpfCcYDW+Hg64Ac+NiPaLxFaG/8aM7JSePbAa71AQN+2hJpsV3/ANvSUaJfbHSFx -xfTRr8m5l33IV7ynjvZCPXWK4Gc5o7/shPKObHjwb03DLJjW0rvD5SYIjfCLjlOk -na2ufQnrmEP0XO77EvP4G/sHBjUaXrthsYTISO3lBTnGoKWNj8YwTFtXILC3O1to -sKWKYe5A6FB6xathUVBfS+Drp0PIYdAU9N3adymv4tZf52ofMsbJNkDqY3JaWmcO -dYHuYTeYg6ZiVhzZeasd3V+wc/CKAD8U5UfD5A== ------END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-6-key.pem b/deploy/dockerephemeral/docker/redis-node-6-key.pem deleted file mode 100644 index 0bc3f366189..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-6-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCmN9ktdBsuxTOP -FUsUqAjMnQSyBz/BpYDGMagy9e7PbtniVGTHHOvGgoq5VvPdtiVTerwefNAQaL3n -LLvg24hOEWBlQuBgK0gW48NPZJAbzYvNdF2jOzIzsDu8edEz4TcI8oKvw2WS5HQG -l21306f2tMN1Ng0O07WoW8cxOYISsKVT9EyQJX4M/Oq5/nzHkXvS97ayFT0OvVdI -RzPUA6VsSyr/X1LgVmZEGfWcdv+cxJGBiXRsiWdW+Y+n6qvRBC2WpTEhCXomtbbD -tuSHe+8EXk9eKSc5QYFNCDWEMk25JuEQpXIMfdiHbMmK+9BgdRTUh8Pm94yD3hkM -O6z5N5e3AgMBAAECggEAQ088YB6zX0Y2McvyooPFRG6VVy5+UAGgWyICtdhHg7Kl -AvUf9k2s4K8+U/11NaQsC1kZUtNCQlLYDARedJkR4mNBAOCLEgaU48gJ8F2NyeR7 -p5Bm1tIC61GDbzh5UiPycGocJ+bdfBWNMpohlzObwdjDifSAZy+uUWYRDMr39G7x -9SH6aLL7ZHBg0Oc4dw6K4GQMrU7sdomQcSqNyi5sn6PN8FsuO1wMp8C8V+U6y5sb -36Y1rz90ZOFqmOBnG/IdPR8tFbdql1Yy31tzy/I4thK+1v4QN6JVLPvyw4H0RzFe -j347k5IsNehRdwltplhckeAUzWGGNiTx0zhQPAchuQKBgQDmyGB055GCtRoEIpqN -ANNa8PxTp2sCH+/J7KZma6gSJ9WY73xtGSVXX/Ubz4l8FHiGoA0CQCElARJ9zff/ -tAiNXqvcQeBPVC23CMJL3hxeHLNs0ipoD8qvdQpGit3DAZMjdjtt5jd48CulEmfP -/rVmeHKChZaPPR1EgrMnIytaCwKBgQC4YWygHnDjW9zekpsDRMKkvK5QMIey9ygB -LqXlXw6GANhVDGSr7zOHBtF1aBc6FA1FKlVRXz3Fag4pPZLd2HbEaKnzfCNPH5PL -UTX8fukftrzY03bvpYcr+/YabPO8H5hkeUqHyH9EyIgdj5hOhKEVj9kJkqENt3el -GvohkgdwhQKBgG0itPqTx6wYGIV8F7o2eby32Zt1wJTwpWTIFKi6oHB1hf0cw6qU -CaSYLEFKk6mpxJVlesFlskbdivETRgQWDzVLX9p5DKp3FGdKLRfToXaf+/mqKYOs -dB0lLAbQBK8DP6G1d8Uw6Wq3qOwXGCC0QvSCYSR4KAr0y7JqXG5Vo1qhAoGATLCh -GNxwgfDEpoL+HNbtys18B3iYCLVKm2tGr2fhR5V0ZbOY7/a3TPNmDdp0xsBuYJVi -FU1zCPi62SZ2PvX5OGp8Pf0lRpTQyWGG/fXfi0RbuigCsVz9IytSyt0EZ/wQS8Iz -YNThMr/h9cGzTP1Xbvt8/8FQYb8s8ayN24a8t20CgYBDyjVifJHw6iVl3vu/O+R9 -+AdSe5bEGGDuIKZRDJbEj2ScgD3Nwqdst7X5wC+rcUuyJNW22GihyLiC/+OCaJPl -9fyaRpWWjEUkzpvR+3GhzzykDnemw1z39AJrg3ewSaBdbw9Bvq0ebrGaDF+uCReY -V+yVEYFsBaK0JrbkIffXbA== ------END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-6.conf b/deploy/dockerephemeral/docker/redis-node-6.conf deleted file mode 100644 index 2989c5550ea..00000000000 --- a/deploy/dockerephemeral/docker/redis-node-6.conf +++ /dev/null @@ -1,17 +0,0 @@ -port 0 -tls-port 6378 -tls-cert-file /usr/local/etc/redis/cert.pem -tls-key-file /usr/local/etc/redis/key.pem -tls-ca-cert-file /usr/local/etc/redis/ca.pem -tls-auth-clients no -tls-cluster yes -tls-replication yes - -cluster-enabled yes -cluster-config-file nodes.conf -cluster-node-timeout 5000 - -appendonly yes - -requirepass very-secure-redis-cluster-password -masterauth very-secure-redis-cluster-password diff --git a/docs/src/developer/developer/building.md b/docs/src/developer/developer/building.md index 6b754e073bb..845053fbf5f 100644 --- a/docs/src/developer/developer/building.md +++ b/docs/src/developer/developer/building.md @@ -166,7 +166,6 @@ These services require most of the deployment dependencies as seen in the archit - Required internal dependencies: - cassandra (with the correct schema) - elasticsearch (with the correct schema) - - redis - Required external dependencies are the following configured AWS services (or “fake” replacements providing the same API): - SES - SQS diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 570a4d355d1..c1b48558c7c 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -1952,96 +1952,6 @@ elasticsearch-index: insecureSkipVerifyTls: true ``` -## Configure Redis authentication - -If the redis used needs authentication with either username and password or just -password (legacy auth), it can be configured like this: - -```yaml -gundeck: - secrets: - redisUsername: - redisPassword: -``` - -**NOTE**: When using redis < 6, the `redisUsername` must not be set at all (not -even set to `null` or empty string, the key must be absent from the config). -When using redis >= 6 and using legacy auth, the `redisUsername` must either be -not set at all or set to `"default"`. - -While doing migrations to another redis instance, the credentials for the -addtional redis can be set as follows: - -```yaml -gundeck: - secrets: - redisAdditionalWriteUsername: # Do not set this at all when using legacy auth - redisAdditionalWritePassword: -``` - -**NOTE**: `redisAddtiionalWriteUsername` follows same restrictions as -`redisUsername` when using legacy auth. - -## Configure TLS for Redis - -If the redis instance requires TLS, it can be configured like this: - -```yaml -gundeck: - config: - redis: - enableTls: true -``` - -In case a custom CA certificate is required it can be provided like this: - -```yaml -gundeck: - config: - redis: - tlsCa: -``` - -There is another way to provide this, in case there already exists a kubernetes -secret containing the CA certificate(s): - -```yaml -gundeck: - config: - redis: - tlsCaSecretRef: - name: - key: -``` - -For configuring `redisAdditionalWrite` in gundeck (this is required during a -migration from one redis instance to another), the settings need to be like -this: - -```yaml -gundeck: - config: - redisAdditionalWrite: - enableTls: true - # One or none of these: - # tlsCa: - # tlsCaSecretRef: -``` - -**WARNING:** Please do this only if you know what you’re doing. - -In case it is not possible to verify TLS certificate of the redis -server, it can be turned off without tuning off TLS like this: - -```yaml -gundeck: - config: - redis: - insecureSkipVerifyTls: true - redisAdditionalWrite: - insecureSkipVerifyTls: true -``` - ## Configure RabbitMQ RabbitMQ authentication must be configured on brig, galley and background-worker. For example: @@ -2074,7 +1984,7 @@ server, verification can be turned off by settings `insecureSkipVerifyTls` to ## Configure PostgreSQL -`brig`, `galley`, and `background-worker` require a PostgreSQL database. The configured user needs to +`brig`, `galley`, `gundeck`, and `background-worker` require a PostgreSQL database. The configured user needs to be able to write data and change the schema (e.g. create and alter tables.) The internal configuration YAML file format and the Helm charts for `brig` and @@ -2131,7 +2041,7 @@ The `port` needs to be a number provided as string. Besides the password file (`postgresqlPassword`), the fields correspond to [libpq-connect parameters](https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS). -The `postgresqlPassword` file is read by `brig`, `galley`, and `background-worker`. Its content is +The `postgresqlPassword` file is read by `brig`, `galley`, `gundeck`, and `background-worker`. Its content is used as `password` field. ### Using PostgreSQL for storing Cassandra-backed data diff --git a/docs/src/how-to/install/infrastructure-configuration.md b/docs/src/how-to/install/infrastructure-configuration.md index 34e1eb2c19d..06ef7a218d1 100644 --- a/docs/src/how-to/install/infrastructure-configuration.md +++ b/docs/src/how-to/install/infrastructure-configuration.md @@ -27,7 +27,6 @@ gundeck: - "10.0.0.0/8" - "elasticsearch-external" - "cassandra-external" - - "redis-ephemeral" - "fake-aws-sqs" - "fake-aws-dynamodb" - "fake-aws-sns" @@ -415,8 +414,8 @@ cassandra cannot reliably be installed on kubernetes. Some people have tried, e.g. [this project](https://github.com/instaclustr/cassandra-operator) though at the time of writing (Nov 2018), this does not yet work as advertised. We -recommend therefore to install cassandra, (possibly also elasticsearch -and redis) separately, i.e. outside of kubernetes (using 3 nodes each). +recommend therefore to install cassandra, (possibly also elasticsearch) +separately, i.e. outside of kubernetes (using 3 nodes each). For further higher-availability: diff --git a/docs/src/how-to/install/troubleshooting.md b/docs/src/how-to/install/troubleshooting.md index ecb0ffb636a..3703500a9fd 100644 --- a/docs/src/how-to/install/troubleshooting.md +++ b/docs/src/how-to/install/troubleshooting.md @@ -252,7 +252,7 @@ These are some steps you can take to debug what is going on when the installatio As an example, we’ll take a case where we try installing `wire-server` with `helm`, but it fails due to `cassandra` being broken in some way. -This guide, while focusing on a `cassandra` related issue, will also provide general steps to debug problems that could be related to other components like `rabbitmq`, `redis`, etc. +This guide, while focusing on a `cassandra` related issue, will also provide general steps to debug problems that could be related to other components like `rabbitmq`, etc. Our first step is to identify and isolate which component is causing the issue. @@ -266,7 +266,6 @@ fake-aws-sns-76fb45cf4f-t6mg6 2/2 Running 0 75m fake-aws-sqs-6495cd7c98-w8f8w 2/2 Running 0 75m rabbitmq-external-0 0/1 Pending 0 78m reaper-84cfbf746d-wk8nc 1/1 Running 0 75m -redis-ephemeral-master-0 1/1 Running 0 76m ``` We then run the `wire-server` helm installation command: @@ -294,7 +293,6 @@ fake-aws-sns-76fb45cf4f-t6mg6 2/2 Running 0 95m fake-aws-sqs-6495cd7c98-w8f8w 2/2 Running 0 95m rabbitmq-external-0 0/1 Pending 0 98m reaper-84cfbf746d-wk8nc 1/1 Running 0 95m -redis-ephemeral-master-0 1/1 Running 0 96m ``` (You can also do `d kubectl get pods -o wide` to get more details though that’s not necessary here) diff --git a/hack/bin/gen-certs.sh b/hack/bin/gen-certs.sh index f995a238aaa..d4840f8af6d 100755 --- a/hack/bin/gen-certs.sh +++ b/hack/bin/gen-certs.sh @@ -81,19 +81,6 @@ install_certs "$TEMP/es" "$ROOT_DIR/deploy/dockerephemeral/docker" \ install_certs "$TEMP/es" "$ROOT_DIR/hack/helm_vars/certs" \ elasticsearch-ca elasticsearch-ca-key -# redis -mkdir -p "$TEMP/redis" -gen_ca "$TEMP/redis" redis.ca.example.com -REDIS="$ROOT_DIR/deploy/dockerephemeral/docker" -cp "$TEMP/redis/ca.pem" "$REDIS/redis-ca.pem" -for redis_node in $(seq 1 6); do - gen_cert "$TEMP/redis" "DNS:redis-${redis_node}, IP:172.20.0.3${redis_node}" - chmod 0644 "$TEMP/redis/key.pem" - install_certs "$TEMP/redis" "$REDIS" "" "" \ - "redis-node-${redis_node}-cert" \ - "redis-node-${redis_node}-key" -done - # rabbitmq RABBITMQ="$ROOT_DIR/deploy/dockerephemeral/rabbitmq-config/certificates" gen_ca "$RABBITMQ" rabbitmq.ca.example.com diff --git a/hack/helm_vars/certs/values.yaml.gotmpl b/hack/helm_vars/certs/values.yaml.gotmpl index 307d50fa48a..7cd8a633653 100644 --- a/hack/helm_vars/certs/values.yaml.gotmpl +++ b/hack/helm_vars/certs/values.yaml.gotmpl @@ -16,59 +16,6 @@ resources: ca: secretName: elasticsearch-ca - # redis CA and certificate - - apiVersion: cert-manager.io/v1 - kind: Issuer - metadata: - name: redis-ca-issuer - namespace: '{{ .Release.Namespace }}' - spec: - selfSigned: {} - - apiVersion: cert-manager.io/v1 - kind: Certificate - metadata: - name: redis-ca - namespace: '{{ .Release.Namespace }}' - spec: - secretName: redis-ca-certificate - isCA: true - duration: 2160h # 90d - renewBefore: 360h # 15d - commonName: redis.example.com - privateKey: - algorithm: RSA - encoding: PKCS1 - size: 2048 - issuerRef: - name: redis-ca-issuer - kind: Issuer - - apiVersion: cert-manager.io/v1 - kind: Issuer - metadata: - name: redis-issuer - namespace: '{{ .Release.Namespace }}' - spec: - ca: - secretName: redis-ca-certificate - - apiVersion: cert-manager.io/v1 - kind: Certificate - metadata: - name: redis - namespace: '{{ .Release.Namespace }}' - spec: - secretName: redis-certificate - isCA: false - duration: 2160h # 90d - renewBefore: 360h # 15d - commonName: redis-ephemeral - privateKey: - algorithm: RSA - encoding: PKCS1 - size: 2048 - issuerRef: - name: redis-issuer - kind: Issuer - # RabbitMQ CA and certificate - apiVersion: cert-manager.io/v1 kind: Issuer diff --git a/hack/helm_vars/redis-ephemeral/values.yaml b/hack/helm_vars/redis-ephemeral/values.yaml deleted file mode 100644 index 996dc30e45c..00000000000 --- a/hack/helm_vars/redis-ephemeral/values.yaml +++ /dev/null @@ -1,47 +0,0 @@ -redis-ephemeral: - image: - registry: public.ecr.aws - repository: docker/library/redis - - redisConfig: | - requirepass very-secure-redis-master-password - - # ephemeral - save "" - - port 0 - tls-port 6379 - tls-cert-file /data/ssl/tls.crt - tls-key-file /data/ssl/tls.key - tls-ca-cert-file /data/ssl/ca.crt - tls-auth-clients no - - extraRedisSecrets: - - name: redis-certificate - mountPath: /data/ssl - - livenessProbe: - enabled: false - customLivenessProbe: - exec: - command: - - sh - - -c - - redis-cli --tls --cacert /data/ssl/ca.crt ping - readinessProbe: - enabled: false - customReadinessProbe: - exec: - command: - - sh - - -c - - redis-cli --tls --cacert /data/ssl/ca.crt ping - startupProbe: - enabled: false - customStartupProbe: - exec: - command: - - sh - - -c - - redis-cli --tls --cacert /data/ssl/ca.crt ping - diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 556e8e1a225..673cb6da817 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -492,13 +492,11 @@ gundeck: tlsCaSecretRef: name: "rabbitmq-certificate" key: "ca.crt" - redis: - host: redis-ephemeral - connectionMode: master - enableTls: true - tlsCaSecretRef: - name: "redis-certificate" - key: "ca.crt" + postgresql: + host: "postgresql" + port: "5432" + user: wire-server + dbname: wire-server aws: account: "123456789012" region: eu-west-1 @@ -514,7 +512,7 @@ gundeck: secrets: awsKeyId: dummykey awsSecretKey: dummysecret - redisPassword: very-secure-redis-master-password + pgPassword: posty-the-gres rabbitmq: username: {{ .Values.rabbitmqUsername }} password: {{ .Values.rabbitmqPassword }} @@ -529,7 +527,6 @@ gundeck: uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} {{- end }} - redisAdditionalWritePassword: very-secure-redis-master-password-2 nginz: replicaCount: 1 @@ -729,10 +726,6 @@ integration: tlsCaSecretRef: name: {{ .Values.elasticsearch.caSecretName }} key: "ca.crt" - redis: - tlsCaSecretRef: - name: "redis-certificate" - key: "ca.crt" rabbitmq: tlsCaSecretRef: name: "rabbitmq-certificate" @@ -746,7 +739,6 @@ integration: uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} {{- end }} - redisPassword: very-secure-redis-master-password tls: caNamespace: wire-federation-v0 diff --git a/hack/helmfile.yaml.gotmpl b/hack/helmfile.yaml.gotmpl index 09c825f9c31..f28ed995af5 100644 --- a/hack/helmfile.yaml.gotmpl +++ b/hack/helmfile.yaml.gotmpl @@ -114,15 +114,6 @@ releases: values: - './helm_vars/certs/values.yaml.gotmpl' - - name: 'redis-ephemeral' - namespace: '{{ .Values.namespace1 }}' - chart: '../.local/charts/redis-ephemeral' - values: - - './helm_vars/wire-image-mirror.yaml' - - './helm_vars/redis-ephemeral/values.yaml' - needs: - - certs - - name: 'cassandra-ephemeral' namespace: '{{ .Values.namespace1 }}' chart: '../.local/charts/cassandra-ephemeral' @@ -147,15 +138,6 @@ releases: name: elasticsearch kind: Issuer - - name: 'redis-ephemeral' - namespace: '{{ .Values.namespace2 }}' - chart: '../.local/charts/redis-ephemeral' - values: - - './helm_vars/wire-image-mirror.yaml' - - './helm_vars/redis-ephemeral/values.yaml' - needs: - - certs - - name: 'cassandra-ephemeral' namespace: '{{ .Values.namespace2 }}' chart: '../.local/charts/cassandra-ephemeral' @@ -216,22 +198,6 @@ releases: values: - './helm_vars/opensearch/values.yaml.gotmpl' - # Required for testing redis migration - - name: redis-ephemeral-2 - namespace: '{{ .Values.namespace1 }}' - chart: '../.local/charts/redis-ephemeral' - values: - - redis-ephemeral: - image: - registry: public.ecr.aws - repository: docker/library/redis - - redisConfig: | - requirepass very-secure-redis-master-password-2 - - # ephemeral - save "" - - name: 'certs' namespace: '{{ .Values.namespace2 }}' chart: bedag/raw @@ -358,7 +324,6 @@ releases: value: true needs: - 'cassandra-ephemeral' - - 'redis-ephemeral' - 'postgresql' - name: 'wire-server' @@ -376,7 +341,6 @@ releases: value: {{ .Values.federationDomain2 }} needs: - 'cassandra-ephemeral' - - 'redis-ephemeral' - 'postgresql' - name: wire-server-enterprise diff --git a/libs/wire-api/src/Wire/API/Presence.hs b/libs/wire-api/src/Wire/API/Presence.hs index 427e0a0f8af..9ec538f064f 100644 --- a/libs/wire-api/src/Wire/API/Presence.hs +++ b/libs/wire-api/src/Wire/API/Presence.hs @@ -23,7 +23,6 @@ import Data.Aeson.Types qualified as A import Data.Attoparsec.ByteString (takeByteString) import Data.ByteString.Char8 qualified as Bytes import Data.ByteString.Conversion -import Data.ByteString.Lazy qualified as Lazy import Data.Id import Data.Misc (Milliseconds) import Data.OpenApi qualified as S @@ -35,7 +34,6 @@ import Imports import Network.URI qualified as Net import Servant.API (ToHttpApiData (toUrlPiece)) --- FUTUREWORK: use Network.URI and toss this newtype. servant should have all these instances for us these days. newtype URI = URI { fromURI :: Net.URI } @@ -77,9 +75,7 @@ data Presence = Presence -- operating the team settings pages without the need for -- end-to-end crypto. clientId :: !(Maybe ClientId), - createdAt :: !Milliseconds, - -- | REFACTOR: temp. addition to ease migration - __field :: !Lazy.ByteString + createdAt :: !Milliseconds } deriving (Eq, Ord, Show) deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema Presence) @@ -94,7 +90,6 @@ instance ToSchema Presence where <*> clientId .= optField "client_id" (maybeWithDefault A.Null schema) -- keep null for backwards compat <*> createdAt .= (fromMaybe 0 <$> (optField "created_at" schema)) ) - <&> ($ ("" :: Lazy.ByteString)) uriSchema :: ValueSchema NamedSwaggerDoc URI uriSchema = mkSchema desc uriFromJSON (Just . uriToJSON) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Presence.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Presence.hs index 97005af0a0d..adb5f582d44 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Presence.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Presence.hs @@ -35,7 +35,6 @@ testObject_Presence_1 = (fromJust $ parse "http://example.com/") Nothing 0 - "" testObject_Presence_2 :: Presence testObject_Presence_2 = @@ -45,7 +44,6 @@ testObject_Presence_2 = (fromJust $ parse "http://example.com/3") (Just (ClientId 1)) 12323 - "" -- __field always has to be "", see ToSchema instance. testObject_Presence_3 :: Presence testObject_Presence_3 = @@ -55,4 +53,3 @@ testObject_Presence_3 = (fromJust $ parse "http://example.com/3") (Just (ClientId 1)) 0 - "" -- __field always has to be "", see ToSchema instance. diff --git a/libs/wire-subsystems/postgres-migrations/20260828093750-gundeck-presence.sql b/libs/wire-subsystems/postgres-migrations/20260828093750-gundeck-presence.sql new file mode 100644 index 00000000000..eb84c4ed7f1 --- /dev/null +++ b/libs/wire-subsystems/postgres-migrations/20260828093750-gundeck-presence.sql @@ -0,0 +1,12 @@ +-- WPB-28377: gundeck presence (replaces redis presence hashes) +CREATE TABLE IF NOT EXISTS presence ( + user_id uuid NOT NULL, + conn_id text NOT NULL, + resource text NOT NULL, + client_id text, + created_at timestamptz NOT NULL, + PRIMARY KEY (user_id, conn_id) +); + +-- index for cleanup deletes like `DELETE ... WHERE created_at < now() - interval '7 days'` +CREATE INDEX presence_created_at_idx ON presence (created_at); diff --git a/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs b/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs index 8d8fe1242ea..cd74664b8a6 100644 --- a/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs +++ b/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs @@ -19,11 +19,13 @@ -- with this program. If not, see . module Wire.JobSubsystem.Migrations - ( mkArbiterConnectionString, + ( defaultSchemaName, + mkArbiterConnectionString, runJobMigrations, ) where +import Arbiter.Core (defaultSchemaName) import Arbiter.Migrations qualified as ArbiterMigrations import Control.Exception (bracket, bracket_, throwIO) import Data.Hashable qualified as Hashable diff --git a/libs/wire-subsystems/src/Wire/Postgres.hs b/libs/wire-subsystems/src/Wire/Postgres.hs index f91f3637efe..a0210c21cf8 100644 --- a/libs/wire-subsystems/src/Wire/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/Postgres.hs @@ -43,6 +43,7 @@ module Wire.Postgres runTransaction, runTransactionWithRetry, runPipeline, + useWithResetAndRetry, parseCount, PGConstraints, diff --git a/postgres-schema.sql b/postgres-schema.sql index de4a57a0f2e..2d3df5fb27f 100644 --- a/postgres-schema.sql +++ b/postgres-schema.sql @@ -1511,6 +1511,21 @@ CREATE TABLE public.mls_history_client ( ALTER TABLE public.mls_history_client OWNER TO "wire-server"; +-- +-- Name: presence; Type: TABLE; Schema: public; Owner: wire-server +-- + +CREATE TABLE public.presence ( + user_id uuid NOT NULL, + conn_id text NOT NULL, + resource text NOT NULL, + client_id text, + created_at timestamp with time zone NOT NULL +); + + +ALTER TABLE public.presence OWNER TO "wire-server"; + -- -- Name: remote_conversation_local_member; Type: TABLE; Schema: public; Owner: wire-server -- @@ -1967,6 +1982,14 @@ ALTER TABLE ONLY public.mls_history_client ADD CONSTRAINT mls_history_client_pkey PRIMARY KEY (group_id, id); +-- +-- Name: presence presence_pkey; Type: CONSTRAINT; Schema: public; Owner: wire-server +-- + +ALTER TABLE ONLY public.presence + ADD CONSTRAINT presence_pkey PRIMARY KEY (user_id, conn_id); + + -- -- Name: remote_conversation_local_member remote_conversation_local_member_pkey; Type: CONSTRAINT; Schema: public; Owner: wire-server -- @@ -2439,6 +2462,13 @@ CREATE INDEX idx_meetings_recurrence_eff_end ON public.meetings USING btree (GRE CREATE INDEX idx_meetings_start_time ON public.meetings USING btree (start_time); +-- +-- Name: presence_created_at_idx; Type: INDEX; Schema: public; Owner: wire-server +-- + +CREATE INDEX presence_created_at_idx ON public.presence USING btree (created_at); + + -- -- Name: user_group_member_user_id_idx; Type: INDEX; Schema: public; Owner: wire-server -- diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index fd170bdbaff..20365d1ebb9 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -69,6 +69,7 @@ import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai import Wire.API.User (AccountStatus (PendingInvitation)) import Wire.DeleteQueue +import Wire.JobSubsystem.Migrations (defaultSchemaName, mkArbiterConnectionString, runJobMigrations) import Wire.OpenTelemetry (withTracer) import Wire.PostgresMigrations import Wire.Sem.Paging qualified as P @@ -117,6 +118,10 @@ migratePostgres opts resetFirst = do pool <- (.rawPool) <$> initPostgresPool opts.postgresqlPool opts.postgresql opts.postgresqlPassword when resetFirst $ resetSchema pool logger runAllMigrations pool logger + -- Also create the arbiter job schema, so that this command yields the full + -- database schema (e.g. for `make postgres-schema`). + arbiterConnStr <- mkArbiterConnectionString opts.postgresql opts.postgresqlPassword + runJobMigrations arbiterConnStr defaultSchemaName flush logger mkApp :: Opts -> IO (Wai.Application, Env) diff --git a/services/gundeck/default.nix b/services/gundeck/default.nix index 2e4f8b69d5f..5de25d7fff5 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -22,14 +22,14 @@ , conduit , containers , criterion -, crypton-x509-store , data-timeout , errors , exceptions , extended , extra , foldl -, hedis +, hasql +, hasql-th , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk , HsOpenSSL @@ -37,7 +37,6 @@ , http-client-tls , http-types , imports -, kan-extensions , lens , lens-aeson , lib @@ -46,7 +45,6 @@ , MonadRandom , mtl , multiset -, network , network-uri , optparse-applicative , prometheus-client @@ -79,6 +77,7 @@ , unliftio , unordered-containers , uuid +, vector , wai , wai-extra , wai-middleware-gunzip @@ -86,6 +85,7 @@ , websockets , wire-api , wire-otel +, wire-subsystems , yaml }: mkDerivation { @@ -110,14 +110,14 @@ mkDerivation { bytestring-conversion cassandra-util containers - crypton-x509-store data-timeout errors exceptions extended extra foldl - hedis + hasql + hasql-th hs-opentelemetry-instrumentation-wai hs-opentelemetry-sdk http-client @@ -148,12 +148,14 @@ mkDerivation { unliftio unordered-containers uuid + vector wai wai-extra wai-middleware-gunzip wai-utilities wire-api wire-otel + wire-subsystems yaml ]; executableHaskellDepends = [ @@ -173,10 +175,8 @@ mkDerivation { http-client http-client-tls imports - kan-extensions lens lens-aeson - network network-uri optparse-applicative random @@ -191,7 +191,6 @@ mkDerivation { tinylog types-common uuid - wai-utilities websockets wire-api yaml diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index 06bf1b5024f..9ad88362bc4 100644 --- a/services/gundeck/gundeck.cabal +++ b/services/gundeck/gundeck.cabal @@ -39,7 +39,6 @@ library Gundeck.Push.Native.Types Gundeck.Push.Websocket Gundeck.React - Gundeck.Redis Gundeck.Run Gundeck.Schema.Run Gundeck.Schema.V1 @@ -58,7 +57,6 @@ library Gundeck.ThreadBudget.Internal Gundeck.Util Gundeck.Util.DelayQueue - Gundeck.Util.Redis other-modules: Paths_gundeck hs-source-dirs: src @@ -126,14 +124,14 @@ library , bytestring-conversion >=0.2 , cassandra-util >=0.16.2 , containers >=0.5 - , crypton-x509-store , data-timeout , errors >=2.0 , exceptions >=0.4 , extended , extra >=1.1 , foldl - , hedis >=0.14.0 + , hasql + , hasql-th , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk , http-client >=0.7 @@ -164,12 +162,14 @@ library , unliftio >=0.2 , unordered-containers >=0.2 , uuid >=1.3 + , vector , wai >=3.2 , wai-extra >=3.0 , wai-middleware-gunzip >=0.0.2 , wai-utilities >=0.16 , wire-api , wire-otel + , wire-subsystems , yaml >=0.8 default-language: GHC2021 @@ -244,7 +244,6 @@ executable gundeck-integration Metrics Paths_gundeck TestSetup - Util hs-source-dirs: test/integration default-extensions: @@ -297,7 +296,7 @@ executable gundeck-integration build-depends: , aeson , async - , base >=4 && <5 + , base >=4 && <5 , base16-bytestring >=0.1 , bilge , bytestring @@ -310,10 +309,8 @@ executable gundeck-integration , http-client , http-client-tls , imports - , kan-extensions , lens , lens-aeson - , network , network-uri , optparse-applicative , random @@ -327,7 +324,6 @@ executable gundeck-integration , tinylog , types-common , uuid - , wai-utilities >=0.16 , websockets >=0.8 , wire-api , yaml diff --git a/services/gundeck/gundeck.integration.yaml b/services/gundeck/gundeck.integration.yaml index 00c80574794..bb19ab89eb7 100644 --- a/services/gundeck/gundeck.integration.yaml +++ b/services/gundeck/gundeck.integration.yaml @@ -13,18 +13,17 @@ cassandra: keyspace: gundeck_test # filterNodesByDatacentre: datacenter1 -redis: - host: 172.20.0.31 - port: 6373 - connectionMode: cluster # master | cluster - enableTls: true - tlsCa: ../../deploy/dockerephemeral/docker/redis-ca.pem - insecureSkipVerifyTls: false - -# redisAdditionalWrite: -# host: 127.0.0.1 -# port: 6379 -# connectionMode: master +postgresql: + host: 127.0.0.1 + port: "5432" + user: wire-server + dbname: backendA + password: posty-the-gres + +postgresqlPool: + size: 20 + acquisitionTimeout: 10s + idlenessTimeout: 10m aws: queueName: integration-gundeck-events diff --git a/services/gundeck/src/Gundeck/Env.hs b/services/gundeck/src/Gundeck/Env.hs index 39f6f98bda7..a7f233fb2c0 100644 --- a/services/gundeck/src/Gundeck/Env.hs +++ b/services/gundeck/src/Gundeck/Env.hs @@ -23,30 +23,19 @@ import Bilge hiding (host, port) import Cassandra (ClientState) import Cassandra.Util (initCassandraForService) import Control.AutoUpdate -import Control.Concurrent.Async (Async) import Control.Lens (makeLenses, (^.)) -import Control.Retry (capDelay, exponentialBackoff) -import Data.ByteString.Char8 qualified as BSChar8 import Data.Id import Data.Misc (Milliseconds (..)) -import Data.Text qualified as Text -import Data.Time.Clock import Data.Time.Clock.POSIX -import Data.X509.CertificateStore as CertStore -import Database.Redis qualified as Redis import Gundeck.Aws qualified as Aws -import Gundeck.Options as Opt hiding (host, port) -import Gundeck.Options qualified as O -import Gundeck.Redis qualified as Redis +import Gundeck.Options import Gundeck.ThreadBudget +import Hasql.Pool.Extended qualified as HasqlPoolExt import Imports import Network.AMQP (Channel) import Network.AMQP.Extended qualified as Q import Network.HTTP.Client (responseTimeoutMicro) import Network.HTTP.Client.TLS (tlsManagerSettings) -import Network.TLS as TLS -import Network.TLS.Extra qualified as TLS -import System.Logger qualified as Log import System.Logger.Extended qualified as Logger data Env = Env @@ -55,8 +44,7 @@ data Env = Env _applog :: !Logger.Logger, _manager :: !Manager, _cstate :: !ClientState, - _rstate :: !Redis.RobustConnection, - _rstateAdditionalWrite :: !(Maybe Redis.RobustConnection), + _hasqlPool :: !HasqlPoolExt.Pool, _awsEnv :: !Aws.Env, _time :: !(IO Milliseconds), _threadBudgetState :: !(Maybe ThreadBudgetState), @@ -65,7 +53,7 @@ data Env = Env makeLenses ''Env -createEnv :: Opts -> IO ([Async ()], Env) +createEnv :: Opts -> IO Env createEnv o = do l <- Logger.mkLogger (o ^. logLevel) (o ^. logNetStrings) (o ^. logFormat) n <- @@ -76,17 +64,7 @@ createEnv o = do managerResponseTimeout = responseTimeoutMicro 5000000 } - redisUsername <- BSChar8.pack <$$> lookupEnv "REDIS_USERNAME" - redisPassword <- BSChar8.pack <$$> lookupEnv "REDIS_PASSWORD" - (rThread, r) <- createRedisPool l (o ^. redis) redisUsername redisPassword "main-redis" - - (rAdditionalThreads, rAdditional) <- case o ^. redisAdditionalWrite of - Nothing -> pure ([], Nothing) - Just additionalRedis -> do - additionalRedisUsername <- BSChar8.pack <$$> lookupEnv "REDIS_ADDITIONAL_WRITE_USERNAME" - addtionalRedisPassword <- BSChar8.pack <$$> lookupEnv "REDIS_ADDITIONAL_WRITE_PASSWORD" - (rAddThread, rAdd) <- createRedisPool l additionalRedis additionalRedisUsername addtionalRedisPassword "additional-write-redis" - pure ([rAddThread], Just rAdd) + pgPool <- HasqlPoolExt.initPostgresPool (o ^. postgresqlPool) (o ^. postgresql) (o ^. postgresqlPassword) p <- initCassandraForService @@ -104,55 +82,8 @@ createEnv o = do } mtbs <- mkThreadBudgetState `mapM` (o ^. settings . maxConcurrentNativePushes) rabbitMqChannelMVar <- Q.mkRabbitMqChannelMVar l (Just "gundeck") (o ^. rabbitmq) - pure $! (rThread : rAdditionalThreads,) $! Env (RequestId defRequestId) o l n p r rAdditional a io mtbs rabbitMqChannelMVar + pure $! Env (RequestId defRequestId) o l n p pgPool a io mtbs rabbitMqChannelMVar reqIdMsg :: RequestId -> Logger.Msg -> Logger.Msg reqIdMsg = ("request" Logger..=) . unRequestId {-# INLINE reqIdMsg #-} - -createRedisPool :: Logger.Logger -> RedisEndpoint -> Maybe ByteString -> Maybe ByteString -> ByteString -> IO (Async (), Redis.RobustConnection) -createRedisPool l ep username password identifier = do - customCertStore <- case ep._tlsCa of - Nothing -> pure Nothing - Just caPath -> CertStore.readCertificateStore caPath - let defClientParams = defaultParamsClient (Text.unpack ep._host) "" - tlsParams = - guard ep._enableTls - $> defClientParams - { clientHooks = - if ep._insecureSkipVerifyTls - then defClientParams.clientHooks {onServerCertificate = \_ _ _ _ -> pure []} - else defClientParams.clientHooks, - clientShared = - case customCertStore of - Nothing -> defClientParams.clientShared - Just sharedCAStore -> defClientParams.clientShared {sharedCAStore}, - clientSupported = - defClientParams.clientSupported - { supportedVersions = [TLS.TLS13, TLS.TLS12], - supportedCiphers = TLS.ciphersuite_strong - } - } - let redisConnInfo = - Redis.defaultConnectInfo - { Redis.connectAddr = Redis.ConnectAddrHostPort (Text.unpack ep._host) (fromIntegral ep._port), - Redis.connectUsername = username, - Redis.connectAuth = password, - Redis.connectTimeout = Just (secondsToNominalDiffTime 5), - Redis.connectMaxConnections = 100, - Redis.connectTLSParams = tlsParams - } - - Log.info l $ - Log.msg (Log.val $ "starting connection to " <> identifier <> "...") - . Log.field "connectionMode" (show $ ep ^. O.connectionMode) - . Log.field "connInfo" (safeShowConnInfo redisConnInfo) - let connectWithRetry = Redis.connectRobust l (capDelay 1000000 (exponentialBackoff 50000)) - r <- case ep ^. O.connectionMode of - Master -> connectWithRetry $ Redis.checkedConnect redisConnInfo - Cluster -> connectWithRetry $ Redis.checkedConnectCluster redisConnInfo - Log.info l $ Log.msg (Log.val $ "Established connection to " <> identifier <> ".") - pure r - -safeShowConnInfo :: Redis.ConnectInfo -> String -safeShowConnInfo connInfo = show $ connInfo {Redis.connectAuth = "[REDACTED]" <$ Redis.connectAuth connInfo} diff --git a/services/gundeck/src/Gundeck/Monad.hs b/services/gundeck/src/Gundeck/Monad.hs index 832bff5d890..db3eda96184 100644 --- a/services/gundeck/src/Gundeck/Monad.hs +++ b/services/gundeck/src/Gundeck/Monad.hs @@ -33,10 +33,6 @@ module Gundeck.Monad runGundeck, posixTime, getRabbitMqChan, - - -- * Select which redis to target - runWithDefaultRedis, - runWithAdditionalRedis, msToUTCSecs, ) where @@ -53,9 +49,7 @@ import Data.Time (UTCTime) import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import Data.UUID as UUID import Data.UUID.V4 as UUID -import Database.Redis qualified as Redis import Gundeck.Env -import Gundeck.Redis qualified as Redis import Imports import Network.AMQP import Network.HTTP.Types @@ -67,7 +61,6 @@ import System.Logger (Logger) import System.Logger qualified as Logger import System.Logger.Class qualified as Log import System.Timeout -import UnliftIO (async) -- | TODO: 'Client' already has an 'Env'. Why do we need two? How does this even work? We should -- probably explain this here. @@ -91,72 +84,6 @@ newtype Gundeck a = Gundeck instance MonadMonitor Gundeck where doIO = liftIO --- | 'Gundeck' doesn't have an instance for 'MonadRedis' because it contains two --- connections to two redis instances. When using 'WithDefaultRedis', any redis --- operation will only target the default redis instance (configured under --- 'redis:' in the gundeck config). To write to both redises use --- 'WithAdditionalRedis'. -newtype WithDefaultRedis a = WithDefaultRedis {runWithDefaultRedis :: Gundeck a} - deriving newtype - ( Functor, - Applicative, - Monad, - MonadIO, - MonadThrow, - MonadCatch, - MonadMask, - MonadReader Env, - MonadClient, - MonadUnliftIO, - Log.MonadLogger - ) - -instance Redis.MonadRedis WithDefaultRedis where - liftRedis action = do - defaultConn <- view rstate - Redis.runRobust defaultConn action - -instance Redis.RedisCtx WithDefaultRedis (Either Redis.Reply) where - returnDecode :: (Redis.RedisResult a) => Redis.Reply -> WithDefaultRedis (Either Redis.Reply a) - returnDecode = Redis.liftRedis . Redis.returnDecode - --- | 'Gundeck' doesn't have an instance for 'MonadRedis' because it contains two --- connections to two redis instances. When using 'WithAdditionalRedis', any --- redis operation will target both redis instances (configured under 'redis:' --- and 'redisAddtionalWrite:' in the gundeck config). To write to only the --- default redis use 'WithDefaultRedis'. -newtype WithAdditionalRedis a = WithAdditionalRedis {runWithAdditionalRedis :: Gundeck a} - deriving newtype - ( Functor, - Applicative, - Monad, - MonadIO, - MonadThrow, - MonadCatch, - MonadMask, - MonadReader Env, - MonadClient, - MonadUnliftIO, - Log.MonadLogger - ) - -instance Redis.MonadRedis WithAdditionalRedis where - liftRedis action = do - defaultConn <- view rstate - ret <- Redis.runRobust defaultConn action - - mAdditionalRedisConn <- view rstateAdditionalWrite - for_ mAdditionalRedisConn $ \additionalRedisConn -> - -- We just fire and forget this call, as there is not much we can do if - -- this fails. - async $ Redis.runRobust additionalRedisConn action - - pure ret - -instance Redis.RedisCtx WithAdditionalRedis (Either Redis.Reply) where - returnDecode :: (Redis.RedisResult a) => Redis.Reply -> WithAdditionalRedis (Either Redis.Reply a) - returnDecode = Redis.liftRedis . Redis.returnDecode - instance Log.MonadLogger Gundeck where log l m = do e <- ask diff --git a/services/gundeck/src/Gundeck/Options.hs b/services/gundeck/src/Gundeck/Options.hs index d70bbc4f91d..5222248da27 100644 --- a/services/gundeck/src/Gundeck/Options.hs +++ b/services/gundeck/src/Gundeck/Options.hs @@ -24,6 +24,7 @@ import Control.Lens hiding (Level) import Data.Aeson.TH import Data.Yaml (FromJSON) import Gundeck.Aws.Arn +import Hasql.Pool.Extended (PoolConfig) import Imports import Network.AMQP.Extended import System.Logger.Extended (Level, LogFormat) @@ -102,30 +103,6 @@ deriveFromJSON toOptionFieldName ''MaxConcurrentNativePushes makeLenses ''MaxConcurrentNativePushes -data RedisConnectionMode - = Master - | Cluster - deriving (Show, Generic) - -deriveJSON defaultOptions {constructorTagModifier = map toLower} ''RedisConnectionMode - -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) - -deriveFromJSON toOptionFieldName ''RedisEndpoint - -makeLenses ''RedisEndpoint - makeLenses ''Settings deriveFromJSON toOptionFieldName ''Settings @@ -135,8 +112,11 @@ data Opts = Opts _gundeck :: !Endpoint, _brig :: !Endpoint, _cassandra :: !CassandraOpts, - _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, _aws :: !AWSOpts, _rabbitmq :: !AmqpEndpoint, _discoUrl :: !(Maybe Text), diff --git a/services/gundeck/src/Gundeck/Presence.hs b/services/gundeck/src/Gundeck/Presence.hs index aa8fb778095..6c6b757ef59 100644 --- a/services/gundeck/src/Gundeck/Presence.hs +++ b/services/gundeck/src/Gundeck/Presence.hs @@ -33,10 +33,10 @@ import Wire.API.CannonId import Wire.API.Presence listH :: UserId -> Gundeck [Presence] -listH = runWithDefaultRedis . Data.list +listH = Data.list listAllH :: CommaSeparatedList UserId -> Gundeck [Presence] -listAllH uids = concat <$> runWithDefaultRedis (Data.listAll (fromCommaSeparatedList uids)) +listAllH uids = concat <$> Data.listAll (fromCommaSeparatedList uids) addH :: Presence -> Gundeck (Headers '[Header "Location" URI] NoContent) addH p = do diff --git a/services/gundeck/src/Gundeck/Presence/Data.hs b/services/gundeck/src/Gundeck/Presence/Data.hs index 6173ace303d..622dba9329b 100644 --- a/services/gundeck/src/Gundeck/Presence/Data.hs +++ b/services/gundeck/src/Gundeck/Presence/Data.hs @@ -20,128 +20,156 @@ module Gundeck.Presence.Data list, listAll, deleteAll, + cleanup, ) where -import Control.Monad.Catch -import Data.Aeson as Aeson -import Data.ByteString qualified as Strict -import Data.ByteString.Builder (byteString) -import Data.ByteString.Char8 qualified as StrictChars -import Data.ByteString.Conversion hiding (fromList) -import Data.ByteString.Lazy qualified as Lazy +import Control.Lens (view) +import Control.Monad.Catch (throwM) +import Data.ByteString.Conversion (fromByteString, toByteString') import Data.Id -import Data.List.NonEmpty qualified as NonEmpty -import Data.Misc (Milliseconds) -import Database.Redis -import Gundeck.Monad (Gundeck, posixTime, runWithAdditionalRedis) -import Gundeck.Util.Redis +import Data.Map.Strict qualified as Map +import Data.Misc (Milliseconds (..)) +import Data.Text (pack, unpack) +import Data.Text.Encoding (decodeUtf8, encodeUtf8) +import Data.Time (UTCTime) +import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds) +import Data.UUID (UUID) +import Data.Vector qualified as Vector +import Gundeck.Env (hasqlPool) +import Gundeck.Monad +import Hasql.Session (Session, statement) +import Hasql.Statement (Statement) +import Hasql.TH import Imports -import System.Logger.Class (MonadLogger) +import System.Logger.Class qualified as Log import Wire.API.Presence +import Wire.Postgres qualified as Postgres --- Note [Migration] --------------------------------------------------------- --- --- Previous redis schema: user:=@= --- New redis schema: user:= = --- --- The previous redis schema encodes cannon's ID in the subkey. The migration --- proceeds as follows: --- --- 1. When adding new entries, we only use the connection as subkey. --- 2. When listing entries (which does not use the subkey fortunately) we --- store the original field name in the `Presence` record property `__field`. --- 3. When deleting entries, we use this `Presence`'s `__field` value. --- 4. Eventually `__field` can be removed from the `Presence` type and the --- connection can be used directly instead. --- - +-- | Register (or refresh) a presence. The server-side timestamp is stamped +-- here, the 'Presence'\'s own 'createdAt' value is ignored (as in the redis +-- implementation before). add :: Presence -> Gundeck () add p = do - now <- posixTime - let k = toKey (userId p) - let v = toField (connId p) - let d = Lazy.toStrict $ Aeson.encode $ PresenceData p.resource p.clientId now - runWithAdditionalRedis . retry x3 $ do - void . fromTxResult <=< (liftRedis . multiExec) $ do - void $ hset k (NonEmpty.singleton (v, d)) - -- nb. All presences of a user are expired 'maxIdleTime' after the - -- last presence was registered. A client who keeps a presence - -- (i.e. websocket) connected for longer than 'maxIdleTime' will be - -- silently dropped and receives no more notifications. - expire k maxIdleTime - where - maxIdleTime = 7 * 24 * 60 * 60 -- 7 days in seconds - -deleteAll :: (MonadMask m, MonadIO m, RedisCtx m (Either Reply), MonadLogger m) => [Presence] -> m () -deleteAll [] = pure () -deleteAll pp = for_ pp $ \p -> do - let k = toKey (userId p) - let f = Lazy.toStrict $ __field p - void . retry x3 $ do - void . liftRedis $ watch (pure k) - value <- either (throwM . RedisSimpleError) id <$> hget k f - void . liftRedis . multiExec $ do - case value of - Nothing -> pure $ pure () - Just v -> do - let p' = readPresence (userId p) (f, v) - if Just p == p' - then void <$> hdel k (pure f) - else pure $ pure () - -list :: (MonadRedis m, MonadThrow m) => UserId -> m [Presence] -list u = do - ePresenses <- liftRedis $ list' u - case ePresenses of - Left r -> throwM $ RedisSimpleError r - Right ps -> pure ps - -list' :: (RedisCtx m f, Functor f) => UserId -> m (f [Presence]) -list' u = mapMaybe (readPresence u) <$$> hgetall (toKey u) - --- FUTUREWORK: Make this not fail if it fails only for a few users. -listAll :: (MonadRedis m, MonadThrow m) => [UserId] -> m [[Presence]] + nowMs <- posixTime + runPool $ + statement + (toUUID (userId p), connIdText (connId p), uriText (resource p), clientToText <$> clientId p, msToUtc (fromIntegral (ms nowMs))) + upsertPresence + +-- | Read all presences of a single user. +list :: UserId -> Gundeck [Presence] +list u = fromMaybe [] . listToMaybe <$> listAll [u] + +-- | Read all presences of the given users, one list per user (input order, +-- empty list for users without presences). Single round trip. +listAll :: [UserId] -> Gundeck [[Presence]] listAll [] = pure [] -listAll uu = mapM list uu +listAll uu = do + rows <- runPool $ statement (Vector.fromList (toUUID <$> uu)) selectByUsers + presencesByUser <- + foldM + ( \acc (u, c, r, cl, t) -> case readPresenceRow u c r cl t of + Just p -> pure $! Map.insertWith (<>) (userId p) [p] acc + Nothing -> do + Log.warn $ + Log.msg (Log.val "ignoring unreadable presence row") + . Log.field "user_id" (show u) + . Log.field "conn_id" (show c) + pure acc + ) + Map.empty + (Vector.toList rows) + pure [Map.findWithDefault [] u presencesByUser | u <- uu] + +-- | Compare-and-delete: only delete the stored presence if it is not newer +-- than the given one (a newer re-registration with the same conn id must not +-- be deleted by a stale disconnect). +deleteAll :: [Presence] -> Gundeck () +deleteAll [] = pure () +deleteAll pp = + runPool . statement params $ deleteMany + where + params = + ( Vector.fromList (toUUID . userId <$> pp), + Vector.fromList (connIdText . connId <$> pp), + Vector.fromList (msToUtc . fromIntegral . ms . createdAt <$> pp) + ) + +-- | Delete presences older than a week. Normal disconnects delete their +-- presence rows; this only guards against leaks from abnormally dead pods +-- (replaces the redis key TTL). +cleanup :: Gundeck () +cleanup = runPool $ statement () deleteStale -- Helpers ------------------------------------------------------------------- -data PresenceData = PresenceData !URI !(Maybe ClientId) !Milliseconds - deriving (Eq) - -instance ToJSON PresenceData where - toJSON (PresenceData r c t) = - object - [ "r" .= r, - "c" .= c, - "t" .= t - ] - -instance FromJSON PresenceData where - parseJSON = withObject "PresenceData" $ \o -> - PresenceData - <$> o - .: "r" - <*> o - .:? "c" - <*> o - .:? "t" - .!= 0 - -toKey :: UserId -> ByteString -toKey u = Lazy.toStrict $ runBuilder (byteString "user:" <> builder u) - -toField :: ConnId -> ByteString -toField (ConnId con) = con - -fromField :: ByteString -> ConnId -fromField = ConnId . StrictChars.takeWhile (/= '@') - -readPresence :: UserId -> (ByteString, ByteString) -> Maybe Presence -readPresence u (f, b) = do - PresenceData uri clt tme <- - if "http" `Strict.isPrefixOf` b - then PresenceData <$> fromByteString b <*> pure Nothing <*> pure 0 - else decodeStrict' b - pure (Presence u (fromField f) uri clt tme (Lazy.fromStrict f)) +-- | Millis <-> UTC. Exact (milliseconds nest inside timestamptz's microseconds); +-- do NOT reuse 'Gundeck.Monad.msToUTCSecs', it truncates to whole seconds. +msToUtc :: Int64 -> UTCTime +msToUtc p = posixSecondsToUTCTime (fromRational (fromIntegral p / 1000 :: Rational)) + +utcToMs :: UTCTime -> Int64 +utcToMs = floor . (* 1000) . utcTimeToPOSIXSeconds + +newtype PresenceDbError = PresenceDbError Text deriving (Show) + +instance Exception PresenceDbError + +runPool :: Session a -> Gundeck a +runPool sess = do + pool <- view hasqlPool + liftIO (Postgres.useWithResetAndRetry pool sess) >>= either (throwM . PresenceDbError . pack . show) pure + +connIdText :: ConnId -> Text +connIdText = decodeUtf8 . fromConnId + +uriText :: URI -> Text +uriText = decodeUtf8 . toByteString' + +readPresenceRow :: UUID -> Text -> Text -> Maybe Text -> UTCTime -> Maybe Presence +readPresenceRow u c r cl t = do + uri <- parse (unpack r) + cid <- traverse parseClient cl + pure (Presence (Id u) (ConnId (encodeUtf8 c)) uri cid (Ms (fromIntegral (utcToMs t)))) + where + parseClient = fromByteString . encodeUtf8 + +upsertPresence :: Statement (UUID, Text, Text, Maybe Text, UTCTime) () +upsertPresence = + [resultlessStatement| + INSERT INTO presence (user_id, conn_id, resource, client_id, created_at) + VALUES ($1 :: uuid, $2 :: text, $3 :: text, $4 :: text?, $5 :: timestamptz) + ON CONFLICT (user_id, conn_id) DO UPDATE + SET resource = EXCLUDED.resource, + client_id = EXCLUDED.client_id, + created_at = EXCLUDED.created_at + |] + +selectByUsers :: Statement (Vector.Vector UUID) (Vector.Vector (UUID, Text, Text, Maybe Text, UTCTime)) +selectByUsers = + [vectorStatement| + SELECT user_id :: uuid, conn_id :: text, resource :: text, client_id :: text?, created_at :: timestamptz + FROM presence + WHERE user_id = ANY ($1 :: uuid[]) + |] + +-- | Compare-and-delete, in one round trip: only delete each stored presence +-- if it is not newer than the given one (a newer re-registration with the +-- same conn id must not be deleted by a stale disconnect). +deleteMany :: Statement (Vector.Vector UUID, Vector.Vector Text, Vector.Vector UTCTime) () +deleteMany = + [resultlessStatement| + DELETE FROM presence p + USING unnest($1 :: uuid[], $2 :: text[], $3 :: timestamptz[]) AS d (user_id, conn_id, created_at) + WHERE p.user_id = d.user_id + AND p.conn_id = d.conn_id + AND p.created_at <= d.created_at + |] + +deleteStale :: Statement () () +deleteStale = + [resultlessStatement| + DELETE FROM presence + WHERE created_at < now() - interval '7 days' + |] diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index a6cdf759062..77149b6efef 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -122,7 +122,7 @@ instance MonadPushAll Gundeck where mpaNotificationTTL = view (options . settings . notificationTTL) mpaCellsEventQueue = view (options . settings . cellsEventQueue) mpaMkNotificationId = mkNotificationId - mpaListAllPresences = runWithDefaultRedis . Presence.listAll + mpaListAllPresences = Presence.listAll mpaBulkPush = Web.bulkPush mpaStreamAdd = Data.add mpaPushNative = pushNative diff --git a/services/gundeck/src/Gundeck/Push/Websocket.hs b/services/gundeck/src/Gundeck/Push/Websocket.hs index 562bcb10730..721dadd8eaf 100644 --- a/services/gundeck/src/Gundeck/Push/Websocket.hs +++ b/services/gundeck/src/Gundeck/Push/Websocket.hs @@ -64,7 +64,7 @@ class (Monad m, MonadThrow m, Log.MonadLogger m) => MonadBulkPush m where instance MonadBulkPush Gundeck where mbpBulkSend = bulkSend - mbpDeleteAllPresences = runWithAdditionalRedis . Presence.deleteAll + mbpDeleteAllPresences = Presence.deleteAll mbpPosixTime = posixTime mbpMapConcurrently = mapConcurrently mbpMonitorBadCannons = monitorBadCannons @@ -315,7 +315,7 @@ push :: push notif (toList -> tgts) originUser originConn conns = do pp <- handleAny noPresences listPresences (ok, gone) <- foldM onResult ([], []) =<< send notif pp - runWithAdditionalRedis $ Presence.deleteAll gone + Presence.deleteAll gone pure ok where listPresences = @@ -324,7 +324,7 @@ push notif (toList -> tgts) originUser originConn conns = do . concat . filterByClient . zip tgts - <$> runWithDefaultRedis (Presence.listAll (view targetUser <$> tgts)) + <$> Presence.listAll (view targetUser <$> tgts) noPresences exn = do Log.err $ Log.field "error" (displayException exn) diff --git a/services/gundeck/src/Gundeck/Redis.hs b/services/gundeck/src/Gundeck/Redis.hs deleted file mode 100644 index e9bf1affafe..00000000000 --- a/services/gundeck/src/Gundeck/Redis.hs +++ /dev/null @@ -1,127 +0,0 @@ -{-# LANGUAGE NumDecimals #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Gundeck.Redis - ( RobustConnection, - connectRobust, - runRobust, - PingException, - ) -where - -import Control.Concurrent.Async (Async, async) -import Control.Monad.Catch qualified as Catch -import Control.Retry -import Database.Redis -import Database.Redis.Connection (ClusterDownError) -import Imports -import System.Logger qualified as Log -import System.Logger.Class (MonadLogger) -import System.Logger.Class qualified as LogClass -import System.Logger.Extended -import UnliftIO.Exception - --- | Connection to Redis which allows reconnecting. -type RobustConnection = MVar Connection - --- | Connection to Redis which can be reestablished on connection errors. --- --- Reconnecting even when Redis IPs change as long as the DNS name remains --- constant. The server type (cluster or not) and the connection information of --- the initial connection are used when reconnecting. --- --- Throws 'ConnectError', 'ConnectTimeout', 'ConnectionLostException', --- 'PingException', or 'IOException' if retry policy is finite. -connectRobust :: - Logger -> - -- | e. g., @exponentialBackoff 50000@ - RetryPolicy -> - -- | action returning a fresh initial 'Connection', e. g., @(checkedConnect connInfo)@ or @(checkedConnectCluster connInfo)@ - IO Connection -> - IO (Async (), RobustConnection) -connectRobust l retryStrategy connectLowLevel = do - robustConnection <- newEmptyMVar @IO @Connection - thread <- - async $ safeForever l $ do - Log.info l $ Log.msg (Log.val "connecting to Redis") - conn <- retry connectLowLevel - Log.info l $ Log.msg (Log.val "successfully connected to Redis") - putMVar robustConnection conn - catch - ( forever $ do - _ <- runRedis conn ping - threadDelay 1e6 - ) - $ \(_ :: SomeException) -> void $ takeMVar robustConnection - pure (thread, robustConnection) - where - retry = - recovering -- retry connecting, e. g., with exponential back-off - retryStrategy - [ const $ Catch.Handler (\(e :: ClusterDownError) -> logEx (Log.err l) e "Redis cluster down" >> pure True), - const $ Catch.Handler (\(e :: ConnectError) -> logEx (Log.err l) e "Redis not in cluster mode" >> pure True), - const $ Catch.Handler (\(e :: ConnectTimeout) -> logEx (Log.err l) e "timeout when connecting to Redis" >> pure True), - const $ Catch.Handler (\(e :: ConnectionLostException) -> logEx (Log.err l) e "Redis connection lost during request" >> pure True), - const $ Catch.Handler (\(e :: PingException) -> logEx (Log.err l) e "pinging Redis failed" >> pure True), - const $ Catch.Handler (\(e :: IOException) -> logEx (Log.err l) e "network error when connecting to Redis" >> pure True) - ] - . const -- ignore RetryStatus - logEx :: (Exception e) => ((Msg -> Msg) -> IO ()) -> e -> ByteString -> IO () - logEx lLevel e description = lLevel $ Log.msg (Log.val description) . Log.field "error" (displayException e) - --- | Run a 'Redis' action through a 'RobustConnection'. --- --- Blocks on connection errors as long as the connection is not reestablished. --- Without externally enforcing timeouts, this may lead to leaking threads. -runRobust :: (MonadUnliftIO m, MonadLogger m, Catch.MonadMask m) => RobustConnection -> Redis a -> m a -runRobust mvar action = retry $ do - robustConnection <- readMVar mvar - liftIO $ runRedis robustConnection action - where - retryStrategy = capDelay 1000000 (exponentialBackoff 50000) - retry = - recovering -- retry connecting, e. g., with exponential back-off - retryStrategy - [ logAndHandle $ Catch.Handler (\(_ :: ConnectionLostException) -> pure True), - logAndHandle $ Catch.Handler (\(_ :: IOException) -> pure True) - ] - . const -- ignore RetryStatus - logAndHandle (Handler handler) _ = - Handler $ \e -> do - LogClass.err $ Log.msg (Log.val "Redis connection failed") . Log.field "error" (displayException e) - handler e - -data PingException = PingException Reply deriving (Show) - -instance Exception PingException - -safeForever :: - forall m. - (MonadUnliftIO m) => - Logger -> - m () -> - m () -safeForever l action = - forever $ - action `catchAny` \e -> do - Log.err l $ Log.msg (Log.val "Uncaught exception while connecting to redis") . Log.field "error" (displayException e) - threadDelay 1e6 -- pause to keep worst-case noise in logs manageable diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index 89e4c9f8ef2..6f4b388d643 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -42,23 +42,25 @@ import Cassandra.Schema (versionCheck) import Control.Error (ExceptT (ExceptT)) import Control.Exception (finally) import Control.Lens ((.~), (^.)) +import Control.Monad.Catch (catchAll) import Control.Monad.Extra import Data.Map qualified as Map import Data.Metrics.AWS (gaugeTokenRemaing) import Data.Metrics.Servant qualified as Metrics import Data.Proxy (Proxy (Proxy)) import Data.Text (unpack) -import Database.Redis qualified as Redis import Gundeck.API.Internal as Internal (InternalAPI, servantSitemap) import Gundeck.API.Public as Public (servantSitemap) import Gundeck.Aws qualified as Aws import Gundeck.Env import Gundeck.Env qualified as Env import Gundeck.Monad -import Gundeck.Options hiding (host, port) +import Gundeck.Options +import Gundeck.Presence.Data qualified as PresenceData import Gundeck.React import Gundeck.Schema.Run (lastSchemaVersion) import Gundeck.ThreadBudget +import Hasql.Pool.Extended (Pool (rawPool)) import Imports import Network.AMQP import Network.AMQP.Types @@ -81,11 +83,13 @@ import Wire.API.Routes.Public.Gundeck (GundeckAPI) import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai import Wire.OpenTelemetry +import Wire.PostgresMigrations qualified as PostgresMigrations run :: Opts -> IO () run opts = withTracer \tracer -> do - (rThreads, env) <- createEnv opts + env <- createEnv opts let logger = env ^. applog + PostgresMigrations.runAllMigrations (env ^. hasqlPool).rawPool logger runDirect env setUpRabbitMqExchangesAndQueues @@ -93,10 +97,10 @@ run opts = withTracer \tracer -> do versionCheck lastSchemaVersion let s = newSettings $ defaultServer (unpack . host $ opts ^. gundeck) (port $ opts ^. gundeck) logger let throttleMillis = fromMaybe defSqsThrottleMillis $ opts ^. (settings . sqsThrottleMillis) - lst <- Async.async $ Aws.execute (env ^. awsEnv) (Aws.listen throttleMillis (runDirect env . onEvent)) wtbs <- forM (env ^. threadBudgetState) $ \tbs -> Async.async $ runDirect env $ watchThreadBudgetState tbs 10 wCollectAuth <- Async.async (collectAuthMetrics (Aws._awsEnv (Env._awsEnv env))) + pcleanup <- Async.async $ runDirect env $ cleanupPresenceLoop logger app <- middleware env <*> pure (mkApp env) inSpan tracer "gundeck" defaultSpanArguments {kind = Otel.Server} (runSettingsWithShutdown s app Nothing) `finally` do @@ -104,10 +108,8 @@ run opts = withTracer \tracer -> do shutdown (env ^. cstate) Async.cancel lst Async.cancel wCollectAuth + Async.cancel pcleanup forM_ wtbs Async.cancel - forM_ rThreads Async.cancel - Redis.disconnect =<< takeMVar (env ^. rstate) - whenJust (env ^. rstateAdditionalWrite) $ (=<<) Redis.disconnect . takeMVar Log.close (env ^. applog) where setUpRabbitMqExchangesAndQueues :: Gundeck () @@ -178,3 +180,19 @@ collectAuthMetrics env = do mbRemaining <- readAuthExpiration env gaugeTokenRemaing mbRemaining threadDelay 1_000_000 + +-- | Hourly janitor replacing the redis key TTL: deletes presence rows older +-- than a week (leak guard for abnormally dead pods). Never let a transient DB +-- error kill the thread — log and retry next hour. +cleanupPresenceLoop :: Log.Logger -> Gundeck () +cleanupPresenceLoop logger = + forever $ + (PresenceData.cleanup >> threadDelay cleanupInterval) + `catchAll` \e -> do + liftIO . Log.err logger $ + Log.msg (Log.val "presence cleanup failed") + . Log.field "error" (displayException (e :: SomeException)) + threadDelay cleanupInterval + +cleanupInterval :: Int +cleanupInterval = 3_600_000_000 -- one hour, in microseconds diff --git a/services/gundeck/src/Gundeck/Util/Redis.hs b/services/gundeck/src/Gundeck/Util/Redis.hs deleted file mode 100644 index d125d04baca..00000000000 --- a/services/gundeck/src/Gundeck/Util/Redis.hs +++ /dev/null @@ -1,61 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Gundeck.Util.Redis where - -import Control.Monad.Catch -import Control.Retry -import Data.ByteString qualified as BS -import Database.Redis -import Imports -import System.Logger.Class (MonadLogger) -import System.Logger.Class qualified as Log -import System.Logger.Message - -retry :: (MonadIO m, MonadMask m, MonadLogger m) => RetryPolicyM m -> m a -> m a -retry x = recovering x handlers . const - -x3 :: RetryPolicy -x3 = limitRetries 3 <> exponentialBackoff 100000 - -handlers :: (MonadLogger m) => [a -> Handler m Bool] -handlers = - [ const . Handler $ \case - RedisSimpleError (Error err) -> pure $ "READONLY" `BS.isPrefixOf` err - RedisTxError err -> pure $ "READONLY" `isPrefixOf` err - err -> do - Log.warn $ - Log.msg (Log.val "Redis error; not retrying.") - ~~ "redis.errMsg" .= show err - pure False - ] - --- Error ------------------------------------------------------------------- - -data RedisError - = RedisSimpleError Reply - | RedisTxAborted - | RedisTxError String - deriving (Show) - -instance Exception RedisError - -fromTxResult :: (MonadThrow m) => TxResult a -> m a -fromTxResult = \case - TxSuccess a -> pure a - TxAborted -> throwM RedisTxAborted - TxError e -> throwM $ RedisTxError e diff --git a/services/gundeck/test/integration/API.hs b/services/gundeck/test/integration/API.hs index 27de5b8602d..6346d07a438 100644 --- a/services/gundeck/test/integration/API.hs +++ b/services/gundeck/test/integration/API.hs @@ -28,7 +28,6 @@ import Bilge hiding (head) import Bilge.Assert import Control.Arrow ((&&&)) import Control.Concurrent.Async (Async, async, concurrently_, wait) -import Control.Concurrent.Async qualified as Async import Control.Lens (view, (%~), (.~), (?~), (^.), (^?), _2) import Control.Retry (constantDelay, limitRetries, recoverAll, retrying) import Data.Aeson @@ -47,8 +46,6 @@ import Data.Set qualified as Set import Data.Text.Encoding qualified as T import Data.UUID qualified as UUID import Data.UUID.V4 -import Gundeck.Options -import Gundeck.Options qualified as O import Imports import Network.HTTP.Client qualified as Http import Network.URI (parseURI) @@ -59,7 +56,6 @@ import System.Timeout (timeout) import Test.Tasty import Test.Tasty.HUnit import TestSetup -import Util (runRedisProxy, withEnvOverrides, withSettingsOverrides) import Wire.API.Event.Gundeck import Wire.API.Internal.Notification import Wire.API.Presence @@ -76,8 +72,7 @@ tests s = test s "Remove stale presence" removeStalePresence, test s "Single user push" singleUserPush, test s "Single user push with large message" singleUserPushLargeMessage, - test s "Send a push, ensure origin does not receive it" sendSingleUserNoPiggyback, - test s "Store notifications even when redis is down" storeNotificationsEvenWhenRedisIsDown + test s "Send a push, ensure origin does not receive it" sendSingleUserNoPiggyback ], testGroup "Notifications" @@ -108,10 +103,6 @@ tests s = test s "control pings with payload produce pongs with the same payload" testControlPingPongWithData, test s "data non-pings are ignored" testNoPingNoPong ], - testGroup - "Redis migration" - [ test s "redis migration should work" testRedisMigration - ], -- TODO: The following tests require (at the moment), the usage real AWS -- services so they are kept in a separate group to simplify testing testGroup @@ -135,8 +126,8 @@ replacePresence = do con <- randomConnId let localhost8080 = URI . fromJust $ parseURI "http://localhost:8080" let localhost8081 = URI . fromJust $ parseURI "http://localhost:8081" - let pres1 = Presence uid (ConnId "dummy_dev") localhost8080 Nothing 0 "" - let pres2 = Presence uid (ConnId "dummy_dev") localhost8081 Nothing 0 "" + let pres1 = Presence uid (ConnId "dummy_dev") localhost8080 Nothing 0 + let pres2 = Presence uid (ConnId "dummy_dev") localhost8081 Nothing 0 void $ connectUser ca uid con setPresence gu pres1 !!! const 201 === statusCode sendPush (push uid [uid]) @@ -269,28 +260,6 @@ sendMultipleUsers = do pevent = KeyMap.fromList ["foo" .= (42 :: Int)] push u us = newPush (Just u) (toRecipients us) pload & pushOriginConnection ?~ ConnId "dev" -storeNotificationsEvenWhenRedisIsDown :: TestM () -storeNotificationsEvenWhenRedisIsDown = do - ally <- randomId - origRedisEndpoint <- view $ tsOpts . redis - let proxyPort = 10112 - redisProxyServer <- liftIO . async $ runRedisProxy (origRedisEndpoint ^. O.host) (origRedisEndpoint ^. O.port) proxyPort - withSettingsOverrides - ( \gundeckSettings -> - gundeckSettings - & redis . Gundeck.Options.host .~ "localhost" - & redis . Gundeck.Options.port .~ proxyPort - ) - $ do - let pload = textPayload "hello" - push = buildPush ally [(ally, RecipientClientsAll)] pload - gu <- view tsGundeck - liftIO $ Async.cancel redisProxyServer - post (runGundeckR gu . path "i/push/v2" . json [push]) !!! const 200 === statusCode - - ns <- listNotifications ally Nothing - liftIO $ assertEqual ("Expected 1 notification, got: " <> show ns) 1 (length ns) - ----------------------------------------------------------------------------- -- Notifications @@ -729,36 +698,6 @@ testLongPushToken = do tkn4 <- randomToken clt gcmToken {tSize = 5000} registerPushTokenRequest uid tkn4 !!! const 413 === statusCode --- * Redis Migration - -testRedisMigration :: TestM () -testRedisMigration = do - uid <- randomUser - con <- randomConnId - cannonURI <- Wire.API.Presence.parse "http://cannon.example" - let presence = Presence uid con cannonURI Nothing 1 "" - redis2 <- view tsRedis2 - - withSettingsOverrides (redisAdditionalWrite ?~ redis2) $ do - g <- view tsGundeck - setPresence g presence - !!! const 201 - === statusCode - retrievedPresence <- - map resource . decodePresence <$> (getPresence g (toByteString' uid) lookupEnv "REDIS_ADDITIONAL_WRITE_USERNAME" - password <- ("REDIS_PASSWORD",) <$$> lookupEnv "REDIS_ADDITIONAL_WRITE_PASSWORD" - pure $ catMaybes [username, password] - - withEnvOverrides redis2CredsAsRedis1Creds $ withSettingsOverrides (redis .~ redis2) $ do - g <- view tsGundeck - retrievedPresence <- - map resource . decodePresence <$> (getPresence g (toByteString' uid) UserId -> Int -> TestM () diff --git a/services/gundeck/test/integration/Main.hs b/services/gundeck/test/integration/Main.hs index 767f28a4ae4..05a385e40b4 100644 --- a/services/gundeck/test/integration/Main.hs +++ b/services/gundeck/test/integration/Main.hs @@ -30,7 +30,7 @@ import Data.Proxy import Data.Tagged import Data.Text.Encoding (encodeUtf8) import Data.Yaml (decodeFileEither) -import Gundeck.Options hiding (host, port) +import Gundeck.Options import Imports hiding (local) import Metrics qualified import Network.HTTP.Client (responseTimeoutMicro) @@ -52,8 +52,7 @@ data IntegrationConfig = IntegrationConfig { gundeck :: Endpoint, cannon :: Endpoint, cannon2 :: Endpoint, - brig :: Endpoint, - redis2 :: RedisEndpoint + brig :: Endpoint } deriving (Show, Generic) @@ -114,6 +113,6 @@ main = withOpenSSL $ runTests go b = BrigR $ mkRequest iConf.brig lg <- Logger.new Logger.defSettings db <- defInitCassandra (gConf ^. cassandra) lg - pure $ TestSetup m g c c2 b db lg gConf (redis2 iConf) - releaseOpts _ = pure () + pure $ TestSetup m g c c2 b db lg mkRequest (Endpoint h p) = Bilge.host (encodeUtf8 h) . Bilge.port p + releaseOpts _ = pure () diff --git a/services/gundeck/test/integration/TestSetup.hs b/services/gundeck/test/integration/TestSetup.hs index ea49d1b3222..70e8cd77dca 100644 --- a/services/gundeck/test/integration/TestSetup.hs +++ b/services/gundeck/test/integration/TestSetup.hs @@ -28,8 +28,6 @@ module TestSetup tsBrig, tsCass, tsLogger, - tsOpts, - tsRedis2, TestM (..), TestSetup (..), BrigR (..), @@ -42,8 +40,6 @@ import Bilge (HttpT (..), Manager, MonadHttp, Request, runHttpT) import Cassandra qualified as Cql import Control.Lens (makeLenses, (^.)) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow) -import Gundeck.Options (RedisEndpoint) -import Gundeck.Options qualified as Gundeck import Imports import System.Logger qualified as Log import Test.Tasty (TestName, TestTree) @@ -79,9 +75,7 @@ data TestSetup = TestSetup _tsCannon2 :: CannonR, _tsBrig :: BrigR, _tsCass :: Cql.ClientState, - _tsLogger :: Log.Logger, - _tsOpts :: Gundeck.Opts, - _tsRedis2 :: RedisEndpoint + _tsLogger :: Log.Logger } makeLenses ''TestSetup diff --git a/services/gundeck/test/integration/Util.hs b/services/gundeck/test/integration/Util.hs deleted file mode 100644 index d6790424b2f..00000000000 --- a/services/gundeck/test/integration/Util.hs +++ /dev/null @@ -1,119 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2025 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Util where - -import Bilge qualified -import Control.Concurrent (forkFinally) -import Control.Concurrent.Async (race_) -import Control.Exception qualified as E -import Control.Lens -import Control.Monad.Catch -import Control.Monad.Codensity -import Data.ByteString qualified as S -import Data.Text qualified as Text -import Gundeck.Env (createEnv) -import Gundeck.Options -import Gundeck.Run (mkApp) -import Imports -import Network.Socket hiding (openSocket) -import Network.Socket.ByteString (recv, sendAll) -import Network.Wai.Utilities.MockServer (withMockServer) -import TestSetup - -withSettingsOverrides :: (Opts -> Opts) -> TestM a -> TestM a -withSettingsOverrides f action = do - ts <- ask - let opts = f (view tsOpts ts) - (_rThreads, env) <- liftIO $ createEnv opts - liftIO . lowerCodensity $ do - let app = mkApp env - p <- withMockServer app - liftIO $ Bilge.runHttpT (ts ^. tsManager) $ runReaderT (runTestM action) $ ts & tsGundeck .~ GundeckR (mkRequest p) - where - mkRequest p = Bilge.host "127.0.0.1" . Bilge.port p - -withEnvOverrides :: forall m a. (MonadIO m, MonadMask m) => [(String, String)] -> m a -> m a -withEnvOverrides envOverrides action = do - bracket (setEnvVars envOverrides) (resetEnvVars) $ const action - where - setEnvVars :: [(String, String)] -> m [(String, Maybe String)] - setEnvVars newVars = liftIO $ do - oldVars <- mapM (\(k, _) -> (k,) <$> lookupEnv k) newVars - mapM_ (uncurry setEnv) newVars - pure oldVars - - resetEnvVars :: [(String, Maybe String)] -> m () - resetEnvVars = - mapM_ (\(k, mV) -> maybe (unsetEnv k) (setEnv k) mV) - -runRedisProxy :: Text -> Word16 -> Word16 -> IO () -runRedisProxy redisHost redisPort proxyPort = do - (servAddr : _) <- getAddrInfo Nothing (Just $ Text.unpack redisHost) (Just $ show redisPort) - runTCPServer Nothing (show proxyPort) $ \client -> do - server <- getServerSocket servAddr - client <~~> server - where - getServerSocket servAddr = do - server <- socket (addrFamily servAddr) Stream defaultProtocol - connect server (addrAddress servAddr) >> pure server - p1 <~~> p2 = finally (race_ (p1 `mapData` p2) (p2 `mapData` p1)) (close p1 >> close p2) - mapData f t = do - content <- recv f 4096 - unless (S.null content) $ sendAll t content >> mapData f t - --- Forked from network-run, added logic to cleanup clients when server is closed - --- | Running a TCP server with an accepted socket and its peer name. -runTCPServer :: Maybe HostName -> ServiceName -> (Socket -> IO a) -> IO b -runTCPServer mhost port' server = withSocketsDo $ do - addr <- resolve Stream mhost port' True - clientThreads <- newTVarIO [] - E.bracket (open addr) (cleanupClients clientThreads) (loop clientThreads) - where - open addr = E.bracketOnError (openServerSocket addr) close $ \sock -> do - listen sock 1024 - pure sock - loop clientThreads sock = forever $ do - E.bracketOnError (accept sock) (close . fst) $ - \(conn, _peer) -> do - thread <- forkFinally (server conn) (const $ gracefulClose conn 5000) - atomically $ modifyTVar clientThreads (thread :) - cleanupClients :: TVar [ThreadId] -> Socket -> IO () - cleanupClients clientThreads sock = do - close sock - mapM_ killThread =<< readTVarIO clientThreads - -resolve :: SocketType -> Maybe HostName -> ServiceName -> Bool -> IO AddrInfo -resolve socketType mhost port' passive = - head <$> getAddrInfo (Just hints) mhost (Just port') - where - hints = - defaultHints - { addrSocketType = socketType, - addrFlags = [AI_PASSIVE | passive] - } - -openServerSocket :: AddrInfo -> IO Socket -openServerSocket addr = E.bracketOnError (openSocket addr) close $ \sock -> do - setSocketOption sock ReuseAddr 1 - withFdSocket sock $ setCloseOnExecIfNeeded - bind sock $ addrAddress addr - pure sock - -openSocket :: AddrInfo -> IO Socket -openSocket addr = socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr) diff --git a/services/gundeck/test/unit/MockGundeck.hs b/services/gundeck/test/unit/MockGundeck.hs index 6e4f27df53d..647e30f376d 100644 --- a/services/gundeck/test/unit/MockGundeck.hs +++ b/services/gundeck/test/unit/MockGundeck.hs @@ -770,7 +770,6 @@ fakePresence userId clientId_ = Presence {..} connId = fakeConnId clientId_ resource = URI . fromJust $ URI.parseURI "http://127.0.0.1:8080" createdAt = 0 - __field = mempty -- | See also: 'fakePresence'. fakeConnId :: ClientId -> ConnId diff --git a/services/integration.yaml b/services/integration.yaml index 2da7e194e1f..acb0e595b23 100644 --- a/services/integration.yaml +++ b/services/integration.yaml @@ -128,12 +128,6 @@ backendTwo: originDomain: b.example.com -redis2: - host: 127.0.0.1 - port: 6379 - connectionMode: master - enableTls: false - insecureSkipVerifyTls: false dynamicBackends: dynamic-backend-1: From 990b4aad85cba61e0928e311c3ddff1aab218f39 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 4 Sep 2026 12:19:48 +0200 Subject: [PATCH 11/29] WPB-23427 Adjust uncommon request flow (#5517) --- changelog.d/3-bug-fixes/WPB-23427 | 1 + integration/test/Test/MLS.hs | 164 +++++++----------- .../ConversationSubsystem/MLS/Proposal.hs | 12 +- 3 files changed, 78 insertions(+), 99 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-23427 diff --git a/changelog.d/3-bug-fixes/WPB-23427 b/changelog.d/3-bug-fixes/WPB-23427 new file mode 100644 index 00000000000..2a20346bd27 --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-23427 @@ -0,0 +1 @@ +MLS message validation has been hardened. diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index ca8dc088b64..d31754f561e 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -726,82 +726,19 @@ testStaleCommit = do resp.status `shouldMatchInt` 409 resp.json %. "label" `shouldMatch` "mls-stale-message" -testPropInvalidEpoch :: (HasCallStack) => App () -testPropInvalidEpoch = do - users@[_alice, bob, charlie, dee] <- createAndConnectUsers (replicate 4 OwnDomain) - [alice1, bob1, charlie1, dee1] <- traverse (createMLSClient def) users +testBareProposalRejected :: (HasCallStack) => App () +testBareProposalRejected = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] convId <- createNewGroup def alice1 - - -- Add bob -> epoch 1 - void $ uploadNewKeyPackage def bob1 - gsBackup <- getClientGroupState alice1 - void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle - gsBackup2 <- getClientGroupState alice1 - - -- try to send a proposal from an old epoch (0) - do - setClientGroupState alice1 gsBackup - void $ uploadNewKeyPackage def dee1 - [prop] <- createAddProposals convId alice1 [dee] - bindResponse (postMLSMessage alice1 prop.message) $ \resp -> do - resp.status `shouldMatchInt` 409 - resp.json %. "label" `shouldMatch` "mls-stale-message" - - -- try to send a proposal from a newer epoch (2) - do - void $ uploadNewKeyPackage def dee1 - void $ uploadNewKeyPackage def charlie1 - setClientGroupState alice1 gsBackup2 - void $ createAddCommit alice1 convId [charlie] -- --> epoch 2 - [prop] <- createAddProposals convId alice1 [dee] - bindResponse (postMLSMessage alice1 prop.message) $ \resp -> do - resp.status `shouldMatchInt` 409 - resp.json %. "label" `shouldMatch` "mls-stale-message" - -- remove charlie from users expected to get a welcome message - modifyMLSState $ \mls -> mls {convs = Map.adjust (\conv -> conv {newMembers = mempty}) convId mls.convs} - - -- alice send a well-formed proposal and commits it - void $ uploadNewKeyPackage def dee1 - setClientGroupState alice1 gsBackup2 - createAddProposals convId alice1 [dee] >>= traverse_ sendAndConsumeMessage - void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundle - ---- | This test submits a ReInit proposal, which is currently ignored by the --- backend, in order to check that unsupported proposal types are accepted. -testPropUnsupported :: (HasCallStack) => App () -testPropUnsupported = do - users@[_alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) - [alice1, bob1] <- traverse (createMLSClient def) users void $ uploadNewKeyPackage def bob1 - convId <- createNewGroup def alice1 void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle - mp <- createReInitProposal convId alice1 - - -- we cannot consume this message, because the membership tag is fake - void $ postMLSMessage mp.sender mp.message >>= getJSON 201 - -testAddUserBareProposalCommit :: (HasCallStack) => App () -testAddUserBareProposalCommit = do - [alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - convId <- createNewGroup def alice1 void $ uploadNewKeyPackage def bob1 - void $ createAddCommit alice1 convId [] >>= sendAndConsumeCommitBundle - - createAddProposals convId alice1 [bob] - >>= traverse_ sendAndConsumeMessage - commit <- createPendingProposalCommit convId alice1 - void $ assertJust "Expected welcome" commit.welcome - void $ sendAndConsumeCommitBundle commit - - -- check that bob can now see the conversation - convs <- getAllConvs bob - convIds <- traverse objConvId convs - void - $ assertBool - "Users added to an MLS group should find it when listing conversations" - (convId `elem` convIds) + [proposal] <- createAddProposals convId alice1 [bob] + postMLSMessage alice1 proposal.message >>= flip withResponse \resp -> do + resp.status `shouldMatchInt` 422 + resp.json %. "label" `shouldMatch` "mls-unsupported-message" testShadowConversation :: (HasCallStack) => App () testShadowConversation = do @@ -841,43 +778,76 @@ testShadowConversationDenied = do bindResponse (postConversation bob1 (defMLS {parent = Just convId.id_})) $ \resp -> do resp.status `shouldMatchInt` 403 -testPropExistingConv :: (HasCallStack) => App () -testPropExistingConv = do - [alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - void $ uploadNewKeyPackage def bob1 - convId <- createNewGroup def alice1 - void $ createAddCommit alice1 convId [] >>= sendAndConsumeCommitBundle - res <- createAddProposals convId alice1 [bob] >>= traverse sendAndConsumeMessage >>= assertOne - shouldBeEmpty (res %. "events") - -- @SF.Separation @TSFI.RESTfulAPI @S2 -- --- This test verifies that the server rejects any commit that does not --- reference all pending proposals in an MLS group. +-- This test verifies that rejected bare proposals do not create pending +-- backend proposals, and that the corresponding membership change succeeds +-- when submitted as a commit. testCommitNotReferencingAllProposals :: (HasCallStack) => App () testCommitNotReferencingAllProposals = do - users@[_alice, bob, charlie] <- createAndConnectUsers (replicate 3 OwnDomain) - - [alice1, bob1, charlie1] <- traverse (createMLSClient def) users + [alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] convId <- createNewGroup def alice1 - traverse_ (uploadNewKeyPackage def) [bob1, charlie1] - void $ createAddCommit alice1 convId [] >>= sendAndConsumeCommitBundle + void $ uploadNewKeyPackage def bob1 + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle - gsBackup <- getClientGroupState alice1 + groupStateBeforeRemoval <- getClientGroupState alice1 + let isRemoveProposal e = do + isNewMLSMessageNotif e &&~ do + msgData <- e %. "payload.0.data" & asByteString + msg <- showMessage def alice1 msgData + fieldEquals msg "message.content.body.Proposal.Remove.removed" (1 :: Int) - -- create proposals for bob and charlie - createAddProposals convId alice1 [bob, charlie] - >>= traverse_ sendAndConsumeMessage + withWebSocket alice1 $ \ws -> do + deleteUser bob + void $ consumeMessageWithPredicate isRemoveProposal convId def alice1 Nothing ws - -- now create a commit referencing only the first proposal - setClientGroupState alice1 gsBackup - commit <- createPendingProposalCommit convId alice1 + groupStateWithRemovalProposal <- getClientGroupState alice1 + bobUser <- asString $ bob %. "id" + -- Keep the integration-test conversation model in sync with the backend + -- deletion. The backend has removed Bob from the Wire conversation and + -- created a backend remove proposal, but the harness's 'mls.convs' map is + -- not updated when Alice consumes that proposal. The client MLS group state + -- still contains Bob's leaf until the commit is applied, so this update is + -- separate from the 'setClientGroupState' calls below. + modifyMLSState $ \mls -> + mls + { convs = + Map.adjust + ( \conv -> + conv + { members = Set.filter (\m -> m.user /= bobUser) conv.members, + memberUsers = Set.filter (/= bob1.qualifiedUserId) conv.memberUsers + } + ) + convId + mls.convs + } - -- send commit and expect and error - bindResponse (postMLSCommitBundle alice1 (mkBundle commit)) $ \resp -> do + -- Restore the state before the backend proposal was consumed and submit a + -- commit that does not reference it. + setClientGroupState alice1 groupStateBeforeRemoval + alice2 <- createMLSClient def alice + void $ uploadNewKeyPackage def alice2 + commitWithoutRemoval <- createAddCommit alice1 convId [alice] + bindResponse (postMLSCommitBundle alice1 (mkBundle commitWithoutRemoval)) $ \resp -> do resp.status `shouldMatchInt` 400 resp.json %. "label" `shouldMatch` "mls-commit-missing-references" + -- 'createAddCommit' updates the harness optimistically with a pending + -- welcome recipient, even though the backend rejects this commit because it + -- omits the pending remove proposal. Clear that bookkeeping before consuming + -- the valid removal commit; otherwise the harness would wait for a welcome + -- for a client that was never added. + modifyMLSState $ \mls -> + mls + { convs = + Map.adjust (\conv -> conv {newMembers = mempty}) convId mls.convs + } + + -- Restore the state containing the backend proposal and submit the commit + -- generated from it. + setClientGroupState alice1 groupStateWithRemovalProposal + void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundle -- @END diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Proposal.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Proposal.hs index 083b1b77d20..0edaaa345a3 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Proposal.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Proposal.hs @@ -242,8 +242,9 @@ applyProposal _ciphersuite (RemoveProposal idx) = do applyProposal _activeData _ = pure mempty processProposal :: - (HasProposalEffects r) => - ( Member (ErrorS 'ConvNotFound) r, + ( HasProposalEffects r, + Member (ErrorS 'MLSUnsupportedMessage) r, + Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'MLSStaleMessage) r, Member (ErrorS 'MLSInvalidLeafNodeSignature) r ) => @@ -259,6 +260,13 @@ processProposal qusr lConvOrSub groupId epoch pub prop = do -- Check if the group ID matches that of a conversation unless (groupId == cnvmlsGroupId mlsMeta) $ throwS @'ConvNotFound + -- Proposals from existing members are not authorized independently of the + -- commit that applies them. They must therefore only be submitted as part + -- of a commit bundle, where the committer's conversation role can be + -- checked. External proposals are retained for the join flow and are + -- restricted by 'checkExternalProposalUser' below. + unless (isExternal pub.sender) $ throwS @'MLSUnsupportedMessage + case cnvmlsActiveData mlsMeta of Nothing -> throw $ mlsProtocolError "Bare proposals at epoch 0 are not supported" Just activeData -> do From 2c2fedbe55d9737bb7633180a2d806b15475eb6f Mon Sep 17 00:00:00 2001 From: VeryMilkyJoe Date: Mon, 7 Sep 2026 12:57:44 +0200 Subject: [PATCH 12/29] WPB-28484 change default total limit bytes value to -1 (#5519) --- .../WPB-28484-change-default-total-limit-bytes-value-to-1 | 1 + charts/wire-server/values.yaml | 2 +- docs/src/developer/reference/config-options.md | 2 +- hack/helm_vars/wire-server/values.yaml.gotmpl | 2 +- integration/test/Test/FeatureFlags/CellsInternal.hs | 2 +- integration/test/Test/FeatureFlags/Util.hs | 2 +- libs/wire-api/src/Wire/API/Team/Feature.hs | 2 +- services/galley/galley.integration.yaml | 2 +- tools/stern/test/integration/API.hs | 2 +- 9 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 changelog.d/2-features/WPB-28484-change-default-total-limit-bytes-value-to-1 diff --git a/changelog.d/2-features/WPB-28484-change-default-total-limit-bytes-value-to-1 b/changelog.d/2-features/WPB-28484-change-default-total-limit-bytes-value-to-1 new file mode 100644 index 00000000000..d5e72d2730b --- /dev/null +++ b/changelog.d/2-features/WPB-28484-change-default-total-limit-bytes-value-to-1 @@ -0,0 +1 @@ +Change the default value of totalLimitBytes from one terrabyte to unlimited \ No newline at end of file diff --git a/charts/wire-server/values.yaml b/charts/wire-server/values.yaml index c6dda2176a2..9a98be5d528 100644 --- a/charts/wire-server/values.yaml +++ b/charts/wire-server/values.yaml @@ -313,7 +313,7 @@ galley: collabora: edition: COOL storage: - totalLimitBytes: "1000000000000" + totalLimitBytes: "-1" perUserQuotaBytes: "-1" allowedGlobalOperations: status: enabled diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index c1b48558c7c..b788c123722 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -811,7 +811,7 @@ config: collabora: edition: COOL storage: - totalLimitBytes: "1000000000000" # 1 TB + totalLimitBytes: "-1" perUserQuotaBytes: "-1" ``` diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 673cb6da817..43373b1cf2e 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -400,7 +400,7 @@ galley: collabora: edition: COOL storage: - totalLimitBytes: "1000000000000" + totalLimitBytes: "-1" perUserQuotaBytes: "-1" allowedGlobalOperations: status: enabled diff --git a/integration/test/Test/FeatureFlags/CellsInternal.hs b/integration/test/Test/FeatureFlags/CellsInternal.hs index 573ba9c8809..0bc135c1c53 100644 --- a/integration/test/Test/FeatureFlags/CellsInternal.hs +++ b/integration/test/Test/FeatureFlags/CellsInternal.hs @@ -96,7 +96,7 @@ defConf = CellsInternalConfig { url = "https://cells-beta.wire.com", collabora = "COOL", - totalLimit = "1000000000000", + totalLimit = "-1", quota = "-1" } diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index 17a994de737..6b0ad2175aa 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -244,7 +244,7 @@ defAllFeatures = "collabora" .= object ["edition" .= "COOL"], "storage" .= object - [ "totalLimitBytes" .= "1000000000000", + [ "totalLimitBytes" .= "-1", "perUserQuotaBytes" .= "-1" ] ] diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 28563253014..eeef0a88b8d 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -2180,7 +2180,7 @@ instance Default CellsInternalConfig where collabora = CellsCollabora Cool, storage = CellsStorage - { totalLimitBytes = Just $ QuotaBytesFinite $ NumBytes $ BigIntString 1000000000000, -- 1 TB + { totalLimitBytes = Just QuotaBytesUnlimited, perUserQuotaBytes = QuotaBytesUnlimited } } diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 85ec6c320c1..34762762965 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -225,7 +225,7 @@ settings: collabora: edition: COOL storage: - totalLimitBytes: "1000000000000" + totalLimitBytes: "-1" perUserQuotaBytes: "-1" allowedGlobalOperations: status: enabled diff --git a/tools/stern/test/integration/API.hs b/tools/stern/test/integration/API.hs index b0b30b5afb3..f882fc4c682 100644 --- a/tools/stern/test/integration/API.hs +++ b/tools/stern/test/integration/API.hs @@ -404,7 +404,7 @@ testCellsInternalConfig = do (_, tid, _) <- createTeamWithNMembers 1 cfg <- getFeatureConfig @CellsInternalConfig tid liftIO $ do - cfg.config.storage.totalLimitBytes @?= Just (QuotaBytesFinite (NumBytes (BigIntString 1000000000000))) + cfg.config.storage.totalLimitBytes @?= Just QuotaBytesUnlimited cfg.config.storage.perUserQuotaBytes @?= QuotaBytesUnlimited let newBackend :: HttpsUrl newBackend = fromMaybe (error "invalid url") . fromByteString $ "https://cells-internal.example.com" From b248725d7cbc67c54d9fbaeee762fabe04aadb63 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 9 Sep 2026 11:28:56 +0200 Subject: [PATCH 13/29] fix the v17 swagger docs (#5529) --- services/brig/docs/swagger-v17.json | 32498 ++++++++++++++++---------- 1 file changed, 20764 insertions(+), 11734 deletions(-) diff --git a/services/brig/docs/swagger-v17.json b/services/brig/docs/swagger-v17.json index 2c63648dc97..dd271f9bac1 100644 --- a/services/brig/docs/swagger-v17.json +++ b/services/brig/docs/swagger-v17.json @@ -1,57 +1,41 @@ { "components": { "schemas": { - "": { - "enum": [ - "audio", - "books", - "business", - "design", - "education", - "entertainment", - "finance", - "fitness", - "food-drink", - "games", - "graphics", - "health", - "integration", - "lifestyle", - "media", - "medical", - "movies", - "music", - "news", - "photography", - "poll", - "productivity", - "quiz", - "rating", - "shopping", - "social", - "sports", - "travel", - "tutorial", - "video", - "weather" - ], - "type": "string" - }, "ASCII": { "example": "aGVsbG8", "type": "string" }, - "Access": { - "description": "How users can join conversations", + "AcceptTeamInvitation_Nzg5NzI3MjA2": { + "description": "Accept an invitation to join a team on Wire.", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "password": { + "description": "The user account password.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "code", + "password" + ], + "type": "object" + }, + "AccessRoleLegacy_LTYwOTAxMDI1": { + "deprecated": true, + "description": "Deprecated, please use access_role_v2", "enum": [ "private", - "invite", - "link", - "code" + "team", + "activated", + "non_activated" ], "type": "string" }, - "AccessRole": { + "AccessRole_Mzk3MDYzMzcw": { "description": "Which users/services can join conversations. This replaces legacy access roles and allows a more fine grained configuration of access roles, and in particular a separation of guest and services access.\n\nThis field is optional. If it is not present, the default will be `[team_member, non_team_member, service]`. Please note that an empty list is not allowed when creating a new conversation.", "enum": [ "team_member", @@ -61,18 +45,13 @@ ], "type": "string" }, - "AccessRoleLegacy": { - "deprecated": true, - "description": "Deprecated, please use access_role_v2", + "AccessTokenType_LTgyOTY0NDE5": { "enum": [ - "private", - "team", - "activated", - "non_activated" + "DPoP" ], "type": "string" }, - "AccessToken": { + "AccessToken_ODIyMTczMjMw": { "properties": { "access_token": { "description": "The opaque access token string", @@ -83,7 +62,7 @@ "type": "integer" }, "token_type": { - "$ref": "#/components/schemas/TokenType" + "$ref": "#/components/schemas/TokenType_NTkyMzk4MjIz" }, "user": { "$ref": "#/components/schemas/UUID" @@ -97,9 +76,23 @@ ], "type": "object" }, - "AccessTokenType": { + "Access_NjkyMzE5ODc0": { + "description": "How users can join conversations", "enum": [ - "DPoP" + "private", + "invite", + "link", + "code" + ], + "type": "string" + }, + "AccountStatus_NzkzNDU1ODU5": { + "enum": [ + "active", + "suspended", + "deleted", + "ephemeral", + "pending-invitation" ], "type": "string" }, @@ -113,11 +106,12 @@ "modify_conversation_access", "modify_other_conversation_member", "leave_conversation", - "delete_conversation" + "delete_conversation", + "modify_add_permission" ], "type": "string" }, - "Activate": { + "Activate_MzUzNzIxODUw": { "description": "Data for an activation request.", "properties": { "code": { @@ -140,7 +134,7 @@ ], "type": "object" }, - "ActivationResponse": { + "ActivationResponse_LTIyOTY5NDE3": { "description": "Response body of a successful activation request", "properties": { "email": { @@ -156,25 +150,7 @@ }, "type": "object" }, - "AddBot": { - "properties": { - "locale": { - "$ref": "#/components/schemas/Locale" - }, - "provider": { - "$ref": "#/components/schemas/UUID" - }, - "service": { - "$ref": "#/components/schemas/UUID" - } - }, - "required": [ - "provider", - "service" - ], - "type": "object" - }, - "AddBotResponse": { + "AddBotResponse_ODA5MzA2NTA1": { "properties": { "accent_id": { "format": "int32", @@ -184,7 +160,7 @@ }, "assets": { "items": { - "$ref": "#/components/schemas/UserAsset" + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" }, "type": "array" }, @@ -193,7 +169,7 @@ "type": "string" }, "event": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" }, "id": { "$ref": "#/components/schemas/UUID" @@ -214,94 +190,67 @@ ], "type": "object" }, - "AllTeamFeatures": { + "AddBot_NjI0ODkyODk3": { "properties": { - "appLock": { - "$ref": "#/components/schemas/AppLockConfig.LockableFeature" - }, - "classifiedDomains": { - "$ref": "#/components/schemas/ClassifiedDomainsConfig.LockableFeature" - }, - "conferenceCalling": { - "$ref": "#/components/schemas/ConferenceCallingConfig.LockableFeature" - }, - "conversationGuestLinks": { - "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" - }, - "digitalSignatures": { - "$ref": "#/components/schemas/DigitalSignaturesConfig.LockableFeature" - }, - "enforceFileDownloadLocation": { - "$ref": "#/components/schemas/EnforceFileDownloadLocation.LockableFeature" - }, - "exposeInvitationURLsToTeamAdmin": { - "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.LockableFeature" - }, - "fileSharing": { - "$ref": "#/components/schemas/FileSharingConfig.LockableFeature" - }, - "legalhold": { - "$ref": "#/components/schemas/LegalholdConfig.LockableFeature" - }, - "limitedEventFanout": { - "$ref": "#/components/schemas/LimitedEventFanoutConfig.LockableFeature" - }, - "mls": { - "$ref": "#/components/schemas/MLSConfig.LockableFeature" - }, - "mlsE2EId": { - "$ref": "#/components/schemas/MlsE2EIdConfig.LockableFeature" - }, - "mlsMigration": { - "$ref": "#/components/schemas/MlsMigration.LockableFeature" - }, - "outlookCalIntegration": { - "$ref": "#/components/schemas/OutlookCalIntegrationConfig.LockableFeature" - }, - "searchVisibility": { - "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.LockableFeature" - }, - "searchVisibilityInbound": { - "$ref": "#/components/schemas/SearchVisibilityInboundConfig.LockableFeature" - }, - "selfDeletingMessages": { - "$ref": "#/components/schemas/SelfDeletingMessagesConfig.LockableFeature" - }, - "sndFactorPasswordChallenge": { - "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.LockableFeature" + "locale": { + "$ref": "#/components/schemas/Locale" }, - "sso": { - "$ref": "#/components/schemas/SSOConfig.LockableFeature" + "provider": { + "$ref": "#/components/schemas/UUID" }, - "validateSAMLemails": { - "$ref": "#/components/schemas/ValidateSAMLEmailsConfig.LockableFeature" + "service": { + "$ref": "#/components/schemas/UUID" } }, "required": [ - "legalhold", - "sso", - "searchVisibility", - "searchVisibilityInbound", - "validateSAMLemails", - "digitalSignatures", - "appLock", - "fileSharing", - "classifiedDomains", - "conferenceCalling", - "selfDeletingMessages", - "conversationGuestLinks", - "sndFactorPasswordChallenge", - "mls", - "exposeInvitationURLsToTeamAdmin", - "outlookCalIntegration", - "mlsE2EId", - "mlsMigration", - "enforceFileDownloadLocation", - "limitedEventFanout" + "provider", + "service" ], "type": "object" }, - "Alpha": { + "AddPermissionUpdate_LTU3MzEwOTY4": { + "description": "The action of changing the permission to add members to a channel", + "properties": { + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + } + }, + "required": [ + "add_permission" + ], + "type": "object" + }, + "AddPermission_LTE1MzgzNzE3": { + "enum": [ + "admins", + "everyone" + ], + "type": "string" + }, + "AdminlessReminder_LTkyMDUxNTk5": { + "properties": { + "deletion_scheduled_for": { + "$ref": "#/components/schemas/UTCTimeMillis" + } + }, + "required": [ + "deletion_scheduled_for" + ], + "type": "object" + }, + "AllowedGlobalOperationsConfig_MzAwOTU1MDkx": { + "properties": { + "mlsConversationReset": { + "type": "boolean" + } + }, + "required": [ + "mlsConversationReset" + ], + "type": "object" + }, + "Alpha_LTE4NDUxNDQ4": { + "description": "ISO 4217 alphabetic codes. This is only stored by the backend, not processed. It can be removed once billing supports currency changes after team creation.", "enum": [ "AED", "AFN", @@ -482,9 +431,28 @@ "ZMW", "ZWL" ], + "example": "EUR", "type": "string" }, - "AppLockConfig": { + "AppInfo_MjgwNTkwOTUz": { + "properties": { + "category": { + "description": "Category name (if uncertain, pick \"other\")", + "type": "string" + }, + "description": { + "maxLength": 300, + "minLength": 0, + "type": "string" + } + }, + "required": [ + "category", + "description" + ], + "type": "object" + }, + "AppLockConfigB_Covered_Identity_NDIxOTc2Njkz": { "properties": { "enforceAppLock": { "type": "boolean" @@ -502,63 +470,48 @@ ], "type": "object" }, - "AppLockConfig.Feature": { + "ApproveLegalHoldForUserRequest_NjEyNzYyMTIx": { "properties": { - "config": { - "$ref": "#/components/schemas/AppLockConfig" - }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" - }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" } }, - "required": [ - "status", - "config" - ], "type": "object" }, - "AppLockConfig.LockableFeature": { + "AssetKey": { + "description": "S3 asset key for an icon image with retention information.", + "example": "3-1-47de4580-ae51-4650-acbb-d10c028cb0ac", + "type": "string" + }, + "AssetSize_OTAwMDA3ODY2": { + "enum": [ + "preview", + "complete" + ], + "type": "string" + }, + "AssetSource": {}, + "Asset_LTIyMjc1NDEz": { "properties": { - "config": { - "$ref": "#/components/schemas/AppLockConfig" - }, - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" + "key": { + "$ref": "#/components/schemas/AssetKey" }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" + "size": { + "$ref": "#/components/schemas/AssetSize_OTAwMDA3ODY2" }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "type": { + "$ref": "#/components/schemas/MTYxOTI3NjM3" } }, "required": [ - "status", - "lockStatus", - "config" + "key", + "type" ], "type": "object" }, - "ApproveLegalHoldForUserRequest": { - "properties": { - "password": { - "maxLength": 1024, - "minLength": 6, - "type": "string" - } - }, - "type": "object" - }, - "Asset": { + "Asset_Qualified_AssetKey_MzU1MjMxNTA5": { "properties": { "domain": { "$ref": "#/components/schemas/Domain" @@ -579,34 +532,38 @@ ], "type": "object" }, - "AssetKey": { - "example": "3-1-47de4580-ae51-4650-acbb-d10c028cb0ac", - "type": "string" - }, - "AssetSize": { - "enum": [ - "preview", - "complete" - ], - "type": "string" - }, - "AssetSource": {}, - "AssetType": { - "enum": [ - "image" + "AuthSFTServer_LTY5MzcyOTE0": { + "description": "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers", + "properties": { + "credential": { + "$ref": "#/components/schemas/ASCII" + }, + "urls": { + "description": "Array containing exactly one SFT server address of the form 'https://:'", + "items": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "type": "array" + }, + "username": { + "$ref": "#/components/schemas/SFTUsername" + } + }, + "required": [ + "urls" ], - "type": "string" + "type": "object" }, "AuthnRequest": { "properties": { "iD": { - "$ref": "#/components/schemas/ID_*_AuthnRequest" + "$ref": "#/components/schemas/Id_AuthnRequest" }, "issueInstant": { "$ref": "#/components/schemas/Time" }, "issuer": { - "type": "string" + "$ref": "#/components/schemas/URI" }, "nameIDPolicy": { "$ref": "#/components/schemas/NameIdPolicy" @@ -623,30 +580,31 @@ "example": "ZXhhbXBsZQo=", "type": "string" }, - "BaseProtocol": { + "Base64URLByteString": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "BaseProtocolTag_LTM0MDE1NTEx": { "enum": [ "proteus", "mls" ], "type": "string" }, - "BindingNewTeamUser": { + "BindingNewTeamUser_LTY0MDQxMDEw": { "properties": { "currency": { - "$ref": "#/components/schemas/Alpha" + "$ref": "#/components/schemas/Alpha_LTE4NDUxNDQ4" }, "icon": { "$ref": "#/components/schemas/Icon" }, "icon_key": { - "description": "team icon asset key", + "description": "The decryption key for the team icon S3 asset", "maxLength": 256, "minLength": 1, "type": "string" }, - "members": { - "description": "initial team member ids (between 1 and 127)" - }, "name": { "description": "team name", "maxLength": 256, @@ -660,15 +618,14 @@ ], "type": "object" }, - "Body": {}, - "BotConvView": { + "BotConvView_LTYzMjIzMjQz": { "properties": { "id": { "$ref": "#/components/schemas/UUID" }, "members": { "items": { - "$ref": "#/components/schemas/OtherMember" + "$ref": "#/components/schemas/OtherMember_LTgzNzE2MTk4" }, "type": "array" }, @@ -682,7 +639,7 @@ ], "type": "object" }, - "BotUserView": { + "BotUserView_LTE2MTkwMTcw": { "properties": { "accent_id": { "format": "int32", @@ -712,136 +669,415 @@ ], "type": "object" }, - "CheckHandles": { + "CellsBackend_LTE1Nzg3NzQ2": { "properties": { - "handles": { - "items": { - "type": "string" - }, - "maxItems": 50, - "minItems": 1, - "type": "array" - }, - "return": { - "maximum": 10, - "minimum": 1, - "type": "integer" + "url": { + "$ref": "#/components/schemas/HttpsUrl" } }, "required": [ - "handles", - "return" + "url" ], "type": "object" }, - "CipherSuiteTag": { - "description": "The cipher suite of the corresponding MLS group", - "maximum": 65535, - "minimum": 0, - "type": "integer" + "CellsCollaboraStatus_MTgzNTQyNzUz": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" }, - "ClassifiedDomainsConfig": { + "CellsCollabora_LTMzNDA5MDIz": { "properties": { - "domains": { - "items": { - "$ref": "#/components/schemas/Domain" - }, - "type": "array" + "edition": { + "$ref": "#/components/schemas/CollaboraEdition_LTg2NDA1NDQ4" } }, "required": [ - "domains" + "edition" ], "type": "object" }, - "ClassifiedDomainsConfig.LockableFeature": { + "CellsConfigB_Covered_Identity_LTE1NzkwOTcz": { + "example": { + "channels": { + "default": "enabled", + "enabled": true + }, + "collabora": { + "enabled": false + }, + "groups": { + "default": "enabled", + "enabled": true + }, + "metadata": { + "namespaces": { + "usermetaTags": { + "allowFreeValues": true, + "defaultValues": [] + } + } + }, + "one2one": { + "default": "enabled", + "enabled": true + }, + "publicLinks": { + "enableFiles": true, + "enableFolders": true, + "enforceExpirationDefault": 0, + "enforceExpirationMax": 0, + "enforcePassword": false + }, + "storage": { + "perFileQuotaBytes": "100000000", + "recycle": { + "allowSkip": false, + "autoPurgeDays": 30, + "disable": false + } + }, + "users": { + "externals": true, + "guests": false + } + }, "properties": { - "config": { - "$ref": "#/components/schemas/ClassifiedDomainsConfig" + "channels": { + "$ref": "#/components/schemas/CellsProperty_NzcxMDIzMzk0" }, - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" + "collabora": { + "$ref": "#/components/schemas/CellsCollaboraStatus_MTgzNTQyNzUz" }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" + "groups": { + "$ref": "#/components/schemas/CellsProperty_NzcxMDIzMzk0" }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "metadata": { + "$ref": "#/components/schemas/CellsMetadata_LTY1OTM5MTM0" + }, + "one2one": { + "$ref": "#/components/schemas/CellsProperty_NzcxMDIzMzk0" + }, + "publicLinks": { + "$ref": "#/components/schemas/CellsPublicLinks_MjgxMzQ3Mzk4" + }, + "storage": { + "$ref": "#/components/schemas/CellsConfigStorage_LTM0NDMwODM4" + }, + "users": { + "$ref": "#/components/schemas/CellsUsers_LTQ4NTEyODA1" } }, "required": [ - "status", - "lockStatus", - "config" + "channels", + "groups", + "one2one", + "users", + "collabora", + "publicLinks", + "storage", + "metadata" ], "type": "object" }, - "Client": { + "CellsConfigStorage_LTM0NDMwODM4": { "properties": { - "capabilities": { - "$ref": "#/components/schemas/ClientCapabilityList" + "perFileQuotaBytes": { + "type": "string" }, - "class": { - "$ref": "#/components/schemas/ClientClass" + "recycle": { + "$ref": "#/components/schemas/CellsRecycle_LTQxMTg3NTkx" + } + }, + "required": [ + "perFileQuotaBytes", + "recycle" + ], + "type": "object" + }, + "CellsInternalConfigB_Covered_Identity_LTUzMDkwOTAz": { + "properties": { + "backend": { + "$ref": "#/components/schemas/CellsBackend_LTE1Nzg3NzQ2" }, - "cookie": { - "type": "string" + "collabora": { + "$ref": "#/components/schemas/CellsCollabora_LTMzNDA5MDIz" }, - "id": { - "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", - "type": "string" + "storage": { + "$ref": "#/components/schemas/CellsStorage_LTY2Mzc5NzY1" + } + }, + "required": [ + "backend", + "collabora", + "storage" + ], + "type": "object" + }, + "CellsMetadata_LTY1OTM5MTM0": { + "properties": { + "namespaces": { + "$ref": "#/components/schemas/CellsNamespaces_MzUxMjEzOTQw" + } + }, + "required": [ + "namespaces" + ], + "type": "object" + }, + "CellsNamespaces_MzUxMjEzOTQw": { + "properties": { + "usermetaTags": { + "$ref": "#/components/schemas/CellsUserMetaTags_LTc4Njk4NTY0" + } + }, + "required": [ + "usermetaTags" + ], + "type": "object" + }, + "CellsPropertyStatus_MTQ5NjE2MzQ4": { + "enum": [ + "enabled", + "disabled", + "enforced" + ], + "type": "string" + }, + "CellsProperty_NzcxMDIzMzk0": { + "properties": { + "default": { + "$ref": "#/components/schemas/CellsPropertyStatus_MTQ5NjE2MzQ4" }, - "label": { - "type": "string" + "enabled": { + "type": "boolean" + } + }, + "required": [ + "enabled", + "default" + ], + "type": "object" + }, + "CellsPublicLinks_MjgxMzQ3Mzk4": { + "properties": { + "enableFiles": { + "type": "boolean" }, - "last_active": { - "$ref": "#/components/schemas/UTCTime" + "enableFolders": { + "type": "boolean" }, - "mls_public_keys": { - "$ref": "#/components/schemas/MLSPublicKeys" + "enforceExpirationDefault": { + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" }, - "model": { + "enforceExpirationMax": { + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "enforcePassword": { + "type": "boolean" + } + }, + "required": [ + "enableFiles", + "enableFolders", + "enforcePassword", + "enforceExpirationMax", + "enforceExpirationDefault" + ], + "type": "object" + }, + "CellsRecycle_LTQxMTg3NTkx": { + "properties": { + "allowSkip": { + "type": "boolean" + }, + "autoPurgeDays": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "disable": { + "type": "boolean" + } + }, + "required": [ + "autoPurgeDays", + "disable", + "allowSkip" + ], + "type": "object" + }, + "CellsState_LTg4MDEwNDA5": { + "enum": [ + "disabled", + "pending", + "ready" + ], + "type": "string" + }, + "CellsStorage_LTY2Mzc5NzY1": { + "properties": { + "perUserQuotaBytes": { + "example": "-1", "type": "string" }, - "time": { - "$ref": "#/components/schemas/UTCTimeMillis" + "totalLimitBytes": { + "example": "-1", + "type": "string" + } + }, + "required": [ + "perUserQuotaBytes" + ], + "type": "object" + }, + "CellsUserMetaTags_LTc4Njk4NTY0": { + "properties": { + "allowFreeValues": { + "type": "boolean" }, - "type": { - "$ref": "#/components/schemas/ClientType" + "defaultValues": { + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ - "id", - "type", - "time" + "defaultValues", + "allowFreeValues" + ], + "type": "object" + }, + "CellsUsers_LTQ4NTEyODA1": { + "properties": { + "externals": { + "type": "boolean" + }, + "guests": { + "type": "boolean" + } + }, + "required": [ + "externals", + "guests" + ], + "type": "object" + }, + "ChallengeToken_Mzk3NTcwOTM3": { + "properties": { + "challenge_token": { + "$ref": "#/components/schemas/Token" + } + }, + "required": [ + "challenge_token" ], "type": "object" }, - "ClientCapability": { + "ChannelPermissions_Mzc1MTM3NTg2": { "enum": [ - "legalhold-implicit-consent" + "team-members", + "everyone", + "admins" ], "type": "string" }, - "ClientCapabilityList": { + "ChannelsConfigB_Covered_Identity_ODk2MTk3NDQ4": { "properties": { - "capabilities": { - "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "allowed_to_create_channels": { + "$ref": "#/components/schemas/ChannelPermissions_Mzc1MTM3NTg2" + }, + "allowed_to_open_channels": { + "$ref": "#/components/schemas/ChannelPermissions_Mzc1MTM3NTg2" + } + }, + "required": [ + "allowed_to_create_channels", + "allowed_to_open_channels" + ], + "type": "object" + }, + "CheckHandles_LTc0OTkxMzAx": { + "properties": { + "handles": { + "items": { + "type": "string" + }, + "maxItems": 50, + "minItems": 1, + "type": "array" + }, + "return": { + "maximum": 10, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "handles", + "return" + ], + "type": "object" + }, + "CheckUserGroupName_LTg0ODU1OTk1": { + "properties": { + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "CipherSuiteTag": { + "description": "The cipher suite of the corresponding MLS group", + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "ClassifiedDomainsConfig_LTg4MDcwMDg2": { + "properties": { + "domains": { "items": { - "$ref": "#/components/schemas/ClientCapability" + "$ref": "#/components/schemas/Domain" }, "type": "array" } }, "required": [ - "capabilities" + "domains" ], "type": "object" }, - "ClientClass": { + "ClientCapabilityList": { + "items": { + "$ref": "#/components/schemas/ClientCapability_MTY2NDAzMjM3" + }, + "type": "array" + }, + "ClientCapability_MTY2NDAzMjM3": { + "enum": [ + "legalhold-implicit-consent", + "consumable-notifications" + ], + "type": "string" + }, + "ClientClass_NjE3MDgwNzcx": { "enum": [ "phone", "tablet", @@ -850,7 +1086,7 @@ ], "type": "string" }, - "ClientIdentity": { + "ClientIdentity_MjAxMjI3NTUw": { "properties": { "client_id": { "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", @@ -870,13 +1106,7 @@ ], "type": "object" }, - "ClientListv6": { - "items": { - "$ref": "#/components/schemas/Client" - }, - "type": "array" - }, - "ClientMismatch": { + "ClientMismatch_ODUyODM0MDQ0": { "properties": { "deleted": { "$ref": "#/components/schemas/UserClients" @@ -899,14 +1129,14 @@ ], "type": "object" }, - "ClientPrekey": { + "ClientPrekey_LTcyODUzMTcw": { "properties": { "client": { "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", "type": "string" }, "prekey": { - "$ref": "#/components/schemas/Prekey" + "$ref": "#/components/schemas/UncheckedPrekeyBundle_LTU1MzQzOTgy" } }, "required": [ @@ -915,7 +1145,7 @@ ], "type": "object" }, - "ClientType": { + "ClientType_MjQ0OTQwMzcw": { "enum": [ "temporary", "permanent", @@ -923,13 +1153,13 @@ ], "type": "string" }, - "Clientv6": { + "Client_MTM1OTcwOTQ1": { "properties": { "capabilities": { "$ref": "#/components/schemas/ClientCapabilityList" }, "class": { - "$ref": "#/components/schemas/ClientClass" + "$ref": "#/components/schemas/ClientClass_NjE3MDgwNzcx" }, "cookie": { "type": "string" @@ -954,7 +1184,7 @@ "$ref": "#/components/schemas/UTCTimeMillis" }, "type": { - "$ref": "#/components/schemas/ClientType" + "$ref": "#/components/schemas/ClientType_MjQ0OTQwMzcw" } }, "required": [ @@ -964,17 +1194,33 @@ ], "type": "object" }, - "CodeChallengeMethod": { + "CodeChallengeMethod_NTIxNzk0NDgw": { "description": "The method used to encode the code challenge. Only `S256` is supported.", "enum": [ "S256" ], "type": "string" }, + "CollaboraEdition_LTg2NDA1NDQ4": { + "enum": [ + "NO", + "CODE", + "COOL" + ], + "type": "string" + }, + "CollaboratorPermission_NDg5NTg2ODgy": { + "description": "

Permission granted to a team collaborator.

  • `create_team_conversation`: equivalent to the `CreateConversation` and `AddRemoveConvMember` permissions for team members (both implied in the `member` role); allows creating team group conversations and adding members to them.
  • \n
  • `implicit_connection`: team members are implicitly connected to each other, allowing conversations (1:1 or group) without an explicit connection request. This permission grants the same to a collaborator.
\n

NB: a member of team A can always open conversations with a collaborator of team A; the permission only controls the collaborator's abilities.

", + "enum": [ + "create_team_conversation", + "implicit_connection" + ], + "type": "string" + }, "CommitBundle": { "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." }, - "CompletePasswordReset": { + "CompletePasswordReset_LTYzMDAxNDA1": { "properties": { "code": { "$ref": "#/components/schemas/ASCII" @@ -995,59 +1241,43 @@ ], "type": "object" }, - "ConferenceCallingConfig": { - "properties": { - "useSFTForOneToOneCalls": { - "type": "boolean" - } - }, - "type": "object" - }, - "ConferenceCallingConfig.Feature": { + "CompletePasswordReset_NDcyMjY5OTc4": { + "description": "Data to complete a password reset", "properties": { - "config": { - "$ref": "#/components/schemas/ConferenceCallingConfig" + "code": { + "$ref": "#/components/schemas/ASCII" }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" + "email": { + "$ref": "#/components/schemas/Email" }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "password": { + "description": "New password (6 - 1024 characters)", + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "phone": { + "$ref": "#/components/schemas/PhoneNumber" } }, "required": [ - "status" + "code", + "password" ], "type": "object" }, - "ConferenceCallingConfig.LockableFeature": { + "ConferenceCallingConfigB_Covered_Identity_NDMwMjcyMzM1": { "properties": { - "config": { - "$ref": "#/components/schemas/ConferenceCallingConfig" - }, - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" - }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "useSFTForOneToOneCalls": { + "type": "boolean" } }, - "required": [ - "status", - "lockStatus" - ], "type": "object" }, - "Connect": { + "Connect_ODY3OTE4NTYx": { "properties": { "email": { "type": "string" @@ -1059,7 +1289,7 @@ "type": "string" }, "qualified_recipient": { - "$ref": "#/components/schemas/Qualified_UserId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" }, "recipient": { "$ref": "#/components/schemas/UUID" @@ -1070,10 +1300,10 @@ ], "type": "object" }, - "ConnectionUpdate": { + "ConnectionUpdate_LTU3MTA1OTA5": { "properties": { "status": { - "$ref": "#/components/schemas/Relation" + "$ref": "#/components/schemas/Relation_LTE4OTU5MTk4" } }, "required": [ @@ -1081,32 +1311,28 @@ ], "type": "object" }, - "Connections_Page": { + "Connections_PagingState": { + "type": "string" + }, + "ContactStatusState_LTg2MjAyNzAx": { + "enum": [ + "contactable", + "non-contactable" + ], + "type": "string" + }, + "ContactStatus_LTUzNzk1MzM4": { "properties": { - "connections": { - "items": { - "$ref": "#/components/schemas/UserConnection" - }, - "type": "array" - }, - "has_more": { - "type": "boolean" - }, - "paging_state": { - "$ref": "#/components/schemas/Connections_PagingState" + "state": { + "$ref": "#/components/schemas/ContactStatusState_LTg2MjAyNzAx" } }, "required": [ - "connections", - "has_more", - "paging_state" + "state" ], "type": "object" }, - "Connections_PagingState": { - "type": "string" - }, - "Contact": { + "Contact_LTcwODE3Mjc5": { "description": "Contact discovered through search", "properties": { "accent_id": { @@ -1124,39 +1350,42 @@ "type": "string" }, "qualified_id": { - "$ref": "#/components/schemas/Qualified_UserId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" }, "team": { "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/UserType_LTU1OTU4OTM5" } }, "required": [ "qualified_id", - "name" + "name", + "type" ], "type": "object" }, - "ConvMembers": { + "ConvMembers_LTc2MDg1NDg2": { "description": "Users of a conversation", "properties": { "others": { "description": "All other current users of this conversation", "items": { - "$ref": "#/components/schemas/OtherMember" + "$ref": "#/components/schemas/OtherMember_LTgzNzE2MTk4" }, "type": "array" }, "self": { - "$ref": "#/components/schemas/Member" + "$ref": "#/components/schemas/Member_OTA5OTgyNzcw" } }, "required": [ - "self", "others" ], "type": "object" }, - "ConvTeamInfo": { + "ConvTeamInfo_Mzc5NjcyNjAz": { "description": "Team information of this conversation", "properties": { "managed": { @@ -1172,7 +1401,7 @@ ], "type": "object" }, - "ConvType": { + "ConvType_MzM0NTE3ODE5": { "enum": [ 0, 1, @@ -1181,104 +1410,17 @@ ], "type": "integer" }, - "Conversation": { - "description": "A conversation object as returned from the server", - "properties": { - "access": { - "items": { - "$ref": "#/components/schemas/Access" - }, - "type": "array" - }, - "access_role": { - "items": { - "$ref": "#/components/schemas/AccessRole" - }, - "type": "array" - }, - "cipher_suite": { - "$ref": "#/components/schemas/CipherSuiteTag" - }, - "creator": { - "$ref": "#/components/schemas/UUID" - }, - "epoch": { - "description": "The epoch number of the corresponding MLS group", - "format": "int64", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" - }, - "epoch_timestamp": { - "$ref": "#/components/schemas/UTCTime" - }, - "group_id": { - "$ref": "#/components/schemas/GroupId" - }, - "id": { - "$ref": "#/components/schemas/UUID" - }, - "last_event": { - "type": "string" - }, - "last_event_time": { - "type": "string" - }, - "members": { - "$ref": "#/components/schemas/ConvMembers" - }, - "message_timer": { - "description": "Per-conversation message timer (can be null)", - "format": "int64", - "maximum": 9223372036854775807, - "minimum": -9223372036854775808, - "type": "integer" - }, - "name": { - "type": "string" - }, - "protocol": { - "$ref": "#/components/schemas/Protocol" - }, - "qualified_id": { - "$ref": "#/components/schemas/Qualified_ConvId" - }, - "receipt_mode": { - "description": "Conversation receipt mode", - "format": "int32", - "maximum": 2147483647, - "minimum": -2147483648, - "type": "integer" - }, - "team": { - "$ref": "#/components/schemas/UUID" - }, - "type": { - "$ref": "#/components/schemas/ConvType" - } - }, - "required": [ - "qualified_id", - "type", - "access", - "access_role", - "members", - "group_id", - "epoch" - ], - "type": "object" - }, - "ConversationAccessData": { + "ConversationAccessData_MjMxMTI5ODc3": { "properties": { "access": { "items": { - "$ref": "#/components/schemas/Access" + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" }, "type": "array" }, "access_role": { "items": { - "$ref": "#/components/schemas/AccessRole" + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" }, "type": "array" } @@ -1289,35 +1431,16 @@ ], "type": "object" }, - "ConversationAccessDataV2": { - "properties": { - "access": { - "items": { - "$ref": "#/components/schemas/Access" - }, - "type": "array" - }, - "access_role": { - "$ref": "#/components/schemas/AccessRoleLegacy" - }, - "access_role_v2": { - "items": { - "$ref": "#/components/schemas/AccessRole" - }, - "type": "array" - } - }, - "required": [ - "access" - ], - "type": "object" - }, - "ConversationCode": { + "ConversationCodeInfo_LTc5MzgzNjg3": { "description": "Contains conversation properties to update", "properties": { "code": { "$ref": "#/components/schemas/ASCII" }, + "has_password": { + "description": "Whether the conversation has a password", + "type": "boolean" + }, "key": { "$ref": "#/components/schemas/ASCII" }, @@ -1327,35 +1450,29 @@ }, "required": [ "key", - "code" + "code", + "uri", + "has_password" ], "type": "object" }, - "ConversationCodeInfo": { + "ConversationCode_Mjg3OTI1NTMx": { "description": "Contains conversation properties to update", "properties": { "code": { "$ref": "#/components/schemas/ASCII" }, - "has_password": { - "description": "Whether the conversation has a password", - "type": "boolean" - }, "key": { "$ref": "#/components/schemas/ASCII" - }, - "uri": { - "$ref": "#/components/schemas/HttpsUrl" } }, "required": [ "key", - "code", - "has_password" + "code" ], "type": "object" }, - "ConversationCoverView": { + "ConversationCoverView_LTMwNDkxMTA1": { "description": "Limited view of Conversation.", "properties": { "has_password": { @@ -1374,32 +1491,21 @@ ], "type": "object" }, - "ConversationIds_Page": { + "ConversationHistoryUpdate_LTg5MDQ5Nzgx": { "properties": { - "has_more": { - "type": "boolean" - }, - "paging_state": { - "$ref": "#/components/schemas/ConversationIds_PagingState" - }, - "qualified_conversations": { - "items": { - "$ref": "#/components/schemas/Qualified_ConvId" - }, - "type": "array" + "history": { + "$ref": "#/components/schemas/History" } }, "required": [ - "qualified_conversations", - "has_more", - "paging_state" + "history" ], "type": "object" }, "ConversationIds_PagingState": { "type": "string" }, - "ConversationMessageTimerUpdate": { + "ConversationMessageTimerUpdate_LTcxMjUwNzQ4": { "description": "Contains conversation properties to update", "properties": { "message_timer": { @@ -1411,7 +1517,22 @@ }, "type": "object" }, - "ConversationReceiptModeUpdate": { + "ConversationPage_LTIwMDU2NDI3": { + "description": "This is the last page if it contains fewer rows than requested. There may be 0 rows on a page.", + "properties": { + "page": { + "items": { + "$ref": "#/components/schemas/ConversationSearchResult_NDI0MTcyMDU3" + }, + "type": "array" + } + }, + "required": [ + "page" + ], + "type": "object" + }, + "ConversationReceiptModeUpdate_NDE4MzUzNTU3": { "description": "Contains conversation receipt mode to update to. Receipt mode tells clients whether certain types of receipts should be sent in the given conversation or not. How this value is interpreted is up to clients.", "properties": { "receipt_mode": { @@ -1427,7 +1548,7 @@ ], "type": "object" }, - "ConversationRename": { + "ConversationRename_ODkwODg1MzQ0": { "properties": { "name": { "description": "The new conversation name", @@ -1439,6 +1560,20 @@ ], "type": "object" }, + "ConversationReset_MzU1Nzc5MjAw": { + "properties": { + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "new_group_id": { + "$ref": "#/components/schemas/GroupId" + } + }, + "required": [ + "group_id" + ], + "type": "object" + }, "ConversationRole": { "properties": { "actions": { @@ -1467,200 +1602,59 @@ ], "type": "object" }, - "ConversationV2": { - "description": "A conversation object as returned from the server", + "ConversationSearchResult_NDI0MTcyMDU3": { "properties": { "access": { "items": { - "$ref": "#/components/schemas/Access" + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" }, "type": "array" }, - "access_role": { - "$ref": "#/components/schemas/AccessRoleLegacy" - }, - "access_role_v2": { - "items": { - "$ref": "#/components/schemas/AccessRole" - }, - "type": "array" - }, - "cipher_suite": { - "$ref": "#/components/schemas/CipherSuiteTag" - }, - "creator": { - "$ref": "#/components/schemas/UUID" - }, - "epoch": { - "description": "The epoch number of the corresponding MLS group", - "format": "int64", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" - }, - "epoch_timestamp": { - "$ref": "#/components/schemas/Epoch Timestamp" - }, - "group_id": { - "$ref": "#/components/schemas/GroupId" + "admin_count": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" }, "id": { "$ref": "#/components/schemas/UUID" }, - "last_event": { - "type": "string" - }, - "last_event_time": { - "type": "string" - }, - "members": { - "$ref": "#/components/schemas/ConvMembers" - }, - "message_timer": { - "description": "Per-conversation message timer (can be null)", - "format": "int64", + "member_count": { "maximum": 9223372036854775807, "minimum": -9223372036854775808, "type": "integer" }, "name": { "type": "string" - }, - "protocol": { - "$ref": "#/components/schemas/Protocol" - }, - "qualified_id": { - "$ref": "#/components/schemas/Qualified_ConvId" - }, - "receipt_mode": { - "description": "Conversation receipt mode", - "format": "int32", - "maximum": 2147483647, - "minimum": -2147483648, - "type": "integer" - }, - "team": { - "$ref": "#/components/schemas/UUID" - }, - "type": { - "$ref": "#/components/schemas/ConvType" } }, "required": [ - "qualified_id", - "type", + "id", "access", - "members", - "group_id", - "epoch", - "epoch_timestamp", - "cipher_suite" + "member_count", + "admin_count" ], "type": "object" }, - "ConversationV3v3": { + "Conversation_GroupConvType_MzQzMTQ1OTg3": { "description": "A conversation object as returned from the server", "properties": { "access": { "items": { - "$ref": "#/components/schemas/Access" + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" }, "type": "array" }, "access_role": { "items": { - "$ref": "#/components/schemas/AccessRole" + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" }, "type": "array" }, - "cipher_suite": { - "$ref": "#/components/schemas/CipherSuiteTag" - }, - "creator": { - "$ref": "#/components/schemas/UUID" - }, - "epoch": { - "description": "The epoch number of the corresponding MLS group", - "format": "int64", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" - }, - "epoch_timestamp": { - "$ref": "#/components/schemas/Epoch Timestamp" - }, - "group_id": { - "$ref": "#/components/schemas/GroupId" - }, - "id": { - "$ref": "#/components/schemas/UUID" - }, - "last_event": { - "type": "string" - }, - "last_event_time": { - "type": "string" - }, - "members": { - "$ref": "#/components/schemas/ConvMembers" - }, - "message_timer": { - "description": "Per-conversation message timer (can be null)", - "format": "int64", - "maximum": 9223372036854775807, - "minimum": -9223372036854775808, - "type": "integer" - }, - "name": { - "type": "string" - }, - "protocol": { - "$ref": "#/components/schemas/Protocol" - }, - "qualified_id": { - "$ref": "#/components/schemas/Qualified_ConvId" - }, - "receipt_mode": { - "description": "Conversation receipt mode", - "format": "int32", - "maximum": 2147483647, - "minimum": -2147483648, - "type": "integer" - }, - "team": { - "$ref": "#/components/schemas/UUID" - }, - "type": { - "$ref": "#/components/schemas/ConvType" - } - }, - "required": [ - "qualified_id", - "type", - "access", - "access_role", - "members", - "group_id", - "epoch", - "epoch_timestamp", - "cipher_suite" - ], - "type": "object" - }, - "ConversationV6v6": { - "description": "A conversation object as returned from the server", - "properties": { - "access": { - "items": { - "$ref": "#/components/schemas/Access" - }, - "type": "array" + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" }, - "access_role": { - "items": { - "$ref": "#/components/schemas/AccessRole" - }, - "type": "array" + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" }, "cipher_suite": { "$ref": "#/components/schemas/CipherSuiteTag" @@ -1678,11 +1672,14 @@ "epoch_timestamp": { "$ref": "#/components/schemas/UTCTime" }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, "group_id": { "$ref": "#/components/schemas/GroupId" }, - "id": { - "$ref": "#/components/schemas/UUID" + "history": { + "$ref": "#/components/schemas/History" }, "last_event": { "type": "string" @@ -1691,7 +1688,7 @@ "type": "string" }, "members": { - "$ref": "#/components/schemas/ConvMembers" + "$ref": "#/components/schemas/ConvMembers_LTc2MDg1NDg2" }, "message_timer": { "description": "Per-conversation message timer (can be null)", @@ -1703,11 +1700,14 @@ "name": { "type": "string" }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, "protocol": { - "$ref": "#/components/schemas/Protocol" + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" }, "qualified_id": { - "$ref": "#/components/schemas/Qualified_ConvId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" }, "receipt_mode": { "description": "Conversation receipt mode", @@ -1720,7 +1720,7 @@ "$ref": "#/components/schemas/UUID" }, "type": { - "$ref": "#/components/schemas/ConvType" + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" } }, "required": [ @@ -1734,26 +1734,26 @@ ], "type": "object" }, - "ConversationsResponse": { + "ConversationsResponse_GroupConvType_ODkxMjM2ODM0": { "description": "Response object for getting metadata of a list of conversations", "properties": { "failed": { "description": "The server failed to fetch these conversations, most likely due to network issues while contacting a remote server", "items": { - "$ref": "#/components/schemas/Qualified_ConvId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" }, "type": "array" }, "found": { "items": { - "$ref": "#/components/schemas/Conversation" + "$ref": "#/components/schemas/OwnConversation_GroupConvType_LTU2MzYxNTg0" }, "type": "array" }, "not_found": { "description": "These conversations either don't exist or are deleted.", "items": { - "$ref": "#/components/schemas/Qualified_ConvId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" }, "type": "array" } @@ -1765,7 +1765,29 @@ ], "type": "object" }, - "Cookie": { + "CookieList_LTM4MzYwNzAz": { + "description": "List of cookie information", + "properties": { + "cookies": { + "items": { + "$ref": "#/components/schemas/Cookie_LTkyMDA3OTI5" + }, + "type": "array" + } + }, + "required": [ + "cookies" + ], + "type": "object" + }, + "CookieType_LTE0MjczNzY3": { + "enum": [ + "session", + "persistent" + ], + "type": "string" + }, + "Cookie_LTkyMDA3OTI5": { "properties": { "created": { "$ref": "#/components/schemas/UTCTime" @@ -1789,7 +1811,7 @@ "type": "integer" }, "type": { - "$ref": "#/components/schemas/CookieType" + "$ref": "#/components/schemas/CookieType_LTE0MjczNzY3" } }, "required": [ @@ -1800,29 +1822,7 @@ ], "type": "object" }, - "CookieList": { - "description": "List of cookie information", - "properties": { - "cookies": { - "items": { - "$ref": "#/components/schemas/Cookie" - }, - "type": "array" - } - }, - "required": [ - "cookies" - ], - "type": "object" - }, - "CookieType": { - "enum": [ - "session", - "persistent" - ], - "type": "string" - }, - "CreateConversationCodeRequest": { + "CreateConversationCodeRequest_NTYzMTA1NDYz": { "description": "Request body for creating a conversation code", "properties": { "password": { @@ -1834,21 +1834,27 @@ }, "type": "object" }, - "CreateGroupConversationv6": { + "CreateGroupConversation_GroupConvType_MTQ3OTc4NDk3": { "description": "A created group-conversation object extended with a list of failed-to-add users", "properties": { "access": { "items": { - "$ref": "#/components/schemas/Access" + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" }, "type": "array" }, "access_role": { "items": { - "$ref": "#/components/schemas/AccessRole" + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" }, "type": "array" }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, "cipher_suite": { "$ref": "#/components/schemas/CipherSuiteTag" }, @@ -1867,15 +1873,18 @@ }, "failed_to_add": { "items": { - "$ref": "#/components/schemas/Qualified_UserId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" }, "type": "array" }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, "group_id": { "$ref": "#/components/schemas/GroupId" }, - "id": { - "$ref": "#/components/schemas/UUID" + "history": { + "$ref": "#/components/schemas/History" }, "last_event": { "type": "string" @@ -1884,7 +1893,7 @@ "type": "string" }, "members": { - "$ref": "#/components/schemas/ConvMembers" + "$ref": "#/components/schemas/ConvMembers_LTc2MDg1NDg2" }, "message_timer": { "description": "Per-conversation message timer (can be null)", @@ -1896,11 +1905,14 @@ "name": { "type": "string" }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, "protocol": { - "$ref": "#/components/schemas/Protocol" + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" }, "qualified_id": { - "$ref": "#/components/schemas/Qualified_ConvId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" }, "receipt_mode": { "description": "Conversation receipt mode", @@ -1913,7 +1925,7 @@ "$ref": "#/components/schemas/UUID" }, "type": { - "$ref": "#/components/schemas/ConvType" + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" } }, "required": [ @@ -1928,7 +1940,7 @@ ], "type": "object" }, - "CreateOAuthAuthorizationCodeRequest": { + "CreateOAuthAuthorizationCodeRequest_LTQ3NzI1Mjkz": { "properties": { "client_id": { "$ref": "#/components/schemas/UUID" @@ -1937,13 +1949,13 @@ "$ref": "#/components/schemas/OAuthCodeChallenge" }, "code_challenge_method": { - "$ref": "#/components/schemas/CodeChallengeMethod" + "$ref": "#/components/schemas/CodeChallengeMethod_NTIxNzk0NDgw" }, "redirect_uri": { "$ref": "#/components/schemas/RedirectUrl" }, "response_type": { - "$ref": "#/components/schemas/OAuthResponseType" + "$ref": "#/components/schemas/OAuthResponseType_ODI2Mjg3NzQx" }, "scope": { "description": "The scopes which are requested to get authorization for, separated by a space", @@ -1965,16 +1977,39 @@ ], "type": "object" }, - "CreateScimToken": { + "CreateScimTokenResponse_LTIzOTU2NDU4": { + "properties": { + "info": { + "$ref": "#/components/schemas/ScimTokenInfo_LTI5NjgwNzA1" + }, + "token": { + "type": "string" + } + }, + "required": [ + "token", + "info" + ], + "type": "object" + }, + "CreateScimToken_OTY0NjYxMDQ2": { "properties": { "description": { "type": "string" }, + "idp": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, "password": { + "maxLength": 1024, + "minLength": 6, "type": "string" }, "verification_code": { - "type": "string" + "$ref": "#/components/schemas/ASCII" } }, "required": [ @@ -1982,23 +2017,37 @@ ], "type": "object" }, - "CreateScimTokenResponse": { + "CreateUserTeam_MzI4NDQ1Mzkw": { "properties": { - "info": { - "$ref": "#/components/schemas/ScimTokenInfo" + "team_id": { + "$ref": "#/components/schemas/UUID" }, - "token": { - "description": "Authentication token", + "team_name": { "type": "string" } }, "required": [ - "token", - "info" + "team_id", + "team_name" + ], + "type": "object" + }, + "CreatedApp_LTM3NjUxOTY1": { + "properties": { + "cookie": { + "$ref": "#/components/schemas/SomeUserToken" + }, + "user": { + "$ref": "#/components/schemas/UserProfile_LTQzMTQxMTE1" + } + }, + "required": [ + "user", + "cookie" ], "type": "object" }, - "CustomBackend": { + "CustomBackend_LTQxODI0MjQ0": { "description": "Description of a custom backend", "properties": { "config_json_url": { @@ -2017,7 +2066,7 @@ "DPoPAccessToken": { "type": "string" }, - "DPoPAccessTokenResponse": { + "DPoPAccessTokenResponse_LTgyODU5MDE3": { "properties": { "expires_in": { "format": "int64", @@ -2029,7 +2078,7 @@ "$ref": "#/components/schemas/DPoPAccessToken" }, "type": { - "$ref": "#/components/schemas/AccessTokenType" + "$ref": "#/components/schemas/AccessTokenType_LTgyOTY0NDE5" } }, "required": [ @@ -2039,18 +2088,7 @@ ], "type": "object" }, - "DeleteClient": { - "properties": { - "password": { - "description": "The password of the authenticated user for verification. The password is not required for deleting temporary clients.", - "maxLength": 1024, - "minLength": 6, - "type": "string" - } - }, - "type": "object" - }, - "DeleteKeyPackages": { + "DeleteKeyPackages_LTQxNTcxNjY3": { "properties": { "key_packages": { "items": { @@ -2066,7 +2104,7 @@ ], "type": "object" }, - "DeleteProvider": { + "DeleteProvider_MzYxMzM3Mjg2": { "properties": { "password": { "maxLength": 1024, @@ -2079,7 +2117,7 @@ ], "type": "object" }, - "DeleteService": { + "DeleteService_LTY2NzY5NzMz": { "properties": { "password": { "maxLength": 1024, @@ -2092,26 +2130,31 @@ ], "type": "object" }, - "DeleteSubConversationRequest": { - "description": "Delete an MLS subconversation", + "DeleteUser_NjE0MjE2Mjkz": { "properties": { - "epoch": { - "format": "int64", - "maximum": 18446744073709551615, - "minimum": 0, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "DeletionCodeTimeout_LTU1MTk0NDI3": { + "properties": { + "expires_in": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, "type": "integer" - }, - "group_id": { - "$ref": "#/components/schemas/GroupId" } }, "required": [ - "group_id", - "epoch" + "expires_in" ], "type": "object" }, - "DeleteUser": { + "DisableLegalHoldForUserRequest_LTYyMDYxOTEy": { "properties": { "password": { "maxLength": 1024, @@ -2121,74 +2164,134 @@ }, "type": "object" }, - "DeletionCodeTimeout": { + "Domain": { + "example": "example.com", + "type": "string" + }, + "DomainOwnershipToken_NTU0ODc1NDE5": { "properties": { - "expires_in": { - "format": "int32", - "maximum": 2147483647, - "minimum": -2147483648, - "type": "integer" + "domain_ownership_token": { + "$ref": "#/components/schemas/Token" } }, "required": [ - "expires_in" + "domain_ownership_token" ], "type": "object" }, - "DeprecatedMatchingResult": { - "deprecated": true, + "DomainRedirectConfigTag_MjE2MDI4MDIw": { + "enum": [ + "remove", + "backend", + "no-registration" + ], + "type": "string" + }, + "DomainRedirectConfig_NTI5NDE5MDQy": { "properties": { - "auto-connects": { - "items": {}, - "type": "array" + "backend": { + "$ref": "#/components/schemas/HttpsUrl_HttpsUrl_NjUyMDgzNzk3" }, - "results": { - "items": {}, - "type": "array" + "domain_redirect": { + "$ref": "#/components/schemas/DomainRedirectConfigTag_MjE2MDI4MDIw" } }, "required": [ - "results", - "auto-connects" + "domain_redirect", + "backend" ], "type": "object" }, - "DigitalSignaturesConfig.LockableFeature": { + "DomainRedirectResponse_V10_LTEyMjI4NTM0": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" + "backend": { + "$ref": "#/components/schemas/HttpsUrl_Maybe_HttpsUrl_LTQ1MDkyMzY2" }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" + "domain_redirect": { + "$ref": "#/components/schemas/DomainRedirectTag_LTY3NjU1MDEy" }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "due_to_existing_account": { + "type": "boolean" + }, + "sso_code": { + "$ref": "#/components/schemas/UUID" } }, "required": [ - "status", - "lockStatus" + "domain_redirect", + "sso_code", + "backend" ], "type": "object" }, - "DisableLegalHoldForUserRequest": { + "DomainRedirectTag_LTY3NjU1MDEy": { + "enum": [ + "none", + "locked", + "sso", + "backend", + "no-registration", + "pre-authorized" + ], + "type": "string" + }, + "DomainRegistrationResponse_V10_MjE0NDkxODY4": { "properties": { - "password": { - "maxLength": 1024, - "minLength": 6, - "type": "string" + "authorized_team": { + "$ref": "#/components/schemas/UUID" + }, + "backend": { + "$ref": "#/components/schemas/HttpsUrl_Maybe_HttpsUrl_LTQ1MDkyMzY2" + }, + "dns_verification_token": { + "$ref": "#/components/schemas/ASCII" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "domain_redirect": { + "$ref": "#/components/schemas/DomainRedirectTag_LTY3NjU1MDEy" + }, + "sso_code": { + "$ref": "#/components/schemas/UUID" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "team_invite": { + "$ref": "#/components/schemas/TeamInviteTag_LTQyNTMyNzA0" } }, + "required": [ + "domain", + "domain_redirect", + "sso_code", + "backend", + "team_invite", + "team" + ], "type": "object" }, - "Domain": { - "example": "example.com", - "type": "string" + "DomainVerificationChallenge_NjIwMzA1MjE5": { + "properties": { + "dns_verification_token": { + "$ref": "#/components/schemas/ASCII" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "token": { + "$ref": "#/components/schemas/Token" + } + }, + "required": [ + "id", + "token", + "dns_verification_token" + ], + "type": "object" }, - "EdMemberLeftReason": { + "EdMemberLeftReason_OTAyMDA4NzEw": { "enum": [ "left", "user-deleted", @@ -2196,12 +2299,39 @@ ], "type": "string" }, + "EdMemberLeftReason_QualifiedUserIdList_LTYyOTg2OTQ1": { + "properties": { + "qualified_user_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "type": "array" + }, + "reason": { + "$ref": "#/components/schemas/EdMemberLeftReason_OTAyMDA4NzEw" + }, + "user_ids": { + "deprecated": true, + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "reason", + "qualified_user_ids", + "user_ids" + ], + "type": "object" + }, "Either_OAuthAccessTokenRequest_OAuthRefreshAccessTokenRequest": { "oneOf": [ { "properties": { "Left": { - "$ref": "#/components/schemas/OAuthAccessTokenRequest" + "$ref": "#/components/schemas/OAuthAccessTokenRequest_LTYyNTcyMzI4" } }, "required": [ @@ -2213,7 +2343,7 @@ { "properties": { "Right": { - "$ref": "#/components/schemas/OAuthRefreshAccessTokenRequest" + "$ref": "#/components/schemas/OAuthRefreshAccessTokenRequest_LTUwNjA3OTI1" } }, "required": [ @@ -2227,7 +2357,7 @@ "Email": { "type": "string" }, - "EmailUpdate": { + "EmailUpdate_LTYwODE0ODQ5": { "properties": { "email": { "$ref": "#/components/schemas/Email" @@ -2238,89 +2368,98 @@ ], "type": "object" }, - "EnforceFileDownloadLocation": { - "properties": { - "enforcedDownloadLocation": { - "type": "string" - } - }, - "type": "object" - }, - "EnforceFileDownloadLocation.Feature": { + "EmailUpdate_NjQ5MDg1OTY0": { "properties": { - "config": { - "$ref": "#/components/schemas/EnforceFileDownloadLocation" - }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" - }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "email": { + "$ref": "#/components/schemas/Email" } }, "required": [ - "status", - "config" + "email" ], "type": "object" }, - "EnforceFileDownloadLocation.LockableFeature": { + "EnforceFilDwadLtCgB_vIy_LTQ2NjU1Nzcx": { "properties": { - "config": { - "$ref": "#/components/schemas/EnforceFileDownloadLocation" - }, - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" - }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "enforcedDownloadLocation": { + "type": "string" } }, - "required": [ - "status", - "lockStatus", - "config" - ], "type": "object" }, - "Epoch Timestamp": { + "EpochTimestamp": { "example": "2021-05-12T10:52:02Z", "format": "yyyy-mm-ddThh:MM:ssZ", "type": "string" }, - "Event": { + "EventType_LTQ3NTQyNDYz": { + "enum": [ + "conversation.member-join", + "conversation.member-leave", + "conversation.member-update", + "conversation.rename", + "conversation.access-update", + "conversation.receipt-mode-update", + "conversation.message-timer-update", + "conversation.code-update", + "conversation.code-delete", + "conversation.create", + "conversation.create-meeting", + "conversation.delete", + "conversation.delete-meeting", + "conversation.mls-reset", + "conversation.connect-request", + "conversation.typing", + "conversation.otr-message-add", + "conversation.mls-message-add", + "conversation.mls-welcome", + "conversation.protocol-update", + "conversation.add-permission-update", + "conversation.history-update", + "conversation.adminless-reminder" + ], + "type": "string" + }, + "EventVia_Mjc4MzcyNzE0": { + "enum": [ + "scim", + "user" + ], + "type": "string" + }, + "Event_LTMwMTMyODM5": { "properties": { "conversation": { "$ref": "#/components/schemas/UUID" }, "data": { - "description": "Encrypted message of a conversation", + "description": "The action of changing the permission to add members to a channel", "example": "ZXhhbXBsZQo=", "properties": { "access": { "items": { - "$ref": "#/components/schemas/Access" + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" }, "type": "array" }, "access_role": { - "$ref": "#/components/schemas/AccessRoleLegacy" + "$ref": "#/components/schemas/AccessRoleLegacy_LTYwOTAxMDI1" }, "access_role_v2": { "items": { - "$ref": "#/components/schemas/AccessRole" + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" }, "type": "array" }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "add_type": { + "$ref": "#/components/schemas/JoinType_LTY4MDg2MzA5" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, "cipher_suite": { "$ref": "#/components/schemas/CipherSuiteTag" }, @@ -2337,6 +2476,12 @@ "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", "type": "string" }, + "deletion_scheduled_for": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "depth": { + "$ref": "#/components/schemas/HistoryDuration" + }, "email": { "type": "string" }, @@ -2348,7 +2493,10 @@ "type": "integer" }, "epoch_timestamp": { - "$ref": "#/components/schemas/Epoch Timestamp" + "$ref": "#/components/schemas/EpochTimestamp" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" }, "group_id": { "$ref": "#/components/schemas/GroupId" @@ -2363,6 +2511,9 @@ "hidden_ref": { "type": "string" }, + "history": { + "$ref": "#/components/schemas/History" + }, "id": { "$ref": "#/components/schemas/UUID" }, @@ -2376,7 +2527,7 @@ "type": "string" }, "members": { - "$ref": "#/components/schemas/ConvMembers" + "$ref": "#/components/schemas/OwnConvMembers_LTEwMzUzODMy" }, "message": { "type": "string" @@ -2391,6 +2542,9 @@ "name": { "type": "string" }, + "new_group_id": { + "$ref": "#/components/schemas/GroupId" + }, "otr_archived": { "type": "boolean" }, @@ -2406,26 +2560,29 @@ "minimum": -2147483648, "type": "integer" }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, "protocol": { - "$ref": "#/components/schemas/Protocol" + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" }, "qualified_id": { - "$ref": "#/components/schemas/Qualified_ConvId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" }, "qualified_recipient": { - "$ref": "#/components/schemas/Qualified_UserId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" }, "qualified_target": { - "$ref": "#/components/schemas/Qualified_UserId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" }, "qualified_user_ids": { "items": { - "$ref": "#/components/schemas/Qualified_UserId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" }, "type": "array" }, "reason": { - "$ref": "#/components/schemas/EdMemberLeftReason" + "$ref": "#/components/schemas/EdMemberLeftReason_OTAyMDA4NzEw" }, "receipt_mode": { "description": "Conversation receipt mode", @@ -2443,7 +2600,7 @@ "type": "string" }, "status": { - "$ref": "#/components/schemas/TypingStatus" + "$ref": "#/components/schemas/TypingStatus_LTg5MzcyNDMy" }, "target": { "$ref": "#/components/schemas/UUID" @@ -2456,7 +2613,7 @@ "type": "string" }, "type": { - "$ref": "#/components/schemas/ConvType" + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" }, "uri": { "$ref": "#/components/schemas/HttpsUrl" @@ -2471,13 +2628,14 @@ }, "users": { "items": { - "$ref": "#/components/schemas/SimpleMember" + "$ref": "#/components/schemas/SimpleMember_NTY5MTcxMzcx" }, "type": "array" } }, "required": [ "users", + "add_type", "reason", "qualified_user_ids", "user_ids", @@ -2486,6 +2644,7 @@ "access", "key", "code", + "uri", "has_password", "qualified_id", "type", @@ -2499,7 +2658,10 @@ "sender", "recipient", "text", - "status" + "status", + "add_permission", + "depth", + "deletion_scheduled_for" ], "type": "object" }, @@ -2507,19 +2669,25 @@ "$ref": "#/components/schemas/UUID" }, "qualified_conversation": { - "$ref": "#/components/schemas/Qualified_ConvId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" }, "qualified_from": { - "$ref": "#/components/schemas/Qualified_UserId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" }, "subconv": { "type": "string" }, + "team": { + "$ref": "#/components/schemas/UUID" + }, "time": { "$ref": "#/components/schemas/UTCTimeMillis" }, "type": { - "$ref": "#/components/schemas/EventType" + "$ref": "#/components/schemas/EventType_LTQ3NTQyNDYz" + }, + "via": { + "$ref": "#/components/schemas/EventVia_Mjc4MzcyNzE0" } }, "required": [ @@ -2527,36 +2695,39 @@ "data", "qualified_conversation", "qualified_from", + "via", "time" ], "type": "object" }, - "EventType": { - "enum": [ - "conversation.member-join", - "conversation.member-leave", - "conversation.member-update", - "conversation.rename", - "conversation.access-update", - "conversation.receipt-mode-update", - "conversation.message-timer-update", - "conversation.code-update", - "conversation.code-delete", - "conversation.create", - "conversation.delete", - "conversation.connect-request", - "conversation.typing", - "conversation.otr-message-add", - "conversation.mls-message-add", - "conversation.mls-welcome", - "conversation.protocol-update" + "Featur_EnfocilDwdLCgBIy_MjcxMzc1NDA5": { + "properties": { + "config": { + "$ref": "#/components/schemas/EnforceFilDwadLtCgB_vIy_LTQ2NjU1Nzcx" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" ], - "type": "string" + "type": "object" }, - "ExposeInvitationURLsToTeamAdminConfig.Feature": { + "Featur_SlfDingMsCoBIdy_LTg2MzYzNjc2": { "properties": { + "config": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfigB_Covered_Identity_LTY0NzQ4MzU1" + }, "status": { - "$ref": "#/components/schemas/FeatureStatus" + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, "ttl": { "example": "unlimited", @@ -2566,17 +2737,18 @@ } }, "required": [ - "status" + "status", + "config" ], "type": "object" }, - "ExposeInvitationURLsToTeamAdminConfig.LockableFeature": { + "Featur_Vsiond_17PvAmlGpCfgBIy_LTIzMjQ4MDk1V17": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" + "config": { + "$ref": "#/components/schemas/Versiond_17_PvtAmlGupCfgBaIy_LTY1MTkyNDcw" }, "status": { - "$ref": "#/components/schemas/FeatureStatus" + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, "ttl": { "example": "unlimited", @@ -2587,30 +2759,87 @@ }, "required": [ "status", - "lockStatus" + "config" ], "type": "object" }, - "FeatureStatus": { + "FeatureStatus_LTMzMTUwODEw": { "enum": [ "enabled", "disabled" ], "type": "string" }, - "FederatedUserSearchPolicy": { - "description": "Search policy that was applied when searching for users", - "enum": [ - "no_search", - "exact_handle_search", - "full_search" + "Feature_AppLockConfigB_Bare_Identity_MTAzMjI5NDYy": { + "properties": { + "config": { + "$ref": "#/components/schemas/AppLockConfigB_Covered_Identity_NDIxOTc2Njkz" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" ], - "type": "string" + "type": "object" + }, + "Feature_CellsConfigB_Bare_Identity_NzU2NjgxNzEw": { + "properties": { + "config": { + "$ref": "#/components/schemas/CellsConfigB_Covered_Identity_LTE1NzkwOTcz" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "Feature_ChannelsConfigB_Bare_Identity_LTU4NTE4MTgx": { + "properties": { + "config": { + "$ref": "#/components/schemas/ChannelsConfigB_Covered_Identity_ODk2MTk3NDQ4" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" }, - "FileSharingConfig.Feature": { + "Feature_ConferenceCallingConfigB_Bare_Identity_Njc2NTcxNTI3": { "properties": { + "config": { + "$ref": "#/components/schemas/ConferenceCallingConfigB_Covered_Identity_NDMwMjcyMzM1" + }, "status": { - "$ref": "#/components/schemas/FeatureStatus" + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, "ttl": { "example": "unlimited", @@ -2624,13 +2853,81 @@ ], "type": "object" }, - "FileSharingConfig.LockableFeature": { + "Feature_ExposeInvitationURLsToTeamAdminConfig_LTY5NzY2Mzg5": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_FileSharingConfig_LTUyNjkxMzM4": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_GuestLinksConfig_NjQyMDMxNjg3": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_LegalholdConfig_NjM3MTkxNjYw": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_MLSConfigB_Bare_Identity_LTI5MjA3MDYy": { + "properties": { + "config": { + "$ref": "#/components/schemas/MLSConfigB_Covered_Identity_LTEzNTk3MzM5" }, "status": { - "$ref": "#/components/schemas/FeatureStatus" + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, "ttl": { "example": "unlimited", @@ -2641,10 +2938,146 @@ }, "required": [ "status", - "lockStatus" + "config" + ], + "type": "object" + }, + "Feature_MeetingsConfig_NDc2MzM0MDE1": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_MlsE2EIdConfigB_Bare_Identity_LTUxODYzODEx": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsE2EIdConfigB_Covered_Identity_NDQ0NjEwNzA3" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "Feature_MlsMigrationConfigB_Bare_Identity_LTQyMDAxMTkz": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsMigrationConfigB_Covered_Identity_LTM3NDQ4Mzg4" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "Feature_OutlookCalIntegrationConfig_LTg0MjIxNTMx": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_SearchVisibilityAvailableConfig_LTMzNTkxODI1": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_SearchVisibilityInboundConfig_MTI1NzQxODY2": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_SndFactorPasswordChallengeConfig_NDc0MzUyMzQz": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" ], "type": "object" }, + "FederatedUserSearchPolicy_MzkwODA4MTM3": { + "description": "Search policy that was applied when searching for users", + "enum": [ + "no_search", + "exact_handle_search", + "full_search" + ], + "type": "string" + }, "Fingerprint": { "example": "ioy3GeIjgQRsobf2EKGO3O8mq/FofFxHRqy0T4ERIZ8=", "type": "string" @@ -2660,7 +3093,46 @@ }, "type": "object" }, - "GetPaginated_Connections": { + "Frequency_Mzk0ODQwOTM3": { + "enum": [ + "daily", + "weekly", + "monthly", + "yearly" + ], + "type": "string" + }, + "GetByEmailReq_LTY4MzE3Njgy": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "GetByEmailResp_LTMxNTY3MjA0": { + "properties": { + "sso_code": { + "$ref": "#/components/schemas/UUID" + } + }, + "type": "object" + }, + "GetDomainRegistrationRequest_LTg4NTM1MzM2": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "GetMuliTabPgRqs_ConcLOrm501_LTIwMjA4NzYw": { "description": "A request to list some or all of a user's Connections, including remote ones", "properties": { "paging_state": { @@ -2676,7 +3148,7 @@ }, "type": "object" }, - "GetPaginated_ConversationIds": { + "GetMuliTabPgRqs_ConvrIdLcOm10_ODU0NjQ1NDMz": { "description": "A request to list some or all of a user's ConversationIds, including remote ones", "properties": { "paging_state": { @@ -2692,6 +3164,21 @@ }, "type": "object" }, + "GroupConvTypeLegacy_NTUxMDI2Mzkw": { + "enum": [ + "group_conversation", + "channel" + ], + "type": "string" + }, + "GroupConvType_LTU4NjU0MTY5": { + "enum": [ + "group_conversation", + "channel", + "meeting" + ], + "type": "string" + }, "GroupId": { "description": "A base64-encoded MLS group ID", "example": "ZXhhbXBsZQo=", @@ -2700,55 +3187,42 @@ "GroupInfoData": { "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." }, - "GuestLinksConfig.Feature": { + "Handle": { + "type": "string" + }, + "HandleUpdate_NTI4NDk1OTAx": { "properties": { - "status": { - "$ref": "#/components/schemas/FeatureStatus" - }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "handle": { + "type": "string" } }, "required": [ - "status" + "handle" ], "type": "object" }, - "GuestLinksConfig.LockableFeature": { + "History": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" - }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "depth": { + "$ref": "#/components/schemas/HistoryDuration" } }, "required": [ - "status", - "lockStatus" + "depth" ], "type": "object" }, - "Handle": { + "HistoryDuration": { "type": "string" }, - "HandleUpdate": { + "HistorySharingConfig_Mjc4MzA1Nzgw": { "properties": { - "handle": { - "type": "string" + "depth": { + "$ref": "#/components/schemas/HistoryDuration" } }, "required": [ - "handle" + "depth" ], "type": "object" }, @@ -2756,21 +3230,41 @@ "example": "https://example.com", "type": "string" }, - "ID_*_AuthnRequest": { + "HttpsUrl_HttpsUrl_NjUyMDgzNzk3": { "properties": { - "iD": { - "$ref": "#/components/schemas/XmlText" + "config_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "webapp_url": { + "$ref": "#/components/schemas/HttpsUrl" } }, "required": [ - "iD" + "config_url", + "webapp_url" + ], + "type": "object" + }, + "HttpsUrl_Maybe_HttpsUrl_LTQ1MDkyMzY2": { + "properties": { + "config_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "webapp_url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "config_url" ], "type": "object" }, "Icon": { + "description": "S3 asset key for an icon image with retention information. Allows special value 'default'.", + "example": "3-1-47de4580-ae51-4650-acbb-d10c028cb0ac", "type": "string" }, - "Id": { + "IdObject_ClientId_LTM3NjQyODM5": { "properties": { "id": { "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", @@ -2782,16 +3276,16 @@ ], "type": "object" }, - "IdPConfig_WireIdP": { + "IdPConfig_WireIdP_NDA5MTE4Mjk0": { "properties": { "extraInfo": { - "$ref": "#/components/schemas/WireIdP" + "$ref": "#/components/schemas/WireIdP_ODMzOTExMzYw" }, "id": { - "$ref": "#/components/schemas/UUID" + "$ref": "#/components/schemas/URI" }, "metadata": { - "$ref": "#/components/schemas/IdPMetadata" + "$ref": "#/components/schemas/IdPMetadata_MTI3NzE4MTA0" } }, "required": [ @@ -2805,7 +3299,7 @@ "properties": { "providers": { "items": { - "$ref": "#/components/schemas/IdPConfig_WireIdP" + "$ref": "#/components/schemas/IdPConfig_WireIdP_NDA5MTE4Mjk0" }, "type": "array" } @@ -2815,17 +3309,27 @@ ], "type": "object" }, - "IdPMetadata": { + "IdPMetadataInfo": { + "maxProperties": 1, + "minProperties": 1, + "properties": { + "value": { + "type": "string" + } + }, + "type": "object" + }, + "IdPMetadata_MTI3NzE4MTA0": { "properties": { "certAuthnResponse": { "items": { - "type": "string" + "$ref": "#/components/schemas/SignedCertificate" }, "minItems": 1, "type": "array" }, "issuer": { - "type": "string" + "$ref": "#/components/schemas/URI" }, "requestURI": { "type": "string" @@ -2838,18 +3342,66 @@ ], "type": "object" }, - "IdPMetadataInfo": { - "maxProperties": 1, - "minProperties": 1, + "Id_AuthnRequest": { "properties": { - "value": { + "iD": { + "type": "string" + } + }, + "required": [ + "iD" + ], + "type": "object" + }, + "InvitationList_ODk4NTQxODc3": { + "description": "A list of sent team invitations.", + "properties": { + "has_more": { + "description": "Indicator that the server has more invitations than returned.", + "type": "boolean" + }, + "invitations": { + "items": { + "$ref": "#/components/schemas/Invitation_NTkzMDYwODc1" + }, + "type": "array" + } + }, + "required": [ + "invitations", + "has_more" + ], + "type": "object" + }, + "InvitationRequest_LTcyMDIzNDc0": { + "description": "A request to join a team on Wire.", + "properties": { + "allow_existing": { + "description": "Whether invitations to existing users are allowed.", + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters).", + "maxLength": 128, + "minLength": 1, "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role_LTIzMjAzMjky" } }, + "required": [ + "email" + ], "type": "object" }, - "Invitation": { - "description": "An invitation to join a team on Wire", + "InvitationUserView_LTUyMTE3Nzkz": { "properties": { "created_at": { "$ref": "#/components/schemas/UTCTimeMillis" @@ -2857,12 +3409,18 @@ "created_by": { "$ref": "#/components/schemas/UUID" }, + "created_by_email": { + "$ref": "#/components/schemas/Email" + }, "email": { "$ref": "#/components/schemas/Email" }, "id": { "$ref": "#/components/schemas/UUID" }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy_NTI0ODc0NTQx" + }, "name": { "description": "Name of the invitee (1 - 128 characters)", "maxLength": 128, @@ -2870,13 +3428,13 @@ "type": "string" }, "role": { - "$ref": "#/components/schemas/Role" + "$ref": "#/components/schemas/Role_LTIzMjAzMjky" }, "team": { "$ref": "#/components/schemas/UUID" }, "url": { - "$ref": "#/components/schemas/URIRef Absolute" + "$ref": "#/components/schemas/URIRef_Absolute" } }, "required": [ @@ -2887,58 +3445,53 @@ ], "type": "object" }, - "InvitationList": { - "description": "A list of sent team invitations.", + "Invitation_NTkzMDYwODc1": { + "description": "An invitation to join a team on Wire. If invitee is invited from an existing personal account, inviter email is included.", "properties": { - "has_more": { - "description": "Indicator that the server has more invitations than returned.", - "type": "boolean" + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" }, - "invitations": { - "items": { - "$ref": "#/components/schemas/Invitation" - }, - "type": "array" - } - }, - "required": [ - "invitations", - "has_more" - ], - "type": "object" - }, - "InvitationRequest": { - "description": "A request to join a team on Wire.", - "properties": { "email": { "$ref": "#/components/schemas/Email" }, - "locale": { - "$ref": "#/components/schemas/Locale" + "id": { + "$ref": "#/components/schemas/UUID" }, "name": { - "description": "Name of the invitee (1 - 128 characters).", + "description": "Name of the invitee (1 - 128 characters)", "maxLength": 128, "minLength": 1, "type": "string" }, "role": { - "$ref": "#/components/schemas/Role" + "$ref": "#/components/schemas/Role_LTIzMjAzMjky" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "url": { + "$ref": "#/components/schemas/URIRef_Absolute" } }, "required": [ + "team", + "id", + "created_at", "email" ], "type": "object" }, - "InviteQualified": { + "InviteQualified_ODYyODIyNjYz": { "properties": { "conversation_role": { "$ref": "#/components/schemas/RoleName" }, "qualified_users": { "items": { - "$ref": "#/components/schemas/Qualified_UserId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" }, "minItems": 1, "type": "array" @@ -2949,7 +3502,7 @@ ], "type": "object" }, - "JoinConversationByCode": { + "JoinConversationByCode_NjgzMzM4Mjg5": { "description": "Request body for joining a conversation by code", "properties": { "code": { @@ -2962,9 +3515,6 @@ "maxLength": 1024, "minLength": 8, "type": "string" - }, - "uri": { - "$ref": "#/components/schemas/HttpsUrl" } }, "required": [ @@ -2973,25 +3523,21 @@ ], "type": "object" }, - "KeyPackage": { - "example": "a2V5IHBhY2thZ2UgZGF0YQo=", + "JoinType_LTY4MDg2MzA5": { + "enum": [ + "external_add", + "internal_add" + ], "type": "string" }, - "KeyPackageBundle": { - "properties": { - "key_packages": { - "items": { - "$ref": "#/components/schemas/KeyPackageBundleEntry" - }, - "type": "array" - } - }, - "required": [ - "key_packages" - ], + "KeyMap_Value_MzAxODEwOTgx": { "type": "object" }, - "KeyPackageBundleEntry": { + "KeyPackage": { + "example": "a2V5IHBhY2thZ2UgZGF0YQo=", + "type": "string" + }, + "KeyPackageBundleEntry_NDQ2MzQ2MzMz": { "properties": { "client": { "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", @@ -3019,15 +3565,11 @@ ], "type": "object" }, - "KeyPackageRef": { - "example": "ZXhhbXBsZQo=", - "type": "string" - }, - "KeyPackageUpload": { + "KeyPackageBundle_MjU2MjY0MDU2": { "properties": { "key_packages": { "items": { - "$ref": "#/components/schemas/KeyPackage" + "$ref": "#/components/schemas/KeyPackageBundleEntry_NDQ2MzQ2MzMz" }, "type": "array" } @@ -3037,78 +3579,50 @@ ], "type": "object" }, - "LHServiceStatus": { - "enum": [ - "configured", - "not_configured", - "disabled" - ], - "type": "string" - }, - "LegalholdConfig.Feature": { + "KeyPackageCount_LTYwNDg5MDcz": { "properties": { - "status": { - "$ref": "#/components/schemas/FeatureStatus" - }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, + "count": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, "type": "integer" } }, "required": [ - "status" + "count" ], "type": "object" }, - "LegalholdConfig.LockableFeature": { + "KeyPackageRef": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "KeyPackageUpload_NTQ2Mjk2NzEx": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" - }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "key_packages": { + "items": { + "$ref": "#/components/schemas/KeyPackage" + }, + "type": "array" } }, "required": [ - "status", - "lockStatus" + "key_packages" ], "type": "object" }, - "LimitedEventFanoutConfig.LockableFeature": { - "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" - }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "status", - "lockStatus" + "LHServiceStatus_ODc3NzE0Mjg3": { + "enum": [ + "configured", + "not_configured", + "disabled" ], - "type": "object" + "type": "string" }, "LimitedQualifiedUserIdList_500": { "properties": { "qualified_users": { "items": { - "$ref": "#/components/schemas/Qualified_UserId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" }, "type": "array" } @@ -3118,19 +3632,12 @@ ], "type": "object" }, - "List1": { - "items": { - "$ref": "#/components/schemas/ASCII" - }, - "minItems": 1, - "type": "array" - }, - "ListConversations": { + "ListConversations_MjkxMTIwODMz": { "description": "A request to list some of a user's conversations, including remote ones. Maximum 1000 qualified conversation IDs", "properties": { "qualified_ids": { "items": { - "$ref": "#/components/schemas/Qualified_ConvId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" }, "maxItems": 1000, "minItems": 1, @@ -3142,7 +3649,7 @@ ], "type": "object" }, - "ListType": { + "ListType_LTkyMDM4MzA1": { "description": "true if 'members' doesn't contain all team members", "enum": [ true, @@ -3150,18 +3657,18 @@ ], "type": "boolean" }, - "ListUsersById": { + "ListUsersById_LTQ5MTE3NDc0": { "properties": { "failed": { "items": { - "$ref": "#/components/schemas/Qualified_UserId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" }, "minItems": 1, "type": "array" }, "found": { "items": { - "$ref": "#/components/schemas/UserProfile" + "$ref": "#/components/schemas/UserProfile_LTQzMTQxMTE1" }, "type": "array" } @@ -3184,13 +3691,13 @@ "properties": { "qualified_handles": { "items": { - "$ref": "#/components/schemas/Qualified_Handle" + "$ref": "#/components/schemas/Qualified_Handle_Nzg0MDE3Nzk4" }, "type": "array" }, "qualified_ids": { "items": { - "$ref": "#/components/schemas/Qualified_UserId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" }, "type": "array" } @@ -3200,7 +3707,7 @@ "Locale": { "type": "string" }, - "LocaleUpdate": { + "LocaleUpdate_LTgzNjgyOTEw": { "properties": { "locale": { "$ref": "#/components/schemas/Locale" @@ -3211,82 +3718,112 @@ ], "type": "object" }, - "LockStatus": { + "LockStatus_LTIyMTU5OTkw": { "enum": [ "locked", "unlocked" ], "type": "string" }, - "Login": { + "LockableFeature_AllowedGlobalOperationsConfig_NjQ1MjA5MDYw": { "properties": { - "email": { - "$ref": "#/components/schemas/Email" + "config": { + "$ref": "#/components/schemas/AllowedGlobalOperationsConfig_MzAwOTU1MDkx" }, - "handle": { - "$ref": "#/components/schemas/Handle" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "label": { - "type": "string" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "password": { - "maxLength": 1024, - "minLength": 6, - "type": "string" + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "LockableFeature_AppLockConfigB_Bare_Identity_ODgzNDI0OTU5": { + "properties": { + "config": { + "$ref": "#/components/schemas/AppLockConfigB_Covered_Identity_NDIxOTc2Njkz" }, - "verification_code": { - "$ref": "#/components/schemas/ASCII" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "password" + "status", + "lockStatus", + "config" ], "type": "object" }, - "MLSConfig": { + "LockableFeature_AppsConfig_MzQyNTMxNTk5": { "properties": { - "allowedCipherSuites": { - "items": { - "$ref": "#/components/schemas/CipherSuiteTag" - }, - "type": "array" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "defaultCipherSuite": { - "$ref": "#/components/schemas/CipherSuiteTag" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "defaultProtocol": { - "$ref": "#/components/schemas/Protocol" + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_AssetAuditLogConfig_NDQ3MzcyMzk2": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "protocolToggleUsers": { - "description": "allowlist of users that may change protocols", - "items": { - "$ref": "#/components/schemas/UUID" - }, - "type": "array" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "supportedProtocols": { - "items": { - "$ref": "#/components/schemas/Protocol" - }, - "type": "array" + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "protocolToggleUsers", - "defaultProtocol", - "allowedCipherSuites", - "defaultCipherSuite", - "supportedProtocols" + "status", + "lockStatus" ], "type": "object" }, - "MLSConfig.Feature": { + "LockableFeature_BackgroundEffectsConfig_MTg1MTk3NTM5": { "properties": { - "config": { - "$ref": "#/components/schemas/MLSConfig" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, "status": { - "$ref": "#/components/schemas/FeatureStatus" + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, "ttl": { "example": "unlimited", @@ -3297,20 +3834,20 @@ }, "required": [ "status", - "config" + "lockStatus" ], "type": "object" }, - "MLSConfig.LockableFeature": { + "LockableFeature_CellsConfigB_Bare_Identity_LTgzMDA1NjI3": { "properties": { "config": { - "$ref": "#/components/schemas/MLSConfig" + "$ref": "#/components/schemas/CellsConfigB_Covered_Identity_LTE1NzkwOTcz" }, "lockStatus": { - "$ref": "#/components/schemas/LockStatus" + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, "status": { - "$ref": "#/components/schemas/FeatureStatus" + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, "ttl": { "example": "unlimited", @@ -3326,273 +3863,210 @@ ], "type": "object" }, - "MLSKeys": { + "LockableFeature_ChannelsConfigB_Bare_Identity_NzA2NDEyMDEw": { "properties": { - "ecdsa_secp256r1_sha256": { - "$ref": "#/components/schemas/SomeKey" + "config": { + "$ref": "#/components/schemas/ChannelsConfigB_Covered_Identity_ODk2MTk3NDQ4" }, - "ecdsa_secp384r1_sha384": { - "$ref": "#/components/schemas/SomeKey" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "ecdsa_secp521r1_sha512": { - "$ref": "#/components/schemas/SomeKey" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "ed25519": { - "$ref": "#/components/schemas/SomeKey" + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "ed25519", - "ecdsa_secp256r1_sha256", - "ecdsa_secp384r1_sha384", - "ecdsa_secp521r1_sha512" + "status", + "lockStatus", + "config" ], "type": "object" }, - "MLSKeysByPurpose": { + "LockableFeature_ChatBubblesConfig_NDgwMTQzNjI2": { "properties": { - "removal": { - "$ref": "#/components/schemas/MLSKeys" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "removal" + "status", + "lockStatus" ], "type": "object" }, - "MLSMessage": { - "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." - }, - "MLSMessageSendingStatus": { + "LockableFeature_ClassifiedDomainsConfig_LTY1ODQwODg1": { "properties": { - "events": { - "description": "A list of events caused by sending the message.", - "items": { - "$ref": "#/components/schemas/Event" - }, - "type": "array" + "config": { + "$ref": "#/components/schemas/ClassifiedDomainsConfig_LTg4MDcwMDg2" }, - "time": { - "$ref": "#/components/schemas/UTCTimeMillis" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "events", - "time" + "status", + "lockStatus", + "config" ], "type": "object" }, - "MLSOne2OneConversation_MLSPublicKey": { + "LockableFeature_ConsumableNotificationsConfig_MjUxMjczMjM0": { "properties": { - "conversation": { - "$ref": "#/components/schemas/Conversation" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "public_keys": { - "$ref": "#/components/schemas/MLSKeysByPurpose" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "conversation", - "public_keys" + "status", + "lockStatus" ], "type": "object" }, - "MLSPublicKey": { - "example": "ZXhhbXBsZQo=", - "type": "string" - }, - "MLSPublicKeys": { - "additionalProperties": { - "example": "ZXhhbXBsZQo=", - "type": "string" - }, - "description": "Mapping from signature scheme (tags) to public key data", - "example": { - "ecdsa_secp256r1_sha256": "ZXhhbXBsZQo=", - "ecdsa_secp384r1_sha384": "ZXhhbXBsZQo=", - "ecdsa_secp521r1_sha512": "ZXhhbXBsZQo=", - "ed25519": "ZXhhbXBsZQo=" - }, - "type": "object" - }, - "ManagedBy": { - "enum": [ - "wire", - "scim" - ], - "type": "string" - }, - "Member": { - "description": "The user ID of the requestor", + "LockableFeature_DigitalSignaturesConfig_ODM2MDA2NDU4": { "properties": { - "conversation_role": { - "$ref": "#/components/schemas/RoleName" - }, - "hidden": { - "type": "boolean" - }, - "hidden_ref": { - "type": "string" - }, - "id": { - "$ref": "#/components/schemas/UUID" - }, - "otr_archived": { - "type": "boolean" - }, - "otr_archived_ref": { - "type": "string" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "otr_muted_ref": { - "type": "string" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "otr_muted_status": { - "format": "int32", - "maximum": 2147483647, - "minimum": -2147483648, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, "type": "integer" - }, - "qualified_id": { - "$ref": "#/components/schemas/Qualified_UserId" - }, - "service": { - "$ref": "#/components/schemas/ServiceRef" - }, - "status": {}, - "status_ref": {}, - "status_time": {} + } }, "required": [ - "qualified_id" + "status", + "lockStatus" ], "type": "object" }, - "MemberUpdate": { + "LockableFeature_DomainRegistrationConfig_NzU0NjczNTE0": { "properties": { - "hidden": { - "type": "boolean" - }, - "hidden_ref": { - "type": "string" - }, - "otr_archived": { - "type": "boolean" - }, - "otr_archived_ref": { - "type": "string" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "otr_muted_ref": { - "type": "string" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "otr_muted_status": { - "format": "int32", - "maximum": 2147483647, - "minimum": -2147483648, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, "type": "integer" } }, + "required": [ + "status", + "lockStatus" + ], "type": "object" }, - "MemberUpdateData": { + "LockableFeature_FileSharingConfig_MjgwNjIzODEz": { "properties": { - "conversation_role": { - "$ref": "#/components/schemas/RoleName" - }, - "hidden": { - "type": "boolean" - }, - "hidden_ref": { - "type": "string" - }, - "otr_archived": { - "type": "boolean" - }, - "otr_archived_ref": { - "type": "string" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "otr_muted_ref": { - "type": "string" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "otr_muted_status": { - "format": "int32", - "maximum": 2147483647, - "minimum": -2147483648, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, "type": "integer" - }, - "qualified_target": { - "$ref": "#/components/schemas/Qualified_UserId" - }, - "target": { - "$ref": "#/components/schemas/UUID" } }, "required": [ - "qualified_target" + "status", + "lockStatus" ], "type": "object" }, - "MessageSendingStatus": { - "description": "The Proteus message sending status. It has these fields:\n- `time`: Time of sending message.\n- `missing`: Clients that the message /should/ have been encrypted for, but wasn't.\n- `redundant`: Clients that the message /should not/ have been encrypted for, but was.\n- `deleted`: Clients that were deleted.\n- `failed_to_send`: When message sending fails for some clients but succeeds for others, e.g., because a remote backend is unreachable, this field will contain the list of clients for which the message sending failed. This list should be empty when message sending is not even tried, like when some clients are missing.", + "LockableFeature_GuestLinksConfig_LTcwNjU0NDMw": { "properties": { - "deleted": { - "$ref": "#/components/schemas/QualifiedUserClients" - }, - "failed_to_confirm_clients": { - "$ref": "#/components/schemas/QualifiedUserClients" - }, - "failed_to_send": { - "$ref": "#/components/schemas/QualifiedUserClients" - }, - "missing": { - "$ref": "#/components/schemas/QualifiedUserClients" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "redundant": { - "$ref": "#/components/schemas/QualifiedUserClients" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "time": { - "$ref": "#/components/schemas/UTCTimeMillis" + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "time", - "missing", - "redundant", - "deleted", - "failed_to_send", - "failed_to_confirm_clients" + "status", + "lockStatus" ], "type": "object" }, - "MlsE2EIdConfig": { + "LockableFeature_LegalholdConfig_LTc5MTk5OTIw": { "properties": { - "acmeDiscoveryUrl": { - "$ref": "#/components/schemas/HttpsUrl" - }, - "crlProxy": { - "$ref": "#/components/schemas/HttpsUrl" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "useProxyOnMobile": { - "type": "boolean" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "verificationExpiration": { - "description": "When a client first tries to fetch or renew a certificate, they may need to login to an identity provider (IdP) depending on their IdP domain authentication policy. The user may have a grace period during which they can \"snooze\" this login. The duration of this grace period (in seconds) is set in the `verificationDuration` parameter, which is enforced separately by each client. After the grace period has expired, the client will not allow the user to use the application until they have logged to refresh the certificate. The default value is 1 day (86400s). The client enrolls using the Automatic Certificate Management Environment (ACME) protocol. The `acmeDiscoveryUrl` parameter must be set to the HTTPS URL of the ACME server discovery endpoint for this team. It is of the form \"https://acme.{backendDomain}/acme/{provisionerName}/discovery\". For example: `https://acme.example.com/acme/provisioner1/discovery`.", - "maximum": 9223372036854775807, - "minimum": -9223372036854775808, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, "type": "integer" } }, "required": [ - "verificationExpiration" + "status", + "lockStatus" ], "type": "object" }, - "MlsE2EIdConfig.Feature": { + "LockableFeature_LimitedEventFanoutConfig_MTg3ODM0NzU0": { "properties": { - "config": { - "$ref": "#/components/schemas/MlsE2EIdConfig" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, "status": { - "$ref": "#/components/schemas/FeatureStatus" + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, "ttl": { "example": "unlimited", @@ -3603,20 +4077,20 @@ }, "required": [ "status", - "config" + "lockStatus" ], "type": "object" }, - "MlsE2EIdConfig.LockableFeature": { + "LockableFeature_MLSConfigB_Bare_Identity_MTg1MTc0NTEw": { "properties": { "config": { - "$ref": "#/components/schemas/MlsE2EIdConfig" + "$ref": "#/components/schemas/MLSConfigB_Covered_Identity_LTEzNTk3MzM5" }, "lockStatus": { - "$ref": "#/components/schemas/LockStatus" + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, "status": { - "$ref": "#/components/schemas/FeatureStatus" + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, "ttl": { "example": "unlimited", @@ -3632,24 +4106,34 @@ ], "type": "object" }, - "MlsMigration": { + "LockableFeature_MeetingsConfig_LTE0OTQ5Nzcw": { "properties": { - "finaliseRegardlessAfter": { - "$ref": "#/components/schemas/UTCTime" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "startTime": { - "$ref": "#/components/schemas/UTCTime" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, + "required": [ + "status", + "lockStatus" + ], "type": "object" }, - "MlsMigration.Feature": { + "LockableFeature_MeetingsPremiumConfig_NDg1ODEyNTA1": { "properties": { - "config": { - "$ref": "#/components/schemas/MlsMigration" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, "status": { - "$ref": "#/components/schemas/FeatureStatus" + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, "ttl": { "example": "unlimited", @@ -3660,20 +4144,20 @@ }, "required": [ "status", - "config" + "lockStatus" ], "type": "object" }, - "MlsMigration.LockableFeature": { + "LockableFeature_MlsE2EIdConfigB_Bare_Identity_MTU2ODkyMDc4": { "properties": { "config": { - "$ref": "#/components/schemas/MlsMigration" + "$ref": "#/components/schemas/MlsE2EIdConfigB_Covered_Identity_NDQ0NjEwNzA3" }, "lockStatus": { - "$ref": "#/components/schemas/LockStatus" + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, "status": { - "$ref": "#/components/schemas/FeatureStatus" + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, "ttl": { "example": "unlimited", @@ -3689,3096 +4173,10586 @@ ], "type": "object" }, - "NameIDFormat": { - "enum": [ - "NameIDFUnspecified", - "NameIDFEmail", - "NameIDFX509", - "NameIDFWindows", - "NameIDFKerberos", - "NameIDFEntity", - "NameIDFPersistent", - "NameIDFTransient" + "LockableFeature_OutlookCalIntegrationConfig_NjQ0MzMyMzY0": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" ], - "type": "string" + "type": "object" }, - "NameIdPolicy": { + "LockableFeature_SSOConfig_NjcyMjU4MDY2": { "properties": { - "allowCreate": { - "type": "boolean" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "format": { - "$ref": "#/components/schemas/NameIDFormat" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "spNameQualifier": { - "$ref": "#/components/schemas/XmlText" + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "format", - "allowCreate" + "status", + "lockStatus" ], "type": "object" }, - "NewAssetToken": { + "LockableFeature_SearchVisibilityAvailableConfig_LTkxMTA5ODk5": { "properties": { - "token": { - "$ref": "#/components/schemas/ASCII" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "token" + "status", + "lockStatus" ], "type": "object" }, - "NewClient": { + "LockableFeature_SearchVisibilityInboundConfig_NzA5NzczNTgy": { "properties": { - "capabilities": { - "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", - "items": { - "$ref": "#/components/schemas/ClientCapability" - }, - "type": "array" - }, - "class": { - "$ref": "#/components/schemas/ClientClass" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "cookie": { - "description": "The cookie label, i.e. the label used when logging in.", - "type": "string" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "label": { - "type": "string" + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_SndFactorPasswordChallengeConfig_MjQ3NzQ2ODgx": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "lastkey": { - "$ref": "#/components/schemas/Prekey" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "mls_public_keys": { - "$ref": "#/components/schemas/MLSPublicKeys" - }, - "model": { - "type": "string" - }, - "password": { - "description": "The password of the authenticated user for verification. Note: Required for registration of the 2nd, 3rd, ... client.", - "maxLength": 1024, - "minLength": 6, - "type": "string" - }, - "prekeys": { - "description": "Prekeys for other clients to establish OTR sessions.", - "items": { - "$ref": "#/components/schemas/Prekey" - }, - "type": "array" + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_StealthUsersConfig_LTE1MTk2NzIz": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "type": { - "$ref": "#/components/schemas/ClientType" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "verification_code": { - "$ref": "#/components/schemas/ASCII" + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "prekeys", - "lastkey", - "type" + "status", + "lockStatus" ], "type": "object" }, - "NewConv": { - "description": "JSON object to create a new conversation. When using 'qualified_users' (preferred), you can omit 'users'", + "LockableFtur_CnfigBIdy_NzY1NDU5MDAy": { "properties": { - "access": { - "items": { - "$ref": "#/components/schemas/Access" - }, - "type": "array" + "config": { + "$ref": "#/components/schemas/ConferenceCallingConfigB_Covered_Identity_NDMwMjcyMzM1" }, - "access_role": { - "items": { - "$ref": "#/components/schemas/AccessRole" - }, - "type": "array" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "conversation_role": { - "$ref": "#/components/schemas/RoleName" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "message_timer": { - "description": "Per-conversation message timer", - "format": "int64", - "maximum": 9223372036854775807, - "minimum": -9223372036854775808, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFtur_CsInfigBdy_LTQ3MTU3Mjg0": { + "properties": { + "config": { + "$ref": "#/components/schemas/CellsInternalConfigB_Covered_Identity_LTUzMDkwOTAz" }, - "name": { - "maxLength": 256, - "minLength": 1, - "type": "string" - }, - "protocol": { - "$ref": "#/components/schemas/BaseProtocol" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "qualified_users": { - "description": "List of qualified user IDs (excluding the requestor) to be part of this conversation", - "items": { - "$ref": "#/components/schemas/Qualified_UserId" - }, - "type": "array" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "receipt_mode": { - "description": "Conversation receipt mode", - "format": "int32", - "maximum": 2147483647, - "minimum": -2147483648, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, "type": "integer" - }, - "team": { - "$ref": "#/components/schemas/ConvTeamInfo" - }, - "users": { - "deprecated": true, - "description": "List of user IDs (excluding the requestor) to be part of this conversation (deprecated)", - "items": { - "$ref": "#/components/schemas/UUID" - }, - "type": "array" } }, + "required": [ + "status", + "lockStatus", + "config" + ], "type": "object" }, - "NewLegalHoldService": { + "LockableFtur_EnfiDwdCgBIy_ODA5OTA5MTQ4": { "properties": { - "auth_token": { - "$ref": "#/components/schemas/ASCII" + "config": { + "$ref": "#/components/schemas/EnforceFilDwadLtCgB_vIy_LTQ2NjU1Nzcx" }, - "base_url": { - "$ref": "#/components/schemas/HttpsUrl" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "public_key": { - "$ref": "#/components/schemas/ServiceKeyPEM" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "base_url", - "public_key", - "auth_token" + "status", + "lockStatus", + "config" ], "type": "object" }, - "NewPasswordReset": { - "description": "Data to initiate a password reset", + "LockableFtur_ExpsInviURTmAdCfg_LTQzMzU2OTY1": { "properties": { - "email": { - "$ref": "#/components/schemas/Email" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "phone": { - "description": "Email", - "type": "string" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, + "required": [ + "status", + "lockStatus" + ], "type": "object" }, - "NewProvider": { + "LockableFtur_MsignCfBIdy_LTE1NjAxNjU2": { "properties": { - "description": { - "maxLength": 1024, - "minLength": 1, - "type": "string" - }, - "email": { - "$ref": "#/components/schemas/Email" + "config": { + "$ref": "#/components/schemas/MlsMigrationConfigB_Covered_Identity_LTM3NDQ4Mzg4" }, - "name": { - "maxLength": 128, - "minLength": 1, - "type": "string" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "password": { - "maxLength": 1024, - "minLength": 6, - "type": "string" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "url": { - "$ref": "#/components/schemas/HttpsUrl" + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "name", - "email", - "url", - "description" + "status", + "lockStatus", + "config" ], "type": "object" }, - "NewProviderResponse": { + "LockableFtur_PvnAdmisGpCfgBIy_MTQxOTU4Mzgw": { "properties": { - "id": { - "$ref": "#/components/schemas/UUID" + "config": { + "$ref": "#/components/schemas/PreventAdminlessGroupsConfigB_Covered_Identity_MjQ2NDYwMTk2" }, - "password": { - "maxLength": 1024, - "minLength": 8, - "type": "string" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "id" + "status", + "lockStatus", + "config" ], "type": "object" }, - "NewService": { + "LockableFtur_RqiExnmVfCg_NjUyMzgzNzY5": { "properties": { - "assets": { - "items": { - "$ref": "#/components/schemas/UserAsset" - }, - "type": "array" - }, - "auth_token": { - "$ref": "#/components/schemas/ASCII" - }, - "base_url": { - "$ref": "#/components/schemas/HttpsUrl" - }, - "description": { - "maxLength": 1024, - "minLength": 1, - "type": "string" - }, - "name": { - "maxLength": 128, - "minLength": 1, - "type": "string" - }, - "public_key": { - "$ref": "#/components/schemas/ServiceKeyPEM" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" }, - "summary": { - "maxLength": 128, - "minLength": 1, - "type": "string" + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" }, - "tags": { - "items": { - "$ref": "#/components/schemas/" - }, - "maxItems": 3, - "minItems": 1, - "type": "array" + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "name", - "summary", - "description", - "base_url", - "public_key", - "assets", - "tags" + "status", + "lockStatus" ], "type": "object" }, - "NewServiceResponse": { + "LockableFtur_SfDingMsCBIdy_LTg5MTEwNTA2": { "properties": { - "auth_token": { - "$ref": "#/components/schemas/ASCII" + "config": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfigB_Covered_Identity_LTY0NzQ4MzU1" }, - "id": { - "$ref": "#/components/schemas/UUID" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "id" + "status", + "lockStatus", + "config" ], "type": "object" }, - "NewTeamMember": { - "description": "Required data when creating new team members", + "LockableFtur_SimpfdUsCnRqQg_Njk5NjU4OTgy": { "properties": { - "member": { - "description": "the team member to add (the legalhold_status field must be null or missing!)", - "properties": { - "created_at": { - "$ref": "#/components/schemas/UTCTimeMillis" - }, - "created_by": { - "$ref": "#/components/schemas/UUID" - }, - "permissions": { - "$ref": "#/components/schemas/Permissions" - }, - "user": { - "$ref": "#/components/schemas/UUID" - } - }, - "required": [ - "user", - "permissions" - ], - "type": "object" + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "member" + "status", + "lockStatus" ], "type": "object" }, - "NewUser": { + "Login_LTgyNTIzMTM1": { "properties": { - "accent_id": { - "format": "int32", - "maximum": 2147483647, - "minimum": -2147483648, - "type": "integer" - }, - "assets": { - "items": { - "$ref": "#/components/schemas/UserAsset" - }, - "type": "array" - }, "email": { "$ref": "#/components/schemas/Email" }, - "email_code": { - "$ref": "#/components/schemas/ASCII" - }, - "expires_in": { - "maximum": 604800, - "minimum": 1, - "type": "integer" - }, - "invitation_code": { - "$ref": "#/components/schemas/ASCII" + "handle": { + "$ref": "#/components/schemas/Handle" }, "label": { "type": "string" }, - "locale": { - "$ref": "#/components/schemas/Locale" - }, - "managed_by": { - "$ref": "#/components/schemas/ManagedBy" - }, - "name": { - "maxLength": 128, - "minLength": 1, - "type": "string" - }, "password": { "maxLength": 1024, - "minLength": 8, + "minLength": 6, "type": "string" }, - "picture": { - "$ref": "#/components/schemas/Pict" - }, - "sso_id": { - "$ref": "#/components/schemas/UserSSOId" - }, - "supported_protocols": { - "items": { - "$ref": "#/components/schemas/BaseProtocol" - }, - "type": "array" - }, - "team": { - "$ref": "#/components/schemas/BindingNewTeamUser" - }, - "team_code": { + "verification_code": { "$ref": "#/components/schemas/ASCII" - }, - "team_id": { - "$ref": "#/components/schemas/UUID" - }, - "uuid": { - "$ref": "#/components/schemas/UUID" } }, "required": [ - "name" + "password" ], "type": "object" }, - "OAuthAccessTokenRequest": { + "MLSConfigB_Covered_Identity_LTEzNTk3MzM5": { + "description": "allowlist of users that may change protocols", "properties": { - "client_id": { - "$ref": "#/components/schemas/UUID" + "allowedCipherSuites": { + "items": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "type": "array" }, - "code": { - "$ref": "#/components/schemas/OAuthAuthorizationCode" + "defaultCipherSuite": { + "$ref": "#/components/schemas/CipherSuiteTag" }, - "code_verifier": { - "description": "The code verifier to complete the code challenge", - "maxLength": 128, - "minLength": 43, - "type": "string" + "defaultProtocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" }, - "grant_type": { - "$ref": "#/components/schemas/OAuthGrantType" + "groupInfoDiagnostics": { + "type": "boolean" }, - "redirect_uri": { - "$ref": "#/components/schemas/RedirectUrl" + "protocolToggleUsers": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "supportedProtocols": { + "items": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "type": "array" } }, "required": [ - "grant_type", - "client_id", - "code_verifier", - "code", - "redirect_uri" + "protocolToggleUsers", + "defaultProtocol", + "allowedCipherSuites", + "defaultCipherSuite", + "supportedProtocols" ], "type": "object" }, - "OAuthAccessTokenResponse": { + "MLSKeysByPurpose_MLSKeys_SomeKey_NjkxMjk0MTAx": { "properties": { - "access_token": { - "description": "The access token, which has a relatively short lifetime", - "type": "string" - }, - "expires_in": { - "description": "The lifetime of the access token in seconds", - "format": "int32", - "maximum": 2147483647, - "minimum": -2147483648, - "type": "integer" - }, - "refresh_token": { - "description": "The refresh token, which has a relatively long lifetime, and can be used to obtain a new access token", - "type": "string" - }, - "token_type": { - "$ref": "#/components/schemas/OAuthAccessTokenType" + "removal": { + "$ref": "#/components/schemas/MLSKeys_SomeKey_LTUzNDA5MzA3" } }, "required": [ - "access_token", - "token_type", - "expires_in", - "refresh_token" + "removal" ], "type": "object" }, - "OAuthAccessTokenType": { - "description": "The type of the access token. Currently only `Bearer` is supported.", - "enum": [ - "Bearer" - ], - "type": "string" - }, - "OAuthApplication": { + "MLSKeys_SomeKey_LTUzNDA5MzA3": { "properties": { - "id": { - "$ref": "#/components/schemas/UUID" + "ecdsa_secp256r1_sha256": { + "$ref": "#/components/schemas/SomeKey" }, - "name": { - "description": "The OAuth client's name", - "maxLength": 256, - "minLength": 6, - "type": "string" + "ecdsa_secp384r1_sha384": { + "$ref": "#/components/schemas/SomeKey" }, - "sessions": { - "description": "The OAuth client's sessions", - "items": { - "$ref": "#/components/schemas/OAuthSession" - }, - "type": "array" + "ecdsa_secp521r1_sha512": { + "$ref": "#/components/schemas/SomeKey" + }, + "ed25519": { + "$ref": "#/components/schemas/SomeKey" } }, "required": [ - "id", - "name", - "sessions" + "ed25519", + "ecdsa_secp256r1_sha256", + "ecdsa_secp384r1_sha384", + "ecdsa_secp521r1_sha512" ], "type": "object" }, - "OAuthAuthorizationCode": { - "description": "The authorization code", - "type": "string" + "MLSMessage": { + "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." }, - "OAuthClient": { + "MLSMessageSendingStatus_NjA1NDA0MTE4": { "properties": { - "application_name": { - "maxLength": 256, - "minLength": 6, - "type": "string" - }, - "client_id": { - "$ref": "#/components/schemas/UUID" + "events": { + "description": "A list of events caused by sending the message.", + "items": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + }, + "type": "array" }, - "redirect_url": { - "$ref": "#/components/schemas/RedirectUrl" + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" } }, "required": [ - "client_id", - "application_name", - "redirect_url" + "events", + "time" ], "type": "object" }, - "OAuthCodeChallenge": { - "description": "Generated by the client from the code verifier (unpadded base64url-encoded SHA256 hash of the code verifier)", - "type": "string" - }, - "OAuthGrantType": { - "description": "Indicates which authorization flow to use. Use `authorization_code` for authorization code flow.", - "enum": [ - "authorization_code", - "refresh_token" - ], - "type": "string" - }, - "OAuthRefreshAccessTokenRequest": { + "MLSOne2OneConversation_SomeKey_GroupConvType_LTg2MTcwMjY3": { "properties": { - "client_id": { - "$ref": "#/components/schemas/UUID" - }, - "grant_type": { - "$ref": "#/components/schemas/OAuthGrantType" + "conversation": { + "$ref": "#/components/schemas/v9_OwnConversation_GroupConvType_LTU2MzYxNTg0" }, - "refresh_token": { - "description": "The refresh token", - "type": "string" + "public_keys": { + "$ref": "#/components/schemas/MLSKeysByPurpose_MLSKeys_SomeKey_NjkxMjk0MTAx" } }, "required": [ - "grant_type", - "client_id", - "refresh_token" + "conversation", + "public_keys" ], "type": "object" }, - "OAuthResponseType": { - "description": "Indicates which authorization flow to use. Use `code` for authorization code flow.", + "MLSPublicKeys": { + "additionalProperties": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "description": "Mapping from signature scheme (tags) to public key data", + "example": { + "ecdsa_secp256r1_sha256": "ZXhhbXBsZQo=", + "ecdsa_secp384r1_sha384": "ZXhhbXBsZQo=", + "ecdsa_secp521r1_sha512": "ZXhhbXBsZQo=", + "ed25519": "ZXhhbXBsZQo=" + }, + "type": "object" + }, + "MLSReset_NzgwODA3ODc4": { + "properties": { + "epoch": { + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + } + }, + "required": [ + "group_id", + "epoch" + ], + "type": "object" + }, + "MTYxOTI3NjM3": { "enum": [ - "code" + "image" + ], + "type": "string" + }, + "ManagedBy_NTI0ODc0NTQx": { + "enum": [ + "wire", + "scim" ], "type": "string" }, - "OAuthRevokeRefreshTokenRequest": { + "MeetingEmailsInvitation_NzgyNzUzMzcz": { + "description": "Emails invitation", "properties": { - "client_id": { - "$ref": "#/components/schemas/UUID" - }, - "refresh_token": { - "description": "The refresh token", - "type": "string" + "emails": { + "items": { + "$ref": "#/components/schemas/Email" + }, + "type": "array" } }, "required": [ - "client_id", - "refresh_token" + "emails" ], "type": "object" }, - "OAuthSession": { + "MeetingWithConversation_LTMyNzA4NzU0": { + "description": "A scheduled meeting with its associated conversation", "properties": { + "conversation": { + "$ref": "#/components/schemas/Conversation_GroupConvType_MzQzMTQ1OTg3" + }, "created_at": { - "$ref": "#/components/schemas/UTCTimeMillis" + "$ref": "#/components/schemas/UTCTime" }, - "refresh_token_id": { - "$ref": "#/components/schemas/UUID" + "end_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "invited_emails": { + "items": { + "$ref": "#/components/schemas/Email" + }, + "type": "array" + }, + "qualified_conversation": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "qualified_creator": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Meeting_MjgxMDUyNjc3" + }, + "recurrence": { + "$ref": "#/components/schemas/Recurrence_LTQ0OTc0ODE2" + }, + "start_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "title": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "tzid": { + "$ref": "#/components/schemas/TimeZone" + }, + "updated_at": { + "$ref": "#/components/schemas/UTCTime" } }, "required": [ - "refresh_token_id", - "created_at" + "qualified_id", + "title", + "qualified_creator", + "start_time", + "end_time", + "tzid", + "qualified_conversation", + "invited_emails", + "created_at", + "updated_at", + "conversation" ], "type": "object" }, - "Object": { - "additionalProperties": true, - "description": "A single notification event", + "Meeting_ODU0OTMzMTgw": { + "description": "A scheduled meeting", "properties": { - "type": { - "description": "Event type", + "created_at": { + "$ref": "#/components/schemas/UTCTime" + }, + "end_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "invited_emails": { + "items": { + "$ref": "#/components/schemas/Email" + }, + "type": "array" + }, + "qualified_conversation": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "qualified_creator": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Meeting_MjgxMDUyNjc3" + }, + "recurrence": { + "$ref": "#/components/schemas/Recurrence_LTQ0OTc0ODE2" + }, + "start_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "title": { + "maxLength": 256, + "minLength": 1, "type": "string" + }, + "tzid": { + "$ref": "#/components/schemas/TimeZone" + }, + "updated_at": { + "$ref": "#/components/schemas/UTCTime" } }, - "title": "Event", + "required": [ + "qualified_id", + "title", + "qualified_creator", + "start_time", + "end_time", + "tzid", + "qualified_conversation", + "invited_emails", + "created_at", + "updated_at" + ], "type": "object" }, - "OtherMember": { + "MemberUpdateData_LTc3Nzc3NTEy": { "properties": { "conversation_role": { "$ref": "#/components/schemas/RoleName" }, - "id": { - "$ref": "#/components/schemas/UUID" + "hidden": { + "type": "boolean" }, - "qualified_id": { - "$ref": "#/components/schemas/Qualified_UserId" + "hidden_ref": { + "type": "string" }, - "service": { - "$ref": "#/components/schemas/ServiceRef" + "otr_archived": { + "type": "boolean" }, - "status": { - "deprecated": true, - "description": "deprecated", - "maximum": 9223372036854775807, - "minimum": -9223372036854775808, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, "type": "integer" + }, + "qualified_target": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "target": { + "$ref": "#/components/schemas/UUID" } }, "required": [ - "qualified_id" + "qualified_target" ], "type": "object" }, - "OtherMemberUpdate": { - "description": "Update user properties of other members relative to a conversation", + "MemberUpdate_LTg4NTQ0OTYz": { "properties": { - "conversation_role": { - "$ref": "#/components/schemas/RoleName" + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" } }, "type": "object" }, - "OtrMessage": { - "description": "Encrypted message of a conversation", + "Member_OTA5OTgyNzcw": { + "description": "The user ID of the requestor if the requestor is a member of the conversation", "properties": { - "data": { - "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", - "type": "string" + "conversation_role": { + "$ref": "#/components/schemas/RoleName" }, - "recipient": { - "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "hidden": { + "type": "boolean" + }, + "hidden_ref": { "type": "string" }, - "sender": { - "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "id": { + "$ref": "#/components/schemas/UUID" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { "type": "string" }, - "text": { - "description": "The ciphertext for the recipient (Base64 in JSON)", + "otr_muted_ref": { "type": "string" - } + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef_LTgxMjY3NzAz" + }, + "status": {}, + "status_ref": {}, + "status_time": {} }, "required": [ - "sender", - "recipient", - "text" + "qualified_id" ], "type": "object" }, - "OutlookCalIntegrationConfig.Feature": { + "MembersJoin_LTg0MDc1NjQ3": { "properties": { - "status": { - "$ref": "#/components/schemas/FeatureStatus" + "add_type": { + "$ref": "#/components/schemas/JoinType_LTY4MDg2MzA5" }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "user_ids": { + "deprecated": true, + "description": "deprecated", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/components/schemas/SimpleMember_NTY5MTcxMzcx" + }, + "type": "array" } }, "required": [ - "status" + "users", + "add_type" ], "type": "object" }, - "OutlookCalIntegrationConfig.LockableFeature": { + "MessageSendingStatus_ODg0NDgyNDk4": { + "description": "The Proteus message sending status. It has these fields:\n- `time`: Time of sending message.\n- `missing`: Clients that the message /should/ have been encrypted for, but wasn't.\n- `redundant`: Clients that the message /should not/ have been encrypted for, but was.\n- `deleted`: Clients that were deleted.\n- `failed_to_send`: When message sending fails for some clients but succeeds for others, e.g., because a remote backend is unreachable, this field will contain the list of clients for which the message sending failed. This list should be empty when message sending is not even tried, like when some clients are missing.", "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" + "deleted": { + "$ref": "#/components/schemas/QualifiedUserClients" }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" + "failed_to_confirm_clients": { + "$ref": "#/components/schemas/QualifiedUserClients" }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "failed_to_send": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "missing": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "redundant": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" } }, "required": [ - "status", - "lockStatus" + "time", + "missing", + "redundant", + "deleted", + "failed_to_send", + "failed_to_confirm_clients" ], "type": "object" }, - "OwnKeyPackages": { + "MlsE2EIdConfigB_Covered_Identity_NDQ0NjEwNzA3": { + "description": "When a client first tries to fetch or renew a certificate, they may need to login to an identity provider (IdP) depending on their IdP domain authentication policy. The user may have a grace period during which they can \"snooze\" this login. The duration of this grace period (in seconds) is set in the `verificationDuration` parameter, which is enforced separately by each client. After the grace period has expired, the client will not allow the user to use the application until they have logged to refresh the certificate. The default value is 1 day (86400s). The client enrolls using the Automatic Certificate Management Environment (ACME) protocol. The `acmeDiscoveryUrl` parameter must be set to the HTTPS URL of the ACME server discovery endpoint for this team. It is of the form \"https://acme.{backendDomain}/acme/{provisionerName}/discovery\". For example: `https://acme.example.com/acme/provisioner1/discovery`.", "properties": { - "count": { + "acmeDiscoveryUrl": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "crlProxy": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "useProxyOnMobile": { + "type": "boolean" + }, + "verificationExpiration": { "maximum": 9223372036854775807, "minimum": -9223372036854775808, "type": "integer" } }, "required": [ - "count" + "verificationExpiration" ], "type": "object" }, - "PagingState": { - "description": "Paging state that should be supplied to retrieve the next page of results", - "type": "string" - }, - "PasswordChange": { + "MlsMigrationConfigB_Covered_Identity_LTM3NDQ4Mzg4": { "properties": { - "new_password": { - "maxLength": 1024, - "minLength": 6, + "allowManualMigration": { + "type": "boolean" + }, + "finaliseRegardlessAfter": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", "type": "string" }, - "old_password": { - "maxLength": 1024, - "minLength": 6, + "startTime": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", "type": "string" } }, - "required": [ - "old_password", - "new_password" - ], "type": "object" }, - "PasswordReqBody": { + "MultiTabePg_ConcsLOrRmU_MTgwMzU4OTU5": { "properties": { - "password": { - "maxLength": 1024, - "minLength": 6, - "type": "string" + "connections": { + "items": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + }, + "type": "array" + }, + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/Connections_PagingState" } }, + "required": [ + "connections", + "has_more", + "paging_state" + ], "type": "object" }, - "PasswordReset": { + "MultiTabePg_ConvrsIdqf_cLORm_Q_Mzg0MjYzNzM0": { "properties": { - "email": { - "$ref": "#/components/schemas/Email" + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/ConversationIds_PagingState" + }, + "qualified_conversations": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "type": "array" } }, "required": [ - "email" + "qualified_conversations", + "has_more", + "paging_state" ], "type": "object" }, - "Permissions": { - "description": "This is just a complicated way of representing a team role. self and copy always have to contain the same integer, and only the following integers are allowed: 1025 (partner), 1587 (member), 5951 (admin), 8191 (owner). Unit tests of the galley-types package in wire-server contain an authoritative list.", + "NP_LockableFtur_ghdCnfiSOVsyAvIRqExmDpB_MGwUT2Q_LTM1Mzc0ODgy": { "properties": { - "copy": { - "format": "int64", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "allowedGlobalOperations": { + "$ref": "#/components/schemas/LockableFeature_AllowedGlobalOperationsConfig_NjQ1MjA5MDYw" }, - "self": { - "format": "int64", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "appLock": { + "$ref": "#/components/schemas/LockableFeature_AppLockConfigB_Bare_Identity_ODgzNDI0OTU5" + }, + "apps": { + "$ref": "#/components/schemas/LockableFeature_AppsConfig_MzQyNTMxNTk5" + }, + "assetAuditLog": { + "$ref": "#/components/schemas/LockableFeature_AssetAuditLogConfig_NDQ3MzcyMzk2" + }, + "backgroundEffects": { + "$ref": "#/components/schemas/LockableFeature_BackgroundEffectsConfig_MTg1MTk3NTM5" + }, + "cells": { + "$ref": "#/components/schemas/LockableFeature_CellsConfigB_Bare_Identity_LTgzMDA1NjI3" + }, + "cellsInternal": { + "$ref": "#/components/schemas/LockableFtur_CsInfigBdy_LTQ3MTU3Mjg0" + }, + "channels": { + "$ref": "#/components/schemas/LockableFeature_ChannelsConfigB_Bare_Identity_NzA2NDEyMDEw" + }, + "chatBubbles": { + "$ref": "#/components/schemas/LockableFeature_ChatBubblesConfig_NDgwMTQzNjI2" + }, + "classifiedDomains": { + "$ref": "#/components/schemas/LockableFeature_ClassifiedDomainsConfig_LTY1ODQwODg1" + }, + "conferenceCalling": { + "$ref": "#/components/schemas/LockableFtur_CnfigBIdy_NzY1NDU5MDAy" + }, + "consumableNotifications": { + "$ref": "#/components/schemas/LockableFeature_ConsumableNotificationsConfig_MjUxMjczMjM0" + }, + "conversationGuestLinks": { + "$ref": "#/components/schemas/LockableFeature_GuestLinksConfig_LTcwNjU0NDMw" + }, + "digitalSignatures": { + "$ref": "#/components/schemas/LockableFeature_DigitalSignaturesConfig_ODM2MDA2NDU4" + }, + "domainRegistration": { + "$ref": "#/components/schemas/LockableFeature_DomainRegistrationConfig_NzU0NjczNTE0" + }, + "enforceFileDownloadLocation": { + "$ref": "#/components/schemas/LockableFtur_EnfiDwdCgBIy_ODA5OTA5MTQ4" + }, + "exposeInvitationURLsToTeamAdmin": { + "$ref": "#/components/schemas/LockableFtur_ExpsInviURTmAdCfg_LTQzMzU2OTY1" + }, + "fileSharing": { + "$ref": "#/components/schemas/LockableFeature_FileSharingConfig_MjgwNjIzODEz" + }, + "legalhold": { + "$ref": "#/components/schemas/LockableFeature_LegalholdConfig_LTc5MTk5OTIw" + }, + "limitedEventFanout": { + "$ref": "#/components/schemas/LockableFeature_LimitedEventFanoutConfig_MTg3ODM0NzU0" + }, + "meetings": { + "$ref": "#/components/schemas/LockableFeature_MeetingsConfig_LTE0OTQ5Nzcw" + }, + "meetingsPremium": { + "$ref": "#/components/schemas/LockableFeature_MeetingsPremiumConfig_NDg1ODEyNTA1" + }, + "mls": { + "$ref": "#/components/schemas/LockableFeature_MLSConfigB_Bare_Identity_MTg1MTc0NTEw" + }, + "mlsE2EId": { + "$ref": "#/components/schemas/LockableFeature_MlsE2EIdConfigB_Bare_Identity_MTU2ODkyMDc4" + }, + "mlsMigration": { + "$ref": "#/components/schemas/LockableFtur_MsignCfBIdy_LTE1NjAxNjU2" + }, + "outlookCalIntegration": { + "$ref": "#/components/schemas/LockableFeature_OutlookCalIntegrationConfig_NjQ0MzMyMzY0" + }, + "preventAdminlessGroups": { + "$ref": "#/components/schemas/LockableFtur_PvnAdmisGpCfgBIy_MTQxOTU4Mzgw" + }, + "searchVisibility": { + "$ref": "#/components/schemas/LockableFeature_SearchVisibilityAvailableConfig_LTkxMTA5ODk5" + }, + "searchVisibilityInbound": { + "$ref": "#/components/schemas/LockableFeature_SearchVisibilityInboundConfig_NzA5NzczNTgy" + }, + "selfDeletingMessages": { + "$ref": "#/components/schemas/LockableFtur_SfDingMsCBIdy_LTg5MTEwNTA2" + }, + "simplifiedUserConnectionRequestQRCode": { + "$ref": "#/components/schemas/LockableFtur_SimpfdUsCnRqQg_Njk5NjU4OTgy" + }, + "sndFactorPasswordChallenge": { + "$ref": "#/components/schemas/LockableFeature_SndFactorPasswordChallengeConfig_MjQ3NzQ2ODgx" + }, + "sso": { + "$ref": "#/components/schemas/LockableFeature_SSOConfig_NjcyMjU4MDY2" + }, + "stealthUsers": { + "$ref": "#/components/schemas/LockableFeature_StealthUsersConfig_LTE1MTk2NzIz" + }, + "validateSAMLemails": { + "$ref": "#/components/schemas/LockableFtur_RqiExnmVfCg_NjUyMzgzNzY5" } }, "required": [ - "self", - "copy" + "legalhold", + "sso", + "searchVisibility", + "searchVisibilityInbound", + "validateSAMLemails", + "digitalSignatures", + "appLock", + "fileSharing", + "classifiedDomains", + "conferenceCalling", + "selfDeletingMessages", + "conversationGuestLinks", + "sndFactorPasswordChallenge", + "mls", + "exposeInvitationURLsToTeamAdmin", + "outlookCalIntegration", + "mlsE2EId", + "mlsMigration", + "enforceFileDownloadLocation", + "limitedEventFanout", + "domainRegistration", + "channels", + "preventAdminlessGroups", + "cells", + "allowedGlobalOperations", + "consumableNotifications", + "chatBubbles", + "apps", + "simplifiedUserConnectionRequestQRCode", + "assetAuditLog", + "stealthUsers", + "cellsInternal", + "meetings", + "meetingsPremium", + "backgroundEffects" ], "type": "object" }, - "PhoneNumber": { - "description": "A known phone number with a pending password reset.", + "NameIDFormat": { + "enum": [ + "NameIDFUnspecified", + "NameIDFEmail", + "NameIDFX509", + "NameIDFWindows", + "NameIDFKerberos", + "NameIDFEntity", + "NameIDFPersistent", + "NameIDFTransient" + ], "type": "string" }, - "Pict": { - "items": { - "type": "object" - }, - "maxItems": 10, - "minItems": 0, - "type": "array" - }, - "Prekey": { + "NameIdPolicy": { "properties": { - "id": { - "maximum": 65535, - "minimum": 0, - "type": "integer" + "allowCreate": { + "type": "boolean" }, - "key": { + "format": { + "$ref": "#/components/schemas/NameIDFormat" + }, + "spNameQualifier": { "type": "string" } }, "required": [ - "id", - "key" + "format", + "allowCreate" ], "type": "object" }, - "PrekeyBundle": { + "NewApp_LTQwODMwMzQ4": { "properties": { - "clients": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { "items": { - "$ref": "#/components/schemas/ClientPrekey" + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" }, "type": "array" }, - "user": { - "$ref": "#/components/schemas/UUID" + "category": { + "description": "Category name (if uncertain, pick \"other\")", + "type": "string" + }, + "description": { + "maxLength": 300, + "minLength": 0, + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" } }, "required": [ - "user", - "clients" - ], - "type": "object" - }, - "Priority": { - "enum": [ - "low", - "high" + "name", + "category", + "description", + "password" ], - "type": "string" - }, - "PropertyKeysAndValues": { "type": "object" }, - "PropertyValue": { - "description": "An arbitrary JSON value for a property" - }, - "Protocol": { - "enum": [ - "proteus", - "mls", - "mixed" - ], - "type": "string" - }, - "ProtocolUpdate": { + "NewAssetToken_NTAwMDQwODYy": { "properties": { - "protocol": { - "$ref": "#/components/schemas/Protocol" + "token": { + "$ref": "#/components/schemas/ASCII" } }, + "required": [ + "token" + ], "type": "object" }, - "Provider": { + "NewClient_ODg1NjY4Njgy": { "properties": { - "description": { - "type": "string" + "capabilities": { + "$ref": "#/components/schemas/ClientCapabilityList" }, - "email": { - "$ref": "#/components/schemas/Email" + "class": { + "$ref": "#/components/schemas/ClientClass_NjE3MDgwNzcx" }, - "id": { - "$ref": "#/components/schemas/UUID" + "cookie": { + "description": "The cookie label, i.e. the label used when logging in.", + "type": "string" }, - "name": { - "maxLength": 128, - "minLength": 1, + "label": { "type": "string" }, - "url": { - "$ref": "#/components/schemas/HttpsUrl" - } - }, - "required": [ - "id", - "name", - "email", - "url", - "description" - ], - "type": "object" - }, - "ProviderActivationResponse": { - "properties": { - "email": { - "$ref": "#/components/schemas/Email" - } - }, - "required": [ - "email" - ], - "type": "object" - }, - "ProviderLogin": { - "properties": { - "email": { - "$ref": "#/components/schemas/Email" + "lastkey": { + "$ref": "#/components/schemas/UncheckedPrekeyBundle_LTU1MzQzOTgy" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "model": { + "type": "string" }, "password": { + "description": "The password of the authenticated user for verification. Note: Required for registration of the 2nd, 3rd, ... client.", "maxLength": 1024, "minLength": 6, "type": "string" - } - }, - "required": [ - "email", - "password" - ], - "type": "object" - }, - "PubClient": { - "properties": { - "class": { - "$ref": "#/components/schemas/ClientClass" }, - "id": { - "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", - "type": "string" + "prekeys": { + "description": "Prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/components/schemas/UncheckedPrekeyBundle_LTU1MzQzOTgy" + }, + "type": "array" + }, + "type": { + "$ref": "#/components/schemas/ClientType_MjQ0OTQwMzcw" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" } }, "required": [ - "id" + "prekeys", + "lastkey", + "type" ], "type": "object" }, - "PublicSubConversation": { - "description": "An MLS subconversation", + "NewConv_LTgzNTk1NDQx": { + "description": "JSON object to create a new conversation. When using 'qualified_users' (preferred), you can omit 'users'", "properties": { - "cipher_suite": { - "$ref": "#/components/schemas/CipherSuiteTag" + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" }, - "epoch": { - "description": "The epoch number of the corresponding MLS group", + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells": { + "type": "boolean" + }, + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "message_timer": { + "description": "Per-conversation message timer", "format": "int64", - "maximum": 18446744073709551615, - "minimum": 0, + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, "type": "integer" }, - "epoch_timestamp": { - "$ref": "#/components/schemas/UTCTime" + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" }, - "group_id": { - "$ref": "#/components/schemas/GroupId" + "parent": { + "$ref": "#/components/schemas/UUID" }, - "members": { + "protocol": { + "$ref": "#/components/schemas/BaseProtocolTag_LTM0MDE1NTEx" + }, + "qualified_users": { + "description": "List of qualified user IDs (excluding the requestor) to be part of this conversation", "items": { - "$ref": "#/components/schemas/ClientIdentity" + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" }, "type": "array" }, - "parent_qualified_id": { - "$ref": "#/components/schemas/Qualified_ConvId" + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" }, - "subconv_id": { - "type": "string" + "skip_creator": { + "description": "Don't add creator to the conversation, only works for team admins not wanting to be part of the channels they create.", + "type": "boolean" + }, + "team": { + "$ref": "#/components/schemas/ConvTeamInfo_Mzc5NjcyNjAz" + }, + "users": { + "deprecated": true, + "description": "List of user IDs (excluding the requestor) to be part of this conversation (deprecated)", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" } }, - "required": [ - "parent_qualified_id", - "subconv_id", - "group_id", - "epoch", - "members" - ], "type": "object" }, - "PushToken": { - "description": "Native Push Token", + "NewLegalHoldService_Mzg0ODQ5NDU1": { "properties": { - "app": { - "description": "Application", - "type": "string" - }, - "client": { - "description": "Client ID", - "type": "string" + "auth_token": { + "$ref": "#/components/schemas/ASCII" }, - "token": { - "description": "Access Token", - "type": "string" + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" }, - "transport": { - "$ref": "#/components/schemas/Transport" + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" } }, "required": [ - "transport", - "app", - "token", - "client" + "base_url", + "public_key", + "auth_token" ], "type": "object" }, - "PushTokenList": { - "description": "List of Native Push Tokens", + "NewMeeting_LTI1NTMzOTU5": { + "description": "Request to create a new meeting", "properties": { - "tokens": { - "description": "Push tokens", + "end_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "invited_emails": { "items": { - "$ref": "#/components/schemas/PushToken" + "$ref": "#/components/schemas/Email" }, "type": "array" + }, + "recurrence": { + "$ref": "#/components/schemas/Recurrence_LTQ0OTc0ODE2" + }, + "start_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "title": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "tzid": { + "$ref": "#/components/schemas/TimeZone" } }, "required": [ - "tokens" + "start_time", + "end_time", + "tzid", + "title" ], "type": "object" }, - "QualifiedNewOtrMessage": { - "description": "This object can only be parsed from Protobuf.\nThe specification for the protobuf types is here: \nhttps://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto." - }, - "QualifiedUserClientPrekeyMapV4": { + "NewOne2OneConv_LTI3OTc4NDAz": { + "description": "JSON object to create a new 1:1 conversation. When using 'qualified_users' (preferred), you can omit 'users'", "properties": { - "failed_to_list": { + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "qualified_users": { + "description": "List of qualified user IDs (excluding the requestor) to be part of this conversation", "items": { - "$ref": "#/components/schemas/Qualified_UserId" + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" }, "type": "array" }, - "qualified_user_client_prekeys": { - "additionalProperties": { - "$ref": "#/components/schemas/UserClientPrekeyMap" - }, - "type": "object" - } - }, - "required": [ - "qualified_user_client_prekeys" - ], - "type": "object" - }, - "QualifiedUserClients": { - "additionalProperties": { - "additionalProperties": { + "team": { + "$ref": "#/components/schemas/ConvTeamInfo_Mzc5NjcyNjAz" + }, + "users": { + "deprecated": true, + "description": "List of user IDs (excluding the requestor) to be part of this conversation (deprecated)", "items": { - "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", - "type": "string" + "$ref": "#/components/schemas/UUID" }, "type": "array" - }, - "type": "object" - }, - "description": "Map of Domain to UserClients", - "example": { - "domain1.example.com": { - "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ - "60f85e4b15ad3786", - "6e323ab31554353b" - ] } }, "type": "object" }, - "QualifiedUserIdList with EdMemberLeftReason": { + "NewOtrMessage_LTUyMTE5MTMw": { "properties": { - "qualified_user_ids": { - "items": { - "$ref": "#/components/schemas/Qualified_UserId" - }, - "type": "array" + "data": { + "type": "string" }, - "reason": { - "$ref": "#/components/schemas/EdMemberLeftReason" + "native_priority": { + "$ref": "#/components/schemas/Priority_ODA3NDM3MDYy" }, - "user_ids": { - "deprecated": true, - "description": "Deprecated, use qualified_user_ids", + "native_push": { + "type": "boolean" + }, + "recipients": { + "$ref": "#/components/schemas/UserClientMap" + }, + "report_missing": { "items": { "$ref": "#/components/schemas/UUID" }, "type": "array" + }, + "sender": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "transient": { + "type": "boolean" } }, "required": [ - "reason", - "qualified_user_ids", - "user_ids" + "sender", + "recipients" ], "type": "object" }, - "QualifiedUserMap_Set_PubClient": { - "additionalProperties": { - "$ref": "#/components/schemas/UserMap_Set_PubClient" - }, - "description": "Map of Domain to (UserMap (Set_PubClient)).", - "example": { - "domain1.example.com": { - "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ - { - "class": "legalhold", - "id": "d0" - } - ] + "NewPasswordReset_LTEyNzAxMTcy": { + "description": "Data to initiate a password reset", + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "phone": { + "description": "Email", + "type": "string" } }, "type": "object" }, - "Qualified_ConvId": { + "NewProviderResponse_OTE0ODI2NjU0": { "properties": { - "domain": { - "$ref": "#/components/schemas/Domain" - }, "id": { "$ref": "#/components/schemas/UUID" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" } }, "required": [ - "domain", "id" ], "type": "object" }, - "Qualified_Handle": { + "NewProvider_LTEyMTY5MjYy": { "properties": { - "domain": { - "$ref": "#/components/schemas/Domain" + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" }, - "handle": { - "$ref": "#/components/schemas/Handle" + "email": { + "$ref": "#/components/schemas/Email" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" } }, "required": [ - "domain", - "handle" + "name", + "email", + "url", + "description" ], "type": "object" }, - "Qualified_UserId": { + "NewServiceResponse_LTExMzcwMjg5": { "properties": { - "domain": { - "$ref": "#/components/schemas/Domain" + "auth_token": { + "$ref": "#/components/schemas/ASCII" }, "id": { "$ref": "#/components/schemas/UUID" } }, "required": [ - "domain", "id" ], "type": "object" }, - "QueuedNotification": { - "description": "A single notification", + "NewService_LTYwOTU1MDQ3": { "properties": { - "id": { - "$ref": "#/components/schemas/UUID" - }, - "payload": { - "description": "List of events", + "assets": { "items": { - "$ref": "#/components/schemas/Object" + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" }, - "minItems": 1, "type": "array" - } - }, - "required": [ - "id", - "payload" - ], - "type": "object" - }, - "QueuedNotificationList": { - "description": "Zero or more notifications", - "properties": { - "has_more": { - "description": "Whether there are still more notifications.", - "type": "boolean" }, - "notifications": { - "description": "Notifications", - "items": { - "$ref": "#/components/schemas/QueuedNotification" - }, - "type": "array" + "auth_token": { + "$ref": "#/components/schemas/ASCII" }, - "time": { - "$ref": "#/components/schemas/UTCTime" - } - }, - "required": [ - "notifications" - ], - "type": "object" - }, - "RTCConfiguration": { - "description": "A subset of the WebRTC 'RTCConfiguration' dictionary", - "properties": { - "ice_servers": { - "description": "Array of 'RTCIceServer' objects", - "items": { - "$ref": "#/components/schemas/RTCIceServer" - }, - "minItems": 1, - "type": "array" + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" }, - "is_federating": { - "description": "True if the client should connect to an SFT in the sft_servers_all and request it to federate", - "type": "boolean" + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" }, - "sft_servers": { - "description": "Array of 'SFTServer' objects (optional)", - "items": { - "$ref": "#/components/schemas/SftServer" - }, - "minItems": 1, - "type": "array" + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" }, - "sft_servers_all": { - "description": "Array of all SFT servers", + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "summary": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "tags": { "items": { - "$ref": "#/components/schemas/SftServer" + "$ref": "#/components/schemas/ServiceTag_LTMyNTEzNjYy" }, + "maxItems": 3, + "minItems": 1, "type": "array" - }, - "ttl": { - "description": "Number of seconds after which the configuration should be refreshed (advisory)", - "format": "int32", - "maximum": 4294967295, - "minimum": 0, - "type": "integer" } }, "required": [ - "ice_servers", - "ttl" + "name", + "summary", + "description", + "base_url", + "public_key", + "assets", + "tags" ], "type": "object" }, - "RTCIceServer": { - "description": "A subset of the WebRTC 'RTCIceServer' object", + "NewTeamCollaborator_LTIxNjEzMTYw": { "properties": { - "credential": { - "$ref": "#/components/schemas/ASCII" - }, - "urls": { - "description": "Array of TURN server addresses of the form 'turn::'", + "permissions": { "items": { - "$ref": "#/components/schemas/TurnURI" + "$ref": "#/components/schemas/CollaboratorPermission_NDg5NTg2ODgy" }, - "minItems": 1, "type": "array" }, - "username": { - "$ref": "#/components/schemas/" + "user": { + "$ref": "#/components/schemas/UUID" } }, "required": [ - "urls", - "username", - "credential" + "user", + "permissions" ], "type": "object" }, - "RedirectUrl": { - "description": "The URL must match the URL that was used to generate the authorization code.", - "type": "string" - }, - "Relation": { - "enum": [ - "accepted", - "blocked", - "pending", - "ignored", - "sent", - "cancelled", - "missing-legalhold-consent" - ], - "type": "string" - }, - "RemoveBotResponse": { + "NewTeamMember_Required_LTg2NjU5OTI2": { + "description": "Required data when creating new team members", "properties": { - "event": { - "$ref": "#/components/schemas/Event" + "member": { + "description": "the team member to add (the legalhold_status field must be null or missing!)", + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions_NDE0ODM5NDUx" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" } }, "required": [ - "event" + "member" ], "type": "object" }, - "RemoveCookies": { - "description": "Data required to remove cookies", + "NewUserGroup_MzYxODU0OTU1": { "properties": { - "ids": { - "description": "A list of cookie IDs to revoke", - "items": { - "format": "int32", - "maximum": 4294967295, - "minimum": 0, - "type": "integer" - }, - "type": "array" - }, - "labels": { - "description": "A list of cookie labels for which to revoke the cookies", + "members": { "items": { - "type": "string" + "$ref": "#/components/schemas/UUID" }, "type": "array" }, - "password": { - "description": "The user's password", - "maxLength": 1024, - "minLength": 6, + "name": { + "maxLength": 4000, + "minLength": 1, "type": "string" } }, "required": [ - "password" + "name", + "members" ], "type": "object" }, - "RemoveLegalHoldSettingsRequest": { - "properties": { - "password": { - "maxLength": 1024, - "minLength": 6, - "type": "string" - } - }, - "type": "object" - }, - "RichField": { + "NewUser_PlainTextPassword_8_LTI4MzI5NzQx": { "properties": { - "type": { - "type": "string" + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" }, - "value": { - "type": "string" - } - }, - "required": [ - "type", - "value" - ], - "type": "object" - }, - "RichInfoAssocList": { - "description": "json object with case-insensitive fields.", - "properties": { - "fields": { + "assets": { "items": { - "$ref": "#/components/schemas/RichField" + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" }, "type": "array" }, - "version": { - "maximum": 9223372036854775807, - "minimum": -9223372036854775808, + "email": { + "$ref": "#/components/schemas/Email" + }, + "email_code": { + "$ref": "#/components/schemas/ASCII" + }, + "expires_in": { + "maximum": 604800, + "minimum": 1, "type": "integer" + }, + "invitation_code": { + "$ref": "#/components/schemas/ASCII" + }, + "label": { + "type": "string" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy_NTI0ODc0NTQx" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict_DEPRECATED_USE_ASSETS_INSTEAD" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocolTag_LTM0MDE1NTEx" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/BindingNewTeamUser_LTY0MDQxMDEw" + }, + "team_code": { + "$ref": "#/components/schemas/ASCII" + }, + "team_id": { + "$ref": "#/components/schemas/UUID" + }, + "uuid": { + "$ref": "#/components/schemas/UUID" } }, "required": [ - "version", - "fields" + "name" ], "type": "object" }, - "Role": { - "description": "Role of the invited user", - "enum": [ - "owner", - "admin", - "member", - "partner" - ], - "type": "string" - }, - "RoleName": { - "description": "Role name, between 2 and 128 chars, 'wire_' prefix is reserved for roles designed by Wire (i.e., no custom roles can have the same prefix)", - "type": "string" - }, - "SSOConfig.LockableFeature": { + "OAuthAccessTokenRequest_LTYyNTcyMzI4": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" + "client_id": { + "$ref": "#/components/schemas/UUID" }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" + "code": { + "$ref": "#/components/schemas/OAuthAuthorizationCode" }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "code_verifier": { + "description": "The code verifier to complete the code challenge", + "maxLength": 128, + "minLength": 43, + "type": "string" + }, + "grant_type": { + "$ref": "#/components/schemas/OAuthGrantType_LTIxODA5NDIw" + }, + "redirect_uri": { + "$ref": "#/components/schemas/RedirectUrl" } }, "required": [ - "status", - "lockStatus" + "grant_type", + "client_id", + "code_verifier", + "code", + "redirect_uri" ], "type": "object" }, - "ScimTokenInfo": { + "OAuthAccessTokenResponse_NzEwOTI4NjQ0": { "properties": { - "created_at": { - "$ref": "#/components/schemas/UTCTime" - }, - "description": { + "access_token": { + "description": "The access token, which has a relatively short lifetime", "type": "string" }, - "id": { - "$ref": "#/components/schemas/UUID" + "expires_in": { + "description": "The lifetime of the access token in seconds", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" }, - "idp": { - "$ref": "#/components/schemas/UUID" + "refresh_token": { + "description": "The refresh token, which has a relatively long lifetime, and can be used to obtain a new access token", + "type": "string" }, - "team": { - "$ref": "#/components/schemas/UUID" + "token_type": { + "$ref": "#/components/schemas/OAuthAccessTokenType_MjU3ODI0NDIw" } }, "required": [ - "team", - "id", - "created_at", - "description" + "access_token", + "token_type", + "expires_in", + "refresh_token" ], "type": "object" }, - "ScimTokenList": { + "OAuthAccessTokenType_MjU3ODI0NDIw": { + "description": "The type of the access token. Currently only `Bearer` is supported.", + "enum": [ + "Bearer" + ], + "type": "string" + }, + "OAuthApplication_Mjk5NTUxNjA1": { "properties": { - "tokens": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "description": "The OAuth client's name", + "maxLength": 256, + "minLength": 6, + "type": "string" + }, + "sessions": { + "description": "The OAuth client's sessions", "items": { - "$ref": "#/components/schemas/ScimTokenInfo" + "$ref": "#/components/schemas/OAuthSession_LTQxOTIxNTMy" }, "type": "array" } }, "required": [ - "tokens" + "id", + "name", + "sessions" ], "type": "object" }, - "SearchResult": { + "OAuthAuthorizationCode": { + "description": "The authorization code", + "type": "string" + }, + "OAuthClient_NzExMTI5NTIy": { "properties": { - "documents": { - "description": "List of contacts found", - "items": { - "$ref": "#/components/schemas/TeamContact" - }, - "type": "array" - }, - "found": { - "description": "Total number of hits", - "maximum": 9223372036854775807, - "minimum": -9223372036854775808, - "type": "integer" - }, - "has_more": { - "description": "Indicates whether there are more results to be fetched", - "type": "boolean" - }, - "paging_state": { - "$ref": "#/components/schemas/PagingState" - }, - "returned": { - "description": "Total number of hits returned", - "maximum": 9223372036854775807, - "minimum": -9223372036854775808, - "type": "integer" + "application_name": { + "maxLength": 256, + "minLength": 6, + "type": "string" }, - "search_policy": { - "$ref": "#/components/schemas/FederatedUserSearchPolicy" + "client_id": { + "$ref": "#/components/schemas/UUID" }, - "took": { - "description": "Search time in ms", - "maximum": 9223372036854775807, - "minimum": -9223372036854775808, - "type": "integer" + "redirect_url": { + "$ref": "#/components/schemas/RedirectUrl" } }, "required": [ - "found", - "returned", - "took", - "documents", - "search_policy" + "client_id", + "application_name", + "redirect_url" ], "type": "object" }, - "SearchVisibilityAvailableConfig.Feature": { - "properties": { - "status": { - "$ref": "#/components/schemas/FeatureStatus" - }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "status" + "OAuthCodeChallenge": { + "description": "Generated by the client from the code verifier (unpadded base64url-encoded SHA256 hash of the code verifier)", + "type": "string" + }, + "OAuthGrantType_LTIxODA5NDIw": { + "description": "Indicates which authorization flow to use. Use `authorization_code` for authorization code flow.", + "enum": [ + "authorization_code", + "refresh_token" ], - "type": "object" + "type": "string" }, - "SearchVisibilityAvailableConfig.LockableFeature": { + "OAuthRefreshAccessTokenRequest_LTUwNjA3OTI1": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" + "client_id": { + "$ref": "#/components/schemas/UUID" }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" + "grant_type": { + "$ref": "#/components/schemas/OAuthGrantType_LTIxODA5NDIw" }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "refresh_token": { + "description": "The refresh token", + "type": "string" } }, "required": [ - "status", - "lockStatus" + "grant_type", + "client_id", + "refresh_token" ], "type": "object" }, - "SearchVisibilityInboundConfig.Feature": { + "OAuthResponseType_ODI2Mjg3NzQx": { + "description": "Indicates which authorization flow to use. Use `code` for authorization code flow.", + "enum": [ + "code" + ], + "type": "string" + }, + "OAuthRevokeRefreshTokenRequest_MjA1MDg4MzQ4": { "properties": { - "status": { - "$ref": "#/components/schemas/FeatureStatus" + "client_id": { + "$ref": "#/components/schemas/UUID" }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "refresh_token": { + "description": "The refresh token", + "type": "string" } }, "required": [ - "status" + "client_id", + "refresh_token" ], "type": "object" }, - "SearchVisibilityInboundConfig.LockableFeature": { + "OAuthSession_LTQxOTIxNTMy": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "refresh_token_id": { + "$ref": "#/components/schemas/UUID" } }, "required": [ - "status", - "lockStatus" + "refresh_token_id", + "created_at" ], "type": "object" }, - "SelfDeletingMessagesConfig": { + "Object": { + "additionalProperties": true, + "description": "A single notification event", "properties": { - "enforcedTimeoutSeconds": { - "format": "int32", - "maximum": 2147483647, - "minimum": -2147483648, - "type": "integer" + "type": { + "description": "Event type", + "type": "string" } }, - "required": [ - "enforcedTimeoutSeconds" - ], + "title": "Event", "type": "object" }, - "SelfDeletingMessagesConfig.Feature": { + "OtherMemberUpdate_LTM1MjYzOTU0": { + "description": "Update user properties of other members relative to a conversation", "properties": { - "config": { - "$ref": "#/components/schemas/SelfDeletingMessagesConfig" - }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" - }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "conversation_role": { + "$ref": "#/components/schemas/RoleName" } }, - "required": [ - "status", - "config" - ], "type": "object" }, - "SelfDeletingMessagesConfig.LockableFeature": { + "OtherMember_LTgzNzE2MTk4": { "properties": { - "config": { - "$ref": "#/components/schemas/SelfDeletingMessagesConfig" + "conversation_role": { + "$ref": "#/components/schemas/RoleName" }, - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" + "id": { + "$ref": "#/components/schemas/UUID" }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, + "service": { + "$ref": "#/components/schemas/ServiceRef_LTgxMjY3NzAz" + }, + "status": { + "deprecated": true, + "description": "deprecated", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, "type": "integer" } }, "required": [ - "status", - "lockStatus", - "config" + "qualified_id" ], "type": "object" }, - "SendActivationCode": { - "description": "Data for requesting an email code to be sent. 'email' must be present.", + "OtrMessage_LTY4MTYzNzg3": { + "description": "Encrypted message of a conversation", "properties": { - "email": { - "$ref": "#/components/schemas/Email" + "data": { + "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", + "type": "string" }, - "locale": { - "$ref": "#/components/schemas/Locale" + "recipient": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "sender": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "text": { + "description": "The ciphertext for the recipient (Base64 in JSON)", + "type": "string" } }, "required": [ - "email" + "sender", + "recipient", + "text" ], "type": "object" }, - "SendVerificationCode": { + "OwnConvMembers_LTEwMzUzODMy": { + "description": "Users of a conversation", "properties": { - "action": { - "$ref": "#/components/schemas/VerificationAction" + "others": { + "description": "All other current users of this conversation", + "items": { + "$ref": "#/components/schemas/OtherMember_LTgzNzE2MTk4" + }, + "type": "array" }, - "email": { - "$ref": "#/components/schemas/Email" + "self": { + "$ref": "#/components/schemas/Member_OTA5OTgyNzcw" } }, "required": [ - "action", - "email" + "self", + "others" ], "type": "object" }, - "Service": { + "OwnConversation_GroupConvType_LTU2MzYxNTg0": { + "description": "A conversation object as returned from the server", "properties": { - "assets": { + "access": { "items": { - "$ref": "#/components/schemas/UserAsset" + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" }, "type": "array" }, - "auth_tokens": { - "$ref": "#/components/schemas/List1" + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" }, - "base_url": { - "$ref": "#/components/schemas/HttpsUrl" + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" }, - "description": { - "type": "string" + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" }, - "enabled": { - "type": "boolean" + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "history": { + "$ref": "#/components/schemas/History" }, "id": { "$ref": "#/components/schemas/UUID" }, - "name": { - "maxLength": 128, - "minLength": 1, + "last_event": { "type": "string" }, - "public_keys": { - "$ref": "#/components/schemas/List1" + "last_event_time": { + "type": "string" }, - "summary": { + "members": { + "$ref": "#/components/schemas/OwnConvMembers_LTEwMzUzODMy" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { "type": "string" }, - "tags": { - "items": { - "$ref": "#/components/schemas/" - }, - "type": "array" - } - }, - "required": [ - "id", - "name", - "summary", - "description", - "base_url", - "auth_tokens", - "public_keys", - "assets", - "tags", - "enabled" - ], - "type": "object" - }, - "ServiceKey": { - "properties": { - "pem": { - "$ref": "#/components/schemas/ServiceKeyPEM" + "parent": { + "$ref": "#/components/schemas/UUID" }, - "size": { + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "receipt_mode": { + "description": "Conversation receipt mode", "format": "int32", "maximum": 2147483647, "minimum": -2147483648, "type": "integer" }, + "team": { + "$ref": "#/components/schemas/UUID" + }, "type": { - "$ref": "#/components/schemas/ServiceKeyType" + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" } }, "required": [ + "qualified_id", "type", - "size", - "pem" + "access", + "access_role", + "members", + "group_id", + "epoch" ], "type": "object" }, - "ServiceKeyPEM": { - "example": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0\nG06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH\nWvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV\nVPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS\nbUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8\n7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la\nnQIDAQAB\n-----END PUBLIC KEY-----\n", - "type": "string" - }, - "ServiceKeyType": { - "enum": [ - "rsa" - ], + "PagingState": { + "description": "Paging state that should be supplied to retrieve the next page of results", "type": "string" }, - "ServiceProfile": { + "PasswordChange_MTgzMDM2NTY2": { + "description": "Data to change a password. The old password is required if a password already exists.", "properties": { - "has_more": { - "type": "boolean" + "new_password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" }, - "services": { - "items": { - "$ref": "#/components/schemas/ServiceProfile" - }, - "type": "array" + "old_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" } }, "required": [ - "has_more", - "services" + "new_password" ], "type": "object" }, - "ServiceRef": { + "PasswordChange_NDI0ODgwNDU0": { "properties": { - "id": { - "$ref": "#/components/schemas/UUID" + "new_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" }, - "provider": { - "$ref": "#/components/schemas/UUID" + "old_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" } }, "required": [ - "id", - "provider" + "old_password", + "new_password" ], "type": "object" }, - "ServiceTagList": { - "items": { - "$ref": "#/components/schemas/" + "PasswordReqBody_LTcxMzE3ODE3": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } }, - "type": "array" + "type": "object" }, - "SftServer": { - "description": "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers", + "PasswordReset_LTYzNDYxNTQ3": { "properties": { - "urls": { - "description": "Array containing exactly one SFT server address of the form 'https://:'", - "items": { - "$ref": "#/components/schemas/HttpsUrl" - }, - "type": "array" + "email": { + "$ref": "#/components/schemas/Email" } }, "required": [ - "urls" + "email" ], "type": "object" }, - "SimpleMember": { + "Permissions_NDE0ODM5NDUx": { + "description": "This is just a complicated way of representing a team role. self and copy always have to contain the same integer, and only the following integers are allowed: 1025 (partner), 1587 (member), 5951 (admin), 8191 (owner). Unit tests of the galley-types package in wire-server contain an authoritative list.", "properties": { - "conversation_role": { - "$ref": "#/components/schemas/RoleName" - }, - "id": { - "$ref": "#/components/schemas/UUID" + "copy": { + "description": "Permissions that this user is able to grant others", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" }, - "qualified_id": { - "$ref": "#/components/schemas/Qualified_UserId" + "self": { + "description": "Permissions that the user has", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" } }, "required": [ - "qualified_id" + "self", + "copy" ], "type": "object" }, - "SimpleMembers": { + "PhoneNumber": { + "description": "A known phone number with a pending password reset.", + "type": "string" + }, + "Pict_DEPRECATED_USE_ASSETS_INSTEAD": { + "items": { + "type": "object" + }, + "maxItems": 10, + "minItems": 0, + "type": "array" + }, + "PrekeyBundle_MzgzOTk4MjYz": { "properties": { - "user_ids": { - "deprecated": true, - "description": "deprecated", + "clients": { "items": { - "$ref": "#/components/schemas/UUID" + "$ref": "#/components/schemas/ClientPrekey_LTcyODUzMTcw" }, "type": "array" }, - "users": { - "items": { - "$ref": "#/components/schemas/SimpleMember" - }, - "type": "array" + "user": { + "$ref": "#/components/schemas/UUID" } }, "required": [ - "users" + "user", + "clients" ], "type": "object" }, - "SndFactorPasswordChallengeConfig.Feature": { + "PreventAdminlessGroupsConfigB_Covered_Identity_MjQ2NDYwMTk2": { "properties": { - "status": { - "$ref": "#/components/schemas/FeatureStatus" - }, - "ttl": { - "example": "unlimited", + "deletionTimeout": { "maximum": 18446744073709551615, "minimum": 0, "type": "integer" + }, + "deletionTimeoutDuration": { + "type": "string" + }, + "promotionStrategy": { + "$ref": "#/components/schemas/PreventAdminlessGroupsPromotionStrategy_Mjc1ODA2MDU1" + }, + "reminderTimeoutDurations": { + "items": { + "type": "string" + }, + "type": "array" + }, + "reminderTimeouts": { + "items": { + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "type": "array" } }, "required": [ - "status" + "promotionStrategy" + ], + "type": "object" + }, + "PreventAdminlessGroupsPromotionStrategy_Mjc1ODA2MDU1": { + "enum": [ + "alphabetical", + "random", + "all" + ], + "type": "string" + }, + "Priority_ODA3NDM3MDYy": { + "enum": [ + "low", + "high" ], + "type": "string" + }, + "PropertyKeysAndValues": { "type": "object" }, - "SndFactorPasswordChallengeConfig.LockableFeature": { + "PropertyValue": { + "description": "An arbitrary JSON value for a property" + }, + "ProtocolTag_ODg1MTE5NjEw": { + "enum": [ + "proteus", + "mls", + "mixed" + ], + "type": "string" + }, + "ProtocolUpdate_NzY1ODgxNDQy": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" - }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + } + }, + "type": "object" + }, + "ProviderActivationResponse_LTgzNTU3MzA5": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" } }, "required": [ - "status", - "lockStatus" + "email" ], "type": "object" }, - "SomeKey": {}, - "Sso": { + "ProviderLogin_LTE2MTk2NTM5": { "properties": { - "issuer": { - "type": "string" + "email": { + "$ref": "#/components/schemas/Email" }, - "nameid": { + "password": { + "maxLength": 1024, + "minLength": 6, "type": "string" } }, "required": [ - "issuer", - "nameid" + "email", + "password" ], "type": "object" }, - "SsoSettings": { + "Provider_NDIyMzQ3ODIy": { "properties": { - "default_sso_code": { + "description": { + "type": "string" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "id": { "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" } }, + "required": [ + "id", + "name", + "email", + "url", + "description" + ], "type": "object" }, - "SupportedProtocolUpdate": { + "PubClient": { "properties": { - "supported_protocols": { - "items": { - "$ref": "#/components/schemas/BaseProtocol" - }, - "type": "array" + "class": { + "$ref": "#/components/schemas/ClientClass_NjE3MDgwNzcx" + }, + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" } }, "required": [ - "supported_protocols" + "id" ], "type": "object" }, - "SystemSettings": { + "PublicSubConversation_MjI2NTIxMzU4": { + "description": "An MLS subconversation", "properties": { - "setEnableMls": { - "description": "Whether MLS is enabled or not", - "type": "boolean" + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" }, - "setRestrictUserCreation": { - "description": "Do not allow certain user creation flows", - "type": "boolean" + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "members": { + "items": { + "$ref": "#/components/schemas/ClientIdentity_MjAxMjI3NTUw" + }, + "type": "array" + }, + "parent_qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "subconv_id": { + "type": "string" } }, "required": [ - "setRestrictUserCreation", - "setEnableMls" + "parent_qualified_id", + "subconv_id", + "group_id", + "epoch", + "members" ], "type": "object" }, - "SystemSettingsPublic": { + "PushTokenList_NDI0Mjc3MzY3": { + "description": "List of Native Push Tokens", "properties": { - "setRestrictUserCreation": { - "description": "Do not allow certain user creation flows", - "type": "boolean" + "tokens": { + "description": "Push tokens", + "items": { + "$ref": "#/components/schemas/PushToken_ODYzMDYzOTA4" + }, + "type": "array" } }, "required": [ - "setRestrictUserCreation" + "tokens" ], "type": "object" }, - "Team": { - "description": "`binding` is deprecated, and should be ignored. The non-binding teams API is not used (and will not be supported from API version V4 onwards), and `binding` will always be `true`.", + "PushToken_ODYzMDYzOTA4": { + "description": "Native Push Token", "properties": { - "binding": { - "$ref": "#/components/schemas/TeamBinding" - }, - "creator": { - "$ref": "#/components/schemas/UUID" - }, - "icon": { - "$ref": "#/components/schemas/Icon" - }, - "icon_key": { + "app": { + "description": "Application", "type": "string" }, - "id": { - "$ref": "#/components/schemas/UUID" + "client": { + "description": "Client ID", + "type": "string" }, - "name": { + "token": { + "description": "Access Token", "type": "string" }, - "splash_screen": { - "$ref": "#/components/schemas/Icon" + "transport": { + "$ref": "#/components/schemas/Transport_NDk2NzU5NDIy" } }, "required": [ - "id", - "creator", - "name", - "icon" + "transport", + "app", + "token", + "client" ], "type": "object" }, - "TeamBinding": { - "deprecated": true, - "description": "Deprecated, please ignore.", - "enum": [ - true, - false - ], - "type": "boolean" - }, - "TeamContact": { + "PutApp_LTE4MDc1OTM4": { "properties": { "accent_id": { - "maximum": 9223372036854775807, - "minimum": -9223372036854775808, + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, "type": "integer" }, - "created_at": { - "$ref": "#/components/schemas/UTCTimeMillis" - }, - "email": { - "$ref": "#/components/schemas/Email" - }, - "email_unvalidated": { - "$ref": "#/components/schemas/Email" - }, - "handle": { - "type": "string" - }, - "id": { - "$ref": "#/components/schemas/UUID" - }, - "managed_by": { - "$ref": "#/components/schemas/ManagedBy" + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" }, - "name": { + "category": { + "description": "Category name (if uncertain, pick \"other\")", "type": "string" }, - "role": { - "$ref": "#/components/schemas/Role" - }, - "saml_idp": { + "description": { + "maxLength": 300, + "minLength": 0, "type": "string" }, - "scim_external_id": { + "name": { + "maxLength": 128, + "minLength": 1, "type": "string" - }, - "sso": { - "$ref": "#/components/schemas/Sso" - }, - "team": { - "$ref": "#/components/schemas/UUID" } }, - "required": [ - "id", - "name" - ], "type": "object" }, - "TeamConversation": { - "description": "Team conversation data", + "QualifiedNewOtrMessage": { + "description": "This object can only be parsed from Protobuf.\nThe specification for the protobuf types is here: \nhttps://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto." + }, + "QualifiedUserClientPrekeyMapV4_MzcxOTAxMTYy": { "properties": { - "conversation": { - "$ref": "#/components/schemas/UUID" + "failed_to_list": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "type": "array" }, - "managed": { - "description": "This field MUST NOT be used by clients. It is here only for backwards compatibility of the interface." + "qualified_user_client_prekeys": { + "additionalProperties": { + "$ref": "#/components/schemas/UserClientPrekeyMap" + }, + "type": "object" } }, "required": [ - "conversation", - "managed" + "qualified_user_client_prekeys" ], "type": "object" }, - "TeamConversationList": { - "description": "Team conversation list", - "properties": { - "conversations": { + "QualifiedUserClients": { + "additionalProperties": { + "additionalProperties": { "items": { - "$ref": "#/components/schemas/TeamConversation" + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" }, "type": "array" + }, + "type": "object" + }, + "description": "Map of Domain to UserClients", + "example": { + "domain1.example.com": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] } }, - "required": [ - "conversations" - ], "type": "object" }, - "TeamDeleteData": { + "QualifiedUserMap_Set_PubClient": { + "additionalProperties": { + "$ref": "#/components/schemas/UserMap_Set_PubClient" + }, + "description": "Map of Domain to (UserMap (Set_PubClient)).", + "example": { + "domain1.example.com": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + { + "class": "legalhold", + "id": "d0" + } + ] + } + }, + "type": "object" + }, + "Qualified_Handle_Nzg0MDE3Nzk4": { "properties": { - "password": { - "maxLength": 1024, - "minLength": 6, - "type": "string" + "domain": { + "$ref": "#/components/schemas/Domain" }, - "verification_code": { - "$ref": "#/components/schemas/ASCII" + "handle": { + "$ref": "#/components/schemas/Handle" } }, + "required": [ + "domain", + "handle" + ], "type": "object" }, - "TeamMember": { - "description": "team member data", + "Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5": { "properties": { - "created_at": { - "$ref": "#/components/schemas/UTCTimeMillis" - }, - "created_by": { - "$ref": "#/components/schemas/UUID" - }, - "legalhold_status": { - "$ref": "#/components/schemas/UserLegalHoldStatus" - }, - "permissions": { - "$ref": "#/components/schemas/Permissions" + "domain": { + "$ref": "#/components/schemas/Domain" }, - "user": { + "id": { "$ref": "#/components/schemas/UUID" } }, "required": [ - "user" + "domain", + "id" ], "type": "object" }, - "TeamMemberDeleteData": { - "description": "Data for a team member deletion request in case of binding teams.", + "Qualified_Id_IdTag_Meeting_MjgxMDUyNjc3": { "properties": { - "password": { - "description": "The account password to authorise the deletion.", - "maxLength": 1024, - "minLength": 6, - "type": "string" + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "id": { + "$ref": "#/components/schemas/UUID" } }, + "required": [ + "domain", + "id" + ], "type": "object" }, - "TeamMemberList": { - "description": "list of team member", + "Qualified_Id_IdTag_User_LTQ1NTIwNDM1": { "properties": { - "hasMore": { - "$ref": "#/components/schemas/ListType" + "domain": { + "$ref": "#/components/schemas/Domain" }, - "members": { - "description": "the array of team members", - "items": { - "$ref": "#/components/schemas/TeamMember" - }, - "type": "array" + "id": { + "$ref": "#/components/schemas/UUID" } }, "required": [ - "members", - "hasMore" + "domain", + "id" ], "type": "object" }, - "TeamMembersPage": { + "QueuedNotificationList_MTU0ODEyNTQ2": { + "description": "Zero or more notifications", "properties": { - "hasMore": { + "has_more": { + "description": "Whether there are still more notifications.", "type": "boolean" }, - "members": { + "notifications": { + "description": "Notifications", "items": { - "$ref": "#/components/schemas/TeamMember" + "$ref": "#/components/schemas/QueuedNotification_NTY2NzY2MTU2" }, "type": "array" }, - "pagingState": { - "$ref": "#/components/schemas/TeamMembers_PagingState" + "time": { + "$ref": "#/components/schemas/UTCTime" } }, "required": [ - "members", - "hasMore", - "pagingState" + "notifications" ], "type": "object" }, - "TeamMembers_PagingState": { - "type": "string" - }, - "TeamSearchVisibility": { - "description": "value of visibility", - "enum": [ - "standard", - "no-name-outside-team" - ], - "type": "string" - }, - "TeamSearchVisibilityView": { - "description": "Search visibility value for the team", + "QueuedNotification_NTY2NzY2MTU2": { + "description": "A single notification", "properties": { - "search_visibility": { - "$ref": "#/components/schemas/TeamSearchVisibility" + "id": { + "$ref": "#/components/schemas/UUID" + }, + "payload": { + "description": "List of events", + "items": { + "$ref": "#/components/schemas/Object" + }, + "minItems": 1, + "type": "array" } }, "required": [ - "search_visibility" + "id", + "payload" ], "type": "object" }, - "TeamSize": { - "description": "A simple object with a total number of team members.", + "RTCConfiguration_LTIwOTc4OTk0": { + "description": "A subset of the WebRTC 'RTCConfiguration' dictionary", "properties": { - "teamSize": { - "description": "Team size.", - "exclusiveMinimum": false, + "ice_servers": { + "description": "Array of 'RTCIceServer' objects", + "items": { + "$ref": "#/components/schemas/RTCIceServer_LTY1NzExODA0" + }, + "minItems": 1, + "type": "array" + }, + "is_federating": { + "description": "True if the client should connect to an SFT in the sft_servers_all and request it to federate", + "type": "boolean" + }, + "sft_servers": { + "description": "Array of 'SFTServer' objects (optional)", + "items": { + "$ref": "#/components/schemas/SFTServer_NDQ0NDkwNDE2" + }, + "minItems": 1, + "type": "array" + }, + "sft_servers_all": { + "description": "Array of all SFT servers", + "items": { + "$ref": "#/components/schemas/AuthSFTServer_LTY5MzcyOTE0" + }, + "type": "array" + }, + "ttl": { + "description": "Number of seconds after which the configuration should be refreshed (advisory)", + "format": "int32", + "maximum": 4294967295, "minimum": 0, "type": "integer" } }, "required": [ - "teamSize" + "ice_servers", + "ttl" ], "type": "object" }, - "TeamUpdateData": { + "RTCIceServer_LTY1NzExODA0": { + "description": "A subset of the WebRTC 'RTCIceServer' object", "properties": { - "icon": { - "$ref": "#/components/schemas/Icon" + "credential": { + "$ref": "#/components/schemas/ASCII" }, - "icon_key": { - "maxLength": 256, - "minLength": 1, - "type": "string" - }, - "name": { - "maxLength": 256, - "minLength": 1, - "type": "string" + "urls": { + "description": "Array of TURN server addresses of the form 'turn::'", + "items": { + "$ref": "#/components/schemas/TurnURI" + }, + "minItems": 1, + "type": "array" }, - "splash_screen": { - "$ref": "#/components/schemas/Icon" + "username": { + "$ref": "#/components/schemas/TurnUsername" } }, + "required": [ + "urls", + "username", + "credential" + ], "type": "object" }, - "Time": { + "Recurrence_LTQ0OTc0ODE2": { + "description": "Recurrence pattern for meetings", "properties": { - "time": { + "frequency": { + "$ref": "#/components/schemas/Frequency_Mzk0ODQwOTM3" + }, + "interval": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "until": { "$ref": "#/components/schemas/UTCTime" } }, "required": [ - "time" + "frequency" ], "type": "object" }, - "TokenType": { - "enum": [ - "Bearer" - ], + "RedirectUrl": { + "description": "The URL must match the URL that was used to generate the authorization code.", "type": "string" }, - "Transport": { - "description": "Transport", - "enum": [ - "GCM", - "APNS", - "APNS_SANDBOX", - "APNS_VOIP", - "APNS_VOIP_SANDBOX" - ], - "type": "string" + "RefreshAppCookieRequest_MjEyMDMyMTk5": { + "properties": { + "password": { + "description": "The password of the authenticated admin for verification. or if the user has only SAML credentials.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" }, - "TurnURI": { - "type": "string" + "RefreshAppCookieResponse_LTQ0MjU1NTIw": { + "properties": { + "cookie": { + "$ref": "#/components/schemas/SomeUserToken" + } + }, + "required": [ + "cookie" + ], + "type": "object" }, - "TypingData": { + "RegisteredDomains_V10_NDYwNzYyMTMy": { "properties": { - "status": { - "$ref": "#/components/schemas/TypingStatus" + "registered_domains": { + "items": { + "$ref": "#/components/schemas/DomainRegistrationResponse_V10_MjE0NDkxODY4" + }, + "type": "array" } }, "required": [ - "status" + "registered_domains" ], "type": "object" }, - "TypingStatus": { + "Relation_LTE4OTU5MTk4": { "enum": [ - "started", - "stopped" + "accepted", + "blocked", + "pending", + "ignored", + "sent", + "cancelled", + "missing-legalhold-consent" ], "type": "string" }, - "URIRef Absolute": { - "description": "URL of the invitation link to be sent to the invitee", - "type": "string" - }, - "UTCTime": { - "example": "2021-05-12T10:52:02Z", - "format": "yyyy-mm-ddThh:MM:ssZ", - "type": "string" - }, - "UTCTimeMillis": { - "description": "The time when the session was created", - "example": "2021-05-12T10:52:02.671Z", - "format": "yyyy-mm-ddThh:MM:ss.qqqZ", - "type": "string" - }, - "UUID": { - "description": "The OAuth client's ID", - "example": "99db9768-04e3-4b5d-9268-831b6a25c4ab", - "format": "uuid", - "type": "string" + "RemoveBotResponse_LTUxNTQ4MDEy": { + "properties": { + "event": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "required": [ + "event" + ], + "type": "object" }, - "Unnamed": { + "RemoveCookies_OTYwMTI0NDMy": { + "description": "Data required to remove cookies", "properties": { - "created_at": { - "$ref": "#/components/schemas/UTCTimeMillis" + "ids": { + "description": "A list of cookie IDs to revoke", + "items": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "type": "array" }, - "created_by": { - "$ref": "#/components/schemas/UUID" + "labels": { + "description": "A list of cookie labels for which to revoke the cookies", + "items": { + "type": "string" + }, + "type": "array" }, - "permissions": { - "$ref": "#/components/schemas/Permissions" + "password": { + "description": "The user's password", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "RemoveLegalHoldSettingsRequest_NzMzMzMyMDAz": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "RichField_LTgwMzc0MTg2": { + "properties": { + "type": { + "type": "string" }, - "user": { - "$ref": "#/components/schemas/UUID" + "value": { + "type": "string" } }, "required": [ - "user", - "permissions" + "type", + "value" ], "type": "object" }, - "UpdateBotPrekeys": { + "RichInfoAssocList": { + "description": "json object with case-insensitive fields.", "properties": { - "prekeys": { + "fields": { "items": { - "$ref": "#/components/schemas/Prekey" + "$ref": "#/components/schemas/RichField_LTgwMzc0MTg2" }, "type": "array" + }, + "version": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" } }, "required": [ - "prekeys" + "version", + "fields" ], "type": "object" }, - "UpdateClient": { + "RmClient_MTQ5OTI2MDY3": { "properties": { - "capabilities": { - "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "password": { + "description": "The password of the authenticated user for verification. The password is not required for deleting temporary clients. or if the user has only SAML credentials.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "RoleName": { + "description": "Role name, between 2 and 128 chars, 'wire_' prefix is reserved for roles designed by Wire (i.e., no custom roles can have the same prefix)", + "type": "string" + }, + "Role_LTIzMjAzMjky": { + "description": "Role of the invited user", + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "SFTServer_NDQ0NDkwNDE2": { + "description": "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers", + "properties": { + "urls": { + "description": "Array containing exactly one SFT server address of the form 'https://:'", "items": { - "$ref": "#/components/schemas/ClientCapability" + "$ref": "#/components/schemas/HttpsUrl" }, "type": "array" + } + }, + "required": [ + "urls" + ], + "type": "object" + }, + "SFTUsername": { + "description": "String containing the SFT username", + "type": "string" + }, + "ScimTokenInfo_LTI5NjgwNzA1": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTime" }, - "label": { - "description": "A new name for this client.", + "description": { "type": "string" }, - "lastkey": { - "$ref": "#/components/schemas/Prekey" + "id": { + "$ref": "#/components/schemas/UUID" }, - "mls_public_keys": { - "$ref": "#/components/schemas/MLSPublicKeys" + "idp": { + "$ref": "#/components/schemas/UUID" }, - "prekeys": { - "description": "New prekeys for other clients to establish OTR sessions.", + "name": { + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team", + "id", + "created_at", + "description", + "name" + ], + "type": "object" + }, + "ScimTokenList_NjQwNTYxOTAw": { + "properties": { + "tokens": { "items": { - "$ref": "#/components/schemas/Prekey" + "$ref": "#/components/schemas/ScimTokenInfo_LTI5NjgwNzA1" }, "type": "array" } }, + "required": [ + "tokens" + ], "type": "object" }, - "UpdateProvider": { + "ScimTokenName_LTgzOTM2OTI4": { "properties": { - "description": { - "type": "string" - }, "name": { - "maxLength": 128, - "minLength": 1, "type": "string" - }, - "url": { - "$ref": "#/components/schemas/HttpsUrl" } }, + "required": [ + "name" + ], "type": "object" }, - "UpdateService": { + "SearchResult_Contact_OTExNzg4MTE0": { "properties": { - "assets": { + "documents": { + "description": "List of contacts found", "items": { - "$ref": "#/components/schemas/UserAsset" + "$ref": "#/components/schemas/Contact_LTcwODE3Mjc5" }, "type": "array" }, - "description": { - "maxLength": 1024, - "minLength": 1, - "type": "string" + "found": { + "description": "Total number of hits", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" }, - "name": { - "maxLength": 128, - "minLength": 1, - "type": "string" + "has_more": { + "description": "Indicates whether there are more results to be fetched", + "type": "boolean" }, - "summary": { - "maxLength": 128, - "minLength": 1, - "type": "string" + "paging_state": { + "$ref": "#/components/schemas/PagingState" }, - "tags": { - "items": { - "$ref": "#/components/schemas/" - }, - "maxItems": 3, - "minItems": 1, - "type": "array" + "returned": { + "description": "Total number of hits returned", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "search_policy": { + "$ref": "#/components/schemas/FederatedUserSearchPolicy_MzkwODA4MTM3" + }, + "took": { + "description": "Search time in ms", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" } }, + "required": [ + "found", + "returned", + "took", + "documents", + "search_policy" + ], "type": "object" }, - "UpdateServiceConn": { + "SearchResult_TeamContact_LTE0NjQ0NzMw": { "properties": { - "auth_tokens": { + "documents": { + "description": "List of contacts found", "items": { - "$ref": "#/components/schemas/ASCII" + "$ref": "#/components/schemas/TeamContact_LTI5MTIxODc0" }, - "maxItems": 2, - "minItems": 1, "type": "array" }, - "base_url": { - "$ref": "#/components/schemas/HttpsUrl" + "found": { + "description": "Total number of hits", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" }, - "enabled": { + "has_more": { + "description": "Indicates whether there are more results to be fetched", "type": "boolean" }, - "password": { - "maxLength": 1024, - "minLength": 6, - "type": "string" + "paging_state": { + "$ref": "#/components/schemas/PagingState" }, - "public_keys": { - "items": { - "$ref": "#/components/schemas/ServiceKeyPEM" - }, - "maxItems": 2, - "minItems": 1, - "type": "array" + "returned": { + "description": "Total number of hits returned", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "search_policy": { + "$ref": "#/components/schemas/FederatedUserSearchPolicy_MzkwODA4MTM3" + }, + "took": { + "description": "Search time in ms", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" } }, "required": [ - "password" + "found", + "returned", + "took", + "documents", + "search_policy" ], "type": "object" }, - "UpdateServiceWhitelist": { + "SelfDeletingMessagesConfigB_Covered_Identity_LTY0NzQ4MzU1": { "properties": { - "id": { - "$ref": "#/components/schemas/UUID" + "enforcedTimeoutSeconds": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforcedTimeoutSeconds" + ], + "type": "object" + }, + "SendActivationCode_LTgyNDAxNzEy": { + "description": "Data for requesting an email code to be sent. 'email' must be present.", + "properties": { + "email": { + "$ref": "#/components/schemas/Email" }, - "provider": { - "$ref": "#/components/schemas/UUID" + "locale": { + "$ref": "#/components/schemas/Locale" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "SendVerificationCode_MjgxNDgxODE2": { + "properties": { + "action": { + "$ref": "#/components/schemas/VerificationAction_LTU0MzYxNzUz" }, - "whitelisted": { - "type": "boolean" + "email": { + "$ref": "#/components/schemas/Email" } }, "required": [ - "provider", - "id", - "whitelisted" + "action", + "email" ], "type": "object" }, - "User": { + "ServerTime_LTM4NTI3MzIx": { + "description": "The current server time", "properties": { - "accent_id": { + "time": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "time" + ], + "type": "object" + }, + "ServiceKeyPEM": { + "example": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0\nG06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH\nWvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV\nVPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS\nbUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8\n7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la\nnQIDAQAB\n-----END PUBLIC KEY-----\n", + "type": "string" + }, + "ServiceKeyType_NTEzNzI4NTA2": { + "enum": [ + "rsa" + ], + "type": "string" + }, + "ServiceKey_NzY5NTY5NzYy": { + "properties": { + "pem": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "size": { "format": "int32", "maximum": 2147483647, "minimum": -2147483648, "type": "integer" }, + "type": { + "$ref": "#/components/schemas/ServiceKeyType_NTEzNzI4NTA2" + } + }, + "required": [ + "type", + "size", + "pem" + ], + "type": "object" + }, + "ServiceProfilePage_Njg1NDQ5Njc4": { + "properties": { + "has_more": { + "type": "boolean" + }, + "services": { + "items": { + "$ref": "#/components/schemas/ServiceProfile_LTc2MDQzNTk3" + }, + "type": "array" + } + }, + "required": [ + "has_more", + "services" + ], + "type": "object" + }, + "ServiceProfile_LTc2MDQzNTk3": { + "properties": { "assets": { "items": { - "$ref": "#/components/schemas/UserAsset" + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" }, "type": "array" }, - "deleted": { - "type": "boolean" - }, - "email": { - "$ref": "#/components/schemas/Email" - }, - "expires_at": { - "$ref": "#/components/schemas/UTCTimeMillis" + "description": { + "type": "string" }, - "handle": { - "$ref": "#/components/schemas/Handle" + "enabled": { + "type": "boolean" }, "id": { "$ref": "#/components/schemas/UUID" }, - "locale": { - "$ref": "#/components/schemas/Locale" - }, - "managed_by": { - "$ref": "#/components/schemas/ManagedBy" - }, "name": { "maxLength": 128, "minLength": 1, "type": "string" }, - "picture": { - "$ref": "#/components/schemas/Pict" - }, - "qualified_id": { - "$ref": "#/components/schemas/Qualified_UserId" - }, - "service": { - "$ref": "#/components/schemas/ServiceRef" + "provider": { + "$ref": "#/components/schemas/UUID" }, - "sso_id": { - "$ref": "#/components/schemas/UserSSOId" + "summary": { + "type": "string" }, - "supported_protocols": { + "tags": { "items": { - "$ref": "#/components/schemas/BaseProtocol" + "$ref": "#/components/schemas/ServiceTag_LTMyNTEzNjYy" }, "type": "array" - }, - "team": { - "$ref": "#/components/schemas/UUID" - }, - "text_status": { - "maxLength": 256, - "minLength": 1, - "type": "string" } }, "required": [ - "qualified_id", + "id", + "provider", "name", - "accent_id", - "locale" + "summary", + "description", + "assets", + "tags", + "enabled" ], "type": "object" }, - "UserAsset": { + "ServiceRef_LTgxMjY3NzAz": { "properties": { - "key": { - "$ref": "#/components/schemas/AssetKey" - }, - "size": { - "$ref": "#/components/schemas/AssetSize" + "id": { + "$ref": "#/components/schemas/UUID" }, - "type": { - "$ref": "#/components/schemas/AssetType" + "provider": { + "$ref": "#/components/schemas/UUID" } }, "required": [ - "key", - "type" + "id", + "provider" ], "type": "object" }, - "UserClientMap": { - "additionalProperties": { - "additionalProperties": { - "type": "string" - }, - "type": "object" + "ServiceTagList": { + "items": { + "$ref": "#/components/schemas/ServiceTag_LTMyNTEzNjYy" }, - "type": "object" + "type": "array" }, - "UserClientPrekeyMap": { - "additionalProperties": { - "additionalProperties": { - "properties": { - "id": { - "maximum": 65535, - "minimum": 0, - "type": "integer" - }, - "key": { - "type": "string" - } + "ServiceTag_LTMyNTEzNjYy": { + "enum": [ + "audio", + "books", + "business", + "design", + "education", + "entertainment", + "finance", + "fitness", + "food-drink", + "games", + "graphics", + "health", + "integration", + "lifestyle", + "media", + "medical", + "movies", + "music", + "news", + "photography", + "poll", + "productivity", + "quiz", + "rating", + "shopping", + "social", + "sports", + "travel", + "tutorial", + "video", + "weather" + ], + "type": "string" + }, + "Service_MjcyOTA5NjQx": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" }, - "required": [ - "id", - "key" - ], - "type": "object" + "type": "array" }, - "type": "object" - }, - "example": { - "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": { - "44901fb0712e588f": { - "id": 1, - "key": "pQABAQECoQBYIOjl7hw0D8YRNq..." - } + "auth_tokens": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "minItems": 1, + "type": "array" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "public_keys": { + "items": { + "$ref": "#/components/schemas/ServiceKey_NzY5NTY5NzYy" + }, + "minItems": 1, + "type": "array" + }, + "summary": { + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/ServiceTag_LTMyNTEzNjYy" + }, + "type": "array" } }, + "required": [ + "id", + "name", + "summary", + "description", + "base_url", + "auth_tokens", + "public_keys", + "assets", + "tags", + "enabled" + ], "type": "object" }, - "UserClients": { - "additionalProperties": { - "items": { - "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", - "type": "string" - }, - "type": "array" - }, - "description": "Map of user id to list of client ids.", - "example": { - "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ - "60f85e4b15ad3786", - "6e323ab31554353b" - ] + "SetSearchable_NDAxODAxODI5": { + "properties": { + "set_searchable": { + "type": "boolean" + } }, + "required": [ + "set_searchable" + ], "type": "object" }, - "UserConnection": { + "SignedCertificate": { + "type": "string" + }, + "SimpleMember_NTY5MTcxMzcx": { "properties": { - "conversation": { - "$ref": "#/components/schemas/UUID" + "conversation_role": { + "$ref": "#/components/schemas/RoleName" }, - "from": { + "id": { "$ref": "#/components/schemas/UUID" }, - "last_update": { - "$ref": "#/components/schemas/UTCTimeMillis" - }, - "qualified_conversation": { - "$ref": "#/components/schemas/Qualified_ConvId" - }, - "qualified_to": { - "$ref": "#/components/schemas/Qualified_UserId" - }, - "status": { - "$ref": "#/components/schemas/Relation" + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "SomeKey": {}, + "SomeUserToken": { + "type": "string" + }, + "SsoSettings": { + "properties": { + "default_sso_code": { + "$ref": "#/components/schemas/URI" + } + }, + "type": "object" + }, + "Sso_LTg1MDM5ODQ3": { + "properties": { + "issuer": { + "type": "string" }, - "to": { - "$ref": "#/components/schemas/UUID" + "nameid": { + "type": "string" } }, "required": [ - "from", - "qualified_to", - "status", - "last_update" + "issuer", + "nameid" ], "type": "object" }, - "UserIdList": { + "SupportedProtocolUpdate_LTE3Njk3MDM4": { "properties": { - "user_ids": { + "supported_protocols": { "items": { - "$ref": "#/components/schemas/UUID" + "$ref": "#/components/schemas/BaseProtocolTag_LTM0MDE1NTEx" }, "type": "array" } }, "required": [ - "user_ids" + "supported_protocols" ], "type": "object" }, - "UserLegalHoldStatus": { - "description": "The state of Legal Hold compliance for the member", - "enum": [ - "enabled", - "pending", - "disabled", - "no_consent" + "SystemSettingsPublic_LTgwNTMxNjU2": { + "properties": { + "nomadProfiles": { + "description": "Whether Nomad client profiles are enabled; null or absence means not enabled.", + "type": "boolean" + }, + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" + } + }, + "required": [ + "setRestrictUserCreation" ], - "type": "string" + "type": "object" }, - "UserLegalHoldStatusResponse": { + "SystemSettings_ODU3MDk5MTA3": { "properties": { - "client": { - "$ref": "#/components/schemas/Id" + "nomadProfiles": { + "description": "Whether Nomad client profiles are enabled; null or absence means not enabled.", + "type": "boolean" }, - "last_prekey": { - "$ref": "#/components/schemas/Prekey" + "setEnableMls": { + "description": "Whether MLS is enabled or not", + "type": "boolean" }, - "status": { - "$ref": "#/components/schemas/UserLegalHoldStatus" + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" } }, "required": [ - "status" + "setRestrictUserCreation", + "setEnableMls" ], "type": "object" }, - "UserMap_Set_PubClient": { - "additionalProperties": { - "items": { - "$ref": "#/components/schemas/PubClient" + "TeamBinding_LTE4NTM5MTc0": { + "deprecated": true, + "description": "Deprecated, please ignore.", + "enum": [ + true, + false + ], + "type": "boolean" + }, + "TeamCollaborator_LTI3MzM1MTYz": { + "properties": { + "permissions": { + "items": { + "$ref": "#/components/schemas/CollaboratorPermission_NDg5NTg2ODgy" + }, + "type": "array" }, - "type": "array", - "uniqueItems": true - }, - "description": "Map of UserId to (Set PubClient)", - "example": { - "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ - { - "class": "legalhold", - "id": "d0" - } - ] + "team": { + "$ref": "#/components/schemas/UUID" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } }, + "required": [ + "user", + "team", + "permissions" + ], "type": "object" }, - "UserProfile": { + "TeamContact_LTI5MTIxODc0": { "properties": { "accent_id": { - "format": "int32", - "maximum": 2147483647, - "minimum": -2147483648, + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, "type": "integer" }, - "assets": { - "items": { - "$ref": "#/components/schemas/UserAsset" - }, - "type": "array" - }, - "deleted": { - "type": "boolean" + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" }, "email": { "$ref": "#/components/schemas/Email" }, - "expires_at": { - "$ref": "#/components/schemas/UTCTimeMillis" + "email_unvalidated": { + "$ref": "#/components/schemas/Email" }, "handle": { - "$ref": "#/components/schemas/Handle" + "type": "string" }, "id": { "$ref": "#/components/schemas/UUID" }, - "legalhold_status": { - "$ref": "#/components/schemas/UserLegalHoldStatus" + "managed_by": { + "$ref": "#/components/schemas/ManagedBy_NTI0ODc0NTQx" }, "name": { - "maxLength": 128, - "minLength": 1, "type": "string" }, - "picture": { - "$ref": "#/components/schemas/Pict" + "role": { + "$ref": "#/components/schemas/Role_LTIzMjAzMjky" }, - "qualified_id": { - "$ref": "#/components/schemas/Qualified_UserId" + "saml_idp": { + "type": "string" }, - "service": { - "$ref": "#/components/schemas/ServiceRef" + "scim_external_id": { + "type": "string" }, - "supported_protocols": { - "items": { - "$ref": "#/components/schemas/BaseProtocol" - }, - "type": "array" + "searchable": { + "type": "boolean" + }, + "sso": { + "$ref": "#/components/schemas/Sso_LTg1MDM5ODQ3" }, "team": { "$ref": "#/components/schemas/UUID" }, - "text_status": { - "maxLength": 256, - "minLength": 1, - "type": "string" + "type": { + "$ref": "#/components/schemas/UserType_LTU1OTU4OTM5" + }, + "user_groups": { + "description": "List of user group ids the user is a member of", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" } }, "required": [ - "qualified_id", + "id", + "type", "name", - "accent_id", - "legalhold_status" + "user_groups", + "searchable" ], "type": "object" }, - "UserSSOId": { - "properties": { - "scim_external_id": { - "type": "string" - }, - "subject": { - "type": "string" - }, - "tenant": { - "type": "string" - } - }, - "type": "object" - }, - "UserUpdate": { + "TeamConversationList_OTI3MzY3NzY0": { + "description": "Team conversation list", "properties": { - "accent_id": { - "format": "int32", - "maximum": 2147483647, - "minimum": -2147483648, - "type": "integer" - }, - "assets": { + "conversations": { "items": { - "$ref": "#/components/schemas/UserAsset" + "$ref": "#/components/schemas/TeamConversation_LTIwNzgyNTEz" }, "type": "array" - }, - "name": { - "maxLength": 128, - "minLength": 1, - "type": "string" - }, - "picture": { - "$ref": "#/components/schemas/Pict" - }, - "text_status": { - "maxLength": 256, - "minLength": 1, - "type": "string" } }, + "required": [ + "conversations" + ], "type": "object" }, - "ValidateSAMLEmailsConfig.LockableFeature": { + "TeamConversation_LTIwNzgyNTEz": { + "description": "Team conversation data", "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, - "status": { - "$ref": "#/components/schemas/FeatureStatus" + "conversation": { + "$ref": "#/components/schemas/UUID" }, - "ttl": { - "example": "unlimited", - "maximum": 18446744073709551615, - "minimum": 0, - "type": "integer" + "managed": { + "description": "This field MUST NOT be used by clients. It is here only for backwards compatibility of the interface." } }, "required": [ - "status", - "lockStatus" + "conversation", + "managed" ], "type": "object" }, - "VerificationAction": { - "enum": [ - "create_scim_token", - "login", - "delete_team" - ], - "type": "string" - }, - "VerifyDeleteUser": { - "description": "Data for verifying an account deletion.", + "TeamDeleteData_ODI5NTU0ODE5": { "properties": { - "code": { - "$ref": "#/components/schemas/ASCII" + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" }, - "key": { + "verification_code": { "$ref": "#/components/schemas/ASCII" } }, - "required": [ - "key", - "code" - ], "type": "object" }, - "VersionInfo": { - "example": { - "development": [ - 7 - ], - "domain": "example.com", - "federation": false, - "supported": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7 - ] - }, + "TeamDomainRedirectTag_MjQwMjc1Mjk3": { + "enum": [ + "no-registration", + "none" + ], + "type": "string" + }, + "TeamInviteConfig_MTg4Nzk4NzMz": { "properties": { - "development": { - "items": { - "$ref": "#/components/schemas/VersionNumber" - }, - "type": "array" + "domain_redirect": { + "$ref": "#/components/schemas/TeamDomainRedirectTag_MjQwMjc1Mjk3" }, - "domain": { - "$ref": "#/components/schemas/Domain" + "sso": { + "example": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "format": "uuid", + "type": "string" }, - "federation": { - "type": "boolean" + "team": { + "$ref": "#/components/schemas/UUID" }, - "supported": { - "items": { - "$ref": "#/components/schemas/VersionNumber" - }, - "type": "array" + "team_invite": { + "$ref": "#/components/schemas/TeamInviteTag_LTQyNTMyNzA0" } }, "required": [ - "supported", - "development", - "federation", - "domain" + "team_invite", + "team" ], "type": "object" }, - "VersionNumber": { + "TeamInviteTag_LTQyNTMyNzA0": { "enum": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7 + "allowed", + "not-allowed", + "team" ], - "type": "integer" + "type": "string" + }, + "TeamMemberDeleteData_LTg2OTEyOTI4": { + "description": "Data for a team member deletion request in case of binding teams.", + "properties": { + "password": { + "description": "The account password to authorise the deletion.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" }, - "ViewLegalHoldService": { + "TeamMemberList_Optional_LTM1ODE2MzM0": { + "description": "list of team member", "properties": { - "settings": { - "$ref": "#/components/schemas/ViewLegalHoldServiceInfo" + "hasMore": { + "$ref": "#/components/schemas/ListType_LTkyMDM4MzA1" }, - "status": { - "$ref": "#/components/schemas/LHServiceStatus" + "members": { + "description": "the array of team members", + "items": { + "$ref": "#/components/schemas/TeamMember_Optional_NTU0MDcyNzI1" + }, + "type": "array" } }, "required": [ - "status" + "members", + "hasMore" ], "type": "object" }, - "ViewLegalHoldServiceInfo": { + "TeamMember_Optional_NTU0MDcyNzI1": { + "description": "team member data", "properties": { - "auth_token": { - "$ref": "#/components/schemas/ASCII" + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" }, - "base_url": { - "$ref": "#/components/schemas/HttpsUrl" + "created_by": { + "$ref": "#/components/schemas/UUID" }, - "fingerprint": { - "$ref": "#/components/schemas/Fingerprint" + "legalhold_status": { + "$ref": "#/components/schemas/UserLegalHoldStatus_LTQ2ODA2NTU5" }, - "public_key": { - "$ref": "#/components/schemas/ServiceKeyPEM" + "permissions": { + "$ref": "#/components/schemas/Permissions_NDE0ODM5NDUx" }, - "team_id": { + "user": { "$ref": "#/components/schemas/UUID" } }, "required": [ - "team_id", - "base_url", - "fingerprint", - "auth_token", - "public_key" + "user" ], "type": "object" }, - "WireIdP": { + "TeamMembersPage_NzYwNDIxODgx": { "properties": { - "apiVersion": { - "$ref": "#/components/schemas/WireIdPAPIVersion" - }, - "handle": { - "type": "string" + "hasMore": { + "type": "boolean" }, - "oldIssuers": { + "members": { "items": { - "type": "string" + "$ref": "#/components/schemas/TeamMember_Optional_NTU0MDcyNzI1" }, "type": "array" }, - "replacedBy": { - "$ref": "#/components/schemas/UUID" - }, - "team": { - "$ref": "#/components/schemas/UUID" + "pagingState": { + "$ref": "#/components/schemas/TeamMembers_PagingState" } }, "required": [ - "team", - "oldIssuers", - "handle" + "members", + "hasMore", + "pagingState" ], "type": "object" }, - "WireIdPAPIVersion": { - "enum": [ - "WireIdPAPIV1", - "WireIdPAPIV2" - ], + "TeamMembers_PagingState": { "type": "string" }, - "XmlText": { + "TeamSearchVisibilityView_Mzg3MzMzMTk3": { + "description": "Search visibility value for the team", "properties": { - "fromXmlText": { - "type": "string" + "search_visibility": { + "$ref": "#/components/schemas/TeamSearchVisibility_LTIzODE2Njk3" } }, "required": [ - "fromXmlText" + "search_visibility" ], "type": "object" }, - "new-otr-message": { - "properties": { - "data": { - "type": "string" - }, - "native_priority": { - "$ref": "#/components/schemas/Priority" + "TeamSearchVisibility_LTIzODE2Njk3": { + "description": "value of visibility", + "enum": [ + "standard", + "no-name-outside-team" + ], + "type": "string" + }, + "TeamSize_LTMzMzk2MTk1": { + "description": "Team member counts broken down by user type.", + "properties": { + "teamSize": { + "description": "Total team members (teamSizeRegulars + teamSizeApps).", + "exclusiveMinimum": false, + "minimum": 0, + "type": "integer" }, - "native_push": { + "teamSizeApps": { + "description": "Number of apps in team.", + "exclusiveMinimum": false, + "minimum": 0, + "type": "integer" + }, + "teamSizeRegulars": { + "description": "Number of regular users in team.", + "exclusiveMinimum": false, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "teamSizeRegulars", + "teamSizeApps" + ], + "type": "object" + }, + "TeamUpdateData_LTE0NTM2NTU5": { + "properties": { + "icon": { + "$ref": "#/components/schemas/Icon" + }, + "icon_key": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "splash_screen": { + "$ref": "#/components/schemas/Icon" + } + }, + "type": "object" + }, + "Team_NDg4MjQwOTIw": { + "description": "`binding` is deprecated, and should be ignored. The non-binding teams API is not used (and will not be supported from API version V4 onwards), and `binding` will always be `true`.", + "properties": { + "binding": { + "$ref": "#/components/schemas/TeamBinding_LTE4NTM5MTc0" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "icon": { + "$ref": "#/components/schemas/Icon" + }, + "icon_key": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "splash_screen": { + "$ref": "#/components/schemas/Icon" + } + }, + "required": [ + "id", + "creator", + "name", + "icon" + ], + "type": "object" + }, + "Time": { + "properties": { + "time": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "time" + ], + "type": "object" + }, + "TimeZone": { + "type": "string" + }, + "Token": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "TokenType_NTkyMzk4MjIz": { + "enum": [ + "Bearer" + ], + "type": "string" + }, + "Transport_NDk2NzU5NDIy": { + "description": "Transport", + "enum": [ + "GCM", + "APNS", + "APNS_SANDBOX", + "APNS_VOIP", + "APNS_VOIP_SANDBOX" + ], + "type": "string" + }, + "TurnURI": { + "type": "string" + }, + "TurnUsername": { + "description": "Username to use for authenticating against the given TURN servers", + "type": "string" + }, + "TypingStatus_LTg5MzcyNDMy": { + "enum": [ + "started", + "stopped" + ], + "type": "string" + }, + "URI": { + "type": "string" + }, + "URIRef_Absolute": { + "description": "URL of the invitation link to be sent to the invitee", + "type": "string" + }, + "UTCTime": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + }, + "UTCTimeMillis": { + "description": "The time when the session was created", + "example": "2021-05-12T10:52:02.671Z", + "format": "yyyy-mm-ddThh:MM:ss.qqqZ", + "type": "string" + }, + "UUID": { + "description": "The OAuth client's ID", + "example": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "format": "uuid", + "type": "string" + }, + "UncheckedPrekeyBundle_LTU1MzQzOTgy": { + "properties": { + "id": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ], + "type": "object" + }, + "UpdateBotPrekeys_LTg3NzYxODg0": { + "properties": { + "prekeys": { + "items": { + "$ref": "#/components/schemas/UncheckedPrekeyBundle_LTU1MzQzOTgy" + }, + "type": "array" + } + }, + "required": [ + "prekeys" + ], + "type": "object" + }, + "UpdateClient_NzU5MjA4MzI1": { + "properties": { + "capabilities": { + "$ref": "#/components/schemas/ClientCapabilityList" + }, + "label": { + "description": "A new name for this client.", + "type": "string" + }, + "lastkey": { + "$ref": "#/components/schemas/UncheckedPrekeyBundle_LTU1MzQzOTgy" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "prekeys": { + "description": "New prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/components/schemas/UncheckedPrekeyBundle_LTU1MzQzOTgy" + }, + "type": "array" + } + }, + "type": "object" + }, + "UpdateMeeting_NTExNzYxMTcz": { + "description": "Request to update a meeting", + "properties": { + "end_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "recurrence": { + "$ref": "#/components/schemas/Recurrence_LTQ0OTc0ODE2" + }, + "start_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "title": { + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "type": "object" + }, + "UpdateProvider_LTQwMjY4MDgy": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "type": "object" + }, + "UpdateServiceConn_LTQ1OTYwNjIz": { + "properties": { + "auth_tokens": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "maxItems": 2, + "minItems": 1, + "type": "array" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "enabled": { "type": "boolean" }, - "recipients": { - "$ref": "#/components/schemas/UserClientMap" + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" }, - "report_missing": { + "public_keys": { + "items": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "maxItems": 2, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "UpdateServiceWhitelist_LTU5MDAwMTIw": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + }, + "whitelisted": { + "type": "boolean" + } + }, + "required": [ + "provider", + "id", + "whitelisted" + ], + "type": "object" + }, + "UpdateService_MjAxNzQ2Njkz": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" + }, + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "summary": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/ServiceTag_LTMyNTEzNjYy" + }, + "maxItems": 3, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, + "UpdateUserGroupChannels_LTIyMjcwMTMx": { + "properties": { + "channels": { "items": { "$ref": "#/components/schemas/UUID" }, "type": "array" + } + }, + "required": [ + "channels" + ], + "type": "object" + }, + "UpdateUserGroupMembers_LTg1MzQ2NDY3": { + "properties": { + "members": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "members" + ], + "type": "object" + }, + "UserClientMap": { + "additionalProperties": { + "additionalProperties": { + "type": "string" }, - "sender": { + "type": "object" + }, + "type": "object" + }, + "UserClientPrekeyMap": { + "additionalProperties": { + "additionalProperties": { + "properties": { + "id": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ], + "type": "object" + }, + "type": "object" + }, + "example": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": { + "44901fb0712e588f": { + "id": 1, + "key": "pQABAQECoQBYIOjl7hw0D8YRNq..." + } + } + }, + "type": "object" + }, + "UserClients": { + "additionalProperties": { + "items": { "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", "type": "string" }, - "transient": { + "type": "array" + }, + "description": "Map of user id to list of client ids.", + "example": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + }, + "type": "object" + }, + "UserConnection_LTY3NzU1ODg0": { + "properties": { + "conversation": { + "$ref": "#/components/schemas/UUID" + }, + "from": { + "$ref": "#/components/schemas/UUID" + }, + "last_update": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "qualified_conversation": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "qualified_to": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "status": { + "$ref": "#/components/schemas/Relation_LTE4OTU5MTk4" + }, + "to": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "from", + "qualified_to", + "status", + "last_update" + ], + "type": "object" + }, + "UserGroupAddUsers_LTgzOTYzNzk0": { + "properties": { + "members": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "members" + ], + "type": "object" + }, + "UserGroupNameAvailability_LTYzMDE1NTk4": { + "properties": { + "name_available": { "type": "boolean" } }, "required": [ - "sender", - "recipients" + "name_available" ], "type": "object" - } - }, - "securitySchemes": { - "ZAuth": { - "description": "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be presented in this format: 'Bearer \\'.", - "in": "header", - "name": "Authorization", - "type": "apiKey" - } - } - }, - "info": { - "description": "## Authentication / Authorization\n\nThe end-points in this API support differing authorization protocols:\nsome are unauthenticated (`/api-version`, `/login`), some require\n[zauth](), and some support both [zauth]() and [oauth]().\n\nThe end-points that require zauth are labelled so in the description\nbelow. The end-points that support oauth as an alternative to zauth\nhave the required oauth scopes listed in the same description.\n\nFuther reading:\n- https://docs.wire.com/developer/reference/oauth.html\n- https://github.com/wireapp/wire-server/blob/develop/libs/wire-api/src/Wire/API/Routes/Public.hs (search for HasSwagger instances)\n- `curl https://staging-nginz-https.zinfra.io/v4/api/swagger.json | jq '.security, .securityDefinitions`\n\n### SSO Endpoints\n\n#### Overview\n\n`/sso/metadata` will be requested by the IdPs to learn how to talk to wire.\n\n`/sso/initiate-login`, `/sso/finalize-login` are for the SAML authentication handshake performed by a user in order to log into wire. They are not exactly standard in their details: they may return HTML or XML; redirect to error URLs instead of throwing errors, etc.\n\n`/identity-providers` end-points are for use in the team settings page when IdPs are registered. They talk json.\n\n\n#### Configuring IdPs\n\nIdPs usually allow you to copy the metadata into your clipboard. That should contain all the details you need to post the idp in your team under `/identity-providers`. (Team id is derived from the authorization credentials of the request.)\n\n##### okta.com\n\nOkta will ask you to provide two URLs when you set it up for talking to wireapp:\n\n1. The `Single sign on URL`. This is the end-point that accepts the user's credentials after successful authentication against the IdP. Choose `/sso/finalize-login` with schema and hostname of the wire server you are configuring.\n\n2. The `Audience URI`. You can find this in the metadata returned by the `/sso/metadata` end-point. It is the contents of the `md:OrganizationURL` element.\n\n##### centrify.com\n\nCentrify allows you to upload the metadata xml document that you get from the `/sso/metadata` end-point. You can also enter the metadata url and have centrify retrieve the xml, but to guarantee integrity of the setup, the metadata should be copied from the team settings page and pasted into the centrify setup page without any URL indirections.\n\n## Federation errors\n\nEndpoints involving federated calls to other domains can return some extra failure responses, common to all endpoints. Instead of listing them as possible responses for each endpoint, we document them here.\n\nFor errors that are more likely to be transient, we suggest clients to retry whatever request resulted in the error. Transient errors are indicated explicitly below.\n\n**Note**: when a failure occurs as a result of making a federated RPC to another backend, the error response contains the following extra fields:\n\n - `type`: \"federation\" (just the literal string in quotes, which can be used as an error type identifier when parsing errors)\n - `domain`: the target backend of the RPC that failed;\n - `path`: the path of the RPC that failed.\n\n### Domain errors\n\nErrors in this category result from trying to communicate with a backend that is considered non-existent or invalid. They can result from invalid user input or client issues, but they can also be a symptom of misconfiguration in one or multiple backends. These errors have a 4xx status code.\n\n - **Remote backend not found** (status: 422, label: `invalid-domain`): This backend attempted to contact a backend which does not exist or is not properly configured. For the most part, clients can consider this error equivalent to a domain not existing, although it should be noted that certain mistakes in the DNS configuration on a remote backend can lead to the backend not being recognized, and hence to this error. It is therefore not advisable to take any destructive action upon encountering this error, such as deleting remote users from conversations.\n - **Federation denied locally** (status: 400, label: `federation-denied`): This backend attempted an RPC to a non-whitelisted backend. Similar considerations as for the previous error apply.\n - **Federation not enabled** (status: 400, label: `federation-not-enabled`): Federation has not been configured for this backend. This will happen if a federation-aware client tries to talk to a backend for which federation is disabled, or if federation was disabled on the backend after reaching a federation-specific state (e.g. conversations with remote users). There is no way to cleanly recover from these errors at this point.\n\n### Local federation errors\n\nAn error in this category likely indicates an issue with the configuration of federation on the local backend. Possibly transient errors are indicated explicitly below. All these errors have a 500 status code.\n\n - **Federation unavailable** (status: 500, label: `federation-not-available`): Federation is configured for this backend, but the local federator cannot be reached. This can be transient, so clients should retry the request.\n - **Federation not implemented** (status: 500, label: `federation-not-implemented`): Federated behaviour for a certain endpoint is not yet implemented.\n - **Federator discovery failed** (status: 400, label: `discovery-failure`): A DNS error occurred during discovery of a remote backend. This can be transient, so clients should retry the request.\n - **Local federation error** (status: 500, label: `federation-local-error`): An error occurred in the communication between this backend and its local federator. These errors are most likely caused by bugs in the backend, and should be reported as such.\n\n### Remote federation errors\n\nErrors in this category are returned in case of communication issues between the local backend and a remote one, or if the remote side encountered an error while processing an RPC. Some errors in this category might be caused by incorrect client behaviour, wrong user input, or incorrect certificate configuration. Possibly transient errors are indicated explicitly. We use non-standard 5xx status codes for these errors.\n\n - **HTTP2 error** (status: 533, label: `federation-http2-error`): The current federator encountered an error when making an HTTP2 request to a remote one. Check the error message for more details.\n - **Connection refused** (status: 521, label: `federation-connection-refused`): The local federator could not connect to a remote one. This could be transient, so clients should retry the request.\n - **TLS failure**: (status: 525, label: `federation-tls-error`): An error occurred during the TLS handshake between the local federator and a remote one. This is most likely due to an issue with the certificate on the remote end.\n - **Remote federation error** (status: 533, label: `federation-remote-error`): The remote backend could not process a request coming from this backend. Check the error message for more details.\n - **Version negotiation error** (status: 533, label: `federation-version-error`): The remote backend returned invalid version information.\n\n### Backend compatibility errors\n\nAn error in this category will be returned when this backend makes an invalid or unsupported RPC to another backend. This can indicate some incompatibility between backends or a backend bug. These errors are unlikely to be transient, so retrying requests is *not* advised.\n\n - **Version mismatch** (status: 531, label: `federation-version-mismatch`): A remote backend is running an unsupported version of the federator.\n - **Invalid content type** (status: 533, label: `federation-invalid-content-type`): An RPC to another backend returned with an invalid content type.\n - **Unsupported content type** (status: 533, label: `federation-unsupported-content-type`): An RPC to another backend returned with an unsupported content type.\n", - "title": "Wire-Server API", - "version": "" - }, - "openapi": "3.0.0", - "paths": { - "/": { - "get": { - "description": " [internal route ID: \"get-services-tags\"]\n\n", + }, + "UserGroupPage_UserGroup_Const_LTMxNDg5MDAy": { + "description": "This is the last page if it contains fewer rows than requested. There may be 0 rows on a page.", + "properties": { + "page": { + "items": { + "$ref": "#/components/schemas/UserGroup_Const_NTMzOTAzMzA1" + }, + "type": "array" + }, + "total": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "page", + "total" + ], + "type": "object" + }, + "UserGroupUpdate_MjUyNTA3Mjgy": { + "properties": { + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "UserGroup_Const_NTMzOTAzMzA1": { + "properties": { + "channels": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "type": "array" + }, + "channelsCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "createdAt": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "managedBy": { + "$ref": "#/components/schemas/ManagedBy_NTI0ODc0NTQx" + }, + "membersCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "name", + "managedBy", + "createdAt" + ], + "type": "object" + }, + "UserGroup_Identity_NTg4MTY1MjEx": { + "properties": { + "channels": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "type": "array" + }, + "channelsCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "createdAt": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "managedBy": { + "$ref": "#/components/schemas/ManagedBy_NTI0ODc0NTQx" + }, + "members": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "membersCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "name", + "members", + "managedBy", + "createdAt" + ], + "type": "object" + }, + "UserIdList_MzA1MTI1Njgx": { + "properties": { + "user_ids": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "user_ids" + ], + "type": "object" + }, + "UserLegalHoldStatusResponse_LTQ1MzUxMTE3": { + "properties": { + "client": { + "$ref": "#/components/schemas/IdObject_ClientId_LTM3NjQyODM5" + }, + "last_prekey": { + "$ref": "#/components/schemas/UncheckedPrekeyBundle_LTU1MzQzOTgy" + }, + "status": { + "$ref": "#/components/schemas/UserLegalHoldStatus_LTQ2ODA2NTU5" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "UserLegalHoldStatus_LTQ2ODA2NTU5": { + "description": "The state of Legal Hold compliance for the member", + "enum": [ + "enabled", + "pending", + "disabled", + "no_consent" + ], + "type": "string" + }, + "UserMap_Set_PubClient": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/PubClient" + }, + "type": "array", + "uniqueItems": true + }, + "description": "Map of UserId to (Set PubClient)", + "example": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + { + "class": "legalhold", + "id": "d0" + } + ] + }, + "type": "object" + }, + "UserProfile_LTQzMTQxMTE1": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "app": { + "$ref": "#/components/schemas/AppInfo_MjgwNTkwOTUz" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" + }, + "contact_status": { + "$ref": "#/components/schemas/ContactStatus_LTUzNzk1MzM4" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "expires_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "legalhold_status": { + "$ref": "#/components/schemas/UserLegalHoldStatus_LTQ2ODA2NTU5" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict_DEPRECATED_USE_ASSETS_INSTEAD" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "searchable": { + "type": "boolean" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef_LTgxMjY3NzAz" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocolTag_LTM0MDE1NTEx" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/UserType_LTU1OTU4OTM5" + } + }, + "required": [ + "qualified_id", + "name", + "accent_id", + "legalhold_status" + ], + "type": "object" + }, + "UserSSOId": { + "properties": { + "scim_external_id": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "tenant": { + "type": "string" + } + }, + "type": "object" + }, + "UserType_LTU1OTU4OTM5": { + "enum": [ + "regular", + "app", + "bot" + ], + "type": "string" + }, + "UserUpdate_MjQ4NTEwOTQz": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict_DEPRECATED_USE_ASSETS_INSTEAD" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "type": "object" + }, + "User_NjA4OTQwMTQ4": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "email_unvalidated": { + "$ref": "#/components/schemas/Email" + }, + "expires_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy_NTI0ODc0NTQx" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict_DEPRECATED_USE_ASSETS_INSTEAD" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "searchable": { + "type": "boolean" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef_LTgxMjY3NzAz" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + }, + "status": { + "$ref": "#/components/schemas/AccountStatus_NzkzNDU1ODU5" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocolTag_LTM0MDE1NTEx" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/UserType_LTU1OTU4OTM5" + } + }, + "required": [ + "qualified_id", + "type", + "name", + "accent_id", + "status", + "locale" + ], + "type": "object" + }, + "VerificationAction_LTU0MzYxNzUz": { + "enum": [ + "create_scim_token", + "login", + "delete_team" + ], + "type": "string" + }, + "VerifyDeleteUser_Njc1NDQ1MDIy": { + "description": "Data for verifying an account deletion.", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "VersionInfo_NTEzMTgzNDQ0": { + "example": { + "development": [ + 17 + ], + "domain": "example.com", + "federation": false, + "supported": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17 + ] + }, + "properties": { + "development": { + "items": { + "$ref": "#/components/schemas/VersionNumber_Njk2NzI5Njk1" + }, + "type": "array" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "federation": { + "type": "boolean" + }, + "supported": { + "items": { + "$ref": "#/components/schemas/VersionNumber_Njk2NzI5Njk1" + }, + "type": "array" + } + }, + "required": [ + "supported", + "development", + "federation", + "domain" + ], + "type": "object" + }, + "VersionNumber_Njk2NzI5Njk1": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17 + ], + "type": "integer" + }, + "Versiond_17_PvtAmlGupCfgBaIy_LTY1MTkyNDcw": { + "properties": { + "deletionTimeoutDuration": { + "type": "string" + }, + "promotionStrategy": { + "$ref": "#/components/schemas/PreventAdminlessGroupsPromotionStrategy_Mjc1ODA2MDU1" + }, + "reminderTimeoutDurations": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "promotionStrategy", + "deletionTimeoutDuration", + "reminderTimeoutDurations" + ], + "type": "object" + }, + "ViewLegalHoldServiceInfo_LTc3NjI2MzQ3": { + "properties": { + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "fingerprint": { + "$ref": "#/components/schemas/Fingerprint" + }, + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "team_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team_id", + "base_url", + "fingerprint", + "auth_token", + "public_key" + ], + "type": "object" + }, + "ViewLegalHoldService_LTE3MzQzNDkw": { + "properties": { + "settings": { + "$ref": "#/components/schemas/ViewLegalHoldServiceInfo_LTc3NjI2MzQ3" + }, + "status": { + "$ref": "#/components/schemas/LHServiceStatus_ODc3NzE0Mjg3" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "WireIdPAPIVersion_NTEyMzIwNTU3": { + "enum": [ + "WireIdPAPIV1", + "WireIdPAPIV2" + ], + "type": "string" + }, + "WireIdP_ODMzOTExMzYw": { + "properties": { + "apiVersion": { + "enum": [ + "WireIdPAPIV1", + "WireIdPAPIV2" + ], + "type": "string" + }, + "domain": { + "example": "example.com", + "type": "string" + }, + "handle": { + "type": "string" + }, + "oldIssuers": { + "items": { + "$ref": "#/components/schemas/URI" + }, + "type": "array" + }, + "replacedBy": { + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team", + "apiVersion", + "oldIssuers", + "replacedBy", + "handle", + "domain" + ], + "type": "object" + }, + "v2_ConversationAccessData_MjMxMTI5ODc3": { + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy_LTYwOTAxMDI1" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + } + }, + "required": [ + "access" + ], + "type": "object" + }, + "v2_OwnConversation_GroupConvTypeLegacy_MjQ0OTcyNjQ3": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy_LTYwOTAxMDI1" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/EpochTimestamp" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvTypeLegacy_NTUxMDI2Mzkw" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers_LTEwMzUzODMy" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite" + ], + "type": "object" + }, + "v2_OwnConversation_GroupConvType_LTU2MzYxNTg0": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy_LTYwOTAxMDI1" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/EpochTimestamp" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers_LTEwMzUzODMy" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite" + ], + "type": "object" + }, + "v3_OwnConversation_GroupConvType_LTU2MzYxNTg0V3": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/EpochTimestamp" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers_LTEwMzUzODMy" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite" + ], + "type": "object" + }, + "v6_OwnConversation_GroupConvType_LTU2MzYxNTg0V6": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers_LTEwMzUzODMy" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + }, + "v9_OwnConversation_GroupConvType_LTU2MzYxNTg0": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers_LTEwMzUzODMy" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + } + }, + "securitySchemes": { + "ZAuth": { + "description": "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be presented in this format: 'Bearer \\'.", + "in": "header", + "name": "Authorization", + "type": "apiKey" + } + } + }, + "info": { + "description": "## Authentication / Authorization\n\nThe end-points in this API support differing authorization protocols:\nsome are unauthenticated (`/api-version`, `/login`), some require\n[zauth](), and some support both [zauth]() and [oauth]().\n\nThe end-points that require zauth are labelled so in the description\nbelow. The end-points that support oauth as an alternative to zauth\nhave the required oauth scopes listed in the same description.\n\nFuther reading:\n- https://docs.wire.com/developer/reference/oauth.html\n- https://github.com/wireapp/wire-server/blob/develop/libs/wire-api/src/Wire/API/Routes/Public.hs (search for HasSwagger instances)\n- `curl https://staging-nginz-https.zinfra.io/v4/api/swagger.json | jq '.security, .securityDefinitions`\n\n### SSO Endpoints\n\n#### Overview\n\n`/sso/metadata` will be requested by the IdPs to learn how to talk to wire.\n\n`/sso/initiate-login`, `/sso/finalize-login` are for the SAML authentication handshake performed by a user in order to log into wire. They are not exactly standard in their details: they may return HTML or XML; redirect to error URLs instead of throwing errors, etc.\n\n`/identity-providers` end-points are for use in the team settings page when IdPs are registered. They talk json.\n\n\n#### Configuring IdPs\n\nIdPs usually allow you to copy the metadata into your clipboard. That should contain all the details you need to post the idp in your team under `/identity-providers`. (Team id is derived from the authorization credentials of the request.)\n\n##### okta.com\n\nOkta will ask you to provide two URLs when you set it up for talking to wireapp:\n\n1. The `Single sign on URL`. This is the end-point that accepts the user's credentials after successful authentication against the IdP. Choose `/sso/finalize-login` with schema and hostname of the wire server you are configuring.\n\n2. The `Audience URI`. You can find this in the metadata returned by the `/sso/metadata` end-point. It is the contents of the `md:OrganizationURL` element.\n\n##### centrify.com\n\nCentrify allows you to upload the metadata xml document that you get from the `/sso/metadata` end-point. You can also enter the metadata url and have centrify retrieve the xml, but to guarantee integrity of the setup, the metadata should be copied from the team settings page and pasted into the centrify setup page without any URL indirections.\n\n## Federation errors\n\nEndpoints involving federated calls to other domains can return some extra failure responses, common to all endpoints. Instead of listing them as possible responses for each endpoint, we document them here.\n\nFor errors that are more likely to be transient, we suggest clients to retry whatever request resulted in the error. Transient errors are indicated explicitly below.\n\n**Note**: when a failure occurs as a result of making a federated RPC to another backend, the error response contains the following extra fields:\n\n - `type`: \"federation\" (just the literal string in quotes, which can be used as an error type identifier when parsing errors)\n - `domain`: the target backend of the RPC that failed;\n - `path`: the path of the RPC that failed.\n\n### Domain errors\n\nErrors in this category result from trying to communicate with a backend that is considered non-existent or invalid. They can result from invalid user input or client issues, but they can also be a symptom of misconfiguration in one or multiple backends. These errors have a 4xx status code.\n\n - **Remote backend not found** (status: 422, label: `invalid-domain`): This backend attempted to contact a backend which does not exist or is not properly configured. For the most part, clients can consider this error equivalent to a domain not existing, although it should be noted that certain mistakes in the DNS configuration on a remote backend can lead to the backend not being recognized, and hence to this error. It is therefore not advisable to take any destructive action upon encountering this error, such as deleting remote users from conversations.\n - **Federation denied locally** (status: 400, label: `federation-denied`): This backend attempted an RPC to a non-whitelisted backend. Similar considerations as for the previous error apply.\n - **Federation not enabled** (status: 400, label: `federation-not-enabled`): Federation has not been configured for this backend. This will happen if a federation-aware client tries to talk to a backend for which federation is disabled, or if federation was disabled on the backend after reaching a federation-specific state (e.g. conversations with remote users). There is no way to cleanly recover from these errors at this point.\n\n### Local federation errors\n\nAn error in this category likely indicates an issue with the configuration of federation on the local backend. Possibly transient errors are indicated explicitly below. All these errors have a 500 status code.\n\n - **Federation unavailable** (status: 500, label: `federation-not-available`): Federation is configured for this backend, but the local federator cannot be reached. This can be transient, so clients should retry the request.\n - **Federation not implemented** (status: 422, label: `federation-not-implemented`): Federated behaviour for a certain endpoint is not yet implemented.\n - **Federator discovery failed** (status: 400, label: `discovery-failure`): A DNS error occurred during discovery of a remote backend. This can be transient, so clients should retry the request.\n - **Local federation error** (status: 500, label: `federation-local-error`): An error occurred in the communication between this backend and its local federator. These errors are most likely caused by bugs in the backend, and should be reported as such.\n\n### Remote federation errors\n\nErrors in this category are returned in case of communication issues between the local backend and a remote one, or if the remote side encountered an error while processing an RPC. Some errors in this category might be caused by incorrect client behaviour, wrong user input, or incorrect certificate configuration. Possibly transient errors are indicated explicitly. We use non-standard 5xx status codes for these errors.\n\n - **HTTP2 error** (status: 533, label: `federation-http2-error`): The current federator encountered an error when making an HTTP2 request to a remote one. Check the error message for more details.\n - **Connection refused** (status: 521, label: `federation-connection-refused`): The local federator could not connect to a remote one. This could be transient, so clients should retry the request.\n - **TLS failure**: (status: 525, label: `federation-tls-error`): An error occurred during the TLS handshake between the local federator and a remote one. This is most likely due to an issue with the certificate on the remote end.\n - **Remote federation error** (status: 533, label: `federation-remote-error`): The remote backend could not process a request coming from this backend. Check the error message for more details.\n - **Version negotiation error** (status: 533, label: `federation-version-error`): The remote backend returned invalid version information.\n\n### Backend compatibility errors\n\nAn error in this category will be returned when this backend makes an invalid or unsupported RPC to another backend. This can indicate some incompatibility between backends or a backend bug. These errors are unlikely to be transient, so retrying requests is *not* advised.\n\n - **Version mismatch** (status: 531, label: `federation-version-mismatch`): A remote backend is running an unsupported version of the federator.\n - **Invalid content type** (status: 533, label: `federation-invalid-content-type`): An RPC to another backend returned with an invalid content type.\n - **Unsupported content type** (status: 533, label: `federation-unsupported-content-type`): An RPC to another backend returned with an unsupported content type.\n", + "title": "Wire-Server API", + "version": "" + }, + "openapi": "3.0.0", + "paths": { + "/access": { + "post": { + "description": " [internal route ID: \"access\"]\n\nYou can provide only a cookie or a cookie and token. Every other combination is invalid. Access tokens can be given as query parameter or authorisation header, with the latter being preferred.", + "operationId": "access", + "parameters": [ + { + "in": "query", + "name": "client_id", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessToken_ODIyMTczMjMw" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AccessToken_ODIyMTczMjMw" + } + } + }, + "description": "OK", + "headers": { + "Set-Cookie": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Obtain an access tokens for a cookie" + } + }, + "/access/logout": { + "post": { + "description": " [internal route ID: \"logout\"]\n\nCalling this endpoint will effectively revoke the given cookie and subsequent calls to /access with the same cookie will result in a 403.", + "operationId": "logout", + "responses": { + "200": { + "description": "Logout" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Log out in order to remove a cookie from the server" + } + }, + "/access/self/email": { + "put": { + "description": " [internal route ID: \"change-self-email\"]\n\n", + "operationId": "change-self-email", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EmailUpdate_NjQ5MDg1OTY0" + } + } + }, + "required": true + }, + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "Update accepted and pending activation of the new email" + }, + "204": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "No update, current and new email address are the same\n\nEmail address activated" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid e-mail address. (label: `invalid-email`) or `body`" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Change your email address" + } + }, + "/activate": { + "get": { + "description": " [internal route ID: \"get-activate\"]\n\nSee also 'POST /activate' which has a larger feature set.", + "operationId": "get-activate", + "parameters": [ + { + "description": "Activation key", + "in": "query", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Activation code", + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse_LTIyOTY5NDE3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse_LTIyOTY5NDE3" + } + } + }, + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful." + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone", + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `code` or `key`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Activate (i.e. confirm) an email address." + }, + "post": { + "description": " [internal route ID: \"post-activate\"]\n\nActivation only succeeds once and the number of failed attempts for a valid key is limited.", + "operationId": "post-activate", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Activate_MzUzNzIxODUw" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse_LTIyOTY5NDE3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse_LTIyOTY5NDE3" + } + } + }, + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful." + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone", + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Activate (i.e. confirm) an email address." + } + }, + "/activate/send": { + "post": { + "description": " [internal route ID: \"post-activate-send\"]\n\n", + "operationId": "post-activate-send", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SendActivationCode_LTgyNDAxNzEy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Activation code sent." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "blacklisted-email", + "message": "The given e-mail address has been blacklisted due to a permanent bounce or a complaint." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + }, + "451": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 451, + "label": "domain-blocked-for-registration", + "message": "[Customer extension] The email domain has been blocked for Wire users. Please contact your IT department." + }, + "properties": { + "code": { + "enum": [ + 451 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-blocked-for-registration" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "[Customer extension] The email domain has been blocked for Wire users. Please contact your IT department. (label: `domain-blocked-for-registration`)" + } + }, + "summary": "Send (or resend) an email activation code." + } + }, + "/api-version": { + "get": { + "description": " [internal route ID: \"get-version\"]\n\n", + "operationId": "get-version", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/VersionInfo_NTEzMTgzNDQ0" + } + } + }, + "description": "" + } + } + } + }, + "/assets": { + "post": { + "description": " [internal route ID: \"assets-upload\"]\n\n

Construct the request as multipart/mixed; set header Content-Type: multipart/mixed; boundary=<boundary>.

Use exactly two parts in this order:

  1. application/json metadata (AssetSettings)
  2. application/octet-stream asset bytes

Each part must include Content-Type and Content-Length; the second part may include Content-MD5. Use CRLF between headers and bodies.

When asset audit logging is enabled, the JSON metadata must include:

  • convId: object { id: UUID, domain: String } (qualified conversation ID)
  • filename: String
  • filetype: String MIME type (e.g. image/png, application/pdf)

Optional metadata: public (Bool, default false), retention (one of eternal, persistent, volatile, eternal-infrequent_access, expiring).

For profile pictures or team icons without a conversation, set convId.id to 00000000-0000-0000-0000-000000000000 and convId.domain to the tenant’s domain; use any reasonable filename.

Note: the server treats the asset bytes as application/octet-stream; filetype is used for auditing only.

Example body (boundary=frontier):

Content-Type: multipart/mixed; boundary=frontier

--frontier
Content-Type: application/json
Content-Length: 191

{\"public\":false,\"retention\":\"volatile\",\"convId\":{\"id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"domain\":\"example.com\"},\"filename\":\"report.pdf\",\"filetype\":\"application/pdf\"}
--frontier
Content-Type: application/octet-stream
Content-Length: 11

Hello Audit
--frontier--
", + "operationId": "assets-upload", + "requestBody": { + "content": { + "multipart/mixed": { + "schema": { + "$ref": "#/components/schemas/AssetSource" + } + } + }, + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server." + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset_Qualified_AssetKey_MzU1MjMxNTA5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Asset_Qualified_AssetKey_MzU1MjMxNTA5" + } + } + }, + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "incomplete-body", + "message": "HTTP content-length header does not match body size" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "incomplete-body", + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nHTTP content-length header does not match body size (label: `incomplete-body`)\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Asset too large (label: `client-error`)" + } + }, + "summary": "Upload an asset" + } + }, + "/assets/{key_domain}/{key}": { + "delete": { + "description": " [internal route ID: \"assets-delete\"]\n\n**Note**: only local assets can be deleted.", + "operationId": "assets-delete", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key_domain` or `key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Delete an asset" + }, + "get": { + "description": " [internal route ID: \"assets-download\"]\n\n**Note**: local assets result in a redirect, while remote assets are streamed directly.", + "operationId": "assets-download", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset returned directly with content type `application/octet-stream`" + }, + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key_domain` or `key` or Asset not found (label: `not-found`)\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Download an asset" + } + }, + "/assets/{key}/token": { + "delete": { + "description": " [internal route ID: \"tokens-delete\"]\n\n**Note**: deleting the token makes the asset public.", + "operationId": "tokens-delete", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset token deleted" + } + }, + "summary": "Delete an asset token" + }, + "post": { + "description": " [internal route ID: \"tokens-renew\"]\n\n", + "operationId": "tokens-renew", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewAssetToken_NTAwMDQwODYy" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Renew an asset token" + } + }, + "/await": { + "get": { + "description": " [internal route ID: \"await-notifications\"]\n\n", + "externalDocs": { + "description": "RFC 6455", + "url": "https://datatracker.ietf.org/doc/html/rfc6455" + }, + "operationId": "await-notifications", + "parameters": [ + { + "description": "Client ID", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "101": { + "description": "Connection upgraded." + }, + "426": { + "description": "Upgrade required." + } + }, + "summary": "Establish websocket connection" + } + }, + "/bot/assets": { + "post": { + "description": " [internal route ID: (\"assets-upload-v3\", bot)]\n\n

Construct the request as multipart/mixed; set header Content-Type: multipart/mixed; boundary=<boundary>.

Use exactly two parts in this order:

  1. application/json metadata (AssetSettings)
  2. application/octet-stream asset bytes

Each part must include Content-Type and Content-Length; the second part may include Content-MD5. Use CRLF between headers and bodies.

When asset audit logging is enabled, the JSON metadata must include:

  • convId: object { id: UUID, domain: String } (qualified conversation ID)
  • filename: String
  • filetype: String MIME type (e.g. image/png, application/pdf)

Optional metadata: public (Bool, default false), retention (one of eternal, persistent, volatile, eternal-infrequent_access, expiring).

For profile pictures or team icons without a conversation, set convId.id to 00000000-0000-0000-0000-000000000000 and convId.domain to the tenant’s domain; use any reasonable filename.

Note: the server treats the asset bytes as application/octet-stream; filetype is used for auditing only.

Example body (boundary=frontier):

Content-Type: multipart/mixed; boundary=frontier

--frontier
Content-Type: application/json
Content-Length: 191

{\"public\":false,\"retention\":\"volatile\",\"convId\":{\"id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"domain\":\"example.com\"},\"filename\":\"report.pdf\",\"filetype\":\"application/pdf\"}
--frontier
Content-Type: application/octet-stream
Content-Length: 11

Hello Audit
--frontier--
", + "operationId": "assets-upload-v3_bot", + "requestBody": { + "content": { + "multipart/mixed": { + "schema": { + "$ref": "#/components/schemas/AssetSource" + } + } + }, + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server." + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset_Qualified_AssetKey_MzU1MjMxNTA5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Asset_Qualified_AssetKey_MzU1MjMxNTA5" + } + } + }, + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "incomplete-body", + "message": "HTTP content-length header does not match body size" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "incomplete-body", + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nHTTP content-length header does not match body size (label: `incomplete-body`)\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Asset too large (label: `client-error`)" + } + }, + "summary": "Upload an asset" + } + }, + "/bot/assets/{key}": { + "delete": { + "description": " [internal route ID: (\"assets-delete-v3\", bot)]\n\n", + "operationId": "assets-delete-v3_bot", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Delete an asset" + }, + "get": { + "description": " [internal route ID: (\"assets-download-v3\", bot)]\n\n", + "operationId": "assets-download-v3_bot", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` or Asset not found (label: `not-found`)" + } + }, + "summary": "Download an asset" + } + }, + "/bot/client": { + "get": { + "description": " [internal route ID: \"bot-get-client\"]\n\n", + "operationId": "bot-get-client", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + } + } + }, + "description": "Client found" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Client not found (label: `client-not-found`)\n\nClient not found (label: `client-not-found`)" + } + }, + "summary": "Get client for bot" + } + }, + "/bot/client/prekeys": { + "get": { + "description": " [internal route ID: \"bot-list-prekeys\"]\n\n", + "operationId": "bot-list-prekeys", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List prekeys for bot" + }, + "post": { + "description": " [internal route ID: \"bot-update-prekeys\"]\n\n", + "operationId": "bot-update-prekeys", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateBotPrekeys_LTg3NzYxODg0" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Client not found (label: `client-not-found`)" + } + }, + "summary": "Update prekeys for bot" + } + }, + "/bot/conversation": { + "get": { + "description": " [internal route ID: \"get-bot-conversation\"]\n\n", + "operationId": "get-bot-conversation", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/BotConvView_LTYzMjIzMjQz" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" + } + } + } + }, + "/bot/conversations/{conv}": { + "post": { + "description": " [internal route ID: \"add-bot\"]\n\n", + "operationId": "add-bot", + "parameters": [ + { + "in": "path", + "name": "conv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AddBot_NjI0ODkyODk3" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddBotResponse_ODA5MzA2NTA1" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AddBotResponse_ODA5MzA2NTA1" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "service-disabled", + "message": "The desired service is currently disabled." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "service-disabled", + "too-many-members", + "invalid-conversation", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The desired service is currently disabled. (label: `service-disabled`)\n\nMaximum number of members per conversation reached. (label: `too-many-members`)\n\nThe operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Add bot" + } + }, + "/bot/conversations/{conv}/{bot}": { + "delete": { + "description": " [internal route ID: \"remove-bot\"]\n\n", + "operationId": "remove-bot", + "parameters": [ + { + "in": "path", + "name": "conv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "bot", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoveBotResponse_LTUxNTQ4MDEy" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RemoveBotResponse_LTUxNTQ4MDEy" + } + } + }, + "description": "User found" + }, + "204": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-conversation", + "message": "The operation is not allowed in this conversation." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-conversation", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Remove bot" + } + }, + "/bot/messages": { + "post": { + "description": " [internal route ID: \"post-bot-message-unqualified\"]\n\n", + "operationId": "post-bot-message-unqualified", + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewOtrMessage_LTUyMTE5MTMw" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + } + }, + "description": "Message sent" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation not found (label: `no-conversation`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + } + }, + "description": "Missing clients" + } + } + } + }, + "/bot/self": { + "delete": { + "description": " [internal route ID: \"bot-delete-self\"]\n\n", + "operationId": "bot-delete-self", + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-bot", + "message": "The targeted user is not a bot." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-bot", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The targeted user is not a bot. (label: `invalid-bot`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Delete self" + }, + "get": { + "description": " [internal route ID: \"bot-get-self\"]\n\n", + "operationId": "bot-get-self", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserProfile_LTQzMTQxMTE1" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "User not found (label: `not-found`)" + } + }, + "summary": "Get self" + } + }, + "/bot/users": { + "get": { + "description": " [internal route ID: \"bot-list-users\"]\n\n", + "operationId": "bot-list-users", + "parameters": [ + { + "in": "query", + "name": "ids", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/BotUserView_LTE2MTkwMTcw" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List users" + } + }, + "/bot/users/prekeys": { + "post": { + "description": " [internal route ID: \"bot-claim-users-prekeys\"]\n\n", + "operationId": "bot-claim-users-prekeys", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserClients" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserClientPrekeyMap" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "missing-legalhold-consent-old-clients", + "too-many-clients", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nToo many clients (label: `too-many-clients`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Claim users prekeys" + } + }, + "/bot/users/{user}/clients": { + "get": { + "description": " [internal route ID: \"bot-get-user-clients\"]\n\n", + "operationId": "bot-get-user-clients", + "parameters": [ + { + "in": "path", + "name": "user", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/PubClient" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Get user clients" + } + }, + "/broadcast/otr/messages": { + "post": { + "description": " [internal route ID: \"post-otr-broadcast-unqualified\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts two query params:\n- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are ignored.\n - When 'false' all missing clients are reported.\n - When comma separated list of user-ids, only clients for listed users are ignored.\n- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are reported.\n - When 'false' all missing clients are ignored.\n - When comma separated list of user-ids, only clients for listed users are reported.\n\nApart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\nAll three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n- `report_missing` in the request body has highest precedence.\n- `ignore_missing` in the query param is the next.\n- `report_missing` in the query param has the lowest precedence.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "operationId": "post-otr-broadcast-unqualified", + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewOtrMessage_LTUyMTE5MTMw" + } + }, + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/NewOtrMessage_LTUyMTE5MTMw" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + } + }, + "description": "Message sent" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "too-many-users-to-broadcast", + "message": "Too many users to fan out the broadcast event to" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-users-to-broadcast" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body` or `report_missing` or `ignore_missing`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "non-binding-team", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Broadcast an encrypted message to all team members and all contacts (accepts JSON or Protobuf)" + } + }, + "/broadcast/proteus/messages": { + "post": { + "description": " [internal route ID: \"post-proteus-broadcast\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n- `ignore_all`: When set, no checks about missing clients are carried out.\n- `report_only`: Takes a list of qualified UserIDs. If any clients of the listed users are missing, the message is not sent. The missing clients are reported in the response.\n- `ignore_only`: Takes a list of qualified UserIDs. If any clients of the non-listed users are missing, the message is not sent. The missing clients are reported in the response.\n\nThe sending of messages in a federated conversation could theoretically fail partially. To make this case unlikely, the backend first gets a list of clients from all the involved backends and then tries to send a message. So, if any backend is down, the message is not propagated to anyone. But the actual message fan out to multiple backends could still fail partially. This type of failure is reported as a 201, the clients for which the message sending failed are part of the response body.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "operationId": "post-proteus-broadcast", + "requestBody": { + "content": { + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/QualifiedNewOtrMessage" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" + } + } + }, + "description": "Message sent" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "too-many-users-to-broadcast", + "message": "Too many users to fan out the broadcast event to" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-users-to-broadcast" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "non-binding-team", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Post an encrypted message to all team members and all contacts (accepts only Protobuf)" + } + }, + "/calls/config/v2": { + "get": { + "description": " [internal route ID: \"get-calls-config-v2\"]\n\n", + "operationId": "get-calls-config-v2", + "parameters": [ + { + "description": "Limit resulting list. Allowed values [1..10]", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "maximum": 10, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RTCConfiguration_LTIwOTc4OTk0" + } + } + }, + "description": "" + } + }, + "summary": "Retrieve all TURN server addresses and credentials. Clients are expected to do a DNS lookup to resolve the IP addresses of the given hostnames " + } + }, + "/clients": { + "get": { + "description": " [internal route ID: \"list-clients\"]\n\n", + "operationId": "list-clients", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + }, + "type": "array" + } + } + }, + "description": "List of clients" + } + }, + "summary": "List the registered clients" + }, + "post": { + "description": " [internal route ID: \"add-client\"]\n\n", + "operationId": "add-client", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewClient_ODg1NjY4Njgy" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + } + } + }, + "description": "Client registered", + "headers": { + "Location": { + "description": "Client ID", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "bad-request", + "message": "Malformed prekeys uploaded" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "bad-request" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMalformed prekeys uploaded (label: `bad-request`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "missing-auth", + "too-many-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nToo many clients (label: `too-many-clients`)" + } + }, + "summary": "Register a new client" + } + }, + "/clients/{cid}/access-token": { + "post": { + "description": " [internal route ID: \"create-access-token\"]\n\nCreate an JWT DPoP access token for the client CSR, given a JWT DPoP proof, specified in the `DPoP` header. The access token will be returned in the JSON response body as a JWT DPoP token.", + "operationId": "create-access-token", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "cid", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "DPoP", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DPoPAccessTokenResponse_LTgyODU5MDE3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DPoPAccessTokenResponse_LTgyODU5MDE3" + } + } + }, + "description": "Access token created", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Create a JWT DPoP access token" + } + }, + "/clients/{client}": { + "delete": { + "description": " [internal route ID: \"delete-client\"]\n\n", + "operationId": "delete-client", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RmClient_MTQ5OTI2MDY3" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Client deleted" + } + }, + "summary": "Delete an existing client" + }, + "get": { + "description": " [internal route ID: \"get-client\"]\n\n", + "operationId": "get-client", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + } + } + }, + "description": "Client found" + }, + "404": { + "description": "`client` or Client not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a registered client by ID" + }, + "put": { + "description": " [internal route ID: \"update-client\"]\n\n", + "operationId": "update-client", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateClient_NzU5MjA4MzI1" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Client updated" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-duplicate-public-key", + "message": "MLS public key for the given signature scheme already exists" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-duplicate-public-key", + "bad-request" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMLS public key for the given signature scheme already exists (label: `mls-duplicate-public-key`)\n\nMalformed prekeys uploaded (label: `bad-request`)" + } + }, + "summary": "Update a registered client" + } + }, + "/clients/{client}/capabilities": { + "get": { + "description": " [internal route ID: \"get-client-capabilities\"]\n\n", + "operationId": "get-client-capabilities", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientCapabilityList" + } + } + }, + "description": "" + } + }, + "summary": "Read back what the client has been posting about itself" + } + }, + "/clients/{client}/nonce": { + "get": { + "description": " [internal route ID: \"get-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "operationId": "get-nonce", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + } + }, + "Replay-Nonce": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Get a new nonce for a client CSR" + }, + "head": { + "description": " [internal route ID: \"head-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "operationId": "head-nonce", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "No Content", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + } + }, + "Replay-Nonce": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Get a new nonce for a client CSR" + } + }, + "/clients/{client}/prekeys": { + "get": { + "description": " [internal route ID: \"get-client-prekeys\"]\n\n", + "operationId": "get-client-prekeys", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "List the remaining prekey IDs of a client" + } + }, + "/connections/{uid_domain}/{uid}": { + "get": { + "description": " [internal route ID: \"get-connection\"]\n\n", + "operationId": "get-connection", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + } + }, + "description": "Connection found" + }, + "404": { + "description": "`uid_domain` or `uid` or Connection not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get an existing connection to another user (local or remote)" + }, + "post": { + "description": " [internal route ID: \"create-connection\"]\n\nYou can have no more than 1000 connections in accepted or sent state", + "operationId": "create-connection", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + } + }, + "description": "Connection existed" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + } + }, + "description": "Connection was created" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-user", + "message": "Invalid user" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-user" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid user (label: `invalid-user`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified email" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "connection-limit", + "missing-legalhold-consent", + "missing-legalhold-consent-old-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The user has no verified email (label: `no-identity`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)" + } + }, + "summary": "Create a connection to another user" + }, + "put": { + "description": " [internal route ID: \"update-connection\"]\n\n", + "operationId": "update-connection", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConnectionUpdate_LTU3MTA1OTA5" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + } + }, + "description": "Connection updated" + }, + "204": { + "description": "Connection unchanged" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-user", + "message": "Invalid user" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-user" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified email" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "bad-conn-update", + "not-connected", + "connection-limit", + "missing-legalhold-consent", + "missing-legalhold-consent-old-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The user has no verified email (label: `no-identity`)\n\nInvalid status transition (label: `bad-conn-update`)\n\nUsers are not connected (label: `not-connected`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)" + } + }, + "summary": "Update a connection to another user" + } + }, + "/conversations": { + "post": { + "description": " [internal route ID: \"create-group-conversation\"]\n\nThis returns 201 when a new conversation is created, and 200 when the conversation already existed\nOAuth scope: `write:conversations`", + "operationId": "create-group-conversation", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewConv_LTgzNTk1NDQx" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateGroupConversation_GroupConvType_MTQ3OTc4NDk3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateGroupConversation_GroupConvType_MTQ3OTc4NDk3" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "history-not-supported", + "message": "Shared history is not supported on this conversation" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "history-not-supported", + "mls-not-enabled", + "non-empty-member-list" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nShared history is not supported on this conversation (label: `history-not-supported`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nAttempting to add group members outside MLS (label: `non-empty-member-list`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "channels-not-enabled", + "message": "The channels feature is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "channels-not-enabled", + "not-mls-conversation", + "missing-legalhold-consent", + "operation-denied", + "no-team-member", + "not-connected", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The channels feature is not enabled for this team (label: `channels-not-enabled`)\n\nThis operation requires an MLS conversation (label: `not-mls-conversation`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nUsers are not connected (label: `not-connected`)\n\nConversation access denied (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Create a new conversation" + } + }, + "/conversations/code-check": { + "post": { + "description": " [internal route ID: \"code-check\"]\n\nIf the guest links team feature is disabled, this will fail with 404 CodeNotFound.Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/join` which responds with 409 GuestLinksDisabled if guest links are disabled.", + "operationId": "code-check", + "parameters": [ + { + "in": "header", + "name": "X-Forwarded-For", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCode_Mjg3OTI1NTMx" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Valid" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-conversation-password", + "message": "Invalid conversation password" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid conversation password (label: `invalid-conversation-password`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + } + }, + "summary": "Check validity of a conversation code." + } + }, + "/conversations/join": { + "get": { + "description": " [internal route ID: \"get-conversation-by-reusable-code\"]\n\n", + "operationId": "get-conversation-by-reusable-code", + "parameters": [ + { + "in": "query", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCoverView_LTMwNDkxMTA1" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "access-denied", + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid conversation password (label: `invalid-conversation-password`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Get limited conversation information by key/code pair" + }, + "post": { + "description": " [internal route ID: \"join-conversation-by-code-unqualified\"]\n\nIf the guest links team feature is disabled, this will fail with 409 GuestLinksDisabled.Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/code-check` which responds with 404 CodeNotFound if guest links are disabled.", + "operationId": "join-conversation-by-code-unqualified", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/JoinConversationByCode_NjgzMzM4Mjg5" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Conversation joined" + }, + "204": { + "description": "Conversation unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "too-many-members", + "message": "Maximum number of members per conversation reached" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-members", + "no-team-member", + "invalid-op", + "access-denied", + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Maximum number of members per conversation reached (label: `too-many-members`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid conversation password (label: `invalid-conversation-password`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Join a conversation using a reusable code" + } + }, + "/conversations/list": { + "post": { + "description": " [internal route ID: \"list-conversations\"]\n\n", + "operationId": "list-conversations", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ListConversations_MjkxMTIwODMz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationsResponse_GroupConvType_ODkxMjM2ODM0" + } + } + }, + "description": "" + } + }, + "summary": "Get conversation metadata for a list of conversation ids" + } + }, + "/conversations/list-ids": { + "post": { + "description": " [internal route ID: \"list-conversation-ids\"]\n\nThe IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", + "operationId": "list-conversation-ids", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetMuliTabPgRqs_ConvrIdLcOm10_ODU0NjQ1NDMz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MultiTabePg_ConvrsIdqf_cLORm_Q_Mzg0MjYzNzM0" + } + } + }, + "description": "" + } + }, + "summary": "Get all conversation IDs." + } + }, + "/conversations/mls-self": { + "get": { + "description": " [internal route ID: \"get-mls-self-conversation\"]\n\n", + "operationId": "get-mls-self-conversation", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/v9_OwnConversation_GroupConvType_LTU2MzYxNTg0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/v9_OwnConversation_GroupConvType_LTU2MzYxNTg0" + } + } + }, + "description": "The MLS self-conversation" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + } + }, + "summary": "Get the user's MLS self-conversation" + } + }, + "/conversations/self": { + "post": { + "description": " [internal route ID: \"create-self-conversation\"]\n\n", + "operationId": "create-self-conversation", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/v6_OwnConversation_GroupConvType_LTU2MzYxNTg0V6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/v6_OwnConversation_GroupConvType_LTU2MzYxNTg0V6" + } + } + }, + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/v6_OwnConversation_GroupConvType_LTU2MzYxNTg0V6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/v6_OwnConversation_GroupConvType_LTU2MzYxNTg0V6" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + } + }, + "summary": "Create a self-conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}": { + "get": { + "description": " [internal route ID: \"get-conversation\"]\n\n", + "operationId": "get-conversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Conversation_GroupConvType_MzQzMTQ1OTg3" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get a conversation by ID" + } + }, + "/conversations/{cnv_domain}/{cnv}/access": { + "put": { + "description": " [internal route ID: \"update-conversation-access\"]\n\n", + "operationId": "update-conversation-access", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationAccessData_MjMxMTI5ODc3" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Access updated" + }, + "204": { + "description": "Access unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid target access" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid target access (label: `invalid-op`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nInsufficient authorization (missing modify_conversation_access) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update access modes for a conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/add-permission": { + "put": { + "description": " [internal route ID: \"update-channel-add-permission\"]\n\n", + "operationId": "update-channel-add-permission", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AddPermissionUpdate_LTU3MzEwOTY4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Add permissions updated" + }, + "204": { + "description": "Add permissions unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid target access" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "not-connected", + "operation-denied", + "no-team-member", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid target access (label: `invalid-op`)\n\nUsers are not connected (label: `not-connected`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_add_permissions) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nTeam not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Update the permissions for adding members to a channel" + } + }, + "/conversations/{cnv_domain}/{cnv}/groupinfo": { + "get": { + "description": " [internal route ID: \"get-group-info\"]\n\n", + "operationId": "get-group-info", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "responses": { "200": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/GroupInfoData" + } + } + }, + "description": "The group information" + }, + "400": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ServiceTagList" + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "" + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "access-denied", - "message": "Access denied." + "code": 404, + "label": "mls-missing-group-info", + "message": "The conversation has no group information" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "access-denied" + "mls-missing-group-info", + "no-conversation" ], "type": "string" }, @@ -6795,47 +14769,101 @@ } } }, - "description": "Access denied. (label: `access-denied`)" + "description": "`cnv_domain` or `cnv` not found\n\nThe conversation has no group information (label: `mls-missing-group-info`)\n\nConversation not found (label: `no-conversation`)" } }, - "summary": "Get services tags" + "summary": "Get MLS group information" } }, - "/access": { - "post": { - "description": " [internal route ID: \"access\"]\n\nYou can provide only a cookie or a cookie and token. Every other combination is invalid. Access tokens can be given as query parameter or authorisation header, with the latter being preferred.Calls federation service brig on send-connection-action", + "/conversations/{cnv_domain}/{cnv}/history": { + "put": { + "description": " [internal route ID: \"update-conversation-history\"]\n\n", + "operationId": "update-conversation-history", "parameters": [ { - "in": "query", - "name": "client_id", - "required": false, + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, "schema": { + "format": "uuid", "type": "string" } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationHistoryUpdate_LTg5MDQ5Nzgx" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AccessToken" + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/AccessToken" + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" } } }, - "description": "OK", - "headers": { - "Set-Cookie": { + "description": "History updated" + }, + "204": { + "description": "History unchanged" + }, + "400": { + "content": { + "application/json;charset=utf-8": { "schema": { - "type": "string" + "example": { + "code": 400, + "label": "history-not-supported", + "message": "Shared history is not supported on this conversation" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "history-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } - } + }, + "description": "Invalid `body`\n\nShared history is not supported on this conversation (label: `history-not-supported`)" }, "403": { "content": { @@ -6843,8 +14871,8 @@ "schema": { "example": { "code": 403, - "label": "invalid-credentials", - "message": "Authentication failed" + "label": "action-denied", + "message": "Insufficient authorization (missing modify_conversation_access)" }, "properties": { "code": { @@ -6855,7 +14883,9 @@ }, "label": { "enum": [ - "invalid-credentials" + "action-denied", + "invalid-op", + "access-denied" ], "type": "string" }, @@ -6872,38 +14902,27 @@ } } }, - "description": "Authentication failed (label: `invalid-credentials`)" - } - }, - "summary": "Obtain an access tokens for a cookie" - } - }, - "/access/logout": { - "post": { - "description": " [internal route ID: \"logout\"]\n\nCalling this endpoint will effectively revoke the given cookie and subsequent calls to /access with the same cookie will result in a 403.", - "responses": { - "200": { - "description": "Logout" + "description": "Insufficient authorization (missing modify_conversation_access) (label: `action-denied`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "invalid-credentials", - "message": "Authentication failed" + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "invalid-credentials" + "no-conversation" ], "type": "string" }, @@ -6920,67 +14939,63 @@ } } }, - "description": "Authentication failed (label: `invalid-credentials`)" + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" } }, - "summary": "Log out in order to remove a cookie from the server" + "summary": "Update history settings of a conversation" } }, - "/access/self/email": { - "put": { - "description": " [internal route ID: \"change-self-email\"]\n\n", + "/conversations/{cnv_domain}/{cnv}/members": { + "post": { + "description": " [internal route ID: \"add-members-to-conversation\"]\n\n", + "operationId": "add-members-to-conversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/EmailUpdate" + "$ref": "#/components/schemas/InviteQualified_ODYyODIyNjYz" } } }, "required": true }, "responses": { - "202": { + "200": { "content": { "application/json": { "schema": { - "example": [], - "items": {}, - "maxItems": 0, - "type": "array" + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" } }, "application/json;charset=utf-8": { "schema": { - "example": [], - "items": {}, - "maxItems": 0, - "type": "array" + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" } } }, - "description": "Update accepted and pending activation of the new email" + "description": "Conversation updated" }, "204": { - "content": { - "application/json": { - "schema": { - "example": [], - "items": {}, - "maxItems": 0, - "type": "array" - } - }, - "application/json;charset=utf-8": { - "schema": { - "example": [], - "items": {}, - "maxItems": 0, - "type": "array" - } - } - }, - "description": "No update, current and new email address are the same" + "description": "Conversation unchanged" }, "400": { "content": { @@ -6988,8 +15003,8 @@ "schema": { "example": { "code": 400, - "label": "invalid-email", - "message": "Invalid e-mail address." + "label": "mls-group-id-not-supported", + "message": "The group ID version of the conversation is not supported by one of the federated backends" }, "properties": { "code": { @@ -7000,7 +15015,7 @@ }, "label": { "enum": [ - "invalid-email" + "mls-group-id-not-supported" ], "type": "string" }, @@ -7017,7 +15032,7 @@ } } }, - "description": "Invalid e-mail address. (label: `invalid-email`) or `body`" + "description": "Invalid `body`\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)" }, "403": { "content": { @@ -7025,8 +15040,8 @@ "schema": { "example": { "code": 403, - "label": "invalid-credentials", - "message": "Authentication failed" + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" }, "properties": { "code": { @@ -7037,8 +15052,13 @@ }, "label": { "enum": [ - "invalid-credentials", - "blacklisted-email" + "missing-legalhold-consent", + "not-connected", + "no-team-member", + "access-denied", + "too-many-members", + "invalid-op", + "action-denied" ], "type": "string" }, @@ -7055,27 +15075,27 @@ } } }, - "description": "Authentication failed (label: `invalid-credentials`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)" + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nMaximum number of members per conversation reached (label: `too-many-members`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInsufficient authorization (missing add_conversation_member) (label: `action-denied`)" }, - "409": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "key-exists", - "message": "The given e-mail address is in use." + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { "enum": [ - 409 + 404 ], "type": "integer" }, "label": { "enum": [ - "key-exists" + "no-conversation" ], "type": "string" }, @@ -7092,74 +15112,151 @@ } } }, - "description": "The given e-mail address is in use. (label: `key-exists`)" + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" } }, - "summary": "Change your email address" - } - }, - "/activate": { - "get": { - "description": " [internal route ID: \"get-activate\"]\n\nSee also 'POST /activate' which has a larger feature set.
Calls federation service brig on send-connection-action", + "summary": "Add qualified members to an existing conversation." + }, + "put": { + "description": " [internal route ID: \"replace-members-in-conversation\"]\n\nThis will add any members not already in the conversation, and remove any members not in the provided list except users that are associated via a user group. The given role in the request body will be applied to all added members. The roles of already existing members will not be changed even if these members are included in the request body and their role differs from the role provided in this request.", + "operationId": "replace-members-in-conversation", "parameters": [ { - "description": "Activation key", - "in": "query", - "name": "key", + "in": "path", + "name": "cnv_domain", "required": true, "schema": { "type": "string" } }, { - "description": "Activation code", - "in": "query", - "name": "code", + "in": "path", + "name": "cnv", "required": true, "schema": { + "format": "uuid", "type": "string" } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InviteQualified_ODYyODIyNjYz" + } + } + }, + "required": true + }, "responses": { "200": { + "description": "Conversation members replaced" + }, + "400": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActivationResponse" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ActivationResponse" + "example": { + "code": 400, + "label": "mls-group-id-not-supported", + "message": "The group ID version of the conversation is not supported by one of the federated backends" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-group-id-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful." - }, - "204": { - "description": "A recent activation was already successful." + "description": "Invalid `body`\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-phone", - "message": "Invalid mobile phone number" + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "invalid-phone", - "invalid-email" + "missing-legalhold-consent", + "not-connected", + "no-team-member", + "access-denied", + "too-many-members", + "invalid-op", + "action-denied" ], "type": "string" }, @@ -7176,7 +15273,7 @@ } } }, - "description": "Invalid `code` or `key`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nMaximum number of members per conversation reached (label: `too-many-members`)\n\nInvalid operation (label: `invalid-op`)\n\nThe conversation would be left without an admin\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nInsufficient authorization (missing add_conversation_member) (label: `action-denied`)" }, "404": { "content": { @@ -7184,8 +15281,8 @@ "schema": { "example": { "code": 404, - "label": "invalid-code", - "message": "Invalid activation code" + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { @@ -7196,7 +15293,7 @@ }, "label": { "enum": [ - "invalid-code" + "no-conversation" ], "type": "string" }, @@ -7213,27 +15310,155 @@ } } }, - "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)" + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" }, "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Replace the members of a conversation." + } + }, + "/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}": { + "delete": { + "description": " [internal route ID: \"remove-member\"]\n\n", + "operationId": "remove-member", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Target User ID", + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Member removed" + }, + "204": { + "description": "No change" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "eligible_members": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "type": "array" + } + }, + "required": [ + "eligible_members" + ], + "type": "object" + } + } + }, + "description": "The conversation would be left without an admin\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" + }, + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "key-exists", - "message": "The given e-mail address is in use." + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { "enum": [ - 409 + 404 ], "type": "integer" }, "label": { "enum": [ - "key-exists" + "no-conversation" ], "type": "string" }, @@ -7250,18 +15475,57 @@ } } }, - "description": "The given e-mail address is in use. (label: `key-exists`)" + "description": "`cnv_domain` or `cnv` or `usr_domain` or `usr` not found\n\nConversation not found (label: `no-conversation`)" } }, - "summary": "Activate (i.e. confirm) an email address." + "summary": "Remove a member from a conversation" }, - "post": { - "description": " [internal route ID: \"post-activate\"]\n\nActivation only succeeds once and the number of failed attempts for a valid key is limited.Calls federation service brig on send-connection-action", + "put": { + "description": " [internal route ID: \"update-other-member\"]\n\n**Note**: at least one field has to be provided.", + "operationId": "update-other-member", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Target User ID", + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Activate" + "$ref": "#/components/schemas/OtherMemberUpdate_LTM1MjYzOTU0" } } }, @@ -7269,43 +15533,28 @@ }, "responses": { "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ActivationResponse" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ActivationResponse" - } - } - }, - "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful." - }, - "204": { - "description": "A recent activation was already successful." + "description": "Membership updated" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-phone", - "message": "Invalid mobile phone number" + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "invalid-phone", - "invalid-email" + "invalid-op", + "action-denied" ], "type": "string" }, @@ -7322,7 +15571,7 @@ } } }, - "description": "Invalid `body`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + "description": "Invalid operation (label: `invalid-op`)\n\nInvalid target (label: `invalid-op`)\n\nInsufficient authorization (missing modify_other_conversation_member) (label: `action-denied`)" }, "404": { "content": { @@ -7330,8 +15579,8 @@ "schema": { "example": { "code": 404, - "label": "invalid-code", - "message": "Invalid activation code" + "label": "no-conversation-member", + "message": "Conversation member not found" }, "properties": { "code": { @@ -7342,44 +15591,8 @@ }, "label": { "enum": [ - "invalid-code" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - } - }, - "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)" - }, - "409": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 409, - "label": "key-exists", - "message": "The given e-mail address is in use." - }, - "properties": { - "code": { - "enum": [ - 409 - ], - "type": "integer" - }, - "label": { - "enum": [ - "key-exists" + "no-conversation-member", + "no-conversation" ], "type": "string" }, @@ -7396,20 +15609,41 @@ } } }, - "description": "The given e-mail address is in use. (label: `key-exists`)" + "description": "`cnv_domain` or `cnv` or `usr_domain` or `usr` not found\n\nConversation member not found (label: `no-conversation-member`)\n\nConversation not found (label: `no-conversation`)" } }, - "summary": "Activate (i.e. confirm) an email address." + "summary": "Update membership of the specified user" } }, - "/activate/send": { - "post": { - "description": " [internal route ID: \"post-activate-send\"]\n\n", + "/conversations/{cnv_domain}/{cnv}/message-timer": { + "put": { + "description": " [internal route ID: \"update-conversation-message-timer\"]\n\n", + "operationId": "update-conversation-message-timer", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SendActivationCode" + "$ref": "#/components/schemas/ConversationMessageTimerUpdate_LTcxMjUwNzQ4" } } }, @@ -7417,27 +15651,44 @@ }, "responses": { "200": { - "description": "Activation code sent." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Message timer updated" }, - "400": { + "204": { + "description": "Message timer unchanged" + }, + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-email", - "message": "Invalid e-mail address." + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "invalid-email" + "invalid-op", + "access-denied", + "action-denied" ], "type": "string" }, @@ -7454,27 +15705,27 @@ } } }, - "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_message_timer) (label: `action-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "blacklisted-email", - "message": "The given e-mail address has been blacklisted due to a permanent bounce or a complaint." + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "blacklisted-email" + "no-conversation" ], "type": "string" }, @@ -7491,27 +15742,85 @@ } } }, - "description": "The given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)" + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update the message timer for a conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/name": { + "put": { + "description": " [internal route ID: \"update-conversation-name\"]\n\n", + "operationId": "update-conversation-name", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } }, - "409": { + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRename_ODkwODg1MzQ0" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Name unchanged" + }, + "204": { + "description": "Name updated" + }, + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "key-exists", - "message": "The given e-mail address is in use." + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" }, "properties": { "code": { "enum": [ - 409 + 403 ], "type": "integer" }, "label": { "enum": [ - "key-exists" + "invalid-op", + "action-denied" ], "type": "string" }, @@ -7528,27 +15837,27 @@ } } }, - "description": "The given e-mail address is in use. (label: `key-exists`)" + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)" }, - "451": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 451, - "label": "domain-blocked-for-registration", - "message": "[Customer extension] the email domain example.com that you are attempting to register a user with has been blocked for creating wire users. Please contact your IT department." + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { "enum": [ - 451 + 404 ], "type": "integer" }, "label": { "enum": [ - "domain-blocked-for-registration" + "no-conversation" ], "type": "string" }, @@ -7565,85 +15874,82 @@ } } }, - "description": "[Customer extension] the email domain example.com that you are attempting to register a user with has been blocked for creating wire users. Please contact your IT department. (label: `domain-blocked-for-registration`)" + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" } }, - "summary": "Send (or resend) an email activation code." - } - }, - "/api-version": { - "get": { - "description": " [internal route ID: \"get-version\"]\n\n", - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/VersionInfo" - } - } - }, - "description": "" - } - } + "summary": "Update conversation name" } }, - "/assets": { + "/conversations/{cnv_domain}/{cnv}/proteus/messages": { "post": { + "description": " [internal route ID: \"post-proteus-message\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n- `ignore_all`: When set, no checks about missing clients are carried out.\n- `report_only`: Takes a list of qualified UserIDs. If any clients of the listed users are missing, the message is not sent. The missing clients are reported in the response.\n- `ignore_only`: Takes a list of qualified UserIDs. If any clients of the non-listed users are missing, the message is not sent. The missing clients are reported in the response.\n\nThe sending of messages in a federated conversation could theoretically fail partially. To make this case unlikely, the backend first gets a list of clients from all the involved backends and then tries to send a message. So, if any backend is down, the message is not propagated to anyone. But the actual message fan out to multiple backends could still fail partially. This type of failure is reported as a 201, the clients for which the message sending failed are part of the response body.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "operationId": "post-proteus-message", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "requestBody": { "content": { - "multipart/mixed": { + "application/x-protobuf": { "schema": { - "$ref": "#/components/schemas/AssetSource" + "$ref": "#/components/schemas/QualifiedNewOtrMessage" } } }, - "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server." + "required": true }, "responses": { "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Asset" + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Asset" + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" } } }, - "description": "Asset posted", - "headers": { - "Location": { - "description": "Asset location", - "schema": { - "format": "url", - "type": "string" - } - } - } + "description": "Message sent" }, - "400": { + "403": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { "example": { - "code": 400, - "label": "invalid-length", - "message": "Invalid content length" + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "invalid-length" + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" ], "type": "string" }, @@ -7658,29 +15964,26 @@ ], "type": "object" } - } - }, - "description": "Invalid `body`\n\nInvalid content length (label: `invalid-length`)" - }, - "413": { - "content": { + }, "application/json;charset=utf-8": { "schema": { "example": { - "code": 413, - "label": "client-error", - "message": "Asset too large" + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" }, "properties": { "code": { "enum": [ - 413 + 403 ], "type": "integer" }, "label": { "enum": [ - "client-error" + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" ], "type": "string" }, @@ -7697,56 +16000,27 @@ } } }, - "description": "Asset too large (label: `client-error`)" - } - }, - "summary": "Upload an asset" - } - }, - "/assets/{key_domain}/{key}": { - "delete": { - "description": "**Note**: only local assets can be deleted.", - "parameters": [ - { - "in": "path", - "name": "key_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "key", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Asset deleted" + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" }, - "403": { + "404": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { "example": { - "code": 403, - "label": "unauthorised", - "message": "Unauthorised operation" + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "unauthorised" + "no-conversation" ], "type": "string" }, @@ -7761,18 +16035,13 @@ ], "type": "object" } - } - }, - "description": "Unauthorised operation (label: `unauthorised`)" - }, - "404": { - "content": { + }, "application/json;charset=utf-8": { "schema": { "example": { "code": 404, - "label": "not-found", - "message": "Asset not found" + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { @@ -7783,7 +16052,7 @@ }, "label": { "enum": [ - "not-found" + "no-conversation" ], "type": "string" }, @@ -7800,82 +16069,99 @@ } } }, - "description": "`key_domain` or `key` not found\n\nAsset not found (label: `not-found`)" + "description": "`cnv_domain` or `cnv` or Conversation not found (label: `no-conversation`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" + } + } + }, + "description": "Missing clients" } }, - "summary": "Delete an asset" - }, - "get": { - "description": "**Note**: local assets result in a redirect, while remote assets are streamed directly.Calls federation service cargohold on stream-asset
Calls federation service cargohold on get-asset", + "summary": "Post an encrypted message to a conversation (accepts only Protobuf)" + } + }, + "/conversations/{cnv_domain}/{cnv}/protocol": { + "put": { + "description": " [internal route ID: \"update-conversation-protocol\"]\n\n**Note**: Only proteus->mixed upgrade is supported.", + "operationId": "update-conversation-protocol", "parameters": [ { "in": "path", - "name": "key_domain", + "name": "cnv_domain", "required": true, "schema": { "type": "string" } }, { + "description": "Conversation ID", "in": "path", - "name": "key", + "name": "cnv", "required": true, "schema": { - "type": "string" - } - }, - { - "in": "header", - "name": "Asset-Token", - "required": false, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "asset_token", - "required": false, - "schema": { + "format": "uuid", "type": "string" } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ProtocolUpdate_NzY1ODgxNDQy" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Asset returned directly with content type `application/octet-stream`" - }, - "302": { - "description": "Asset found", - "headers": { - "Location": { - "description": "Asset location", + "content": { + "application/json": { "schema": { - "format": "url", - "type": "string" + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" } } - } + }, + "description": "Conversation updated" }, - "404": { + "204": { + "description": "Conversation unchanged" + }, + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "not-found", - "message": "Asset not found" + "code": 400, + "label": "mls-migration-criteria-not-satisfied", + "message": "The migration criteria for mixed to MLS protocol transition are not satisfied for this conversation" }, "properties": { "code": { "enum": [ - 404 + 400 ], "type": "integer" }, "label": { "enum": [ - "not-found" + "mls-migration-criteria-not-satisfied" ], "type": "string" }, @@ -7892,53 +16178,7 @@ } } }, - "description": "`key_domain` or `key` or Asset not found (label: `not-found`)\n\nAsset not found (label: `not-found`)" - } - }, - "summary": "Download an asset" - } - }, - "/assets/{key}/token": { - "delete": { - "description": "**Note**: deleting the token makes the asset public.", - "parameters": [ - { - "in": "path", - "name": "key", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Asset token deleted" - } - }, - "summary": "Delete an asset token" - }, - "post": { - "parameters": [ - { - "in": "path", - "name": "key", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/NewAssetToken" - } - } - }, - "description": "" + "description": "Invalid `body`\n\nThe migration criteria for mixed to MLS protocol transition are not satisfied for this conversation (label: `mls-migration-criteria-not-satisfied`)" }, "403": { "content": { @@ -7946,8 +16186,8 @@ "schema": { "example": { "code": 403, - "label": "unauthorised", - "message": "Unauthorised operation" + "label": "operation-denied", + "message": "Insufficient permissions" }, "properties": { "code": { @@ -7958,7 +16198,11 @@ }, "label": { "enum": [ - "unauthorised" + "operation-denied", + "no-team-member", + "invalid-op", + "action-denied", + "invalid-protocol-transition" ], "type": "string" }, @@ -7975,7 +16219,7 @@ } } }, - "description": "Unauthorised operation (label: `unauthorised`)" + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nProtocol transition is invalid (label: `invalid-protocol-transition`)" }, "404": { "content": { @@ -7983,8 +16227,8 @@ "schema": { "example": { "code": 404, - "label": "not-found", - "message": "Asset not found" + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { @@ -7995,7 +16239,8 @@ }, "label": { "enum": [ - "not-found" + "no-team", + "no-conversation" ], "type": "string" }, @@ -8012,97 +16257,87 @@ } } }, - "description": "`key` not found\n\nAsset not found (label: `not-found`)" + "description": "`cnv_domain` or `cnv` not found\n\nTeam not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" } }, - "summary": "Renew an asset token" + "summary": "Update the protocol of the conversation" } }, - "/await": { - "get": { - "description": " [internal route ID: \"await-notifications\"]\n\n", - "externalDocs": { - "description": "RFC 6455", - "url": "https://datatracker.ietf.org/doc/html/rfc6455" - }, + "/conversations/{cnv_domain}/{cnv}/receipt-mode": { + "put": { + "description": " [internal route ID: \"update-conversation-receipt-mode\"]\n\n", + "operationId": "update-conversation-receipt-mode", "parameters": [ { - "description": "Client ID", - "in": "query", - "name": "client", - "required": false, + "in": "path", + "name": "cnv_domain", + "required": true, "schema": { "type": "string" } - } - ], - "responses": { - "101": { - "description": "Connection upgraded." }, - "426": { - "description": "Upgrade required." + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } } - }, - "summary": "Establish websocket connection" - } - }, - "/bot/assets": { - "post": { + ], "requestBody": { "content": { - "multipart/mixed": { + "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/AssetSource" + "$ref": "#/components/schemas/ConversationReceiptModeUpdate_NDE4MzUzNTU3" } } }, - "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server." + "required": true }, "responses": { - "201": { + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Asset" + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Asset" + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" } } }, - "description": "Asset posted", - "headers": { - "Location": { - "description": "Asset location", - "schema": { - "format": "url", - "type": "string" - } - } - } + "description": "Receipt mode updated" }, - "400": { + "204": { + "description": "Receipt mode unchanged" + }, + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-length", - "message": "Invalid content length" + "code": 403, + "label": "mls-receipts-not-allowed", + "message": "Read receipts on MLS conversations are not allowed" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "invalid-length" + "mls-receipts-not-allowed", + "invalid-op", + "access-denied", + "action-denied" ], "type": "string" }, @@ -8119,27 +16354,27 @@ } } }, - "description": "Invalid `body`\n\nInvalid content length (label: `invalid-length`)" + "description": "Read receipts on MLS conversations are not allowed (label: `mls-receipts-not-allowed`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_receipt_mode) (label: `action-denied`)" }, - "413": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 413, - "label": "client-error", - "message": "Asset too large" + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { "enum": [ - 413 + 404 ], "type": "integer" }, "label": { "enum": [ - "client-error" + "no-conversation" ], "type": "string" }, @@ -8156,64 +16391,46 @@ } } }, - "description": "Asset too large (label: `client-error`)" + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" } }, - "summary": "Upload an asset" + "summary": "Update receipt mode for a conversation" } }, - "/bot/assets/{key}": { - "delete": { + "/conversations/{cnv_domain}/{cnv}/self": { + "get": { + "description": " [internal route ID: \"get-conversation-self\"]\n\n", + "operationId": "get-conversation-self", "parameters": [ { "in": "path", - "name": "key", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", "required": true, "schema": { + "format": "uuid", "type": "string" } } ], "responses": { "200": { - "description": "Asset deleted" - }, - "403": { "content": { "application/json;charset=utf-8": { "schema": { - "example": { - "code": 403, - "label": "unauthorised", - "message": "Unauthorised operation" - }, - "properties": { - "code": { - "enum": [ - 403 - ], - "type": "integer" - }, - "label": { - "enum": [ - "unauthorised" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" + "$ref": "#/components/schemas/Member_OTA5OTgyNzcw" } } }, - "description": "Unauthorised operation (label: `unauthorised`)" + "description": "" }, "404": { "content": { @@ -8221,8 +16438,8 @@ "schema": { "example": { "code": 404, - "label": "not-found", - "message": "Asset not found" + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { @@ -8233,7 +16450,7 @@ }, "label": { "enum": [ - "not-found" + "no-conversation" ], "type": "string" }, @@ -8250,91 +16467,56 @@ } } }, - "description": "`key` not found\n\nAsset not found (label: `not-found`)" + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" } }, - "summary": "Delete an asset" + "summary": "Get self membership properties" }, - "get": { + "put": { + "description": " [internal route ID: \"update-conversation-self\"]\n\n**Note**: at least one field has to be provided.", + "operationId": "update-conversation-self", "parameters": [ { "in": "path", - "name": "key", + "name": "cnv_domain", "required": true, "schema": { "type": "string" } }, { - "in": "header", - "name": "Asset-Token", - "required": false, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "asset_token", - "required": false, + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, "schema": { + "format": "uuid", "type": "string" } } ], - "responses": { - "302": { - "description": "Asset found", - "headers": { - "Location": { - "description": "Asset location", - "schema": { - "format": "url", - "type": "string" - } + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MemberUpdate_LTg4NTQ0OTYz" } } }, + "required": true + }, + "responses": { + "200": { + "description": "Update successful" + }, "404": { "content": { - "application/json": { - "schema": { - "example": { - "code": 404, - "label": "not-found", - "message": "Asset not found" - }, - "properties": { - "code": { - "enum": [ - 404 - ], - "type": "integer" - }, - "label": { - "enum": [ - "not-found" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - }, "application/json;charset=utf-8": { "schema": { "example": { "code": 404, - "label": "not-found", - "message": "Asset not found" + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { @@ -8345,7 +16527,7 @@ }, "label": { "enum": [ - "not-found" + "no-conversation" ], "type": "string" }, @@ -8362,50 +16544,94 @@ } } }, - "description": "`key` or Asset not found (label: `not-found`)" + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" } }, - "summary": "Download an asset" + "summary": "Update self membership properties" } }, - "/bot/client": { - "get": { - "description": " [internal route ID: \"bot-get-client-v6\"]\n\n", + "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}": { + "delete": { + "description": " [internal route ID: \"delete-subconversation\"]\n\n", + "operationId": "delete-subconversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSReset_NzgwODA3ODc4" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Clientv6" + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Clientv6" + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" } } }, - "description": "Client found" + "description": "Deletion successful" }, - "403": { + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "access-denied", - "message": "Access denied." + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" }, "properties": { "code": { "enum": [ - 403 + 400 ], "type": "integer" }, "label": { "enum": [ - "access-denied" + "mls-not-enabled" ], "type": "string" }, @@ -8422,27 +16648,27 @@ } } }, - "description": "Access denied. (label: `access-denied`)" + "description": "Invalid `body`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" }, - "404": { + "403": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "client-not-found", - "message": "Client not found" + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "client-not-found" + "access-denied" ], "type": "string" }, @@ -8457,13 +16683,18 @@ ], "type": "object" } - }, + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { "code": 404, - "label": "client-not-found", - "message": "Client not found" + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { @@ -8474,7 +16705,7 @@ }, "label": { "enum": [ - "client-not-found" + "no-conversation" ], "type": "string" }, @@ -8491,50 +16722,27 @@ } } }, - "description": "Client not found (label: `client-not-found`)\n\nClient not found (label: `client-not-found`)" - } - }, - "summary": "Get client for bot" - } - }, - "/bot/client/prekeys": { - "get": { - "description": " [internal route ID: \"bot-list-prekeys\"]\n\n", - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "items": { - "maximum": 65535, - "minimum": 0, - "type": "integer" - }, - "type": "array" - } - } - }, - "description": "" + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" }, - "403": { + "409": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "access-denied", - "message": "Access denied." + "code": 409, + "label": "mls-stale-message", + "message": "The conversation epoch in a message is too old" }, "properties": { "code": { "enum": [ - 403 + 409 ], "type": "integer" }, "label": { "enum": [ - "access-denied" + "mls-stale-message" ], "type": "string" }, @@ -8551,26 +16759,56 @@ } } }, - "description": "Access denied. (label: `access-denied`)" + "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" } }, - "summary": "List prekeys for bot" + "summary": "Delete an MLS subconversation" }, - "post": { - "description": " [internal route ID: \"bot-update-prekeys\"]\n\n", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/UpdateBotPrekeys" - } + "get": { + "description": " [internal route ID: \"get-subconversation\"]\n\n", + "operationId": "get-subconversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" } }, - "required": true - }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { - "description": "" + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicSubConversation_MjI2NTIxMzU4" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PublicSubConversation_MjI2NTIxMzU4" + } + } + }, + "description": "Subconversation" }, "403": { "content": { @@ -8578,8 +16816,8 @@ "schema": { "example": { "code": 403, - "label": "access-denied", - "message": "Access denied." + "label": "mls-subconv-unsupported-convtype", + "message": "MLS subconversations are only supported for regular conversations" }, "properties": { "code": { @@ -8590,6 +16828,7 @@ }, "label": { "enum": [ + "mls-subconv-unsupported-convtype", "access-denied" ], "type": "string" @@ -8607,7 +16846,7 @@ } } }, - "description": "Access denied. (label: `access-denied`)" + "description": "MLS subconversations are only supported for regular conversations (label: `mls-subconv-unsupported-convtype`)\n\nConversation access denied (label: `access-denied`)" }, "404": { "content": { @@ -8615,8 +16854,8 @@ "schema": { "example": { "code": 404, - "label": "client-not-found", - "message": "Client not found" + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { @@ -8627,7 +16866,7 @@ }, "label": { "enum": [ - "client-not-found" + "no-conversation" ], "type": "string" }, @@ -8644,47 +16883,73 @@ } } }, - "description": "Client not found (label: `client-not-found`)" + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get information about an MLS subconversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}/groupinfo": { + "get": { + "description": " [internal route ID: \"get-subconversation-group-info\"]\n\n", + "operationId": "get-subconversation-group-info", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } } - }, - "summary": "Update prekeys for bot" - } - }, - "/bot/conversation": { - "get": { - "description": " [internal route ID: \"get-bot-conversation\"]\n\n", + ], "responses": { "200": { "content": { - "application/json;charset=utf-8": { + "message/mls": { "schema": { - "$ref": "#/components/schemas/BotConvView" + "$ref": "#/components/schemas/GroupInfoData" } } }, - "description": "" + "description": "The group information" }, - "403": { + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" }, "properties": { "code": { "enum": [ - 403 + 400 ], "type": "integer" }, "label": { "enum": [ - "no-team-member", - "operation-denied", - "access-denied" + "mls-not-enabled" ], "type": "string" }, @@ -8701,7 +16966,7 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" }, "404": { "content": { @@ -8709,8 +16974,8 @@ "schema": { "example": { "code": 404, - "label": "no-team", - "message": "Team not found" + "label": "mls-missing-group-info", + "message": "The conversation has no group information" }, "properties": { "code": { @@ -8721,7 +16986,7 @@ }, "label": { "enum": [ - "no-team", + "mls-missing-group-info", "no-conversation" ], "type": "string" @@ -8739,79 +17004,67 @@ } } }, - "description": "Team not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nThe conversation has no group information (label: `mls-missing-group-info`)\n\nConversation not found (label: `no-conversation`)" } - } + }, + "summary": "Get MLS group information of subconversation" } }, - "/bot/messages": { - "post": { - "description": " [internal route ID: \"post-bot-message-unqualified\"]\n\nCalls federation service brig on get-user-clients
Calls federation service galley on on-message-sent", + "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}/self": { + "delete": { + "description": " [internal route ID: \"leave-subconversation\"]\n\n", + "operationId": "leave-subconversation", "parameters": [ { - "in": "query", - "name": "ignore_missing", - "required": false, + "in": "path", + "name": "cnv_domain", + "required": true, "schema": { "type": "string" } }, { - "in": "query", - "name": "report_missing", - "required": false, + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/new-otr-message" - } - } - }, - "required": true - }, "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ClientMismatch" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ClientMismatch" - } - } - }, - "description": "Message sent" + "200": { + "description": "OK" }, - "403": { + "400": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "unknown-client", - "message": "Unknown Client" + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" }, "properties": { "code": { "enum": [ - 403 + 400 ], "type": "integer" }, "label": { "enum": [ - "unknown-client", - "missing-legalhold-consent-old-clients", - "missing-legalhold-consent" + "mls-not-enabled", + "mls-protocol-error" ], "type": "string" }, @@ -8826,13 +17079,18 @@ ], "type": "object" } - }, + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nMLS protocol error (label: `mls-protocol-error`)" + }, + "403": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { "code": 403, - "label": "unknown-client", - "message": "Unknown Client" + "label": "access-denied", + "message": "Conversation access denied" }, "properties": { "code": { @@ -8843,9 +17101,7 @@ }, "label": { "enum": [ - "unknown-client", - "missing-legalhold-consent-old-clients", - "missing-legalhold-consent" + "access-denied" ], "type": "string" }, @@ -8862,11 +17118,11 @@ } } }, - "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + "description": "Conversation access denied (label: `access-denied`)" }, "404": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { "code": 404, @@ -8897,24 +17153,29 @@ ], "type": "object" } - }, + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "code": 409, + "label": "mls-stale-message", + "message": "The conversation epoch in a message is too old" }, "properties": { "code": { "enum": [ - 404 + 409 ], "type": "integer" }, "label": { "enum": [ - "no-conversation" + "mls-stale-message" ], "type": "string" }, @@ -8931,53 +17192,69 @@ } } }, - "description": "Conversation not found (label: `no-conversation`)\n\nConversation not found (label: `no-conversation`)" - }, - "412": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ClientMismatch" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ClientMismatch" - } - } - }, - "description": "Missing clients" + "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" } - } + }, + "summary": "Leave an MLS subconversation" } }, - "/bot/self": { - "delete": { - "description": " [internal route ID: \"bot-delete-self\"]\n\n", + "/conversations/{cnv_domain}/{cnv}/typing": { + "post": { + "description": " [internal route ID: \"member-typing-qualified\"]\n\n", + "operationId": "member-typing-qualified", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TypingStatus_LTg5MzcyNDMy" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "" + "description": "Notification sent" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "invalid-bot", - "message": "The targeted user is not a bot." + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "invalid-bot", - "access-denied" + "no-conversation" ], "type": "string" }, @@ -8994,23 +17271,43 @@ } } }, - "description": "The targeted user is not a bot. (label: `invalid-bot`)\n\nAccess denied. (label: `access-denied`)" + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" } }, - "summary": "Delete self" - }, - "get": { - "description": " [internal route ID: \"bot-get-self\"]\n\n", + "summary": "Sending typing notifications" + } + }, + "/conversations/{cnv}/code": { + "delete": { + "description": " [internal route ID: \"remove-code-unqualified\"]\n\n", + "operationId": "remove-code-unqualified", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "responses": { "200": { "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/UserProfile" + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" } } }, - "description": "" + "description": "Conversation code deleted." }, "403": { "content": { @@ -9019,7 +17316,7 @@ "example": { "code": 403, "label": "access-denied", - "message": "Access denied." + "message": "Conversation access denied" }, "properties": { "code": { @@ -9047,7 +17344,7 @@ } } }, - "description": "Access denied. (label: `access-denied`)" + "description": "Conversation access denied (label: `access-denied`)" }, "404": { "content": { @@ -9055,8 +17352,8 @@ "schema": { "example": { "code": 404, - "label": "not-found", - "message": "User not found" + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { @@ -9067,7 +17364,7 @@ }, "label": { "enum": [ - "not-found" + "no-conversation" ], "type": "string" }, @@ -9084,21 +17381,22 @@ } } }, - "description": "User not found (label: `not-found`)" + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" } }, - "summary": "Get self" - } - }, - "/bot/users": { + "summary": "Delete conversation code" + }, "get": { - "description": " [internal route ID: \"bot-list-users\"]\n\n", + "description": " [internal route ID: \"get-code\"]\n\n\nOAuth scope: `write:conversations_code`", + "operationId": "get-code", "parameters": [ { - "in": "query", - "name": "ids", + "description": "Conversation ID", + "in": "path", + "name": "cnv", "required": true, "schema": { + "format": "uuid", "type": "string" } } @@ -9106,16 +17404,18 @@ "responses": { "200": { "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo_LTc5MzgzNjg3" + } + }, "application/json;charset=utf-8": { "schema": { - "items": { - "$ref": "#/components/schemas/BotUserView" - }, - "type": "array" + "$ref": "#/components/schemas/ConversationCodeInfo_LTc5MzgzNjg3" } } }, - "description": "" + "description": "Conversation Code" }, "403": { "content": { @@ -9124,7 +17424,7 @@ "example": { "code": 403, "label": "access-denied", - "message": "Access denied." + "message": "Conversation access denied" }, "properties": { "code": { @@ -9152,58 +17452,28 @@ } } }, - "description": "Access denied. (label: `access-denied`)" - } - }, - "summary": "List users" - } - }, - "/bot/users/prekeys": { - "post": { - "description": " [internal route ID: \"bot-claim-users-prekeys\"]\n\n", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/UserClients" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/UserClientPrekeyMap" - } - } - }, - "description": "" + "description": "Conversation access denied (label: `access-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "missing-legalhold-consent", - "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "missing-legalhold-consent", - "missing-legalhold-consent-old-clients", - "too-many-clients", - "access-denied" + "no-conversation", + "no-conversation-code" ], "type": "string" }, @@ -9220,59 +17490,27 @@ } } }, - "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nToo many clients (label: `too-many-clients`)\n\nAccess denied. (label: `access-denied`)" - } - }, - "summary": "Claim users prekeys" - } - }, - "/bot/users/{User ID}/clients": { - "get": { - "description": " [internal route ID: \"bot-get-user-clients\"]\n\n", - "parameters": [ - { - "in": "path", - "name": "User ID", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "items": { - "$ref": "#/components/schemas/PubClient" - }, - "type": "array" - } - } - }, - "description": "" + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" }, - "403": { + "409": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "access-denied", - "message": "Access denied." + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" }, "properties": { "code": { "enum": [ - 403 + 409 ], "type": "integer" }, "label": { "enum": [ - "access-denied" + "guest-links-disabled" ], "type": "string" }, @@ -9289,29 +17527,22 @@ } } }, - "description": "Access denied. (label: `access-denied`)" + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" } }, - "summary": "Get user clients" - } - }, - "/broadcast/otr/messages": { + "summary": "Get existing conversation code" + }, "post": { - "description": " [internal route ID: \"post-otr-broadcast-unqualified\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts two query params:\n- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are ignored.\n - When 'false' all missing clients are reported.\n - When comma separated list of user-ids, only clients for listed users are ignored.\n- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are reported.\n - When 'false' all missing clients are ignored.\n - When comma separated list of user-ids, only clients for listed users are reported.\n\nApart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\nAll three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n- `report_missing` in the request body has highest precedence.\n- `ignore_missing` in the query param is the next.\n- `report_missing` in the query param has the lowest precedence.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "description": " [internal route ID: \"create-conversation-code-unqualified\"]\n\n\nOAuth scope: `write:conversations_code`", + "operationId": "create-conversation-code-unqualified", "parameters": [ { - "in": "query", - "name": "ignore_missing", - "required": false, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "report_missing", - "required": false, + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, "schema": { + "format": "uuid", "type": "string" } } @@ -9320,52 +17551,62 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/new-otr-message" - } - }, - "application/x-protobuf": { - "schema": { - "$ref": "#/components/schemas/new-otr-message" + "$ref": "#/components/schemas/CreateConversationCodeRequest_NTYzMTA1NDYz" } } }, "required": true }, "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo_LTc5MzgzNjg3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo_LTc5MzgzNjg3" + } + } + }, + "description": "Conversation code already exists." + }, "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ClientMismatch" + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ClientMismatch" + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" } } }, - "description": "Message sent" + "description": "Conversation code created." }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "too-many-users-to-broadcast", - "message": "Too many users to fan out the broadcast event to" + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "too-many-users-to-broadcast" + "access-denied" ], "type": "string" }, @@ -9382,29 +17623,27 @@ } } }, - "description": "Invalid `body` or `report_missing` or `ignore_missing`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + "description": "Conversation access denied (label: `access-denied`)" }, - "403": { + "404": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "unknown-client", - "message": "Unknown Client" + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "unknown-client", - "missing-legalhold-consent-old-clients", - "missing-legalhold-consent" + "no-conversation" ], "type": "string" }, @@ -9419,26 +17658,30 @@ ], "type": "object" } - }, + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "unknown-client", - "message": "Unknown Client" + "code": 409, + "label": "create-conv-code-conflict", + "message": "Conversation code already exists with a different password setting than the requested one." }, "properties": { "code": { "enum": [ - 403 + 409 ], "type": "integer" }, "label": { "enum": [ - "unknown-client", - "missing-legalhold-consent-old-clients", - "missing-legalhold-consent" + "create-conv-code-conflict", + "guest-links-disabled" ], "type": "string" }, @@ -9455,27 +17698,58 @@ } } }, - "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" - }, - "404": { + "description": "Conversation code already exists with a different password setting than the requested one. (label: `create-conv-code-conflict`)\n\nThe guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Create or recreate a conversation code" + } + }, + "/conversations/{cnv}/features/conversationGuestLinks": { + "get": { + "description": " [internal route ID: \"get-conversation-guest-links-status\"]\n\n", + "operationId": "get-conversation-guest-links-status", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { - "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "$ref": "#/components/schemas/LockableFeature_GuestLinksConfig_LTcwNjU0NDMw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-conversation" + "access-denied" ], "type": "string" }, @@ -9490,7 +17764,12 @@ ], "type": "object" } - }, + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { @@ -9507,9 +17786,7 @@ }, "label": { "enum": [ - "no-conversation", - "non-binding-team", - "no-team" + "no-conversation" ], "type": "string" }, @@ -9526,35 +17803,53 @@ } } }, - "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)" - }, - "412": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ClientMismatch" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ClientMismatch" - } - } - }, - "description": "Missing clients" + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" } }, - "summary": "Broadcast an encrypted message to all team members and all contacts (accepts JSON or Protobuf)" + "summary": "Get the status of the guest links feature for a conversation that potentially has been created by someone from another team." } }, - "/broadcast/proteus/messages": { + "/conversations/{cnv}/otr/messages": { "post": { - "description": " [internal route ID: \"post-proteus-broadcast\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n- `ignore_all`: When set, no checks about missing clients are carried out.\n- `report_only`: Takes a list of qualified UserIDs. If any clients of the listed users are missing, the message is not sent. The missing clients are reported in the response.\n- `ignore_only`: Takes a list of qualified UserIDs. If any clients of the non-listed users are missing, the message is not sent. The missing clients are reported in the response.\n\nThe sending of messages in a federated conversation could theoretically fail partially. To make this case unlikely, the backend first gets a list of clients from all the involved backends and then tries to send a message. So, if any backend is down, the message is not propagated to anyone. But the actual message fan out to multiple backends could still fail partially. This type of failure is reported as a 201, the clients for which the message sending failed are part of the response body.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "description": " [internal route ID: \"post-otr-message-unqualified\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts two query params:\n- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are ignored.\n - When 'false' all missing clients are reported.\n - When comma separated list of user-ids, only clients for listed users are ignored.\n- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are reported.\n - When 'false' all missing clients are ignored.\n - When comma separated list of user-ids, only clients for listed users are reported.\n\nApart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\nAll three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n- `report_missing` in the request body has highest precedence.\n- `ignore_missing` in the query param is the next.\n- `report_missing` in the query param has the lowest precedence.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "operationId": "post-otr-message-unqualified", + "parameters": [ + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "ignore_missing", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "schema": { + "type": "string" + } + } + ], "requestBody": { "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewOtrMessage_LTUyMTE5MTMw" + } + }, "application/x-protobuf": { "schema": { - "$ref": "#/components/schemas/QualifiedNewOtrMessage" + "$ref": "#/components/schemas/NewOtrMessage_LTUyMTE5MTMw" } } }, @@ -9565,54 +17860,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MessageSendingStatus" + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MessageSendingStatus" + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" } } }, "description": "Message sent" }, - "400": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 400, - "label": "too-many-users-to-broadcast", - "message": "Too many users to fan out the broadcast event to" - }, - "properties": { - "code": { - "enum": [ - 400 - ], - "type": "integer" - }, - "label": { - "enum": [ - "too-many-users-to-broadcast" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - } - }, - "description": "Invalid `body`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" - }, "403": { "content": { "application/json": { @@ -9736,9 +17994,7 @@ }, "label": { "enum": [ - "no-conversation", - "non-binding-team", - "no-team" + "no-conversation" ], "type": "string" }, @@ -9755,59 +18011,39 @@ } } }, - "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)" + "description": "`cnv` or Conversation not found (label: `no-conversation`)" }, "412": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MessageSendingStatus" + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MessageSendingStatus" + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" } } }, "description": "Missing clients" } }, - "summary": "Post an encrypted message to all team members and all contacts (accepts only Protobuf)" - } - }, - "/calls/config": { - "get": { - "deprecated": true, - "description": " [internal route ID: \"get-calls-config\"]\n\n", - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/RTCConfiguration" - } - } - }, - "description": "" - } - }, - "summary": "Retrieve TURN server addresses and credentials for IP addresses, scheme `turn` and transport `udp` only (deprecated)" + "summary": "Post an encrypted message to a conversation (accepts JSON or Protobuf)" } }, - "/calls/config/v2": { + "/conversations/{cnv}/roles": { "get": { - "description": " [internal route ID: \"get-calls-config-v2\"]\n\n", + "description": " [internal route ID: \"get-conversation-roles\"]\n\n", + "operationId": "get-conversation-roles", "parameters": [ { - "description": "Limit resulting list. Allowed values [1..10]", - "in": "query", - "name": "limit", - "required": false, + "in": "path", + "name": "cnv", + "required": true, "schema": { - "maximum": 10, - "minimum": 1, - "type": "integer" + "format": "uuid", + "type": "string" } } ], @@ -9816,93 +18052,31 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/RTCConfiguration" + "$ref": "#/components/schemas/ConversationRolesList" } } }, "description": "" - } - }, - "summary": "Retrieve all TURN server addresses and credentials. Clients are expected to do a DNS lookup to resolve the IP addresses of the given hostnames " - } - }, - "/clients": { - "get": { - "description": " [internal route ID: \"list-clients-v6\"]\n\n", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ClientListv6" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ClientListv6" - } - } - }, - "description": "List of clients" - } - }, - "summary": "List the registered clients" - }, - "post": { - "description": " [internal route ID: \"add-client\"]\n\nCalls federation service brig on send-connection-action", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/NewClient" - } - } - }, - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Client" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/Client" - } - } - }, - "description": "Client registered", - "headers": { - "Location": { - "description": "Client ID", - "schema": { - "type": "string" - } - } - } }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "bad-request", - "message": "Malformed prekeys uploaded" + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "bad-request" + "access-denied" ], "type": "string" }, @@ -9919,30 +18093,27 @@ } } }, - "description": "Invalid `body`\n\nMalformed prekeys uploaded (label: `bad-request`)" + "description": "Conversation access denied (label: `access-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "code-authentication-required", - "message": "Code authentication is required" + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "code-authentication-required", - "code-authentication-failed", - "missing-auth", - "too-many-clients" + "no-conversation" ], "type": "string" }, @@ -9959,29 +18130,22 @@ } } }, - "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nToo many clients (label: `too-many-clients`)" + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" } }, - "summary": "Register a new client" + "summary": "Get existing roles available for the given conversation" } }, - "/clients/{cid}/access-token": { - "post": { - "description": " [internal route ID: \"create-access-token\"]\n\nCreate an JWT DPoP access token for the client CSR, given a JWT DPoP proof, specified in the `DPoP` header. The access token will be returned in the JSON response body as a JWT DPoP token.", + "/cookies": { + "get": { + "description": " [internal route ID: \"list-cookies\"]\n\n", + "operationId": "list-cookies", "parameters": [ { - "description": "ClientId", - "in": "path", - "name": "cid", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "header", - "name": "DPoP", - "required": true, + "description": "Filter by label (comma-separated list)", + "in": "query", + "name": "labels", + "required": false, "schema": { "type": "string" } @@ -9992,112 +18156,30 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DPoPAccessTokenResponse" + "$ref": "#/components/schemas/CookieList_LTM4MzYwNzAz" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/DPoPAccessTokenResponse" + "$ref": "#/components/schemas/CookieList_LTM4MzYwNzAz" } } }, - "description": "Access token created", - "headers": { - "Cache-Control": { - "schema": { - "type": "string" - } - } - } + "description": "List of cookies" } }, - "summary": "Create a JWT DPoP access token" + "summary": "Retrieve the list of cookies currently stored for the user" } }, - "/clients/{client}": { - "delete": { - "description": " [internal route ID: \"delete-client\"]\n\n", - "parameters": [ - { - "description": "ClientId", - "in": "path", - "name": "client", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/DeleteClient" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Client deleted" - } - }, - "summary": "Delete an existing client" - }, - "get": { - "description": " [internal route ID: \"get-client-v6\"]\n\n", - "parameters": [ - { - "description": "ClientId", - "in": "path", - "name": "client", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Clientv6" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/Clientv6" - } - } - }, - "description": "Client found" - }, - "404": { - "description": "`client` or Client not found(**Note**: This error has an empty body for legacy reasons)" - } - }, - "summary": "Get a registered client by ID" - }, - "put": { - "description": " [internal route ID: \"update-client\"]\n\n", - "parameters": [ - { - "description": "ClientId", - "in": "path", - "name": "client", - "required": true, - "schema": { - "type": "string" - } - } - ], + "/cookies/remove": { + "post": { + "description": " [internal route ID: \"remove-cookies\"]\n\n", + "operationId": "remove-cookies", "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/UpdateClient" + "$ref": "#/components/schemas/RemoveCookies_OTYwMTI0NDMy" } } }, @@ -10105,27 +18187,27 @@ }, "responses": { "200": { - "description": "Client updated" + "description": "Cookies revoked" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "bad-request", - "message": "Malformed prekeys uploaded" + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "bad-request" + "invalid-credentials" ], "type": "string" }, @@ -10142,20 +18224,21 @@ } } }, - "description": "Invalid `body`\n\nMalformed prekeys uploaded (label: `bad-request`)" + "description": "Authentication failed (label: `invalid-credentials`)" } }, - "summary": "Update a registered client" + "summary": "Revoke stored cookies" } }, - "/clients/{client}/capabilities": { + "/custom-backend/by-domain/{domain}": { "get": { - "description": " [internal route ID: \"get-client-capabilities\"]\n\n", + "description": " [internal route ID: \"get-custom-backend-by-domain\"]\n\n", + "operationId": "get-custom-backend-by-domain", "parameters": [ { - "description": "ClientId", + "description": "URL-encoded email domain", "in": "path", - "name": "client", + "name": "domain", "required": true, "schema": { "type": "string" @@ -10167,233 +18250,196 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ClientCapabilityList" + "$ref": "#/components/schemas/CustomBackend_LTQxODI0MjQ0" } } }, "description": "" - } - }, - "summary": "Read back what the client has been posting about itself" - } - }, - "/clients/{client}/nonce": { - "get": { - "description": " [internal route ID: \"get-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", - "parameters": [ - { - "description": "ClientId", - "in": "path", - "name": "client", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "No Content", - "headers": { - "Cache-Control": { - "schema": { - "type": "string" - } - }, - "Replay-Nonce": { - "schema": { - "type": "string" - } - } - } - } - }, - "summary": "Get a new nonce for a client CSR" - }, - "head": { - "description": " [internal route ID: \"head-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", - "parameters": [ - { - "description": "ClientId", - "in": "path", - "name": "client", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "No Content", - "headers": { - "Cache-Control": { - "schema": { - "type": "string" - } - }, - "Replay-Nonce": { - "schema": { - "type": "string" - } - } - } - } - }, - "summary": "Get a new nonce for a client CSR" - } - }, - "/clients/{client}/prekeys": { - "get": { - "description": " [internal route ID: \"get-client-prekeys\"]\n\n", - "parameters": [ - { - "description": "ClientId", - "in": "path", - "name": "client", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { + }, + "404": { "content": { "application/json;charset=utf-8": { "schema": { - "items": { - "maximum": 65535, - "minimum": 0, - "type": "integer" + "example": { + "code": 404, + "label": "custom-backend-not-found", + "message": "Custom backend not found" }, - "type": "array" + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "custom-backend-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "" + "description": "`domain` not found\n\nCustom backend not found (label: `custom-backend-not-found`)" } - }, - "summary": "List the remaining prekey IDs of a client" - } - }, - "/connections/{uid_domain}/{uid}": { - "get": { - "description": " [internal route ID: \"get-connection\"]\n\n", - "parameters": [ - { - "in": "path", - "name": "uid_domain", - "required": true, - "schema": { - "type": "string" + }, + "summary": "Shows information about custom backends related to a given email domain" + } + }, + "/delete": { + "post": { + "description": " [internal route ID: \"verify-delete\"]\n\n", + "operationId": "verify-delete", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/VerifyDeleteUser_Njc1NDQ1MDIy" + } } }, - { - "description": "User Id", - "in": "path", - "name": "uid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], + "required": true + }, "responses": { "200": { + "description": "Deletion is initiated." + }, + "403": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserConnection" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/UserConnection" + "example": { + "code": 403, + "label": "invalid-code", + "message": "Invalid verification code" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "Connection found" - }, - "404": { - "description": "`uid_domain` or `uid` or Connection not found(**Note**: This error has an empty body for legacy reasons)" + "description": "Invalid verification code (label: `invalid-code`)" } }, - "summary": "Get an existing connection to another user (local or remote)" - }, + "summary": "Verify account deletion with a code." + } + }, + "/domain-verification/{domain}/authorize-team": { "post": { - "description": " [internal route ID: \"create-connection\"]\n\nYou can have no more than 1000 connections in accepted or sent state
Calls federation service brig on send-connection-action
Calls federation service brig on get-users-by-ids", + "description": " [internal route ID: \"domain-verification-authorize-team\"]\n\n", + "operationId": "domain-verification-authorize-team", "parameters": [ { "in": "path", - "name": "uid_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "User Id", - "in": "path", - "name": "uid", + "name": "domain", "required": true, "schema": { - "format": "uuid", "type": "string" } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainOwnershipToken_NTU0ODc1NDE5" + } + } + }, + "required": true + }, "responses": { "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserConnection" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/UserConnection" - } - } - }, - "description": "Connection existed" + "description": "Authorized" }, - "201": { + "401": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserConnection" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/UserConnection" + "example": { + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" + }, + "properties": { + "code": { + "enum": [ + 401 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-auth-failure" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "Connection was created" + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" }, - "400": { + "402": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-user", - "message": "Invalid user" + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" }, "properties": { "code": { "enum": [ - 400 + 402 ], "type": "integer" }, "label": { "enum": [ - "invalid-user" + "domain-registration-update-payment-required" ], "type": "string" }, @@ -10410,7 +18456,7 @@ } } }, - "description": "Invalid user (label: `invalid-user`)" + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" }, "403": { "content": { @@ -10418,8 +18464,8 @@ "schema": { "example": { "code": 403, - "label": "no-identity", - "message": "The user has no verified email" + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" }, "properties": { "code": { @@ -10430,10 +18476,7 @@ }, "label": { "enum": [ - "no-identity", - "connection-limit", - "missing-legalhold-consent", - "missing-legalhold-consent-old-clients" + "operation-forbidden-for-domain-registration-state" ], "type": "string" }, @@ -10450,29 +18493,30 @@ } } }, - "description": "The user has no verified email (label: `no-identity`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)" + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" } }, - "summary": "Create a connection to another user" - }, - "put": { - "description": " [internal route ID: \"update-connection\"]\n\nCalls federation service brig on send-connection-action
Calls federation service brig on get-users-by-ids", + "summary": "Authorize a team to operate on a verified domain" + } + }, + "/domain-verification/{domain}/backend": { + "post": { + "description": " [internal route ID: \"update-domain-redirect\"]\n\n", + "operationId": "update-domain-redirect", "parameters": [ { - "in": "path", - "name": "uid_domain", + "in": "header", + "name": "Authorization", "required": true, "schema": { "type": "string" } }, { - "description": "User Id", "in": "path", - "name": "uid", + "name": "domain", "required": true, "schema": { - "format": "uuid", "type": "string" } } @@ -10481,7 +18525,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConnectionUpdate" + "$ref": "#/components/schemas/DomainRedirectConfig_NTI5NDE5MDQy" } } }, @@ -10489,42 +18533,27 @@ }, "responses": { "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserConnection" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/UserConnection" - } - } - }, - "description": "Connection updated" + "description": "Updated" }, - "204": { - "description": "Connection unchanged" - }, - "400": { + "401": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-user", - "message": "Invalid user" + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" }, "properties": { "code": { "enum": [ - 400 + 401 ], "type": "integer" }, "label": { "enum": [ - "invalid-user" + "domain-registration-update-auth-failure" ], "type": "string" }, @@ -10541,7 +18570,7 @@ } } }, - "description": "Invalid `body`\n\nInvalid user (label: `invalid-user`)" + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" }, "403": { "content": { @@ -10549,8 +18578,8 @@ "schema": { "example": { "code": 403, - "label": "no-identity", - "message": "The user has no verified email" + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" }, "properties": { "code": { @@ -10561,12 +18590,7 @@ }, "label": { "enum": [ - "no-identity", - "bad-conn-update", - "not-connected", - "connection-limit", - "missing-legalhold-consent", - "missing-legalhold-consent-old-clients" + "operation-forbidden-for-domain-registration-state" ], "type": "string" }, @@ -10583,94 +18607,104 @@ } } }, - "description": "The user has no verified email (label: `no-identity`)\n\nInvalid status transition (label: `bad-conn-update`)\n\nUsers are not connected (label: `not-connected`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)" + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" } }, - "summary": "Update a connection to another user" + "summary": "Update the domain redirect configuration" } }, - "/conversations": { + "/domain-verification/{domain}/challenges": { "post": { - "description": " [internal route ID: \"create-group-conversation\"]\n\nThis returns 201 when a new conversation is created, and 200 when the conversation already existed
Calls federation service galley on on-conversation-updated
Calls federation service galley on on-conversation-created
Calls federation service brig on get-not-fully-connected-backends
Calls federation service brig on api-version", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/NewConv" - } + "description": " [internal route ID: \"domain-verification-challenge\"]\n\n", + "operationId": "domain-verification-challenge", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConversationV6v6" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConversationV6v6" + "$ref": "#/components/schemas/DomainVerificationChallenge_NjIwMzA1MjE5" } } }, - "description": "Conversation existed", - "headers": { - "Location": { - "description": "Conversation ID", - "schema": { - "format": "uuid", - "type": "string" - } + "description": "" + } + }, + "summary": "Get a DNS verification challenge" + } + }, + "/domain-verification/{domain}/challenges/{challengeId}": { + "post": { + "description": " [internal route ID: \"verify-challenge\"]\n\n", + "operationId": "verify-challenge", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "challengeId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChallengeToken_Mzk3NTcwOTM3" } } }, - "201": { + "required": true + }, + "responses": { + "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateGroupConversationv6" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/CreateGroupConversationv6" + "$ref": "#/components/schemas/DomainOwnershipToken_NTU0ODc1NDE5" } } }, - "description": "Conversation created", - "headers": { - "Location": { - "description": "Conversation ID", - "schema": { - "format": "uuid", - "type": "string" - } - } - } + "description": "" }, - "400": { + "401": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "mls-not-enabled", - "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" }, "properties": { "code": { "enum": [ - 400 + 401 ], "type": "integer" }, "label": { "enum": [ - "mls-not-enabled", - "non-empty-member-list" + "domain-registration-update-auth-failure" ], "type": "string" }, @@ -10687,7 +18721,7 @@ } } }, - "description": "Invalid `body`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nAttempting to add group members outside MLS (label: `non-empty-member-list`)" + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" }, "403": { "content": { @@ -10695,8 +18729,8 @@ "schema": { "example": { "code": 403, - "label": "missing-legalhold-consent", - "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + "label": "domain-verification-failed", + "message": "Domain verification failed" }, "properties": { "code": { @@ -10707,11 +18741,7 @@ }, "label": { "enum": [ - "missing-legalhold-consent", - "operation-denied", - "no-team-member", - "not-connected", - "access-denied" + "domain-verification-failed" ], "type": "string" }, @@ -10728,62 +18758,68 @@ } } }, - "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nUsers are not connected (label: `not-connected`)\n\nConversation access denied (label: `access-denied`)" + "description": "Domain verification failed (label: `domain-verification-failed`)" }, - "409": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { - "properties": { - "non_federating_backends": { - "items": { - "$ref": "#/components/schemas/Domain" - }, - "type": "array" - } + "example": { + "code": 404, + "label": "challenge-not-found", + "message": "Challenge not found" }, - "required": [ - "non_federating_backends" - ], - "type": "object" - } - } - }, - "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" - }, - "533": { - "content": { - "application/json;charset=utf-8": { - "schema": { "properties": { - "unreachable_backends": { - "items": { - "$ref": "#/components/schemas/Domain" - }, - "type": "array" + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "challenge-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" } }, "required": [ - "unreachable_backends" + "code", + "label", + "message" ], "type": "object" } } }, - "description": "Some domains are unreachable" + "description": "`domain` or `challengeId` not found\n\nChallenge not found (label: `challenge-not-found`)" } }, - "summary": "Create a new conversation" + "summary": "Verify a DNS verification challenge" } }, - "/conversations/code-check": { + "/domain-verification/{domain}/team": { "post": { - "description": " [internal route ID: \"code-check\"]\n\nIf the guest links team feature is disabled, this will fail with 404 CodeNotFound.Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/join` which responds with 409 GuestLinksDisabled if guest links are disabled.", + "description": " [internal route ID: \"update-team-invite\"]\n\n", + "operationId": "update-team-invite", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConversationCode" + "$ref": "#/components/schemas/TeamInviteConfig_MTg4Nzk4NzMz" } } }, @@ -10791,27 +18827,27 @@ }, "responses": { "200": { - "description": "Valid" + "description": "Updated" }, - "403": { + "402": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "invalid-conversation-password", - "message": "Invalid conversation password" + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" }, "properties": { "code": { "enum": [ - 403 + 402 ], "type": "integer" }, "label": { "enum": [ - "invalid-conversation-password" + "domain-registration-update-payment-required" ], "type": "string" }, @@ -10828,28 +18864,27 @@ } } }, - "description": "Invalid conversation password (label: `invalid-conversation-password`)" + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" }, - "404": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-conversation", - "no-conversation-code" + "operation-forbidden-for-domain-registration-state" ], "type": "string" }, @@ -10866,52 +18901,138 @@ } } }, - "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" } }, - "summary": "Check validity of a conversation code." + "summary": "Update the team-invite configuration" } }, - "/conversations/join": { - "get": { - "description": " [internal route ID: \"get-conversation-by-reusable-code\"]\n\n", + "/domain-verification/{domain}/team/challenges/{challengeId}": { + "post": { + "description": " [internal route ID: \"verify-challenge-team\"]\n\n", + "operationId": "verify-challenge-team", "parameters": [ { - "in": "query", - "name": "key", + "in": "path", + "name": "domain", "required": true, "schema": { "type": "string" } }, { - "in": "query", - "name": "code", + "in": "path", + "name": "challengeId", "required": true, "schema": { + "format": "uuid", "type": "string" } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChallengeToken_Mzk3NTcwOTM3" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConversationCoverView" + "$ref": "#/components/schemas/DomainOwnershipToken_NTU0ODc1NDE5" } } }, "description": "" }, + "401": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" + }, + "properties": { + "code": { + "enum": [ + 401 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-auth-failure" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" + }, + "402": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" + }, + "properties": { + "code": { + "enum": [ + 402 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-payment-required" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" + }, "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" }, "properties": { "code": { @@ -10922,9 +19043,7 @@ }, "label": { "enum": [ - "no-team-member", - "access-denied", - "invalid-conversation-password" + "operation-forbidden-for-domain-registration-state" ], "type": "string" }, @@ -10941,28 +19060,86 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid conversation password (label: `invalid-conversation-password`)" + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Verify a DNS verification challenge for a team" + } + }, + "/events": { + "get": { + "description": " [internal route ID: \"consume-events\"]\n\nThis is the rabbitMQ-based variant of \"await-notifications\"", + "externalDocs": { + "description": "RFC 6455", + "url": "https://datatracker.ietf.org/doc/html/rfc6455" + }, + "operationId": "consume-events", + "parameters": [ + { + "description": "Client ID", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } }, - "404": { + { + "description": "Synchronization marker ID", + "in": "query", + "name": "sync_marker", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "101": { + "description": "Connection upgraded." + }, + "426": { + "description": "Upgrade required." + } + }, + "summary": "Consume events over a websocket connection" + } + }, + "/feature-configs": { + "get": { + "description": " [internal route ID: \"get-all-feature-configs-for-user\"]\n\nGets feature configs for a user. If the user is a member of a team and has the required permissions, this will return the team's feature configs.If the user is not a member of a team, this will return the personal feature configs (the server defaults).\nOAuth scope: `read:feature_configs`", + "operationId": "get-all-feature-configs-for-user", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NP_LockableFtur_ghdCnfiSOVsyAvIRqExmDpB_MGwUT2Q_LTM1Mzc0ODgy" + } + } + }, + "description": "" + }, + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-conversation", - "no-conversation-code" + "operation-denied", + "no-team-member" ], "type": "string" }, @@ -10979,27 +19156,27 @@ } } }, - "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" }, - "409": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "guest-links-disabled", - "message": "The guest link feature is disabled and all guest links have been revoked" + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 409 + 404 ], "type": "integer" }, "label": { "enum": [ - "guest-links-disabled" + "no-team" ], "type": "string" }, @@ -11016,18 +19193,21 @@ } } }, - "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + "description": "Team not found (label: `no-team`)" } }, - "summary": "Get limited conversation information by key/code pair" - }, + "summary": "Gets feature configs for a user" + } + }, + "/get-domain-registration": { "post": { - "description": " [internal route ID: \"join-conversation-by-code-unqualified\"]\n\nIf the guest links team feature is disabled, this will fail with 409 GuestLinksDisabled.Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/code-check` which responds with 404 CodeNotFound if guest links are disabled.Calls federation service galley on on-conversation-updated", + "description": " [internal route ID: \"get-domain-registration\"]\n\n- `due_to_existing_account`: boolean (optional, only present if `domain_redirect` is `no-registration`)\n- `backend`: object (optional, must be present if `domain_redirect` is `backend`)\n - `config_url`: string (required)\n - `webapp_url`: string (optional)\n- `sso_code`: string (optional, must be present if `domain_redirect` is `sso`)", + "operationId": "get-domain-registration", "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/JoinConversationByCode" + "$ref": "#/components/schemas/GetDomainRegistrationRequest_LTg4NTM1MzM2" } } }, @@ -11036,45 +19216,33 @@ "responses": { "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Event" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/DomainRedirectResponse_V10_LTEyMjI4NTM0" } } }, - "description": "Conversation joined" - }, - "204": { - "description": "Conversation unchanged" + "description": "" }, - "403": { + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "too-many-members", - "message": "Maximum number of members per conversation reached" + "code": 400, + "label": "invalid-domain", + "message": "Invalid domain" }, "properties": { "code": { "enum": [ - 403 + 400 ], "type": "integer" }, "label": { "enum": [ - "too-many-members", - "no-team-member", - "invalid-op", - "access-denied", - "invalid-conversation-password" + "invalid-domain" ], "type": "string" }, @@ -11091,28 +19259,107 @@ } } }, - "description": "Maximum number of members per conversation reached (label: `too-many-members`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid conversation password (label: `invalid-conversation-password`)" + "description": "Invalid `body`\n\nInvalid domain (label: `invalid-domain`)" + } + }, + "summary": "Get domain registration configuration by email" + } + }, + "/handles": { + "post": { + "description": " [internal route ID: \"check-user-handles\"]\n\n", + "operationId": "check-user-handles", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CheckHandles_LTc0OTkxMzAx" + } + } }, - "404": { + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Handle" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Handle" + }, + "type": "array" + } + } + }, + "description": "List of free handles" + } + }, + "summary": "Check availability of user handles" + } + }, + "/handles/{handle}": { + "head": { + "description": " [internal route ID: \"check-user-handle\"]\n\n", + "operationId": "check-user-handle", + "parameters": [ + { + "in": "path", + "name": "handle", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "Handle is taken" + }, + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "code": 400, + "label": "invalid-handle", + "message": "The given handle is invalid (less than 2 or more than 256 characters; chars not in \"a-z0-9_.-\"; or on the blocklist)" }, "properties": { "code": { "enum": [ - 404 + 400 ], "type": "integer" }, "label": { "enum": [ - "no-conversation", - "no-conversation-code" + "invalid-handle" ], "type": "string" }, @@ -11129,27 +19376,27 @@ } } }, - "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + "description": "The given handle is invalid (less than 2 or more than 256 characters; chars not in \"a-z0-9_.-\"; or on the blocklist) (label: `invalid-handle`)" }, - "409": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "guest-links-disabled", - "message": "The guest link feature is disabled and all guest links have been revoked" + "code": 404, + "label": "not-found", + "message": "Handle not found" }, "properties": { "code": { "enum": [ - 409 + 404 ], "type": "integer" }, "label": { "enum": [ - "guest-links-disabled" + "not-found" ], "type": "string" }, @@ -11166,48 +19413,185 @@ } } }, - "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + "description": "`handle` not found\n\nHandle not found (label: `not-found`)" } }, - "summary": "Join a conversation using a reusable code" + "summary": "Check whether a user handle can be taken" } }, - "/conversations/list": { + "/identity-providers": { + "get": { + "description": " [internal route ID: \"idp-get-all\"]\n\n", + "operationId": "idp-get-all", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPList" + } + } + }, + "description": "" + } + } + }, "post": { - "description": " [internal route ID: \"list-conversations\"]\n\nCalls federation service galley on get-conversations", + "description": " [internal route ID: \"idp-create\"]\n\nCreate a new identity provider.\n\nThe `api_version` parameter controls the uniqueness constraint for IdP issuers:\n- `v1`: IdP issuers must be globally unique across the entire backend (all teams)\n- `v2` (default): IdP issuers must be unique per team (can be reused across different teams)\n\nThese constraints apply to both, multi-ingress and standard backends.", + "operationId": "idp-create", + "parameters": [ + { + "in": "query", + "name": "replaces", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "api_version", + "required": false, + "schema": { + "default": "v2", + "enum": [ + "v1", + "v2" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "handle", + "required": false, + "schema": { + "maxLength": 32, + "minLength": 1, + "type": "string" + } + } + ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ListConversations" + "$ref": "#/components/schemas/IdPMetadataInfo" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" } } }, "required": true }, "responses": { - "200": { + "201": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConversationsResponse" + "$ref": "#/components/schemas/IdPConfig_WireIdP_NDA5MTE4Mjk0" } } }, "description": "" } - }, - "summary": "Get conversation metadata for a list of conversation ids" + } } }, - "/conversations/list-ids": { - "post": { - "description": " [internal route ID: \"list-conversation-ids\"]\n\nThe IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", + "/identity-providers/{id}": { + "delete": { + "description": " [internal route ID: \"idp-delete\"]\n\n", + "operationId": "idp-delete", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "purge", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "204": { + "description": "" + } + } + }, + "get": { + "description": " [internal route ID: \"idp-get\"]\n\n", + "operationId": "idp-get", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP_NDA5MTE4Mjk0" + } + } + }, + "description": "" + } + } + }, + "put": { + "description": " [internal route ID: \"idp-update\"]\n\n", + "operationId": "idp-update", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "handle", + "required": false, + "schema": { + "maxLength": 32, + "minLength": 1, + "type": "string" + } + } + ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/GetPaginated_ConversationIds" + "$ref": "#/components/schemas/IdPMetadataInfo" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" } } }, @@ -11218,84 +19602,93 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConversationIds_Page" + "$ref": "#/components/schemas/IdPConfig_WireIdP_NDA5MTE4Mjk0" } } }, "description": "" } - }, - "summary": "Get all conversation IDs." + } } }, - "/conversations/mls-self": { + "/identity-providers/{id}/raw": { "get": { - "description": " [internal route ID: \"get-mls-self-conversation\"]\n\n", + "description": " [internal route ID: \"idp-get-raw\"]\n\n", + "operationId": "idp-get-raw", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "responses": { "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Conversation" - } - }, - "application/json;charset=utf-8": { + "application/xml": { "schema": { - "$ref": "#/components/schemas/Conversation" + "type": "string" } } }, - "description": "The MLS self-conversation" + "description": "" + } + } + } + }, + "/list-connections": { + "post": { + "description": " [internal route ID: \"list-connections\"]\n\nThe IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", + "operationId": "list-connections", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetMuliTabPgRqs_ConcLOrm501_LTIwMjA4NzYw" + } + } }, - "400": { + "required": true + }, + "responses": { + "200": { "content": { "application/json;charset=utf-8": { "schema": { - "example": { - "code": 400, - "label": "mls-not-enabled", - "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" - }, - "properties": { - "code": { - "enum": [ - 400 - ], - "type": "integer" - }, - "label": { - "enum": [ - "mls-not-enabled" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" + "$ref": "#/components/schemas/MultiTabePg_ConcsLOrRmU_MTgwMzU4OTU5" } } }, - "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + "description": "" } }, - "summary": "Get the user's MLS self-conversation" + "summary": "List the connections to other users, including remote users" } }, - "/conversations/one2one": { + "/list-users": { "post": { - "description": " [internal route ID: \"create-one-to-one-conversation\"]\n\nCalls federation service galley on on-conversation-created", + "description": " [internal route ID: \"list-users-by-ids-or-handles\"]\n\nThe 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive.", + "operationId": "list-users-by-ids-or-handles", + "parameters": [ + { + "description": "Include whether each local user can currently be contacted", + "in": "query", + "name": "include-contact-status", + "required": false, + "schema": { + "type": "boolean" + } + } + ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/NewConv" + "$ref": "#/components/schemas/ListUsersQuery" } } }, @@ -11304,47 +19697,61 @@ "responses": { "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConversationV3v3" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConversationV3v3" + "$ref": "#/components/schemas/ListUsersById_LTQ5MTE3NDc0" } } }, - "description": "Conversation existed", - "headers": { - "Location": { - "description": "Conversation ID", - "schema": { - "format": "uuid", - "type": "string" - } + "description": "" + } + }, + "summary": "List users" + } + }, + "/login": { + "post": { + "description": " [internal route ID: \"login\"]\n\nLogins are throttled at the server's discretion", + "operationId": "login", + "parameters": [ + { + "description": "Request a persistent cookie instead of a session cookie", + "in": "query", + "name": "persist", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Login_LTgyNTIzMTM1" } } }, - "201": { + "required": true + }, + "responses": { + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConversationV3v3" + "$ref": "#/components/schemas/AccessToken_ODIyMTczMjMw" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConversationV3v3" + "$ref": "#/components/schemas/AccessToken_ODIyMTczMjMw" } } }, - "description": "Conversation created", + "description": "OK", "headers": { - "Location": { - "description": "Conversation ID", + "Set-Cookie": { "schema": { - "format": "uuid", "type": "string" } } @@ -11356,8 +19763,8 @@ "schema": { "example": { "code": 403, - "label": "missing-legalhold-consent", - "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + "label": "code-authentication-required", + "message": "Code authentication is required" }, "properties": { "code": { @@ -11368,13 +19775,11 @@ }, "label": { "enum": [ - "missing-legalhold-consent", - "operation-denied", - "not-connected", - "no-team-member", - "non-binding-team-members", - "invalid-op", - "access-denied" + "code-authentication-required", + "code-authentication-failed", + "pending-activation", + "suspended", + "invalid-credentials" ], "type": "string" }, @@ -11391,28 +19796,61 @@ } } }, - "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nBoth users must be members of the same binding team (label: `non-binding-team-members`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)" + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nAccount pending activation (label: `pending-activation`)\n\nAccount suspended (label: `suspended`)\n\nAuthentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Authenticate a user to obtain a cookie and first access token" + } + }, + "/meetings": { + "post": { + "description": " [internal route ID: \"create-meeting\"]\n\n", + "operationId": "create-meeting", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewMeeting_LTI1NTMzOTU5" + } + } }, - "404": { + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeetingWithConversation_LTMyNzA4NzU0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingWithConversation_LTMyNzA4NzU0" + } + } + }, + "description": "Meeting created" + }, + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" + "code": 403, + "label": "invalid-op", + "message": "Invalid meeting times, empty update, or meetings feature disabled" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-team", - "non-binding-team" + "invalid-op" ], "type": "string" }, @@ -11429,7 +19867,7 @@ } } }, - "description": "Team not found (label: `no-team`)\n\nNot a member of a binding team (label: `non-binding-team`)" + "description": "Invalid meeting times, empty update, or meetings feature disabled (label: `invalid-op`)" }, "533": { "content": { @@ -11453,16 +19891,39 @@ "description": "Some domains are unreachable" } }, - "summary": "Create a 1:1 conversation" + "summary": "Create a new meeting" } }, - "/conversations/one2one/{usr_domain}/{usr}": { + "/meetings/list": { "get": { - "description": " [internal route ID: \"get-one-to-one-mls-conversation@v6\"]\n\n", + "description": " [internal route ID: \"list-meetings\"]\n\n", + "operationId": "list-meetings", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Meeting_ODU0OTMzMTgw" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "List all meetings for the authenticated user" + } + }, + "/meetings/{domain}/{id}": { + "delete": { + "description": " [internal route ID: \"delete-meeting\"]\n\n", + "operationId": "delete-meeting", "parameters": [ { "in": "path", - "name": "usr_domain", + "name": "domain", "required": true, "schema": { "type": "string" @@ -11470,49 +19931,38 @@ }, { "in": "path", - "name": "usr", + "name": "id", "required": true, "schema": { "format": "uuid", "type": "string" } } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MLSOne2OneConversation_MLSPublicKey" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/MLSOne2OneConversation_MLSPublicKey" - } - } - }, - "description": "MLS 1-1 conversation" + ], + "responses": { + "200": { + "description": "Meeting deleted" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "mls-not-enabled", - "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + "code": 403, + "label": "invalid-op", + "message": "Invalid meeting times, empty update, or meetings feature disabled" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "mls-not-enabled" + "invalid-op", + "access-denied" ], "type": "string" }, @@ -11529,27 +19979,27 @@ } } }, - "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + "description": "Invalid meeting times, empty update, or meetings feature disabled (label: `invalid-op`)\n\nYou do not have permission to access this resource (label: `access-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "not-connected", - "message": "Users are not connected" + "code": 404, + "label": "meeting-not-found", + "message": "Meeting not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "not-connected" + "meeting-not-found" ], "type": "string" }, @@ -11566,75 +20016,26 @@ } } }, - "description": "Users are not connected (label: `not-connected`)" + "description": "`domain` or `id` or Meeting not found (label: `meeting-not-found`)" } }, - "summary": "Get an MLS 1:1 conversation" - } - }, - "/conversations/self": { - "post": { - "description": " [internal route ID: \"create-self-conversation\"]\n\n", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConversationV6v6" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ConversationV6v6" - } - } - }, - "description": "Conversation existed", - "headers": { - "Location": { - "description": "Conversation ID", - "schema": { - "format": "uuid", - "type": "string" - } - } + "summary": "Delete a meeting" + }, + "get": { + "description": " [internal route ID: \"get-meeting\"]\n\n", + "operationId": "get-meeting", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" } }, - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConversationV6v6" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ConversationV6v6" - } - } - }, - "description": "Conversation created", - "headers": { - "Location": { - "description": "Conversation ID", - "schema": { - "format": "uuid", - "type": "string" - } - } - } - } - }, - "summary": "Create a self-conversation" - } - }, - "/conversations/{Conversation ID}/bots": { - "post": { - "description": " [internal route ID: \"add-bot\"]\n\n", - "parameters": [ { "in": "path", - "name": "Conversation ID", + "name": "id", "required": true, "schema": { "format": "uuid", @@ -11642,54 +20043,36 @@ } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/AddBot" - } - } - }, - "required": true - }, "responses": { - "201": { + "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AddBotResponse" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/AddBotResponse" + "$ref": "#/components/schemas/Meeting_ODU0OTMzMTgw" } } }, "description": "" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "service-disabled", - "message": "The desired service is currently disabled." + "code": 404, + "label": "meeting-not-found", + "message": "Meeting not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "service-disabled", - "too-many-members", - "invalid-conversation", - "access-denied" + "meeting-not-found" ], "type": "string" }, @@ -11706,28 +20089,26 @@ } } }, - "description": "The desired service is currently disabled. (label: `service-disabled`)\n\nMaximum number of members per conversation reached. (label: `too-many-members`)\n\nThe operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)" + "description": "`domain` or `id` or Meeting not found (label: `meeting-not-found`)" } }, - "summary": "Add bot" - } - }, - "/conversations/{Conversation ID}/bots/{Bot ID}": { - "delete": { - "description": " [internal route ID: \"remove-bot\"]\n\n", + "summary": "Get a single meeting by ID" + }, + "put": { + "description": " [internal route ID: \"update-meeting\"]\n\n", + "operationId": "update-meeting", "parameters": [ { "in": "path", - "name": "Conversation ID", + "name": "domain", "required": true, "schema": { - "format": "uuid", "type": "string" } }, { "in": "path", - "name": "Bot ID", + "name": "id", "required": true, "schema": { "format": "uuid", @@ -11735,24 +20116,31 @@ } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateMeeting_NTExNzYxMTcz" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RemoveBotResponse" + "$ref": "#/components/schemas/MeetingWithConversation_LTMyNzA4NzU0" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/RemoveBotResponse" + "$ref": "#/components/schemas/MeetingWithConversation_LTMyNzA4NzU0" } } }, - "description": "User found" - }, - "204": { - "description": "" + "description": "Meeting updated" }, "403": { "content": { @@ -11760,8 +20148,8 @@ "schema": { "example": { "code": 403, - "label": "invalid-conversation", - "message": "The operation is not allowed in this conversation." + "label": "invalid-op", + "message": "Invalid meeting times, empty update, or meetings feature disabled" }, "properties": { "code": { @@ -11772,7 +20160,7 @@ }, "label": { "enum": [ - "invalid-conversation", + "invalid-op", "access-denied" ], "type": "string" @@ -11790,19 +20178,57 @@ } } }, - "description": "The operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)" + "description": "Invalid meeting times, empty update, or meetings feature disabled (label: `invalid-op`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "meeting-not-found", + "message": "Meeting not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "meeting-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`domain` or `id` or Meeting not found (label: `meeting-not-found`)" } }, - "summary": "Remove bot" + "summary": "Update an existing meeting" } }, - "/conversations/{cnv_domain}/{cnv}": { - "get": { - "description": " [internal route ID: \"get-conversation\"]\n\nCalls federation service galley on get-conversations", + "/meetings/{domain}/{id}/invitations": { + "post": { + "description": " [internal route ID: \"add-meeting-invitation\"]\n\n", + "operationId": "add-meeting-invitation", "parameters": [ { "in": "path", - "name": "cnv_domain", + "name": "domain", "required": true, "schema": { "type": "string" @@ -11810,7 +20236,7 @@ }, { "in": "path", - "name": "cnv", + "name": "id", "required": true, "schema": { "format": "uuid", @@ -11818,16 +20244,19 @@ } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingEmailsInvitation_NzgyNzUzMzcz" + } + } + }, + "required": true + }, "responses": { "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/Conversation" - } - } - }, - "description": "" + "description": "Invitation added" }, "403": { "content": { @@ -11835,8 +20264,8 @@ "schema": { "example": { "code": 403, - "label": "access-denied", - "message": "Conversation access denied" + "label": "invalid-op", + "message": "Invalid meeting times, empty update, or meetings feature disabled" }, "properties": { "code": { @@ -11847,6 +20276,7 @@ }, "label": { "enum": [ + "invalid-op", "access-denied" ], "type": "string" @@ -11864,7 +20294,7 @@ } } }, - "description": "Conversation access denied (label: `access-denied`)" + "description": "Invalid meeting times, empty update, or meetings feature disabled (label: `invalid-op`)\n\nYou do not have permission to access this resource (label: `access-denied`)" }, "404": { "content": { @@ -11872,8 +20302,8 @@ "schema": { "example": { "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "label": "meeting-not-found", + "message": "Meeting not found" }, "properties": { "code": { @@ -11884,7 +20314,7 @@ }, "label": { "enum": [ - "no-conversation" + "meeting-not-found" ], "type": "string" }, @@ -11901,28 +20331,26 @@ } } }, - "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + "description": "`domain` or `id` or Meeting not found (label: `meeting-not-found`)" } }, - "summary": "Get a conversation by ID" - } - }, - "/conversations/{cnv_domain}/{cnv}/access": { + "summary": "Add an email to the invited emails" + }, "put": { - "description": " [internal route ID: \"update-conversation-access\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "description": " [internal route ID: \"replace-meeting-invitation\"]\n\n", + "operationId": "replace-meeting-invitation", "parameters": [ { "in": "path", - "name": "cnv_domain", + "name": "domain", "required": true, "schema": { "type": "string" } }, { - "description": "Conversation ID", "in": "path", - "name": "cnv", + "name": "id", "required": true, "schema": { "format": "uuid", @@ -11934,7 +20362,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConversationAccessData" + "$ref": "#/components/schemas/MeetingEmailsInvitation_NzgyNzUzMzcz" } } }, @@ -11942,22 +20370,7 @@ }, "responses": { "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Event" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/Event" - } - } - }, - "description": "Access updated" - }, - "204": { - "description": "Access unchanged" + "description": "Invitations replaced" }, "403": { "content": { @@ -11966,7 +20379,7 @@ "example": { "code": 403, "label": "invalid-op", - "message": "Invalid target access" + "message": "Invalid meeting times, empty update, or meetings feature disabled" }, "properties": { "code": { @@ -11978,8 +20391,7 @@ "label": { "enum": [ "invalid-op", - "access-denied", - "action-denied" + "access-denied" ], "type": "string" }, @@ -11996,7 +20408,7 @@ } } }, - "description": "Invalid target access (label: `invalid-op`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nInsufficient authorization (missing modify_conversation_access) (label: `action-denied`)" + "description": "Invalid meeting times, empty update, or meetings feature disabled (label: `invalid-op`)\n\nYou do not have permission to access this resource (label: `access-denied`)" }, "404": { "content": { @@ -12004,8 +20416,8 @@ "schema": { "example": { "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "label": "meeting-not-found", + "message": "Meeting not found" }, "properties": { "code": { @@ -12016,7 +20428,7 @@ }, "label": { "enum": [ - "no-conversation" + "meeting-not-found" ], "type": "string" }, @@ -12033,19 +20445,20 @@ } } }, - "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + "description": "`domain` or `id` or Meeting not found (label: `meeting-not-found`)" } }, - "summary": "Update access modes for a conversation" + "summary": "Replace the invited emails" } }, - "/conversations/{cnv_domain}/{cnv}/groupinfo": { - "get": { - "description": " [internal route ID: \"get-group-info\"]\n\nCalls federation service galley on query-group-info", + "/meetings/{domain}/{id}/invitations/delete": { + "post": { + "description": " [internal route ID: \"remove-meeting-invitation\"]\n\n", + "operationId": "remove-meeting-invitation", "parameters": [ { "in": "path", - "name": "cnv_domain", + "name": "domain", "required": true, "schema": { "type": "string" @@ -12053,7 +20466,7 @@ }, { "in": "path", - "name": "cnv", + "name": "id", "required": true, "schema": { "format": "uuid", @@ -12061,36 +20474,40 @@ } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingEmailsInvitation_NzgyNzUzMzcz" + } + } + }, + "required": true + }, "responses": { "200": { - "content": { - "message/mls": { - "schema": { - "$ref": "#/components/schemas/GroupInfoData" - } - } - }, - "description": "The group information" + "description": "Invitations removed" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "mls-not-enabled", - "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + "code": 403, + "label": "invalid-op", + "message": "Invalid meeting times, empty update, or meetings feature disabled" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "mls-not-enabled" + "invalid-op", + "access-denied" ], "type": "string" }, @@ -12107,7 +20524,7 @@ } } }, - "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + "description": "Invalid meeting times, empty update, or meetings feature disabled (label: `invalid-op`)\n\nYou do not have permission to access this resource (label: `access-denied`)" }, "404": { "content": { @@ -12115,8 +20532,8 @@ "schema": { "example": { "code": 404, - "label": "mls-missing-group-info", - "message": "The conversation has no group information" + "label": "meeting-not-found", + "message": "Meeting not found" }, "properties": { "code": { @@ -12127,8 +20544,7 @@ }, "label": { "enum": [ - "mls-missing-group-info", - "no-conversation" + "meeting-not-found" ], "type": "string" }, @@ -12145,62 +20561,87 @@ } } }, - "description": "`cnv_domain` or `cnv` not found\n\nThe conversation has no group information (label: `mls-missing-group-info`)\n\nConversation not found (label: `no-conversation`)" + "description": "`domain` or `id` or Meeting not found (label: `meeting-not-found`)" } }, - "summary": "Get MLS group information" + "summary": "Remove emails from the invited emails" } }, - "/conversations/{cnv_domain}/{cnv}/members": { + "/mls/commit-bundles": { "post": { - "description": " [internal route ID: \"add-members-to-conversation\"]\n\nCalls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", - "parameters": [ - { - "in": "path", - "name": "cnv_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "cnv", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], + "description": " [internal route ID: \"mls-commit-bundle\"]\n\n\n\n**Note**: this endpoint can execute proposals, and therefore return all possible errors associated with adding or removing members to a conversation, in addition to the ones listed below. See the documentation of [POST /conversations/{cnv}/members/v2](#/default/post_conversations__cnv__members_v2) and [POST /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}](#/default/delete_conversations__cnv_domain___cnv__members__usr_domain___usr_) for more details on the possible error responses of each type of proposal.", + "operationId": "mls-commit-bundle", "requestBody": { "content": { - "application/json;charset=utf-8": { + "message/mls": { "schema": { - "$ref": "#/components/schemas/InviteQualified" + "$ref": "#/components/schemas/CommitBundle" } } }, "required": true }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/MLSMessageSendingStatus_NjA1NDA0MTE4" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/MLSMessageSendingStatus_NjA1NDA0MTE4" } } }, - "description": "Conversation updated" + "description": "Commit accepted and forwarded" }, - "204": { - "description": "Conversation unchanged" + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-invalid-leaf-node-signature", + "message": "Invalid leaf node signature" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-invalid-leaf-node-signature", + "mls-group-id-not-supported", + "mls-welcome-mismatch", + "mls-self-removal-not-allowed", + "mls-protocol-error", + "mls-not-enabled", + "mls-invalid-leaf-node-index", + "mls-group-conversation-mismatch", + "mls-commit-missing-references", + "mls-client-sender-user-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid leaf node signature (label: `mls-invalid-leaf-node-signature`)\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)\n\nSubmitted group info is inconsistent with the backend group state (label `inconsistent-group-state`)\n\nThe list of targets of a welcome message does not match the list of new clients in a group (label: `mls-welcome-mismatch`)\n\nSelf removal from group is not allowed (label: `mls-self-removal-not-allowed`)\n\nMLS protocol error (label: `mls-protocol-error`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nA referenced leaf node index points to a blank or non-existing node (label: `mls-invalid-leaf-node-index`)\n\nConversation ID resolved from Group ID does not match submitted Conversation ID (label: `mls-group-conversation-mismatch`)\n\nThe commit is not referencing all pending proposals (label: `mls-commit-missing-references`)\n\nUser ID resolved from Client ID does not match message's sender user ID (label: `mls-client-sender-user-mismatch`)" }, "403": { "content": { @@ -12208,8 +20649,8 @@ "schema": { "example": { "code": 403, - "label": "missing-legalhold-consent", - "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + "label": "mls-identity-mismatch", + "message": "Leaf node signature key does not match the client's key" }, "properties": { "code": { @@ -12220,13 +20661,11 @@ }, "label": { "enum": [ + "mls-identity-mismatch", + "mls-subconv-join-parent-missing", "missing-legalhold-consent", - "not-connected", - "no-team-member", - "access-denied", - "too-many-members", - "invalid-op", - "action-denied" + "legalhold-not-enabled", + "access-denied" ], "type": "string" }, @@ -12243,7 +20682,7 @@ } } }, - "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nMaximum number of members per conversation reached (label: `too-many-members`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInsufficient authorization (missing add_conversation_member) (label: `action-denied`)" + "description": "Leaf node signature key does not match the client's key (label: `mls-identity-mismatch`)\n\nMLS client cannot join the subconversation because it is not member of the parent conversation (label: `mls-subconv-join-parent-missing`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nConversation access denied (label: `access-denied`)" }, "404": { "content": { @@ -12251,8 +20690,8 @@ "schema": { "example": { "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "label": "mls-proposal-not-found", + "message": "A proposal referenced in a commit message could not be found" }, "properties": { "code": { @@ -12263,7 +20702,9 @@ }, "label": { "enum": [ - "no-conversation" + "mls-proposal-not-found", + "no-conversation", + "no-conversation-member" ], "type": "string" }, @@ -12280,28 +20721,66 @@ } } }, - "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + "description": "A proposal referenced in a commit message could not be found (label: `mls-proposal-not-found`)\n\nConversation not found (label: `no-conversation`)\n\nConversation member not found (label: `no-conversation-member`)" }, "409": { "content": { "application/json;charset=utf-8": { "schema": { "properties": { - "non_federating_backends": { + "missing_users": { "items": { - "$ref": "#/components/schemas/Domain" + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" }, "type": "array" } }, "required": [ - "non_federating_backends" + "missing_users" ], "type": "object" } } }, - "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + "description": "Group is out of sync\n\nAdding members to the conversation is not possible because the backends involved do not form a fully connected graph\n\nA user who is under legal-hold may not participate in MLS conversations (label: `mls-legal-hold-not-allowed`)\n\nThe conversation epoch in a message is too old (label: `mls-stale-message`)\n\nA proposal of type Add or Remove does not apply to the full list of clients for a user (label: `mls-client-mismatch`)" + }, + "422": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 422, + "label": "mls-unsupported-proposal", + "message": "Unsupported proposal type" + }, + "properties": { + "code": { + "enum": [ + 422 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-unsupported-proposal", + "mls-unsupported-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unsupported proposal type (label: `mls-unsupported-proposal`)\n\nAttempted to send a message with an unsupported combination of content type and wire format (label: `mls-unsupported-message`)" }, "533": { "content": { @@ -12325,25 +20804,26 @@ "description": "Some domains are unreachable" } }, - "summary": "Add qualified members to an existing conversation." + "summary": "Post a MLS CommitBundle" } }, - "/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}": { - "delete": { - "description": " [internal route ID: \"remove-member\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated
Calls federation service galley on leave-conversation", + "/mls/key-packages/claim/{user_domain}/{user}": { + "post": { + "description": " [internal route ID: \"mls-key-packages-claim\"]\n\nOnly key packages for the specified ciphersuite are claimed.", + "operationId": "mls-key-packages-claim", "parameters": [ { "in": "path", - "name": "cnv_domain", + "name": "user_domain", "required": true, "schema": { "type": "string" } }, { - "description": "Conversation ID", + "description": "User Id", "in": "path", - "name": "cnv", + "name": "user", "required": true, "schema": { "format": "uuid", @@ -12351,21 +20831,12 @@ } }, { - "in": "path", - "name": "usr_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "Target User ID", - "in": "path", - "name": "usr", + "description": "Ciphersuite in hex format (e.g. 0x0002)", + "in": "query", + "name": "ciphersuite", "required": true, "schema": { - "format": "uuid", - "type": "string" + "type": "number" } } ], @@ -12374,40 +20845,109 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/KeyPackageBundle_MjU2MjY0MDU2" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/KeyPackageBundle_MjU2MjY0MDU2" } } - }, - "description": "Member removed" + }, + "description": "Claimed key packages" + } + }, + "summary": "Claim one key package for each client of the given user" + } + }, + "/mls/key-packages/self/{client}": { + "delete": { + "description": " [internal route ID: \"mls-key-packages-delete\"]\n\n", + "operationId": "mls-key-packages-delete", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Ciphersuite in hex format (e.g. 0x0002)", + "in": "query", + "name": "ciphersuite", + "required": true, + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteKeyPackages_LTQxNTcxNjY3" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "OK" + } + }, + "summary": "Delete all key packages for a given ciphersuite and client" + }, + "post": { + "description": " [internal route ID: \"mls-key-packages-upload\"]\n\nThe request body should be a json object containing a list of base64-encoded key packages.", + "operationId": "mls-key-packages-upload", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/KeyPackageUpload_NTQ2Mjk2NzEx" + } + } }, - "204": { - "description": "No change" + "required": true + }, + "responses": { + "201": { + "description": "Key packages uploaded" }, - "403": { + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "invalid-op", - "message": "Invalid operation" + "code": 400, + "label": "mls-protocol-error", + "message": "MLS protocol error" }, "properties": { "code": { "enum": [ - 403 + 400 ], "type": "integer" }, "label": { "enum": [ - "invalid-op", - "action-denied" + "mls-protocol-error" ], "type": "string" }, @@ -12424,27 +20964,27 @@ } } }, - "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" + "description": "Invalid `body`\n\nMLS protocol error (label: `mls-protocol-error`)" }, - "404": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "code": 403, + "label": "mls-identity-mismatch", + "message": "Key package credential does not match qualified client ID" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-conversation" + "mls-identity-mismatch" ], "type": "string" }, @@ -12461,47 +21001,30 @@ } } }, - "description": "`cnv_domain` or `cnv` or `usr_domain` or `usr` not found\n\nConversation not found (label: `no-conversation`)" + "description": "Key package credential does not match qualified client ID (label: `mls-identity-mismatch`)" } }, - "summary": "Remove a member from a conversation" + "summary": "Upload a fresh batch of key packages" }, "put": { - "description": " [internal route ID: \"update-other-member\"]\n\n**Note**: at least one field has to be provided.Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "description": " [internal route ID: \"mls-key-packages-replace\"]\n\nThe request body should be a json object containing a list of base64-encoded key packages. Use this sparingly.", + "operationId": "mls-key-packages-replace", "parameters": [ { + "description": "ClientId", "in": "path", - "name": "cnv_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "Conversation ID", - "in": "path", - "name": "cnv", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "in": "path", - "name": "usr_domain", + "name": "client", "required": true, "schema": { "type": "string" } }, { - "description": "Target User ID", - "in": "path", - "name": "usr", + "description": "Comma-separated list of ciphersuites in hex format (e.g. 0x0002)", + "in": "query", + "name": "ciphersuites", "required": true, "schema": { - "format": "uuid", "type": "string" } } @@ -12510,36 +21033,35 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/OtherMemberUpdate" + "$ref": "#/components/schemas/KeyPackageUpload_NTQ2Mjk2NzEx" } } }, "required": true }, "responses": { - "200": { - "description": "Membership updated" + "201": { + "description": "Key packages replaced" }, - "403": { + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "invalid-op", - "message": "Invalid operation" + "code": 400, + "label": "mls-protocol-error", + "message": "MLS protocol error" }, "properties": { "code": { "enum": [ - 403 + 400 ], "type": "integer" }, "label": { "enum": [ - "invalid-op", - "action-denied" + "mls-protocol-error" ], "type": "string" }, @@ -12556,28 +21078,27 @@ } } }, - "description": "Invalid operation (label: `invalid-op`)\n\nInvalid target (label: `invalid-op`)\n\nInsufficient authorization (missing modify_other_conversation_member) (label: `action-denied`)" + "description": "Invalid `body` or `ciphersuites`\n\nMLS protocol error (label: `mls-protocol-error`)" }, - "404": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation-member", - "message": "Conversation member not found" + "code": 403, + "label": "mls-identity-mismatch", + "message": "Key package credential does not match qualified client ID" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-conversation-member", - "no-conversation" + "mls-identity-mismatch" ], "type": "string" }, @@ -12594,63 +21115,129 @@ } } }, - "description": "`cnv_domain` or `cnv` or `usr_domain` or `usr` not found\n\nConversation member not found (label: `no-conversation-member`)\n\nConversation not found (label: `no-conversation`)" + "description": "Key package credential does not match qualified client ID (label: `mls-identity-mismatch`)" } }, - "summary": "Update membership of the specified user" + "summary": "Upload a fresh batch of key packages and replace the old ones" } }, - "/conversations/{cnv_domain}/{cnv}/message-timer": { - "put": { - "description": " [internal route ID: \"update-conversation-message-timer\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "/mls/key-packages/self/{client}/count": { + "get": { + "description": " [internal route ID: \"mls-key-packages-count\"]\n\n", + "operationId": "mls-key-packages-count", "parameters": [ { + "description": "ClientId", "in": "path", - "name": "cnv_domain", + "name": "client", "required": true, "schema": { "type": "string" } }, { - "description": "Conversation ID", - "in": "path", - "name": "cnv", + "description": "Ciphersuite in hex format (e.g. 0x0002)", + "in": "query", + "name": "ciphersuite", "required": true, "schema": { - "format": "uuid", - "type": "string" + "type": "number" } } ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyPackageCount_LTYwNDg5MDcz" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/KeyPackageCount_LTYwNDg5MDcz" + } + } + }, + "description": "Number of key packages" + } + }, + "summary": "Return the number of unclaimed key packages for a given ciphersuite and client" + } + }, + "/mls/messages": { + "post": { + "description": " [internal route ID: \"mls-message\"]\n\n\n\n**Note**: this endpoint can execute proposals, and therefore return all possible errors associated with adding or removing members to a conversation, in addition to the ones listed below. See the documentation of [POST /conversations/{cnv}/members/v2](#/default/post_conversations__cnv__members_v2) and [POST /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}](#/default/delete_conversations__cnv_domain___cnv__members__usr_domain___usr_) for more details on the possible error responses of each type of proposal.", + "operationId": "mls-message", "requestBody": { "content": { - "application/json;charset=utf-8": { + "message/mls": { "schema": { - "$ref": "#/components/schemas/ConversationMessageTimerUpdate" + "$ref": "#/components/schemas/MLSMessage" } } }, "required": true }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/MLSMessageSendingStatus_NjA1NDA0MTE4" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/MLSMessageSendingStatus_NjA1NDA0MTE4" } } }, - "description": "Message timer updated" + "description": "Message sent" }, - "204": { - "description": "Message timer unchanged" + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-invalid-leaf-node-signature", + "message": "Invalid leaf node signature" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-invalid-leaf-node-signature", + "mls-self-removal-not-allowed", + "mls-protocol-error", + "mls-not-enabled", + "mls-invalid-leaf-node-index", + "mls-group-conversation-mismatch", + "mls-commit-missing-references", + "mls-client-sender-user-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid leaf node signature (label: `mls-invalid-leaf-node-signature`)\n\nSubmitted group info is inconsistent with the backend group state (label `inconsistent-group-state`)\n\nSelf removal from group is not allowed (label: `mls-self-removal-not-allowed`)\n\nMLS protocol error (label: `mls-protocol-error`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nA referenced leaf node index points to a blank or non-existing node (label: `mls-invalid-leaf-node-index`)\n\nConversation ID resolved from Group ID does not match submitted Conversation ID (label: `mls-group-conversation-mismatch`)\n\nThe commit is not referencing all pending proposals (label: `mls-commit-missing-references`)\n\nUser ID resolved from Client ID does not match message's sender user ID (label: `mls-client-sender-user-mismatch`)" }, "403": { "content": { @@ -12658,8 +21245,8 @@ "schema": { "example": { "code": 403, - "label": "invalid-op", - "message": "Invalid operation" + "label": "mls-subconv-join-parent-missing", + "message": "MLS client cannot join the subconversation because it is not member of the parent conversation" }, "properties": { "code": { @@ -12670,9 +21257,10 @@ }, "label": { "enum": [ - "invalid-op", - "access-denied", - "action-denied" + "mls-subconv-join-parent-missing", + "missing-legalhold-consent", + "legalhold-not-enabled", + "access-denied" ], "type": "string" }, @@ -12689,7 +21277,7 @@ } } }, - "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_message_timer) (label: `action-denied`)" + "description": "MLS client cannot join the subconversation because it is not member of the parent conversation (label: `mls-subconv-join-parent-missing`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nConversation access denied (label: `access-denied`)" }, "404": { "content": { @@ -12697,8 +21285,8 @@ "schema": { "example": { "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "label": "mls-proposal-not-found", + "message": "A proposal referenced in a commit message could not be found" }, "properties": { "code": { @@ -12709,7 +21297,9 @@ }, "label": { "enum": [ - "no-conversation" + "mls-proposal-not-found", + "no-conversation", + "no-conversation-member" ], "type": "string" }, @@ -12726,84 +21316,49 @@ } } }, - "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" - } - }, - "summary": "Update the message timer for a conversation" - } - }, - "/conversations/{cnv_domain}/{cnv}/name": { - "put": { - "description": " [internal route ID: \"update-conversation-name\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", - "parameters": [ - { - "in": "path", - "name": "cnv_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "Conversation ID", - "in": "path", - "name": "cnv", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ConversationRename" - } - } + "description": "A proposal referenced in a commit message could not be found (label: `mls-proposal-not-found`)\n\nConversation not found (label: `no-conversation`)\n\nConversation member not found (label: `no-conversation-member`)" }, - "required": true - }, - "responses": { - "200": { + "409": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Event" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Event" + "properties": { + "missing_users": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "type": "array" + } + }, + "required": [ + "missing_users" + ], + "type": "object" } } }, - "description": "Name unchanged" - }, - "204": { - "description": "Name updated" + "description": "Group is out of sync\n\nAdding members to the conversation is not possible because the backends involved do not form a fully connected graph\n\nThe conversation epoch in a message is too old (label: `mls-stale-message`)\n\nA proposal of type Add or Remove does not apply to the full list of clients for a user (label: `mls-client-mismatch`)" }, - "403": { + "422": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "invalid-op", - "message": "Invalid operation" + "code": 422, + "label": "mls-unsupported-proposal", + "message": "Unsupported proposal type" }, "properties": { "code": { "enum": [ - 403 + 422 ], "type": "integer" }, "label": { "enum": [ - "invalid-op", - "action-denied" + "mls-unsupported-proposal", + "mls-unsupported-message" ], "type": "string" }, @@ -12820,27 +21375,86 @@ } } }, - "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)" + "description": "Unsupported proposal type (label: `mls-unsupported-proposal`)\n\nAttempted to send a message with an unsupported combination of content type and wire format (label: `mls-unsupported-message`)" }, - "404": { + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Post an MLS message" + } + }, + "/mls/public-keys": { + "get": { + "description": " [internal route ID: \"mls-public-keys\"]\n\nThe format of the returned key is determined by the `format` query parameter:\n - raw (default): base64-encoded raw public keys\n - jwk: keys are nested objects in JWK format.", + "operationId": "mls-public-keys", + "parameters": [ + { + "in": "query", + "name": "format", + "required": false, + "schema": { + "enum": [ + "raw", + "jwk" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSKeysByPurpose_MLSKeys_SomeKey_NjkxMjk0MTAx" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSKeysByPurpose_MLSKeys_SomeKey_NjkxMjk0MTAx" + } + } + }, + "description": "Public keys" + }, + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" }, "properties": { "code": { "enum": [ - 404 + 400 ], "type": "integer" }, "label": { "enum": [ - "no-conversation" + "mls-not-enabled" ], "type": "string" }, @@ -12857,68 +21471,78 @@ } } }, - "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + "description": "Invalid `format`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" } }, - "summary": "Update conversation name" + "summary": "Get public keys used by the backend to sign external proposals" } }, - "/conversations/{cnv_domain}/{cnv}/proteus/messages": { + "/mls/reset-conversation": { "post": { - "description": " [internal route ID: \"post-proteus-message\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n- `ignore_all`: When set, no checks about missing clients are carried out.\n- `report_only`: Takes a list of qualified UserIDs. If any clients of the listed users are missing, the message is not sent. The missing clients are reported in the response.\n- `ignore_only`: Takes a list of qualified UserIDs. If any clients of the non-listed users are missing, the message is not sent. The missing clients are reported in the response.\n\nThe sending of messages in a federated conversation could theoretically fail partially. To make this case unlikely, the backend first gets a list of clients from all the involved backends and then tries to send a message. So, if any backend is down, the message is not propagated to anyone. But the actual message fan out to multiple backends could still fail partially. This type of failure is reported as a 201, the clients for which the message sending failed are part of the response body.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.Calls federation service galley on send-message
Calls federation service galley on on-message-sent
Calls federation service brig on get-user-clients", - "parameters": [ - { - "in": "path", - "name": "cnv_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "cnv", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], + "description": " [internal route ID: \"mls-reset-conversation\"]\n\n", + "operationId": "mls-reset-conversation", "requestBody": { "content": { - "application/x-protobuf": { + "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/QualifiedNewOtrMessage" + "$ref": "#/components/schemas/MLSReset_NzgwODA3ODc4" } } }, "required": true }, "responses": { - "201": { + "200": { + "description": "Conversation reset" + }, + "400": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MessageSendingStatus" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MessageSendingStatus" + "example": { + "code": 400, + "label": "mls-protocol-error", + "message": "MLS protocol error" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-protocol-error", + "mls-group-id-not-supported", + "mls-federated-reset-not-supported", + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "Message sent" + "description": "MLS protocol error (label: `mls-protocol-error`)\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)\n\nReset is not supported by the owning backend of the conversation (label: `mls-federated-reset-not-supported`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`) or `body`" }, "403": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { "code": 403, - "label": "unknown-client", - "message": "Unknown Client" + "label": "action-denied", + "message": "Insufficient authorization (missing leave_conversation)" }, "properties": { "code": { @@ -12929,9 +21553,9 @@ }, "label": { "enum": [ - "unknown-client", - "missing-legalhold-consent-old-clients", - "missing-legalhold-consent" + "action-denied", + "invalid-op", + "access-denied" ], "type": "string" }, @@ -12946,26 +21570,66 @@ ], "type": "object" } - }, + } + }, + "description": "Insufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "unknown-client", - "message": "Unknown Client" + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "unknown-client", - "missing-legalhold-consent-old-clients", - "missing-legalhold-consent" + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-stale-message", + "message": "The conversation epoch in a message is too old" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-stale-message" ], "type": "string" }, @@ -12982,7 +21646,64 @@ } } }, - "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" + } + }, + "summary": "Reset an MLS conversation to epoch 0" + } + }, + "/notifications": { + "get": { + "description": " [internal route ID: \"get-notifications\"]\n\nSee also: GET /teams/notifications", + "operationId": "get-notifications", + "parameters": [ + { + "description": "Only return notifications more recent than this", + "in": "query", + "name": "since", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Maximum number of notifications to return", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 10000, + "minimum": 100, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueuedNotificationList_MTU0ODEyNTQ2" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotificationList_MTU0ODEyNTQ2" + } + } + }, + "description": "Notification list" }, "404": { "content": { @@ -12990,8 +21711,8 @@ "schema": { "example": { "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "label": "not-found", + "message": "Some notifications not found" }, "properties": { "code": { @@ -13002,7 +21723,7 @@ }, "label": { "enum": [ - "no-conversation" + "not-found" ], "type": "string" }, @@ -13022,8 +21743,8 @@ "schema": { "example": { "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "label": "not-found", + "message": "Some notifications not found" }, "properties": { "code": { @@ -13034,7 +21755,7 @@ }, "label": { "enum": [ - "no-conversation" + "not-found" ], "type": "string" }, @@ -13051,139 +21772,62 @@ } } }, - "description": "`cnv_domain` or `cnv` or Conversation not found (label: `no-conversation`)" - }, - "412": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MessageSendingStatus" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/MessageSendingStatus" - } - } - }, - "description": "Missing clients" + "description": "Some notifications not found (label: `not-found`)" } }, - "summary": "Post an encrypted message to a conversation (accepts only Protobuf)" + "summary": "Fetch notifications" } }, - "/conversations/{cnv_domain}/{cnv}/protocol": { - "put": { - "description": " [internal route ID: \"update-conversation-protocol\"]\n\n**Note**: Only proteus->mixed upgrade is supported.", + "/notifications/last": { + "get": { + "description": " [internal route ID: \"get-last-notification\"]\n\n", + "operationId": "get-last-notification", "parameters": [ { - "in": "path", - "name": "cnv_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "Conversation ID", - "in": "path", - "name": "cnv", - "required": true, + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, "schema": { - "format": "uuid", "type": "string" } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ProtocolUpdate" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/QueuedNotification_NTY2NzY2MTU2" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Event" - } - } - }, - "description": "Conversation updated" - }, - "204": { - "description": "Conversation unchanged" - }, - "400": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 400, - "label": "mls-migration-criteria-not-satisfied", - "message": "The migration criteria for mixed to MLS protocol transition are not satisfied for this conversation" - }, - "properties": { - "code": { - "enum": [ - 400 - ], - "type": "integer" - }, - "label": { - "enum": [ - "mls-migration-criteria-not-satisfied" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" + "$ref": "#/components/schemas/QueuedNotification_NTY2NzY2MTU2" } } }, - "description": "Invalid `body`\n\nThe migration criteria for mixed to MLS protocol transition are not satisfied for this conversation (label: `mls-migration-criteria-not-satisfied`)" + "description": "Notification found" }, - "403": { + "404": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { "example": { - "code": 403, - "label": "operation-denied", - "message": "Insufficient permissions" + "code": 404, + "label": "not-found", + "message": "Some notifications not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "operation-denied", - "no-team-member", - "invalid-op", - "action-denied", - "invalid-protocol-transition" + "not-found" ], "type": "string" }, @@ -13198,18 +21842,13 @@ ], "type": "object" } - } - }, - "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nProtocol transition is invalid (label: `invalid-protocol-transition`)" - }, - "404": { - "content": { + }, "application/json;charset=utf-8": { "schema": { "example": { "code": 404, - "label": "no-team", - "message": "Team not found" + "label": "not-found", + "message": "Some notifications not found" }, "properties": { "code": { @@ -13220,8 +21859,7 @@ }, "label": { "enum": [ - "no-team", - "no-conversation" + "not-found" ], "type": "string" }, @@ -13238,85 +21876,72 @@ } } }, - "description": "`cnv_domain` or `cnv` not found\n\nTeam not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" + "description": "Some notifications not found (label: `not-found`)" } }, - "summary": "Update the protocol of the conversation" + "summary": "Fetch the last notification" } }, - "/conversations/{cnv_domain}/{cnv}/receipt-mode": { - "put": { - "description": " [internal route ID: \"update-conversation-receipt-mode\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on update-conversation
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "/notifications/{id}": { + "get": { + "description": " [internal route ID: \"get-notification-by-id\"]\n\n", + "operationId": "get-notification-by-id", "parameters": [ { + "description": "Notification ID", "in": "path", - "name": "cnv_domain", + "name": "id", "required": true, "schema": { + "format": "uuid", "type": "string" } }, { - "description": "Conversation ID", - "in": "path", - "name": "cnv", - "required": true, + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, "schema": { - "format": "uuid", "type": "string" } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ConversationReceiptModeUpdate" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/QueuedNotification_NTY2NzY2MTU2" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/QueuedNotification_NTY2NzY2MTU2" } } }, - "description": "Receipt mode updated" - }, - "204": { - "description": "Receipt mode unchanged" + "description": "Notification found" }, - "403": { + "404": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { "example": { - "code": 403, - "label": "invalid-op", - "message": "Invalid operation" + "code": 404, + "label": "not-found", + "message": "Some notifications not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "invalid-op", - "access-denied", - "action-denied" + "not-found" ], "type": "string" }, @@ -13331,18 +21956,13 @@ ], "type": "object" } - } - }, - "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_receipt_mode) (label: `action-denied`)" - }, - "404": { - "content": { + }, "application/json;charset=utf-8": { "schema": { "example": { "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "label": "not-found", + "message": "Some notifications not found" }, "properties": { "code": { @@ -13353,7 +21973,7 @@ }, "label": { "enum": [ - "no-conversation" + "not-found" ], "type": "string" }, @@ -13370,28 +21990,51 @@ } } }, - "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + "description": "`id` or Some notifications not found (label: `not-found`)" } }, - "summary": "Update receipt mode for a conversation" + "summary": "Fetch a notification by ID" } }, - "/conversations/{cnv_domain}/{cnv}/self": { - "put": { - "description": " [internal route ID: \"update-conversation-self\"]\n\n**Note**: at least one field has to be provided.", + "/oauth/applications": { + "get": { + "description": " [internal route ID: \"get-oauth-applications\"]\n\nGet all OAuth applications with active account access for a user.", + "operationId": "get-oauth-applications", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/OAuthApplication_Mjk5NTUxNjA1" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/OAuthApplication_Mjk5NTUxNjA1" + }, + "type": "array" + } + } + }, + "description": "OAuth applications found" + } + }, + "summary": "Get OAuth applications with account access" + } + }, + "/oauth/applications/{OAuthClientId}/sessions": { + "delete": { + "description": " [internal route ID: \"revoke-oauth-account-access\"]\n\n", + "operationId": "revoke-oauth-account-access", "parameters": [ { + "description": "The ID of the OAuth client", "in": "path", - "name": "cnv_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "Conversation ID", - "in": "path", - "name": "cnv", + "name": "OAuthClientId", "required": true, "schema": { "format": "uuid", @@ -13403,35 +22046,35 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MemberUpdate" + "$ref": "#/components/schemas/PasswordReqBody_LTcxMzE3ODE3" } } }, "required": true }, "responses": { - "200": { - "description": "Update successful" + "204": { + "description": "OAuth application access revoked" }, - "404": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "code": 403, + "label": "access-denied", + "message": "Access denied." }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-conversation" + "access-denied" ], "type": "string" }, @@ -13448,27 +22091,21 @@ } } }, - "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + "description": "Access denied. (label: `access-denied`)" } }, - "summary": "Update self membership properties" + "summary": "Revoke account access from an OAuth application" } }, - "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}": { + "/oauth/applications/{OAuthClientId}/sessions/{RefreshTokenId}": { "delete": { - "description": " [internal route ID: \"delete-subconversation\"]\n\nCalls federation service galley on delete-sub-conversation", + "description": " [internal route ID: \"delete-oauth-refresh-token\"]\n\nRevoke an active OAuth session by providing the refresh token ID.", + "operationId": "delete-oauth-refresh-token", "parameters": [ { + "description": "The ID of the OAuth client", "in": "path", - "name": "cnv_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "cnv", + "name": "OAuthClientId", "required": true, "schema": { "format": "uuid", @@ -13476,10 +22113,12 @@ } }, { + "description": "The ID of the refresh token", "in": "path", - "name": "subconv", + "name": "RefreshTokenId", "required": true, "schema": { + "format": "uuid", "type": "string" } } @@ -13488,7 +22127,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/DeleteSubConversationRequest" + "$ref": "#/components/schemas/PasswordReqBody_LTcxMzE3ODE3" } } }, @@ -13497,14 +22136,6 @@ "responses": { "200": { "content": { - "application/json": { - "schema": { - "example": [], - "items": {}, - "maxItems": 0, - "type": "array" - } - }, "application/json;charset=utf-8": { "schema": { "example": [], @@ -13514,27 +22145,27 @@ } } }, - "description": "Deletion successful" + "description": "" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "mls-not-enabled", - "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + "code": 403, + "label": "access-denied", + "message": "Access denied." }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "mls-not-enabled" + "access-denied" ], "type": "string" }, @@ -13551,27 +22182,27 @@ } } }, - "description": "Invalid `body`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + "description": "Access denied. (label: `access-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "access-denied", - "message": "Conversation access denied" + "code": 404, + "label": "not-found", + "message": "OAuth client not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "access-denied" + "not-found" ], "type": "string" }, @@ -13588,27 +22219,56 @@ } } }, - "description": "Conversation access denied (label: `access-denied`)" + "description": "`OAuthClientId` or `RefreshTokenId` not found\n\nOAuth client not found (label: `not-found`)" + } + }, + "summary": "Revoke an active OAuth session" + } + }, + "/oauth/authorization/codes": { + "post": { + "description": " [internal route ID: \"create-oauth-auth-code\"]\n\nCurrently only supports the 'code' response type, which corresponds to the authorization code flow.", + "operationId": "create-oauth-auth-code", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateOAuthAuthorizationCodeRequest_LTQ3NzI1Mjkz" + } + } }, - "404": { + "required": true + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "400": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "code": 400, + "label": "redirect-url-miss-match", + "message": "The redirect URL does not match the one registered with the client" }, "properties": { "code": { "enum": [ - 404 + 400 ], "type": "integer" }, "label": { "enum": [ - "no-conversation" + "redirect-url-miss-match" ], "type": "string" }, @@ -13623,29 +22283,24 @@ ], "type": "object" } - } - }, - "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" - }, - "409": { - "content": { + }, "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "mls-stale-message", - "message": "The conversation epoch in a message is too old" + "code": 400, + "label": "redirect-url-miss-match", + "message": "The redirect URL does not match the one registered with the client" }, "properties": { "code": { "enum": [ - 409 + 400 ], "type": "integer" }, "label": { "enum": [ - "mls-stale-message" + "redirect-url-miss-match" ], "type": "string" }, @@ -13662,38 +22317,53 @@ } } }, - "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" + "description": "Bad Request\n\nThe redirect URL does not match the one registered with the client (label: `redirect-url-miss-match`) or `body`", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "Forbidden", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } } }, - "summary": "Delete an MLS subconversation" - }, + "summary": "Create an OAuth authorization code" + } + }, + "/oauth/clients/{OAuthClientId}": { "get": { - "description": " [internal route ID: \"get-subconversation\"]\n\nCalls federation service galley on get-sub-conversation", + "description": " [internal route ID: \"get-oauth-client\"]\n\n", + "operationId": "get-oauth-client", "parameters": [ { + "description": "The ID of the OAuth client", "in": "path", - "name": "cnv_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "cnv", + "name": "OAuthClientId", "required": true, "schema": { "format": "uuid", "type": "string" } - }, - { - "in": "path", - "name": "subconv", - "required": true, - "schema": { - "type": "string" - } } ], "responses": { @@ -13701,16 +22371,16 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublicSubConversation" + "$ref": "#/components/schemas/OAuthClient_NzExMTI5NTIy" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/PublicSubConversation" + "$ref": "#/components/schemas/OAuthClient_NzExMTI5NTIy" } } }, - "description": "Subconversation" + "description": "OAuth client found" }, "403": { "content": { @@ -13718,8 +22388,8 @@ "schema": { "example": { "code": 403, - "label": "mls-subconv-unsupported-convtype", - "message": "MLS subconversations are only supported for regular conversations" + "label": "forbidden", + "message": "OAuth is disabled" }, "properties": { "code": { @@ -13730,8 +22400,7 @@ }, "label": { "enum": [ - "mls-subconv-unsupported-convtype", - "access-denied" + "forbidden" ], "type": "string" }, @@ -13748,16 +22417,48 @@ } } }, - "description": "MLS subconversations are only supported for regular conversations (label: `mls-subconv-unsupported-convtype`)\n\nConversation access denied (label: `access-denied`)" + "description": "OAuth is disabled (label: `forbidden`)" }, "404": { "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, "application/json;charset=utf-8": { "schema": { "example": { "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "label": "not-found", + "message": "OAuth client not found" }, "properties": { "code": { @@ -13768,7 +22469,7 @@ }, "label": { "enum": [ - "no-conversation" + "not-found" ], "type": "string" }, @@ -13785,72 +22486,59 @@ } } }, - "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + "description": "`OAuthClientId` or OAuth client not found (label: `not-found`)\n\nOAuth client not found (label: `not-found`)" } }, - "summary": "Get information about an MLS subconversation" + "summary": "Get OAuth client information" } }, - "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}/groupinfo": { - "get": { - "description": " [internal route ID: \"get-subconversation-group-info\"]\n\nCalls federation service galley on query-group-info", - "parameters": [ - { - "in": "path", - "name": "cnv_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "cnv", - "required": true, - "schema": { - "format": "uuid", - "type": "string" + "/oauth/revoke": { + "post": { + "description": " [internal route ID: \"revoke-oauth-refresh-token\"]\n\nRevoke an access token.", + "operationId": "revoke-oauth-refresh-token", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OAuthRevokeRefreshTokenRequest_MjA1MDg4MzQ4" + } } }, - { - "in": "path", - "name": "subconv", - "required": true, - "schema": { - "type": "string" - } - } - ], + "required": true + }, "responses": { "200": { "content": { - "message/mls": { + "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/GroupInfoData" + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" } } }, - "description": "The group information" + "description": "" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "mls-not-enabled", - "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + "code": 403, + "label": "forbidden", + "message": "Invalid refresh token" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "mls-not-enabled" + "forbidden" ], "type": "string" }, @@ -13867,7 +22555,7 @@ } } }, - "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + "description": "Invalid refresh token (label: `forbidden`)" }, "404": { "content": { @@ -13875,8 +22563,8 @@ "schema": { "example": { "code": 404, - "label": "mls-missing-group-info", - "message": "The conversation has no group information" + "label": "not-found", + "message": "OAuth client not found" }, "properties": { "code": { @@ -13887,8 +22575,7 @@ }, "label": { "enum": [ - "mls-missing-group-info", - "no-conversation" + "not-found" ], "type": "string" }, @@ -13905,66 +22592,27 @@ } } }, - "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nThe conversation has no group information (label: `mls-missing-group-info`)\n\nConversation not found (label: `no-conversation`)" - } - }, - "summary": "Get MLS group information of subconversation" - } - }, - "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}/self": { - "delete": { - "description": " [internal route ID: \"leave-subconversation\"]\n\nCalls federation service galley on leave-sub-conversation
Calls federation service galley on on-mls-message-sent", - "parameters": [ - { - "in": "path", - "name": "cnv_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "cnv", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "in": "path", - "name": "subconv", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" + "description": "OAuth client not found (label: `not-found`)" }, - "400": { + "500": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "mls-not-enabled", - "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + "code": 500, + "label": "jwt-error", + "message": "Internal error while handling JWT token" }, "properties": { "code": { "enum": [ - 400 + 500 ], "type": "integer" }, "label": { "enum": [ - "mls-not-enabled", - "mls-protocol-error" + "jwt-error" ], "type": "string" }, @@ -13981,7 +22629,36 @@ } } }, - "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nMLS protocol error (label: `mls-protocol-error`)" + "description": "Internal error while handling JWT token (label: `jwt-error`)" + } + }, + "summary": "Revoke an OAuth refresh token" + } + }, + "/oauth/token": { + "post": { + "description": " [internal route ID: \"create-oauth-access-token\"]\n\nObtain a new access token from an authorization code or a refresh token.", + "operationId": "create-oauth-access-token", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Either_OAuthAccessTokenRequest_OAuthRefreshAccessTokenRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OAuthAccessTokenResponse_NzEwOTI4NjQ0" + } + } + }, + "description": "" }, "403": { "content": { @@ -13989,8 +22666,8 @@ "schema": { "example": { "code": 403, - "label": "access-denied", - "message": "Conversation access denied" + "label": "invalid_grant", + "message": "Invalid grant" }, "properties": { "code": { @@ -14001,7 +22678,8 @@ }, "label": { "enum": [ - "access-denied" + "invalid_grant", + "forbidden" ], "type": "string" }, @@ -14018,7 +22696,7 @@ } } }, - "description": "Conversation access denied (label: `access-denied`)" + "description": "Invalid grant (label: `invalid_grant`)\n\nInvalid client credentials (label: `forbidden`)\n\nInvalid grant type (label: `forbidden`)\n\nInvalid refresh token (label: `forbidden`)\n\nOAuth is disabled (label: `forbidden`)" }, "404": { "content": { @@ -14026,8 +22704,8 @@ "schema": { "example": { "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "label": "not-found", + "message": "OAuth client not found" }, "properties": { "code": { @@ -14038,7 +22716,7 @@ }, "label": { "enum": [ - "no-conversation" + "not-found" ], "type": "string" }, @@ -14055,27 +22733,27 @@ } } }, - "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + "description": "OAuth client not found (label: `not-found`)\n\nOAuth authorization code not found (label: `not-found`)" }, - "409": { + "500": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "mls-stale-message", - "message": "The conversation epoch in a message is too old" + "code": 500, + "label": "jwt-error", + "message": "Internal error while handling JWT token" }, "properties": { "code": { "enum": [ - 409 + 500 ], "type": "integer" }, "label": { "enum": [ - "mls-stale-message" + "jwt-error" ], "type": "string" }, @@ -14092,40 +22770,21 @@ } } }, - "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" + "description": "Internal error while handling JWT token (label: `jwt-error`)" } }, - "summary": "Leave an MLS subconversation" + "summary": "Create an OAuth access token" } }, - "/conversations/{cnv_domain}/{cnv}/typing": { + "/one2one-conversations": { "post": { - "description": " [internal route ID: \"member-typing-qualified\"]\n\nCalls federation service galley on on-typing-indicator-updated
Calls federation service galley on update-typing-indicator", - "parameters": [ - { - "in": "path", - "name": "cnv_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "Conversation ID", - "in": "path", - "name": "cnv", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], + "description": " [internal route ID: \"create-one-to-one-conversation\"]\n\n", + "operationId": "create-one-to-one-conversation", "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/TypingData" + "$ref": "#/components/schemas/NewOne2OneConv_LTI3OTc4NDAz" } } }, @@ -14133,93 +22792,52 @@ }, "responses": { "200": { - "description": "Notification sent" - }, - "404": { "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/v3_OwnConversation_GroupConvType_LTU2MzYxNTg0V3" + } + }, "application/json;charset=utf-8": { "schema": { - "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" - }, - "properties": { - "code": { - "enum": [ - 404 - ], - "type": "integer" - }, - "label": { - "enum": [ - "no-conversation" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" + "$ref": "#/components/schemas/v3_OwnConversation_GroupConvType_LTU2MzYxNTg0V3" } } }, - "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" - } - }, - "summary": "Sending typing notifications" - } - }, - "/conversations/{cnv}": { - "put": { - "deprecated": true, - "description": " [internal route ID: \"update-conversation-name-deprecated\"]\n\nUse `/conversations/:domain/:conv/name` instead.Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", - "parameters": [ - { - "description": "Conversation ID", - "in": "path", - "name": "cnv", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ConversationRename" + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } } } }, - "required": true - }, - "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/v3_OwnConversation_GroupConvType_LTU2MzYxNTg0V3" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/v3_OwnConversation_GroupConvType_LTU2MzYxNTg0V3" } } }, - "description": "Name updated" - }, - "204": { - "description": "Name unchanged" + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } }, "403": { "content": { @@ -14227,8 +22845,8 @@ "schema": { "example": { "code": 403, - "label": "invalid-op", - "message": "Invalid operation" + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" }, "properties": { "code": { @@ -14239,8 +22857,13 @@ }, "label": { "enum": [ + "missing-legalhold-consent", + "operation-denied", + "not-connected", + "no-team-member", + "non-binding-team-members", "invalid-op", - "action-denied" + "access-denied" ], "type": "string" }, @@ -14257,7 +22880,7 @@ } } }, - "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)" + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nBoth users must be members of the same binding team (label: `non-binding-team-members`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)" }, "404": { "content": { @@ -14265,8 +22888,8 @@ "schema": { "example": { "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { @@ -14277,7 +22900,8 @@ }, "label": { "enum": [ - "no-conversation" + "no-team", + "non-binding-team" ], "type": "string" }, @@ -14294,25 +22918,66 @@ } } }, - "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + "description": "Team not found (label: `no-team`)\n\nNot a member of a binding team (label: `non-binding-team`)" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" } }, - "summary": "Update conversation name (deprecated)" + "summary": "Create a 1:1 conversation" } }, - "/conversations/{cnv}/code": { - "delete": { - "description": " [internal route ID: \"remove-code-unqualified\"]\n\n", + "/one2one-conversations/{usr_domain}/{usr}": { + "get": { + "description": " [internal route ID: \"get-one-to-one-mls-conversation\"]\n\n", + "operationId": "get-one-to-one-mls-conversation", "parameters": [ { - "description": "Conversation ID", "in": "path", - "name": "cnv", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "usr", "required": true, "schema": { "format": "uuid", "type": "string" } + }, + { + "in": "query", + "name": "format", + "required": false, + "schema": { + "enum": [ + "raw", + "jwk" + ], + "type": "string" + } } ], "responses": { @@ -14320,16 +22985,53 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/MLSOne2OneConversation_SomeKey_GroupConvType_LTg2MTcwMjY3" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/MLSOne2OneConversation_SomeKey_GroupConvType_LTg2MTcwMjY3" } } }, - "description": "Conversation code deleted." + "description": "MLS 1-1 conversation" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `format`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" }, "403": { "content": { @@ -14337,8 +23039,8 @@ "schema": { "example": { "code": 403, - "label": "access-denied", - "message": "Conversation access denied" + "label": "not-connected", + "message": "Users are not connected" }, "properties": { "code": { @@ -14349,7 +23051,7 @@ }, "label": { "enum": [ - "access-denied" + "not-connected" ], "type": "string" }, @@ -14366,27 +23068,71 @@ } } }, - "description": "Conversation access denied (label: `access-denied`)" + "description": "Users are not connected (label: `not-connected`)" + } + }, + "summary": "Get an MLS 1:1 conversation" + } + }, + "/password-reset": { + "post": { + "description": " [internal route ID: \"post-password-reset\"]\n\n", + "operationId": "post-password-reset", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewPasswordReset_LTEyNzAxMTcy" + } + } }, - "404": { + "required": true + }, + "responses": { + "201": { + "description": "Password reset code created and sent by email." + } + }, + "summary": "Initiate a password reset." + } + }, + "/password-reset/complete": { + "post": { + "description": " [internal route ID: \"post-password-reset-complete\"]\n\n", + "operationId": "post-password-reset-complete", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CompletePasswordReset_NDcyMjY5OTc4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Password reset successful." + }, + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." }, "properties": { "code": { "enum": [ - 404 + 400 ], "type": "integer" }, "label": { "enum": [ - "no-conversation" + "invalid-code" ], "type": "string" }, @@ -14403,21 +23149,103 @@ } } }, - "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)" + } + }, + "summary": "Complete a password reset." + } + }, + "/properties": { + "delete": { + "description": " [internal route ID: \"clear-properties\"]\n\n", + "operationId": "clear-properties", + "responses": { + "200": { + "description": "Properties cleared" + } + }, + "summary": "Clear all properties" + }, + "get": { + "description": " [internal route ID: \"list-property-keys\"]\n\n", + "operationId": "list-property-keys", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "type": "array" + } + } + }, + "description": "List of property keys" + } + }, + "summary": "List all property keys" + } + }, + "/properties-values": { + "get": { + "description": " [internal route ID: \"list-properties\"]\n\n", + "operationId": "list-properties", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PropertyKeysAndValues" + } + } + }, + "description": "" } }, - "summary": "Delete conversation code" + "summary": "List all properties with key and value" + } + }, + "/properties/{key}": { + "delete": { + "description": " [internal route ID: \"delete-property\"]\n\n", + "operationId": "delete-property", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "printable", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Property deleted" + } + }, + "summary": "Delete a property" }, "get": { - "description": " [internal route ID: \"get-code\"]\n\n", + "description": " [internal route ID: \"get-property\"]\n\n", + "operationId": "get-property", "parameters": [ { - "description": "Conversation ID", "in": "path", - "name": "cnv", + "name": "key", "required": true, "schema": { - "format": "uuid", + "format": "printable", "type": "string" } } @@ -14427,111 +23255,94 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConversationCodeInfo" + "$ref": "#/components/schemas/PropertyValue" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConversationCodeInfo" + "$ref": "#/components/schemas/PropertyValue" } } }, - "description": "Conversation Code" + "description": "The property value" }, - "403": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 403, - "label": "access-denied", - "message": "Conversation access denied" - }, - "properties": { - "code": { - "enum": [ - 403 - ], - "type": "integer" - }, - "label": { - "enum": [ - "access-denied" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } + "404": { + "description": "`key` or Property not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a property value" + }, + "put": { + "description": " [internal route ID: \"set-property\"]\n\n", + "operationId": "set-property", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "printable", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PropertyValue" } - }, - "description": "Conversation access denied (label: `access-denied`)" + } }, - "404": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" - }, - "properties": { - "code": { - "enum": [ - 404 - ], - "type": "integer" - }, - "label": { - "enum": [ - "no-conversation", - "no-conversation-code" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } + "required": true + }, + "responses": { + "200": { + "description": "Property set" + } + }, + "summary": "Set a user property" + } + }, + "/provider": { + "delete": { + "description": " [internal route ID: \"provider-delete\"]\n\n", + "operationId": "provider-delete", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteProvider_MzYxMzM3Mjg2" } - }, - "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + } }, - "409": { + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "guest-links-disabled", - "message": "The guest link feature is disabled and all guest links have been revoked" + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" }, "properties": { "code": { "enum": [ - 409 + 403 ], "type": "integer" }, "label": { "enum": [ - "guest-links-disabled" + "invalid-credentials", + "invalid-provider", + "access-denied" ], "type": "string" }, @@ -14548,65 +23359,29 @@ } } }, - "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" } }, - "summary": "Get existing conversation code" + "summary": "Delete a provider" }, - "post": { - "description": " [internal route ID: \"create-conversation-code-unqualified\"]\n\n\nOAuth scope: `write:conversations_code`", - "parameters": [ - { - "description": "Conversation ID", - "in": "path", - "name": "cnv", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/CreateConversationCodeRequest" - } - } - }, - "required": true - }, + "get": { + "description": " [internal route ID: \"provider-get-account\"]\n\n", + "operationId": "provider-get-account", "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConversationCodeInfo" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ConversationCodeInfo" - } - } - }, - "description": "Conversation code already exists." - }, - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/Provider_NDIyMzQ3ODIy" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/Provider_NDIyMzQ3ODIy" } } }, - "description": "Conversation code created." + "description": "" }, "403": { "content": { @@ -14615,7 +23390,7 @@ "example": { "code": 403, "label": "access-denied", - "message": "Conversation access denied" + "message": "Access denied." }, "properties": { "code": { @@ -14643,16 +23418,16 @@ } } }, - "description": "Conversation access denied (label: `access-denied`)" + "description": "Access denied. (label: `access-denied`)" }, "404": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { "example": { "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "label": "not-found", + "message": "Provider not found." }, "properties": { "code": { @@ -14663,7 +23438,7 @@ }, "label": { "enum": [ - "no-conversation" + "not-found" ], "type": "string" }, @@ -14678,30 +23453,24 @@ ], "type": "object" } - } - }, - "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" - }, - "409": { - "content": { + }, "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "create-conv-code-conflict", - "message": "Conversation code already exists with a different password setting than the requested one." + "code": 404, + "label": "not-found", + "message": "Provider not found." }, "properties": { "code": { "enum": [ - 409 + 404 ], "type": "integer" }, "label": { "enum": [ - "create-conv-code-conflict", - "guest-links-disabled" + "not-found" ], "type": "string" }, @@ -14718,36 +23487,26 @@ } } }, - "description": "Conversation code already exists with a different password setting than the requested one. (label: `create-conv-code-conflict`)\n\nThe guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + "description": "Provider not found. (label: `not-found`)\n\nProvider not found. (label: `not-found`)" } }, - "summary": "Create or recreate a conversation code" - } - }, - "/conversations/{cnv}/features/conversationGuestLinks": { - "get": { - "description": " [internal route ID: \"get-conversation-guest-links-status\"]\n\n", - "parameters": [ - { - "description": "Conversation ID", - "in": "path", - "name": "cnv", - "required": true, - "schema": { - "format": "uuid", - "type": "string" + "summary": "Get account" + }, + "put": { + "description": " [internal route ID: \"provider-update\"]\n\n", + "operationId": "provider-update", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateProvider_LTQwMjY4MDgy" + } } - } - ], + }, + "required": true + }, "responses": { "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" - } - } - }, "description": "" }, "403": { @@ -14756,8 +23515,8 @@ "schema": { "example": { "code": 403, - "label": "access-denied", - "message": "Conversation access denied" + "label": "invalid-provider", + "message": "The provider does not exist." }, "properties": { "code": { @@ -14768,6 +23527,7 @@ }, "label": { "enum": [ + "invalid-provider", "access-denied" ], "type": "string" @@ -14785,147 +23545,73 @@ } } }, - "description": "Conversation access denied (label: `access-denied`)" - }, - "404": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" - }, - "properties": { - "code": { - "enum": [ - 404 - ], - "type": "integer" - }, - "label": { - "enum": [ - "no-conversation" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - } - }, - "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + "description": "The provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" } }, - "summary": "Get the status of the guest links feature for a conversation that potentially has been created by someone from another team." + "summary": "Update a provider" } }, - "/conversations/{cnv}/members/{usr}": { - "put": { - "deprecated": true, - "description": " [internal route ID: \"update-other-member-unqualified\"]\n\nUse `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` insteadCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "/provider/activate": { + "get": { + "description": " [internal route ID: \"provider-activate\"]\n\n", + "operationId": "provider-activate", "parameters": [ { - "description": "Conversation ID", - "in": "path", - "name": "cnv", + "in": "query", + "name": "key", "required": true, "schema": { - "format": "uuid", "type": "string" } }, { - "description": "Target User ID", - "in": "path", - "name": "usr", + "in": "query", + "name": "code", "required": true, "schema": { - "format": "uuid", "type": "string" } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/OtherMemberUpdate" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Membership updated" - }, - "403": { "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderActivationResponse_LTgzNTU3MzA5" + } + }, "application/json;charset=utf-8": { "schema": { - "example": { - "code": 403, - "label": "invalid-op", - "message": "Invalid operation" - }, - "properties": { - "code": { - "enum": [ - 403 - ], - "type": "integer" - }, - "label": { - "enum": [ - "invalid-op", - "action-denied" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" + "$ref": "#/components/schemas/ProviderActivationResponse_LTgzNTU3MzA5" } } }, - "description": "Invalid operation (label: `invalid-op`)\n\nInvalid target (label: `invalid-op`)\n\nInsufficient authorization (missing modify_other_conversation_member) (label: `action-denied`)" + "description": "" }, - "404": { + "204": { + "description": "" + }, + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation-member", - "message": "Conversation member not found" + "code": 403, + "label": "invalid-code", + "message": "Invalid verification code" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-conversation-member", - "no-conversation" + "invalid-code", + "access-denied" ], "type": "string" }, @@ -14942,78 +23628,71 @@ } } }, - "description": "`cnv` or `usr` not found\n\nConversation member not found (label: `no-conversation-member`)\n\nConversation not found (label: `no-conversation`)" + "description": "Invalid verification code (label: `invalid-code`)\n\nAccess denied. (label: `access-denied`)" } }, - "summary": "Update membership of the specified user (deprecated)" + "summary": "Activate a provider" } }, - "/conversations/{cnv}/message-timer": { - "put": { - "deprecated": true, - "description": " [internal route ID: \"update-conversation-message-timer-unqualified\"]\n\nUse `/conversations/:domain/:cnv/message-timer` instead.Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", - "parameters": [ - { - "description": "Conversation ID", - "in": "path", - "name": "cnv", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], + "/provider/assets": { + "post": { + "description": " [internal route ID: (\"assets-upload-v3\", provider)]\n\n

Construct the request as multipart/mixed; set header Content-Type: multipart/mixed; boundary=<boundary>.

Use exactly two parts in this order:

  1. application/json metadata (AssetSettings)
  2. application/octet-stream asset bytes

Each part must include Content-Type and Content-Length; the second part may include Content-MD5. Use CRLF between headers and bodies.

When asset audit logging is enabled, the JSON metadata must include:

  • convId: object { id: UUID, domain: String } (qualified conversation ID)
  • filename: String
  • filetype: String MIME type (e.g. image/png, application/pdf)

Optional metadata: public (Bool, default false), retention (one of eternal, persistent, volatile, eternal-infrequent_access, expiring).

For profile pictures or team icons without a conversation, set convId.id to 00000000-0000-0000-0000-000000000000 and convId.domain to the tenant’s domain; use any reasonable filename.

Note: the server treats the asset bytes as application/octet-stream; filetype is used for auditing only.

Example body (boundary=frontier):

Content-Type: multipart/mixed; boundary=frontier

--frontier
Content-Type: application/json
Content-Length: 191

{\"public\":false,\"retention\":\"volatile\",\"convId\":{\"id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"domain\":\"example.com\"},\"filename\":\"report.pdf\",\"filetype\":\"application/pdf\"}
--frontier
Content-Type: application/octet-stream
Content-Length: 11

Hello Audit
--frontier--
", + "operationId": "assets-upload-v3_provider", "requestBody": { "content": { - "application/json;charset=utf-8": { + "multipart/mixed": { "schema": { - "$ref": "#/components/schemas/ConversationMessageTimerUpdate" + "$ref": "#/components/schemas/AssetSource" } } }, - "required": true + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server." }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/Asset_Qualified_AssetKey_MzU1MjMxNTA5" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Event" + "$ref": "#/components/schemas/Asset_Qualified_AssetKey_MzU1MjMxNTA5" } } }, - "description": "Message timer updated" - }, - "204": { - "description": "Message timer unchanged" + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } }, - "403": { + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "invalid-op", - "message": "Invalid operation" + "code": 400, + "label": "incomplete-body", + "message": "HTTP content-length header does not match body size" }, "properties": { "code": { "enum": [ - 403 + 400 ], "type": "integer" }, "label": { "enum": [ - "invalid-op", - "access-denied", - "action-denied" + "incomplete-body", + "invalid-length" ], "type": "string" }, @@ -15030,27 +23709,27 @@ } } }, - "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_message_timer) (label: `action-denied`)" + "description": "Invalid `body`\n\nHTTP content-length header does not match body size (label: `incomplete-body`)\n\nInvalid content length (label: `invalid-length`)" }, - "404": { + "413": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "code": 413, + "label": "client-error", + "message": "Asset too large" }, "properties": { "code": { "enum": [ - 404 + 413 ], "type": "integer" }, "label": { "enum": [ - "no-conversation" + "client-error" ], "type": "string" }, @@ -15067,56 +23746,29 @@ } } }, - "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + "description": "Asset too large (label: `client-error`)" } }, - "summary": "Update the message timer for a conversation (deprecated)" + "summary": "Upload an asset" } }, - "/conversations/{cnv}/name": { - "put": { - "deprecated": true, - "description": " [internal route ID: \"update-conversation-name-unqualified\"]\n\nUse `/conversations/:domain/:conv/name` instead.Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "/provider/assets/{key}": { + "delete": { + "description": " [internal route ID: (\"assets-delete-v3\", provider)]\n\n", + "operationId": "assets-delete-v3_provider", "parameters": [ { - "description": "Conversation ID", "in": "path", - "name": "cnv", + "name": "key", "required": true, "schema": { - "format": "uuid", "type": "string" } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ConversationRename" - } - } - }, - "required": true - }, "responses": { "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Event" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/Event" - } - } - }, - "description": "Name updated" - }, - "204": { - "description": "Name unchanged" + "description": "Asset deleted" }, "403": { "content": { @@ -15124,8 +23776,8 @@ "schema": { "example": { "code": 403, - "label": "invalid-op", - "message": "Invalid operation" + "label": "unauthorised", + "message": "Unauthorised operation" }, "properties": { "code": { @@ -15136,8 +23788,7 @@ }, "label": { "enum": [ - "invalid-op", - "action-denied" + "unauthorised" ], "type": "string" }, @@ -15154,7 +23805,7 @@ } } }, - "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)" + "description": "Unauthorised operation (label: `unauthorised`)" }, "404": { "content": { @@ -15162,8 +23813,8 @@ "schema": { "example": { "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "label": "not-found", + "message": "Asset not found" }, "properties": { "code": { @@ -15174,7 +23825,7 @@ }, "label": { "enum": [ - "no-conversation" + "not-found" ], "type": "string" }, @@ -15191,28 +23842,26 @@ } } }, - "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + "description": "`key` not found\n\nAsset not found (label: `not-found`)" } }, - "summary": "Update conversation name (deprecated)" - } - }, - "/conversations/{cnv}/otr/messages": { - "post": { - "description": " [internal route ID: \"post-otr-message-unqualified\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts two query params:\n- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are ignored.\n - When 'false' all missing clients are reported.\n - When comma separated list of user-ids, only clients for listed users are ignored.\n- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are reported.\n - When 'false' all missing clients are ignored.\n - When comma separated list of user-ids, only clients for listed users are reported.\n\nApart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\nAll three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n- `report_missing` in the request body has highest precedence.\n- `ignore_missing` in the query param is the next.\n- `report_missing` in the query param has the lowest precedence.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.Calls federation service brig on get-user-clients
Calls federation service galley on on-message-sent", + "summary": "Delete an asset" + }, + "get": { + "description": " [internal route ID: (\"assets-download-v3\", provider)]\n\n", + "operationId": "assets-download-v3_provider", "parameters": [ { "in": "path", - "name": "cnv", + "name": "key", "required": true, "schema": { - "format": "uuid", "type": "string" } }, { - "in": "query", - "name": "ignore_missing", + "in": "header", + "name": "Asset-Token", "required": false, "schema": { "type": "string" @@ -15220,65 +23869,45 @@ }, { "in": "query", - "name": "report_missing", + "name": "asset_token", "required": false, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/new-otr-message" - } - }, - "application/x-protobuf": { - "schema": { - "$ref": "#/components/schemas/new-otr-message" - } - } - }, - "required": true - }, "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ClientMismatch" - } - }, - "application/json;charset=utf-8": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", "schema": { - "$ref": "#/components/schemas/ClientMismatch" + "format": "url", + "type": "string" } } - }, - "description": "Message sent" + } }, - "403": { + "404": { "content": { "application/json": { "schema": { "example": { - "code": 403, - "label": "unknown-client", - "message": "Unknown Client" + "code": 404, + "label": "not-found", + "message": "Asset not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "unknown-client", - "missing-legalhold-consent-old-clients", - "missing-legalhold-consent" + "not-found" ], "type": "string" }, @@ -15297,22 +23926,20 @@ "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "unknown-client", - "message": "Unknown Client" + "code": 404, + "label": "not-found", + "message": "Asset not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "unknown-client", - "missing-legalhold-consent-old-clients", - "missing-legalhold-consent" + "not-found" ], "type": "string" }, @@ -15329,27 +23956,49 @@ } } }, - "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + "description": "`key` or Asset not found (label: `not-found`)" + } + }, + "summary": "Download an asset" + } + }, + "/provider/email": { + "put": { + "description": " [internal route ID: \"provider-update-email\"]\n\n", + "operationId": "provider-update-email", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EmailUpdate_LTYwODE0ODQ5" + } + } }, - "404": { + "required": true + }, + "responses": { + "202": { + "description": "" + }, + "400": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." }, "properties": { "code": { "enum": [ - 404 + 400 ], "type": "integer" }, "label": { "enum": [ - "no-conversation" + "invalid-email" ], "type": "string" }, @@ -15364,24 +24013,30 @@ ], "type": "object" } - }, + } + }, + "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "code": 403, + "label": "invalid-provider", + "message": "The provider does not exist." }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-conversation" + "invalid-provider", + "access-denied" ], "type": "string" }, @@ -15398,48 +24053,58 @@ } } }, - "description": "`cnv` or Conversation not found (label: `no-conversation`)" + "description": "The provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" }, - "412": { + "429": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ClientMismatch" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ClientMismatch" + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Too many request to generate a verification code." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "Missing clients" + "description": "Too many request to generate a verification code. (label: `too-many-requests`)" } }, - "summary": "Post an encrypted message to a conversation (accepts JSON or Protobuf)" + "summary": "Update a provider email" } }, - "/conversations/{cnv}/receipt-mode": { - "put": { - "deprecated": true, - "description": " [internal route ID: \"update-conversation-receipt-mode-unqualified\"]\n\nUse `PUT /conversations/:domain/:cnv/receipt-mode` instead.Calls federation service brig on get-users-by-ids
Calls federation service galley on update-conversation
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", - "parameters": [ - { - "description": "Conversation ID", - "in": "path", - "name": "cnv", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], + "/provider/login": { + "post": { + "description": " [internal route ID: \"provider-login\"]\n\n", + "operationId": "provider-login", "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConversationReceiptModeUpdate" + "$ref": "#/components/schemas/ProviderLogin_LTE2MTk2NTM5" } } }, @@ -15447,22 +24112,74 @@ }, "responses": { "200": { - "content": { - "application/json": { + "description": "OK", + "headers": { + "Set-Cookie": { "schema": { - "$ref": "#/components/schemas/Event" + "type": "string" } - }, + } + } + }, + "403": { + "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Event" + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "Receipt mode updated" + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Login as a provider" + } + }, + "/provider/password": { + "put": { + "description": " [internal route ID: \"provider-update-password\"]\n\n", + "operationId": "provider-update-password", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordChange_NDI0ODgwNDU0" + } + } }, - "204": { - "description": "Receipt mode unchanged" + "required": true + }, + "responses": { + "200": { + "description": "" }, "403": { "content": { @@ -15470,8 +24187,8 @@ "schema": { "example": { "code": 403, - "label": "invalid-op", - "message": "Invalid operation" + "label": "invalid-credentials", + "message": "Authentication failed" }, "properties": { "code": { @@ -15482,9 +24199,8 @@ }, "label": { "enum": [ - "invalid-op", - "access-denied", - "action-denied" + "invalid-credentials", + "access-denied" ], "type": "string" }, @@ -15501,27 +24217,27 @@ } } }, - "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_receipt_mode) (label: `action-denied`)" + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" }, - "404": { + "409": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." }, "properties": { "code": { "enum": [ - 404 + 409 ], "type": "integer" }, "label": { "enum": [ - "no-conversation" + "password-must-differ" ], "type": "string" }, @@ -15538,36 +24254,67 @@ } } }, - "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)" } }, - "summary": "Update receipt mode for a conversation (deprecated)" + "summary": "Update a provider password" } }, - "/conversations/{cnv}/roles": { - "get": { - "description": " [internal route ID: \"get-conversation-roles\"]\n\n", - "parameters": [ - { - "in": "path", - "name": "cnv", - "required": true, - "schema": { - "format": "uuid", - "type": "string" + "/provider/password-reset": { + "post": { + "description": " [internal route ID: \"provider-password-reset\"]\n\n", + "operationId": "provider-password-reset", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordReset_LTYzNDYxNTQ3" + } } - } - ], + }, + "required": true + }, "responses": { - "200": { + "201": { + "description": "" + }, + "400": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConversationRolesList" + "example": { + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code", + "invalid-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "" + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)\n\nInvalid email or mobile number for password reset. (label: `invalid-key`)" }, "403": { "content": { @@ -15575,8 +24322,8 @@ "schema": { "example": { "code": 403, - "label": "access-denied", - "message": "Conversation access denied" + "label": "invalid-credentials", + "message": "Authentication failed" }, "properties": { "code": { @@ -15587,6 +24334,7 @@ }, "label": { "enum": [ + "invalid-credentials", "access-denied" ], "type": "string" @@ -15604,27 +24352,28 @@ } } }, - "description": "Conversation access denied (label: `access-denied`)" + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" }, - "404": { + "409": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." }, "properties": { "code": { "enum": [ - 404 + 409 ], "type": "integer" }, "label": { "enum": [ - "no-conversation" + "password-must-differ", + "code-exists" ], "type": "string" }, @@ -15641,90 +24390,27 @@ } } }, - "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" - } - }, - "summary": "Get existing roles available for the given conversation" - } - }, - "/conversations/{cnv}/self": { - "get": { - "deprecated": true, - "description": " [internal route ID: \"get-conversation-self-unqualified\"]\n\n", - "parameters": [ - { - "description": "Conversation ID", - "in": "path", - "name": "cnv", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/Member" - } - } - }, - "description": "" - } - }, - "summary": "Get self membership properties (deprecated)" - }, - "put": { - "deprecated": true, - "description": " [internal route ID: \"update-conversation-self-unqualified\"]\n\nUse `/conversations/:domain/:conv/self` instead.", - "parameters": [ - { - "description": "Conversation ID", - "in": "path", - "name": "cnv", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/MemberUpdate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Update successful" + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)\n\nA password reset is already in progress. (label: `code-exists`)\n\nA password reset is already in progress. (label: `code-exists`)" }, - "404": { + "429": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "code": 429, + "label": "too-many-requests", + "message": "Too many request to generate a verification code." }, "properties": { "code": { "enum": [ - 404 + 429 ], "type": "integer" }, "label": { "enum": [ - "no-conversation" + "too-many-requests" ], "type": "string" }, @@ -15741,54 +24427,21 @@ } } }, - "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" - } - }, - "summary": "Update self membership properties (deprecated)" - } - }, - "/cookies": { - "get": { - "description": " [internal route ID: \"list-cookies\"]\n\n", - "parameters": [ - { - "description": "Filter by label (comma-separated list)", - "in": "query", - "name": "labels", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CookieList" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/CookieList" - } - } - }, - "description": "List of cookies" + "description": "Too many request to generate a verification code. (label: `too-many-requests`)" } }, - "summary": "Retrieve the list of cookies currently stored for the user" + "summary": "Begin a password reset" } }, - "/cookies/remove": { + "/provider/password-reset/complete": { "post": { - "description": " [internal route ID: \"remove-cookies\"]\n\n", + "description": " [internal route ID: \"provider-password-reset-complete\"]\n\n", + "operationId": "provider-password-reset-complete", "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/RemoveCookies" + "$ref": "#/components/schemas/CompletePasswordReset_LTYzMDAxNDA1" } } }, @@ -15796,7 +24449,44 @@ }, "responses": { "200": { - "description": "Cookies revoked" + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)" }, "403": { "content": { @@ -15816,7 +24506,9 @@ }, "label": { "enum": [ - "invalid-credentials" + "invalid-credentials", + "invalid-code", + "access-denied" ], "type": "string" }, @@ -15833,56 +24525,27 @@ } } }, - "description": "Authentication failed (label: `invalid-credentials`)" - } - }, - "summary": "Revoke stored cookies" - } - }, - "/custom-backend/by-domain/{domain}": { - "get": { - "description": " [internal route ID: \"get-custom-backend-by-domain\"]\n\n", - "parameters": [ - { - "description": "URL-encoded email domain", - "in": "path", - "name": "domain", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/CustomBackend" - } - } - }, - "description": "" + "description": "Authentication failed (label: `invalid-credentials`)\n\nInvalid verification code (label: `invalid-code`)\n\nAccess denied. (label: `access-denied`)" }, - "404": { + "409": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "custom-backend-not-found", - "message": "Custom backend not found" + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." }, "properties": { "code": { "enum": [ - 404 + 409 ], "type": "integer" }, "label": { "enum": [ - "custom-backend-not-found" + "password-must-differ" ], "type": "string" }, @@ -15899,48 +24562,71 @@ } } }, - "description": "`domain` not found\n\nCustom backend not found (label: `custom-backend-not-found`)" + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)" } }, - "summary": "Shows information about custom backends related to a given email domain" + "summary": "Complete a password reset" } }, - "/delete": { + "/provider/register": { "post": { - "description": " [internal route ID: \"verify-delete\"]\n\nCalls federation service brig on send-connection-action", + "description": " [internal route ID: \"provider-register\"]\n\n", + "operationId": "provider-register", + "parameters": [ + { + "in": "header", + "name": "X-Forwarded-For", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/VerifyDeleteUser" + "$ref": "#/components/schemas/NewProvider_LTEyMTY5MjYy" } } }, "required": true }, "responses": { - "200": { - "description": "Deletion is initiated." + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewProviderResponse_OTE0ODI2NjU0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewProviderResponse_OTE0ODI2NjU0" + } + } + }, + "description": "" }, - "403": { + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "invalid-code", - "message": "Invalid verification code" + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." }, "properties": { "code": { "enum": [ - 403 + 400 ], "type": "integer" }, "label": { "enum": [ - "invalid-code" + "invalid-email" ], "type": "string" }, @@ -15957,25 +24643,7 @@ } } }, - "description": "Invalid verification code (label: `invalid-code`)" - } - }, - "summary": "Verify account deletion with a code." - } - }, - "/feature-configs": { - "get": { - "description": " [internal route ID: \"get-all-feature-configs-for-user\"]\n\nGets feature configs for a user. If the user is a member of a team and has the required permissions, this will return the team's feature configs.If the user is not a member of a team, this will return the personal feature configs (the server defaults).\nOAuth scope: `read:feature_configs`", - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/AllTeamFeatures" - } - } - }, - "description": "" + "description": "Invalid `body` or `X-Forwarded-For`\n\nInvalid e-mail address. (label: `invalid-email`)" }, "403": { "content": { @@ -15983,8 +24651,8 @@ "schema": { "example": { "code": 403, - "label": "operation-denied", - "message": "Insufficient permissions" + "label": "access-denied", + "message": "Access denied." }, "properties": { "code": { @@ -15995,8 +24663,7 @@ }, "label": { "enum": [ - "operation-denied", - "no-team-member" + "access-denied" ], "type": "string" }, @@ -16013,27 +24680,27 @@ } } }, - "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + "description": "Access denied. (label: `access-denied`)" }, - "404": { + "429": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" + "code": 429, + "label": "too-many-requests", + "message": "Too many request to generate a verification code." }, "properties": { "code": { "enum": [ - 404 + 429 ], "type": "integer" }, "label": { "enum": [ - "no-team" + "too-many-requests" ], "type": "string" }, @@ -16050,72 +24717,78 @@ } } }, - "description": "Team not found (label: `no-team`)" + "description": "Too many request to generate a verification code. (label: `too-many-requests`)" } }, - "summary": "Gets feature configs for a user" + "summary": "Register a new provider" } }, - "/identity-providers": { + "/provider/services": { "get": { + "description": " [internal route ID: \"get-provider-services\"]\n\n", + "operationId": "get-provider-services", "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/IdPList" + "items": { + "$ref": "#/components/schemas/Service_MjcyOTA5NjQx" + }, + "type": "array" } } }, "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" } - } + }, + "summary": "List provider services" }, "post": { - "parameters": [ - { - "in": "query", - "name": "replaces", - "required": false, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "in": "query", - "name": "api_version", - "required": false, - "schema": { - "default": "v2", - "enum": [ - "v1", - "v2" - ], - "type": "string" - } - }, - { - "in": "query", - "name": "handle", - "required": false, - "schema": { - "maxLength": 1, - "minLength": 32, - "type": "string" - } - } - ], + "description": " [internal route ID: \"post-provider-services\"]\n\n", + "operationId": "post-provider-services", "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/IdPMetadataInfo" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/IdPMetadataInfo" + "$ref": "#/components/schemas/NewService_LTYwOTU1MDQ3" } } }, @@ -16124,126 +24797,105 @@ "responses": { "201": { "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewServiceResponse_LTExMzcwMjg5" + } + }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/IdPConfig_WireIdP" + "$ref": "#/components/schemas/NewServiceResponse_LTExMzcwMjg5" } } }, "description": "" - } - } - } - }, - "/identity-providers/{id}": { - "delete": { - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } }, - { - "in": "query", - "name": "purge", - "required": false, - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "204": { - "description": "" - } - } - }, - "get": { - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { + "400": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/IdPConfig_WireIdP" + "example": { + "code": 400, + "label": "invalid-service-key", + "message": "Invalid service key." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-service-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "" - } - } - }, - "put": { - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "in": "query", - "name": "handle", - "required": false, - "schema": { - "maxLength": 1, - "minLength": 32, - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/IdPMetadataInfo" - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/IdPMetadataInfo" - } - } + "description": "Invalid `body`\n\nInvalid service key. (label: `invalid-service-key`)" }, - "required": true - }, - "responses": { - "200": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/IdPConfig_WireIdP" + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "" + "description": "Access denied. (label: `access-denied`)" } - } + }, + "summary": "Create a new service" } }, - "/identity-providers/{id}/raw": { - "get": { + "/provider/services/{service-id}": { + "delete": { + "description": " [internal route ID: \"delete-provider-services-by-service-id\"]\n\n", + "operationId": "delete-provider-services-by-service-id", "parameters": [ { "in": "path", - "name": "id", + "name": "service-id", "required": true, "schema": { "format": "uuid", @@ -16251,122 +24903,122 @@ } } ], - "responses": { - "200": { - "content": { - "application/xml": { - "schema": { - "type": "string" - } - } - }, - "description": "" - } - } - } - }, - "/list-connections": { - "post": { - "description": " [internal route ID: \"list-connections\"]\n\nThe IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/GetPaginated_Connections" + "$ref": "#/components/schemas/DeleteService_LTY2NzY5NzMz" } } }, "required": true }, "responses": { - "200": { + "202": { + "description": "" + }, + "403": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Connections_Page" + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "" - } - }, - "summary": "List the connections to other users, including remote users" - } - }, - "/list-users": { - "post": { - "description": " [internal route ID: \"list-users-by-ids-or-handles\"]\n\nThe 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive.Calls federation service brig on get-users-by-ids", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ListUsersQuery" - } - } + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" }, - "required": true - }, - "responses": { - "200": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ListUsersById" + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "" + "description": "`service-id` not found\n\nService not found. (label: `not-found`)" } }, - "summary": "List users" - } - }, - "/login": { - "post": { - "description": " [internal route ID: \"login\"]\n\nLogins are throttled at the server's discretionCalls federation service brig on send-connection-action", + "summary": "Delete service" + }, + "get": { + "description": " [internal route ID: \"get-provider-services-by-service-id\"]\n\n", + "operationId": "get-provider-services-by-service-id", "parameters": [ { - "description": "Request a persistent cookie instead of a session cookie", - "in": "query", - "name": "persist", - "required": false, - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/Login" - } + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AccessToken" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/AccessToken" + "$ref": "#/components/schemas/Service_MjcyOTA5NjQx" } } }, - "description": "OK", - "headers": { - "Set-Cookie": { - "schema": { - "type": "string" - } - } - } + "description": "" }, "403": { "content": { @@ -16374,8 +25026,8 @@ "schema": { "example": { "code": 403, - "label": "code-authentication-required", - "message": "Code authentication is required" + "label": "access-denied", + "message": "Access denied." }, "properties": { "code": { @@ -16386,11 +25038,7 @@ }, "label": { "enum": [ - "code-authentication-required", - "code-authentication-failed", - "pending-activation", - "suspended", - "invalid-credentials" + "access-denied" ], "type": "string" }, @@ -16407,67 +25055,27 @@ } } }, - "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nAccount pending activation (label: `pending-activation`)\n\nAccount suspended (label: `suspended`)\n\nAuthentication failed (label: `invalid-credentials`)" - } - }, - "summary": "Authenticate a user to obtain a cookie and first access token" - } - }, - "/mls/commit-bundles": { - "post": { - "description": " [internal route ID: \"mls-commit-bundle\"]\n\n\n\n**Note**: this endpoint can execute proposals, and therefore return all possible errors associated with adding or removing members to a conversation, in addition to the ones listed below. See the documentation of [POST /conversations/{cnv}/members/v2](#/default/post_conversations__cnv__members_v2) and [POST /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}](#/default/delete_conversations__cnv_domain___cnv__members__usr_domain___usr_) for more details on the possible error responses of each type of proposal.
Calls federation service brig on api-version
Calls federation service brig on get-users-by-ids
Calls federation service brig on get-mls-clients
Calls federation service galley on on-conversation-updated
Calls federation service galley on send-mls-commit-bundle
Calls federation service galley on mls-welcome
Calls federation service galley on on-mls-message-sent", - "requestBody": { - "content": { - "message/mls": { - "schema": { - "$ref": "#/components/schemas/CommitBundle" - } - } - }, - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MLSMessageSendingStatus" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/MLSMessageSendingStatus" - } - } - }, - "description": "Commit accepted and forwarded" + "description": "Access denied. (label: `access-denied`)" }, - "400": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "mls-welcome-mismatch", - "message": "The list of targets of a welcome message does not match the list of new clients in a group" + "code": 404, + "label": "not-found", + "message": "Service not found." }, "properties": { "code": { "enum": [ - 400 + 404 ], "type": "integer" }, "label": { "enum": [ - "mls-welcome-mismatch", - "mls-self-removal-not-allowed", - "mls-protocol-error", - "mls-not-enabled", - "mls-invalid-leaf-node-index", - "mls-group-conversation-mismatch", - "mls-commit-missing-references", - "mls-client-sender-user-mismatch" + "not-found" ], "type": "string" }, @@ -16484,7 +25092,38 @@ } } }, - "description": "Invalid `body`\n\nThe list of targets of a welcome message does not match the list of new clients in a group (label: `mls-welcome-mismatch`)\n\nSelf removal from group is not allowed (label: `mls-self-removal-not-allowed`)\n\nMLS protocol error (label: `mls-protocol-error`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nA referenced leaf node index points to a blank or non-existing node (label: `mls-invalid-leaf-node-index`)\n\nConversation ID resolved from Group ID does not match submitted Conversation ID (label: `mls-group-conversation-mismatch`)\n\nThe commit is not referencing all pending proposals (label: `mls-commit-missing-references`)\n\nUser ID resolved from Client ID does not match message's sender user ID (label: `mls-client-sender-user-mismatch`)" + "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Get provider service by service id" + }, + "put": { + "description": " [internal route ID: \"put-provider-services-by-service-id\"]\n\n", + "operationId": "put-provider-services-by-service-id", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateService_MjAxNzQ2Njkz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Provider service updated" }, "403": { "content": { @@ -16492,8 +25131,8 @@ "schema": { "example": { "code": 403, - "label": "mls-subconv-join-parent-missing", - "message": "MLS client cannot join the subconversation because it is not member of the parent conversation" + "label": "access-denied", + "message": "Access denied." }, "properties": { "code": { @@ -16504,9 +25143,6 @@ }, "label": { "enum": [ - "mls-subconv-join-parent-missing", - "missing-legalhold-consent", - "legalhold-not-enabled", "access-denied" ], "type": "string" @@ -16524,7 +25160,7 @@ } } }, - "description": "MLS client cannot join the subconversation because it is not member of the parent conversation (label: `mls-subconv-join-parent-missing`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nConversation access denied (label: `access-denied`)" + "description": "Access denied. (label: `access-denied`)" }, "404": { "content": { @@ -16532,8 +25168,8 @@ "schema": { "example": { "code": 404, - "label": "mls-proposal-not-found", - "message": "A proposal referenced in a commit message could not be found" + "label": "not-found", + "message": "Provider not found." }, "properties": { "code": { @@ -16544,9 +25180,7 @@ }, "label": { "enum": [ - "mls-proposal-not-found", - "no-conversation", - "no-conversation-member" + "not-found" ], "type": "string" }, @@ -16563,49 +25197,98 @@ } } }, - "description": "A proposal referenced in a commit message could not be found (label: `mls-proposal-not-found`)\n\nConversation not found (label: `no-conversation`)\n\nConversation member not found (label: `no-conversation-member`)" + "description": "`service-id` not found\n\nProvider not found. (label: `not-found`)\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Update provider service" + } + }, + "/provider/services/{service-id}/connection": { + "put": { + "description": " [internal route ID: \"put-provider-services-connection-by-service-id\"]\n\n", + "operationId": "put-provider-services-connection-by-service-id", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateServiceConn_LTQ1OTYwNjIz" + } + } }, - "409": { + "required": true + }, + "responses": { + "200": { + "description": "Provider service connection updated" + }, + "400": { "content": { "application/json;charset=utf-8": { "schema": { + "example": { + "code": 400, + "label": "invalid-service-key", + "message": "Invalid service key." + }, "properties": { - "non_federating_backends": { - "items": { - "$ref": "#/components/schemas/Domain" - }, - "type": "array" + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-service-key" + ], + "type": "string" + }, + "message": { + "type": "string" } }, "required": [ - "non_federating_backends" + "code", + "label", + "message" ], "type": "object" } } }, - "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph\n\nThe conversation epoch in a message is too old (label: `mls-stale-message`)\n\nA proposal of type Add or Remove does not apply to the full list of clients for a user (label: `mls-client-mismatch`)" + "description": "Invalid `body`\n\nInvalid service key. (label: `invalid-service-key`)" }, - "422": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 422, - "label": "mls-unsupported-proposal", - "message": "Unsupported proposal type" + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" }, "properties": { "code": { "enum": [ - 422 + 403 ], "type": "integer" }, "label": { "enum": [ - "mls-unsupported-proposal", - "mls-unsupported-message" + "invalid-credentials", + "access-denied" ], "type": "string" }, @@ -16622,63 +25305,62 @@ } } }, - "description": "Unsupported proposal type (label: `mls-unsupported-proposal`)\n\nAttempted to send a message with an unsupported combination of content type and wire format (label: `mls-unsupported-message`)" + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" }, - "533": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, "properties": { - "unreachable_backends": { - "items": { - "$ref": "#/components/schemas/Domain" - }, - "type": "array" + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" } }, "required": [ - "unreachable_backends" + "code", + "label", + "message" ], "type": "object" } } }, - "description": "Some domains are unreachable" + "description": "`service-id` not found\n\nService not found. (label: `not-found`)" } }, - "summary": "Post a MLS CommitBundle" + "summary": "Update provider service connection" } }, - "/mls/key-packages/claim/{user_domain}/{user}": { - "post": { - "description": " [internal route ID: \"mls-key-packages-claim\"]\n\nOnly key packages for the specified ciphersuite are claimed. For backwards compatibility, the `ciphersuite` parameter is optional, defaulting to ciphersuite 0x0001 when omitted.", + "/providers/{pid}": { + "get": { + "description": " [internal route ID: \"provider-get-profile\"]\n\n", + "operationId": "provider-get-profile", "parameters": [ { "in": "path", - "name": "user_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "User Id", - "in": "path", - "name": "user", + "name": "pid", "required": true, "schema": { "format": "uuid", "type": "string" } - }, - { - "description": "Ciphersuite in hex format (e.g. 0xf031) - default is 0x0001", - "in": "query", - "name": "ciphersuite", - "required": false, - "schema": { - "type": "number" - } } ], "responses": { @@ -16686,107 +25368,68 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/KeyPackageBundle" + "$ref": "#/components/schemas/Provider_NDIyMzQ3ODIy" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/KeyPackageBundle" + "$ref": "#/components/schemas/Provider_NDIyMzQ3ODIy" } } }, - "description": "Claimed key packages" - } - }, - "summary": "Claim one key package for each client of the given user" - } - }, - "/mls/key-packages/self/{client}": { - "delete": { - "description": " [internal route ID: \"mls-key-packages-delete\"]\n\n", - "parameters": [ - { - "description": "ClientId", - "in": "path", - "name": "client", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "Ciphersuite in hex format (e.g. 0xf031) - default is 0x0001", - "in": "query", - "name": "ciphersuite", - "required": false, - "schema": { - "type": "number" - } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/DeleteKeyPackages" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "OK" - } - }, - "summary": "Delete all key packages for a given ciphersuite and client" - }, - "post": { - "description": " [internal route ID: \"mls-key-packages-upload\"]\n\nThe request body should be a json object containing a list of base64-encoded key packages.", - "parameters": [ - { - "description": "ClientId", - "in": "path", - "name": "client", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/KeyPackageUpload" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Key packages uploaded" + "description": "" }, - "400": { + "404": { "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "mls-protocol-error", - "message": "MLS protocol error" + "code": 404, + "label": "not-found", + "message": "Provider not found." }, "properties": { "code": { "enum": [ - 400 + 404 ], "type": "integer" }, "label": { "enum": [ - "mls-protocol-error" + "not-found" ], "type": "string" }, @@ -16803,7 +25446,40 @@ } } }, - "description": "Invalid `body`\n\nMLS protocol error (label: `mls-protocol-error`)" + "description": "`pid` or Provider not found. (label: `not-found`)" + } + }, + "summary": "Get profile" + } + }, + "/providers/{provider-id}/services": { + "get": { + "description": " [internal route ID: \"get-provider-services-by-provider-id\"]\n\n", + "operationId": "get-provider-services-by-provider-id", + "parameters": [ + { + "in": "path", + "name": "provider-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/ServiceProfile_LTc2MDQzNTk3" + }, + "type": "array" + } + } + }, + "description": "" }, "403": { "content": { @@ -16811,8 +25487,8 @@ "schema": { "example": { "code": 403, - "label": "mls-identity-mismatch", - "message": "Key package credential does not match qualified client ID" + "label": "access-denied", + "message": "Access denied." }, "properties": { "code": { @@ -16823,7 +25499,7 @@ }, "label": { "enum": [ - "mls-identity-mismatch" + "access-denied" ], "type": "string" }, @@ -16840,66 +25516,66 @@ } } }, - "description": "Key package credential does not match qualified client ID (label: `mls-identity-mismatch`)" + "description": "Access denied. (label: `access-denied`)" } }, - "summary": "Upload a fresh batch of key packages" - }, - "put": { - "description": " [internal route ID: \"mls-key-packages-replace\"]\n\nThe request body should be a json object containing a list of base64-encoded key packages. Use this sparingly.", + "summary": "Get provider services by provider id" + } + }, + "/providers/{provider-id}/services/{service-id}": { + "get": { + "description": " [internal route ID: \"get-provider-services-by-provider-id-and-service-id\"]\n\n", + "operationId": "get-provider-services-by-provider-id-and-service-id", "parameters": [ { - "description": "ClientId", "in": "path", - "name": "client", + "name": "provider-id", "required": true, "schema": { + "format": "uuid", "type": "string" } }, { - "description": "Comma-separated list of ciphersuites in hex format (e.g. 0xf031) - default is 0x0001", - "in": "query", - "name": "ciphersuites", - "required": false, + "in": "path", + "name": "service-id", + "required": true, "schema": { + "format": "uuid", "type": "string" } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/KeyPackageUpload" - } - } - }, - "required": true - }, "responses": { - "201": { - "description": "Key packages replaced" + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceProfile_LTc2MDQzNTk3" + } + } + }, + "description": "" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "mls-protocol-error", - "message": "MLS protocol error" + "code": 403, + "label": "access-denied", + "message": "Access denied." }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "mls-protocol-error" + "access-denied" ], "type": "string" }, @@ -16916,27 +25592,27 @@ } } }, - "description": "Invalid `body` or `ciphersuites`\n\nMLS protocol error (label: `mls-protocol-error`)" + "description": "Access denied. (label: `access-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "mls-identity-mismatch", - "message": "Key package credential does not match qualified client ID" + "code": 404, + "label": "not-found", + "message": "Service not found." }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "mls-identity-mismatch" + "not-found" ], "type": "string" }, @@ -16953,63 +25629,45 @@ } } }, - "description": "Key package credential does not match qualified client ID (label: `mls-identity-mismatch`)" - } - }, - "summary": "Upload a fresh batch of key packages and replace the old ones" - } - }, - "/mls/key-packages/self/{client}/count": { - "get": { - "description": " [internal route ID: \"mls-key-packages-count\"]\n\n", - "parameters": [ - { - "description": "ClientId", - "in": "path", - "name": "client", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "Ciphersuite in hex format (e.g. 0xf031) - default is 0x0001", - "in": "query", - "name": "ciphersuite", - "required": false, - "schema": { - "type": "number" - } + "description": "`provider-id` or `service-id` not found\n\nService not found. (label: `not-found`)" } - ], + }, + "summary": "Get provider service by provider id and service id" + } + }, + "/proxy/giphy/v1/gifs": {}, + "/proxy/googlemaps/api/staticmap": {}, + "/proxy/googlemaps/maps/api/geocode": {}, + "/proxy/soundcloud/resolve": {}, + "/proxy/soundcloud/stream": {}, + "/proxy/spotify/api/token": {}, + "/proxy/youtube/v3": {}, + "/push/tokens": { + "get": { + "description": " [internal route ID: \"get-push-tokens\"]\n\n", + "operationId": "get-push-tokens", "responses": { "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OwnKeyPackages" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/OwnKeyPackages" + "$ref": "#/components/schemas/PushTokenList_NDI0Mjc3MzY3" } } }, - "description": "Number of key packages" + "description": "" } }, - "summary": "Return the number of unclaimed key packages for a given ciphersuite and client" - } - }, - "/mls/messages": { + "summary": "List the user's registered push tokens" + }, "post": { - "description": " [internal route ID: \"mls-message\"]\n\n\n\n**Note**: this endpoint can execute proposals, and therefore return all possible errors associated with adding or removing members to a conversation, in addition to the ones listed below. See the documentation of [POST /conversations/{cnv}/members/v2](#/default/post_conversations__cnv__members_v2) and [POST /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}](#/default/delete_conversations__cnv_domain___cnv__members__usr_domain___usr_) for more details on the possible error responses of each type of proposal.
Calls federation service brig on get-mls-clients
Calls federation service galley on on-conversation-updated
Calls federation service galley on send-mls-message
Calls federation service galley on on-mls-message-sent", + "description": " [internal route ID: \"register-push-token\"]\n\n", + "operationId": "register-push-token", "requestBody": { "content": { - "message/mls": { + "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MLSMessage" + "$ref": "#/components/schemas/PushToken_ODYzMDYzOTA4" } } }, @@ -17020,25 +25678,32 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MLSMessageSendingStatus" + "$ref": "#/components/schemas/PushToken_ODYzMDYzOTA4" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MLSMessageSendingStatus" + "$ref": "#/components/schemas/PushToken_ODYzMDYzOTA4" } } }, - "description": "Message sent" + "description": "Push token registered", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } }, "400": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { "example": { "code": 400, - "label": "mls-self-removal-not-allowed", - "message": "Self removal from group is not allowed" + "label": "apns-voip-not-supported", + "message": "Adding APNS_VOIP tokens is not supported" }, "properties": { "code": { @@ -17049,13 +25714,7 @@ }, "label": { "enum": [ - "mls-self-removal-not-allowed", - "mls-protocol-error", - "mls-not-enabled", - "mls-invalid-leaf-node-index", - "mls-group-conversation-mismatch", - "mls-commit-missing-references", - "mls-client-sender-user-mismatch" + "apns-voip-not-supported" ], "type": "string" }, @@ -17070,32 +25729,24 @@ ], "type": "object" } - } - }, - "description": "Invalid `body`\n\nSelf removal from group is not allowed (label: `mls-self-removal-not-allowed`)\n\nMLS protocol error (label: `mls-protocol-error`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nA referenced leaf node index points to a blank or non-existing node (label: `mls-invalid-leaf-node-index`)\n\nConversation ID resolved from Group ID does not match submitted Conversation ID (label: `mls-group-conversation-mismatch`)\n\nThe commit is not referencing all pending proposals (label: `mls-commit-missing-references`)\n\nUser ID resolved from Client ID does not match message's sender user ID (label: `mls-client-sender-user-mismatch`)" - }, - "403": { - "content": { + }, "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "mls-subconv-join-parent-missing", - "message": "MLS client cannot join the subconversation because it is not member of the parent conversation" + "code": 400, + "label": "apns-voip-not-supported", + "message": "Adding APNS_VOIP tokens is not supported" }, "properties": { "code": { "enum": [ - 403 + 400 ], "type": "integer" }, "label": { "enum": [ - "mls-subconv-join-parent-missing", - "missing-legalhold-consent", - "legalhold-not-enabled", - "access-denied" + "apns-voip-not-supported" ], "type": "string" }, @@ -17112,16 +25763,16 @@ } } }, - "description": "MLS client cannot join the subconversation because it is not member of the parent conversation (label: `mls-subconv-join-parent-missing`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nConversation access denied (label: `access-denied`)" + "description": "Adding APNS_VOIP tokens is not supported (label: `apns-voip-not-supported`) or `body`" }, "404": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { "example": { "code": 404, - "label": "mls-proposal-not-found", - "message": "A proposal referenced in a commit message could not be found" + "label": "app-not-found", + "message": "App does not exist" }, "properties": { "code": { @@ -17132,9 +25783,8 @@ }, "label": { "enum": [ - "mls-proposal-not-found", - "no-conversation", - "no-conversation-member" + "app-not-found", + "invalid-token" ], "type": "string" }, @@ -17149,51 +25799,25 @@ ], "type": "object" } - } - }, - "description": "A proposal referenced in a commit message could not be found (label: `mls-proposal-not-found`)\n\nConversation not found (label: `no-conversation`)\n\nConversation member not found (label: `no-conversation-member`)" - }, - "409": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "properties": { - "non_federating_backends": { - "items": { - "$ref": "#/components/schemas/Domain" - }, - "type": "array" - } - }, - "required": [ - "non_federating_backends" - ], - "type": "object" - } - } - }, - "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph\n\nThe conversation epoch in a message is too old (label: `mls-stale-message`)\n\nA proposal of type Add or Remove does not apply to the full list of clients for a user (label: `mls-client-mismatch`)" - }, - "422": { - "content": { + }, "application/json;charset=utf-8": { "schema": { "example": { - "code": 422, - "label": "mls-unsupported-proposal", - "message": "Unsupported proposal type" + "code": 404, + "label": "app-not-found", + "message": "App does not exist" }, "properties": { "code": { "enum": [ - 422 + 404 ], "type": "integer" }, "label": { "enum": [ - "mls-unsupported-proposal", - "mls-unsupported-message" + "app-not-found", + "invalid-token" ], "type": "string" }, @@ -17210,85 +25834,63 @@ } } }, - "description": "Unsupported proposal type (label: `mls-unsupported-proposal`)\n\nAttempted to send a message with an unsupported combination of content type and wire format (label: `mls-unsupported-message`)" + "description": "App does not exist (label: `app-not-found`)\n\nInvalid push token (label: `invalid-token`)" }, - "533": { + "413": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { + "example": { + "code": 413, + "label": "sns-thread-budget-reached", + "message": "Too many concurrent calls to SNS; is SNS down?" + }, "properties": { - "unreachable_backends": { - "items": { - "$ref": "#/components/schemas/Domain" - }, - "type": "array" + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "sns-thread-budget-reached", + "token-too-long", + "metadata-too-long" + ], + "type": "string" + }, + "message": { + "type": "string" } }, "required": [ - "unreachable_backends" + "code", + "label", + "message" ], "type": "object" } - } - }, - "description": "Some domains are unreachable" - } - }, - "summary": "Post an MLS message" - } - }, - "/mls/public-keys": { - "get": { - "description": " [internal route ID: \"mls-public-keys\"]\n\nThe format of the returned key is determined by the `format` query parameter:\n - raw (default): base64-encoded raw public keys\n - jwk: keys are nested objects in JWK format.", - "parameters": [ - { - "in": "query", - "name": "format", - "required": false, - "schema": { - "enum": [ - "raw", - "jwk" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MLSKeysByPurpose" - } }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/MLSKeysByPurpose" - } - } - }, - "description": "Public keys" - }, - "400": { - "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "mls-not-enabled", - "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + "code": 413, + "label": "sns-thread-budget-reached", + "message": "Too many concurrent calls to SNS; is SNS down?" }, "properties": { "code": { "enum": [ - 400 + 413 ], "type": "integer" }, "label": { "enum": [ - "mls-not-enabled" + "sns-thread-budget-reached", + "token-too-long", + "metadata-too-long" ], "type": "string" }, @@ -17305,63 +25907,30 @@ } } }, - "description": "Invalid `format`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + "description": "Too many concurrent calls to SNS; is SNS down? (label: `sns-thread-budget-reached`)\n\nPush token length must be < 8192 for GCM or 400 for APNS (label: `token-too-long`)\n\nTried to add token to endpoint resulting in metadata length > 2048 (label: `metadata-too-long`)" } }, - "summary": "Get public keys used by the backend to sign external proposals" + "summary": "Register a native push token" } }, - "/notifications": { - "get": { - "description": " [internal route ID: \"get-notifications\"]\n\n", + "/push/tokens/{pid}": { + "delete": { + "description": " [internal route ID: \"delete-push-token\"]\n\n", + "operationId": "delete-push-token", "parameters": [ { - "description": "Only return notifications more recent than this", - "in": "query", - "name": "since", - "required": false, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "description": "Only return notifications targeted at the given client", - "in": "query", - "name": "client", - "required": false, + "description": "The push token to delete", + "in": "path", + "name": "pid", + "required": true, "schema": { "type": "string" } - }, - { - "description": "Maximum number of notifications to return", - "in": "query", - "name": "size", - "required": false, - "schema": { - "format": "int32", - "maximum": 10000, - "minimum": 100, - "type": "integer" - } } ], "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/QueuedNotificationList" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/QueuedNotificationList" - } - } - }, - "description": "Notification list" + "204": { + "description": "Push token unregistered" }, "404": { "content": { @@ -17370,7 +25939,7 @@ "example": { "code": 404, "label": "not-found", - "message": "Some notifications not found" + "message": "Push token not found" }, "properties": { "code": { @@ -17402,7 +25971,7 @@ "example": { "code": 404, "label": "not-found", - "message": "Some notifications not found" + "message": "Push token not found" }, "properties": { "code": { @@ -17430,61 +25999,88 @@ } } }, - "description": "Some notifications not found (label: `not-found`)" + "description": "`pid` or Push token not found (label: `not-found`)" } }, - "summary": "Fetch notifications" + "summary": "Unregister a native push token" } }, - "/notifications/last": { - "get": { - "description": " [internal route ID: \"get-last-notification\"]\n\n", + "/register": { + "post": { + "description": " [internal route ID: \"register\"]\n\nIf the environment where the registration takes place is private and a registered email address is not whitelisted, a 403 error is returned.", + "operationId": "register", "parameters": [ { - "description": "Only return notifications targeted at the given client", - "in": "query", - "name": "client", - "required": false, + "in": "header", + "name": "X-Forwarded-For", + "required": true, "schema": { "type": "string" } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewUser_PlainTextPassword_8_LTI4MzI5NzQx" + } + } + }, + "required": true + }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QueuedNotification" + "$ref": "#/components/schemas/User_NjA4OTQwMTQ4" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/QueuedNotification" + "$ref": "#/components/schemas/User_NjA4OTQwMTQ4" } } }, - "description": "Notification found" + "description": "User created and pending activation", + "headers": { + "Location": { + "description": "UserId", + "schema": { + "format": "uuid", + "type": "string" + } + }, + "Set-Cookie": { + "description": "Cookie", + "schema": { + "type": "string" + } + } + } }, - "404": { + "400": { "content": { "application/json": { "schema": { "example": { - "code": 404, - "label": "not-found", - "message": "Some notifications not found" + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." }, "properties": { "code": { "enum": [ - 404 + 400 ], "type": "integer" }, "label": { "enum": [ - "not-found" + "invalid-invitation-code", + "invalid-email", + "invalid-phone" ], "type": "string" }, @@ -17503,20 +26099,22 @@ "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "not-found", - "message": "Some notifications not found" + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." }, "properties": { "code": { "enum": [ - 404 + 400 ], "type": "integer" }, "label": { "enum": [ - "not-found" + "invalid-invitation-code", + "invalid-email", + "invalid-phone" ], "type": "string" }, @@ -17533,51 +26131,88 @@ } } }, - "description": "Some notifications not found (label: `not-found`)" - } - }, - "summary": "Fetch the last notification" - } - }, - "/notifications/{id}": { - "get": { - "description": " [internal route ID: \"get-notification-by-id\"]\n\n", - "parameters": [ - { - "description": "Notification ID", - "in": "path", - "name": "id", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } + "description": "Invalid invitation code. (label: `invalid-invitation-code`)\n\nInvalid e-mail address. (label: `invalid-email`)\n\nInvalid mobile phone number (label: `invalid-phone`) or `body` or `X-Forwarded-For`" }, - { - "description": "Only return notifications targeted at the given client", - "in": "query", - "name": "client", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { + "403": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QueuedNotification" + "example": { + "code": 403, + "label": "unauthorized", + "message": "Unauthorized e-mail address" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorized", + "missing-identity", + "blacklisted-email", + "too-many-team-members", + "user-creation-restricted", + "ephemeral-user-creation-disabled", + "managed-by-scim" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/QueuedNotification" + "example": { + "code": 403, + "label": "unauthorized", + "message": "Unauthorized e-mail address" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorized", + "missing-identity", + "blacklisted-email", + "too-many-team-members", + "user-creation-restricted", + "ephemeral-user-creation-disabled", + "managed-by-scim" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "Notification found" + "description": "Unauthorized e-mail address (label: `unauthorized`)\n\nUsing an invitation code requires registering the given email. (label: `missing-identity`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nToo many members in this team. (label: `too-many-team-members`)\n\nThis instance does not allow creation of personal users or teams. (label: `user-creation-restricted`)\n\nEphemeral user creation is disabled on this instance. (label: `ephemeral-user-creation-disabled`)\n\nUpdating name is not allowed, because it is managed by SCIM, or E2EId is enabled (label: `managed-by-scim`)" }, "404": { "content": { @@ -17585,8 +26220,8 @@ "schema": { "example": { "code": 404, - "label": "not-found", - "message": "Some notifications not found" + "label": "invalid-code", + "message": "User does not exist" }, "properties": { "code": { @@ -17597,7 +26232,7 @@ }, "label": { "enum": [ - "not-found" + "invalid-code" ], "type": "string" }, @@ -17617,8 +26252,8 @@ "schema": { "example": { "code": 404, - "label": "not-found", - "message": "Some notifications not found" + "label": "invalid-code", + "message": "User does not exist" }, "properties": { "code": { @@ -17629,7 +26264,7 @@ }, "label": { "enum": [ - "not-found" + "invalid-code" ], "type": "string" }, @@ -17646,49 +26281,89 @@ } } }, - "description": "`id` or Some notifications not found (label: `not-found`)" - } - }, - "summary": "Fetch a notification by ID" - } - }, - "/oauth/applications": { - "get": { - "description": " [internal route ID: \"get-oauth-applications\"]\n\nGet all OAuth applications with active account access for a user.", - "responses": { - "200": { + "description": "User does not exist (label: `invalid-code`)\n\nInvalid activation code (label: `invalid-code`)" + }, + "409": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/OAuthApplication" + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." }, - "type": "array" + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } }, "application/json;charset=utf-8": { "schema": { - "items": { - "$ref": "#/components/schemas/OAuthApplication" + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." }, - "type": "array" + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "OAuth applications found" + "description": "The given e-mail address is in use. (label: `key-exists`)" } }, - "summary": "Get OAuth applications with account access" + "summary": "Register a new user." } }, - "/oauth/applications/{OAuthClientId}": { + "/scim/auth-tokens": { "delete": { - "description": " [internal route ID: \"revoke-oauth-account-access-v6\"]\n\n", + "description": " [internal route ID: \"auth-tokens-delete\"]\n\n", + "operationId": "auth-tokens-delete", "parameters": [ { - "description": "The ID of the OAuth client", - "in": "path", - "name": "OAuthClientId", + "in": "query", + "name": "id", "required": true, "schema": { "format": "uuid", @@ -17698,59 +26373,6 @@ ], "responses": { "204": { - "description": "OAuth application access revoked" - } - }, - "summary": "Revoke account access from an OAuth application" - } - }, - "/oauth/applications/{OAuthClientId}/sessions/{RefreshTokenId}": { - "delete": { - "description": " [internal route ID: \"delete-oauth-refresh-token\"]\n\nRevoke an active OAuth session by providing the refresh token ID.", - "parameters": [ - { - "description": "The ID of the OAuth client", - "in": "path", - "name": "OAuthClientId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "description": "The ID of the refresh token", - "in": "path", - "name": "RefreshTokenId", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/PasswordReqBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": [], - "items": {}, - "maxItems": 0, - "type": "array" - } - } - }, "description": "" }, "403": { @@ -17759,8 +26381,8 @@ "schema": { "example": { "code": 403, - "label": "access-denied", - "message": "Access denied." + "label": "code-authentication-required", + "message": "Code authentication is required" }, "properties": { "code": { @@ -17771,7 +26393,8 @@ }, "label": { "enum": [ - "access-denied" + "code-authentication-required", + "code-authentication-failed" ], "type": "string" }, @@ -17788,27 +26411,44 @@ } } }, - "description": "Access denied. (label: `access-denied`)" + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" + } + } + }, + "get": { + "description": " [internal route ID: \"auth-tokens-list\"]\n\n", + "operationId": "auth-tokens-list", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ScimTokenList_NjQwNTYxOTAw" + } + } + }, + "description": "" }, - "404": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "not-found", - "message": "OAuth client not found" + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "not-found" + "code-authentication-required", + "code-authentication-failed" ], "type": "string" }, @@ -17825,55 +26465,54 @@ } } }, - "description": "`OAuthClientId` or `RefreshTokenId` not found\n\nOAuth client not found (label: `not-found`)" + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" } - }, - "summary": "Revoke an active OAuth session" - } - }, - "/oauth/authorization/codes": { + } + }, "post": { - "description": " [internal route ID: \"create-oauth-auth-code\"]\n\nCurrently only supports the 'code' response type, which corresponds to the authorization code flow.", + "description": " [internal route ID: \"auth-tokens-create\"]\n\n", + "operationId": "auth-tokens-create", "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/CreateOAuthAuthorizationCodeRequest" + "$ref": "#/components/schemas/CreateScimToken_OTY0NjYxMDQ2" } } }, "required": true }, "responses": { - "201": { - "description": "Created", - "headers": { - "Location": { + "200": { + "content": { + "application/json;charset=utf-8": { "schema": { - "type": "string" + "$ref": "#/components/schemas/CreateScimTokenResponse_LTIzOTU2NDU4" } } - } + }, + "description": "" }, - "400": { + "403": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "redirect-url-miss-match", - "message": "The redirect URL does not match the one registered with the client" + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "redirect-url-miss-match" + "code-authentication-required", + "code-authentication-failed" ], "type": "string" }, @@ -17888,24 +26527,72 @@ ], "type": "object" } - }, + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" + } + } + } + }, + "/scim/auth-tokens/{id}": { + "put": { + "description": " [internal route ID: \"auth-tokens-put-name\"]\n\n", + "operationId": "auth-tokens-put-name", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ScimTokenName_LTgzOTM2OTI4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "redirect-url-miss-match", - "message": "The redirect URL does not match the one registered with the client" + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "redirect-url-miss-match" + "code-authentication-required", + "code-authentication-failed" ], "type": "string" }, @@ -17922,50 +26609,52 @@ } } }, - "description": "Bad Request\n\nThe redirect URL does not match the one registered with the client (label: `redirect-url-miss-match`) or `body`", - "headers": { - "Location": { - "schema": { - "type": "string" - } - } - } - }, - "403": { - "description": "Forbidden", - "headers": { - "Location": { - "schema": { - "type": "string" - } - } - } - }, - "404": { - "description": "Not Found", - "headers": { - "Location": { - "schema": { - "type": "string" - } - } - } + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" } - }, - "summary": "Create an OAuth authorization code" + } } }, - "/oauth/clients/{OAuthClientId}": { + "/search/contacts": { "get": { - "description": " [internal route ID: \"get-oauth-client\"]\n\n", + "description": " [internal route ID: \"search-contacts\"]\n\n", + "operationId": "search-contacts", "parameters": [ { - "description": "The ID of the OAuth client", - "in": "path", - "name": "OAuthClientId", + "description": "Search query

The search query is normalized: lower-cased and diacritics are removed ('Björn' becomes 'bjorn'). The normalized search query matches accounts that, in this order of priority:

  • are equal to the normalized full handle;
  • are equal to the normalized full user display name;
  • prefix-match the normalized handle;
  • prefix-match the normalized user display name.

NB: '@' Does NOT do anything special, ignoring user display names.

See also: [authoritative ElasticSearch query](https://github.com/wireapp/wire-server/blob/83c25cca6a5e9d2205c102410b452eb78fc50a00/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs#L251-L288)

", + "in": "query", + "name": "q", "required": true, "schema": { - "format": "uuid", + "type": "string" + } + }, + { + "description": "Searched domain. Note: This is optional only for backwards compatibility, future versions will mandate this.", + "in": "query", + "name": "domain", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Number of results to return (min: 1, max: 500, default 15)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Only user types. Omitted or empty (type=) means no filtering.", + "in": "query", + "name": "type", + "required": false, + "schema": { "type": "string" } } @@ -17973,18 +26662,13 @@ "responses": { "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OAuthClient" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/OAuthClient" + "$ref": "#/components/schemas/SearchResult_Contact_OTExNzg4MTE0" } } }, - "description": "OAuth client found" + "description": "" }, "403": { "content": { @@ -17992,8 +26676,8 @@ "schema": { "example": { "code": 403, - "label": "forbidden", - "message": "OAuth is disabled" + "label": "insufficient-permissions", + "message": "Insufficient permissions" }, "properties": { "code": { @@ -18004,7 +26688,7 @@ }, "label": { "enum": [ - "forbidden" + "insufficient-permissions" ], "type": "string" }, @@ -18021,27 +26705,64 @@ } } }, - "description": "OAuth is disabled (label: `forbidden`)" + "description": "Insufficient permissions (label: `insufficient-permissions`)" + } + }, + "summary": "Search for users" + } + }, + "/self": { + "delete": { + "description": " [internal route ID: \"delete-self\"]\n\nif the account has a verified identity, a verification code is sent and needs to be confirmed to authorise the deletion. if the account has no verified identity but a password, it must be provided. if password is correct, or if neither a verified identity nor a password exists, account deletion is scheduled immediately.", + "operationId": "delete-self", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteUser_NjE0MjE2Mjkz" + } + } }, - "404": { + "required": true + }, + "responses": { + "200": { + "description": "Deletion is initiated." + }, + "202": { "content": { "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletionCodeTimeout_LTU1MTk0NDI3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeletionCodeTimeout_LTU1MTk0NDI3" + } + } + }, + "description": "Deletion is pending verification with a code." + }, + "400": { + "content": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "not-found", - "message": "OAuth client not found" + "code": 400, + "label": "invalid-user", + "message": "Invalid user" }, "properties": { "code": { "enum": [ - 404 + 400 ], "type": "integer" }, "label": { "enum": [ - "not-found" + "invalid-user" ], "type": "string" }, @@ -18056,24 +26777,33 @@ ], "type": "object" } - }, + } + }, + "description": "Invalid `body`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "not-found", - "message": "OAuth client not found" + "code": 403, + "label": "no-self-delete-for-team-owner", + "message": "Team owners are not allowed to delete themselves; ask a fellow owner" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "not-found" + "no-self-delete-for-team-owner", + "pending-delete", + "missing-auth", + "invalid-credentials", + "invalid-code" ], "type": "string" }, @@ -18090,20 +26820,36 @@ } } }, - "description": "`OAuthClientId` or OAuth client not found (label: `not-found`)\n\nOAuth client not found (label: `not-found`)" + "description": "Team owners are not allowed to delete themselves; ask a fellow owner (label: `no-self-delete-for-team-owner`)\n\nA verification code for account deletion is still pending (label: `pending-delete`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nAuthentication failed (label: `invalid-credentials`)\n\nInvalid verification code (label: `invalid-code`)" } }, - "summary": "Get OAuth client information" - } - }, - "/oauth/revoke": { - "post": { - "description": " [internal route ID: \"revoke-oauth-refresh-token\"]\n\nRevoke an access token.", + "summary": "Initiate account deletion." + }, + "get": { + "description": " [internal route ID: \"get-self\"]\n\n\nOAuth scope: `read:self`", + "operationId": "get-self", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/User_NjA4OTQwMTQ4" + } + } + }, + "description": "" + } + }, + "summary": "Get your own profile" + }, + "put": { + "description": " [internal route ID: \"put-self\"]\n\n", + "operationId": "put-self", "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/OAuthRevokeRefreshTokenRequest" + "$ref": "#/components/schemas/UserUpdate_MjQ4NTEwOTQz" } } }, @@ -18111,26 +26857,28 @@ }, "responses": { "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": [], - "items": {}, - "maxItems": 0, - "type": "array" - } - } - }, - "description": "" + "description": "User updated" + } + }, + "summary": "Update your profile." + } + }, + "/self/email": { + "delete": { + "description": " [internal route ID: \"remove-email\"]\n\nYour email address can only be removed if you also have a phone number.", + "operationId": "remove-email", + "responses": { + "200": { + "description": "Identity Removed" }, "403": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { "example": { "code": 403, - "label": "forbidden", - "message": "Invalid refresh token" + "label": "last-identity", + "message": "The last user identity cannot be removed." }, "properties": { "code": { @@ -18141,7 +26889,8 @@ }, "label": { "enum": [ - "forbidden" + "last-identity", + "no-identity" ], "type": "string" }, @@ -18156,29 +26905,25 @@ ], "type": "object" } - } - }, - "description": "Invalid refresh token (label: `forbidden`)" - }, - "404": { - "content": { + }, "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "not-found", - "message": "OAuth client not found" + "code": 403, + "label": "last-identity", + "message": "The last user identity cannot be removed." }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "not-found" + "last-identity", + "no-identity" ], "type": "string" }, @@ -18195,57 +26940,43 @@ } } }, - "description": "OAuth client not found (label: `not-found`)" - }, - "500": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 500, - "label": "jwt-error", - "message": "Internal error while handling JWT token" - }, - "properties": { - "code": { - "enum": [ - 500 - ], - "type": "integer" - }, - "label": { - "enum": [ - "jwt-error" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } + "description": "The last user identity cannot be removed. (label: `last-identity`)\n\nThe user has no verified email (label: `no-identity`)" + } + }, + "summary": "Remove your email address." + } + }, + "/self/handle": { + "put": { + "description": " [internal route ID: \"change-handle\"]\n\n", + "operationId": "change-handle", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/HandleUpdate_NTI4NDk1OTAx" } - }, - "description": "Internal error while handling JWT token (label: `jwt-error`)" + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Handle Changed" } }, - "summary": "Revoke an OAuth refresh token" + "summary": "Change your handle." } }, - "/oauth/token": { - "post": { - "description": " [internal route ID: \"create-oauth-access-token\"]\n\nObtain a new access token from an authorization code or a refresh token.", + "/self/locale": { + "put": { + "description": " [internal route ID: \"change-locale\"]\n\n", + "operationId": "change-locale", "requestBody": { "content": { - "application/x-www-form-urlencoded": { + "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Either_OAuthAccessTokenRequest_OAuthRefreshAccessTokenRequest" + "$ref": "#/components/schemas/LocaleUpdate_LTgzNjgyOTEw" } } }, @@ -18253,23 +26984,51 @@ }, "responses": { "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/OAuthAccessTokenResponse" - } + "description": "Local Changed" + } + }, + "summary": "Change your locale." + } + }, + "/self/password": { + "head": { + "description": " [internal route ID: \"check-password-exists\"]\n\n", + "operationId": "check-password-exists", + "responses": { + "200": { + "description": "Password is set" + }, + "404": { + "description": "Password is not set" + } + }, + "summary": "Check that your password is set." + }, + "put": { + "description": " [internal route ID: \"change-password\"]\n\n", + "operationId": "change-password", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordChange_MTgzMDM2NTY2" } - }, - "description": "" + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Password Changed" }, "403": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { "example": { "code": 403, - "label": "invalid_grant", - "message": "Invalid grant" + "label": "invalid-credentials", + "message": "Authentication failed" }, "properties": { "code": { @@ -18280,8 +27039,8 @@ }, "label": { "enum": [ - "invalid_grant", - "forbidden" + "invalid-credentials", + "no-identity" ], "type": "string" }, @@ -18296,29 +27055,25 @@ ], "type": "object" } - } - }, - "description": "Invalid grant (label: `invalid_grant`)\n\nInvalid client credentials (label: `forbidden`)\n\nInvalid grant type (label: `forbidden`)\n\nInvalid refresh token (label: `forbidden`)\n\nOAuth is disabled (label: `forbidden`)" - }, - "404": { - "content": { + }, "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "not-found", - "message": "OAuth client not found" + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "not-found" + "invalid-credentials", + "no-identity" ], "type": "string" }, @@ -18335,27 +27090,27 @@ } } }, - "description": "OAuth client not found (label: `not-found`)\n\nOAuth authorization code not found (label: `not-found`)" + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe user has no verified email (label: `no-identity`)" }, - "500": { + "409": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { "example": { - "code": 500, - "label": "jwt-error", - "message": "Internal error while handling JWT token" + "code": 409, + "label": "password-must-differ", + "message": "For password change, new and old password must be different." }, "properties": { "code": { "enum": [ - 500 + 409 ], "type": "integer" }, "label": { "enum": [ - "jwt-error" + "password-must-differ" ], "type": "string" }, @@ -18370,72 +27125,55 @@ ], "type": "object" } - } - }, - "description": "Internal error while handling JWT token (label: `jwt-error`)" - } - }, - "summary": "Create an OAuth access token" - } - }, - "/onboarding/v3": { - "post": { - "deprecated": true, - "description": " [internal route ID: \"onboarding\"]\n\nDEPRECATED: the feature has been turned off, the end-point does nothing and always returns '{\"results\":[],\"auto-connects\":[]}'.", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/Body" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { + }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/DeprecatedMatchingResult" + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password change, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "" - } - }, - "summary": "Upload contacts and invoke matching." - } - }, - "/password-reset": { - "post": { - "description": " [internal route ID: \"post-password-reset\"]\n\n", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/NewPasswordReset" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Password reset code created and sent by email." + "description": "For password change, new and old password must be different. (label: `password-must-differ`)" } }, - "summary": "Initiate a password reset." + "summary": "Change your password." } }, - "/password-reset/complete": { - "post": { - "description": " [internal route ID: \"post-password-reset-complete\"]\n\n", + "/self/supported-protocols": { + "put": { + "description": " [internal route ID: \"change-supported-protocols\"]\n\n", + "operationId": "change-supported-protocols", "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/CompletePasswordReset" + "$ref": "#/components/schemas/SupportedProtocolUpdate_LTE3Njk3MDM4" } } }, @@ -18443,27 +27181,27 @@ }, "responses": { "200": { - "description": "Password reset successful." + "description": "Supported protocols changed" }, - "400": { + "409": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-code", - "message": "Invalid password reset code." + "code": 409, + "label": "mls-protocol-error", + "message": "MLS protocol cannot be removed" }, "properties": { "code": { "enum": [ - 400 + 409 ], "type": "integer" }, "label": { "enum": [ - "invalid-code" + "mls-protocol-error" ], "type": "string" }, @@ -18480,61 +27218,108 @@ } } }, - "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)" + "description": "MLS protocol cannot be removed (label: `mls-protocol-error`)" } }, - "summary": "Complete a password reset." + "summary": "Change your supported protocols" } }, - "/password-reset/{key}": { - "post": { - "deprecated": true, - "description": " [internal route ID: \"post-password-reset-key-deprecated\"]\n\nDEPRECATED: Use 'POST /password-reset/complete'.", + "/services": { + "get": { + "description": " [internal route ID: \"get-services\"]\n\n", + "operationId": "get-services", "parameters": [ { - "description": "An opaque key for a pending password reset.", - "in": "path", - "name": "key", - "required": true, + "in": "query", + "name": "tags", + "required": false, "schema": { + "enum": [ + "audio", + "books", + "business", + "design", + "education", + "entertainment", + "finance", + "fitness", + "food-drink", + "games", + "graphics", + "health", + "integration", + "lifestyle", + "media", + "medical", + "movies", + "music", + "news", + "photography", + "poll", + "productivity", + "quiz", + "rating", + "shopping", + "social", + "sports", + "travel", + "tutorial", + "video", + "weather" + ], "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/PasswordReset" - } + }, + { + "in": "query", + "name": "start", + "required": false, + "schema": { + "type": "string" } }, - "required": true - }, + { + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 100, + "minimum": 10, + "type": "integer" + } + } + ], "responses": { "200": { - "description": "Password reset successful." + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceProfilePage_Njg1NDQ5Njc4" + } + } + }, + "description": "" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-code", - "message": "Invalid password reset code." + "code": 403, + "label": "access-denied", + "message": "Access denied." }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "invalid-code", - "invalid-key" + "access-denied" ], "type": "string" }, @@ -18551,28 +27336,46 @@ } } }, - "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)\n\nInvalid email or mobile number for password reset. (label: `invalid-key`)" + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List services" + } + }, + "/services/tags": { + "get": { + "description": " [internal route ID: \"get-services-tags\"]\n\n", + "operationId": "get-services-tags", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceTagList" + } + } + }, + "description": "" }, - "409": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "password-must-differ", - "message": "For password reset, new and old password must be different." + "code": 403, + "label": "access-denied", + "message": "Access denied." }, "properties": { "code": { "enum": [ - 409 + 403 ], "type": "integer" }, "label": { "enum": [ - "password-must-differ", - "code-exists" + "access-denied" ], "type": "string" }, @@ -18589,98 +27392,194 @@ } } }, - "description": "For password reset, new and old password must be different. (label: `password-must-differ`)\n\nA password reset is already in progress. (label: `code-exists`)" + "description": "Access denied. (label: `access-denied`)" } }, - "summary": "Complete a password reset." + "summary": "Get services tags" } }, - "/properties": { - "delete": { - "description": " [internal route ID: \"clear-properties\"]\n\n", + "/sso/finalize-login": { + "post": { + "deprecated": true, + "description": " [internal route ID: \"auth-resp-legacy\"]\n\nDEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "operationId": "auth-resp-legacy", "responses": { "200": { - "description": "Properties cleared" + "content": { + "text/plain;charset=utf-8": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/finalize-login/{team}": { + "post": { + "description": " [internal route ID: \"auth-resp\"]\n\n", + "operationId": "auth-resp", + "parameters": [ + { + "in": "path", + "name": "team", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": { + "schema": { + "type": "string" + } + } + }, + "description": "" } + } + } + }, + "/sso/get-by-email": { + "post": { + "description": " [internal route ID: \"sso-get-by-email\"]\n\n", + "operationId": "sso-get-by-email", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetByEmailReq_LTY4MzE3Njgy" + } + } + }, + "required": true }, - "summary": "Clear all properties" - }, - "get": { - "description": " [internal route ID: \"list-property-keys\"]\n\n", "responses": { "200": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/ASCII" - }, - "type": "array" + "$ref": "#/components/schemas/GetByEmailResp_LTMxNTY3MjA0" } }, "application/json;charset=utf-8": { "schema": { - "items": { - "$ref": "#/components/schemas/ASCII" - }, - "type": "array" + "$ref": "#/components/schemas/GetByEmailResp_LTMxNTY3MjA0" } } }, - "description": "List of property keys" - } - }, - "summary": "List all property keys" - } - }, - "/properties-values": { - "get": { - "description": " [internal route ID: \"list-properties\"]\n\n", - "responses": { - "200": { + "description": "SSO code found" + }, + "404": { "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetByEmailResp_LTMxNTY3MjA0" + } + }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/PropertyKeysAndValues" + "$ref": "#/components/schemas/GetByEmailResp_LTMxNTY3MjA0" } } }, - "description": "" + "description": "SSO code not found or feature disabled" } - }, - "summary": "List all properties with key and value" + } } }, - "/properties/{key}": { - "delete": { - "description": " [internal route ID: \"delete-property\"]\n\n", + "/sso/initiate-login/{idp}": { + "get": { + "description": " [internal route ID: \"auth-req\"]\n\n", + "operationId": "auth-req", "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "label", + "required": false, + "schema": { + "type": "string" + } + }, { "in": "path", - "name": "key", + "name": "idp", "required": true, "schema": { - "format": "printable", + "format": "uuid", "type": "string" } } ], "responses": { "200": { - "description": "Property deleted" + "content": { + "text/html": { + "schema": { + "$ref": "#/components/schemas/FormRedirect" + } + } + }, + "description": "" } - }, - "summary": "Delete a property" + } }, - "get": { - "description": " [internal route ID: \"get-property\"]\n\n", + "head": { + "description": " [internal route ID: \"auth-req-precheck\"]\n\n", + "operationId": "auth-req-precheck", "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "label", + "required": false, + "schema": { + "type": "string" + } + }, { "in": "path", - "name": "key", + "name": "idp", "required": true, "schema": { - "format": "printable", + "format": "uuid", "type": "string" } } @@ -18688,64 +27587,126 @@ "responses": { "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PropertyValue" - } - }, - "application/json;charset=utf-8": { + "text/plain;charset=utf-8": {} + }, + "description": "" + } + } + } + }, + "/sso/metadata": { + "get": { + "deprecated": true, + "description": " [internal route ID: \"sso-metadata\"]\n\nDEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "operationId": "sso-metadata", + "responses": { + "200": { + "content": { + "application/xml": { "schema": { - "$ref": "#/components/schemas/PropertyValue" + "type": "string" } } }, - "description": "The property value" - }, - "404": { - "description": "`key` or Property not found(**Note**: This error has an empty body for legacy reasons)" + "description": "" } - }, - "summary": "Get a property value" - }, - "put": { - "description": " [internal route ID: \"set-property\"]\n\n", + } + } + }, + "/sso/metadata/{team}": { + "get": { + "description": " [internal route ID: \"sso-team-metadata\"]\n\n", + "operationId": "sso-team-metadata", "parameters": [ { "in": "path", - "name": "key", + "name": "team", "required": true, "schema": { - "format": "printable", + "format": "uuid", "type": "string" } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/PropertyValue" + "responses": { + "200": { + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/settings": { + "get": { + "description": " [internal route ID: \"sso-settings\"]\n\n", + "operationId": "sso-settings", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SsoSettings" + } + } + }, + "description": "" + } + } + } + }, + "/system/settings": { + "get": { + "description": " [internal route ID: \"get-system-settings\"]\n\n", + "operationId": "get-system-settings", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SystemSettings_ODU3MDk5MTA3" + } } - } - }, - "required": true + }, + "description": "" + } }, + "summary": "Returns a curated set of system configuration settings for authorized users." + } + }, + "/system/settings/unauthorized": { + "get": { + "description": " [internal route ID: \"get-system-settings-unauthorized\"]\n\n", + "operationId": "get-system-settings-unauthorized", "responses": { "200": { - "description": "Property set" + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SystemSettingsPublic_LTgwNTMxNjU2" + } + } + }, + "description": "" } }, - "summary": "Set a user property" + "summary": "Returns a curated set of system configuration settings." } }, - "/provider": { - "delete": { - "description": " [internal route ID: \"provider-delete\"]\n\n", + "/teams/invitations/accept": { + "post": { + "description": " [internal route ID: \"accept-team-invitation\"]\n\n", + "operationId": "accept-team-invitation", "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/DeleteProvider" + "$ref": "#/components/schemas/AcceptTeamInvitation_Nzg5NzI3MjA2" } } }, @@ -18753,7 +27714,7 @@ }, "responses": { "200": { - "description": "" + "description": "Team invitation accepted." }, "403": { "content": { @@ -18761,8 +27722,8 @@ "schema": { "example": { "code": 403, - "label": "invalid-credentials", - "message": "Authentication failed" + "label": "missing-auth", + "message": "Re-authentication via password required" }, "properties": { "code": { @@ -18773,9 +27734,10 @@ }, "label": { "enum": [ + "missing-auth", "invalid-credentials", - "invalid-provider", - "access-denied" + "missing-identity", + "too-many-team-members" ], "type": "string" }, @@ -18792,48 +27754,28 @@ } } }, - "description": "Authentication failed (label: `invalid-credentials`)\n\nThe provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" - } - }, - "summary": "Delete a provider" - }, - "get": { - "description": " [internal route ID: \"provider-get-account\"]\n\n", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Provider" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/Provider" - } - } - }, - "description": "" + "description": "Re-authentication via password required (label: `missing-auth`)\n\nAuthentication failed (label: `invalid-credentials`)\n\nUsing an invitation code requires registering the given email. (label: `missing-identity`)\n\nToo many members in this team. (label: `too-many-team-members`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "access-denied", - "message": "Access denied." + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "access-denied" + "invalid-code", + "not-found" ], "type": "string" }, @@ -18850,7 +27792,30 @@ } } }, - "description": "Access denied. (label: `access-denied`)" + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)\n\nNo pending invitations exists. (label: `not-found`)" + } + }, + "summary": "Accept a team invitation, changing a personal account into a team member account." + } + }, + "/teams/invitations/by-email": { + "head": { + "description": " [internal route ID: \"head-team-invitations\"]\n\n", + "operationId": "head-team-invitations", + "parameters": [ + { + "description": "Email address", + "in": "query", + "name": "email", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Pending invitation exists." }, "404": { "content": { @@ -18859,7 +27824,7 @@ "example": { "code": 404, "label": "not-found", - "message": "Provider not found." + "message": "No pending invitations exists." }, "properties": { "code": { @@ -18891,7 +27856,7 @@ "example": { "code": 404, "label": "not-found", - "message": "Provider not found." + "message": "No pending invitations exists." }, "properties": { "code": { @@ -18919,47 +27884,59 @@ } } }, - "description": "Provider not found. (label: `not-found`)\n\nProvider not found. (label: `not-found`)" - } - }, - "summary": "Get account" - }, - "put": { - "description": " [internal route ID: \"provider-update\"]\n\n", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/UpdateProvider" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "" + "description": "No pending invitations exists. (label: `not-found`)" }, - "403": { + "409": { "content": { + "application/json": { + "schema": { + "example": { + "code": 409, + "label": "conflicting-invitations", + "message": "Multiple conflicting invitations to different teams exists." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "conflicting-invitations" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "invalid-provider", - "message": "The provider does not exist." + "code": 409, + "label": "conflicting-invitations", + "message": "Multiple conflicting invitations to different teams exists." }, "properties": { "code": { "enum": [ - 403 + 409 ], "type": "integer" }, "label": { "enum": [ - "invalid-provider", - "access-denied" + "conflicting-invitations" ], "type": "string" }, @@ -18976,25 +27953,19 @@ } } }, - "description": "The provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" + "description": "Multiple conflicting invitations to different teams exists. (label: `conflicting-invitations`)" } }, - "summary": "Update a provider" + "summary": "Check if there is an invitation pending given an email address." } }, - "/provider/activate": { + "/teams/invitations/info": { "get": { - "description": " [internal route ID: \"provider-activate\"]\n\n", + "description": " [internal route ID: \"get-team-invitation-info\"]\n\n", + "operationId": "get-team-invitation-info", "parameters": [ { - "in": "query", - "name": "key", - "required": true, - "schema": { - "type": "string" - } - }, - { + "description": "Invitation code", "in": "query", "name": "code", "required": true, @@ -19008,40 +27979,36 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProviderActivationResponse" + "$ref": "#/components/schemas/InvitationUserView_LTUyMTE3Nzkz" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ProviderActivationResponse" + "$ref": "#/components/schemas/InvitationUserView_LTUyMTE3Nzkz" } } }, - "description": "" - }, - "204": { - "description": "" + "description": "Invitation info" }, - "403": { + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "invalid-code", - "message": "Invalid verification code" + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." }, "properties": { "code": { "enum": [ - 403 + 400 ], "type": "integer" }, "label": { "enum": [ - "invalid-code", - "access-denied" + "invalid-invitation-code" ], "type": "string" }, @@ -19058,48 +28025,50 @@ } } }, - "description": "Invalid verification code (label: `invalid-code`)\n\nAccess denied. (label: `access-denied`)" + "description": "Invalid `code`\n\nInvalid invitation code. (label: `invalid-invitation-code`)" } }, - "summary": "Activate a provider" + "summary": "Get invitation info given a code." } }, - "/provider/assets": { - "post": { - "requestBody": { - "content": { - "multipart/mixed": { - "schema": { - "$ref": "#/components/schemas/AssetSource" - } + "/teams/notifications": { + "get": { + "description": " [internal route ID: \"get-team-notifications\"]\n\n

This is a work-around for scalability issues with gundeck user event fan-out. It does not track all team-wide events, but only `member-join`.

Note that `/teams/notifications` behaves differently from `/notifications`:

  • If there is a gap between the notification id requested with `since` and the available data, team queues respond with 200 and the data that could be found. They do NOT respond with status 404, but valid data in the body.\n
  • The notification with the id given via `since` is included in the response if it exists. You should remove this and only use it to decide whether there was a gap between your last request and this one.\n
  • If the notification id does *not* exist, you get the more recent events from the queue (instead of all of them). This can be done because a notification id is a UUIDv1, which is essentially a time stamp.\n
  • There is no corresponding `/last` end-point to get only the most recent event. That end-point was only useful to avoid having to pull the entire queue. In team queues, if you have never requested the queue before and have no prior notification id, just pull with timestamp 'now'.

See also: GET /notifications

", + "operationId": "get-team-notifications", + "parameters": [ + { + "description": "Notification id to start with in the response (UUIDv1)", + "in": "query", + "name": "since", + "required": false, + "schema": { + "format": "uuid", + "type": "string" } }, - "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server." - }, + { + "description": "Maximum number of events to return (1..10000; default: 1000)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 10000, + "minimum": 1, + "type": "integer" + } + } + ], "responses": { - "201": { + "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Asset" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Asset" + "$ref": "#/components/schemas/QueuedNotificationList_MTU0ODEyNTQ2" } } }, - "description": "Asset posted", - "headers": { - "Location": { - "description": "Asset location", - "schema": { - "format": "url", - "type": "string" - } - } - } + "description": "" }, "400": { "content": { @@ -19107,8 +28076,8 @@ "schema": { "example": { "code": 400, - "label": "invalid-length", - "message": "Invalid content length" + "label": "invalid-notification-id", + "message": "Could not parse notification id (must be UUIDv1)." }, "properties": { "code": { @@ -19119,7 +28088,7 @@ }, "label": { "enum": [ - "invalid-length" + "invalid-notification-id" ], "type": "string" }, @@ -19136,27 +28105,27 @@ } } }, - "description": "Invalid `body`\n\nInvalid content length (label: `invalid-length`)" + "description": "Invalid `size` or `since`\n\nCould not parse notification id (must be UUIDv1). (label: `invalid-notification-id`)" }, - "413": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 413, - "label": "client-error", - "message": "Asset too large" + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 413 + 404 ], "type": "integer" }, "label": { "enum": [ - "client-error" + "no-team" ], "type": "string" }, @@ -19173,84 +28142,63 @@ } } }, - "description": "Asset too large (label: `client-error`)" + "description": "Team not found (label: `no-team`)" } }, - "summary": "Upload an asset" + "summary": "Read recently added team members from team queue" } }, - "/provider/assets/{key}": { - "delete": { + "/teams/{team-id}/services/whitelist": { + "post": { + "description": " [internal route ID: \"post-team-whitelist-by-team-id\"]\n\n", + "operationId": "post-team-whitelist-by-team-id", "parameters": [ { "in": "path", - "name": "key", + "name": "team-id", "required": true, "schema": { + "format": "uuid", "type": "string" } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateServiceWhitelist_LTU5MDAwMTIw" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Asset deleted" + "description": "UpdateServiceWhitelistRespChanged" }, - "403": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 403, - "label": "unauthorised", - "message": "Unauthorised operation" - }, - "properties": { - "code": { - "enum": [ - 403 - ], - "type": "integer" - }, - "label": { - "enum": [ - "unauthorised" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - } - }, - "description": "Unauthorised operation (label: `unauthorised`)" + "204": { + "description": "UpdateServiceWhitelistRespUnchanged" }, - "404": { + "409": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "not-found", - "message": "Asset not found" + "code": 409, + "label": "mls-services-not-allowed", + "message": "Services not allowed in MLS" }, "properties": { "code": { "enum": [ - 404 + 409 ], "type": "integer" }, "label": { "enum": [ - "not-found" + "mls-services-not-allowed" ], "type": "string" }, @@ -19267,70 +28215,147 @@ } } }, - "description": "`key` not found\n\nAsset not found (label: `not-found`)" + "description": "Services not allowed in MLS (label: `mls-services-not-allowed`)" } }, - "summary": "Delete an asset" - }, + "summary": "Update service whitelist" + } + }, + "/teams/{team-id}/services/whitelisted": { "get": { + "description": " [internal route ID: \"get-whitelisted-services-by-team-id\"]\n\n", + "operationId": "get-whitelisted-services-by-team-id", "parameters": [ { "in": "path", - "name": "key", + "name": "team-id", "required": true, "schema": { + "format": "uuid", "type": "string" } }, { - "in": "header", - "name": "Asset-Token", + "in": "query", + "name": "prefix", "required": false, "schema": { + "maxLength": 128, + "minLength": 1, "type": "string" } }, { "in": "query", - "name": "asset_token", + "name": "filter_disabled", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "size", "required": false, "schema": { + "format": "int32", + "maximum": 100, + "minimum": 10, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceProfilePage_Njg1NDQ5Njc4" + } + } + }, + "description": "" + } + }, + "summary": "Get whitelisted services by team id" + } + }, + "/teams/{teamId}/registered-domains": { + "get": { + "description": " [internal route ID: \"get-all-registered-domains\"]\n\n", + "operationId": "get-all-registered-domains", + "parameters": [ + { + "in": "path", + "name": "teamId", + "required": true, + "schema": { + "format": "uuid", "type": "string" } } ], "responses": { - "302": { - "description": "Asset found", - "headers": { - "Location": { - "description": "Asset location", + "200": { + "content": { + "application/json;charset=utf-8": { "schema": { - "format": "url", - "type": "string" + "$ref": "#/components/schemas/RegisteredDomains_V10_NDYwNzYyMTMy" } } + }, + "description": "" + } + }, + "summary": "Get all registered domains" + } + }, + "/teams/{teamId}/registered-domains/{domain}": { + "delete": { + "description": " [internal route ID: \"delete-registered-domain\"]\n\n", + "operationId": "delete-registered-domain", + "parameters": [ + { + "in": "path", + "name": "teamId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" } }, - "404": { + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "402": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "not-found", - "message": "Asset not found" + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" }, "properties": { "code": { "enum": [ - 404 + 402 ], "type": "integer" }, "label": { "enum": [ - "not-found" + "domain-registration-update-payment-required" ], "type": "string" }, @@ -19345,24 +28370,29 @@ ], "type": "object" } - }, + } + }, + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" + }, + "403": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "not-found", - "message": "Asset not found" + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "not-found" + "operation-forbidden-for-domain-registration-state" ], "type": "string" }, @@ -19379,20 +28409,32 @@ } } }, - "description": "`key` or Asset not found (label: `not-found`)" + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" } }, - "summary": "Download an asset" + "summary": "Delete a registered domain" } }, - "/provider/email": { - "put": { - "description": " [internal route ID: \"provider-update-email\"]\n\n", + "/teams/{tid}": { + "delete": { + "description": " [internal route ID: \"delete-team\"]\n\n", + "operationId": "delete-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/EmailUpdate" + "$ref": "#/components/schemas/TeamDeleteData_ODI5NTU0ODE5" } } }, @@ -19400,27 +28442,68 @@ }, "responses": { "202": { - "description": "" + "description": "Team is scheduled for removal" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-email", - "message": "Invalid e-mail address." + "code": 403, + "label": "code-authentication-required", + "message": "Verification code required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "access-denied", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Verification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (missing DeleteTeam) (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 400 + 404 ], "type": "integer" }, "label": { "enum": [ - "invalid-email" + "no-team" ], "type": "string" }, @@ -19437,28 +28520,27 @@ } } }, - "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" }, - "403": { + "429": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "invalid-provider", - "message": "The provider does not exist." + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." }, "properties": { "code": { "enum": [ - 403 + 429 ], "type": "integer" }, "label": { "enum": [ - "invalid-provider", - "access-denied" + "too-many-requests" ], "type": "string" }, @@ -19475,27 +28557,27 @@ } } }, - "description": "The provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" + "description": "Please try again later. (label: `too-many-requests`)" }, - "429": { + "503": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 429, - "label": "too-many-requests", - "message": "Too many request to generate a verification code." + "code": 503, + "label": "queue-full", + "message": "The delete queue is full; no further delete requests can be processed at the moment" }, "properties": { "code": { "enum": [ - 429 + 503 ], "type": "integer" }, "label": { "enum": [ - "too-many-requests" + "queue-full" ], "type": "string" }, @@ -19512,56 +28594,55 @@ } } }, - "description": "Too many request to generate a verification code. (label: `too-many-requests`)" + "description": "The delete queue is full; no further delete requests can be processed at the moment (label: `queue-full`)" } }, - "summary": "Update a provider email" - } - }, - "/provider/login": { - "post": { - "description": " [internal route ID: \"provider-login\"]\n\n", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ProviderLogin" - } + "summary": "Delete a team" + }, + "get": { + "description": " [internal route ID: \"get-team\"]\n\n", + "operationId": "get-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "200": { - "description": "OK", - "headers": { - "Set-Cookie": { + "content": { + "application/json;charset=utf-8": { "schema": { - "type": "string" + "$ref": "#/components/schemas/Team_NDg4MjQwOTIw" } } - } + }, + "description": "" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "invalid-credentials", - "message": "Authentication failed" + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "invalid-credentials", - "access-denied" + "no-team" ], "type": "string" }, @@ -19578,20 +28659,30 @@ } } }, - "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Login as a provider" - } - }, - "/provider/password": { + "summary": "Get a team by ID" + }, "put": { - "description": " [internal route ID: \"provider-update-password\"]\n\n", + "description": " [internal route ID: \"update-team\"]\n\n", + "operationId": "update-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/PasswordChange" + "$ref": "#/components/schemas/TeamUpdateData_LTE0NTM2NTU5" } } }, @@ -19599,7 +28690,7 @@ }, "responses": { "200": { - "description": "" + "description": "Team updated" }, "403": { "content": { @@ -19607,8 +28698,8 @@ "schema": { "example": { "code": 403, - "label": "invalid-credentials", - "message": "Authentication failed" + "label": "operation-denied", + "message": "Insufficient permissions (missing SetTeamData)" }, "properties": { "code": { @@ -19619,8 +28710,8 @@ }, "label": { "enum": [ - "invalid-credentials", - "access-denied" + "operation-denied", + "no-team-member" ], "type": "string" }, @@ -19637,103 +28728,179 @@ } } }, - "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" - }, - "409": { + "description": "Insufficient permissions (missing SetTeamData) (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Update team properties" + } + }, + "/teams/{tid}/apps": { + "get": { + "description": " [internal route ID: \"get-apps\"]\n\n", + "operationId": "get-apps", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { "content": { "application/json;charset=utf-8": { "schema": { - "example": { - "code": 409, - "label": "password-must-differ", - "message": "For password reset, new and old password must be different." - }, - "properties": { - "code": { - "enum": [ - 409 - ], - "type": "integer" - }, - "label": { - "enum": [ - "password-must-differ" - ], - "type": "string" - }, - "message": { - "type": "string" - } + "items": { + "$ref": "#/components/schemas/UserProfile_LTQzMTQxMTE1" }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" + "type": "array" } } }, - "description": "For password reset, new and old password must be different. (label: `password-must-differ`)" + "description": "" } }, - "summary": "Update a provider password" + "summary": "Get all apps owned by the given team (not including collaborators)" + }, + "post": { + "description": " [internal route ID: \"create-app\"]\n\n", + "operationId": "create-app", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewApp_LTQwODMwMzQ4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreatedApp_LTM3NjUxOTY1" + } + } + }, + "description": "" + } + }, + "summary": "Create a new app" } }, - "/provider/password-reset": { - "post": { - "description": " [internal route ID: \"provider-password-reset\"]\n\n", + "/teams/{tid}/apps/{app}": { + "put": { + "description": " [internal route ID: \"put-app\"]\n\n", + "operationId": "put-app", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "app", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/PasswordReset" + "$ref": "#/components/schemas/PutApp_LTE4MDc1OTM4" } } }, "required": true }, "responses": { - "201": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, "description": "" + } + }, + "summary": "Update metadata of an existing app" + } + }, + "/teams/{tid}/apps/{app}/cookies": { + "post": { + "description": " [internal route ID: \"refresh-app-cookie\"]\n\n", + "operationId": "refresh-app-cookie", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } }, - "400": { + { + "in": "path", + "name": "app", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RefreshAppCookieRequest_MjEyMDMyMTk5" + } + } + }, + "required": true + }, + "responses": { + "200": { "content": { "application/json;charset=utf-8": { "schema": { - "example": { - "code": 400, - "label": "invalid-code", - "message": "Invalid password reset code." - }, - "properties": { - "code": { - "enum": [ - 400 - ], - "type": "integer" - }, - "label": { - "enum": [ - "invalid-code", - "invalid-key" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" + "$ref": "#/components/schemas/RefreshAppCookieResponse_LTQ0MjU1NTIw" } } }, - "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)\n\nInvalid email or mobile number for password reset. (label: `invalid-key`)" + "description": "" }, "403": { "content": { @@ -19741,8 +28908,8 @@ "schema": { "example": { "code": 403, - "label": "invalid-credentials", - "message": "Authentication failed" + "label": "missing-auth", + "message": "Re-authentication via password required" }, "properties": { "code": { @@ -19753,8 +28920,7 @@ }, "label": { "enum": [ - "invalid-credentials", - "access-denied" + "missing-auth" ], "type": "string" }, @@ -19771,149 +28937,208 @@ } } }, - "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + "description": "Re-authentication via password required (label: `missing-auth`)" + } + }, + "summary": "Get a new app authentication token" + } + }, + "/teams/{tid}/channels/search": { + "get": { + "description": " [internal route ID: \"search-channels\"]\n\n", + "operationId": "search-channels", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } }, - "409": { + { + "description": "Search string", + "in": "query", + "name": "q", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "sort_order", + "required": false, + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "page_size", + "required": false, + "schema": { + "description": "integer from [1..500]", + "type": "number" + } + }, + { + "description": "`name` of the last seen channel of the current page, used to get the next page.", + "in": "query", + "name": "last_seen_name", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "`id` of the last seen channel, used to get the next page, used as a tie breaker. **Must** be sent to get the next page.", + "in": "query", + "name": "last_seen_id", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "discoverable", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "200": { "content": { "application/json;charset=utf-8": { "schema": { - "example": { - "code": 409, - "label": "password-must-differ", - "message": "For password reset, new and old password must be different." - }, - "properties": { - "code": { - "enum": [ - 409 - ], - "type": "integer" - }, - "label": { - "enum": [ - "password-must-differ", - "code-exists" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" + "$ref": "#/components/schemas/ConversationPage_LTIwMDU2NDI3" } } }, - "description": "For password reset, new and old password must be different. (label: `password-must-differ`)\n\nA password reset is already in progress. (label: `code-exists`)\n\nA password reset is already in progress. (label: `code-exists`)" - }, - "429": { + "description": "" + } + }, + "summary": "Search channels" + } + }, + "/teams/{tid}/collaborators": { + "get": { + "description": " [internal route ID: \"get-team-collaborators\"]\n\n", + "operationId": "get-team-collaborators", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { - "example": { - "code": 429, - "label": "too-many-requests", - "message": "Too many request to generate a verification code." + "items": { + "$ref": "#/components/schemas/TeamCollaborator_LTI3MzM1MTYz" }, - "properties": { - "code": { - "enum": [ - 429 - ], - "type": "integer" - }, - "label": { - "enum": [ - "too-many-requests" - ], - "type": "string" - }, - "message": { - "type": "string" - } + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/TeamCollaborator_LTI3MzM1MTYz" }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" + "type": "array" } } }, - "description": "Too many request to generate a verification code. (label: `too-many-requests`)" + "description": "Return collaborators" } }, - "summary": "Begin a password reset" - } - }, - "/provider/password-reset/complete": { + "summary": "Get all collaborators of the team." + }, "post": { - "description": " [internal route ID: \"provider-password-reset-complete\"]\n\n", + "description": " [internal route ID: \"add-team-collaborator\"]\n\n", + "operationId": "add-team-collaborator", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/CompletePasswordReset" + "$ref": "#/components/schemas/NewTeamCollaborator_LTIxNjEzMTYw" } } - }, - "required": true - }, + }, + "required": true + }, + "responses": { + "200": { + "description": "" + } + }, + "summary": "Add a collaborator to the team." + } + }, + "/teams/{tid}/collaborators/{uid}": { + "delete": { + "description": " [internal route ID: \"remove-team-collaborator\"]\n\n", + "operationId": "remove-team-collaborator", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "responses": { "200": { "description": "" }, - "400": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 400, - "label": "invalid-code", - "message": "Invalid password reset code." - }, - "properties": { - "code": { - "enum": [ - 400 - ], - "type": "integer" - }, - "label": { - "enum": [ - "invalid-code" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - } - }, - "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)" - }, "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { "code": 403, - "label": "invalid-credentials", - "message": "Authentication failed" + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { @@ -19924,46 +29149,8 @@ }, "label": { "enum": [ - "invalid-credentials", - "invalid-code", - "access-denied" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - } - }, - "description": "Authentication failed (label: `invalid-credentials`)\n\nInvalid verification code (label: `invalid-code`)\n\nAccess denied. (label: `access-denied`)" - }, - "409": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 409, - "label": "password-must-differ", - "message": "For password reset, new and old password must be different." - }, - "properties": { - "code": { - "enum": [ - 409 - ], - "type": "integer" - }, - "label": { - "enum": [ - "password-must-differ" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -19980,60 +29167,102 @@ } } }, - "description": "For password reset, new and old password must be different. (label: `password-must-differ`)" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" } }, - "summary": "Complete a password reset" - } - }, - "/provider/register": { - "post": { - "description": " [internal route ID: \"provider-register\"]\n\n", + "summary": "Remove a collaborator from the team." + }, + "put": { + "description": " [internal route ID: \"update-team-collaborator\"]\n\n", + "operationId": "update-team-collaborator", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/NewProvider" + "items": { + "$ref": "#/components/schemas/CollaboratorPermission_NDg5NTg2ODgy" + }, + "type": "array", + "uniqueItems": true } } }, "required": true }, "responses": { - "201": { + "200": { + "description": "" + } + }, + "summary": "Update a collaborator permissions from the team." + } + }, + "/teams/{tid}/conversations": { + "get": { + "description": " [internal route ID: \"get-team-conversations\"]\n\n", + "operationId": "get-team-conversations", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewProviderResponse" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/NewProviderResponse" + "$ref": "#/components/schemas/TeamConversationList_OTI3MzY3NzY0" } } }, "description": "" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-email", - "message": "Invalid e-mail address." + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "invalid-email" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -20050,7 +29279,37 @@ } } }, - "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + } + }, + "summary": "Get team conversations" + } + }, + "/teams/{tid}/conversations/roles": { + "get": { + "description": " [internal route ID: \"get-team-conversation-roles\"]\n\n", + "operationId": "get-team-conversation-roles", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRolesList" + } + } + }, + "description": "" }, "403": { "content": { @@ -20058,8 +29317,8 @@ "schema": { "example": { "code": 403, - "label": "access-denied", - "message": "Access denied." + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { @@ -20070,7 +29329,7 @@ }, "label": { "enum": [ - "access-denied" + "no-team-member" ], "type": "string" }, @@ -20087,27 +29346,61 @@ } } }, - "description": "Access denied. (label: `access-denied`)" + "description": "Requesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get existing roles available for the given team" + } + }, + "/teams/{tid}/conversations/{cid}": { + "delete": { + "description": " [internal route ID: \"delete-team-conversation\"]\n\n", + "operationId": "delete-team-conversation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } }, - "429": { + { + "in": "path", + "name": "cid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Conversation deleted" + }, + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 429, - "label": "too-many-requests", - "message": "Too many request to generate a verification code." + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 429 + 403 ], "type": "integer" }, "label": { "enum": [ - "too-many-requests" + "no-team-member", + "invalid-op", + "action-denied" ], "type": "string" }, @@ -20124,48 +29417,27 @@ } } }, - "description": "Too many request to generate a verification code. (label: `too-many-requests`)" - } - }, - "summary": "Register a new provider" - } - }, - "/provider/services": { - "get": { - "description": " [internal route ID: \"get-provider-services\"]\n\n", - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "items": { - "$ref": "#/components/schemas/Service" - }, - "type": "array" - } - } - }, - "description": "" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing delete_conversation) (label: `action-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "access-denied", - "message": "Access denied." + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "access-denied" + "no-conversation" ], "type": "string" }, @@ -20182,58 +29454,65 @@ } } }, - "description": "Access denied. (label: `access-denied`)" + "description": "`tid` or `cid` not found\n\nConversation not found (label: `no-conversation`)" } }, - "summary": "List provider services" + "summary": "Remove a team conversation" }, - "post": { - "description": " [internal route ID: \"post-provider-services\"]\n\n", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/NewService" - } + "get": { + "description": " [internal route ID: \"get-team-conversation\"]\n\n", + "operationId": "get-team-conversation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" } }, - "required": true - }, + { + "in": "path", + "name": "cid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "responses": { - "201": { + "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewServiceResponse" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/NewServiceResponse" + "$ref": "#/components/schemas/TeamConversation_LTIwNzgyNTEz" } } }, "description": "" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-service-key", - "message": "Invalid service key." + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "invalid-service-key" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -20250,27 +29529,27 @@ } } }, - "description": "Invalid `body`\n\nInvalid service key. (label: `invalid-service-key`)" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "access-denied", - "message": "Access denied." + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "access-denied" + "no-conversation" ], "type": "string" }, @@ -20287,19 +29566,20 @@ } } }, - "description": "Access denied. (label: `access-denied`)" + "description": "`tid` or `cid` not found\n\nConversation not found (label: `no-conversation`)" } }, - "summary": "Create a new service" + "summary": "Get one team conversation" } }, - "/provider/services/{service-id}": { - "delete": { - "description": " [internal route ID: \"delete-provider-services-by-service-id\"]\n\n", + "/teams/{tid}/features": { + "get": { + "description": " [internal route ID: \"get-all-feature-configs-for-team\"]\n\nGets feature configs for a team. User must be a member of the team and have permission to view team features.", + "operationId": "get-all-feature-configs-for-team", "parameters": [ { "in": "path", - "name": "service-id", + "name": "tid", "required": true, "schema": { "format": "uuid", @@ -20307,18 +29587,15 @@ } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/DeleteService" - } - } - }, - "required": true - }, "responses": { - "202": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NP_LockableFtur_ghdCnfiSOVsyAvIRqExmDpB_MGwUT2Q_LTM1Mzc0ODgy" + } + } + }, "description": "" }, "403": { @@ -20327,8 +29604,8 @@ "schema": { "example": { "code": 403, - "label": "invalid-credentials", - "message": "Authentication failed" + "label": "operation-denied", + "message": "Insufficient permissions" }, "properties": { "code": { @@ -20339,8 +29616,8 @@ }, "label": { "enum": [ - "invalid-credentials", - "access-denied" + "operation-denied", + "no-team-member" ], "type": "string" }, @@ -20357,7 +29634,7 @@ } } }, - "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" }, "404": { "content": { @@ -20365,8 +29642,8 @@ "schema": { "example": { "code": 404, - "label": "not-found", - "message": "Service not found." + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { @@ -20377,7 +29654,7 @@ }, "label": { "enum": [ - "not-found" + "no-team" ], "type": "string" }, @@ -20394,17 +29671,20 @@ } } }, - "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Delete service" - }, + "summary": "Gets feature configs for a team" + } + }, + "/teams/{tid}/features/allowedGlobalOperations": { "get": { - "description": " [internal route ID: \"get-provider-services-by-service-id\"]\n\n", + "description": " [internal route ID: (\"get\", AllowedGlobalOperationsConfig)]\n\n", + "operationId": "get_AllowedGlobalOperationsConfig", "parameters": [ { "in": "path", - "name": "service-id", + "name": "tid", "required": true, "schema": { "format": "uuid", @@ -20417,7 +29697,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Service" + "$ref": "#/components/schemas/LockableFeature_AllowedGlobalOperationsConfig_NjQ1MjA5MDYw" } } }, @@ -20429,8 +29709,8 @@ "schema": { "example": { "code": 403, - "label": "access-denied", - "message": "Access denied." + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { @@ -20441,7 +29721,8 @@ }, "label": { "enum": [ - "access-denied" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -20458,7 +29739,7 @@ } } }, - "description": "Access denied. (label: `access-denied`)" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, "404": { "content": { @@ -20466,8 +29747,8 @@ "schema": { "example": { "code": 404, - "label": "not-found", - "message": "Service not found." + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { @@ -20478,7 +29759,7 @@ }, "label": { "enum": [ - "not-found" + "no-team" ], "type": "string" }, @@ -20495,17 +29776,20 @@ } } }, - "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get provider service by service id" - }, - "put": { - "description": " [internal route ID: \"put-provider-services-by-service-id\"]\n\n", + "summary": "Get config for allowedGlobalOperations" + } + }, + "/teams/{tid}/features/appLock": { + "get": { + "description": " [internal route ID: (\"get\", AppLockConfigB)]\n\n", + "operationId": "get_AppLockConfigB", "parameters": [ { "in": "path", - "name": "service-id", + "name": "tid", "required": true, "schema": { "format": "uuid", @@ -20513,19 +29797,16 @@ } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/UpdateService" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Provider service updated" + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_AppLockConfigB_Bare_Identity_ODgzNDI0OTU5" + } + } + }, + "description": "" }, "403": { "content": { @@ -20533,8 +29814,8 @@ "schema": { "example": { "code": 403, - "label": "access-denied", - "message": "Access denied." + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { @@ -20545,7 +29826,8 @@ }, "label": { "enum": [ - "access-denied" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -20562,7 +29844,7 @@ } } }, - "description": "Access denied. (label: `access-denied`)" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, "404": { "content": { @@ -20570,8 +29852,8 @@ "schema": { "example": { "code": 404, - "label": "not-found", - "message": "Provider not found." + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { @@ -20582,7 +29864,7 @@ }, "label": { "enum": [ - "not-found" + "no-team" ], "type": "string" }, @@ -20599,19 +29881,18 @@ } } }, - "description": "`service-id` not found\n\nProvider not found. (label: `not-found`)\n\nService not found. (label: `not-found`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Update provider service" - } - }, - "/provider/services/{service-id}/connection": { + "summary": "Get config for appLock" + }, "put": { - "description": " [internal route ID: \"put-provider-services-connection-by-service-id\"]\n\n", + "description": " [internal route ID: (\"put\", AppLockConfigB)]\n\n", + "operationId": "put_AppLockConfigB", "parameters": [ { "in": "path", - "name": "service-id", + "name": "tid", "required": true, "schema": { "format": "uuid", @@ -20620,55 +29901,25 @@ } ], "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/UpdateServiceConn" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Provider service connection updated" - }, - "400": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 400, - "label": "invalid-service-key", - "message": "Invalid service key." - }, - "properties": { - "code": { - "enum": [ - 400 - ], - "type": "integer" - }, - "label": { - "enum": [ - "invalid-service-key" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_AppLockConfigB_Bare_Identity_MTAzMjI5NDYy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_AppLockConfigB_Bare_Identity_ODgzNDI0OTU5" } } }, - "description": "Invalid `body`\n\nInvalid service key. (label: `invalid-service-key`)" + "description": "" }, "403": { "content": { @@ -20676,8 +29927,8 @@ "schema": { "example": { "code": 403, - "label": "invalid-credentials", - "message": "Authentication failed" + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { @@ -20688,8 +29939,8 @@ }, "label": { "enum": [ - "invalid-credentials", - "access-denied" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -20706,7 +29957,7 @@ } } }, - "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, "404": { "content": { @@ -20714,8 +29965,8 @@ "schema": { "example": { "code": 404, - "label": "not-found", - "message": "Service not found." + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { @@ -20726,7 +29977,7 @@ }, "label": { "enum": [ - "not-found" + "no-team" ], "type": "string" }, @@ -20743,19 +29994,20 @@ } } }, - "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Update provider service connection" + "summary": "Put config for appLock" } }, - "/providers/{pid}": { + "/teams/{tid}/features/apps": { "get": { - "description": " [internal route ID: \"provider-get-profile\"]\n\n", + "description": " [internal route ID: (\"get\", AppsConfig)]\n\n", + "operationId": "get_AppsConfig", "parameters": [ { "in": "path", - "name": "pid", + "name": "tid", "required": true, "schema": { "format": "uuid", @@ -20766,38 +30018,34 @@ "responses": { "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Provider" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Provider" + "$ref": "#/components/schemas/LockableFeature_AppsConfig_MzQyNTMxNTk5" } } }, "description": "" }, - "404": { + "403": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "not-found", - "message": "Provider not found." + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "not-found" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -20812,13 +30060,18 @@ ], "type": "object" } - }, + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { "code": 404, - "label": "not-found", - "message": "Provider not found." + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { @@ -20829,7 +30082,7 @@ }, "label": { "enum": [ - "not-found" + "no-team" ], "type": "string" }, @@ -20846,19 +30099,20 @@ } } }, - "description": "`pid` or Provider not found. (label: `not-found`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get profile" + "summary": "Get config for apps" } }, - "/providers/{provider-id}/services": { + "/teams/{tid}/features/assetAuditLog": { "get": { - "description": " [internal route ID: \"get-provider-services-by-provider-id\"]\n\n", + "description": " [internal route ID: (\"get\", AssetAuditLogConfig)]\n\n", + "operationId": "get_AssetAuditLogConfig", "parameters": [ { "in": "path", - "name": "provider-id", + "name": "tid", "required": true, "schema": { "format": "uuid", @@ -20871,10 +30125,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "items": { - "$ref": "#/components/schemas/ServiceProfile" - }, - "type": "array" + "$ref": "#/components/schemas/LockableFeature_AssetAuditLogConfig_NDQ3MzcyMzk2" } } }, @@ -20886,8 +30137,8 @@ "schema": { "example": { "code": 403, - "label": "access-denied", - "message": "Access denied." + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { @@ -20898,7 +30149,8 @@ }, "label": { "enum": [ - "access-denied" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -20915,28 +30167,57 @@ } } }, - "description": "Access denied. (label: `access-denied`)" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get provider services by provider id" + "summary": "Get config for assetAuditLog" } }, - "/providers/{provider-id}/services/{service-id}": { + "/teams/{tid}/features/cells": { "get": { - "description": " [internal route ID: \"get-provider-services-by-provider-id-and-service-id\"]\n\n", + "description": " [internal route ID: (\"get\", CellsConfigB)]\n\n", + "operationId": "get_CellsConfigB", "parameters": [ { "in": "path", - "name": "provider-id", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "in": "path", - "name": "service-id", + "name": "tid", "required": true, "schema": { "format": "uuid", @@ -20949,7 +30230,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ServiceProfile" + "$ref": "#/components/schemas/LockableFeature_CellsConfigB_Bare_Identity_LTgzMDA1NjI3" } } }, @@ -20961,8 +30242,8 @@ "schema": { "example": { "code": 403, - "label": "access-denied", - "message": "Access denied." + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { @@ -20973,7 +30254,8 @@ }, "label": { "enum": [ - "access-denied" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -20990,7 +30272,7 @@ } } }, - "description": "Access denied. (label: `access-denied`)" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, "404": { "content": { @@ -20998,8 +30280,8 @@ "schema": { "example": { "code": 404, - "label": "not-found", - "message": "Service not found." + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { @@ -21010,7 +30292,7 @@ }, "label": { "enum": [ - "not-found" + "no-team" ], "type": "string" }, @@ -21027,87 +30309,66 @@ } } }, - "description": "`provider-id` or `service-id` not found\n\nService not found. (label: `not-found`)" - } - }, - "summary": "Get provider service by provider id and service id" - } - }, - "/proxy/giphy/v1/gifs": {}, - "/proxy/googlemaps/api/staticmap": {}, - "/proxy/googlemaps/maps/api/geocode": {}, - "/proxy/youtube/v3": {}, - "/push/tokens": { - "get": { - "description": " [internal route ID: \"get-push-tokens\"]\n\n", - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/PushTokenList" - } - } - }, - "description": "" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "List the user's registered push tokens" + "summary": "Get config for cells" }, - "post": { - "description": " [internal route ID: \"register-push-token\"]\n\n", + "put": { + "description": " [internal route ID: (\"put\", CellsConfigB)]\n\n", + "operationId": "put_CellsConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/PushToken" + "$ref": "#/components/schemas/Feature_CellsConfigB_Bare_Identity_NzU2NjgxNzEw" } } }, "required": true }, "responses": { - "201": { + "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PushToken" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/PushToken" + "$ref": "#/components/schemas/LockableFeature_CellsConfigB_Bare_Identity_LTgzMDA1NjI3" } } }, - "description": "Push token registered", - "headers": { - "Location": { - "schema": { - "type": "string" - } - } - } + "description": "" }, - "400": { + "403": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "apns-voip-not-supported", - "message": "Adding APNS_VOIP tokens is not supported" + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "apns-voip-not-supported" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -21122,24 +30383,29 @@ ], "type": "object" } - }, + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "apns-voip-not-supported", - "message": "Adding APNS_VOIP tokens is not supported" + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 400 + 404 ], "type": "integer" }, "label": { "enum": [ - "apns-voip-not-supported" + "no-team" ], "type": "string" }, @@ -21156,28 +30422,58 @@ } } }, - "description": "Adding APNS_VOIP tokens is not supported (label: `apns-voip-not-supported`) or `body`" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for cells" + } + }, + "/teams/{tid}/features/cellsInternal": { + "get": { + "description": " [internal route ID: (\"get\", CellsInternalConfigB)]\n\n", + "operationId": "get_CellsInternalConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_CsInfigBdy_LTQ3MTU3Mjg0" + } + } + }, + "description": "" }, - "404": { + "403": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "app-not-found", - "message": "App does not exist" + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "app-not-found", - "invalid-token" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -21192,13 +30488,18 @@ ], "type": "object" } - }, + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { "code": 404, - "label": "app-not-found", - "message": "App does not exist" + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { @@ -21209,8 +30510,7 @@ }, "label": { "enum": [ - "app-not-found", - "invalid-token" + "no-team" ], "type": "string" }, @@ -21227,29 +30527,58 @@ } } }, - "description": "App does not exist (label: `app-not-found`)\n\nInvalid push token (label: `invalid-token`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for cellsInternal" + } + }, + "/teams/{tid}/features/channels": { + "get": { + "description": " [internal route ID: (\"get\", ChannelsConfigB)]\n\n", + "operationId": "get_ChannelsConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_ChannelsConfigB_Bare_Identity_NzA2NDEyMDEw" + } + } + }, + "description": "" }, - "413": { + "403": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 413, - "label": "sns-thread-budget-reached", - "message": "Too many concurrent calls to SNS; is SNS down?" + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 413 + 403 ], "type": "integer" }, "label": { "enum": [ - "sns-thread-budget-reached", - "token-too-long", - "metadata-too-long" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -21264,26 +30593,29 @@ ], "type": "object" } - }, + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 413, - "label": "sns-thread-budget-reached", - "message": "Too many concurrent calls to SNS; is SNS down?" + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 413 + 404 ], "type": "integer" }, "label": { "enum": [ - "sns-thread-budget-reached", - "token-too-long", - "metadata-too-long" + "no-team" ], "type": "string" }, @@ -21300,49 +30632,66 @@ } } }, - "description": "Too many concurrent calls to SNS; is SNS down? (label: `sns-thread-budget-reached`)\n\nPush token length must be < 8192 for GCM or 400 for APNS (label: `token-too-long`)\n\nTried to add token to endpoint resulting in metadata length > 2048 (label: `metadata-too-long`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Register a native push token" - } - }, - "/push/tokens/{pid}": { - "delete": { - "description": " [internal route ID: \"delete-push-token\"]\n\n", + "summary": "Get config for channels" + }, + "put": { + "description": " [internal route ID: (\"put\", ChannelsConfigB)]\n\n", + "operationId": "put_ChannelsConfigB", "parameters": [ { - "description": "The push token to delete", "in": "path", - "name": "pid", + "name": "tid", "required": true, "schema": { + "format": "uuid", "type": "string" } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_ChannelsConfigB_Bare_Identity_LTU4NTE4MTgx" + } + } + }, + "required": true + }, "responses": { - "204": { - "description": "Push token unregistered" + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_ChannelsConfigB_Bare_Identity_NzA2NDEyMDEw" + } + } + }, + "description": "" }, - "404": { + "403": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "not-found", - "message": "Push token not found" + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "not-found" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -21357,13 +30706,18 @@ ], "type": "object" } - }, + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { "code": 404, - "label": "not-found", - "message": "Push token not found" + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { @@ -21374,7 +30728,7 @@ }, "label": { "enum": [ - "not-found" + "no-team" ], "type": "string" }, @@ -21391,111 +30745,58 @@ } } }, - "description": "`pid` or Push token not found (label: `not-found`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Unregister a native push token" + "summary": "Put config for channels" } }, - "/register": { - "post": { - "description": " [internal route ID: \"register\"]\n\nIf the environment where the registration takes place is private and a registered email address is not whitelisted, a 403 error is returned.Calls federation service brig on send-connection-action", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/NewUser" - } + "/teams/{tid}/features/chatBubbles": { + "get": { + "description": " [internal route ID: (\"get\", ChatBubblesConfig)]\n\n", + "operationId": "get_ChatBubblesConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" } - }, - "required": true - }, + } + ], "responses": { - "201": { + "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/User" + "$ref": "#/components/schemas/LockableFeature_ChatBubblesConfig_NDgwMTQzNjI2" } } }, - "description": "User created and pending activation", - "headers": { - "Location": { - "description": "UserId", - "schema": { - "format": "uuid", - "type": "string" - } - }, - "Set-Cookie": { - "description": "Cookie", - "schema": { - "type": "string" - } - } - } + "description": "" }, - "400": { + "403": { "content": { - "application/json": { - "schema": { - "example": { - "code": 400, - "label": "invalid-invitation-code", - "message": "Invalid invitation code." - }, - "properties": { - "code": { - "enum": [ - 400 - ], - "type": "integer" - }, - "label": { - "enum": [ - "invalid-invitation-code", - "invalid-email", - "invalid-phone" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - }, "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-invitation-code", - "message": "Invalid invitation code." + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "invalid-invitation-code", - "invalid-email", - "invalid-phone" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -21512,31 +30813,27 @@ } } }, - "description": "Invalid invitation code. (label: `invalid-invitation-code`)\n\nInvalid e-mail address. (label: `invalid-email`)\n\nInvalid mobile phone number (label: `invalid-phone`) or `body`" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, - "403": { + "404": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "unauthorized", - "message": "Unauthorized e-mail address" + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "unauthorized", - "missing-identity", - "blacklisted-email", - "too-many-team-members", - "user-creation-restricted" + "no-team" ], "type": "string" }, @@ -21551,13 +30848,48 @@ ], "type": "object" } - }, + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for chatBubbles" + } + }, + "/teams/{tid}/features/classifiedDomains": { + "get": { + "description": " [internal route ID: (\"get\", ClassifiedDomainsConfig)]\n\n", + "operationId": "get_ClassifiedDomainsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_ClassifiedDomainsConfig_LTY1ODQwODg1" + } + } + }, + "description": "" + }, + "403": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { "code": 403, - "label": "unauthorized", - "message": "Unauthorized e-mail address" + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { @@ -21568,11 +30900,8 @@ }, "label": { "enum": [ - "unauthorized", - "missing-identity", - "blacklisted-email", - "too-many-team-members", - "user-creation-restricted" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -21589,16 +30918,16 @@ } } }, - "description": "Unauthorized e-mail address (label: `unauthorized`)\n\nUsing an invitation code requires registering the given email. (label: `missing-identity`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nToo many members in this team. (label: `too-many-team-members`)\n\nThis instance does not allow creation of personal users or teams. (label: `user-creation-restricted`)" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, "404": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { "code": 404, - "label": "invalid-code", - "message": "User does not exist" + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { @@ -21609,7 +30938,7 @@ }, "label": { "enum": [ - "invalid-code" + "no-team" ], "type": "string" }, @@ -21624,24 +30953,60 @@ ], "type": "object" } - }, + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for classifiedDomains" + } + }, + "/teams/{tid}/features/conferenceCalling": { + "get": { + "description": " [internal route ID: (\"get\", ConferenceCallingConfigB)]\n\n", + "operationId": "get_ConferenceCallingConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_CnfigBIdy_NzY1NDU5MDAy" + } + } + }, + "description": "" + }, + "403": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "invalid-code", - "message": "User does not exist" + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "invalid-code" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -21658,27 +31023,27 @@ } } }, - "description": "User does not exist (label: `invalid-code`)\n\nInvalid activation code (label: `invalid-code`)" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, - "409": { + "404": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "key-exists", - "message": "The given e-mail address is in use." + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 409 + 404 ], "type": "integer" }, "label": { "enum": [ - "key-exists" + "no-team" ], "type": "string" }, @@ -21693,24 +31058,68 @@ ], "type": "object" } - }, + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for conferenceCalling" + }, + "put": { + "description": " [internal route ID: (\"put\", ConferenceCallingConfigB)]\n\n", + "operationId": "put_ConferenceCallingConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_ConferenceCallingConfigB_Bare_Identity_Njc2NTcxNTI3" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_CnfigBIdy_NzY1NDU5MDAy" + } + } + }, + "description": "" + }, + "403": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "key-exists", - "message": "The given e-mail address is in use." + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 409 + 403 ], "type": "integer" }, "label": { "enum": [ - "key-exists" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -21727,50 +31136,27 @@ } } }, - "description": "The given e-mail address is in use. (label: `key-exists`)" - } - }, - "summary": "Register a new user." - } - }, - "/scim/auth-tokens": { - "delete": { - "parameters": [ - { - "in": "query", - "name": "id", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "code-authentication-required", - "message": "Code authentication is required" + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "code-authentication-required", - "code-authentication-failed", - "password-authentication-failed" + "no-team" ], "type": "string" }, @@ -21787,17 +31173,33 @@ } } }, - "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nPassword authentication failed. (label: `password-authentication-failed`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } - } - }, + }, + "summary": "Put config for conferenceCalling" + } + }, + "/teams/{tid}/features/consumableNotifications": { "get": { + "description": " [internal route ID: (\"get\", ConsumableNotificationsConfig)]\n\n", + "operationId": "get_ConsumableNotificationsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ScimTokenList" + "$ref": "#/components/schemas/LockableFeature_ConsumableNotificationsConfig_MjUxMjczMjM0" } } }, @@ -21809,8 +31211,8 @@ "schema": { "example": { "code": 403, - "label": "code-authentication-required", - "message": "Code authentication is required" + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { @@ -21821,9 +31223,8 @@ }, "label": { "enum": [ - "code-authentication-required", - "code-authentication-failed", - "password-authentication-failed" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -21840,53 +31241,27 @@ } } }, - "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nPassword authentication failed. (label: `password-authentication-failed`)" - } - } - }, - "post": { - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/CreateScimToken" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/CreateScimTokenResponse" - } - } - }, - "description": "" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "code-authentication-required", - "message": "Code authentication is required" + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "code-authentication-required", - "code-authentication-failed", - "password-authentication-failed" + "no-team" ], "type": "string" }, @@ -21903,44 +31278,25 @@ } } }, - "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nPassword authentication failed. (label: `password-authentication-failed`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } - } + }, + "summary": "Get config for consumableNotifications" } }, - "/search/contacts": { + "/teams/{tid}/features/conversationGuestLinks": { "get": { - "description": " [internal route ID: \"search-contacts\"]\n\nCalls federation service brig on search-users
Calls federation service brig on get-users-by-ids", + "description": " [internal route ID: (\"get\", GuestLinksConfig)]\n\n", + "operationId": "get_GuestLinksConfig", "parameters": [ { - "description": "Search query", - "in": "query", - "name": "q", + "in": "path", + "name": "tid", "required": true, "schema": { + "format": "uuid", "type": "string" } - }, - { - "description": "Searched domain. Note: This is optional only for backwards compatibility, future versions will mandate this.", - "in": "query", - "name": "domain", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Number of results to return (min: 1, max: 500, default 15)", - "in": "query", - "name": "size", - "required": false, - "schema": { - "format": "int32", - "maximum": 500, - "minimum": 1, - "type": "integer" - } } ], "responses": { @@ -21948,67 +31304,32 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SearchResult" + "$ref": "#/components/schemas/LockableFeature_GuestLinksConfig_LTcwNjU0NDMw" } } }, "description": "" - } - }, - "summary": "Search for users" - } - }, - "/self": { - "delete": { - "description": " [internal route ID: \"delete-self\"]\n\nif the account has a verified identity, a verification code is sent and needs to be confirmed to authorise the deletion. if the account has no verified identity but a password, it must be provided. if password is correct, or if neither a verified identity nor a password exists, account deletion is scheduled immediately.Calls federation service brig on send-connection-action", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/DeleteUser" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Deletion is initiated." - }, - "202": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeletionCodeTimeout" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/DeletionCodeTimeout" - } - } - }, - "description": "Deletion is pending verification with a code." }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-user", - "message": "Invalid user" + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "invalid-user" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -22025,31 +31346,27 @@ } } }, - "description": "Invalid `body`\n\nInvalid user (label: `invalid-user`)" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "no-self-delete-for-team-owner", - "message": "Team owners are not allowed to delete themselves; ask a fellow owner" + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "no-self-delete-for-team-owner", - "pending-delete", - "missing-auth", - "invalid-credentials", - "invalid-code" + "no-team" ], "type": "string" }, @@ -22066,34 +31383,30 @@ } } }, - "description": "Team owners are not allowed to delete themselves; ask a fellow owner (label: `no-self-delete-for-team-owner`)\n\nA verification code for account deletion is still pending (label: `pending-delete`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nAuthentication failed (label: `invalid-credentials`)\n\nInvalid verification code (label: `invalid-code`)" - } - }, - "summary": "Initiate account deletion." - }, - "get": { - "description": " [internal route ID: \"get-self\"]\n\n\nOAuth scope: `read:self`", - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - }, - "description": "" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get your own profile" + "summary": "Get config for conversationGuestLinks" }, "put": { - "description": " [internal route ID: \"put-self\"]\n\nCalls federation service brig on send-connection-action", + "description": " [internal route ID: (\"put\", GuestLinksConfig)]\n\n", + "operationId": "put_GuestLinksConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/UserUpdate" + "$ref": "#/components/schemas/Feature_GuestLinksConfig_NjQyMDMxNjg3" } } }, @@ -22101,27 +31414,23 @@ }, "responses": { "200": { - "description": "User updated" - } - }, - "summary": "Update your profile." - } - }, - "/self/email": { - "delete": { - "description": " [internal route ID: \"remove-email\"]\n\nYour email address can only be removed if you also have a phone number.Calls federation service brig on send-connection-action", - "responses": { - "200": { - "description": "Identity Removed" + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_GuestLinksConfig_LTcwNjU0NDMw" + } + } + }, + "description": "" }, "403": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { "code": 403, - "label": "last-identity", - "message": "The last user identity cannot be removed." + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { @@ -22132,9 +31441,8 @@ }, "label": { "enum": [ - "last-identity", - "no-password", - "no-identity" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -22149,26 +31457,29 @@ ], "type": "object" } - }, + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "last-identity", - "message": "The last user identity cannot be removed." + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "last-identity", - "no-password", - "no-identity" + "no-team" ], "type": "string" }, @@ -22185,91 +31496,46 @@ } } }, - "description": "The last user identity cannot be removed. (label: `last-identity`)\n\nThe user has no password. (label: `no-password`)\n\nThe user has no verified email (label: `no-identity`)" - } - }, - "summary": "Remove your email address." - } - }, - "/self/handle": { - "put": { - "description": " [internal route ID: \"change-handle\"]\n\nCalls federation service brig on send-connection-action
Calls federation service brig on send-connection-action", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/HandleUpdate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Handle Changed" - } - }, - "summary": "Change your handle." - } - }, - "/self/locale": { - "put": { - "description": " [internal route ID: \"change-locale\"]\n\nCalls federation service brig on send-connection-action", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/LocaleUpdate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Local Changed" - } - }, - "summary": "Change your locale." - } - }, - "/self/password": { - "head": { - "description": " [internal route ID: \"check-password-exists\"]\n\n", - "responses": { - "200": { - "description": "Password is set" - }, - "404": { - "description": "Password is not set" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Check that your password is set." - }, - "put": { - "description": " [internal route ID: \"change-password\"]\n\n", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/PasswordChange" - } + "summary": "Put config for conversationGuestLinks" + } + }, + "/teams/{tid}/features/digitalSignatures": { + "get": { + "description": " [internal route ID: (\"get\", DigitalSignaturesConfig)]\n\n", + "operationId": "get_DigitalSignaturesConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "200": { - "description": "Password Changed" + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_DigitalSignaturesConfig_ODM2MDA2NDU4" + } + } + }, + "description": "" }, "403": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { "code": 403, - "label": "invalid-credentials", - "message": "Authentication failed" + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { @@ -22280,8 +31546,8 @@ }, "label": { "enum": [ - "invalid-credentials", - "no-identity" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -22296,25 +31562,29 @@ ], "type": "object" } - }, + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "invalid-credentials", - "message": "Authentication failed" + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "invalid-credentials", - "no-identity" + "no-team" ], "type": "string" }, @@ -22331,27 +31601,58 @@ } } }, - "description": "Authentication failed (label: `invalid-credentials`)\n\nThe user has no verified email (label: `no-identity`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for digitalSignatures" + } + }, + "/teams/{tid}/features/domainRegistration": { + "get": { + "description": " [internal route ID: (\"get\", DomainRegistrationConfig)]\n\n", + "operationId": "get_DomainRegistrationConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_DomainRegistrationConfig_NzU0NjczNTE0" + } + } + }, + "description": "" }, - "409": { + "403": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "password-must-differ", - "message": "For password change, new and old password must be different." + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 409 + 403 ], "type": "integer" }, "label": { "enum": [ - "password-must-differ" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -22366,24 +31667,29 @@ ], "type": "object" } - }, + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "password-must-differ", - "message": "For password change, new and old password must be different." + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 409 + 404 ], "type": "integer" }, "label": { "enum": [ - "password-must-differ" + "no-team" ], "type": "string" }, @@ -22400,96 +31706,25 @@ } } }, - "description": "For password change, new and old password must be different. (label: `password-must-differ`)" - } - }, - "summary": "Change your password." - } - }, - "/self/supported-protocols": { - "put": { - "description": " [internal route ID: \"change-supported-protocols\"]\n\n", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/SupportedProtocolUpdate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Supported protocols changed" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Change your supported protocols" + "summary": "Get config for domainRegistration" } }, - "/services": { + "/teams/{tid}/features/enforceFileDownloadLocation": { "get": { - "description": " [internal route ID: \"get-services\"]\n\n", + "description": " [internal route ID: (\"get\", EnforceFileDownloadLocationConfigB)]\n\n

Custom feature: only supported on some dedicated on-prem systems.

", + "operationId": "get_EnforceFileDownloadLocationConfigB", "parameters": [ { - "in": "query", - "name": "tags", - "required": false, - "schema": { - "enum": [ - "audio", - "books", - "business", - "design", - "education", - "entertainment", - "finance", - "fitness", - "food-drink", - "games", - "graphics", - "health", - "integration", - "lifestyle", - "media", - "medical", - "movies", - "music", - "news", - "photography", - "poll", - "productivity", - "quiz", - "rating", - "shopping", - "social", - "sports", - "travel", - "tutorial", - "video", - "weather" - ], - "type": "string" - } - }, - { - "in": "query", - "name": "start", - "required": false, + "in": "path", + "name": "tid", + "required": true, "schema": { + "format": "uuid", "type": "string" } - }, - { - "in": "query", - "name": "size", - "required": false, - "schema": { - "format": "int32", - "maximum": 100, - "minimum": 10, - "type": "integer" - } } ], "responses": { @@ -22497,7 +31732,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ServiceProfile" + "$ref": "#/components/schemas/LockableFtur_EnfiDwdCgBIy_ODA5OTA5MTQ4" } } }, @@ -22509,8 +31744,8 @@ "schema": { "example": { "code": 403, - "label": "access-denied", - "message": "Access denied." + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { @@ -22521,7 +31756,8 @@ }, "label": { "enum": [ - "access-denied" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -22538,36 +31774,55 @@ } } }, - "description": "Access denied. (label: `access-denied`)" - } - }, - "summary": "List services" - } - }, - "/sso/finalize-login": { - "post": { - "deprecated": true, - "description": "DEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", - "responses": { - "200": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { "content": { - "text/plain;charset=utf-8": { + "application/json;charset=utf-8": { "schema": { - "type": "string" + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } - } - } - }, - "/sso/finalize-login/{team}": { - "post": { + }, + "summary": "Get config for enforceFileDownloadLocation" + }, + "put": { + "description": " [internal route ID: (\"put\", EnforceFileDownloadLocationConfigB)]\n\n

Custom feature: only supported on some dedicated on-prem systems.

", + "operationId": "put_EnforceFileDownloadLocationConfigB", "parameters": [ { "in": "path", - "name": "team", + "name": "tid", "required": true, "schema": { "format": "uuid", @@ -22575,124 +31830,114 @@ } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Featur_EnfocilDwdLCgBIy_MjcxMzc1NDA5" + } + } + }, + "required": true + }, "responses": { "200": { "content": { - "text/plain;charset=utf-8": { + "application/json;charset=utf-8": { "schema": { - "type": "string" + "$ref": "#/components/schemas/LockableFtur_EnfiDwdCgBIy_ODA5OTA5MTQ4" } } }, "description": "" - } - } - } - }, - "/sso/initiate-login/{idp}": { - "get": { - "parameters": [ - { - "in": "query", - "name": "success_redirect", - "required": false, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "error_redirect", - "required": false, - "schema": { - "type": "string" - } }, - { - "in": "path", - "name": "idp", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { + "403": { "content": { - "text/html": { + "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/FormRedirect" + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "" - } - } - }, - "head": { - "parameters": [ - { - "in": "query", - "name": "success_redirect", - "required": false, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "error_redirect", - "required": false, - "schema": { - "type": "string" - } + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, - { - "in": "path", - "name": "idp", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "text/plain;charset=utf-8": {} - }, - "description": "" - } - } - } - }, - "/sso/metadata": { - "get": { - "deprecated": true, - "description": "DEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", - "responses": { - "200": { + "404": { "content": { - "application/xml": { + "application/json;charset=utf-8": { "schema": { - "type": "string" + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } - } + }, + "summary": "Put config for enforceFileDownloadLocation" } }, - "/sso/metadata/{team}": { + "/teams/{tid}/features/exposeInvitationURLsToTeamAdmin": { "get": { + "description": " [internal route ID: (\"get\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "operationId": "get_ExposeInvitationURLsToTeamAdminConfig", "parameters": [ { "in": "path", - "name": "team", + "name": "tid", "required": true, "schema": { "format": "uuid", @@ -22700,109 +31945,150 @@ } } ], - "responses": { - "200": { - "content": { - "application/xml": { - "schema": { - "type": "string" - } - } - }, - "description": "" - } - } - } - }, - "/sso/settings": { - "get": { "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SsoSettings" + "$ref": "#/components/schemas/LockableFtur_ExpsInviURTmAdCfg_LTQzMzU2OTY1" } } }, "description": "" - } - } - } - }, - "/system/settings": { - "get": { - "description": " [internal route ID: \"get-system-settings\"]\n\n", - "responses": { - "200": { + }, + "403": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SystemSettings" + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } - }, - "description": "" - } - }, - "summary": "Returns a curated set of system configuration settings for authorized users." - } - }, - "/system/settings/unauthorized": { - "get": { - "description": " [internal route ID: \"get-system-settings-unauthorized\"]\n\n", - "responses": { - "200": { + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SystemSettingsPublic" + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Returns a curated set of system configuration settings." - } - }, - "/teams/invitations/by-email": { - "head": { - "description": " [internal route ID: \"head-team-invitations\"]\n\n", + "summary": "Get config for exposeInvitationURLsToTeamAdmin" + }, + "put": { + "description": " [internal route ID: (\"put\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "operationId": "put_ExposeInvitationURLsToTeamAdminConfig", "parameters": [ { - "description": "Email address", - "in": "query", - "name": "email", + "in": "path", + "name": "tid", "required": true, "schema": { + "format": "uuid", "type": "string" } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_ExposeInvitationURLsToTeamAdminConfig_LTY5NzY2Mzg5" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Pending invitation exists." + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_ExpsInviURTmAdCfg_LTQzMzU2OTY1" + } + } + }, + "description": "" }, - "404": { + "403": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "not-found", - "message": "No pending invitations exists." + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "not-found" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -22817,13 +32103,18 @@ ], "type": "object" } - }, + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { "code": 404, - "label": "not-found", - "message": "No pending invitations exists." + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { @@ -22834,7 +32125,7 @@ }, "label": { "enum": [ - "not-found" + "no-team" ], "type": "string" }, @@ -22851,27 +32142,58 @@ } } }, - "description": "No pending invitations exists. (label: `not-found`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for exposeInvitationURLsToTeamAdmin" + } + }, + "/teams/{tid}/features/fileSharing": { + "get": { + "description": " [internal route ID: (\"get\", FileSharingConfig)]\n\n", + "operationId": "get_FileSharingConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_FileSharingConfig_MjgwNjIzODEz" + } + } + }, + "description": "" }, - "409": { + "403": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "conflicting-invitations", - "message": "Multiple conflicting invitations to different teams exists." + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 409 + 403 ], "type": "integer" }, "label": { "enum": [ - "conflicting-invitations" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -22886,24 +32208,29 @@ ], "type": "object" } - }, + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 409, - "label": "conflicting-invitations", - "message": "Multiple conflicting invitations to different teams exists." + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 409 + 404 ], "type": "integer" }, "label": { "enum": [ - "conflicting-invitations" + "no-team" ], "type": "string" }, @@ -22920,61 +32247,103 @@ } } }, - "description": "Multiple conflicting invitations to different teams exists. (label: `conflicting-invitations`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Check if there is an invitation pending given an email address." - } - }, - "/teams/invitations/info": { - "get": { - "description": " [internal route ID: \"get-team-invitation-info\"]\n\n", + "summary": "Get config for fileSharing" + }, + "put": { + "description": " [internal route ID: (\"put\", FileSharingConfig)]\n\n", + "operationId": "put_FileSharingConfig", "parameters": [ { - "description": "Invitation code", - "in": "query", - "name": "code", + "in": "path", + "name": "tid", "required": true, "schema": { + "format": "uuid", "type": "string" } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_FileSharingConfig_LTUyNjkxMzM4" + } + } + }, + "required": true + }, "responses": { "200": { "content": { - "application/json": { + "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Invitation" + "$ref": "#/components/schemas/LockableFeature_FileSharingConfig_MjgwNjIzODEz" } - }, + } + }, + "description": "" + }, + "403": { + "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Invitation" + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "Invitation info" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, - "400": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-invitation-code", - "message": "Invalid invitation code." + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 400 + 404 ], "type": "integer" }, "label": { "enum": [ - "invalid-invitation-code" + "no-team" ], "type": "string" }, @@ -22991,37 +32360,25 @@ } } }, - "description": "Invalid `code`\n\nInvalid invitation code. (label: `invalid-invitation-code`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get invitation info given a code." + "summary": "Put config for fileSharing" } }, - "/teams/notifications": { + "/teams/{tid}/features/legalhold": { "get": { - "description": " [internal route ID: \"get-team-notifications\"]\n\nThis is a work-around for scalability issues with gundeck user event fan-out. It does not track all team-wide events, but only `member-join`.\nNote that `/teams/notifications` behaves differently from `/notifications`:\n- If there is a gap between the notification id requested with `since` and the available data, team queues respond with 200 and the data that could be found. They do NOT respond with status 404, but valid data in the body.\n- The notification with the id given via `since` is included in the response if it exists. You should remove this and only use it to decide whether there was a gap between your last request and this one.\n- If the notification id does *not* exist, you get the more recent events from the queue (instead of all of them). This can be done because a notification id is a UUIDv1, which is essentially a time stamp.\n- There is no corresponding `/last` end-point to get only the most recent event. That end-point was only useful to avoid having to pull the entire queue. In team queues, if you have never requested the queue before and have no prior notification id, just pull with timestamp 'now'.", + "description": " [internal route ID: (\"get\", LegalholdConfig)]\n\n", + "operationId": "get_LegalholdConfig", "parameters": [ { - "description": "Notification id to start with in the response (UUIDv1)", - "in": "query", - "name": "since", - "required": false, + "in": "path", + "name": "tid", + "required": true, "schema": { "format": "uuid", "type": "string" } - }, - { - "description": "Maximum number of events to return (1..10000; default: 1000)", - "in": "query", - "name": "size", - "required": false, - "schema": { - "format": "int32", - "maximum": 10000, - "minimum": 1, - "type": "integer" - } } ], "responses": { @@ -23029,31 +32386,32 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/QueuedNotificationList" + "$ref": "#/components/schemas/LockableFeature_LegalholdConfig_LTc5MTk5OTIw" } } }, "description": "" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-notification-id", - "message": "Could not parse notification id (must be UUIDv1)." + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "invalid-notification-id" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -23070,7 +32428,7 @@ } } }, - "description": "Invalid `size` or `since`\n\nCould not parse notification id (must be UUIDv1). (label: `invalid-notification-id`)" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, "404": { "content": { @@ -23107,19 +32465,18 @@ } } }, - "description": "Team not found (label: `no-team`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Read recently added team members from team queue" - } - }, - "/teams/{team-id}/services/whitelist": { - "post": { - "description": " [internal route ID: \"post-team-whitelist-by-team-id\"]\n\n", + "summary": "Get config for legalhold" + }, + "put": { + "description": " [internal route ID: (\"put\", LegalholdConfig)]\n\n", + "operationId": "put_LegalholdConfig", "parameters": [ { "in": "path", - "name": "team-id", + "name": "tid", "required": true, "schema": { "format": "uuid", @@ -23131,108 +32488,59 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/UpdateServiceWhitelist" + "$ref": "#/components/schemas/Feature_LegalholdConfig_NjM3MTkxNjYw" } } }, "required": true }, - "responses": { - "200": { - "description": "UpdateServiceWhitelistRespChanged" - }, - "204": { - "description": "UpdateServiceWhitelistRespUnchanged" - } - }, - "summary": "Update service whitelist" - } - }, - "/teams/{team-id}/services/whitelisted": { - "get": { - "description": " [internal route ID: \"get-whitelisted-services-by-team-id\"]\n\n", - "parameters": [ - { - "in": "path", - "name": "team-id", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "in": "query", - "name": "prefix", - "required": false, - "schema": { - "maxLength": 1, - "minLength": 128, - "type": "string" - } - }, - { - "in": "query", - "name": "filter_disabled", - "required": false, - "schema": { - "type": "boolean" - } - }, - { - "in": "query", - "name": "size", - "required": false, - "schema": { - "format": "int32", - "maximum": 100, - "minimum": 10, - "type": "integer" - } - } - ], "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ServiceProfile" + "$ref": "#/components/schemas/LockableFeature_LegalholdConfig_LTc5MTk5OTIw" } } }, "description": "" - } - }, - "summary": "Get whitelisted services by team id" - } - }, - "/teams/{tid}": { - "delete": { - "description": " [internal route ID: \"delete-team\"]\n\n", - "parameters": [ - { - "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/TeamDeleteData" - } - } }, - "required": true - }, - "responses": { - "202": { - "description": "Team is scheduled for removal" + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" }, "403": { "content": { @@ -23240,8 +32548,8 @@ "schema": { "example": { "code": 403, - "label": "code-authentication-required", - "message": "Verification code required" + "label": "legalhold-disable-unimplemented", + "message": "legal hold cannot be disabled for whitelisted teams" }, "properties": { "code": { @@ -23252,11 +32560,12 @@ }, "label": { "enum": [ - "code-authentication-required", - "code-authentication-failed", - "access-denied", - "operation-denied", - "no-team-member" + "legalhold-disable-unimplemented", + "legalhold-not-enabled", + "too-large-team-for-legalhold", + "action-denied", + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -23273,7 +32582,7 @@ } } }, - "description": "Verification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (missing DeleteTeam) (label: `operation-denied`)" + "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nCannot enable legalhold on large teams (reason: for removing LH from team, we need to iterate over all members, which is only supported for teams with less than 2k members) (label: `too-large-team-for-legalhold`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, "404": { "content": { @@ -23312,25 +32621,26 @@ }, "description": "`tid` not found\n\nTeam not found (label: `no-team`)" }, - "503": { + "500": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 503, - "label": "queue-full", - "message": "The delete queue is full; no further delete requests can be processed at the moment" + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." }, "properties": { "code": { "enum": [ - 503 + 500 ], "type": "integer" }, "label": { "enum": [ - "queue-full" + "legalhold-internal", + "legalhold-illegal-op" ], "type": "string" }, @@ -23347,13 +32657,16 @@ } } }, - "description": "The delete queue is full; no further delete requests can be processed at the moment (label: `queue-full`)" + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" } }, - "summary": "Delete a team" - }, + "summary": "Put config for legalhold" + } + }, + "/teams/{tid}/features/limitedEventFanout": { "get": { - "description": " [internal route ID: \"get-team\"]\n\n", + "description": " [internal route ID: (\"get\", LimitedEventFanoutConfig)]\n\n", + "operationId": "get_LimitedEventFanoutConfig", "parameters": [ { "in": "path", @@ -23370,31 +32683,32 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Team" + "$ref": "#/components/schemas/LockableFeature_LimitedEventFanoutConfig_MTg3ODM0NzU0" } } }, "description": "" }, - "404": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-team" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -23411,58 +32725,27 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" - } - }, - "summary": "Get a team by ID" - }, - "put": { - "description": " [internal route ID: \"update-team\"]\n\n", - "parameters": [ - { - "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/TeamUpdateData" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Team updated" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "operation-denied", - "message": "Insufficient permissions (missing SetTeamData)" + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "operation-denied", - "no-team-member" + "no-team" ], "type": "string" }, @@ -23479,15 +32762,16 @@ } } }, - "description": "Insufficient permissions (missing SetTeamData) (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Update team properties" + "summary": "Get config for limitedEventFanout" } }, - "/teams/{tid}/conversations": { + "/teams/{tid}/features/meetings": { "get": { - "description": " [internal route ID: \"get-team-conversations\"]\n\n", + "description": " [internal route ID: (\"get\", MeetingsConfig)]\n\n", + "operationId": "get_MeetingsConfig", "parameters": [ { "in": "path", @@ -23504,7 +32788,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/TeamConversationList" + "$ref": "#/components/schemas/LockableFeature_MeetingsConfig_LTE0OTQ5Nzcw" } } }, @@ -23547,142 +32831,6 @@ } }, "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" - } - }, - "summary": "Get team conversations" - } - }, - "/teams/{tid}/conversations/roles": { - "get": { - "description": " [internal route ID: \"get-team-conversation-roles\"]\n\n", - "parameters": [ - { - "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ConversationRolesList" - } - } - }, - "description": "" - }, - "403": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" - }, - "properties": { - "code": { - "enum": [ - 403 - ], - "type": "integer" - }, - "label": { - "enum": [ - "no-team-member" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - } - }, - "description": "Requesting user is not a team member (label: `no-team-member`)" - } - }, - "summary": "Get existing roles available for the given team" - } - }, - "/teams/{tid}/conversations/{cid}": { - "delete": { - "description": " [internal route ID: \"delete-team-conversation\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", - "parameters": [ - { - "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "in": "path", - "name": "cid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Conversation deleted" - }, - "403": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" - }, - "properties": { - "code": { - "enum": [ - 403 - ], - "type": "integer" - }, - "label": { - "enum": [ - "no-team-member", - "invalid-op", - "action-denied" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - } - }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing delete_conversation) (label: `action-denied`)" }, "404": { "content": { @@ -23690,8 +32838,8 @@ "schema": { "example": { "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { @@ -23702,7 +32850,7 @@ }, "label": { "enum": [ - "no-conversation" + "no-team" ], "type": "string" }, @@ -23719,13 +32867,14 @@ } } }, - "description": "`tid` or `cid` not found\n\nConversation not found (label: `no-conversation`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Remove a team conversation" + "summary": "Get config for meetings" }, - "get": { - "description": " [internal route ID: \"get-team-conversation\"]\n\n", + "put": { + "description": " [internal route ID: (\"put\", MeetingsConfig)]\n\n", + "operationId": "put_MeetingsConfig", "parameters": [ { "in": "path", @@ -23735,23 +32884,24 @@ "format": "uuid", "type": "string" } - }, - { - "in": "path", - "name": "cid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_MeetingsConfig_NDc2MzM0MDE1" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/TeamConversation" + "$ref": "#/components/schemas/LockableFeature_MeetingsConfig_LTE0OTQ5Nzcw" } } }, @@ -23801,8 +32951,8 @@ "schema": { "example": { "code": 404, - "label": "no-conversation", - "message": "Conversation not found" + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { @@ -23813,7 +32963,7 @@ }, "label": { "enum": [ - "no-conversation" + "no-team" ], "type": "string" }, @@ -23830,15 +32980,16 @@ } } }, - "description": "`tid` or `cid` not found\n\nConversation not found (label: `no-conversation`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get one team conversation" + "summary": "Put config for meetings" } }, - "/teams/{tid}/features": { + "/teams/{tid}/features/mls": { "get": { - "description": " [internal route ID: \"get-all-feature-configs-for-team\"]\n\nGets feature configs for a team. User must be a member of the team and have permission to view team features.", + "description": " [internal route ID: (\"get\", MLSConfigB)]\n\n", + "operationId": "get_MLSConfigB", "parameters": [ { "in": "path", @@ -23855,7 +33006,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/AllTeamFeatures" + "$ref": "#/components/schemas/LockableFeature_MLSConfigB_Bare_Identity_MTg1MTc0NTEw" } } }, @@ -23867,8 +33018,8 @@ "schema": { "example": { "code": 403, - "label": "operation-denied", - "message": "Insufficient permissions" + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { @@ -23879,8 +33030,8 @@ }, "label": { "enum": [ - "operation-denied", - "no-team-member" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -23897,7 +33048,7 @@ } } }, - "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, "404": { "content": { @@ -23937,12 +33088,11 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Gets feature configs for a team" - } - }, - "/teams/{tid}/features/appLock": { - "get": { - "description": " [internal route ID: (\"get\", AppLockConfig)]\n\n", + "summary": "Get config for mls" + }, + "put": { + "description": " [internal route ID: (\"put\", MLSConfigB)]\n\n", + "operationId": "put_MLSConfigB", "parameters": [ { "in": "path", @@ -23954,12 +33104,22 @@ } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_MLSConfigB_Bare_Identity_LTI5MjA3MDYy" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/AppLockConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFeature_MLSConfigB_Bare_Identity_MTg1MTc0NTEw" } } }, @@ -24041,10 +33201,13 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get config for appLock" - }, - "put": { - "description": " [internal route ID: (\"put\", AppLockConfig)]\n\n", + "summary": "Put config for mls" + } + }, + "/teams/{tid}/features/mlsE2EId": { + "get": { + "description": " [internal route ID: (\"get\", MlsE2EIdConfigB)]\n\n", + "operationId": "get_MlsE2EIdConfigB", "parameters": [ { "in": "path", @@ -24056,22 +33219,12 @@ } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/AppLockConfig.Feature" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/AppLockConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFeature_MlsE2EIdConfigB_Bare_Identity_MTU2ODkyMDc4" } } }, @@ -24153,12 +33306,11 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Put config for appLock" - } - }, - "/teams/{tid}/features/classifiedDomains": { - "get": { - "description": " [internal route ID: (\"get\", ClassifiedDomainsConfig)]\n\n", + "summary": "Get config for mlsE2EId" + }, + "put": { + "description": " [internal route ID: (\"put\", MlsE2EIdConfigB)]\n\n", + "operationId": "put_MlsE2EIdConfigB", "parameters": [ { "in": "path", @@ -24170,12 +33322,22 @@ } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_MlsE2EIdConfigB_Bare_Identity_LTUxODYzODEx" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ClassifiedDomainsConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFeature_MlsE2EIdConfigB_Bare_Identity_MTU2ODkyMDc4" } } }, @@ -24257,12 +33419,13 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get config for classifiedDomains" + "summary": "Put config for mlsE2EId" } }, - "/teams/{tid}/features/conferenceCalling": { + "/teams/{tid}/features/mlsMigration": { "get": { - "description": " [internal route ID: (\"get\", ConferenceCallingConfig)]\n\n", + "description": " [internal route ID: (\"get\", MlsMigrationConfigB)]\n\n", + "operationId": "get_MlsMigrationConfigB", "parameters": [ { "in": "path", @@ -24279,7 +33442,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConferenceCallingConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFtur_MsignCfBIdy_LTE1NjAxNjU2" } } }, @@ -24361,10 +33524,11 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get config for conferenceCalling" + "summary": "Get config for mlsMigration" }, "put": { - "description": " [internal route ID: (\"put\", ConferenceCallingConfig)]\n\n", + "description": " [internal route ID: (\"put\", MlsMigrationConfigB)]\n\n", + "operationId": "put_MlsMigrationConfigB", "parameters": [ { "in": "path", @@ -24380,7 +33544,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConferenceCallingConfig.Feature" + "$ref": "#/components/schemas/Feature_MlsMigrationConfigB_Bare_Identity_LTQyMDAxMTkz" } } }, @@ -24391,7 +33555,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConferenceCallingConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFtur_MsignCfBIdy_LTE1NjAxNjU2" } } }, @@ -24473,12 +33637,13 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Put config for conferenceCalling" + "summary": "Put config for mlsMigration" } }, - "/teams/{tid}/features/conversationGuestLinks": { + "/teams/{tid}/features/outlookCalIntegration": { "get": { - "description": " [internal route ID: (\"get\", GuestLinksConfig)]\n\n", + "description": " [internal route ID: (\"get\", OutlookCalIntegrationConfig)]\n\n", + "operationId": "get_OutlookCalIntegrationConfig", "parameters": [ { "in": "path", @@ -24495,7 +33660,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFeature_OutlookCalIntegrationConfig_NjQ0MzMyMzY0" } } }, @@ -24577,10 +33742,11 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get config for conversationGuestLinks" + "summary": "Get config for outlookCalIntegration" }, "put": { - "description": " [internal route ID: (\"put\", GuestLinksConfig)]\n\n", + "description": " [internal route ID: (\"put\", OutlookCalIntegrationConfig)]\n\n", + "operationId": "put_OutlookCalIntegrationConfig", "parameters": [ { "in": "path", @@ -24596,7 +33762,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/GuestLinksConfig.Feature" + "$ref": "#/components/schemas/Feature_OutlookCalIntegrationConfig_LTg0MjIxNTMx" } } }, @@ -24607,7 +33773,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFeature_OutlookCalIntegrationConfig_NjQ0MzMyMzY0" } } }, @@ -24689,12 +33855,13 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Put config for conversationGuestLinks" + "summary": "Put config for outlookCalIntegration" } }, - "/teams/{tid}/features/digitalSignatures": { + "/teams/{tid}/features/preventAdminlessGroups": { "get": { - "description": " [internal route ID: (\"get\", DigitalSignaturesConfig)]\n\n", + "description": " [internal route ID: (\"get\", PreventAdminlessGroupsConfigB)]\n\n", + "operationId": "get_PreventAdminlessGroupsConfigB", "parameters": [ { "in": "path", @@ -24711,7 +33878,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/DigitalSignaturesConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFtur_PvnAdmisGpCfgBIy_MTQxOTU4Mzgw" } } }, @@ -24793,12 +33960,11 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get config for digitalSignatures" - } - }, - "/teams/{tid}/features/enforceFileDownloadLocation": { - "get": { - "description": " [internal route ID: (\"get\", EnforceFileDownloadLocationConfig)]\n\n

Custom feature: only supported on some dedicated on-prem systems.

", + "summary": "Get config for preventAdminlessGroups" + }, + "put": { + "description": " [internal route ID: \"put-PreventAdminlessGroupsConfig@v17\"]\n\n

For API version 17, use duration strings for the timeout fields. The request body must have the following shape:

{\n  "config": {\n    "deletionTimeoutDuration": "7d",\n    "promotionStrategy": "alphabetical",\n    "reminderTimeoutDurations": [\n      "2d",\n      "4d",\n      "6d"\n    ]\n  },\n  "status": "enabled"\n}

Older API versions use the legacy numeric fields deletionTimeout and reminderTimeouts.

", + "operationId": "put-PreventAdminlessGroupsConfig@v17", "parameters": [ { "in": "path", @@ -24810,12 +33976,22 @@ } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Featur_Vsiond_17PvAmlGpCfgBIy_LTIzMjQ4MDk1V17" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/EnforceFileDownloadLocation.LockableFeature" + "$ref": "#/components/schemas/LockableFtur_PvnAdmisGpCfgBIy_MTQxOTU4Mzgw" } } }, @@ -24897,10 +34073,13 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get config for enforceFileDownloadLocation" - }, - "put": { - "description": " [internal route ID: (\"put\", EnforceFileDownloadLocationConfig)]\n\n

Custom feature: only supported on some dedicated on-prem systems.

", + "summary": "Put config for preventAdminlessGroups" + } + }, + "/teams/{tid}/features/searchVisibility": { + "get": { + "description": " [internal route ID: (\"get\", SearchVisibilityAvailableConfig)]\n\n", + "operationId": "get_SearchVisibilityAvailableConfig", "parameters": [ { "in": "path", @@ -24912,22 +34091,12 @@ } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/EnforceFileDownloadLocation.Feature" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/EnforceFileDownloadLocation.LockableFeature" + "$ref": "#/components/schemas/LockableFeature_SearchVisibilityAvailableConfig_LTkxMTA5ODk5" } } }, @@ -25009,12 +34178,11 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Put config for enforceFileDownloadLocation" - } - }, - "/teams/{tid}/features/exposeInvitationURLsToTeamAdmin": { - "get": { - "description": " [internal route ID: (\"get\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "summary": "Get config for searchVisibility" + }, + "put": { + "description": " [internal route ID: (\"put\", SearchVisibilityAvailableConfig)]\n\n", + "operationId": "put_SearchVisibilityAvailableConfig", "parameters": [ { "in": "path", @@ -25026,12 +34194,22 @@ } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_SearchVisibilityAvailableConfig_LTMzNTkxODI1" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFeature_SearchVisibilityAvailableConfig_LTkxMTA5ODk5" } } }, @@ -25113,10 +34291,13 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get config for exposeInvitationURLsToTeamAdmin" - }, - "put": { - "description": " [internal route ID: (\"put\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "summary": "Put config for searchVisibility" + } + }, + "/teams/{tid}/features/searchVisibilityInbound": { + "get": { + "description": " [internal route ID: (\"get\", SearchVisibilityInboundConfig)]\n\n", + "operationId": "get_SearchVisibilityInboundConfig", "parameters": [ { "in": "path", @@ -25128,22 +34309,12 @@ } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.Feature" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFeature_SearchVisibilityInboundConfig_NzA5NzczNTgy" } } }, @@ -25225,12 +34396,11 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Put config for exposeInvitationURLsToTeamAdmin" - } - }, - "/teams/{tid}/features/fileSharing": { - "get": { - "description": " [internal route ID: (\"get\", FileSharingConfig)]\n\n", + "summary": "Get config for searchVisibilityInbound" + }, + "put": { + "description": " [internal route ID: (\"put\", SearchVisibilityInboundConfig)]\n\n", + "operationId": "put_SearchVisibilityInboundConfig", "parameters": [ { "in": "path", @@ -25242,12 +34412,22 @@ } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_SearchVisibilityInboundConfig_MTI1NzQxODY2" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/FileSharingConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFeature_SearchVisibilityInboundConfig_NzA5NzczNTgy" } } }, @@ -25329,10 +34509,13 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get config for fileSharing" - }, - "put": { - "description": " [internal route ID: (\"put\", FileSharingConfig)]\n\n", + "summary": "Put config for searchVisibilityInbound" + } + }, + "/teams/{tid}/features/selfDeletingMessages": { + "get": { + "description": " [internal route ID: (\"get\", SelfDeletingMessagesConfigB)]\n\n", + "operationId": "get_SelfDeletingMessagesConfigB", "parameters": [ { "in": "path", @@ -25344,22 +34527,12 @@ } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/FileSharingConfig.Feature" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/FileSharingConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFtur_SfDingMsCBIdy_LTg5MTEwNTA2" } } }, @@ -25441,12 +34614,11 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Put config for fileSharing" - } - }, - "/teams/{tid}/features/legalhold": { - "get": { - "description": " [internal route ID: (\"get\", LegalholdConfig)]\n\n", + "summary": "Get config for selfDeletingMessages" + }, + "put": { + "description": " [internal route ID: (\"put\", SelfDeletingMessagesConfigB)]\n\n", + "operationId": "put_SelfDeletingMessagesConfigB", "parameters": [ { "in": "path", @@ -25458,12 +34630,22 @@ } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Featur_SlfDingMsCoBIdy_LTg2MzYzNjc2" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/LegalholdConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFtur_SfDingMsCBIdy_LTg5MTEwNTA2" } } }, @@ -25545,10 +34727,13 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get config for legalhold" - }, - "put": { - "description": " [internal route ID: (\"put\", LegalholdConfig)]\n\n", + "summary": "Put config for selfDeletingMessages" + } + }, + "/teams/{tid}/features/simplifiedUserConnectionRequestQRCode": { + "get": { + "description": " [internal route ID: (\"get\", SimplifiedUserConnectionRequestQRCodeConfig)]\n\n", + "operationId": "get_SimplifiedUserConnectionRequestQRCodeConfig", "parameters": [ { "in": "path", @@ -25560,46 +34745,37 @@ } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/LegalholdConfig.Feature" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/LegalholdConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFtur_SimpfdUsCnRqQg_Njk5NjU4OTgy" } } }, "description": "" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "legalhold-not-registered", - "message": "legal hold service has not been registered for this team" + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "legalhold-not-registered" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -25616,32 +34792,27 @@ } } }, - "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "legalhold-disable-unimplemented", - "message": "legal hold cannot be disabled for whitelisted teams" + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "legalhold-disable-unimplemented", - "legalhold-not-enabled", - "too-large-team-for-legalhold", - "action-denied", - "no-team-member", - "operation-denied" + "no-team" ], "type": "string" }, @@ -25658,27 +34829,58 @@ } } }, - "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nCannot enable legalhold on large teams (reason: for removing LH from team, we need to iterate over all members, which is only supported for teams with less than 2k members) (label: `too-large-team-for-legalhold`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for simplifiedUserConnectionRequestQRCode" + } + }, + "/teams/{tid}/features/sndFactorPasswordChallenge": { + "get": { + "description": " [internal route ID: (\"get\", SndFactorPasswordChallengeConfig)]\n\n", + "operationId": "get_SndFactorPasswordChallengeConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_SndFactorPasswordChallengeConfig_MjQ3NzQ2ODgx" + } + } + }, + "description": "" }, - "404": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-team" + "no-team-member", + "operation-denied" ], "type": "string" }, @@ -25695,28 +34897,27 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, - "500": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 500, - "label": "legalhold-internal", - "message": "legal hold service: could not block connections when resolving policy conflicts." + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 500 + 404 ], "type": "integer" }, "label": { "enum": [ - "legalhold-internal", - "legalhold-illegal-op" + "no-team" ], "type": "string" }, @@ -25733,15 +34934,14 @@ } } }, - "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Put config for legalhold" - } - }, - "/teams/{tid}/features/limitedEventFanout": { - "get": { - "description": " [internal route ID: (\"get\", LimitedEventFanoutConfig)]\n\n", + "summary": "Get config for sndFactorPasswordChallenge" + }, + "put": { + "description": " [internal route ID: (\"put\", SndFactorPasswordChallengeConfig)]\n\n", + "operationId": "put_SndFactorPasswordChallengeConfig", "parameters": [ { "in": "path", @@ -25753,12 +34953,22 @@ } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_SndFactorPasswordChallengeConfig_NDc0MzUyMzQz" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/LimitedEventFanoutConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFeature_SndFactorPasswordChallengeConfig_MjQ3NzQ2ODgx" } } }, @@ -25840,12 +35050,13 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get config for limitedEventFanout" + "summary": "Put config for sndFactorPasswordChallenge" } }, - "/teams/{tid}/features/mls": { + "/teams/{tid}/features/sso": { "get": { - "description": " [internal route ID: (\"get\", MLSConfig)]\n\n", + "description": " [internal route ID: (\"get\", SSOConfig)]\n\n", + "operationId": "get_SSOConfig", "parameters": [ { "in": "path", @@ -25862,7 +35073,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MLSConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFeature_SSOConfig_NjcyMjU4MDY2" } } }, @@ -25944,10 +35155,13 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get config for mls" - }, - "put": { - "description": " [internal route ID: (\"put\", MLSConfig)]\n\n", + "summary": "Get config for sso" + } + }, + "/teams/{tid}/features/stealthUsers": { + "get": { + "description": " [internal route ID: (\"get\", StealthUsersConfig)]\n\n", + "operationId": "get_StealthUsersConfig", "parameters": [ { "in": "path", @@ -25959,22 +35173,12 @@ } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/MLSConfig.Feature" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MLSConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFeature_StealthUsersConfig_LTE1MTk2NzIz" } } }, @@ -26056,12 +35260,13 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Put config for mls" + "summary": "Get config for stealthUsers" } }, - "/teams/{tid}/features/mlsE2EId": { + "/teams/{tid}/features/validateSAMLemails": { "get": { - "description": " [internal route ID: (\"get\", MlsE2EIdConfig)]\n\n", + "description": " [internal route ID: (\"get\", RequireExternalEmailVerificationConfig)]\n\n

Controls whether externally managed email addresses (from SAML or SCIM) must be verified by the user, or are auto-activated.

The external feature name is kept as validateSAMLemails for backward compatibility. That name is misleading because the feature also applies to SCIM-managed users, and it controls email ownership verification rather than generic email validation.

", + "operationId": "get_RequireExternalEmailVerificationConfig", "parameters": [ { "in": "path", @@ -26078,7 +35283,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MlsE2EIdConfig.LockableFeature" + "$ref": "#/components/schemas/LockableFtur_RqiExnmVfCg_NjUyMzgzNzY5" } } }, @@ -26160,10 +35365,13 @@ "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get config for mlsE2EId" - }, - "put": { - "description": " [internal route ID: (\"put\", MlsE2EIdConfig)]\n\n", + "summary": "Get config for validateSAMLemails" + } + }, + "/teams/{tid}/get-members-by-ids-using-post": { + "post": { + "description": " [internal route ID: \"get-team-members-by-ids\"]\n\nThe `has_more` field in the response body is always `false`.", + "operationId": "get-team-members-by-ids", "parameters": [ { "in": "path", @@ -26173,13 +35381,25 @@ "format": "uuid", "type": "string" } + }, + { + "description": "Maximum results to be returned", + "in": "query", + "name": "maxResults", + "required": false, + "schema": { + "format": "int32", + "maximum": 2000, + "minimum": 1, + "type": "integer" + } } ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MlsE2EIdConfig.Feature" + "$ref": "#/components/schemas/UserIdList_MzA1MTI1Njgx" } } }, @@ -26190,32 +35410,31 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MlsE2EIdConfig.LockableFeature" + "$ref": "#/components/schemas/TeamMemberList_Optional_LTM1ODE2MzM0" } } }, "description": "" }, - "403": { + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "code": 400, + "label": "too-many-uids", + "message": "Can only process 2000 user ids per request." }, "properties": { "code": { "enum": [ - 403 + 400 ], "type": "integer" }, "label": { "enum": [ - "no-team-member", - "operation-denied" + "too-many-uids" ], "type": "string" }, @@ -26232,27 +35451,27 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + "description": "Invalid `body` or `maxResults`\n\nCan only process 2000 user ids per request. (label: `too-many-uids`)" }, - "404": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-team" + "no-team-member" ], "type": "string" }, @@ -26269,15 +35488,16 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + "description": "Requesting user is not a team member (label: `no-team-member`)" } }, - "summary": "Put config for mlsE2EId" + "summary": "Get team members by user id list" } }, - "/teams/{tid}/features/mlsMigration": { + "/teams/{tid}/invitations": { "get": { - "description": " [internal route ID: (\"get\", MlsMigrationConfig)]\n\n", + "description": " [internal route ID: \"get-team-invitations\"]\n\n", + "operationId": "get-team-invitations", "parameters": [ { "in": "path", @@ -26287,18 +35507,45 @@ "format": "uuid", "type": "string" } + }, + { + "description": "Invitation id to start from (ascending).", + "in": "query", + "name": "start", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Number of results to return (default 100, max 500).", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } } ], "responses": { "200": { "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationList_ODk4NTQxODc3" + } + }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MlsMigration.LockableFeature" + "$ref": "#/components/schemas/InvitationList_ODk4NTQxODc3" } } }, - "description": "" + "description": "List of sent invitations" }, "403": { "content": { @@ -26306,8 +35553,8 @@ "schema": { "example": { "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "label": "insufficient-permissions", + "message": "Insufficient team permissions" }, "properties": { "code": { @@ -26318,45 +35565,7 @@ }, "label": { "enum": [ - "no-team-member", - "operation-denied" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - } - }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" - }, - "404": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" - }, - "properties": { - "code": { - "enum": [ - 404 - ], - "type": "integer" - }, - "label": { - "enum": [ - "no-team" + "insufficient-permissions" ], "type": "string" }, @@ -26373,13 +35582,14 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + "description": "Insufficient team permissions (label: `insufficient-permissions`)" } }, - "summary": "Get config for mlsMigration" + "summary": "List the sent team invitations" }, - "put": { - "description": " [internal route ID: (\"put\", MlsMigrationConfig)]\n\n", + "post": { + "description": " [internal route ID: \"send-team-invitation\"]\n\nInvitations are sent by email. The maximum allowed number of pending team invitations is equal to the team size.", + "operationId": "send-team-invitation", "parameters": [ { "in": "path", @@ -26395,22 +35605,73 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MlsMigration.Feature" + "$ref": "#/components/schemas/InvitationRequest_LTcyMDIzNDc0" } } }, "required": true }, "responses": { - "200": { + "201": { "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invitation_NTkzMDYwODc1" + } + }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MlsMigration.LockableFeature" + "$ref": "#/components/schemas/Invitation_NTkzMDYwODc1" } } }, - "description": "" + "description": "Invitation was created and sent.", + "headers": { + "Location": { + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code", + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid invitation code. (label: `invalid-invitation-code`)\n\nInvalid e-mail address. (label: `invalid-email`)" }, "403": { "content": { @@ -26418,8 +35679,8 @@ "schema": { "example": { "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "label": "insufficient-permissions", + "message": "Insufficient team permissions" }, "properties": { "code": { @@ -26430,8 +35691,11 @@ }, "label": { "enum": [ - "no-team-member", - "operation-denied" + "insufficient-permissions", + "too-many-team-invitations", + "blacklisted-email", + "no-identity", + "no-email" ], "type": "string" }, @@ -26448,27 +35712,59 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + "description": "Insufficient team permissions (label: `insufficient-permissions`)\n\nToo many team invitations for this team (label: `too-many-team-invitations`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nThe user has no verified email (label: `no-identity`)\n\nThis operation requires the user to have a verified email address. (label: `no-email`)" + } + }, + "summary": "Create and send a new team invitation." + } + }, + "/teams/{tid}/invitations/{iid}": { + "delete": { + "description": " [internal route ID: \"delete-team-invitation\"]\n\n", + "operationId": "delete-team-invitation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } }, - "404": { + { + "in": "path", + "name": "iid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Invitation deleted" + }, + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-team" + "insufficient-permissions" ], "type": "string" }, @@ -26485,15 +35781,14 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + "description": "Insufficient team permissions (label: `insufficient-permissions`)" } }, - "summary": "Put config for mlsMigration" - } - }, - "/teams/{tid}/features/outlookCalIntegration": { + "summary": "Delete a pending team invitation by ID." + }, "get": { - "description": " [internal route ID: (\"get\", OutlookCalIntegrationConfig)]\n\n", + "description": " [internal route ID: \"get-team-invitation\"]\n\n", + "operationId": "get-team-invitation", "parameters": [ { "in": "path", @@ -26503,18 +35798,32 @@ "format": "uuid", "type": "string" } + }, + { + "in": "path", + "name": "iid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } } ], "responses": { "200": { "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invitation_NTkzMDYwODc1" + } + }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/OutlookCalIntegrationConfig.LockableFeature" + "$ref": "#/components/schemas/Invitation_NTkzMDYwODc1" } } }, - "description": "" + "description": "Invitation" }, "403": { "content": { @@ -26522,8 +35831,8 @@ "schema": { "example": { "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "label": "insufficient-permissions", + "message": "Insufficient team permissions" }, "properties": { "code": { @@ -26534,8 +35843,7 @@ }, "label": { "enum": [ - "no-team-member", - "operation-denied" + "insufficient-permissions" ], "type": "string" }, @@ -26552,16 +35860,16 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + "description": "Insufficient team permissions (label: `insufficient-permissions`)" }, "404": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { "example": { "code": 404, - "label": "no-team", - "message": "Team not found" + "label": "not-found", + "message": "Notification not found." }, "properties": { "code": { @@ -26572,7 +35880,7 @@ }, "label": { "enum": [ - "no-team" + "not-found" ], "type": "string" }, @@ -26587,67 +35895,24 @@ ], "type": "object" } - } - }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" - } - }, - "summary": "Get config for outlookCalIntegration" - }, - "put": { - "description": " [internal route ID: (\"put\", OutlookCalIntegrationConfig)]\n\n", - "parameters": [ - { - "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/OutlookCalIntegrationConfig.Feature" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/OutlookCalIntegrationConfig.LockableFeature" - } - } - }, - "description": "" - }, - "403": { - "content": { + }, "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "code": 404, + "label": "not-found", + "message": "Notification not found." }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { - "enum": [ - "no-team-member", - "operation-denied" + "enum": [ + "not-found" ], "type": "string" }, @@ -26664,27 +35929,27 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + "description": "`tid` or `iid` or Notification not found. (label: `not-found`)" }, - "404": { + "409": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" + "code": 409, + "label": "duplicate-entry", + "message": "Entry already exists" }, "properties": { "code": { "enum": [ - 404 + 409 ], "type": "integer" }, "label": { "enum": [ - "no-team" + "duplicate-entry" ], "type": "string" }, @@ -26701,15 +35966,16 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + "description": "Entry already exists (label: `duplicate-entry`)" } }, - "summary": "Put config for outlookCalIntegration" + "summary": "Get a pending team invitation by ID." } }, - "/teams/{tid}/features/searchVisibility": { - "get": { - "description": " [internal route ID: (\"get\", SearchVisibilityAvailableConfig)]\n\n", + "/teams/{tid}/legalhold/consent": { + "post": { + "description": " [internal route ID: \"consent-to-legal-hold\"]\n\n", + "operationId": "consent-to-legal-hold", "parameters": [ { "in": "path", @@ -26722,36 +35988,69 @@ } ], "responses": { - "200": { + "201": { + "description": "Grant consent successful" + }, + "204": { + "description": "Consent already granted" + }, + "403": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.LockableFeature" + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "" + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, + "code": 404, "label": "no-team-member", - "message": "Requesting user is not a team member" + "message": "Team member not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "no-team-member", - "operation-denied" + "no-team-member" ], "type": "string" }, @@ -26768,27 +36067,28 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + "description": "`tid` not found\n\nTeam member not found (label: `no-team-member`)" }, - "404": { + "500": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." }, "properties": { "code": { "enum": [ - 404 + 500 ], "type": "integer" }, "label": { "enum": [ - "no-team" + "legalhold-internal", + "legalhold-illegal-op" ], "type": "string" }, @@ -26805,13 +36105,16 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" } }, - "summary": "Get config for searchVisibility" - }, - "put": { - "description": " [internal route ID: (\"put\", SearchVisibilityAvailableConfig)]\n\n", + "summary": "Consent to legal hold" + } + }, + "/teams/{tid}/legalhold/settings": { + "delete": { + "description": " [internal route ID: \"delete-legal-hold-settings\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientRemoved event to members with a legalhold client (via brig)\n- UserLegalHoldDisabled event to contacts of members with a legalhold client (via brig)", + "operationId": "delete-legal-hold-settings", "parameters": [ { "in": "path", @@ -26827,43 +36130,35 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.Feature" + "$ref": "#/components/schemas/RemoveLegalHoldSettingsRequest_NzMzMzMyMDAz" } } }, "required": true }, "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.LockableFeature" - } - } - }, - "description": "" + "204": { + "description": "Legal hold service settings deleted" }, - "403": { + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" }, "properties": { "code": { "enum": [ - 403 + 400 ], "type": "integer" }, "label": { "enum": [ - "no-team-member", - "operation-denied" + "legalhold-not-registered" ], "type": "string" }, @@ -26880,27 +36175,35 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" }, - "404": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" + "code": 403, + "label": "legalhold-disable-unimplemented", + "message": "legal hold cannot be disabled for whitelisted teams" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-team" + "legalhold-disable-unimplemented", + "legalhold-not-enabled", + "invalid-op", + "action-denied", + "no-team-member", + "operation-denied", + "code-authentication-required", + "code-authentication-failed", + "access-denied" ], "type": "string" }, @@ -26917,57 +36220,27 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" - } - }, - "summary": "Put config for searchVisibility" - } - }, - "/teams/{tid}/features/searchVisibilityInbound": { - "get": { - "description": " [internal route ID: (\"get\", SearchVisibilityInboundConfig)]\n\n", - "parameters": [ - { - "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/SearchVisibilityInboundConfig.LockableFeature" - } - } - }, - "description": "" + "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" }, - "403": { + "429": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." }, "properties": { "code": { "enum": [ - 403 + 429 ], "type": "integer" }, "label": { "enum": [ - "no-team-member", - "operation-denied" + "too-many-requests" ], "type": "string" }, @@ -26984,27 +36257,28 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + "description": "Please try again later. (label: `too-many-requests`)" }, - "404": { + "500": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." }, "properties": { "code": { "enum": [ - 404 + 500 ], "type": "integer" }, "label": { "enum": [ - "no-team" + "legalhold-internal", + "legalhold-illegal-op" ], "type": "string" }, @@ -27021,13 +36295,14 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" } }, - "summary": "Get config for searchVisibilityInbound" + "summary": "Delete legal hold service settings" }, - "put": { - "description": " [internal route ID: (\"put\", SearchVisibilityInboundConfig)]\n\n", + "get": { + "description": " [internal route ID: \"get-legal-hold-settings\"]\n\n", + "operationId": "get-legal-hold-settings", "parameters": [ { "in": "path", @@ -27039,22 +36314,12 @@ } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/SearchVisibilityInboundConfig.Feature" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SearchVisibilityInboundConfig.LockableFeature" + "$ref": "#/components/schemas/ViewLegalHoldService_LTE3MzQzNDkw" } } }, @@ -27066,8 +36331,8 @@ "schema": { "example": { "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "label": "operation-denied", + "message": "Insufficient permissions" }, "properties": { "code": { @@ -27078,45 +36343,8 @@ }, "label": { "enum": [ - "no-team-member", - "operation-denied" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - } - }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" - }, - "404": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" - }, - "properties": { - "code": { - "enum": [ - 404 - ], - "type": "integer" - }, - "label": { - "enum": [ - "no-team" + "operation-denied", + "no-team-member" ], "type": "string" }, @@ -27133,15 +36361,14 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" } }, - "summary": "Put config for searchVisibilityInbound" - } - }, - "/teams/{tid}/features/selfDeletingMessages": { - "get": { - "description": " [internal route ID: (\"get\", SelfDeletingMessagesConfig)]\n\n", + "summary": "Get legal hold service settings" + }, + "post": { + "description": " [internal route ID: \"create-legal-hold-settings\"]\n\n", + "operationId": "create-legal-hold-settings", "parameters": [ { "in": "path", @@ -27153,37 +36380,52 @@ } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewLegalHoldService_Mzg0ODQ5NDU1" + } + } + }, + "required": true + }, "responses": { - "200": { + "201": { "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewLegalHoldService_LTE3MzQzNDkw" + } + }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SelfDeletingMessagesConfig.LockableFeature" + "$ref": "#/components/schemas/ViewLegalHoldService_LTE3MzQzNDkw" } } }, - "description": "" + "description": "Legal hold service settings created" }, - "403": { + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "code": 400, + "label": "legalhold-status-bad", + "message": "legal hold service: invalid response" }, "properties": { "code": { "enum": [ - 403 + 400 ], "type": "integer" }, "label": { "enum": [ - "no-team-member", - "operation-denied" + "legalhold-status-bad", + "legalhold-invalid-key" ], "type": "string" }, @@ -27200,27 +36442,29 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + "description": "Invalid `body`\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)\n\nlegal hold service pubkey is invalid (label: `legalhold-invalid-key`)" }, - "404": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-team" + "legalhold-not-enabled", + "operation-denied", + "no-team-member" ], "type": "string" }, @@ -27237,13 +36481,16 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" } }, - "summary": "Get config for selfDeletingMessages" - }, - "put": { - "description": " [internal route ID: (\"put\", SelfDeletingMessagesConfig)]\n\n", + "summary": "Create legal hold service settings" + } + }, + "/teams/{tid}/legalhold/{uid}": { + "delete": { + "description": " [internal route ID: \"disable-legal-hold-for-user\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientRemoved event to the user owning the client (via brig)\n- UserLegalHoldDisabled event to contacts of the user owning the client (via brig)", + "operationId": "disable-legal-hold-for-user", "parameters": [ { "in": "path", @@ -27253,13 +36500,22 @@ "format": "uuid", "type": "string" } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } } ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SelfDeletingMessagesConfig.Feature" + "$ref": "#/components/schemas/DisableLegalHoldForUserRequest_LTYyMDYxOTEy" } } }, @@ -27267,35 +36523,30 @@ }, "responses": { "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/SelfDeletingMessagesConfig.LockableFeature" - } - } - }, - "description": "" + "description": "Disable legal hold successful" }, - "403": { + "204": { + "description": "Legal hold was not enabled" + }, + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" }, "properties": { "code": { "enum": [ - 403 + 400 ], "type": "integer" }, "label": { "enum": [ - "no-team-member", - "operation-denied" + "legalhold-not-registered" ], "type": "string" }, @@ -27312,27 +36563,32 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" }, - "404": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-team" + "operation-denied", + "no-team-member", + "action-denied", + "code-authentication-required", + "code-authentication-failed", + "access-denied" ], "type": "string" }, @@ -27349,57 +36605,27 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" - } - }, - "summary": "Put config for selfDeletingMessages" - } - }, - "/teams/{tid}/features/sndFactorPasswordChallenge": { - "get": { - "description": " [internal route ID: (\"get\", SndFactorPasswordChallengeConfig)]\n\n", - "parameters": [ - { - "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.LockableFeature" - } - } - }, - "description": "" + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" }, - "403": { + "429": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." }, "properties": { "code": { "enum": [ - 403 + 429 ], "type": "integer" }, "label": { "enum": [ - "no-team-member", - "operation-denied" + "too-many-requests" ], "type": "string" }, @@ -27416,27 +36642,28 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + "description": "Please try again later. (label: `too-many-requests`)" }, - "404": { + "500": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." }, "properties": { "code": { "enum": [ - 404 + 500 ], "type": "integer" }, "label": { "enum": [ - "no-team" + "legalhold-internal", + "legalhold-illegal-op" ], "type": "string" }, @@ -27453,13 +36680,14 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" } }, - "summary": "Get config for sndFactorPasswordChallenge" + "summary": "Disable legal hold for user" }, - "put": { - "description": " [internal route ID: (\"put\", SndFactorPasswordChallengeConfig)]\n\n", + "get": { + "description": " [internal route ID: \"get-legal-hold\"]\n\n", + "operationId": "get-legal-hold", "parameters": [ { "in": "path", @@ -27469,49 +36697,47 @@ "format": "uuid", "type": "string" } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.Feature" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.LockableFeature" + "$ref": "#/components/schemas/UserLegalHoldStatusResponse_LTQ1MzUxMTE3" } } }, "description": "" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, + "code": 404, "label": "no-team-member", - "message": "Requesting user is not a team member" + "message": "Team member not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "no-team-member", - "operation-denied" + "no-team-member" ], "type": "string" }, @@ -27528,27 +36754,61 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + } + }, + "summary": "Get legal hold status" + }, + "post": { + "description": " [internal route ID: \"request-legal-hold-device\"]\n\nThis endpoint can lead to the following events being sent:\n- LegalHoldClientRequested event to contacts of the user the device is requested for, if they didn't already have a legalhold client (via brig)", + "operationId": "request-legal-hold-device", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } }, - "404": { + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Request device successful" + }, + "204": { + "description": "Request device already pending" + }, + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" }, "properties": { "code": { "enum": [ - 404 + 400 ], "type": "integer" }, "label": { "enum": [ - "no-team" + "legalhold-not-registered", + "legalhold-status-bad" ], "type": "string" }, @@ -27565,36 +36825,7 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" - } - }, - "summary": "Put config for sndFactorPasswordChallenge" - } - }, - "/teams/{tid}/features/sso": { - "get": { - "description": " [internal route ID: (\"get\", SSOConfig)]\n\n", - "parameters": [ - { - "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/SSOConfig.LockableFeature" - } - } - }, - "description": "" + "description": "legal hold service has not been registered for this team (label: `legalhold-not-registered`)\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)" }, "403": { "content": { @@ -27602,8 +36833,8 @@ "schema": { "example": { "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" }, "properties": { "code": { @@ -27614,8 +36845,10 @@ }, "label": { "enum": [ + "legalhold-not-enabled", + "operation-denied", "no-team-member", - "operation-denied" + "action-denied" ], "type": "string" }, @@ -27632,7 +36865,7 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" }, "404": { "content": { @@ -27640,8 +36873,8 @@ "schema": { "example": { "code": 404, - "label": "no-team", - "message": "Team not found" + "label": "no-team-member", + "message": "Team member not found" }, "properties": { "code": { @@ -27652,7 +36885,7 @@ }, "label": { "enum": [ - "no-team" + "no-team-member" ], "type": "string" }, @@ -27669,57 +36902,29 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" - } - }, - "summary": "Get config for sso" - } - }, - "/teams/{tid}/features/validateSAMLemails": { - "get": { - "description": " [internal route ID: (\"get\", ValidateSAMLEmailsConfig)]\n\n", - "parameters": [ - { - "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ValidateSAMLEmailsConfig.LockableFeature" - } - } - }, - "description": "" + "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" }, - "403": { + "409": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "code": 409, + "label": "mls-legal-hold-not-allowed", + "message": "A user who is under legal-hold may not participate in MLS conversations" }, "properties": { "code": { "enum": [ - 403 + 409 ], "type": "integer" }, "label": { "enum": [ - "no-team-member", - "operation-denied" + "mls-legal-hold-not-allowed", + "legalhold-no-consent", + "legalhold-already-enabled" ], "type": "string" }, @@ -27736,27 +36941,28 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + "description": "A user who is under legal-hold may not participate in MLS conversations (label: `mls-legal-hold-not-allowed`)\n\nuser has not given consent to using legal hold (label: `legalhold-no-consent`)\n\nlegal hold is already enabled for this user (label: `legalhold-already-enabled`)" }, - "404": { + "500": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team", - "message": "Team not found" + "code": 500, + "label": "legalhold-illegal-op", + "message": "internal server error: inconsistent change of user's legalhold state" }, "properties": { "code": { "enum": [ - 404 + 500 ], "type": "integer" }, "label": { "enum": [ - "no-team" + "legalhold-illegal-op", + "legalhold-internal" ], "type": "string" }, @@ -27773,15 +36979,16 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + "description": "internal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)\n\nlegal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)" } }, - "summary": "Get config for validateSAMLemails" + "summary": "Request legal hold device" } }, - "/teams/{tid}/get-members-by-ids-using-post": { - "post": { - "description": " [internal route ID: \"get-team-members-by-ids\"]\n\nThe `has_more` field in the response body is always `false`.", + "/teams/{tid}/legalhold/{uid}/approve": { + "put": { + "description": " [internal route ID: \"approve-legal-hold-device\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientAdded event to the user owning the client (via brig)\n- UserLegalHoldEnabled event to contacts of the user owning the client (via brig)\n- ClientRemoved event to the user, if removing old client due to max number (via brig)", + "operationId": "approve-legal-hold-device", "parameters": [ { "in": "path", @@ -27793,15 +37000,12 @@ } }, { - "description": "Maximum results to be returned", - "in": "query", - "name": "maxResults", - "required": false, + "in": "path", + "name": "uid", + "required": true, "schema": { - "format": "int32", - "maximum": 2000, - "minimum": 1, - "type": "integer" + "format": "uuid", + "type": "string" } } ], @@ -27809,7 +37013,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/UserIdList" + "$ref": "#/components/schemas/ApproveLegalHoldForUserRequest_NjEyNzYyMTIx" } } }, @@ -27817,14 +37021,7 @@ }, "responses": { "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/TeamMemberList" - } - } - }, - "description": "" + "description": "Legal hold approved" }, "400": { "content": { @@ -27832,8 +37029,8 @@ "schema": { "example": { "code": 400, - "label": "too-many-uids", - "message": "Can only process 2000 user ids per request." + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" }, "properties": { "code": { @@ -27844,7 +37041,7 @@ }, "label": { "enum": [ - "too-many-uids" + "legalhold-not-registered" ], "type": "string" }, @@ -27861,7 +37058,7 @@ } } }, - "description": "Invalid `body` or `maxResults`\n\nCan only process 2000 user ids per request. (label: `too-many-uids`)" + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" }, "403": { "content": { @@ -27869,8 +37066,8 @@ "schema": { "example": { "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" }, "properties": { "code": { @@ -27881,7 +37078,12 @@ }, "label": { "enum": [ - "no-team-member" + "legalhold-not-enabled", + "no-team-member", + "action-denied", + "access-denied", + "code-authentication-required", + "code-authentication-failed" ], "type": "string" }, @@ -27898,83 +37100,27 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)" - } - }, - "summary": "Get team members by user id list" - } - }, - "/teams/{tid}/invitations": { - "get": { - "description": " [internal route ID: \"get-team-invitations\"]\n\n", - "parameters": [ - { - "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "description": "Invitation id to start from (ascending).", - "in": "query", - "name": "start", - "required": false, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "description": "Number of results to return (default 100, max 500).", - "in": "query", - "name": "size", - "required": false, - "schema": { - "format": "int32", - "maximum": 500, - "minimum": 1, - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvitationList" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/InvitationList" - } - } - }, - "description": "List of sent invitations" + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "insufficient-permissions", - "message": "Insufficient team permissions" + "code": 404, + "label": "legalhold-no-device-allocated", + "message": "no legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow." }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "insufficient-permissions" + "legalhold-no-device-allocated" ], "type": "string" }, @@ -27991,77 +37137,64 @@ } } }, - "description": "Insufficient team permissions (label: `insufficient-permissions`)" - } - }, - "summary": "List the sent team invitations" - }, - "post": { - "description": " [internal route ID: \"send-team-invitation\"]\n\nInvitations are sent by email. The maximum allowed number of pending team invitations is equal to the team size.", - "parameters": [ - { - "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/InvitationRequest" - } - } + "description": "`tid` or `uid` not found\n\nno legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow. (label: `legalhold-no-device-allocated`)" }, - "required": true - }, - "responses": { - "201": { + "409": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Invitation" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Invitation" + "example": { + "code": 409, + "label": "legalhold-already-enabled", + "message": "legal hold is already enabled for this user" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-already-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "Invitation was created and sent.", - "headers": { - "Location": { - "schema": { - "format": "url", - "type": "string" - } - } - } + "description": "legal hold is already enabled for this user (label: `legalhold-already-enabled`)" }, - "400": { + "412": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-email", - "message": "Invalid e-mail address." + "code": 412, + "label": "legalhold-not-pending", + "message": "legal hold cannot be approved without being in a pending state" }, "properties": { "code": { "enum": [ - 400 + 412 ], "type": "integer" }, "label": { "enum": [ - "invalid-email" + "legalhold-not-pending" ], "type": "string" }, @@ -28078,31 +37211,27 @@ } } }, - "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + "description": "legal hold cannot be approved without being in a pending state (label: `legalhold-not-pending`)" }, - "403": { + "429": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "insufficient-permissions", - "message": "Insufficient team permissions" + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." }, "properties": { "code": { "enum": [ - 403 + 429 ], "type": "integer" }, "label": { "enum": [ - "insufficient-permissions", - "too-many-team-invitations", - "blacklisted-email", - "no-identity", - "no-email" + "too-many-requests" ], "type": "string" }, @@ -28119,58 +37248,28 @@ } } }, - "description": "Insufficient team permissions (label: `insufficient-permissions`)\n\nToo many team invitations for this team (label: `too-many-team-invitations`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nThe user has no verified email (label: `no-identity`)\n\nThis operation requires the user to have a verified email address. (label: `no-email`)" - } - }, - "summary": "Create and send a new team invitation." - } - }, - "/teams/{tid}/invitations/{iid}": { - "delete": { - "description": " [internal route ID: \"delete-team-invitation\"]\n\n", - "parameters": [ - { - "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "in": "path", - "name": "iid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Invitation deleted" + "description": "Please try again later. (label: `too-many-requests`)" }, - "403": { + "500": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "insufficient-permissions", - "message": "Insufficient team permissions" + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." }, "properties": { "code": { "enum": [ - 403 + 500 ], "type": "integer" }, "label": { "enum": [ - "insufficient-permissions" + "legalhold-internal", + "legalhold-illegal-op" ], "type": "string" }, @@ -28187,13 +37286,16 @@ } } }, - "description": "Insufficient team permissions (label: `insufficient-permissions`)" + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" } }, - "summary": "Delete a pending team invitation by ID." - }, + "summary": "Approve legal hold device" + } + }, + "/teams/{tid}/members": { "get": { - "description": " [internal route ID: \"get-team-invitation\"]\n\n", + "description": " [internal route ID: \"get-team-members\"]\n\n", + "operationId": "get-team-members", "parameters": [ { "in": "path", @@ -28205,11 +37307,23 @@ } }, { - "in": "path", - "name": "iid", - "required": true, + "description": "Maximum results to be returned", + "in": "query", + "name": "maxResults", + "required": false, + "schema": { + "format": "int32", + "maximum": 2000, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Optional, when not specified, the first page will be returned.Every returned page contains a `pagingState`, this should be supplied to retrieve the next page.", + "in": "query", + "name": "pagingState", + "required": false, "schema": { - "format": "uuid", "type": "string" } } @@ -28217,18 +37331,13 @@ "responses": { "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Invitation" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Invitation" + "$ref": "#/components/schemas/TeamMembersPage_NzYwNDIxODgx" } } }, - "description": "Invitation" + "description": "" }, "403": { "content": { @@ -28236,8 +37345,8 @@ "schema": { "example": { "code": 403, - "label": "insufficient-permissions", - "message": "Insufficient team permissions" + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { @@ -28248,76 +37357,7 @@ }, "label": { "enum": [ - "insufficient-permissions" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - } - }, - "description": "Insufficient team permissions (label: `insufficient-permissions`)" - }, - "404": { - "content": { - "application/json": { - "schema": { - "example": { - "code": 404, - "label": "not-found", - "message": "Notification not found." - }, - "properties": { - "code": { - "enum": [ - 404 - ], - "type": "integer" - }, - "label": { - "enum": [ - "not-found" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - }, - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 404, - "label": "not-found", - "message": "Notification not found." - }, - "properties": { - "code": { - "enum": [ - 404 - ], - "type": "integer" - }, - "label": { - "enum": [ - "not-found" + "no-team-member" ], "type": "string" }, @@ -28334,15 +37374,14 @@ } } }, - "description": "`tid` or `iid` or Notification not found. (label: `not-found`)" + "description": "Requesting user is not a team member (label: `no-team-member`)" } }, - "summary": "Get a pending team invitation by ID." - } - }, - "/teams/{tid}/legalhold/consent": { - "post": { - "description": " [internal route ID: \"consent-to-legal-hold\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "summary": "Get team members" + }, + "put": { + "description": " [internal route ID: \"update-team-member\"]\n\n", + "operationId": "update-team-member", "parameters": [ { "in": "path", @@ -28354,12 +37393,19 @@ } } ], - "responses": { - "201": { - "description": "Grant consent successful" + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewTeamMember_Required_LTg2NjU5OTI2" + } + } }, - "204": { - "description": "Consent already granted" + "required": true + }, + "responses": { + "200": { + "description": "" }, "403": { "content": { @@ -28367,8 +37413,8 @@ "schema": { "example": { "code": 403, - "label": "invalid-op", - "message": "Invalid operation" + "label": "operation-denied", + "message": "Insufficient permissions" }, "properties": { "code": { @@ -28379,8 +37425,11 @@ }, "label": { "enum": [ - "invalid-op", - "action-denied" + "operation-denied", + "no-team-member", + "too-many-team-admins", + "invalid-permissions", + "access-denied" ], "type": "string" }, @@ -28397,7 +37446,7 @@ } } }, - "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nMaximum number of admins per team reached (label: `too-many-team-admins`)\n\nThe specified permissions are invalid (label: `invalid-permissions`)\n\nYou do not have permission to access this resource (label: `access-denied`)" }, "404": { "content": { @@ -28417,7 +37466,8 @@ }, "label": { "enum": [ - "no-team-member" + "no-team-member", + "no-team" ], "type": "string" }, @@ -28434,28 +37484,53 @@ } } }, - "description": "`tid` not found\n\nTeam member not found (label: `no-team-member`)" + "description": "`tid` not found\n\nTeam member not found (label: `no-team-member`)\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Update an existing team member" + } + }, + "/teams/{tid}/members/csv": { + "get": { + "description": " [internal route ID: \"get-team-members-csv\"]\n\nThe endpoint returns data in chunked transfer encoding. Internal server errors might result in a failed transfer instead of a 500 response.", + "operationId": "get-team-members-csv", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/csv": {} + }, + "description": "CSV of team members" }, - "500": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 500, - "label": "legalhold-internal", - "message": "legal hold service: could not block connections when resolving policy conflicts." + "code": 403, + "label": "access-denied", + "message": "You do not have permission to access this resource" }, "properties": { "code": { "enum": [ - 500 + 403 ], "type": "integer" }, "label": { "enum": [ - "legalhold-internal", - "legalhold-illegal-op" + "access-denied" ], "type": "string" }, @@ -28472,15 +37547,16 @@ } } }, - "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + "description": "You do not have permission to access this resource (label: `access-denied`)" } }, - "summary": "Consent to legal hold" + "summary": "Get all members of the team as a CSV file" } }, - "/teams/{tid}/legalhold/settings": { + "/teams/{tid}/members/{uid}": { "delete": { - "description": " [internal route ID: \"delete-legal-hold-settings\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientRemoved event to members with a legalhold client (via brig)\n- UserLegalHoldDisabled event to contacts of members with a legalhold client (via brig)
Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "description": " [internal route ID: \"delete-team-member\"]\n\n", + "operationId": "delete-team-member", "parameters": [ { "in": "path", @@ -28490,41 +37566,57 @@ "format": "uuid", "type": "string" } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } } ], "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/RemoveLegalHoldSettingsRequest" + "$ref": "#/components/schemas/TeamMemberDeleteData_LTg2OTEyOTI4" } } }, "required": true }, "responses": { - "204": { - "description": "Legal hold service settings deleted" + "200": { + "description": "" }, - "400": { + "202": { + "description": "Team member scheduled for deletion" + }, + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "legalhold-not-registered", - "message": "legal hold service has not been registered for this team" + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "legalhold-not-registered" + "operation-denied", + "no-team-member", + "access-denied", + "code-authentication-required", + "code-authentication-failed" ], "type": "string" }, @@ -28541,35 +37633,28 @@ } } }, - "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "legalhold-disable-unimplemented", - "message": "legal hold cannot be disabled for whitelisted teams" + "code": 404, + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "legalhold-disable-unimplemented", - "legalhold-not-enabled", - "invalid-op", - "action-denied", - "no-team-member", - "operation-denied", - "code-authentication-required", - "code-authentication-failed", - "access-denied" + "no-team", + "no-team-member" ], "type": "string" }, @@ -28586,28 +37671,27 @@ } } }, - "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + "description": "`tid` or `uid` not found\n\nTeam not found (label: `no-team`)\n\nTeam member not found (label: `no-team-member`)" }, - "500": { + "429": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 500, - "label": "legalhold-internal", - "message": "legal hold service: could not block connections when resolving policy conflicts." + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." }, "properties": { "code": { "enum": [ - 500 + 429 ], "type": "integer" }, "label": { "enum": [ - "legalhold-internal", - "legalhold-illegal-op" + "too-many-requests" ], "type": "string" }, @@ -28624,13 +37708,14 @@ } } }, - "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + "description": "Please try again later. (label: `too-many-requests`)" } }, - "summary": "Delete legal hold service settings" + "summary": "Remove an existing team member" }, "get": { - "description": " [internal route ID: \"get-legal-hold-settings\"]\n\n", + "description": " [internal route ID: \"get-team-member\"]\n\n", + "operationId": "get-team-member", "parameters": [ { "in": "path", @@ -28640,66 +37725,10 @@ "format": "uuid", "type": "string" } - } - ], - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/ViewLegalHoldService" - } - } - }, - "description": "" }, - "403": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 403, - "label": "operation-denied", - "message": "Insufficient permissions" - }, - "properties": { - "code": { - "enum": [ - 403 - ], - "type": "integer" - }, - "label": { - "enum": [ - "operation-denied", - "no-team-member" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - } - }, - "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" - } - }, - "summary": "Get legal hold service settings" - }, - "post": { - "description": " [internal route ID: \"create-legal-hold-settings\"]\n\n", - "parameters": [ { "in": "path", - "name": "tid", + "name": "uid", "required": true, "schema": { "format": "uuid", @@ -28707,52 +37736,36 @@ } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/NewLegalHoldService" - } - } - }, - "required": true - }, "responses": { - "201": { + "200": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ViewLegalHoldService" - } - }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ViewLegalHoldService" + "$ref": "#/components/schemas/TeamMember_Optional_NTU0MDcyNzI1" } } }, - "description": "Legal hold service settings created" + "description": "" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "legalhold-status-bad", - "message": "legal hold service: invalid response" + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "legalhold-status-bad", - "legalhold-invalid-key" + "no-team-member" ], "type": "string" }, @@ -28769,28 +37782,26 @@ } } }, - "description": "Invalid `body`\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)\n\nlegal hold service pubkey is invalid (label: `legalhold-invalid-key`)" + "description": "Requesting user is not a team member (label: `no-team-member`)" }, - "403": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "legalhold-not-enabled", - "message": "legal hold is not enabled for this team" + "code": 404, + "label": "no-team-member", + "message": "Team member not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "legalhold-not-enabled", - "operation-denied", "no-team-member" ], "type": "string" @@ -28808,15 +37819,16 @@ } } }, - "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" } }, - "summary": "Create legal hold service settings" + "summary": "Get single team member" } }, - "/teams/{tid}/legalhold/{uid}": { - "delete": { - "description": " [internal route ID: \"disable-legal-hold-for-user\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientRemoved event to the user owning the client (via brig)\n- UserLegalHoldDisabled event to contacts of the user owning the client (via brig)
Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "/teams/{tid}/search": { + "get": { + "description": " [internal route ID: \"browse-team\"]\n\n", + "operationId": "browse-team", "parameters": [ { "in": "path", @@ -28827,9 +37839,135 @@ "type": "string" } }, + { + "description": "Search expression", + "in": "query", + "name": "q", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Role filter, eg. `member,partner`. Empty list means do not filter.", + "in": "query", + "name": "frole", + "required": false, + "schema": { + "items": { + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "type": "array" + } + }, + { + "description": "Can be one of name, handle, email, saml_idp, managed_by, role, created_at.", + "in": "query", + "name": "sortby", + "required": false, + "schema": { + "enum": [ + "name", + "handle", + "email", + "saml_idp", + "managed_by", + "role", + "created_at" + ], + "type": "string" + } + }, + { + "description": "Can be one of asc, desc.", + "in": "query", + "name": "sortorder", + "required": false, + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "description": "Number of results to return (min: 1, max: 500, default: 15)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "maximum": 500, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Optional, when not specified, the first page will be returned. Every returned page contains a `paging_state`, this should be supplied to retrieve the next page.", + "in": "query", + "name": "pagingState", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Filter for (un-)verified email", + "in": "query", + "name": "email", + "required": false, + "schema": { + "enum": [ + "unverified", + "verified" + ], + "type": "string" + } + }, + { + "description": "Optional, return only non-searchable members when false.", + "in": "query", + "name": "searchable", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchResult_TeamContact_LTE0NjQ0NzMw" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchResult_TeamContact_LTE0NjQ0NzMw" + } + } + }, + "description": "Search results" + } + }, + "summary": "Browse team for members (requires add-user permission)" + } + }, + "/teams/{tid}/search-visibility": { + "get": { + "description": " [internal route ID: \"get-search-visibility\"]\n\n", + "operationId": "get-search-visibility", + "parameters": [ { "in": "path", - "name": "uid", + "name": "tid", "required": true, "schema": { "format": "uuid", @@ -28837,59 +37975,16 @@ } } ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/DisableLegalHoldForUserRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Disable legal hold successful" - }, - "204": { - "description": "Legal hold was not enabled" - }, - "400": { "content": { "application/json;charset=utf-8": { "schema": { - "example": { - "code": 400, - "label": "legalhold-not-registered", - "message": "legal hold service has not been registered for this team" - }, - "properties": { - "code": { - "enum": [ - 400 - ], - "type": "integer" - }, - "label": { - "enum": [ - "legalhold-not-registered" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" + "$ref": "#/components/schemas/TeamSearchVisibilityView_Mzg3MzMzMTk3" } } }, - "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + "description": "" }, "403": { "content": { @@ -28910,11 +38005,7 @@ "label": { "enum": [ "operation-denied", - "no-team-member", - "action-denied", - "code-authentication-required", - "code-authentication-failed", - "access-denied" + "no-team-member" ], "type": "string" }, @@ -28931,28 +38022,60 @@ } } }, - "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Shows the value for search visibility" + }, + "put": { + "description": " [internal route ID: \"set-search-visibility\"]\n\n", + "operationId": "set-search-visibility", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamSearchVisibilityView_Mzg3MzMzMTk3" + } + } }, - "500": { + "required": true + }, + "responses": { + "204": { + "description": "Search visibility set" + }, + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 500, - "label": "legalhold-internal", - "message": "legal hold service: could not block connections when resolving policy conflicts." + "code": 403, + "label": "team-search-visibility-not-enabled", + "message": "Custom search is not available for this team" }, "properties": { "code": { "enum": [ - 500 + 403 ], "type": "integer" }, "label": { "enum": [ - "legalhold-internal", - "legalhold-illegal-op" + "team-search-visibility-not-enabled", + "operation-denied", + "no-team-member" ], "type": "string" }, @@ -28969,43 +38092,7 @@ } } }, - "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" - } - }, - "summary": "Disable legal hold for user" - }, - "get": { - "description": " [internal route ID: \"get-legal-hold\"]\n\n", - "parameters": [ - { - "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "in": "path", - "name": "uid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/UserLegalHoldStatusResponse" - } - } - }, - "description": "" + "description": "Custom search is not available for this team (label: `team-search-visibility-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" }, "404": { "content": { @@ -29013,8 +38100,8 @@ "schema": { "example": { "code": 404, - "label": "no-team-member", - "message": "Team member not found" + "label": "no-team", + "message": "Team not found" }, "properties": { "code": { @@ -29025,7 +38112,7 @@ }, "label": { "enum": [ - "no-team-member" + "no-team" ], "type": "string" }, @@ -29042,13 +38129,16 @@ } } }, - "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" } }, - "summary": "Get legal hold status" - }, - "post": { - "description": " [internal route ID: \"request-legal-hold-device\"]\n\nThis endpoint can lead to the following events being sent:\n- LegalHoldClientRequested event to contacts of the user the device is requested for, if they didn't already have a legalhold client (via brig)
Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "summary": "Sets the search visibility for the whole team" + } + }, + "/teams/{tid}/size": { + "get": { + "description": " [internal route ID: \"get-team-size\"]\n\nCan be out of sync by roughly the `refresh_interval` of the ES index.", + "operationId": "get-team-size", "parameters": [ { "in": "path", @@ -29058,23 +38148,23 @@ "format": "uuid", "type": "string" } - }, - { - "in": "path", - "name": "uid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } } ], "responses": { - "201": { - "description": "Request device successful" - }, - "204": { - "description": "Request device already pending" + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamSize_LTMzMzk2MTk1" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamSize_LTMzMzk2MTk1" + } + } + }, + "description": "Number of team members" }, "400": { "content": { @@ -29082,8 +38172,8 @@ "schema": { "example": { "code": 400, - "label": "legalhold-not-registered", - "message": "legal hold service has not been registered for this team" + "label": "invalid-invitation-code", + "message": "Invalid invitation code." }, "properties": { "code": { @@ -29094,8 +38184,7 @@ }, "label": { "enum": [ - "legalhold-not-registered", - "legalhold-status-bad" + "invalid-invitation-code" ], "type": "string" }, @@ -29112,16 +38201,69 @@ } } }, - "description": "legal hold service has not been registered for this team (label: `legalhold-not-registered`)\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)" + "description": "Invalid invitation code. (label: `invalid-invitation-code`)" + } + }, + "summary": "Get the number of team members as an integer" + } + }, + "/time": { + "get": { + "description": " [internal route ID: \"get-server-time\"]\n\nReturns the current server time in UTC with seconds precision.", + "operationId": "get-server-time", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServerTime_LTM4NTI3MzIx" + } + } + }, + "description": "" + } + }, + "summary": "Get the current server time" + } + }, + "/upgrade-personal-to-team": { + "post": { + "description": " [internal route ID: \"upgrade-personal-to-team\"]\n\n", + "operationId": "upgrade-personal-to-team", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/BindingNewTeamUser_LTY0MDQxMDEw" + } + } }, - "403": { + "required": true + }, + "responses": { + "200": { "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserTeam_MzI4NDQ1Mzkw" + } + }, "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateUserTeam_MzI4NDQ1Mzkw" + } + } + }, + "description": "Team created" + }, + "403": { + "content": { + "application/json": { "schema": { "example": { "code": 403, - "label": "legalhold-not-enabled", - "message": "legal hold is not enabled for this team" + "label": "user-already-in-a-team", + "message": "Switching teams is not allowed" }, "properties": { "code": { @@ -29132,10 +38274,7 @@ }, "label": { "enum": [ - "legalhold-not-enabled", - "operation-denied", - "no-team-member", - "action-denied" + "user-already-in-a-team" ], "type": "string" }, @@ -29150,29 +38289,24 @@ ], "type": "object" } - } - }, - "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" - }, - "404": { - "content": { + }, "application/json;charset=utf-8": { "schema": { "example": { - "code": 404, - "label": "no-team-member", - "message": "Team member not found" + "code": 403, + "label": "user-already-in-a-team", + "message": "Switching teams is not allowed" }, "properties": { "code": { "enum": [ - 404 + 403 ], "type": "integer" }, "label": { "enum": [ - "no-team-member" + "user-already-in-a-team" ], "type": "string" }, @@ -29189,28 +38323,27 @@ } } }, - "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + "description": "Switching teams is not allowed (label: `user-already-in-a-team`)" }, - "409": { + "404": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { "example": { - "code": 409, - "label": "legalhold-no-consent", - "message": "user has not given consent to using legal hold" + "code": 404, + "label": "not-found", + "message": "User not found" }, "properties": { "code": { "enum": [ - 409 + 404 ], "type": "integer" }, "label": { "enum": [ - "legalhold-no-consent", - "legalhold-already-enabled" + "not-found" ], "type": "string" }, @@ -29225,30 +38358,24 @@ ], "type": "object" } - } - }, - "description": "user has not given consent to using legal hold (label: `legalhold-no-consent`)\n\nlegal hold is already enabled for this user (label: `legalhold-already-enabled`)" - }, - "500": { - "content": { + }, "application/json;charset=utf-8": { "schema": { "example": { - "code": 500, - "label": "legalhold-illegal-op", - "message": "internal server error: inconsistent change of user's legalhold state" + "code": 404, + "label": "not-found", + "message": "User not found" }, "properties": { "code": { "enum": [ - 500 + 404 ], "type": "integer" }, "label": { "enum": [ - "legalhold-illegal-op", - "legalhold-internal" + "not-found" ], "type": "string" }, @@ -29265,40 +38392,131 @@ } } }, - "description": "internal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)\n\nlegal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)" + "description": "User not found (label: `not-found`)" } }, - "summary": "Request legal hold device" + "summary": "Upgrade personal user to team owner" } }, - "/teams/{tid}/legalhold/{uid}/approve": { - "put": { - "description": " [internal route ID: \"approve-legal-hold-device\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientAdded event to the user owning the client (via brig)\n- UserLegalHoldEnabled event to contacts of the user owning the client (via brig)\n- ClientRemoved event to the user, if removing old client due to max number (via brig)
Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "/user-groups": { + "get": { + "description": " [internal route ID: \"get-user-groups\"]\n\n", + "operationId": "get-user-groups", "parameters": [ { - "in": "path", - "name": "tid", - "required": true, + "description": "Search string", + "in": "query", + "name": "q", + "required": false, "schema": { - "format": "uuid", "type": "string" } }, { - "in": "path", - "name": "uid", - "required": true, + "in": "query", + "name": "sort_by", + "required": false, + "schema": { + "enum": [ + "name", + "created_at" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "sort_order", + "required": false, + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "page_size", + "required": false, + "schema": { + "description": "integer from [1..500]", + "type": "number" + } + }, + { + "description": "`name` of the last seen user group, used to get the next page when sorting by name.", + "in": "query", + "name": "last_seen_name", + "required": false, + "schema": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + { + "description": "`created_at` field of the last seen user group, used to get the next page when sorting by created_at.", + "in": "query", + "name": "last_seen_created_at", + "required": false, + "schema": { + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + } + }, + { + "description": "`id` of the last seen group, used to get the next page. **Must** be sent to get the next page.", + "in": "query", + "name": "last_seen_id", + "required": false, "schema": { "format": "uuid", "type": "string" } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "include_channels", + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "include_member_count", + "schema": { + "default": false, + "type": "boolean" + } } ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroupPage_UserGroup_Const_LTMxNDg5MDAy" + } + } + }, + "description": "" + } + }, + "summary": "Fetch groups accessible to the logged-in user" + }, + "post": { + "description": " [internal route ID: \"create-user-group\"]\n\n", + "operationId": "create-user-group", "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ApproveLegalHoldForUserRequest" + "$ref": "#/components/schemas/NewUserGroup_MzYxODU0OTU1" } } }, @@ -29306,7 +38524,14 @@ }, "responses": { "200": { - "description": "Legal hold approved" + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroup_Identity_NTg4MTY1MjEx" + } + } + }, + "description": "" }, "400": { "content": { @@ -29314,8 +38539,8 @@ "schema": { "example": { "code": 400, - "label": "legalhold-not-registered", - "message": "legal hold service has not been registered for this team" + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." }, "properties": { "code": { @@ -29326,7 +38551,7 @@ }, "label": { "enum": [ - "legalhold-not-registered" + "user-group-invalid" ], "type": "string" }, @@ -29343,7 +38568,7 @@ } } }, - "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + "description": "Invalid `body`\n\nOnly team members of the same team can be added to a user group. (label: `user-group-invalid`)" }, "403": { "content": { @@ -29351,8 +38576,8 @@ "schema": { "example": { "code": 403, - "label": "legalhold-not-enabled", - "message": "legal hold is not enabled for this team" + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." }, "properties": { "code": { @@ -29363,12 +38588,7 @@ }, "label": { "enum": [ - "legalhold-not-enabled", - "no-team-member", - "action-denied", - "access-denied", - "code-authentication-required", - "code-authentication-failed" + "user-group-write-forbidden" ], "type": "string" }, @@ -29385,101 +38605,83 @@ } } }, - "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + } + } + } + }, + "/user-groups/check-name": { + "post": { + "description": " [internal route ID: \"check-user-group-name-available\"]\n\n", + "operationId": "check-user-group-name-available", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CheckUserGroupName_LTg0ODU1OTk1" + } + } }, - "404": { + "required": true + }, + "responses": { + "200": { "content": { - "application/json;charset=utf-8": { + "application/json": { "schema": { - "example": { - "code": 404, - "label": "legalhold-no-device-allocated", - "message": "no legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow." - }, - "properties": { - "code": { - "enum": [ - 404 - ], - "type": "integer" - }, - "label": { - "enum": [ - "legalhold-no-device-allocated" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" + "$ref": "#/components/schemas/UserGroupNameAvailability_LTYzMDE1NTk4" } - } - }, - "description": "`tid` or `uid` not found\n\nno legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow. (label: `legalhold-no-device-allocated`)" - }, - "409": { - "content": { + }, "application/json;charset=utf-8": { "schema": { - "example": { - "code": 409, - "label": "legalhold-already-enabled", - "message": "legal hold is already enabled for this user" - }, - "properties": { - "code": { - "enum": [ - 409 - ], - "type": "integer" - }, - "label": { - "enum": [ - "legalhold-already-enabled" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" + "$ref": "#/components/schemas/UserGroupNameAvailability_LTYzMDE1NTk4" } } }, - "description": "legal hold is already enabled for this user (label: `legalhold-already-enabled`)" + "description": "OK" + } + }, + "summary": "[STUB] Check if a user group name is available" + } + }, + "/user-groups/{gid}": { + "delete": { + "description": " [internal route ID: \"delete-user-group\"]\n\n", + "operationId": "delete-user-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "User group deleted" }, - "412": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 412, - "label": "legalhold-not-pending", - "message": "legal hold cannot be approved without being in a pending state" + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." }, "properties": { "code": { "enum": [ - 412 + 403 ], "type": "integer" }, "label": { "enum": [ - "legalhold-not-pending" + "user-group-write-forbidden" ], "type": "string" }, @@ -29496,28 +38698,27 @@ } } }, - "description": "legal hold cannot be approved without being in a pending state (label: `legalhold-not-pending`)" + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" }, - "500": { + "404": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 500, - "label": "legalhold-internal", - "message": "legal hold service: could not block connections when resolving policy conflicts." + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" }, "properties": { "code": { "enum": [ - 500 + 404 ], "type": "integer" }, "label": { "enum": [ - "legalhold-internal", - "legalhold-illegal-op" + "user-group-not-found" ], "type": "string" }, @@ -29534,19 +38735,17 @@ } } }, - "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" } - }, - "summary": "Approve legal hold device" - } - }, - "/teams/{tid}/members": { + } + }, "get": { - "description": " [internal route ID: \"get-team-members\"]\n\n", + "description": " [internal route ID: \"get-user-group\"]\n\n", + "operationId": "get-user-group", "parameters": [ { "in": "path", - "name": "tid", + "name": "gid", "required": true, "schema": { "format": "uuid", @@ -29554,57 +38753,82 @@ } }, { - "description": "Maximum results to be returned", - "in": "query", - "name": "maxResults", - "required": false, - "schema": { - "format": "int32", - "maximum": 2000, - "minimum": 1, - "type": "integer" - } - }, - { - "description": "Optional, when not specified, the first page will be returned.Every returned page contains a `pagingState`, this should be supplied to retrieve the next page.", + "allowEmptyValue": true, "in": "query", - "name": "pagingState", - "required": false, + "name": "include_channels", "schema": { - "type": "string" + "default": false, + "type": "boolean" } } ], "responses": { "200": { "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserGroup_Identity_NTg4MTY1MjEx" + } + }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/TeamMembersPage" + "$ref": "#/components/schemas/UserGroup_Identity_NTg4MTY1MjEx" } } }, - "description": "" + "description": "User Group Found" }, - "403": { + "404": { "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" }, "properties": { "code": { "enum": [ - 403 + 404 ], "type": "integer" }, "label": { "enum": [ - "no-team-member" + "user-group-not-found" ], "type": "string" }, @@ -29621,17 +38845,18 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)" + "description": "`gid` or User group not found (label: `user-group-not-found`)\n\nUser group not found (label: `user-group-not-found`)" } }, - "summary": "Get team members" + "summary": "Fetch a group accessible to the logged-in user" }, "put": { - "description": " [internal route ID: \"update-team-member\"]\n\n", + "description": " [internal route ID: \"update-user-group\"]\n\n", + "operationId": "update-user-group", "parameters": [ { "in": "path", - "name": "tid", + "name": "gid", "required": true, "schema": { "format": "uuid", @@ -29643,7 +38868,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/NewTeamMember" + "$ref": "#/components/schemas/UserGroupUpdate_MjUyNTA3Mjgy" } } }, @@ -29651,7 +38876,7 @@ }, "responses": { "200": { - "description": "" + "description": "User added updated" }, "403": { "content": { @@ -29659,8 +38884,8 @@ "schema": { "example": { "code": 403, - "label": "operation-denied", - "message": "Insufficient permissions" + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." }, "properties": { "code": { @@ -29671,11 +38896,7 @@ }, "label": { "enum": [ - "operation-denied", - "no-team-member", - "too-many-team-admins", - "invalid-permissions", - "access-denied" + "user-group-write-forbidden" ], "type": "string" }, @@ -29692,7 +38913,7 @@ } } }, - "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nMaximum number of admins per team reached (label: `too-many-team-admins`)\n\nThe specified permissions are invalid (label: `invalid-permissions`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" }, "404": { "content": { @@ -29700,8 +38921,8 @@ "schema": { "example": { "code": 404, - "label": "no-team-member", - "message": "Team member not found" + "label": "user-group-not-found", + "message": "User group not found" }, "properties": { "code": { @@ -29712,70 +38933,7 @@ }, "label": { "enum": [ - "no-team-member", - "no-team" - ], - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "label", - "message" - ], - "type": "object" - } - } - }, - "description": "`tid` not found\n\nTeam member not found (label: `no-team-member`)\n\nTeam not found (label: `no-team`)" - } - }, - "summary": "Update an existing team member" - } - }, - "/teams/{tid}/members/csv": { - "get": { - "description": " [internal route ID: \"get-team-members-csv\"]\n\nThe endpoint returns data in chunked transfer encoding. Internal server errors might result in a failed transfer instead of a 500 response.", - "parameters": [ - { - "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "text/csv": {} - }, - "description": "CSV of team members" - }, - "403": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "example": { - "code": 403, - "label": "access-denied", - "message": "You do not have permission to access this resource" - }, - "properties": { - "code": { - "enum": [ - 403 - ], - "type": "integer" - }, - "label": { - "enum": [ - "access-denied" + "user-group-not-found" ], "type": "string" }, @@ -29792,19 +38950,19 @@ } } }, - "description": "You do not have permission to access this resource (label: `access-denied`)" - } - }, - "summary": "Get all members of the team as a CSV file" + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } } }, - "/teams/{tid}/members/{uid}": { - "delete": { - "description": " [internal route ID: \"delete-team-member\"]\n\n", + "/user-groups/{gid}/channels": { + "put": { + "description": " [internal route ID: \"update-user-group-channels\"]\n\n", + "operationId": "update-user-group-channels", "parameters": [ { "in": "path", - "name": "tid", + "name": "gid", "required": true, "schema": { "format": "uuid", @@ -29812,12 +38970,12 @@ } }, { - "in": "path", - "name": "uid", - "required": true, + "allowEmptyValue": true, + "in": "query", + "name": "append_only", "schema": { - "format": "uuid", - "type": "string" + "default": false, + "type": "boolean" } } ], @@ -29825,7 +38983,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/TeamMemberDeleteData" + "$ref": "#/components/schemas/UpdateUserGroupChannels_LTIyMjcwMTMx" } } }, @@ -29833,10 +38991,7 @@ }, "responses": { "200": { - "description": "" - }, - "202": { - "description": "Team member scheduled for deletion" + "description": "User group channels updated" }, "403": { "content": { @@ -29844,8 +38999,8 @@ "schema": { "example": { "code": 403, - "label": "operation-denied", - "message": "Insufficient permissions" + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." }, "properties": { "code": { @@ -29856,11 +39011,7 @@ }, "label": { "enum": [ - "operation-denied", - "no-team-member", - "access-denied", - "code-authentication-required", - "code-authentication-failed" + "user-group-write-forbidden" ], "type": "string" }, @@ -29877,7 +39028,7 @@ } } }, - "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" }, "404": { "content": { @@ -29885,8 +39036,8 @@ "schema": { "example": { "code": 404, - "label": "no-team", - "message": "Team not found" + "label": "user-group-not-found", + "message": "User group not found" }, "properties": { "code": { @@ -29897,8 +39048,7 @@ }, "label": { "enum": [ - "no-team", - "no-team-member" + "user-group-not-found" ], "type": "string" }, @@ -29915,26 +39065,20 @@ } } }, - "description": "`tid` or `uid` not found\n\nTeam not found (label: `no-team`)\n\nTeam member not found (label: `no-team-member`)" + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" } }, - "summary": "Remove an existing team member" - }, - "get": { - "description": " [internal route ID: \"get-team-member\"]\n\n", + "summary": "Replaces the channels with the given list." + } + }, + "/user-groups/{gid}/users": { + "post": { + "description": " [internal route ID: \"add-users-to-group-bulk\"]\n\n", + "operationId": "add-users-to-group-bulk", "parameters": [ { "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "in": "path", - "name": "uid", + "name": "gid", "required": true, "schema": { "format": "uuid", @@ -29942,16 +39086,56 @@ } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroupAddUsers_LTgzOTYzNzk0" + } + } + }, + "required": true + }, "responses": { - "200": { + "204": { + "description": "Users added to group" + }, + "400": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/TeamMember" + "example": { + "code": 400, + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-invalid" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" } } }, - "description": "" + "description": "Invalid `body`\n\nOnly team members of the same team can be added to a user group. (label: `user-group-invalid`)" }, "403": { "content": { @@ -29959,8 +39143,8 @@ "schema": { "example": { "code": 403, - "label": "no-team-member", - "message": "Requesting user is not a team member" + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." }, "properties": { "code": { @@ -29971,7 +39155,7 @@ }, "label": { "enum": [ - "no-team-member" + "user-group-write-forbidden" ], "type": "string" }, @@ -29988,7 +39172,7 @@ } } }, - "description": "Requesting user is not a team member (label: `no-team-member`)" + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" }, "404": { "content": { @@ -29996,8 +39180,8 @@ "schema": { "example": { "code": 404, - "label": "no-team-member", - "message": "Team member not found" + "label": "user-group-not-found", + "message": "User group not found" }, "properties": { "code": { @@ -30008,7 +39192,7 @@ }, "label": { "enum": [ - "no-team-member" + "user-group-not-found" ], "type": "string" }, @@ -30025,132 +39209,59 @@ } } }, - "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" } - }, - "summary": "Get single team member" - } - }, - "/teams/{tid}/search": { - "get": { - "description": " [internal route ID: \"browse-team\"]\n\n", + } + }, + "put": { + "description": " [internal route ID: \"update-user-group-members\"]\n\n", + "operationId": "update-user-group-members", "parameters": [ { "in": "path", - "name": "tid", + "name": "gid", "required": true, "schema": { "format": "uuid", "type": "string" } - }, - { - "description": "Search expression", - "in": "query", - "name": "q", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Role filter, eg. `member,partner`. Empty list means do not filter.", - "in": "query", - "name": "frole", - "required": false, - "schema": { - "items": { - "enum": [ - "owner", - "admin", - "member", - "partner" - ], - "type": "string" - }, - "type": "array" - } - }, - { - "description": "Can be one of name, handle, email, saml_idp, managed_by, role, created_at.", - "in": "query", - "name": "sortby", - "required": false, - "schema": { - "enum": [ - "name", - "handle", - "email", - "saml_idp", - "managed_by", - "role", - "created_at" - ], - "type": "string" - } - }, - { - "description": "Can be one of asc, desc.", - "in": "query", - "name": "sortorder", - "required": false, - "schema": { - "enum": [ - "asc", - "desc" - ], - "type": "string" - } - }, - { - "description": "Number of results to return (min: 1, max: 500, default: 15)", - "in": "query", - "name": "size", - "required": false, - "schema": { - "format": "int32", - "maximum": 500, - "minimum": 1, - "type": "integer" - } - }, - { - "description": "Optional, when not specified, the first page will be returned. Every returned page contains a `paging_state`, this should be supplied to retrieve the next page.", - "in": "query", - "name": "pagingState", - "required": false, - "schema": { - "type": "string" - } } ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateUserGroupMembers_LTg1MzQ2NDY3" + } + } + }, + "required": true + }, "responses": { "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SearchResult" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/SearchResult" - } - } - }, - "description": "Search results" + "description": "User group members updated" } }, - "summary": "Browse team for members (requires add-user permission)" + "summary": "[STUB] Update user group members. Replaces the users with the given list." } }, - "/teams/{tid}/search-visibility": { - "get": { - "description": " [internal route ID: \"get-search-visibility\"]\n\n", + "/user-groups/{gid}/users/{uid}": { + "delete": { + "description": " [internal route ID: \"remove-user-from-group\"]\n\n", + "operationId": "remove-user-from-group", "parameters": [ { "in": "path", - "name": "tid", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", "required": true, "schema": { "format": "uuid", @@ -30159,36 +39270,28 @@ } ], "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/TeamSearchVisibilityView" - } - } - }, - "description": "" + "204": { + "description": "User removed from group" }, - "403": { + "400": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 403, - "label": "operation-denied", - "message": "Insufficient permissions" + "code": 400, + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." }, "properties": { "code": { "enum": [ - 403 + 400 ], "type": "integer" }, "label": { "enum": [ - "operation-denied", - "no-team-member" + "user-group-invalid" ], "type": "string" }, @@ -30205,37 +39308,7 @@ } } }, - "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" - } - }, - "summary": "Shows the value for search visibility" - }, - "put": { - "description": " [internal route ID: \"set-search-visibility\"]\n\n", - "parameters": [ - { - "in": "path", - "name": "tid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/TeamSearchVisibilityView" - } - } - }, - "required": true - }, - "responses": { - "204": { - "description": "Search visibility set" + "description": "Only team members of the same team can be added to a user group. (label: `user-group-invalid`)" }, "403": { "content": { @@ -30243,8 +39316,8 @@ "schema": { "example": { "code": 403, - "label": "team-search-visibility-not-enabled", - "message": "Custom search is not available for this team" + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." }, "properties": { "code": { @@ -30255,9 +39328,7 @@ }, "label": { "enum": [ - "team-search-visibility-not-enabled", - "operation-denied", - "no-team-member" + "user-group-write-forbidden" ], "type": "string" }, @@ -30274,7 +39345,7 @@ } } }, - "description": "Custom search is not available for this team (label: `team-search-visibility-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" }, "404": { "content": { @@ -30282,8 +39353,8 @@ "schema": { "example": { "code": 404, - "label": "no-team", - "message": "Team not found" + "label": "user-group-not-found", + "message": "User group not found" }, "properties": { "code": { @@ -30294,7 +39365,7 @@ }, "label": { "enum": [ - "no-team" + "user-group-not-found" ], "type": "string" }, @@ -30311,19 +39382,26 @@ } } }, - "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + "description": "`gid` or `uid` not found\n\nUser group not found (label: `user-group-not-found`)" } - }, - "summary": "Sets the search visibility for the whole team" - } - }, - "/teams/{tid}/size": { - "get": { - "description": " [internal route ID: \"get-team-size\"]\n\nCan be out of sync by roughly the `refresh_interval` of the ES index.", + } + }, + "post": { + "description": " [internal route ID: \"add-user-to-group\"]\n\n", + "operationId": "add-user-to-group", "parameters": [ { "in": "path", - "name": "tid", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", "required": true, "schema": { "format": "uuid", @@ -30332,20 +39410,8 @@ } ], "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamSize" - } - }, - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/TeamSize" - } - } - }, - "description": "Number of team members" + "204": { + "description": "User added to group" }, "400": { "content": { @@ -30353,8 +39419,8 @@ "schema": { "example": { "code": 400, - "label": "invalid-invitation-code", - "message": "Invalid invitation code." + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." }, "properties": { "code": { @@ -30365,7 +39431,7 @@ }, "label": { "enum": [ - "invalid-invitation-code" + "user-group-invalid" ], "type": "string" }, @@ -30382,105 +39448,27 @@ } } }, - "description": "Invalid invitation code. (label: `invalid-invitation-code`)" - } - }, - "summary": "Get the number of team members as an integer" - } - }, - "/users/handles": { - "post": { - "description": " [internal route ID: \"check-user-handles\"]\n\n", - "requestBody": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/CheckHandles" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Handle" - }, - "type": "array" - } - }, - "application/json;charset=utf-8": { - "schema": { - "items": { - "$ref": "#/components/schemas/Handle" - }, - "type": "array" - } - } - }, - "description": "List of free handles" - } - }, - "summary": "Check availability of user handles" - } - }, - "/users/handles/{handle}": { - "head": { - "description": " [internal route ID: \"check-user-handle\"]\n\n", - "parameters": [ - { - "in": "path", - "name": "handle", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "example": [], - "items": {}, - "maxItems": 0, - "type": "array" - } - }, - "application/json;charset=utf-8": { - "schema": { - "example": [], - "items": {}, - "maxItems": 0, - "type": "array" - } - } - }, - "description": "Handle is taken" + "description": "Only team members of the same team can be added to a user group. (label: `user-group-invalid`)" }, - "400": { + "403": { "content": { "application/json;charset=utf-8": { "schema": { "example": { - "code": 400, - "label": "invalid-handle", - "message": "The given handle is invalid (less than 2 or more than 256 characters; chars not in \"a-z0-9_.-\"; or on the blocklist)" + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." }, "properties": { "code": { "enum": [ - 400 + 403 ], "type": "integer" }, "label": { "enum": [ - "invalid-handle" + "user-group-write-forbidden" ], "type": "string" }, @@ -30497,7 +39485,7 @@ } } }, - "description": "The given handle is invalid (less than 2 or more than 256 characters; chars not in \"a-z0-9_.-\"; or on the blocklist) (label: `invalid-handle`)" + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" }, "404": { "content": { @@ -30505,8 +39493,8 @@ "schema": { "example": { "code": 404, - "label": "not-found", - "message": "Handle not found" + "label": "user-group-not-found", + "message": "User group not found" }, "properties": { "code": { @@ -30517,7 +39505,7 @@ }, "label": { "enum": [ - "not-found" + "user-group-not-found" ], "type": "string" }, @@ -30534,15 +39522,15 @@ } } }, - "description": "`handle` not found\n\nHandle not found (label: `not-found`)" + "description": "`gid` or `uid` not found\n\nUser group not found (label: `user-group-not-found`)" } - }, - "summary": "Check whether a user handle can be taken" + } } }, "/users/list-clients": { "post": { - "description": " [internal route ID: \"list-clients-bulk@v2\"]\n\nIf a backend is unreachable, the clients from that backend will be omitted from the responseCalls federation service brig on get-user-clients", + "description": " [internal route ID: \"list-clients-bulk@v2\"]\n\nIf a backend is unreachable, the clients from that backend will be omitted from the response", + "operationId": "list-clients-bulk@v2", "requestBody": { "content": { "application/json;charset=utf-8": { @@ -30575,7 +39563,8 @@ }, "/users/list-prekeys": { "post": { - "description": " [internal route ID: \"get-multi-user-prekey-bundle-qualified\"]\n\nYou can't request information for more users than maximum conversation size.Calls federation service brig on claim-multi-prekey-bundle", + "description": " [internal route ID: \"get-multi-user-prekey-bundle-qualified\"]\n\nYou can't request information for more users than maximum conversation size.", + "operationId": "get-multi-user-prekey-bundle-qualified", "requestBody": { "content": { "application/json;charset=utf-8": { @@ -30591,7 +39580,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/QualifiedUserClientPrekeyMapV4" + "$ref": "#/components/schemas/QualifiedUserClientPrekeyMapV4_MzcxOTAxMTYy" } } }, @@ -30603,7 +39592,8 @@ }, "/users/{uid_domain}/{uid}": { "get": { - "description": " [internal route ID: \"get-user-qualified\"]\n\nCalls federation service brig on get-users-by-ids", + "description": " [internal route ID: \"get-user-qualified\"]\n\n", + "operationId": "get-user-qualified", "parameters": [ { "in": "path", @@ -30629,12 +39619,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserProfile" + "$ref": "#/components/schemas/UserProfile_LTQzMTQxMTE1" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/UserProfile" + "$ref": "#/components/schemas/UserProfile_LTQzMTQxMTE1" } } }, @@ -30713,50 +39703,10 @@ "summary": "Get a user by Domain and UserId" } }, - "/users/{uid_domain}/{uid}/clients": { - "get": { - "description": " [internal route ID: \"get-user-clients-qualified\"]\n\nCalls federation service brig on get-user-clients", - "parameters": [ - { - "in": "path", - "name": "uid_domain", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "User Id", - "in": "path", - "name": "uid", - "required": true, - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json;charset=utf-8": { - "schema": { - "items": { - "$ref": "#/components/schemas/PubClient" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "summary": "Get all of a user's clients" - } - }, "/users/{uid_domain}/{uid}/clients/{client}": { "get": { - "description": " [internal route ID: \"get-user-client-qualified\"]\n\nCalls federation service brig on get-user-clients", + "description": " [internal route ID: \"get-user-client-qualified\"]\n\nHint: to list all clients of one or more users, use POST /users/list-clients.", + "operationId": "get-user-client-qualified", "parameters": [ { "in": "path", @@ -30803,7 +39753,8 @@ }, "/users/{uid_domain}/{uid}/prekeys": { "get": { - "description": " [internal route ID: \"get-users-prekey-bundle-qualified\"]\n\nCalls federation service brig on claim-prekey-bundle", + "description": " [internal route ID: \"get-users-prekey-bundle-qualified\"]\n\n", + "operationId": "get-users-prekey-bundle-qualified", "parameters": [ { "in": "path", @@ -30829,7 +39780,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/PrekeyBundle" + "$ref": "#/components/schemas/PrekeyBundle_MzgzOTk4MjYz" } } }, @@ -30841,7 +39792,8 @@ }, "/users/{uid_domain}/{uid}/prekeys/{client}": { "get": { - "description": " [internal route ID: \"get-users-prekeys-client-qualified\"]\n\nCalls federation service brig on claim-prekey", + "description": " [internal route ID: \"get-users-prekeys-client-qualified\"]\n\n", + "operationId": "get-users-prekeys-client-qualified", "parameters": [ { "in": "path", @@ -30876,7 +39828,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ClientPrekey" + "$ref": "#/components/schemas/ClientPrekey_LTcyODUzMTcw" } } }, @@ -30889,6 +39841,7 @@ "/users/{uid_domain}/{uid}/supported-protocols": { "get": { "description": " [internal route ID: \"get-supported-protocols\"]\n\n", + "operationId": "get-supported-protocols", "parameters": [ { "in": "path", @@ -30915,7 +39868,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/BaseProtocol" + "$ref": "#/components/schemas/BaseProtocolTag_LTM0MDE1NTEx" }, "type": "array", "uniqueItems": true @@ -30924,7 +39877,7 @@ "application/json;charset=utf-8": { "schema": { "items": { - "$ref": "#/components/schemas/BaseProtocol" + "$ref": "#/components/schemas/BaseProtocolTag_LTM0MDE1NTEx" }, "type": "array", "uniqueItems": true @@ -30940,6 +39893,7 @@ "/users/{uid}/email": { "put": { "description": " [internal route ID: \"update-user-email\"]\n\nIf the user has a pending email validation, the validation email will be resent.", + "operationId": "update-user-email", "parameters": [ { "description": "User Id", @@ -30956,7 +39910,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/EmailUpdate" + "$ref": "#/components/schemas/EmailUpdate_NjQ5MDg1OTY0" } } }, @@ -30983,6 +39937,7 @@ "/users/{uid}/rich-info": { "get": { "description": " [internal route ID: \"get-rich-info\"]\n\n", + "operationId": "get-rich-info", "parameters": [ { "description": "User Id", @@ -31052,14 +40007,59 @@ "summary": "Get a user's rich info" } }, + "/users/{uid}/searchable": { + "post": { + "description": " [internal route ID: \"set-user-searchable\"]\n\n", + "operationId": "set-user-searchable", + "parameters": [ + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SetSearchable_NDAxODAxODI5" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "Set user's visibility in search" + } + }, "/verification-code/send": { "post": { "description": " [internal route ID: \"send-verification-code\"]\n\n", + "operationId": "send-verification-code", "requestBody": { "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SendVerificationCode" + "$ref": "#/components/schemas/SendVerificationCode_MjgxNDgxODE2" } } }, @@ -31072,6 +40072,36 @@ }, "summary": "Send a verification code to a given email address." } + }, + "/websocket": { + "get": { + "description": " [internal route ID: \"websocket\"]\n\nThis is a temporary copy of await, please do not use it", + "externalDocs": { + "description": "RFC 6455", + "url": "https://datatracker.ietf.org/doc/html/rfc6455" + }, + "operationId": "websocket", + "parameters": [ + { + "description": "Client ID", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "101": { + "description": "Connection upgraded." + }, + "426": { + "description": "Upgrade required." + } + }, + "summary": "Establish websocket connection" + } } }, "security": [ @@ -31081,7 +40111,7 @@ ], "servers": [ { - "url": "/v6" + "url": "/v17" } ] } From 915f304c215401cf81c42d20332904592e3bc477 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 9 Sep 2026 17:23:18 +0200 Subject: [PATCH 14/29] WPB-28565 Finalize API version v18 (#5531) --- .../WPB-28565-finalize-api-version-v18 | 1 + integration/test/Test/Swagger.hs | 2 +- integration/test/Testlib/Env.hs | 2 +- .../src/Wire/API/Routes/Public/Swagger.hs | 20 +- libs/wire-api/src/Wire/API/Routes/Version.hs | 8 +- services/brig/docs/swagger-v17.json | 53 +- services/brig/docs/swagger-v18.json | 40122 ++++++++++++++++ services/brig/src/Brig/API/Public.hs | 3 +- 8 files changed, 40174 insertions(+), 37 deletions(-) create mode 100644 changelog.d/1-api-changes/WPB-28565-finalize-api-version-v18 create mode 100644 services/brig/docs/swagger-v18.json diff --git a/changelog.d/1-api-changes/WPB-28565-finalize-api-version-v18 b/changelog.d/1-api-changes/WPB-28565-finalize-api-version-v18 new file mode 100644 index 00000000000..ebcce755966 --- /dev/null +++ b/changelog.d/1-api-changes/WPB-28565-finalize-api-version-v18 @@ -0,0 +1 @@ +Finalize API version v18 and create development version v19. diff --git a/integration/test/Test/Swagger.hs b/integration/test/Test/Swagger.hs index c150b0d62f0..a326ff18abb 100644 --- a/integration/test/Test/Swagger.hs +++ b/integration/test/Test/Swagger.hs @@ -30,7 +30,7 @@ import Testlib.Prelude import UnliftIO.Temporary existingVersions :: Set Int -existingVersions = Set.fromList [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] +existingVersions = Set.fromList [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] internalApis :: Set String internalApis = Set.fromList ["brig", "cannon", "cargohold", "cannon", "spar"] diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 6c0a774953c..92675404337 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -133,7 +133,7 @@ mkGlobalEnv cfgFile = do gFederationV1Domain = intConfig.federationV1.originDomain, gFederationV2Domain = intConfig.federationV2.originDomain, gDynamicDomains = (.domain) <$> Map.elems intConfig.dynamicBackends, - gDefaultAPIVersion = 18, + gDefaultAPIVersion = 19, gManager = manager, gServicesCwdBase = devEnvProjectRoot <&> ( "services"), gBackendResourcePool = resourcePool, diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Swagger.hs b/libs/wire-api/src/Wire/API/Routes/Public/Swagger.hs index 63791ced758..9a822b51711 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Swagger.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Swagger.hs @@ -48,7 +48,7 @@ import Wire.API.SwaggerHelper (cleanupSwagger) -- 'S.OpenApi' has to be assembled at a statically known version. devVersion :: Version devVersion = - if maxBound == V18 + if maxBound == V19 then maxBound else -- if you get this error, you also need to update the version literals below. @@ -60,15 +60,15 @@ devVersion = -- @info.description@, so setting it afterwards is equivalent. devVersionSwagger :: S.OpenApi devVersionSwagger = - ( serviceSwagger @VersionAPITag @'V18 - <> serviceSwagger @BrigAPITag @'V18 - <> serviceSwagger @GalleyAPITag @'V18 - <> serviceSwagger @SparAPITag @'V18 - <> serviceSwagger @CargoholdAPITag @'V18 - <> serviceSwagger @CannonAPITag @'V18 - <> serviceSwagger @GundeckAPITag @'V18 - <> serviceSwagger @ProxyAPITag @'V18 - <> serviceSwagger @OAuthAPITag @'V18 + ( serviceSwagger @VersionAPITag @'V19 + <> serviceSwagger @BrigAPITag @'V19 + <> serviceSwagger @GalleyAPITag @'V19 + <> serviceSwagger @SparAPITag @'V19 + <> serviceSwagger @CargoholdAPITag @'V19 + <> serviceSwagger @CannonAPITag @'V19 + <> serviceSwagger @GundeckAPITag @'V19 + <> serviceSwagger @ProxyAPITag @'V19 + <> serviceSwagger @OAuthAPITag @'V19 ) & S.info . S.title .~ "Wire-Server API" & S.servers .~ [S.Server ("/" <> toUrlPiece devVersion) Nothing mempty] diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index 119c62b3dfa..f360467b700 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -103,7 +103,7 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (GenericUniform)) -- and 'developmentVersions' stay in sync; everything else here should keep working without -- change. See also documentation in the *docs* directory. -- https://docs.wire.com/developer/developer/api-versioning.html#version-bump-checklist -data Version = V0 | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | V11 | V12 | V13 | V14 | V15 | V16 | V17 | V18 +data Version = V0 | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | V11 | V12 | V13 | V14 | V15 | V16 | V17 | V18 | V19 deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) deriving (FromJSON, ToJSON) via (Schema Version) deriving (Arbitrary) via (GenericUniform Version) @@ -144,6 +144,8 @@ instance RenderableSymbol V17 where renderSymbol = "V17" instance RenderableSymbol V18 where renderSymbol = "V18" +instance RenderableSymbol V19 where renderSymbol = "V19" + -- | Manual enumeration of version integrals (the `` in the constructor `V`). -- -- This is not the same as 'fromEnum': we will remove unsupported versions in the future, @@ -170,6 +172,7 @@ versionInt V15 = 15 versionInt V16 = 16 versionInt V17 = 17 versionInt V18 = 18 +versionInt V19 = 19 supportedVersions :: [Version] supportedVersions = [minBound .. maxBound] @@ -295,7 +298,8 @@ isDevelopmentVersion V14 = False isDevelopmentVersion V15 = False isDevelopmentVersion V16 = False isDevelopmentVersion V17 = False -isDevelopmentVersion V18 = True +isDevelopmentVersion V18 = False +isDevelopmentVersion V19 = True developmentVersions :: [Version] developmentVersions = filter isDevelopmentVersion supportedVersions diff --git a/services/brig/docs/swagger-v17.json b/services/brig/docs/swagger-v17.json index dd271f9bac1..22cea431331 100644 --- a/services/brig/docs/swagger-v17.json +++ b/services/brig/docs/swagger-v17.json @@ -2742,10 +2742,10 @@ ], "type": "object" }, - "Featur_Vsiond_17PvAmlGpCfgBIy_LTIzMjQ4MDk1V17": { + "Featur_Vsiond_16PvAmlGpCfgBIy_MjY0MDMyNDA1V16": { "properties": { "config": { - "$ref": "#/components/schemas/Versiond_17_PvtAmlGupCfgBaIy_LTY1MTkyNDcw" + "$ref": "#/components/schemas/Versiond_16_PvtAmlGupCfgBaIy_NDc4OTU0NTE3" }, "status": { "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" @@ -8064,6 +8064,9 @@ "maxLength": 256, "minLength": 1, "type": "string" + }, + "tzid": { + "$ref": "#/components/schemas/TimeZone" } }, "type": "object" @@ -8746,7 +8749,7 @@ "VersionInfo_NTEzMTgzNDQ0": { "example": { "development": [ - 17 + 18 ], "domain": "example.com", "federation": false, @@ -8768,7 +8771,8 @@ 14, 15, 16, - 17 + 17, + 18 ] }, "properties": { @@ -8818,29 +8822,34 @@ 14, 15, 16, - 17 + 17, + 18 ], "type": "integer" }, - "Versiond_17_PvtAmlGupCfgBaIy_LTY1MTkyNDcw": { + "Versiond_16_PvtAmlGupCfgBaIy_NDc4OTU0NTE3": { "properties": { - "deletionTimeoutDuration": { - "type": "string" + "deletionTimeout": { + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" }, "promotionStrategy": { "$ref": "#/components/schemas/PreventAdminlessGroupsPromotionStrategy_Mjc1ODA2MDU1" }, - "reminderTimeoutDurations": { + "reminderTimeouts": { "items": { - "type": "string" + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" }, "type": "array" } }, "required": [ "promotionStrategy", - "deletionTimeoutDuration", - "reminderTimeoutDurations" + "deletionTimeout", + "reminderTimeouts" ], "type": "object" }, @@ -15160,8 +15169,8 @@ "summary": "Add qualified members to an existing conversation." }, "put": { - "description": " [internal route ID: \"replace-members-in-conversation\"]\n\nThis will add any members not already in the conversation, and remove any members not in the provided list except users that are associated via a user group. The given role in the request body will be applied to all added members. The roles of already existing members will not be changed even if these members are included in the request body and their role differs from the role provided in this request.", - "operationId": "replace-members-in-conversation", + "description": " [internal route ID: \"replace-members-in-conversation@v16\"]\n\nThis will add any members not already in the conversation, and remove any members not in the provided list except users that are associated via a user group. The given role in the request body will be applied to all added members. The roles of already existing members will not be changed even if these members are included in the request body and their role differs from the role provided in this request.", + "operationId": "replace-members-in-conversation@v16", "parameters": [ { "in": "path", @@ -15273,7 +15282,7 @@ } } }, - "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nMaximum number of members per conversation reached (label: `too-many-members`)\n\nInvalid operation (label: `invalid-op`)\n\nThe conversation would be left without an admin\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nInsufficient authorization (missing add_conversation_member) (label: `action-denied`)" + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nMaximum number of members per conversation reached (label: `too-many-members`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nInsufficient authorization (missing add_conversation_member) (label: `action-denied`)" }, "404": { "content": { @@ -15750,7 +15759,7 @@ }, "/conversations/{cnv_domain}/{cnv}/name": { "put": { - "description": " [internal route ID: \"update-conversation-name\"]\n\n", + "description": " [internal route ID: \"update-conversation-name\"]\n\n\nOAuth scope: `write:conversations_name`", "operationId": "update-conversation-name", "parameters": [ { @@ -19804,7 +19813,7 @@ }, "/meetings": { "post": { - "description": " [internal route ID: \"create-meeting\"]\n\n", + "description": " [internal route ID: \"create-meeting\"]\n\n\nOAuth scope: `write:meetings`", "operationId": "create-meeting", "requestBody": { "content": { @@ -26007,8 +26016,8 @@ }, "/register": { "post": { - "description": " [internal route ID: \"register\"]\n\nIf the environment where the registration takes place is private and a registered email address is not whitelisted, a 403 error is returned.", - "operationId": "register", + "description": " [internal route ID: \"register@v16\"]\n\nIf the environment where the registration takes place is private and a registered email address is not whitelisted, a 403 error is returned.", + "operationId": "register@v16", "parameters": [ { "in": "header", @@ -33963,8 +33972,8 @@ "summary": "Get config for preventAdminlessGroups" }, "put": { - "description": " [internal route ID: \"put-PreventAdminlessGroupsConfig@v17\"]\n\n

For API version 17, use duration strings for the timeout fields. The request body must have the following shape:

{\n  "config": {\n    "deletionTimeoutDuration": "7d",\n    "promotionStrategy": "alphabetical",\n    "reminderTimeoutDurations": [\n      "2d",\n      "4d",\n      "6d"\n    ]\n  },\n  "status": "enabled"\n}

Older API versions use the legacy numeric fields deletionTimeout and reminderTimeouts.

", - "operationId": "put-PreventAdminlessGroupsConfig@v17", + "description": " [internal route ID: \"put-PreventAdminlessGroupsConfig@v16\"]\n\n", + "operationId": "put-PreventAdminlessGroupsConfig@v16", "parameters": [ { "in": "path", @@ -33980,7 +33989,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Featur_Vsiond_17PvAmlGpCfgBIy_LTIzMjQ4MDk1V17" + "$ref": "#/components/schemas/Featur_Vsiond_16PvAmlGpCfgBIy_MjY0MDMyNDA1V16" } } }, diff --git a/services/brig/docs/swagger-v18.json b/services/brig/docs/swagger-v18.json new file mode 100644 index 00000000000..555fae62371 --- /dev/null +++ b/services/brig/docs/swagger-v18.json @@ -0,0 +1,40122 @@ +{ + "components": { + "schemas": { + "ASCII": { + "example": "aGVsbG8", + "type": "string" + }, + "AcceptTeamInvitation_Nzg5NzI3MjA2": { + "description": "Accept an invitation to join a team on Wire.", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "password": { + "description": "The user account password.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "code", + "password" + ], + "type": "object" + }, + "AccessRoleLegacy_LTYwOTAxMDI1": { + "deprecated": true, + "description": "Deprecated, please use access_role_v2", + "enum": [ + "private", + "team", + "activated", + "non_activated" + ], + "type": "string" + }, + "AccessRole_Mzk3MDYzMzcw": { + "description": "Which users/services can join conversations. This replaces legacy access roles and allows a more fine grained configuration of access roles, and in particular a separation of guest and services access.\n\nThis field is optional. If it is not present, the default will be `[team_member, non_team_member, service]`. Please note that an empty list is not allowed when creating a new conversation.", + "enum": [ + "team_member", + "non_team_member", + "guest", + "service" + ], + "type": "string" + }, + "AccessTokenType_LTgyOTY0NDE5": { + "enum": [ + "DPoP" + ], + "type": "string" + }, + "AccessToken_ODIyMTczMjMw": { + "properties": { + "access_token": { + "description": "The opaque access token string", + "type": "string" + }, + "expires_in": { + "description": "The number of seconds this token is valid", + "type": "integer" + }, + "token_type": { + "$ref": "#/components/schemas/TokenType_NTkyMzk4MjIz" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "access_token", + "token_type", + "expires_in" + ], + "type": "object" + }, + "Access_NjkyMzE5ODc0": { + "description": "How users can join conversations", + "enum": [ + "private", + "invite", + "link", + "code" + ], + "type": "string" + }, + "AccountStatus_NzkzNDU1ODU5": { + "enum": [ + "active", + "suspended", + "deleted", + "ephemeral", + "pending-invitation" + ], + "type": "string" + }, + "Action": { + "enum": [ + "add_conversation_member", + "remove_conversation_member", + "modify_conversation_name", + "modify_conversation_message_timer", + "modify_conversation_receipt_mode", + "modify_conversation_access", + "modify_other_conversation_member", + "leave_conversation", + "delete_conversation", + "modify_add_permission" + ], + "type": "string" + }, + "Activate_MzUzNzIxODUw": { + "description": "Data for an activation request.", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "dryrun": { + "description": "At least one of key, email, or phone has to be present while key takes precedence over email, and email takes precedence over phone. Whether to perform a dryrun, i.e. to only check whether activation would succeed. Dry-runs never issue access cookies or tokens on success but failures still count towards the maximum failure count.", + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "code", + "dryrun" + ], + "type": "object" + }, + "ActivationResponse_LTIyOTY5NDE3": { + "description": "Response body of a successful activation request", + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "first": { + "description": "Whether this is the first successful activation (i.e. account activation).", + "type": "boolean" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + } + }, + "type": "object" + }, + "AddBotResponse_ODA5MzA2NTA1": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" + }, + "client": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "event": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "client", + "name", + "accent_id", + "assets", + "event" + ], + "type": "object" + }, + "AddBot_NjI0ODkyODk3": { + "properties": { + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + }, + "service": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "provider", + "service" + ], + "type": "object" + }, + "AddPermissionUpdate_LTU3MzEwOTY4": { + "description": "The action of changing the permission to add members to a channel", + "properties": { + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + } + }, + "required": [ + "add_permission" + ], + "type": "object" + }, + "AddPermission_LTE1MzgzNzE3": { + "enum": [ + "admins", + "everyone" + ], + "type": "string" + }, + "AdminlessReminder_LTkyMDUxNTk5": { + "properties": { + "deletion_scheduled_for": { + "$ref": "#/components/schemas/UTCTimeMillis" + } + }, + "required": [ + "deletion_scheduled_for" + ], + "type": "object" + }, + "AllowedGlobalOperationsConfig_MzAwOTU1MDkx": { + "properties": { + "mlsConversationReset": { + "type": "boolean" + } + }, + "required": [ + "mlsConversationReset" + ], + "type": "object" + }, + "Alpha_LTE4NDUxNDQ4": { + "description": "ISO 4217 alphabetic codes. This is only stored by the backend, not processed. It can be removed once billing supports currency changes after team creation.", + "enum": [ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BOV", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHE", + "CHF", + "CHW", + "CLF", + "CLP", + "CNY", + "COP", + "COU", + "CRC", + "CUC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MUR", + "MVR", + "MWK", + "MXN", + "MXV", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "SSP", + "STD", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "USN", + "UYI", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XBA", + "XBB", + "XBC", + "XBD", + "XCD", + "XDR", + "XOF", + "XPD", + "XPF", + "XPT", + "XSU", + "XTS", + "XUA", + "XXX", + "YER", + "ZAR", + "ZMW", + "ZWL" + ], + "example": "EUR", + "type": "string" + }, + "AppInfo_MjgwNTkwOTUz": { + "properties": { + "category": { + "description": "Category name (if uncertain, pick \"other\")", + "type": "string" + }, + "description": { + "maxLength": 300, + "minLength": 0, + "type": "string" + } + }, + "required": [ + "category", + "description" + ], + "type": "object" + }, + "AppLockConfigB_Covered_Identity_NDIxOTc2Njkz": { + "properties": { + "enforceAppLock": { + "type": "boolean" + }, + "inactivityTimeoutSecs": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforceAppLock", + "inactivityTimeoutSecs" + ], + "type": "object" + }, + "ApproveLegalHoldForUserRequest_NjEyNzYyMTIx": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "AssetKey": { + "description": "S3 asset key for an icon image with retention information.", + "example": "3-1-47de4580-ae51-4650-acbb-d10c028cb0ac", + "type": "string" + }, + "AssetSize_OTAwMDA3ODY2": { + "enum": [ + "preview", + "complete" + ], + "type": "string" + }, + "AssetSource": {}, + "Asset_LTIyMjc1NDEz": { + "properties": { + "key": { + "$ref": "#/components/schemas/AssetKey" + }, + "size": { + "$ref": "#/components/schemas/AssetSize_OTAwMDA3ODY2" + }, + "type": { + "$ref": "#/components/schemas/MTYxOTI3NjM3" + } + }, + "required": [ + "key", + "type" + ], + "type": "object" + }, + "Asset_Qualified_AssetKey_MzU1MjMxNTA5": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "expires": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "key": { + "$ref": "#/components/schemas/AssetKey" + }, + "token": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "key", + "domain" + ], + "type": "object" + }, + "AuthSFTServer_LTY5MzcyOTE0": { + "description": "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers", + "properties": { + "credential": { + "$ref": "#/components/schemas/ASCII" + }, + "urls": { + "description": "Array containing exactly one SFT server address of the form 'https://:'", + "items": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "type": "array" + }, + "username": { + "$ref": "#/components/schemas/SFTUsername" + } + }, + "required": [ + "urls" + ], + "type": "object" + }, + "AuthnRequest": { + "properties": { + "iD": { + "$ref": "#/components/schemas/Id_AuthnRequest" + }, + "issueInstant": { + "$ref": "#/components/schemas/Time" + }, + "issuer": { + "$ref": "#/components/schemas/URI" + }, + "nameIDPolicy": { + "$ref": "#/components/schemas/NameIdPolicy" + } + }, + "required": [ + "iD", + "issueInstant", + "issuer" + ], + "type": "object" + }, + "Base64ByteString": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "Base64URLByteString": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "BaseProtocolTag_LTM0MDE1NTEx": { + "enum": [ + "proteus", + "mls" + ], + "type": "string" + }, + "BindingNewTeamUser_LTY0MDQxMDEw": { + "properties": { + "currency": { + "$ref": "#/components/schemas/Alpha_LTE4NDUxNDQ4" + }, + "icon": { + "$ref": "#/components/schemas/Icon" + }, + "icon_key": { + "description": "The decryption key for the team icon S3 asset", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "name": { + "description": "team name", + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "icon" + ], + "type": "object" + }, + "BotConvView_LTYzMjIzMjQz": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "members": { + "items": { + "$ref": "#/components/schemas/OtherMember_LTgzNzE2MTk4" + }, + "type": "array" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "members" + ], + "type": "object" + }, + "BotUserView_LTE2MTkwMTcw": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id", + "name", + "accent_id" + ], + "type": "object" + }, + "CellsBackend_LTE1Nzg3NzQ2": { + "properties": { + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "url" + ], + "type": "object" + }, + "CellsCollaboraStatus_MTgzNTQyNzUz": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "CellsCollabora_LTMzNDA5MDIz": { + "properties": { + "edition": { + "$ref": "#/components/schemas/CollaboraEdition_LTg2NDA1NDQ4" + } + }, + "required": [ + "edition" + ], + "type": "object" + }, + "CellsConfigB_Covered_Identity_LTE1NzkwOTcz": { + "example": { + "channels": { + "default": "enabled", + "enabled": true + }, + "collabora": { + "enabled": false + }, + "groups": { + "default": "enabled", + "enabled": true + }, + "metadata": { + "namespaces": { + "usermetaTags": { + "allowFreeValues": true, + "defaultValues": [] + } + } + }, + "one2one": { + "default": "enabled", + "enabled": true + }, + "publicLinks": { + "enableFiles": true, + "enableFolders": true, + "enforceExpirationDefault": 0, + "enforceExpirationMax": 0, + "enforcePassword": false + }, + "storage": { + "perFileQuotaBytes": "100000000", + "recycle": { + "allowSkip": false, + "autoPurgeDays": 30, + "disable": false + } + }, + "users": { + "externals": true, + "guests": false + } + }, + "properties": { + "channels": { + "$ref": "#/components/schemas/CellsProperty_NzcxMDIzMzk0" + }, + "collabora": { + "$ref": "#/components/schemas/CellsCollaboraStatus_MTgzNTQyNzUz" + }, + "groups": { + "$ref": "#/components/schemas/CellsProperty_NzcxMDIzMzk0" + }, + "metadata": { + "$ref": "#/components/schemas/CellsMetadata_LTY1OTM5MTM0" + }, + "one2one": { + "$ref": "#/components/schemas/CellsProperty_NzcxMDIzMzk0" + }, + "publicLinks": { + "$ref": "#/components/schemas/CellsPublicLinks_MjgxMzQ3Mzk4" + }, + "storage": { + "$ref": "#/components/schemas/CellsConfigStorage_LTM0NDMwODM4" + }, + "users": { + "$ref": "#/components/schemas/CellsUsers_LTQ4NTEyODA1" + } + }, + "required": [ + "channels", + "groups", + "one2one", + "users", + "collabora", + "publicLinks", + "storage", + "metadata" + ], + "type": "object" + }, + "CellsConfigStorage_LTM0NDMwODM4": { + "properties": { + "perFileQuotaBytes": { + "type": "string" + }, + "recycle": { + "$ref": "#/components/schemas/CellsRecycle_LTQxMTg3NTkx" + } + }, + "required": [ + "perFileQuotaBytes", + "recycle" + ], + "type": "object" + }, + "CellsInternalConfigB_Covered_Identity_LTUzMDkwOTAz": { + "properties": { + "backend": { + "$ref": "#/components/schemas/CellsBackend_LTE1Nzg3NzQ2" + }, + "collabora": { + "$ref": "#/components/schemas/CellsCollabora_LTMzNDA5MDIz" + }, + "storage": { + "$ref": "#/components/schemas/CellsStorage_LTY2Mzc5NzY1" + } + }, + "required": [ + "backend", + "collabora", + "storage" + ], + "type": "object" + }, + "CellsMetadata_LTY1OTM5MTM0": { + "properties": { + "namespaces": { + "$ref": "#/components/schemas/CellsNamespaces_MzUxMjEzOTQw" + } + }, + "required": [ + "namespaces" + ], + "type": "object" + }, + "CellsNamespaces_MzUxMjEzOTQw": { + "properties": { + "usermetaTags": { + "$ref": "#/components/schemas/CellsUserMetaTags_LTc4Njk4NTY0" + } + }, + "required": [ + "usermetaTags" + ], + "type": "object" + }, + "CellsPropertyStatus_MTQ5NjE2MzQ4": { + "enum": [ + "enabled", + "disabled", + "enforced" + ], + "type": "string" + }, + "CellsProperty_NzcxMDIzMzk0": { + "properties": { + "default": { + "$ref": "#/components/schemas/CellsPropertyStatus_MTQ5NjE2MzQ4" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "enabled", + "default" + ], + "type": "object" + }, + "CellsPublicLinks_MjgxMzQ3Mzk4": { + "properties": { + "enableFiles": { + "type": "boolean" + }, + "enableFolders": { + "type": "boolean" + }, + "enforceExpirationDefault": { + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "enforceExpirationMax": { + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "enforcePassword": { + "type": "boolean" + } + }, + "required": [ + "enableFiles", + "enableFolders", + "enforcePassword", + "enforceExpirationMax", + "enforceExpirationDefault" + ], + "type": "object" + }, + "CellsRecycle_LTQxMTg3NTkx": { + "properties": { + "allowSkip": { + "type": "boolean" + }, + "autoPurgeDays": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "disable": { + "type": "boolean" + } + }, + "required": [ + "autoPurgeDays", + "disable", + "allowSkip" + ], + "type": "object" + }, + "CellsState_LTg4MDEwNDA5": { + "enum": [ + "disabled", + "pending", + "ready" + ], + "type": "string" + }, + "CellsStorage_LTY2Mzc5NzY1": { + "properties": { + "perUserQuotaBytes": { + "example": "-1", + "type": "string" + }, + "totalLimitBytes": { + "example": "-1", + "type": "string" + } + }, + "required": [ + "perUserQuotaBytes" + ], + "type": "object" + }, + "CellsUserMetaTags_LTc4Njk4NTY0": { + "properties": { + "allowFreeValues": { + "type": "boolean" + }, + "defaultValues": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "defaultValues", + "allowFreeValues" + ], + "type": "object" + }, + "CellsUsers_LTQ4NTEyODA1": { + "properties": { + "externals": { + "type": "boolean" + }, + "guests": { + "type": "boolean" + } + }, + "required": [ + "externals", + "guests" + ], + "type": "object" + }, + "ChallengeToken_Mzk3NTcwOTM3": { + "properties": { + "challenge_token": { + "$ref": "#/components/schemas/Token" + } + }, + "required": [ + "challenge_token" + ], + "type": "object" + }, + "ChannelPermissions_Mzc1MTM3NTg2": { + "enum": [ + "team-members", + "everyone", + "admins" + ], + "type": "string" + }, + "ChannelsConfigB_Covered_Identity_ODk2MTk3NDQ4": { + "properties": { + "allowed_to_create_channels": { + "$ref": "#/components/schemas/ChannelPermissions_Mzc1MTM3NTg2" + }, + "allowed_to_open_channels": { + "$ref": "#/components/schemas/ChannelPermissions_Mzc1MTM3NTg2" + } + }, + "required": [ + "allowed_to_create_channels", + "allowed_to_open_channels" + ], + "type": "object" + }, + "CheckHandles_LTc0OTkxMzAx": { + "properties": { + "handles": { + "items": { + "type": "string" + }, + "maxItems": 50, + "minItems": 1, + "type": "array" + }, + "return": { + "maximum": 10, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "handles", + "return" + ], + "type": "object" + }, + "CheckUserGroupName_LTg0ODU1OTk1": { + "properties": { + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "CipherSuiteTag": { + "description": "The cipher suite of the corresponding MLS group", + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "ClassifiedDomainsConfig_LTg4MDcwMDg2": { + "properties": { + "domains": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "domains" + ], + "type": "object" + }, + "ClientCapabilityList": { + "items": { + "$ref": "#/components/schemas/ClientCapability_MTY2NDAzMjM3" + }, + "type": "array" + }, + "ClientCapability_MTY2NDAzMjM3": { + "enum": [ + "legalhold-implicit-consent", + "consumable-notifications" + ], + "type": "string" + }, + "ClientClass_NjE3MDgwNzcx": { + "enum": [ + "phone", + "tablet", + "desktop", + "legalhold" + ], + "type": "string" + }, + "ClientIdentity_MjAxMjI3NTUw": { + "properties": { + "client_id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "user_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "user_id", + "client_id" + ], + "type": "object" + }, + "ClientMismatch_ODUyODM0MDQ0": { + "properties": { + "deleted": { + "$ref": "#/components/schemas/UserClients" + }, + "missing": { + "$ref": "#/components/schemas/UserClients" + }, + "redundant": { + "$ref": "#/components/schemas/UserClients" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + } + }, + "required": [ + "time", + "missing", + "redundant", + "deleted" + ], + "type": "object" + }, + "ClientPrekey_LTcyODUzMTcw": { + "properties": { + "client": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "prekey": { + "$ref": "#/components/schemas/UncheckedPrekeyBundle_LTU1MzQzOTgy" + } + }, + "required": [ + "client", + "prekey" + ], + "type": "object" + }, + "ClientType_MjQ0OTQwMzcw": { + "enum": [ + "temporary", + "permanent", + "legalhold" + ], + "type": "string" + }, + "Client_MTM1OTcwOTQ1": { + "properties": { + "capabilities": { + "$ref": "#/components/schemas/ClientCapabilityList" + }, + "class": { + "$ref": "#/components/schemas/ClientClass_NjE3MDgwNzcx" + }, + "cookie": { + "type": "string" + }, + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "label": { + "type": "string" + }, + "last_active": { + "$ref": "#/components/schemas/UTCTime" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "model": { + "type": "string" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "type": { + "$ref": "#/components/schemas/ClientType_MjQ0OTQwMzcw" + } + }, + "required": [ + "id", + "type", + "time" + ], + "type": "object" + }, + "CodeChallengeMethod_NTIxNzk0NDgw": { + "description": "The method used to encode the code challenge. Only `S256` is supported.", + "enum": [ + "S256" + ], + "type": "string" + }, + "CollaboraEdition_LTg2NDA1NDQ4": { + "enum": [ + "NO", + "CODE", + "COOL" + ], + "type": "string" + }, + "CollaboratorPermission_NDg5NTg2ODgy": { + "description": "

Permission granted to a team collaborator.

  • `create_team_conversation`: equivalent to the `CreateConversation` and `AddRemoveConvMember` permissions for team members (both implied in the `member` role); allows creating team group conversations and adding members to them.
  • \n
  • `implicit_connection`: team members are implicitly connected to each other, allowing conversations (1:1 or group) without an explicit connection request. This permission grants the same to a collaborator.
\n

NB: a member of team A can always open conversations with a collaborator of team A; the permission only controls the collaborator's abilities.

", + "enum": [ + "create_team_conversation", + "implicit_connection" + ], + "type": "string" + }, + "CommitBundle": { + "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." + }, + "CompletePasswordReset_LTYzMDAxNDA1": { + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "key", + "code", + "password" + ], + "type": "object" + }, + "CompletePasswordReset_NDcyMjY5OTc4": { + "description": "Data to complete a password reset", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "password": { + "description": "New password (6 - 1024 characters)", + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "phone": { + "$ref": "#/components/schemas/PhoneNumber" + } + }, + "required": [ + "code", + "password" + ], + "type": "object" + }, + "ConferenceCallingConfigB_Covered_Identity_NDMwMjcyMzM1": { + "properties": { + "useSFTForOneToOneCalls": { + "type": "boolean" + } + }, + "type": "object" + }, + "Connect_ODY3OTE4NTYx": { + "properties": { + "email": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "qualified_recipient": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "recipient": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "qualified_recipient" + ], + "type": "object" + }, + "ConnectionUpdate_LTU3MTA1OTA5": { + "properties": { + "status": { + "$ref": "#/components/schemas/Relation_LTE4OTU5MTk4" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Connections_PagingState": { + "type": "string" + }, + "ContactStatusState_LTg2MjAyNzAx": { + "enum": [ + "contactable", + "non-contactable" + ], + "type": "string" + }, + "ContactStatus_LTUzNzk1MzM4": { + "properties": { + "state": { + "$ref": "#/components/schemas/ContactStatusState_LTg2MjAyNzAx" + } + }, + "required": [ + "state" + ], + "type": "object" + }, + "Contact_LTcwODE3Mjc5": { + "description": "Contact discovered through search", + "properties": { + "accent_id": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "handle": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/UserType_LTU1OTU4OTM5" + } + }, + "required": [ + "qualified_id", + "name", + "type" + ], + "type": "object" + }, + "ConvMembers_LTc2MDg1NDg2": { + "description": "Users of a conversation", + "properties": { + "others": { + "description": "All other current users of this conversation", + "items": { + "$ref": "#/components/schemas/OtherMember_LTgzNzE2MTk4" + }, + "type": "array" + }, + "self": { + "$ref": "#/components/schemas/Member_OTA5OTgyNzcw" + } + }, + "required": [ + "others" + ], + "type": "object" + }, + "ConvTeamInfo_Mzc5NjcyNjAz": { + "description": "Team information of this conversation", + "properties": { + "managed": { + "description": "This field MUST NOT be used by clients. It is here only for backwards compatibility of the interface." + }, + "teamid": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "teamid", + "managed" + ], + "type": "object" + }, + "ConvType_MzM0NTE3ODE5": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer" + }, + "ConversationAccessData_MjMxMTI5ODc3": { + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + } + }, + "required": [ + "access", + "access_role" + ], + "type": "object" + }, + "ConversationCodeInfo_LTc5MzgzNjg3": { + "description": "Contains conversation properties to update", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "has_password": { + "description": "Whether the conversation has a password", + "type": "boolean" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "uri": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "key", + "code", + "uri", + "has_password" + ], + "type": "object" + }, + "ConversationCode_Mjg3OTI1NTMx": { + "description": "Contains conversation properties to update", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "ConversationCoverView_LTMwNDkxMTA1": { + "description": "Limited view of Conversation.", + "properties": { + "has_password": { + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "has_password" + ], + "type": "object" + }, + "ConversationHistoryUpdate_LTg5MDQ5Nzgx": { + "properties": { + "history": { + "$ref": "#/components/schemas/History" + } + }, + "required": [ + "history" + ], + "type": "object" + }, + "ConversationIds_PagingState": { + "type": "string" + }, + "ConversationMessageTimerUpdate_LTcxMjUwNzQ4": { + "description": "Contains conversation properties to update", + "properties": { + "message_timer": { + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "type": "object" + }, + "ConversationPage_LTIwMDU2NDI3": { + "description": "This is the last page if it contains fewer rows than requested. There may be 0 rows on a page.", + "properties": { + "page": { + "items": { + "$ref": "#/components/schemas/ConversationSearchResult_NDI0MTcyMDU3" + }, + "type": "array" + } + }, + "required": [ + "page" + ], + "type": "object" + }, + "ConversationReceiptModeUpdate_NDE4MzUzNTU3": { + "description": "Contains conversation receipt mode to update to. Receipt mode tells clients whether certain types of receipts should be sent in the given conversation or not. How this value is interpreted is up to clients.", + "properties": { + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "receipt_mode" + ], + "type": "object" + }, + "ConversationRename_ODkwODg1MzQ0": { + "properties": { + "name": { + "description": "The new conversation name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "ConversationReset_MzU1Nzc5MjAw": { + "properties": { + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "new_group_id": { + "$ref": "#/components/schemas/GroupId" + } + }, + "required": [ + "group_id" + ], + "type": "object" + }, + "ConversationRole": { + "properties": { + "actions": { + "description": "The set of actions allowed for this role", + "items": { + "$ref": "#/components/schemas/Action" + }, + "type": "array" + }, + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + } + } + }, + "ConversationRolesList": { + "properties": { + "conversation_roles": { + "items": { + "$ref": "#/components/schemas/ConversationRole" + }, + "type": "array" + } + }, + "required": [ + "conversation_roles" + ], + "type": "object" + }, + "ConversationSearchResult_NDI0MTcyMDU3": { + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "admin_count": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "member_count": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "access", + "member_count", + "admin_count" + ], + "type": "object" + }, + "Conversation_GroupConvType_MzQzMTQ1OTg3": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/ConvMembers_LTc2MDg1NDg2" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + }, + "ConversationsResponse_GroupConvType_ODkxMjM2ODM0": { + "description": "Response object for getting metadata of a list of conversations", + "properties": { + "failed": { + "description": "The server failed to fetch these conversations, most likely due to network issues while contacting a remote server", + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "type": "array" + }, + "found": { + "items": { + "$ref": "#/components/schemas/OwnConversation_GroupConvType_LTU2MzYxNTg0" + }, + "type": "array" + }, + "not_found": { + "description": "These conversations either don't exist or are deleted.", + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "type": "array" + } + }, + "required": [ + "found", + "not_found", + "failed" + ], + "type": "object" + }, + "CookieList_LTM4MzYwNzAz": { + "description": "List of cookie information", + "properties": { + "cookies": { + "items": { + "$ref": "#/components/schemas/Cookie_LTkyMDA3OTI5" + }, + "type": "array" + } + }, + "required": [ + "cookies" + ], + "type": "object" + }, + "CookieType_LTE0MjczNzY3": { + "enum": [ + "session", + "persistent" + ], + "type": "string" + }, + "Cookie_LTkyMDA3OTI5": { + "properties": { + "created": { + "$ref": "#/components/schemas/UTCTime" + }, + "expires": { + "$ref": "#/components/schemas/UTCTime" + }, + "id": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "label": { + "type": "string" + }, + "successor": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "type": { + "$ref": "#/components/schemas/CookieType_LTE0MjczNzY3" + } + }, + "required": [ + "id", + "type", + "created", + "expires" + ], + "type": "object" + }, + "CreateConversationCodeRequest_NTYzMTA1NDYz": { + "description": "Request body for creating a conversation code", + "properties": { + "password": { + "description": "Password for accessing the conversation via guest link. Set to null or omit for no password.", + "maxLength": 1024, + "minLength": 8, + "type": "string" + } + }, + "type": "object" + }, + "CreateGroupConversation_GroupConvType_MTQ3OTc4NDk3": { + "description": "A created group-conversation object extended with a list of failed-to-add users", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "failed_to_add": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "type": "array" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/ConvMembers_LTc2MDg1NDg2" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch", + "failed_to_add" + ], + "type": "object" + }, + "CreateOAuthAuthorizationCodeRequest_LTQ3NzI1Mjkz": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "code_challenge": { + "$ref": "#/components/schemas/OAuthCodeChallenge" + }, + "code_challenge_method": { + "$ref": "#/components/schemas/CodeChallengeMethod_NTIxNzk0NDgw" + }, + "redirect_uri": { + "$ref": "#/components/schemas/RedirectUrl" + }, + "response_type": { + "$ref": "#/components/schemas/OAuthResponseType_ODI2Mjg3NzQx" + }, + "scope": { + "description": "The scopes which are requested to get authorization for, separated by a space", + "type": "string" + }, + "state": { + "description": "An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery", + "type": "string" + } + }, + "required": [ + "client_id", + "scope", + "response_type", + "redirect_uri", + "state", + "code_challenge_method", + "code_challenge" + ], + "type": "object" + }, + "CreateScimTokenResponse_LTIzOTU2NDU4": { + "properties": { + "info": { + "$ref": "#/components/schemas/ScimTokenInfo_LTI5NjgwNzA1" + }, + "token": { + "type": "string" + } + }, + "required": [ + "token", + "info" + ], + "type": "object" + }, + "CreateScimToken_OTY0NjYxMDQ2": { + "properties": { + "description": { + "type": "string" + }, + "idp": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "description" + ], + "type": "object" + }, + "CreateUserTeam_MzI4NDQ1Mzkw": { + "properties": { + "team_id": { + "$ref": "#/components/schemas/UUID" + }, + "team_name": { + "type": "string" + } + }, + "required": [ + "team_id", + "team_name" + ], + "type": "object" + }, + "CreatedApp_LTM3NjUxOTY1": { + "properties": { + "cookie": { + "$ref": "#/components/schemas/SomeUserToken" + }, + "user": { + "$ref": "#/components/schemas/UserProfile_LTQzMTQxMTE1" + } + }, + "required": [ + "user", + "cookie" + ], + "type": "object" + }, + "CustomBackend_LTQxODI0MjQ0": { + "description": "Description of a custom backend", + "properties": { + "config_json_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "webapp_welcome_url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "config_json_url", + "webapp_welcome_url" + ], + "type": "object" + }, + "DPoPAccessToken": { + "type": "string" + }, + "DPoPAccessTokenResponse_LTgyODU5MDE3": { + "properties": { + "expires_in": { + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "token": { + "$ref": "#/components/schemas/DPoPAccessToken" + }, + "type": { + "$ref": "#/components/schemas/AccessTokenType_LTgyOTY0NDE5" + } + }, + "required": [ + "token", + "type", + "expires_in" + ], + "type": "object" + }, + "DeleteKeyPackages_LTQxNTcxNjY3": { + "properties": { + "key_packages": { + "items": { + "$ref": "#/components/schemas/KeyPackageRef" + }, + "maxItems": 1000, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "key_packages" + ], + "type": "object" + }, + "DeleteProvider_MzYxMzM3Mjg2": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "DeleteService_LTY2NzY5NzMz": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "DeleteUser_NjE0MjE2Mjkz": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "DeletionCodeTimeout_LTU1MTk0NDI3": { + "properties": { + "expires_in": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "expires_in" + ], + "type": "object" + }, + "DisableLegalHoldForUserRequest_LTYyMDYxOTEy": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "Domain": { + "example": "example.com", + "type": "string" + }, + "DomainOwnershipToken_NTU0ODc1NDE5": { + "properties": { + "domain_ownership_token": { + "$ref": "#/components/schemas/Token" + } + }, + "required": [ + "domain_ownership_token" + ], + "type": "object" + }, + "DomainRedirectConfigTag_MjE2MDI4MDIw": { + "enum": [ + "remove", + "backend", + "no-registration" + ], + "type": "string" + }, + "DomainRedirectConfig_NTI5NDE5MDQy": { + "properties": { + "backend": { + "$ref": "#/components/schemas/HttpsUrl_HttpsUrl_NjUyMDgzNzk3" + }, + "domain_redirect": { + "$ref": "#/components/schemas/DomainRedirectConfigTag_MjE2MDI4MDIw" + } + }, + "required": [ + "domain_redirect", + "backend" + ], + "type": "object" + }, + "DomainRedirectResponse_V10_LTEyMjI4NTM0": { + "properties": { + "backend": { + "$ref": "#/components/schemas/HttpsUrl_Maybe_HttpsUrl_LTQ1MDkyMzY2" + }, + "domain_redirect": { + "$ref": "#/components/schemas/DomainRedirectTag_LTY3NjU1MDEy" + }, + "due_to_existing_account": { + "type": "boolean" + }, + "sso_code": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain_redirect", + "sso_code", + "backend" + ], + "type": "object" + }, + "DomainRedirectTag_LTY3NjU1MDEy": { + "enum": [ + "none", + "locked", + "sso", + "backend", + "no-registration", + "pre-authorized" + ], + "type": "string" + }, + "DomainRegistrationResponse_V10_MjE0NDkxODY4": { + "properties": { + "authorized_team": { + "$ref": "#/components/schemas/UUID" + }, + "backend": { + "$ref": "#/components/schemas/HttpsUrl_Maybe_HttpsUrl_LTQ1MDkyMzY2" + }, + "dns_verification_token": { + "$ref": "#/components/schemas/ASCII" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "domain_redirect": { + "$ref": "#/components/schemas/DomainRedirectTag_LTY3NjU1MDEy" + }, + "sso_code": { + "$ref": "#/components/schemas/UUID" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "team_invite": { + "$ref": "#/components/schemas/TeamInviteTag_LTQyNTMyNzA0" + } + }, + "required": [ + "domain", + "domain_redirect", + "sso_code", + "backend", + "team_invite", + "team" + ], + "type": "object" + }, + "DomainVerificationChallenge_NjIwMzA1MjE5": { + "properties": { + "dns_verification_token": { + "$ref": "#/components/schemas/ASCII" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "token": { + "$ref": "#/components/schemas/Token" + } + }, + "required": [ + "id", + "token", + "dns_verification_token" + ], + "type": "object" + }, + "EdMemberLeftReason_OTAyMDA4NzEw": { + "enum": [ + "left", + "user-deleted", + "removed" + ], + "type": "string" + }, + "EdMemberLeftReason_QualifiedUserIdList_LTYyOTg2OTQ1": { + "properties": { + "qualified_user_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "type": "array" + }, + "reason": { + "$ref": "#/components/schemas/EdMemberLeftReason_OTAyMDA4NzEw" + }, + "user_ids": { + "deprecated": true, + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "reason", + "qualified_user_ids", + "user_ids" + ], + "type": "object" + }, + "Either_OAuthAccessTokenRequest_OAuthRefreshAccessTokenRequest": { + "oneOf": [ + { + "properties": { + "Left": { + "$ref": "#/components/schemas/OAuthAccessTokenRequest_LTYyNTcyMzI4" + } + }, + "required": [ + "Left" + ], + "title": "Left", + "type": "object" + }, + { + "properties": { + "Right": { + "$ref": "#/components/schemas/OAuthRefreshAccessTokenRequest_LTUwNjA3OTI1" + } + }, + "required": [ + "Right" + ], + "title": "Right", + "type": "object" + } + ] + }, + "Email": { + "type": "string" + }, + "EmailUpdate_LTYwODE0ODQ5": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "EmailUpdate_NjQ5MDg1OTY0": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "EnforceFilDwadLtCgB_vIy_LTQ2NjU1Nzcx": { + "properties": { + "enforcedDownloadLocation": { + "type": "string" + } + }, + "type": "object" + }, + "EpochTimestamp": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + }, + "EventType_LTQ3NTQyNDYz": { + "enum": [ + "conversation.member-join", + "conversation.member-leave", + "conversation.member-update", + "conversation.rename", + "conversation.access-update", + "conversation.receipt-mode-update", + "conversation.message-timer-update", + "conversation.code-update", + "conversation.code-delete", + "conversation.create", + "conversation.create-meeting", + "conversation.delete", + "conversation.delete-meeting", + "conversation.mls-reset", + "conversation.connect-request", + "conversation.typing", + "conversation.otr-message-add", + "conversation.mls-message-add", + "conversation.mls-welcome", + "conversation.protocol-update", + "conversation.add-permission-update", + "conversation.history-update", + "conversation.adminless-reminder" + ], + "type": "string" + }, + "EventVia_Mjc4MzcyNzE0": { + "enum": [ + "scim", + "user" + ], + "type": "string" + }, + "Event_LTMwMTMyODM5": { + "properties": { + "conversation": { + "$ref": "#/components/schemas/UUID" + }, + "data": { + "description": "The action of changing the permission to add members to a channel", + "example": "ZXhhbXBsZQo=", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy_LTYwOTAxMDI1" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "add_type": { + "$ref": "#/components/schemas/JoinType_LTY4MDg2MzA5" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "data": { + "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", + "type": "string" + }, + "deletion_scheduled_for": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "depth": { + "$ref": "#/components/schemas/HistoryDuration" + }, + "email": { + "type": "string" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/EpochTimestamp" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "has_password": { + "description": "Whether the conversation has a password", + "type": "boolean" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers_LTEwMzUzODMy" + }, + "message": { + "type": "string" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "new_group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "qualified_recipient": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "qualified_target": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "qualified_user_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "type": "array" + }, + "reason": { + "$ref": "#/components/schemas/EdMemberLeftReason_OTAyMDA4NzEw" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "recipient": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "sender": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/TypingStatus_LTg5MzcyNDMy" + }, + "target": { + "$ref": "#/components/schemas/UUID" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "text": { + "description": "The ciphertext for the recipient (Base64 in JSON)", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" + }, + "uri": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "user_ids": { + "deprecated": true, + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/components/schemas/SimpleMember_NTY5MTcxMzcx" + }, + "type": "array" + } + }, + "required": [ + "users", + "add_type", + "reason", + "qualified_user_ids", + "user_ids", + "qualified_target", + "name", + "access", + "key", + "code", + "uri", + "has_password", + "qualified_id", + "type", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite", + "qualified_recipient", + "receipt_mode", + "sender", + "recipient", + "text", + "status", + "add_permission", + "depth", + "deletion_scheduled_for" + ], + "type": "object" + }, + "from": { + "$ref": "#/components/schemas/UUID" + }, + "qualified_conversation": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "qualified_from": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "subconv": { + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "type": { + "$ref": "#/components/schemas/EventType_LTQ3NTQyNDYz" + }, + "via": { + "$ref": "#/components/schemas/EventVia_Mjc4MzcyNzE0" + } + }, + "required": [ + "type", + "data", + "qualified_conversation", + "qualified_from", + "via", + "time" + ], + "type": "object" + }, + "Featur_EnfocilDwdLCgBIy_MjcxMzc1NDA5": { + "properties": { + "config": { + "$ref": "#/components/schemas/EnforceFilDwadLtCgB_vIy_LTQ2NjU1Nzcx" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "Featur_SlfDingMsCoBIdy_LTg2MzYzNjc2": { + "properties": { + "config": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfigB_Covered_Identity_LTY0NzQ4MzU1" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "Featur_Vsiond_18PvAmlGpCfgBIy_MjUwNjk2MjQwV18": { + "properties": { + "config": { + "$ref": "#/components/schemas/Versiond_18_PvtAmlGupCfgBaIy_LTUyNzk3MzUx" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "FeatureStatus_LTMzMTUwODEw": { + "enum": [ + "enabled", + "disabled" + ], + "type": "string" + }, + "Feature_AppLockConfigB_Bare_Identity_MTAzMjI5NDYy": { + "properties": { + "config": { + "$ref": "#/components/schemas/AppLockConfigB_Covered_Identity_NDIxOTc2Njkz" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "Feature_CellsConfigB_Bare_Identity_NzU2NjgxNzEw": { + "properties": { + "config": { + "$ref": "#/components/schemas/CellsConfigB_Covered_Identity_LTE1NzkwOTcz" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "Feature_ChannelsConfigB_Bare_Identity_LTU4NTE4MTgx": { + "properties": { + "config": { + "$ref": "#/components/schemas/ChannelsConfigB_Covered_Identity_ODk2MTk3NDQ4" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "Feature_ConferenceCallingConfigB_Bare_Identity_Njc2NTcxNTI3": { + "properties": { + "config": { + "$ref": "#/components/schemas/ConferenceCallingConfigB_Covered_Identity_NDMwMjcyMzM1" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_ExposeInvitationURLsToTeamAdminConfig_LTY5NzY2Mzg5": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_FileSharingConfig_LTUyNjkxMzM4": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_GuestLinksConfig_NjQyMDMxNjg3": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_LegalholdConfig_NjM3MTkxNjYw": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_MLSConfigB_Bare_Identity_LTI5MjA3MDYy": { + "properties": { + "config": { + "$ref": "#/components/schemas/MLSConfigB_Covered_Identity_LTEzNTk3MzM5" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "Feature_MeetingsConfig_NDc2MzM0MDE1": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_MlsE2EIdConfigB_Bare_Identity_LTUxODYzODEx": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsE2EIdConfigB_Covered_Identity_NDQ0NjEwNzA3" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "Feature_MlsMigrationConfigB_Bare_Identity_LTQyMDAxMTkz": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsMigrationConfigB_Covered_Identity_LTM3NDQ4Mzg4" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "Feature_OutlookCalIntegrationConfig_LTg0MjIxNTMx": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_SearchVisibilityAvailableConfig_LTMzNTkxODI1": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_SearchVisibilityInboundConfig_MTI1NzQxODY2": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Feature_SndFactorPasswordChallengeConfig_NDc0MzUyMzQz": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "FederatedUserSearchPolicy_MzkwODA4MTM3": { + "description": "Search policy that was applied when searching for users", + "enum": [ + "no_search", + "exact_handle_search", + "full_search" + ], + "type": "string" + }, + "Fingerprint": { + "example": "ioy3GeIjgQRsobf2EKGO3O8mq/FofFxHRqy0T4ERIZ8=", + "type": "string" + }, + "FormRedirect": { + "properties": { + "uri": { + "type": "string" + }, + "xml": { + "$ref": "#/components/schemas/AuthnRequest" + } + }, + "type": "object" + }, + "Frequency_Mzk0ODQwOTM3": { + "enum": [ + "daily", + "weekly", + "monthly", + "yearly" + ], + "type": "string" + }, + "GetByEmailReq_LTY4MzE3Njgy": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "GetByEmailResp_LTMxNTY3MjA0": { + "properties": { + "sso_code": { + "$ref": "#/components/schemas/UUID" + } + }, + "type": "object" + }, + "GetDomainRegistrationRequest_LTg4NTM1MzM2": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "GetMuliTabPgRqs_ConcLOrm501_LTIwMjA4NzYw": { + "description": "A request to list some or all of a user's Connections, including remote ones", + "properties": { + "paging_state": { + "$ref": "#/components/schemas/Connections_PagingState" + }, + "size": { + "description": "optional, must be <= 500, defaults to 100.", + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "GetMuliTabPgRqs_ConvrIdLcOm10_ODU0NjQ1NDMz": { + "description": "A request to list some or all of a user's ConversationIds, including remote ones", + "properties": { + "paging_state": { + "$ref": "#/components/schemas/ConversationIds_PagingState" + }, + "size": { + "description": "optional, must be <= 1000, defaults to 1000.", + "format": "int32", + "maximum": 1000, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "GroupConvTypeLegacy_NTUxMDI2Mzkw": { + "enum": [ + "group_conversation", + "channel" + ], + "type": "string" + }, + "GroupConvType_LTU4NjU0MTY5": { + "enum": [ + "group_conversation", + "channel", + "meeting" + ], + "type": "string" + }, + "GroupId": { + "description": "A base64-encoded MLS group ID", + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "GroupInfoData": { + "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." + }, + "Handle": { + "type": "string" + }, + "HandleUpdate_NTI4NDk1OTAx": { + "properties": { + "handle": { + "type": "string" + } + }, + "required": [ + "handle" + ], + "type": "object" + }, + "History": { + "properties": { + "depth": { + "$ref": "#/components/schemas/HistoryDuration" + } + }, + "required": [ + "depth" + ], + "type": "object" + }, + "HistoryDuration": { + "type": "string" + }, + "HistorySharingConfig_Mjc4MzA1Nzgw": { + "properties": { + "depth": { + "$ref": "#/components/schemas/HistoryDuration" + } + }, + "required": [ + "depth" + ], + "type": "object" + }, + "HttpsUrl": { + "example": "https://example.com", + "type": "string" + }, + "HttpsUrl_HttpsUrl_NjUyMDgzNzk3": { + "properties": { + "config_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "webapp_url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "config_url", + "webapp_url" + ], + "type": "object" + }, + "HttpsUrl_Maybe_HttpsUrl_LTQ1MDkyMzY2": { + "properties": { + "config_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "webapp_url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "config_url" + ], + "type": "object" + }, + "Icon": { + "description": "S3 asset key for an icon image with retention information. Allows special value 'default'.", + "example": "3-1-47de4580-ae51-4650-acbb-d10c028cb0ac", + "type": "string" + }, + "IdObject_ClientId_LTM3NjQyODM5": { + "properties": { + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "IdPConfig_WireIdP_NDA5MTE4Mjk0": { + "properties": { + "extraInfo": { + "$ref": "#/components/schemas/WireIdP_ODMzOTExMzYw" + }, + "id": { + "$ref": "#/components/schemas/URI" + }, + "metadata": { + "$ref": "#/components/schemas/IdPMetadata_MTI3NzE4MTA0" + } + }, + "required": [ + "id", + "metadata", + "extraInfo" + ], + "type": "object" + }, + "IdPList": { + "properties": { + "providers": { + "items": { + "$ref": "#/components/schemas/IdPConfig_WireIdP_NDA5MTE4Mjk0" + }, + "type": "array" + } + }, + "required": [ + "providers" + ], + "type": "object" + }, + "IdPMetadataInfo": { + "maxProperties": 1, + "minProperties": 1, + "properties": { + "value": { + "type": "string" + } + }, + "type": "object" + }, + "IdPMetadata_MTI3NzE4MTA0": { + "properties": { + "certAuthnResponse": { + "items": { + "$ref": "#/components/schemas/SignedCertificate" + }, + "minItems": 1, + "type": "array" + }, + "issuer": { + "$ref": "#/components/schemas/URI" + }, + "requestURI": { + "type": "string" + } + }, + "required": [ + "issuer", + "requestURI", + "certAuthnResponse" + ], + "type": "object" + }, + "Id_AuthnRequest": { + "properties": { + "iD": { + "type": "string" + } + }, + "required": [ + "iD" + ], + "type": "object" + }, + "InvitationList_ODk4NTQxODc3": { + "description": "A list of sent team invitations.", + "properties": { + "has_more": { + "description": "Indicator that the server has more invitations than returned.", + "type": "boolean" + }, + "invitations": { + "items": { + "$ref": "#/components/schemas/Invitation_NTkzMDYwODc1" + }, + "type": "array" + } + }, + "required": [ + "invitations", + "has_more" + ], + "type": "object" + }, + "InvitationRequest_LTcyMDIzNDc0": { + "description": "A request to join a team on Wire.", + "properties": { + "allow_existing": { + "description": "Whether invitations to existing users are allowed.", + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters).", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role_LTIzMjAzMjky" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "InvitationUserView_LTUyMTE3Nzkz": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "created_by_email": { + "$ref": "#/components/schemas/Email" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy_NTI0ODc0NTQx" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters)", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role_LTIzMjAzMjky" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "url": { + "$ref": "#/components/schemas/URIRef_Absolute" + } + }, + "required": [ + "team", + "id", + "created_at", + "email" + ], + "type": "object" + }, + "Invitation_NTkzMDYwODc1": { + "description": "An invitation to join a team on Wire. If invitee is invited from an existing personal account, inviter email is included.", + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters)", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role_LTIzMjAzMjky" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "url": { + "$ref": "#/components/schemas/URIRef_Absolute" + } + }, + "required": [ + "team", + "id", + "created_at", + "email" + ], + "type": "object" + }, + "InviteQualified_ODYyODIyNjYz": { + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "qualified_users": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "qualified_users" + ], + "type": "object" + }, + "JoinConversationByCode_NjgzMzM4Mjg5": { + "description": "Request body for joining a conversation by code", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "JoinType_LTY4MDg2MzA5": { + "enum": [ + "external_add", + "internal_add" + ], + "type": "string" + }, + "KeyMap_Value_MzAxODEwOTgx": { + "type": "object" + }, + "KeyPackage": { + "example": "a2V5IHBhY2thZ2UgZGF0YQo=", + "type": "string" + }, + "KeyPackageBundleEntry_NDQ2MzQ2MzMz": { + "properties": { + "client": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "key_package": { + "$ref": "#/components/schemas/KeyPackage" + }, + "key_package_ref": { + "$ref": "#/components/schemas/KeyPackageRef" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "user", + "client", + "key_package_ref", + "key_package" + ], + "type": "object" + }, + "KeyPackageBundle_MjU2MjY0MDU2": { + "properties": { + "key_packages": { + "items": { + "$ref": "#/components/schemas/KeyPackageBundleEntry_NDQ2MzQ2MzMz" + }, + "type": "array" + } + }, + "required": [ + "key_packages" + ], + "type": "object" + }, + "KeyPackageCount_LTYwNDg5MDcz": { + "properties": { + "count": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "count" + ], + "type": "object" + }, + "KeyPackageRef": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "KeyPackageUpload_NTQ2Mjk2NzEx": { + "properties": { + "key_packages": { + "items": { + "$ref": "#/components/schemas/KeyPackage" + }, + "type": "array" + } + }, + "required": [ + "key_packages" + ], + "type": "object" + }, + "LHServiceStatus_ODc3NzE0Mjg3": { + "enum": [ + "configured", + "not_configured", + "disabled" + ], + "type": "string" + }, + "LimitedQualifiedUserIdList_500": { + "properties": { + "qualified_users": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "type": "array" + } + }, + "required": [ + "qualified_users" + ], + "type": "object" + }, + "ListConversations_MjkxMTIwODMz": { + "description": "A request to list some of a user's conversations, including remote ones. Maximum 1000 qualified conversation IDs", + "properties": { + "qualified_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "maxItems": 1000, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "qualified_ids" + ], + "type": "object" + }, + "ListType_LTkyMDM4MzA1": { + "description": "true if 'members' doesn't contain all team members", + "enum": [ + true, + false + ], + "type": "boolean" + }, + "ListUsersById_LTQ5MTE3NDc0": { + "properties": { + "failed": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "minItems": 1, + "type": "array" + }, + "found": { + "items": { + "$ref": "#/components/schemas/UserProfile_LTQzMTQxMTE1" + }, + "type": "array" + } + }, + "required": [ + "found" + ], + "type": "object" + }, + "ListUsersQuery": { + "description": "exactly one of qualified_ids or qualified_handles must be provided.", + "example": { + "qualified_ids": [ + { + "domain": "example.com", + "id": "00000000-0000-0000-0000-000000000000" + } + ] + }, + "properties": { + "qualified_handles": { + "items": { + "$ref": "#/components/schemas/Qualified_Handle_Nzg0MDE3Nzk4" + }, + "type": "array" + }, + "qualified_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "type": "array" + } + }, + "type": "object" + }, + "Locale": { + "type": "string" + }, + "LocaleUpdate_LTgzNjgyOTEw": { + "properties": { + "locale": { + "$ref": "#/components/schemas/Locale" + } + }, + "required": [ + "locale" + ], + "type": "object" + }, + "LockStatus_LTIyMTU5OTkw": { + "enum": [ + "locked", + "unlocked" + ], + "type": "string" + }, + "LockableFeature_AllowedGlobalOperationsConfig_NjQ1MjA5MDYw": { + "properties": { + "config": { + "$ref": "#/components/schemas/AllowedGlobalOperationsConfig_MzAwOTU1MDkx" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "LockableFeature_AppLockConfigB_Bare_Identity_ODgzNDI0OTU5": { + "properties": { + "config": { + "$ref": "#/components/schemas/AppLockConfigB_Covered_Identity_NDIxOTc2Njkz" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "LockableFeature_AppsConfig_MzQyNTMxNTk5": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_AssetAuditLogConfig_NDQ3MzcyMzk2": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_BackgroundEffectsConfig_MTg1MTk3NTM5": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_CellsConfigB_Bare_Identity_LTgzMDA1NjI3": { + "properties": { + "config": { + "$ref": "#/components/schemas/CellsConfigB_Covered_Identity_LTE1NzkwOTcz" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "LockableFeature_ChannelsConfigB_Bare_Identity_NzA2NDEyMDEw": { + "properties": { + "config": { + "$ref": "#/components/schemas/ChannelsConfigB_Covered_Identity_ODk2MTk3NDQ4" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "LockableFeature_ChatBubblesConfig_NDgwMTQzNjI2": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_ClassifiedDomainsConfig_LTY1ODQwODg1": { + "properties": { + "config": { + "$ref": "#/components/schemas/ClassifiedDomainsConfig_LTg4MDcwMDg2" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "LockableFeature_ConsumableNotificationsConfig_MjUxMjczMjM0": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_DigitalSignaturesConfig_ODM2MDA2NDU4": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_DomainRegistrationConfig_NzU0NjczNTE0": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_FileSharingConfig_MjgwNjIzODEz": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_GuestLinksConfig_LTcwNjU0NDMw": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_LegalholdConfig_LTc5MTk5OTIw": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_LimitedEventFanoutConfig_MTg3ODM0NzU0": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_MLSConfigB_Bare_Identity_MTg1MTc0NTEw": { + "properties": { + "config": { + "$ref": "#/components/schemas/MLSConfigB_Covered_Identity_LTEzNTk3MzM5" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "LockableFeature_MeetingsConfig_LTE0OTQ5Nzcw": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_MeetingsPremiumConfig_NDg1ODEyNTA1": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_MlsE2EIdConfigB_Bare_Identity_MTU2ODkyMDc4": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsE2EIdConfigB_Covered_Identity_NDQ0NjEwNzA3" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "LockableFeature_OutlookCalIntegrationConfig_NjQ0MzMyMzY0": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_SSOConfig_NjcyMjU4MDY2": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_SearchVisibilityAvailableConfig_LTkxMTA5ODk5": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_SearchVisibilityInboundConfig_NzA5NzczNTgy": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_SndFactorPasswordChallengeConfig_MjQ3NzQ2ODgx": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFeature_StealthUsersConfig_LTE1MTk2NzIz": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFtur_CnfigBIdy_NzY1NDU5MDAy": { + "properties": { + "config": { + "$ref": "#/components/schemas/ConferenceCallingConfigB_Covered_Identity_NDMwMjcyMzM1" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFtur_CsInfigBdy_LTQ3MTU3Mjg0": { + "properties": { + "config": { + "$ref": "#/components/schemas/CellsInternalConfigB_Covered_Identity_LTUzMDkwOTAz" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "LockableFtur_EnfiDwdCgBIy_ODA5OTA5MTQ4": { + "properties": { + "config": { + "$ref": "#/components/schemas/EnforceFilDwadLtCgB_vIy_LTQ2NjU1Nzcx" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "LockableFtur_ExpsInviURTmAdCfg_LTQzMzU2OTY1": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFtur_MsignCfBIdy_LTE1NjAxNjU2": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsMigrationConfigB_Covered_Identity_LTM3NDQ4Mzg4" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "LockableFtur_PvnAdmisGpCfgBIy_MTQxOTU4Mzgw": { + "properties": { + "config": { + "$ref": "#/components/schemas/PreventAdminlessGroupsConfigB_Covered_Identity_MjQ2NDYwMTk2" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "LockableFtur_RqiExnmVfCg_NjUyMzgzNzY5": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LockableFtur_SfDingMsCBIdy_LTg5MTEwNTA2": { + "properties": { + "config": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfigB_Covered_Identity_LTY0NzQ4MzU1" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "LockableFtur_SimpfdUsCnRqQg_Njk5NjU4OTgy": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus_LTIyMTU5OTkw" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus_LTMzMTUwODEw" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Login_LTgyNTIzMTM1": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "label": { + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "MLSConfigB_Covered_Identity_LTEzNTk3MzM5": { + "description": "allowlist of users that may change protocols", + "properties": { + "allowedCipherSuites": { + "items": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "type": "array" + }, + "defaultCipherSuite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "defaultProtocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "groupInfoDiagnostics": { + "type": "boolean" + }, + "protocolToggleUsers": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "supportedProtocols": { + "items": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "type": "array" + } + }, + "required": [ + "protocolToggleUsers", + "defaultProtocol", + "allowedCipherSuites", + "defaultCipherSuite", + "supportedProtocols" + ], + "type": "object" + }, + "MLSKeysByPurpose_MLSKeys_SomeKey_NjkxMjk0MTAx": { + "properties": { + "removal": { + "$ref": "#/components/schemas/MLSKeys_SomeKey_LTUzNDA5MzA3" + } + }, + "required": [ + "removal" + ], + "type": "object" + }, + "MLSKeys_SomeKey_LTUzNDA5MzA3": { + "properties": { + "ecdsa_secp256r1_sha256": { + "$ref": "#/components/schemas/SomeKey" + }, + "ecdsa_secp384r1_sha384": { + "$ref": "#/components/schemas/SomeKey" + }, + "ecdsa_secp521r1_sha512": { + "$ref": "#/components/schemas/SomeKey" + }, + "ed25519": { + "$ref": "#/components/schemas/SomeKey" + } + }, + "required": [ + "ed25519", + "ecdsa_secp256r1_sha256", + "ecdsa_secp384r1_sha384", + "ecdsa_secp521r1_sha512" + ], + "type": "object" + }, + "MLSMessage": { + "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." + }, + "MLSMessageSendingStatus_NjA1NDA0MTE4": { + "properties": { + "events": { + "description": "A list of events caused by sending the message.", + "items": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + }, + "type": "array" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + } + }, + "required": [ + "events", + "time" + ], + "type": "object" + }, + "MLSOne2OneConversation_SomeKey_GroupConvType_LTg2MTcwMjY3": { + "properties": { + "conversation": { + "$ref": "#/components/schemas/v9_OwnConversation_GroupConvType_LTU2MzYxNTg0" + }, + "public_keys": { + "$ref": "#/components/schemas/MLSKeysByPurpose_MLSKeys_SomeKey_NjkxMjk0MTAx" + } + }, + "required": [ + "conversation", + "public_keys" + ], + "type": "object" + }, + "MLSPublicKeys": { + "additionalProperties": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "description": "Mapping from signature scheme (tags) to public key data", + "example": { + "ecdsa_secp256r1_sha256": "ZXhhbXBsZQo=", + "ecdsa_secp384r1_sha384": "ZXhhbXBsZQo=", + "ecdsa_secp521r1_sha512": "ZXhhbXBsZQo=", + "ed25519": "ZXhhbXBsZQo=" + }, + "type": "object" + }, + "MLSReset_NzgwODA3ODc4": { + "properties": { + "epoch": { + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + } + }, + "required": [ + "group_id", + "epoch" + ], + "type": "object" + }, + "MTYxOTI3NjM3": { + "enum": [ + "image" + ], + "type": "string" + }, + "ManagedBy_NTI0ODc0NTQx": { + "enum": [ + "wire", + "scim" + ], + "type": "string" + }, + "MeetingEmailsInvitation_NzgyNzUzMzcz": { + "description": "Emails invitation", + "properties": { + "emails": { + "items": { + "$ref": "#/components/schemas/Email" + }, + "type": "array" + } + }, + "required": [ + "emails" + ], + "type": "object" + }, + "MeetingWithConversation_LTMyNzA4NzU0": { + "description": "A scheduled meeting with its associated conversation", + "properties": { + "conversation": { + "$ref": "#/components/schemas/Conversation_GroupConvType_MzQzMTQ1OTg3" + }, + "created_at": { + "$ref": "#/components/schemas/UTCTime" + }, + "end_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "invited_emails": { + "items": { + "$ref": "#/components/schemas/Email" + }, + "type": "array" + }, + "qualified_conversation": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "qualified_creator": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Meeting_MjgxMDUyNjc3" + }, + "recurrence": { + "$ref": "#/components/schemas/Recurrence_LTQ0OTc0ODE2" + }, + "start_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "title": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "tzid": { + "$ref": "#/components/schemas/TimeZone" + }, + "updated_at": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "qualified_id", + "title", + "qualified_creator", + "start_time", + "end_time", + "tzid", + "qualified_conversation", + "invited_emails", + "created_at", + "updated_at", + "conversation" + ], + "type": "object" + }, + "Meeting_ODU0OTMzMTgw": { + "description": "A scheduled meeting", + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTime" + }, + "end_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "invited_emails": { + "items": { + "$ref": "#/components/schemas/Email" + }, + "type": "array" + }, + "qualified_conversation": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "qualified_creator": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Meeting_MjgxMDUyNjc3" + }, + "recurrence": { + "$ref": "#/components/schemas/Recurrence_LTQ0OTc0ODE2" + }, + "start_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "title": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "tzid": { + "$ref": "#/components/schemas/TimeZone" + }, + "updated_at": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "qualified_id", + "title", + "qualified_creator", + "start_time", + "end_time", + "tzid", + "qualified_conversation", + "invited_emails", + "created_at", + "updated_at" + ], + "type": "object" + }, + "MemberUpdateData_LTc3Nzc3NTEy": { + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "qualified_target": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "target": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "qualified_target" + ], + "type": "object" + }, + "MemberUpdate_LTg4NTQ0OTYz": { + "properties": { + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "type": "object" + }, + "Member_OTA5OTgyNzcw": { + "description": "The user ID of the requestor if the requestor is a member of the conversation", + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef_LTgxMjY3NzAz" + }, + "status": {}, + "status_ref": {}, + "status_time": {} + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "MembersJoin_LTg0MDc1NjQ3": { + "properties": { + "add_type": { + "$ref": "#/components/schemas/JoinType_LTY4MDg2MzA5" + }, + "user_ids": { + "deprecated": true, + "description": "deprecated", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/components/schemas/SimpleMember_NTY5MTcxMzcx" + }, + "type": "array" + } + }, + "required": [ + "users", + "add_type" + ], + "type": "object" + }, + "MessageSendingStatus_ODg0NDgyNDk4": { + "description": "The Proteus message sending status. It has these fields:\n- `time`: Time of sending message.\n- `missing`: Clients that the message /should/ have been encrypted for, but wasn't.\n- `redundant`: Clients that the message /should not/ have been encrypted for, but was.\n- `deleted`: Clients that were deleted.\n- `failed_to_send`: When message sending fails for some clients but succeeds for others, e.g., because a remote backend is unreachable, this field will contain the list of clients for which the message sending failed. This list should be empty when message sending is not even tried, like when some clients are missing.", + "properties": { + "deleted": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "failed_to_confirm_clients": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "failed_to_send": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "missing": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "redundant": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + } + }, + "required": [ + "time", + "missing", + "redundant", + "deleted", + "failed_to_send", + "failed_to_confirm_clients" + ], + "type": "object" + }, + "MlsE2EIdConfigB_Covered_Identity_NDQ0NjEwNzA3": { + "description": "When a client first tries to fetch or renew a certificate, they may need to login to an identity provider (IdP) depending on their IdP domain authentication policy. The user may have a grace period during which they can \"snooze\" this login. The duration of this grace period (in seconds) is set in the `verificationDuration` parameter, which is enforced separately by each client. After the grace period has expired, the client will not allow the user to use the application until they have logged to refresh the certificate. The default value is 1 day (86400s). The client enrolls using the Automatic Certificate Management Environment (ACME) protocol. The `acmeDiscoveryUrl` parameter must be set to the HTTPS URL of the ACME server discovery endpoint for this team. It is of the form \"https://acme.{backendDomain}/acme/{provisionerName}/discovery\". For example: `https://acme.example.com/acme/provisioner1/discovery`.", + "properties": { + "acmeDiscoveryUrl": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "crlProxy": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "useProxyOnMobile": { + "type": "boolean" + }, + "verificationExpiration": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "verificationExpiration" + ], + "type": "object" + }, + "MlsMigrationConfigB_Covered_Identity_LTM3NDQ4Mzg4": { + "properties": { + "allowManualMigration": { + "type": "boolean" + }, + "finaliseRegardlessAfter": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + }, + "startTime": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + } + }, + "type": "object" + }, + "MultiTabePg_ConcsLOrRmU_MTgwMzU4OTU5": { + "properties": { + "connections": { + "items": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + }, + "type": "array" + }, + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/Connections_PagingState" + } + }, + "required": [ + "connections", + "has_more", + "paging_state" + ], + "type": "object" + }, + "MultiTabePg_ConvrsIdqf_cLORm_Q_Mzg0MjYzNzM0": { + "properties": { + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/ConversationIds_PagingState" + }, + "qualified_conversations": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "type": "array" + } + }, + "required": [ + "qualified_conversations", + "has_more", + "paging_state" + ], + "type": "object" + }, + "NP_LockableFtur_ghdCnfiSOVsyAvIRqExmDpB_MGwUT2Q_LTM1Mzc0ODgy": { + "properties": { + "allowedGlobalOperations": { + "$ref": "#/components/schemas/LockableFeature_AllowedGlobalOperationsConfig_NjQ1MjA5MDYw" + }, + "appLock": { + "$ref": "#/components/schemas/LockableFeature_AppLockConfigB_Bare_Identity_ODgzNDI0OTU5" + }, + "apps": { + "$ref": "#/components/schemas/LockableFeature_AppsConfig_MzQyNTMxNTk5" + }, + "assetAuditLog": { + "$ref": "#/components/schemas/LockableFeature_AssetAuditLogConfig_NDQ3MzcyMzk2" + }, + "backgroundEffects": { + "$ref": "#/components/schemas/LockableFeature_BackgroundEffectsConfig_MTg1MTk3NTM5" + }, + "cells": { + "$ref": "#/components/schemas/LockableFeature_CellsConfigB_Bare_Identity_LTgzMDA1NjI3" + }, + "cellsInternal": { + "$ref": "#/components/schemas/LockableFtur_CsInfigBdy_LTQ3MTU3Mjg0" + }, + "channels": { + "$ref": "#/components/schemas/LockableFeature_ChannelsConfigB_Bare_Identity_NzA2NDEyMDEw" + }, + "chatBubbles": { + "$ref": "#/components/schemas/LockableFeature_ChatBubblesConfig_NDgwMTQzNjI2" + }, + "classifiedDomains": { + "$ref": "#/components/schemas/LockableFeature_ClassifiedDomainsConfig_LTY1ODQwODg1" + }, + "conferenceCalling": { + "$ref": "#/components/schemas/LockableFtur_CnfigBIdy_NzY1NDU5MDAy" + }, + "consumableNotifications": { + "$ref": "#/components/schemas/LockableFeature_ConsumableNotificationsConfig_MjUxMjczMjM0" + }, + "conversationGuestLinks": { + "$ref": "#/components/schemas/LockableFeature_GuestLinksConfig_LTcwNjU0NDMw" + }, + "digitalSignatures": { + "$ref": "#/components/schemas/LockableFeature_DigitalSignaturesConfig_ODM2MDA2NDU4" + }, + "domainRegistration": { + "$ref": "#/components/schemas/LockableFeature_DomainRegistrationConfig_NzU0NjczNTE0" + }, + "enforceFileDownloadLocation": { + "$ref": "#/components/schemas/LockableFtur_EnfiDwdCgBIy_ODA5OTA5MTQ4" + }, + "exposeInvitationURLsToTeamAdmin": { + "$ref": "#/components/schemas/LockableFtur_ExpsInviURTmAdCfg_LTQzMzU2OTY1" + }, + "fileSharing": { + "$ref": "#/components/schemas/LockableFeature_FileSharingConfig_MjgwNjIzODEz" + }, + "legalhold": { + "$ref": "#/components/schemas/LockableFeature_LegalholdConfig_LTc5MTk5OTIw" + }, + "limitedEventFanout": { + "$ref": "#/components/schemas/LockableFeature_LimitedEventFanoutConfig_MTg3ODM0NzU0" + }, + "meetings": { + "$ref": "#/components/schemas/LockableFeature_MeetingsConfig_LTE0OTQ5Nzcw" + }, + "meetingsPremium": { + "$ref": "#/components/schemas/LockableFeature_MeetingsPremiumConfig_NDg1ODEyNTA1" + }, + "mls": { + "$ref": "#/components/schemas/LockableFeature_MLSConfigB_Bare_Identity_MTg1MTc0NTEw" + }, + "mlsE2EId": { + "$ref": "#/components/schemas/LockableFeature_MlsE2EIdConfigB_Bare_Identity_MTU2ODkyMDc4" + }, + "mlsMigration": { + "$ref": "#/components/schemas/LockableFtur_MsignCfBIdy_LTE1NjAxNjU2" + }, + "outlookCalIntegration": { + "$ref": "#/components/schemas/LockableFeature_OutlookCalIntegrationConfig_NjQ0MzMyMzY0" + }, + "preventAdminlessGroups": { + "$ref": "#/components/schemas/LockableFtur_PvnAdmisGpCfgBIy_MTQxOTU4Mzgw" + }, + "searchVisibility": { + "$ref": "#/components/schemas/LockableFeature_SearchVisibilityAvailableConfig_LTkxMTA5ODk5" + }, + "searchVisibilityInbound": { + "$ref": "#/components/schemas/LockableFeature_SearchVisibilityInboundConfig_NzA5NzczNTgy" + }, + "selfDeletingMessages": { + "$ref": "#/components/schemas/LockableFtur_SfDingMsCBIdy_LTg5MTEwNTA2" + }, + "simplifiedUserConnectionRequestQRCode": { + "$ref": "#/components/schemas/LockableFtur_SimpfdUsCnRqQg_Njk5NjU4OTgy" + }, + "sndFactorPasswordChallenge": { + "$ref": "#/components/schemas/LockableFeature_SndFactorPasswordChallengeConfig_MjQ3NzQ2ODgx" + }, + "sso": { + "$ref": "#/components/schemas/LockableFeature_SSOConfig_NjcyMjU4MDY2" + }, + "stealthUsers": { + "$ref": "#/components/schemas/LockableFeature_StealthUsersConfig_LTE1MTk2NzIz" + }, + "validateSAMLemails": { + "$ref": "#/components/schemas/LockableFtur_RqiExnmVfCg_NjUyMzgzNzY5" + } + }, + "required": [ + "legalhold", + "sso", + "searchVisibility", + "searchVisibilityInbound", + "validateSAMLemails", + "digitalSignatures", + "appLock", + "fileSharing", + "classifiedDomains", + "conferenceCalling", + "selfDeletingMessages", + "conversationGuestLinks", + "sndFactorPasswordChallenge", + "mls", + "exposeInvitationURLsToTeamAdmin", + "outlookCalIntegration", + "mlsE2EId", + "mlsMigration", + "enforceFileDownloadLocation", + "limitedEventFanout", + "domainRegistration", + "channels", + "preventAdminlessGroups", + "cells", + "allowedGlobalOperations", + "consumableNotifications", + "chatBubbles", + "apps", + "simplifiedUserConnectionRequestQRCode", + "assetAuditLog", + "stealthUsers", + "cellsInternal", + "meetings", + "meetingsPremium", + "backgroundEffects" + ], + "type": "object" + }, + "NameIDFormat": { + "enum": [ + "NameIDFUnspecified", + "NameIDFEmail", + "NameIDFX509", + "NameIDFWindows", + "NameIDFKerberos", + "NameIDFEntity", + "NameIDFPersistent", + "NameIDFTransient" + ], + "type": "string" + }, + "NameIdPolicy": { + "properties": { + "allowCreate": { + "type": "boolean" + }, + "format": { + "$ref": "#/components/schemas/NameIDFormat" + }, + "spNameQualifier": { + "type": "string" + } + }, + "required": [ + "format", + "allowCreate" + ], + "type": "object" + }, + "NewApp_LTQwODMwMzQ4": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" + }, + "category": { + "description": "Category name (if uncertain, pick \"other\")", + "type": "string" + }, + "description": { + "maxLength": 300, + "minLength": 0, + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "name", + "category", + "description", + "password" + ], + "type": "object" + }, + "NewAssetToken_NTAwMDQwODYy": { + "properties": { + "token": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "token" + ], + "type": "object" + }, + "NewClient_ODg1NjY4Njgy": { + "properties": { + "capabilities": { + "$ref": "#/components/schemas/ClientCapabilityList" + }, + "class": { + "$ref": "#/components/schemas/ClientClass_NjE3MDgwNzcx" + }, + "cookie": { + "description": "The cookie label, i.e. the label used when logging in.", + "type": "string" + }, + "label": { + "type": "string" + }, + "lastkey": { + "$ref": "#/components/schemas/UncheckedPrekeyBundle_LTU1MzQzOTgy" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "model": { + "type": "string" + }, + "password": { + "description": "The password of the authenticated user for verification. Note: Required for registration of the 2nd, 3rd, ... client.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "prekeys": { + "description": "Prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/components/schemas/UncheckedPrekeyBundle_LTU1MzQzOTgy" + }, + "type": "array" + }, + "type": { + "$ref": "#/components/schemas/ClientType_MjQ0OTQwMzcw" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "prekeys", + "lastkey", + "type" + ], + "type": "object" + }, + "NewConv_LTgzNTk1NDQx": { + "description": "JSON object to create a new conversation. When using 'qualified_users' (preferred), you can omit 'users'", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells": { + "type": "boolean" + }, + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "message_timer": { + "description": "Per-conversation message timer", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/BaseProtocolTag_LTM0MDE1NTEx" + }, + "qualified_users": { + "description": "List of qualified user IDs (excluding the requestor) to be part of this conversation", + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "type": "array" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "skip_creator": { + "description": "Don't add creator to the conversation, only works for team admins not wanting to be part of the channels they create.", + "type": "boolean" + }, + "team": { + "$ref": "#/components/schemas/ConvTeamInfo_Mzc5NjcyNjAz" + }, + "users": { + "deprecated": true, + "description": "List of user IDs (excluding the requestor) to be part of this conversation (deprecated)", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "type": "object" + }, + "NewLegalHoldService_Mzg0ODQ5NDU1": { + "properties": { + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" + } + }, + "required": [ + "base_url", + "public_key", + "auth_token" + ], + "type": "object" + }, + "NewMeeting_LTI1NTMzOTU5": { + "description": "Request to create a new meeting", + "properties": { + "end_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "invited_emails": { + "items": { + "$ref": "#/components/schemas/Email" + }, + "type": "array" + }, + "recurrence": { + "$ref": "#/components/schemas/Recurrence_LTQ0OTc0ODE2" + }, + "start_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "title": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "tzid": { + "$ref": "#/components/schemas/TimeZone" + } + }, + "required": [ + "start_time", + "end_time", + "tzid", + "title" + ], + "type": "object" + }, + "NewOne2OneConv_LTI3OTc4NDAz": { + "description": "JSON object to create a new 1:1 conversation. When using 'qualified_users' (preferred), you can omit 'users'", + "properties": { + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "qualified_users": { + "description": "List of qualified user IDs (excluding the requestor) to be part of this conversation", + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/ConvTeamInfo_Mzc5NjcyNjAz" + }, + "users": { + "deprecated": true, + "description": "List of user IDs (excluding the requestor) to be part of this conversation (deprecated)", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "type": "object" + }, + "NewOtrMessage_LTUyMTE5MTMw": { + "properties": { + "data": { + "type": "string" + }, + "native_priority": { + "$ref": "#/components/schemas/Priority_ODA3NDM3MDYy" + }, + "native_push": { + "type": "boolean" + }, + "recipients": { + "$ref": "#/components/schemas/UserClientMap" + }, + "report_missing": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "sender": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "transient": { + "type": "boolean" + } + }, + "required": [ + "sender", + "recipients" + ], + "type": "object" + }, + "NewPasswordReset_LTEyNzAxMTcy": { + "description": "Data to initiate a password reset", + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "phone": { + "description": "Email", + "type": "string" + } + }, + "type": "object" + }, + "NewProviderResponse_OTE0ODI2NjU0": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "NewProvider_LTEyMTY5MjYy": { + "properties": { + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "name", + "email", + "url", + "description" + ], + "type": "object" + }, + "NewServiceResponse_LTExMzcwMjg5": { + "properties": { + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "NewService_LTYwOTU1MDQ3": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" + }, + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "summary": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/ServiceTag_LTMyNTEzNjYy" + }, + "maxItems": 3, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "name", + "summary", + "description", + "base_url", + "public_key", + "assets", + "tags" + ], + "type": "object" + }, + "NewTeamCollaborator_LTIxNjEzMTYw": { + "properties": { + "permissions": { + "items": { + "$ref": "#/components/schemas/CollaboratorPermission_NDg5NTg2ODgy" + }, + "type": "array" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + }, + "NewTeamMember_Required_LTg2NjU5OTI2": { + "description": "Required data when creating new team members", + "properties": { + "member": { + "description": "the team member to add (the legalhold_status field must be null or missing!)", + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions_NDE0ODM5NDUx" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + } + }, + "required": [ + "member" + ], + "type": "object" + }, + "NewUserGroup_MzYxODU0OTU1": { + "properties": { + "members": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "members" + ], + "type": "object" + }, + "NewUser_PlainTextPassword_8_LTI4MzI5NzQx": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "email_code": { + "$ref": "#/components/schemas/ASCII" + }, + "expires_in": { + "maximum": 604800, + "minimum": 1, + "type": "integer" + }, + "invitation_code": { + "$ref": "#/components/schemas/ASCII" + }, + "label": { + "type": "string" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy_NTI0ODc0NTQx" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict_DEPRECATED_USE_ASSETS_INSTEAD" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocolTag_LTM0MDE1NTEx" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/BindingNewTeamUser_LTY0MDQxMDEw" + }, + "team_code": { + "$ref": "#/components/schemas/ASCII" + }, + "team_id": { + "$ref": "#/components/schemas/UUID" + }, + "uuid": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "OAuthAccessTokenRequest_LTYyNTcyMzI4": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "code": { + "$ref": "#/components/schemas/OAuthAuthorizationCode" + }, + "code_verifier": { + "description": "The code verifier to complete the code challenge", + "maxLength": 128, + "minLength": 43, + "type": "string" + }, + "grant_type": { + "$ref": "#/components/schemas/OAuthGrantType_LTIxODA5NDIw" + }, + "redirect_uri": { + "$ref": "#/components/schemas/RedirectUrl" + } + }, + "required": [ + "grant_type", + "client_id", + "code_verifier", + "code", + "redirect_uri" + ], + "type": "object" + }, + "OAuthAccessTokenResponse_NzEwOTI4NjQ0": { + "properties": { + "access_token": { + "description": "The access token, which has a relatively short lifetime", + "type": "string" + }, + "expires_in": { + "description": "The lifetime of the access token in seconds", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "refresh_token": { + "description": "The refresh token, which has a relatively long lifetime, and can be used to obtain a new access token", + "type": "string" + }, + "token_type": { + "$ref": "#/components/schemas/OAuthAccessTokenType_MjU3ODI0NDIw" + } + }, + "required": [ + "access_token", + "token_type", + "expires_in", + "refresh_token" + ], + "type": "object" + }, + "OAuthAccessTokenType_MjU3ODI0NDIw": { + "description": "The type of the access token. Currently only `Bearer` is supported.", + "enum": [ + "Bearer" + ], + "type": "string" + }, + "OAuthApplication_Mjk5NTUxNjA1": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "description": "The OAuth client's name", + "maxLength": 256, + "minLength": 6, + "type": "string" + }, + "sessions": { + "description": "The OAuth client's sessions", + "items": { + "$ref": "#/components/schemas/OAuthSession_LTQxOTIxNTMy" + }, + "type": "array" + } + }, + "required": [ + "id", + "name", + "sessions" + ], + "type": "object" + }, + "OAuthAuthorizationCode": { + "description": "The authorization code", + "type": "string" + }, + "OAuthClient_NzExMTI5NTIy": { + "properties": { + "application_name": { + "maxLength": 256, + "minLength": 6, + "type": "string" + }, + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "redirect_url": { + "$ref": "#/components/schemas/RedirectUrl" + } + }, + "required": [ + "client_id", + "application_name", + "redirect_url" + ], + "type": "object" + }, + "OAuthCodeChallenge": { + "description": "Generated by the client from the code verifier (unpadded base64url-encoded SHA256 hash of the code verifier)", + "type": "string" + }, + "OAuthGrantType_LTIxODA5NDIw": { + "description": "Indicates which authorization flow to use. Use `authorization_code` for authorization code flow.", + "enum": [ + "authorization_code", + "refresh_token" + ], + "type": "string" + }, + "OAuthRefreshAccessTokenRequest_LTUwNjA3OTI1": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "grant_type": { + "$ref": "#/components/schemas/OAuthGrantType_LTIxODA5NDIw" + }, + "refresh_token": { + "description": "The refresh token", + "type": "string" + } + }, + "required": [ + "grant_type", + "client_id", + "refresh_token" + ], + "type": "object" + }, + "OAuthResponseType_ODI2Mjg3NzQx": { + "description": "Indicates which authorization flow to use. Use `code` for authorization code flow.", + "enum": [ + "code" + ], + "type": "string" + }, + "OAuthRevokeRefreshTokenRequest_MjA1MDg4MzQ4": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "refresh_token": { + "description": "The refresh token", + "type": "string" + } + }, + "required": [ + "client_id", + "refresh_token" + ], + "type": "object" + }, + "OAuthSession_LTQxOTIxNTMy": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "refresh_token_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "refresh_token_id", + "created_at" + ], + "type": "object" + }, + "Object": { + "additionalProperties": true, + "description": "A single notification event", + "properties": { + "type": { + "description": "Event type", + "type": "string" + } + }, + "title": "Event", + "type": "object" + }, + "OtherMemberUpdate_LTM1MjYzOTU0": { + "description": "Update user properties of other members relative to a conversation", + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + } + }, + "type": "object" + }, + "OtherMember_LTgzNzE2MTk4": { + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef_LTgxMjY3NzAz" + }, + "status": { + "deprecated": true, + "description": "deprecated", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "OtrMessage_LTY4MTYzNzg3": { + "description": "Encrypted message of a conversation", + "properties": { + "data": { + "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", + "type": "string" + }, + "recipient": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "sender": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "text": { + "description": "The ciphertext for the recipient (Base64 in JSON)", + "type": "string" + } + }, + "required": [ + "sender", + "recipient", + "text" + ], + "type": "object" + }, + "OwnConvMembers_LTEwMzUzODMy": { + "description": "Users of a conversation", + "properties": { + "others": { + "description": "All other current users of this conversation", + "items": { + "$ref": "#/components/schemas/OtherMember_LTgzNzE2MTk4" + }, + "type": "array" + }, + "self": { + "$ref": "#/components/schemas/Member_OTA5OTgyNzcw" + } + }, + "required": [ + "self", + "others" + ], + "type": "object" + }, + "OwnConversation_GroupConvType_LTU2MzYxNTg0": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers_LTEwMzUzODMy" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + }, + "PagingState": { + "description": "Paging state that should be supplied to retrieve the next page of results", + "type": "string" + }, + "PasswordChange_MTgzMDM2NTY2": { + "description": "Data to change a password. The old password is required if a password already exists.", + "properties": { + "new_password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "old_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "new_password" + ], + "type": "object" + }, + "PasswordChange_NDI0ODgwNDU0": { + "properties": { + "new_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "old_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "old_password", + "new_password" + ], + "type": "object" + }, + "PasswordReqBody_LTcxMzE3ODE3": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "PasswordReset_LTYzNDYxNTQ3": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "Permissions_NDE0ODM5NDUx": { + "description": "This is just a complicated way of representing a team role. self and copy always have to contain the same integer, and only the following integers are allowed: 1025 (partner), 1587 (member), 5951 (admin), 8191 (owner). Unit tests of the galley-types package in wire-server contain an authoritative list.", + "properties": { + "copy": { + "description": "Permissions that this user is able to grant others", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "self": { + "description": "Permissions that the user has", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "self", + "copy" + ], + "type": "object" + }, + "PhoneNumber": { + "description": "A known phone number with a pending password reset.", + "type": "string" + }, + "Pict_DEPRECATED_USE_ASSETS_INSTEAD": { + "items": { + "type": "object" + }, + "maxItems": 10, + "minItems": 0, + "type": "array" + }, + "PrekeyBundle_MzgzOTk4MjYz": { + "properties": { + "clients": { + "items": { + "$ref": "#/components/schemas/ClientPrekey_LTcyODUzMTcw" + }, + "type": "array" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "clients" + ], + "type": "object" + }, + "PreventAdminlessGroupsConfigB_Covered_Identity_MjQ2NDYwMTk2": { + "properties": { + "deletionTimeout": { + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "deletionTimeoutDuration": { + "type": "string" + }, + "promotionStrategy": { + "$ref": "#/components/schemas/PreventAdminlessGroupsPromotionStrategy_Mjc1ODA2MDU1" + }, + "reminderTimeoutDurations": { + "items": { + "type": "string" + }, + "type": "array" + }, + "reminderTimeouts": { + "items": { + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + }, + "required": [ + "promotionStrategy" + ], + "type": "object" + }, + "PreventAdminlessGroupsPromotionStrategy_Mjc1ODA2MDU1": { + "enum": [ + "alphabetical", + "random", + "all" + ], + "type": "string" + }, + "Priority_ODA3NDM3MDYy": { + "enum": [ + "low", + "high" + ], + "type": "string" + }, + "PropertyKeysAndValues": { + "type": "object" + }, + "PropertyValue": { + "description": "An arbitrary JSON value for a property" + }, + "ProtocolTag_ODg1MTE5NjEw": { + "enum": [ + "proteus", + "mls", + "mixed" + ], + "type": "string" + }, + "ProtocolUpdate_NzY1ODgxNDQy": { + "properties": { + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + } + }, + "type": "object" + }, + "ProviderActivationResponse_LTgzNTU3MzA5": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "ProviderLogin_LTE2MTk2NTM5": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "email", + "password" + ], + "type": "object" + }, + "Provider_NDIyMzQ3ODIy": { + "properties": { + "description": { + "type": "string" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "id", + "name", + "email", + "url", + "description" + ], + "type": "object" + }, + "PubClient": { + "properties": { + "class": { + "$ref": "#/components/schemas/ClientClass_NjE3MDgwNzcx" + }, + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "PublicSubConversation_MjI2NTIxMzU4": { + "description": "An MLS subconversation", + "properties": { + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "members": { + "items": { + "$ref": "#/components/schemas/ClientIdentity_MjAxMjI3NTUw" + }, + "type": "array" + }, + "parent_qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "subconv_id": { + "type": "string" + } + }, + "required": [ + "parent_qualified_id", + "subconv_id", + "group_id", + "epoch", + "members" + ], + "type": "object" + }, + "PushTokenList_NDI0Mjc3MzY3": { + "description": "List of Native Push Tokens", + "properties": { + "tokens": { + "description": "Push tokens", + "items": { + "$ref": "#/components/schemas/PushToken_ODYzMDYzOTA4" + }, + "type": "array" + } + }, + "required": [ + "tokens" + ], + "type": "object" + }, + "PushToken_ODYzMDYzOTA4": { + "description": "Native Push Token", + "properties": { + "app": { + "description": "Application", + "type": "string" + }, + "client": { + "description": "Client ID", + "type": "string" + }, + "token": { + "description": "Access Token", + "type": "string" + }, + "transport": { + "$ref": "#/components/schemas/Transport_NDk2NzU5NDIy" + } + }, + "required": [ + "transport", + "app", + "token", + "client" + ], + "type": "object" + }, + "PutApp_LTE4MDc1OTM4": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" + }, + "category": { + "description": "Category name (if uncertain, pick \"other\")", + "type": "string" + }, + "description": { + "maxLength": 300, + "minLength": 0, + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + "type": "object" + }, + "QualifiedNewOtrMessage": { + "description": "This object can only be parsed from Protobuf.\nThe specification for the protobuf types is here: \nhttps://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto." + }, + "QualifiedUserClientPrekeyMapV4_MzcxOTAxMTYy": { + "properties": { + "failed_to_list": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "type": "array" + }, + "qualified_user_client_prekeys": { + "additionalProperties": { + "$ref": "#/components/schemas/UserClientPrekeyMap" + }, + "type": "object" + } + }, + "required": [ + "qualified_user_client_prekeys" + ], + "type": "object" + }, + "QualifiedUserClients": { + "additionalProperties": { + "additionalProperties": { + "items": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "type": "array" + }, + "type": "object" + }, + "description": "Map of Domain to UserClients", + "example": { + "domain1.example.com": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + } + }, + "type": "object" + }, + "QualifiedUserMap_Set_PubClient": { + "additionalProperties": { + "$ref": "#/components/schemas/UserMap_Set_PubClient" + }, + "description": "Map of Domain to (UserMap (Set_PubClient)).", + "example": { + "domain1.example.com": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + { + "class": "legalhold", + "id": "d0" + } + ] + } + }, + "type": "object" + }, + "Qualified_Handle_Nzg0MDE3Nzk4": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + } + }, + "required": [ + "domain", + "handle" + ], + "type": "object" + }, + "Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "Qualified_Id_IdTag_Meeting_MjgxMDUyNjc3": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "Qualified_Id_IdTag_User_LTQ1NTIwNDM1": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "QueuedNotificationList_MTU0ODEyNTQ2": { + "description": "Zero or more notifications", + "properties": { + "has_more": { + "description": "Whether there are still more notifications.", + "type": "boolean" + }, + "notifications": { + "description": "Notifications", + "items": { + "$ref": "#/components/schemas/QueuedNotification_NTY2NzY2MTU2" + }, + "type": "array" + }, + "time": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "notifications" + ], + "type": "object" + }, + "QueuedNotification_NTY2NzY2MTU2": { + "description": "A single notification", + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "payload": { + "description": "List of events", + "items": { + "$ref": "#/components/schemas/Object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "id", + "payload" + ], + "type": "object" + }, + "RTCConfiguration_LTIwOTc4OTk0": { + "description": "A subset of the WebRTC 'RTCConfiguration' dictionary", + "properties": { + "ice_servers": { + "description": "Array of 'RTCIceServer' objects", + "items": { + "$ref": "#/components/schemas/RTCIceServer_LTY1NzExODA0" + }, + "minItems": 1, + "type": "array" + }, + "is_federating": { + "description": "True if the client should connect to an SFT in the sft_servers_all and request it to federate", + "type": "boolean" + }, + "sft_servers": { + "description": "Array of 'SFTServer' objects (optional)", + "items": { + "$ref": "#/components/schemas/SFTServer_NDQ0NDkwNDE2" + }, + "minItems": 1, + "type": "array" + }, + "sft_servers_all": { + "description": "Array of all SFT servers", + "items": { + "$ref": "#/components/schemas/AuthSFTServer_LTY5MzcyOTE0" + }, + "type": "array" + }, + "ttl": { + "description": "Number of seconds after which the configuration should be refreshed (advisory)", + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "ice_servers", + "ttl" + ], + "type": "object" + }, + "RTCIceServer_LTY1NzExODA0": { + "description": "A subset of the WebRTC 'RTCIceServer' object", + "properties": { + "credential": { + "$ref": "#/components/schemas/ASCII" + }, + "urls": { + "description": "Array of TURN server addresses of the form 'turn::'", + "items": { + "$ref": "#/components/schemas/TurnURI" + }, + "minItems": 1, + "type": "array" + }, + "username": { + "$ref": "#/components/schemas/TurnUsername" + } + }, + "required": [ + "urls", + "username", + "credential" + ], + "type": "object" + }, + "Recurrence_LTQ0OTc0ODE2": { + "description": "Recurrence pattern for meetings", + "properties": { + "frequency": { + "$ref": "#/components/schemas/Frequency_Mzk0ODQwOTM3" + }, + "interval": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "until": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "frequency" + ], + "type": "object" + }, + "RedirectUrl": { + "description": "The URL must match the URL that was used to generate the authorization code.", + "type": "string" + }, + "RefreshAppCookieRequest_MjEyMDMyMTk5": { + "properties": { + "password": { + "description": "The password of the authenticated admin for verification. or if the user has only SAML credentials.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "RefreshAppCookieResponse_LTQ0MjU1NTIw": { + "properties": { + "cookie": { + "$ref": "#/components/schemas/SomeUserToken" + } + }, + "required": [ + "cookie" + ], + "type": "object" + }, + "RegisteredDomains_V10_NDYwNzYyMTMy": { + "properties": { + "registered_domains": { + "items": { + "$ref": "#/components/schemas/DomainRegistrationResponse_V10_MjE0NDkxODY4" + }, + "type": "array" + } + }, + "required": [ + "registered_domains" + ], + "type": "object" + }, + "Relation_LTE4OTU5MTk4": { + "enum": [ + "accepted", + "blocked", + "pending", + "ignored", + "sent", + "cancelled", + "missing-legalhold-consent" + ], + "type": "string" + }, + "RemoveBotResponse_LTUxNTQ4MDEy": { + "properties": { + "event": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "required": [ + "event" + ], + "type": "object" + }, + "RemoveCookies_OTYwMTI0NDMy": { + "description": "Data required to remove cookies", + "properties": { + "ids": { + "description": "A list of cookie IDs to revoke", + "items": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "type": "array" + }, + "labels": { + "description": "A list of cookie labels for which to revoke the cookies", + "items": { + "type": "string" + }, + "type": "array" + }, + "password": { + "description": "The user's password", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "RemoveLegalHoldSettingsRequest_NzMzMzMyMDAz": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "RichField_LTgwMzc0MTg2": { + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ], + "type": "object" + }, + "RichInfoAssocList": { + "description": "json object with case-insensitive fields.", + "properties": { + "fields": { + "items": { + "$ref": "#/components/schemas/RichField_LTgwMzc0MTg2" + }, + "type": "array" + }, + "version": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "version", + "fields" + ], + "type": "object" + }, + "RmClient_MTQ5OTI2MDY3": { + "properties": { + "password": { + "description": "The password of the authenticated user for verification. The password is not required for deleting temporary clients. or if the user has only SAML credentials.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "RoleName": { + "description": "Role name, between 2 and 128 chars, 'wire_' prefix is reserved for roles designed by Wire (i.e., no custom roles can have the same prefix)", + "type": "string" + }, + "Role_LTIzMjAzMjky": { + "description": "Role of the invited user", + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "SFTServer_NDQ0NDkwNDE2": { + "description": "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers", + "properties": { + "urls": { + "description": "Array containing exactly one SFT server address of the form 'https://:'", + "items": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "type": "array" + } + }, + "required": [ + "urls" + ], + "type": "object" + }, + "SFTUsername": { + "description": "String containing the SFT username", + "type": "string" + }, + "ScimTokenInfo_LTI5NjgwNzA1": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTime" + }, + "description": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "idp": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team", + "id", + "created_at", + "description", + "name" + ], + "type": "object" + }, + "ScimTokenList_NjQwNTYxOTAw": { + "properties": { + "tokens": { + "items": { + "$ref": "#/components/schemas/ScimTokenInfo_LTI5NjgwNzA1" + }, + "type": "array" + } + }, + "required": [ + "tokens" + ], + "type": "object" + }, + "ScimTokenName_LTgzOTM2OTI4": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "SearchResult_Contact_OTExNzg4MTE0": { + "properties": { + "documents": { + "description": "List of contacts found", + "items": { + "$ref": "#/components/schemas/Contact_LTcwODE3Mjc5" + }, + "type": "array" + }, + "found": { + "description": "Total number of hits", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "has_more": { + "description": "Indicates whether there are more results to be fetched", + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/PagingState" + }, + "returned": { + "description": "Total number of hits returned", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "search_policy": { + "$ref": "#/components/schemas/FederatedUserSearchPolicy_MzkwODA4MTM3" + }, + "took": { + "description": "Search time in ms", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "found", + "returned", + "took", + "documents", + "search_policy" + ], + "type": "object" + }, + "SearchResult_TeamContact_LTE0NjQ0NzMw": { + "properties": { + "documents": { + "description": "List of contacts found", + "items": { + "$ref": "#/components/schemas/TeamContact_LTI5MTIxODc0" + }, + "type": "array" + }, + "found": { + "description": "Total number of hits", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "has_more": { + "description": "Indicates whether there are more results to be fetched", + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/PagingState" + }, + "returned": { + "description": "Total number of hits returned", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "search_policy": { + "$ref": "#/components/schemas/FederatedUserSearchPolicy_MzkwODA4MTM3" + }, + "took": { + "description": "Search time in ms", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "found", + "returned", + "took", + "documents", + "search_policy" + ], + "type": "object" + }, + "SelfDeletingMessagesConfigB_Covered_Identity_LTY0NzQ4MzU1": { + "properties": { + "enforcedTimeoutSeconds": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforcedTimeoutSeconds" + ], + "type": "object" + }, + "SendActivationCode_LTgyNDAxNzEy": { + "description": "Data for requesting an email code to be sent. 'email' must be present.", + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "SendVerificationCode_MjgxNDgxODE2": { + "properties": { + "action": { + "$ref": "#/components/schemas/VerificationAction_LTU0MzYxNzUz" + }, + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "action", + "email" + ], + "type": "object" + }, + "ServerTime_LTM4NTI3MzIx": { + "description": "The current server time", + "properties": { + "time": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "time" + ], + "type": "object" + }, + "ServiceKeyPEM": { + "example": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0\nG06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH\nWvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV\nVPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS\nbUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8\n7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la\nnQIDAQAB\n-----END PUBLIC KEY-----\n", + "type": "string" + }, + "ServiceKeyType_NTEzNzI4NTA2": { + "enum": [ + "rsa" + ], + "type": "string" + }, + "ServiceKey_NzY5NTY5NzYy": { + "properties": { + "pem": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "size": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "type": { + "$ref": "#/components/schemas/ServiceKeyType_NTEzNzI4NTA2" + } + }, + "required": [ + "type", + "size", + "pem" + ], + "type": "object" + }, + "ServiceProfilePage_Njg1NDQ5Njc4": { + "properties": { + "has_more": { + "type": "boolean" + }, + "services": { + "items": { + "$ref": "#/components/schemas/ServiceProfile_LTc2MDQzNTk3" + }, + "type": "array" + } + }, + "required": [ + "has_more", + "services" + ], + "type": "object" + }, + "ServiceProfile_LTc2MDQzNTk3": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + }, + "summary": { + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/ServiceTag_LTMyNTEzNjYy" + }, + "type": "array" + } + }, + "required": [ + "id", + "provider", + "name", + "summary", + "description", + "assets", + "tags", + "enabled" + ], + "type": "object" + }, + "ServiceRef_LTgxMjY3NzAz": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id", + "provider" + ], + "type": "object" + }, + "ServiceTagList": { + "items": { + "$ref": "#/components/schemas/ServiceTag_LTMyNTEzNjYy" + }, + "type": "array" + }, + "ServiceTag_LTMyNTEzNjYy": { + "enum": [ + "audio", + "books", + "business", + "design", + "education", + "entertainment", + "finance", + "fitness", + "food-drink", + "games", + "graphics", + "health", + "integration", + "lifestyle", + "media", + "medical", + "movies", + "music", + "news", + "photography", + "poll", + "productivity", + "quiz", + "rating", + "shopping", + "social", + "sports", + "travel", + "tutorial", + "video", + "weather" + ], + "type": "string" + }, + "Service_MjcyOTA5NjQx": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" + }, + "auth_tokens": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "minItems": 1, + "type": "array" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "public_keys": { + "items": { + "$ref": "#/components/schemas/ServiceKey_NzY5NTY5NzYy" + }, + "minItems": 1, + "type": "array" + }, + "summary": { + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/ServiceTag_LTMyNTEzNjYy" + }, + "type": "array" + } + }, + "required": [ + "id", + "name", + "summary", + "description", + "base_url", + "auth_tokens", + "public_keys", + "assets", + "tags", + "enabled" + ], + "type": "object" + }, + "SetSearchable_NDAxODAxODI5": { + "properties": { + "set_searchable": { + "type": "boolean" + } + }, + "required": [ + "set_searchable" + ], + "type": "object" + }, + "SignedCertificate": { + "type": "string" + }, + "SimpleMember_NTY5MTcxMzcx": { + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "SomeKey": {}, + "SomeUserToken": { + "type": "string" + }, + "SsoSettings": { + "properties": { + "default_sso_code": { + "$ref": "#/components/schemas/URI" + } + }, + "type": "object" + }, + "Sso_LTg1MDM5ODQ3": { + "properties": { + "issuer": { + "type": "string" + }, + "nameid": { + "type": "string" + } + }, + "required": [ + "issuer", + "nameid" + ], + "type": "object" + }, + "SupportedProtocolUpdate_LTE3Njk3MDM4": { + "properties": { + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocolTag_LTM0MDE1NTEx" + }, + "type": "array" + } + }, + "required": [ + "supported_protocols" + ], + "type": "object" + }, + "SystemSettingsPublic_LTgwNTMxNjU2": { + "properties": { + "nomadProfiles": { + "description": "Whether Nomad client profiles are enabled; null or absence means not enabled.", + "type": "boolean" + }, + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" + } + }, + "required": [ + "setRestrictUserCreation" + ], + "type": "object" + }, + "SystemSettings_ODU3MDk5MTA3": { + "properties": { + "nomadProfiles": { + "description": "Whether Nomad client profiles are enabled; null or absence means not enabled.", + "type": "boolean" + }, + "setEnableMls": { + "description": "Whether MLS is enabled or not", + "type": "boolean" + }, + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" + } + }, + "required": [ + "setRestrictUserCreation", + "setEnableMls" + ], + "type": "object" + }, + "TeamBinding_LTE4NTM5MTc0": { + "deprecated": true, + "description": "Deprecated, please ignore.", + "enum": [ + true, + false + ], + "type": "boolean" + }, + "TeamCollaborator_LTI3MzM1MTYz": { + "properties": { + "permissions": { + "items": { + "$ref": "#/components/schemas/CollaboratorPermission_NDg5NTg2ODgy" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "team", + "permissions" + ], + "type": "object" + }, + "TeamContact_LTI5MTIxODc0": { + "properties": { + "accent_id": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "email_unvalidated": { + "$ref": "#/components/schemas/Email" + }, + "handle": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy_NTI0ODc0NTQx" + }, + "name": { + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role_LTIzMjAzMjky" + }, + "saml_idp": { + "type": "string" + }, + "scim_external_id": { + "type": "string" + }, + "searchable": { + "type": "boolean" + }, + "sso": { + "$ref": "#/components/schemas/Sso_LTg1MDM5ODQ3" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/UserType_LTU1OTU4OTM5" + }, + "user_groups": { + "description": "List of user group ids the user is a member of", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "id", + "type", + "name", + "user_groups", + "searchable" + ], + "type": "object" + }, + "TeamConversationList_OTI3MzY3NzY0": { + "description": "Team conversation list", + "properties": { + "conversations": { + "items": { + "$ref": "#/components/schemas/TeamConversation_LTIwNzgyNTEz" + }, + "type": "array" + } + }, + "required": [ + "conversations" + ], + "type": "object" + }, + "TeamConversation_LTIwNzgyNTEz": { + "description": "Team conversation data", + "properties": { + "conversation": { + "$ref": "#/components/schemas/UUID" + }, + "managed": { + "description": "This field MUST NOT be used by clients. It is here only for backwards compatibility of the interface." + } + }, + "required": [ + "conversation", + "managed" + ], + "type": "object" + }, + "TeamDeleteData_ODI5NTU0ODE5": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" + } + }, + "type": "object" + }, + "TeamDomainRedirectTag_MjQwMjc1Mjk3": { + "enum": [ + "no-registration", + "none" + ], + "type": "string" + }, + "TeamInviteConfig_MTg4Nzk4NzMz": { + "properties": { + "domain_redirect": { + "$ref": "#/components/schemas/TeamDomainRedirectTag_MjQwMjc1Mjk3" + }, + "sso": { + "example": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "format": "uuid", + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "team_invite": { + "$ref": "#/components/schemas/TeamInviteTag_LTQyNTMyNzA0" + } + }, + "required": [ + "team_invite", + "team" + ], + "type": "object" + }, + "TeamInviteTag_LTQyNTMyNzA0": { + "enum": [ + "allowed", + "not-allowed", + "team" + ], + "type": "string" + }, + "TeamMemberDeleteData_LTg2OTEyOTI4": { + "description": "Data for a team member deletion request in case of binding teams.", + "properties": { + "password": { + "description": "The account password to authorise the deletion.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "TeamMemberList_Optional_LTM1ODE2MzM0": { + "description": "list of team member", + "properties": { + "hasMore": { + "$ref": "#/components/schemas/ListType_LTkyMDM4MzA1" + }, + "members": { + "description": "the array of team members", + "items": { + "$ref": "#/components/schemas/TeamMember_Optional_NTU0MDcyNzI1" + }, + "type": "array" + } + }, + "required": [ + "members", + "hasMore" + ], + "type": "object" + }, + "TeamMember_Optional_NTU0MDcyNzI1": { + "description": "team member data", + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "legalhold_status": { + "$ref": "#/components/schemas/UserLegalHoldStatus_LTQ2ODA2NTU5" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions_NDE0ODM5NDUx" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user" + ], + "type": "object" + }, + "TeamMembersPage_NzYwNDIxODgx": { + "properties": { + "hasMore": { + "type": "boolean" + }, + "members": { + "items": { + "$ref": "#/components/schemas/TeamMember_Optional_NTU0MDcyNzI1" + }, + "type": "array" + }, + "pagingState": { + "$ref": "#/components/schemas/TeamMembers_PagingState" + } + }, + "required": [ + "members", + "hasMore", + "pagingState" + ], + "type": "object" + }, + "TeamMembers_PagingState": { + "type": "string" + }, + "TeamSearchVisibilityView_Mzg3MzMzMTk3": { + "description": "Search visibility value for the team", + "properties": { + "search_visibility": { + "$ref": "#/components/schemas/TeamSearchVisibility_LTIzODE2Njk3" + } + }, + "required": [ + "search_visibility" + ], + "type": "object" + }, + "TeamSearchVisibility_LTIzODE2Njk3": { + "description": "value of visibility", + "enum": [ + "standard", + "no-name-outside-team" + ], + "type": "string" + }, + "TeamSize_LTMzMzk2MTk1": { + "description": "Team member counts broken down by user type.", + "properties": { + "teamSize": { + "description": "Total team members (teamSizeRegulars + teamSizeApps).", + "exclusiveMinimum": false, + "minimum": 0, + "type": "integer" + }, + "teamSizeApps": { + "description": "Number of apps in team.", + "exclusiveMinimum": false, + "minimum": 0, + "type": "integer" + }, + "teamSizeRegulars": { + "description": "Number of regular users in team.", + "exclusiveMinimum": false, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "teamSizeRegulars", + "teamSizeApps" + ], + "type": "object" + }, + "TeamUpdateData_LTE0NTM2NTU5": { + "properties": { + "icon": { + "$ref": "#/components/schemas/Icon" + }, + "icon_key": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "splash_screen": { + "$ref": "#/components/schemas/Icon" + } + }, + "type": "object" + }, + "Team_NDg4MjQwOTIw": { + "description": "`binding` is deprecated, and should be ignored. The non-binding teams API is not used (and will not be supported from API version V4 onwards), and `binding` will always be `true`.", + "properties": { + "binding": { + "$ref": "#/components/schemas/TeamBinding_LTE4NTM5MTc0" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "icon": { + "$ref": "#/components/schemas/Icon" + }, + "icon_key": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "splash_screen": { + "$ref": "#/components/schemas/Icon" + } + }, + "required": [ + "id", + "creator", + "name", + "icon" + ], + "type": "object" + }, + "Time": { + "properties": { + "time": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "time" + ], + "type": "object" + }, + "TimeZone": { + "type": "string" + }, + "Token": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "TokenType_NTkyMzk4MjIz": { + "enum": [ + "Bearer" + ], + "type": "string" + }, + "Transport_NDk2NzU5NDIy": { + "description": "Transport", + "enum": [ + "GCM", + "APNS", + "APNS_SANDBOX", + "APNS_VOIP", + "APNS_VOIP_SANDBOX" + ], + "type": "string" + }, + "TurnURI": { + "type": "string" + }, + "TurnUsername": { + "description": "Username to use for authenticating against the given TURN servers", + "type": "string" + }, + "TypingStatus_LTg5MzcyNDMy": { + "enum": [ + "started", + "stopped" + ], + "type": "string" + }, + "URI": { + "type": "string" + }, + "URIRef_Absolute": { + "description": "URL of the invitation link to be sent to the invitee", + "type": "string" + }, + "UTCTime": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + }, + "UTCTimeMillis": { + "description": "The time when the session was created", + "example": "2021-05-12T10:52:02.671Z", + "format": "yyyy-mm-ddThh:MM:ss.qqqZ", + "type": "string" + }, + "UUID": { + "description": "The OAuth client's ID", + "example": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "format": "uuid", + "type": "string" + }, + "UncheckedPrekeyBundle_LTU1MzQzOTgy": { + "properties": { + "id": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ], + "type": "object" + }, + "UpdateBotPrekeys_LTg3NzYxODg0": { + "properties": { + "prekeys": { + "items": { + "$ref": "#/components/schemas/UncheckedPrekeyBundle_LTU1MzQzOTgy" + }, + "type": "array" + } + }, + "required": [ + "prekeys" + ], + "type": "object" + }, + "UpdateClient_NzU5MjA4MzI1": { + "properties": { + "capabilities": { + "$ref": "#/components/schemas/ClientCapabilityList" + }, + "label": { + "description": "A new name for this client.", + "type": "string" + }, + "lastkey": { + "$ref": "#/components/schemas/UncheckedPrekeyBundle_LTU1MzQzOTgy" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "prekeys": { + "description": "New prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/components/schemas/UncheckedPrekeyBundle_LTU1MzQzOTgy" + }, + "type": "array" + } + }, + "type": "object" + }, + "UpdateMeeting_NTExNzYxMTcz": { + "description": "Request to update a meeting", + "properties": { + "end_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "recurrence": { + "$ref": "#/components/schemas/Recurrence_LTQ0OTc0ODE2" + }, + "start_time": { + "$ref": "#/components/schemas/UTCTime" + }, + "title": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "tzid": { + "$ref": "#/components/schemas/TimeZone" + } + }, + "type": "object" + }, + "UpdateProvider_LTQwMjY4MDgy": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "type": "object" + }, + "UpdateServiceConn_LTQ1OTYwNjIz": { + "properties": { + "auth_tokens": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "maxItems": 2, + "minItems": 1, + "type": "array" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "enabled": { + "type": "boolean" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "public_keys": { + "items": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "maxItems": 2, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "UpdateServiceWhitelist_LTU5MDAwMTIw": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + }, + "whitelisted": { + "type": "boolean" + } + }, + "required": [ + "provider", + "id", + "whitelisted" + ], + "type": "object" + }, + "UpdateService_MjAxNzQ2Njkz": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" + }, + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "summary": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/ServiceTag_LTMyNTEzNjYy" + }, + "maxItems": 3, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, + "UpdateUserGroupChannels_LTIyMjcwMTMx": { + "properties": { + "channels": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "channels" + ], + "type": "object" + }, + "UpdateUserGroupMembers_LTg1MzQ2NDY3": { + "properties": { + "members": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "members" + ], + "type": "object" + }, + "UserClientMap": { + "additionalProperties": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": "object" + }, + "UserClientPrekeyMap": { + "additionalProperties": { + "additionalProperties": { + "properties": { + "id": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ], + "type": "object" + }, + "type": "object" + }, + "example": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": { + "44901fb0712e588f": { + "id": 1, + "key": "pQABAQECoQBYIOjl7hw0D8YRNq..." + } + } + }, + "type": "object" + }, + "UserClients": { + "additionalProperties": { + "items": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "type": "array" + }, + "description": "Map of user id to list of client ids.", + "example": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + }, + "type": "object" + }, + "UserConnection_LTY3NzU1ODg0": { + "properties": { + "conversation": { + "$ref": "#/components/schemas/UUID" + }, + "from": { + "$ref": "#/components/schemas/UUID" + }, + "last_update": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "qualified_conversation": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "qualified_to": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "status": { + "$ref": "#/components/schemas/Relation_LTE4OTU5MTk4" + }, + "to": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "from", + "qualified_to", + "status", + "last_update" + ], + "type": "object" + }, + "UserGroupAddUsers_LTgzOTYzNzk0": { + "properties": { + "members": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "members" + ], + "type": "object" + }, + "UserGroupNameAvailability_LTYzMDE1NTk4": { + "properties": { + "name_available": { + "type": "boolean" + } + }, + "required": [ + "name_available" + ], + "type": "object" + }, + "UserGroupPage_UserGroup_Const_LTMxNDg5MDAy": { + "description": "This is the last page if it contains fewer rows than requested. There may be 0 rows on a page.", + "properties": { + "page": { + "items": { + "$ref": "#/components/schemas/UserGroup_Const_NTMzOTAzMzA1" + }, + "type": "array" + }, + "total": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "page", + "total" + ], + "type": "object" + }, + "UserGroupUpdate_MjUyNTA3Mjgy": { + "properties": { + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "UserGroup_Const_NTMzOTAzMzA1": { + "properties": { + "channels": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "type": "array" + }, + "channelsCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "createdAt": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "managedBy": { + "$ref": "#/components/schemas/ManagedBy_NTI0ODc0NTQx" + }, + "membersCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "name", + "managedBy", + "createdAt" + ], + "type": "object" + }, + "UserGroup_Identity_NTg4MTY1MjEx": { + "properties": { + "channels": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "type": "array" + }, + "channelsCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "createdAt": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "managedBy": { + "$ref": "#/components/schemas/ManagedBy_NTI0ODc0NTQx" + }, + "members": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "membersCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "name", + "members", + "managedBy", + "createdAt" + ], + "type": "object" + }, + "UserIdList_MzA1MTI1Njgx": { + "properties": { + "user_ids": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "user_ids" + ], + "type": "object" + }, + "UserLegalHoldStatusResponse_LTQ1MzUxMTE3": { + "properties": { + "client": { + "$ref": "#/components/schemas/IdObject_ClientId_LTM3NjQyODM5" + }, + "last_prekey": { + "$ref": "#/components/schemas/UncheckedPrekeyBundle_LTU1MzQzOTgy" + }, + "status": { + "$ref": "#/components/schemas/UserLegalHoldStatus_LTQ2ODA2NTU5" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "UserLegalHoldStatus_LTQ2ODA2NTU5": { + "description": "The state of Legal Hold compliance for the member", + "enum": [ + "enabled", + "pending", + "disabled", + "no_consent" + ], + "type": "string" + }, + "UserMap_Set_PubClient": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/PubClient" + }, + "type": "array", + "uniqueItems": true + }, + "description": "Map of UserId to (Set PubClient)", + "example": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + { + "class": "legalhold", + "id": "d0" + } + ] + }, + "type": "object" + }, + "UserProfile_LTQzMTQxMTE1": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "app": { + "$ref": "#/components/schemas/AppInfo_MjgwNTkwOTUz" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" + }, + "contact_status": { + "$ref": "#/components/schemas/ContactStatus_LTUzNzk1MzM4" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "expires_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "legalhold_status": { + "$ref": "#/components/schemas/UserLegalHoldStatus_LTQ2ODA2NTU5" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict_DEPRECATED_USE_ASSETS_INSTEAD" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "searchable": { + "type": "boolean" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef_LTgxMjY3NzAz" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocolTag_LTM0MDE1NTEx" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/UserType_LTU1OTU4OTM5" + } + }, + "required": [ + "qualified_id", + "name", + "accent_id", + "legalhold_status" + ], + "type": "object" + }, + "UserSSOId": { + "properties": { + "scim_external_id": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "tenant": { + "type": "string" + } + }, + "type": "object" + }, + "UserType_LTU1OTU4OTM5": { + "enum": [ + "regular", + "app", + "bot" + ], + "type": "string" + }, + "UserUpdate_MjQ4NTEwOTQz": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict_DEPRECATED_USE_ASSETS_INSTEAD" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "type": "object" + }, + "User_NjA4OTQwMTQ4": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/Asset_LTIyMjc1NDEz" + }, + "type": "array" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "email_unvalidated": { + "$ref": "#/components/schemas/Email" + }, + "expires_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy_NTI0ODc0NTQx" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict_DEPRECATED_USE_ASSETS_INSTEAD" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "searchable": { + "type": "boolean" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef_LTgxMjY3NzAz" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + }, + "status": { + "$ref": "#/components/schemas/AccountStatus_NzkzNDU1ODU5" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocolTag_LTM0MDE1NTEx" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/UserType_LTU1OTU4OTM5" + } + }, + "required": [ + "qualified_id", + "type", + "name", + "accent_id", + "status", + "locale" + ], + "type": "object" + }, + "VerificationAction_LTU0MzYxNzUz": { + "enum": [ + "create_scim_token", + "login", + "delete_team" + ], + "type": "string" + }, + "VerifyDeleteUser_Njc1NDQ1MDIy": { + "description": "Data for verifying an account deletion.", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "VersionInfo_NTEzMTgzNDQ0": { + "example": { + "development": [ + 18 + ], + "domain": "example.com", + "federation": false, + "supported": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18 + ] + }, + "properties": { + "development": { + "items": { + "$ref": "#/components/schemas/VersionNumber_Njk2NzI5Njk1" + }, + "type": "array" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "federation": { + "type": "boolean" + }, + "supported": { + "items": { + "$ref": "#/components/schemas/VersionNumber_Njk2NzI5Njk1" + }, + "type": "array" + } + }, + "required": [ + "supported", + "development", + "federation", + "domain" + ], + "type": "object" + }, + "VersionNumber_Njk2NzI5Njk1": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18 + ], + "type": "integer" + }, + "Versiond_18_PvtAmlGupCfgBaIy_LTUyNzk3MzUx": { + "properties": { + "deletionTimeoutDuration": { + "type": "string" + }, + "promotionStrategy": { + "$ref": "#/components/schemas/PreventAdminlessGroupsPromotionStrategy_Mjc1ODA2MDU1" + }, + "reminderTimeoutDurations": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "promotionStrategy", + "deletionTimeoutDuration", + "reminderTimeoutDurations" + ], + "type": "object" + }, + "ViewLegalHoldServiceInfo_LTc3NjI2MzQ3": { + "properties": { + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "fingerprint": { + "$ref": "#/components/schemas/Fingerprint" + }, + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "team_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team_id", + "base_url", + "fingerprint", + "auth_token", + "public_key" + ], + "type": "object" + }, + "ViewLegalHoldService_LTE3MzQzNDkw": { + "properties": { + "settings": { + "$ref": "#/components/schemas/ViewLegalHoldServiceInfo_LTc3NjI2MzQ3" + }, + "status": { + "$ref": "#/components/schemas/LHServiceStatus_ODc3NzE0Mjg3" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "WireIdPAPIVersion_NTEyMzIwNTU3": { + "enum": [ + "WireIdPAPIV1", + "WireIdPAPIV2" + ], + "type": "string" + }, + "WireIdP_ODMzOTExMzYw": { + "properties": { + "apiVersion": { + "enum": [ + "WireIdPAPIV1", + "WireIdPAPIV2" + ], + "type": "string" + }, + "domain": { + "example": "example.com", + "type": "string" + }, + "handle": { + "type": "string" + }, + "oldIssuers": { + "items": { + "$ref": "#/components/schemas/URI" + }, + "type": "array" + }, + "replacedBy": { + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team", + "apiVersion", + "oldIssuers", + "replacedBy", + "handle", + "domain" + ], + "type": "object" + }, + "v2_ConversationAccessData_MjMxMTI5ODc3": { + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy_LTYwOTAxMDI1" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + } + }, + "required": [ + "access" + ], + "type": "object" + }, + "v2_OwnConversation_GroupConvTypeLegacy_MjQ0OTcyNjQ3": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy_LTYwOTAxMDI1" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/EpochTimestamp" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvTypeLegacy_NTUxMDI2Mzkw" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers_LTEwMzUzODMy" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite" + ], + "type": "object" + }, + "v2_OwnConversation_GroupConvType_LTU2MzYxNTg0": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy_LTYwOTAxMDI1" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/EpochTimestamp" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers_LTEwMzUzODMy" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite" + ], + "type": "object" + }, + "v3_OwnConversation_GroupConvType_LTU2MzYxNTg0V3": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/EpochTimestamp" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers_LTEwMzUzODMy" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite" + ], + "type": "object" + }, + "v6_OwnConversation_GroupConvType_LTU2MzYxNTg0V6": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers_LTEwMzUzODMy" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + }, + "v9_OwnConversation_GroupConvType_LTU2MzYxNTg0": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access_NjkyMzE5ODc0" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole_Mzk3MDYzMzcw" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission_LTE1MzgzNzE3" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState_LTg4MDEwNDA5" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType_LTU4NjU0MTY5" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "history": { + "$ref": "#/components/schemas/History" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers_LTEwMzUzODMy" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/ProtocolTag_ODg1MTE5NjEw" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_Conversation_LTQ5NDQwNjc5" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType_MzM0NTE3ODE5" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + } + }, + "securitySchemes": { + "ZAuth": { + "description": "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be presented in this format: 'Bearer \\'.", + "in": "header", + "name": "Authorization", + "type": "apiKey" + } + } + }, + "info": { + "description": "## Authentication / Authorization\n\nThe end-points in this API support differing authorization protocols:\nsome are unauthenticated (`/api-version`, `/login`), some require\n[zauth](), and some support both [zauth]() and [oauth]().\n\nThe end-points that require zauth are labelled so in the description\nbelow. The end-points that support oauth as an alternative to zauth\nhave the required oauth scopes listed in the same description.\n\nFuther reading:\n- https://docs.wire.com/developer/reference/oauth.html\n- https://github.com/wireapp/wire-server/blob/develop/libs/wire-api/src/Wire/API/Routes/Public.hs (search for HasSwagger instances)\n- `curl https://staging-nginz-https.zinfra.io/v4/api/swagger.json | jq '.security, .securityDefinitions`\n\n### SSO Endpoints\n\n#### Overview\n\n`/sso/metadata` will be requested by the IdPs to learn how to talk to wire.\n\n`/sso/initiate-login`, `/sso/finalize-login` are for the SAML authentication handshake performed by a user in order to log into wire. They are not exactly standard in their details: they may return HTML or XML; redirect to error URLs instead of throwing errors, etc.\n\n`/identity-providers` end-points are for use in the team settings page when IdPs are registered. They talk json.\n\n\n#### Configuring IdPs\n\nIdPs usually allow you to copy the metadata into your clipboard. That should contain all the details you need to post the idp in your team under `/identity-providers`. (Team id is derived from the authorization credentials of the request.)\n\n##### okta.com\n\nOkta will ask you to provide two URLs when you set it up for talking to wireapp:\n\n1. The `Single sign on URL`. This is the end-point that accepts the user's credentials after successful authentication against the IdP. Choose `/sso/finalize-login` with schema and hostname of the wire server you are configuring.\n\n2. The `Audience URI`. You can find this in the metadata returned by the `/sso/metadata` end-point. It is the contents of the `md:OrganizationURL` element.\n\n##### centrify.com\n\nCentrify allows you to upload the metadata xml document that you get from the `/sso/metadata` end-point. You can also enter the metadata url and have centrify retrieve the xml, but to guarantee integrity of the setup, the metadata should be copied from the team settings page and pasted into the centrify setup page without any URL indirections.\n\n## Federation errors\n\nEndpoints involving federated calls to other domains can return some extra failure responses, common to all endpoints. Instead of listing them as possible responses for each endpoint, we document them here.\n\nFor errors that are more likely to be transient, we suggest clients to retry whatever request resulted in the error. Transient errors are indicated explicitly below.\n\n**Note**: when a failure occurs as a result of making a federated RPC to another backend, the error response contains the following extra fields:\n\n - `type`: \"federation\" (just the literal string in quotes, which can be used as an error type identifier when parsing errors)\n - `domain`: the target backend of the RPC that failed;\n - `path`: the path of the RPC that failed.\n\n### Domain errors\n\nErrors in this category result from trying to communicate with a backend that is considered non-existent or invalid. They can result from invalid user input or client issues, but they can also be a symptom of misconfiguration in one or multiple backends. These errors have a 4xx status code.\n\n - **Remote backend not found** (status: 422, label: `invalid-domain`): This backend attempted to contact a backend which does not exist or is not properly configured. For the most part, clients can consider this error equivalent to a domain not existing, although it should be noted that certain mistakes in the DNS configuration on a remote backend can lead to the backend not being recognized, and hence to this error. It is therefore not advisable to take any destructive action upon encountering this error, such as deleting remote users from conversations.\n - **Federation denied locally** (status: 400, label: `federation-denied`): This backend attempted an RPC to a non-whitelisted backend. Similar considerations as for the previous error apply.\n - **Federation not enabled** (status: 400, label: `federation-not-enabled`): Federation has not been configured for this backend. This will happen if a federation-aware client tries to talk to a backend for which federation is disabled, or if federation was disabled on the backend after reaching a federation-specific state (e.g. conversations with remote users). There is no way to cleanly recover from these errors at this point.\n\n### Local federation errors\n\nAn error in this category likely indicates an issue with the configuration of federation on the local backend. Possibly transient errors are indicated explicitly below. All these errors have a 500 status code.\n\n - **Federation unavailable** (status: 500, label: `federation-not-available`): Federation is configured for this backend, but the local federator cannot be reached. This can be transient, so clients should retry the request.\n - **Federation not implemented** (status: 422, label: `federation-not-implemented`): Federated behaviour for a certain endpoint is not yet implemented.\n - **Federator discovery failed** (status: 400, label: `discovery-failure`): A DNS error occurred during discovery of a remote backend. This can be transient, so clients should retry the request.\n - **Local federation error** (status: 500, label: `federation-local-error`): An error occurred in the communication between this backend and its local federator. These errors are most likely caused by bugs in the backend, and should be reported as such.\n\n### Remote federation errors\n\nErrors in this category are returned in case of communication issues between the local backend and a remote one, or if the remote side encountered an error while processing an RPC. Some errors in this category might be caused by incorrect client behaviour, wrong user input, or incorrect certificate configuration. Possibly transient errors are indicated explicitly. We use non-standard 5xx status codes for these errors.\n\n - **HTTP2 error** (status: 533, label: `federation-http2-error`): The current federator encountered an error when making an HTTP2 request to a remote one. Check the error message for more details.\n - **Connection refused** (status: 521, label: `federation-connection-refused`): The local federator could not connect to a remote one. This could be transient, so clients should retry the request.\n - **TLS failure**: (status: 525, label: `federation-tls-error`): An error occurred during the TLS handshake between the local federator and a remote one. This is most likely due to an issue with the certificate on the remote end.\n - **Remote federation error** (status: 533, label: `federation-remote-error`): The remote backend could not process a request coming from this backend. Check the error message for more details.\n - **Version negotiation error** (status: 533, label: `federation-version-error`): The remote backend returned invalid version information.\n\n### Backend compatibility errors\n\nAn error in this category will be returned when this backend makes an invalid or unsupported RPC to another backend. This can indicate some incompatibility between backends or a backend bug. These errors are unlikely to be transient, so retrying requests is *not* advised.\n\n - **Version mismatch** (status: 531, label: `federation-version-mismatch`): A remote backend is running an unsupported version of the federator.\n - **Invalid content type** (status: 533, label: `federation-invalid-content-type`): An RPC to another backend returned with an invalid content type.\n - **Unsupported content type** (status: 533, label: `federation-unsupported-content-type`): An RPC to another backend returned with an unsupported content type.\n", + "title": "Wire-Server API", + "version": "" + }, + "openapi": "3.0.0", + "paths": { + "/access": { + "post": { + "description": " [internal route ID: \"access\"]\n\nYou can provide only a cookie or a cookie and token. Every other combination is invalid. Access tokens can be given as query parameter or authorisation header, with the latter being preferred.", + "operationId": "access", + "parameters": [ + { + "in": "query", + "name": "client_id", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessToken_ODIyMTczMjMw" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AccessToken_ODIyMTczMjMw" + } + } + }, + "description": "OK", + "headers": { + "Set-Cookie": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Obtain an access tokens for a cookie" + } + }, + "/access/logout": { + "post": { + "description": " [internal route ID: \"logout\"]\n\nCalling this endpoint will effectively revoke the given cookie and subsequent calls to /access with the same cookie will result in a 403.", + "operationId": "logout", + "responses": { + "200": { + "description": "Logout" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Log out in order to remove a cookie from the server" + } + }, + "/access/self/email": { + "put": { + "description": " [internal route ID: \"change-self-email\"]\n\n", + "operationId": "change-self-email", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EmailUpdate_NjQ5MDg1OTY0" + } + } + }, + "required": true + }, + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "Update accepted and pending activation of the new email" + }, + "204": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "No update, current and new email address are the same\n\nEmail address activated" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid e-mail address. (label: `invalid-email`) or `body`" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Change your email address" + } + }, + "/activate": { + "get": { + "description": " [internal route ID: \"get-activate\"]\n\nSee also 'POST /activate' which has a larger feature set.", + "operationId": "get-activate", + "parameters": [ + { + "description": "Activation key", + "in": "query", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Activation code", + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse_LTIyOTY5NDE3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse_LTIyOTY5NDE3" + } + } + }, + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful." + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone", + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `code` or `key`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Activate (i.e. confirm) an email address." + }, + "post": { + "description": " [internal route ID: \"post-activate\"]\n\nActivation only succeeds once and the number of failed attempts for a valid key is limited.", + "operationId": "post-activate", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Activate_MzUzNzIxODUw" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse_LTIyOTY5NDE3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse_LTIyOTY5NDE3" + } + } + }, + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful." + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone", + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Activate (i.e. confirm) an email address." + } + }, + "/activate/send": { + "post": { + "description": " [internal route ID: \"post-activate-send\"]\n\n", + "operationId": "post-activate-send", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SendActivationCode_LTgyNDAxNzEy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Activation code sent." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "blacklisted-email", + "message": "The given e-mail address has been blacklisted due to a permanent bounce or a complaint." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + }, + "451": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 451, + "label": "domain-blocked-for-registration", + "message": "[Customer extension] The email domain has been blocked for Wire users. Please contact your IT department." + }, + "properties": { + "code": { + "enum": [ + 451 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-blocked-for-registration" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "[Customer extension] The email domain has been blocked for Wire users. Please contact your IT department. (label: `domain-blocked-for-registration`)" + } + }, + "summary": "Send (or resend) an email activation code." + } + }, + "/api-version": { + "get": { + "description": " [internal route ID: \"get-version\"]\n\n", + "operationId": "get-version", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/VersionInfo_NTEzMTgzNDQ0" + } + } + }, + "description": "" + } + } + } + }, + "/assets": { + "post": { + "description": " [internal route ID: \"assets-upload\"]\n\n

Construct the request as multipart/mixed; set header Content-Type: multipart/mixed; boundary=<boundary>.

Use exactly two parts in this order:

  1. application/json metadata (AssetSettings)
  2. application/octet-stream asset bytes

Each part must include Content-Type and Content-Length; the second part may include Content-MD5. Use CRLF between headers and bodies.

When asset audit logging is enabled, the JSON metadata must include:

  • convId: object { id: UUID, domain: String } (qualified conversation ID)
  • filename: String
  • filetype: String MIME type (e.g. image/png, application/pdf)

Optional metadata: public (Bool, default false), retention (one of eternal, persistent, volatile, eternal-infrequent_access, expiring).

For profile pictures or team icons without a conversation, set convId.id to 00000000-0000-0000-0000-000000000000 and convId.domain to the tenant’s domain; use any reasonable filename.

Note: the server treats the asset bytes as application/octet-stream; filetype is used for auditing only.

Example body (boundary=frontier):

Content-Type: multipart/mixed; boundary=frontier

--frontier
Content-Type: application/json
Content-Length: 191

{\"public\":false,\"retention\":\"volatile\",\"convId\":{\"id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"domain\":\"example.com\"},\"filename\":\"report.pdf\",\"filetype\":\"application/pdf\"}
--frontier
Content-Type: application/octet-stream
Content-Length: 11

Hello Audit
--frontier--
", + "operationId": "assets-upload", + "requestBody": { + "content": { + "multipart/mixed": { + "schema": { + "$ref": "#/components/schemas/AssetSource" + } + } + }, + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server." + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset_Qualified_AssetKey_MzU1MjMxNTA5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Asset_Qualified_AssetKey_MzU1MjMxNTA5" + } + } + }, + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "incomplete-body", + "message": "HTTP content-length header does not match body size" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "incomplete-body", + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nHTTP content-length header does not match body size (label: `incomplete-body`)\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Asset too large (label: `client-error`)" + } + }, + "summary": "Upload an asset" + } + }, + "/assets/{key_domain}/{key}": { + "delete": { + "description": " [internal route ID: \"assets-delete\"]\n\n**Note**: only local assets can be deleted.", + "operationId": "assets-delete", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key_domain` or `key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Delete an asset" + }, + "get": { + "description": " [internal route ID: \"assets-download\"]\n\n**Note**: local assets result in a redirect, while remote assets are streamed directly.", + "operationId": "assets-download", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset returned directly with content type `application/octet-stream`" + }, + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key_domain` or `key` or Asset not found (label: `not-found`)\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Download an asset" + } + }, + "/assets/{key}/token": { + "delete": { + "description": " [internal route ID: \"tokens-delete\"]\n\n**Note**: deleting the token makes the asset public.", + "operationId": "tokens-delete", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset token deleted" + } + }, + "summary": "Delete an asset token" + }, + "post": { + "description": " [internal route ID: \"tokens-renew\"]\n\n", + "operationId": "tokens-renew", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewAssetToken_NTAwMDQwODYy" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Renew an asset token" + } + }, + "/await": { + "get": { + "description": " [internal route ID: \"await-notifications\"]\n\n", + "externalDocs": { + "description": "RFC 6455", + "url": "https://datatracker.ietf.org/doc/html/rfc6455" + }, + "operationId": "await-notifications", + "parameters": [ + { + "description": "Client ID", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "101": { + "description": "Connection upgraded." + }, + "426": { + "description": "Upgrade required." + } + }, + "summary": "Establish websocket connection" + } + }, + "/bot/assets": { + "post": { + "description": " [internal route ID: (\"assets-upload-v3\", bot)]\n\n

Construct the request as multipart/mixed; set header Content-Type: multipart/mixed; boundary=<boundary>.

Use exactly two parts in this order:

  1. application/json metadata (AssetSettings)
  2. application/octet-stream asset bytes

Each part must include Content-Type and Content-Length; the second part may include Content-MD5. Use CRLF between headers and bodies.

When asset audit logging is enabled, the JSON metadata must include:

  • convId: object { id: UUID, domain: String } (qualified conversation ID)
  • filename: String
  • filetype: String MIME type (e.g. image/png, application/pdf)

Optional metadata: public (Bool, default false), retention (one of eternal, persistent, volatile, eternal-infrequent_access, expiring).

For profile pictures or team icons without a conversation, set convId.id to 00000000-0000-0000-0000-000000000000 and convId.domain to the tenant’s domain; use any reasonable filename.

Note: the server treats the asset bytes as application/octet-stream; filetype is used for auditing only.

Example body (boundary=frontier):

Content-Type: multipart/mixed; boundary=frontier

--frontier
Content-Type: application/json
Content-Length: 191

{\"public\":false,\"retention\":\"volatile\",\"convId\":{\"id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"domain\":\"example.com\"},\"filename\":\"report.pdf\",\"filetype\":\"application/pdf\"}
--frontier
Content-Type: application/octet-stream
Content-Length: 11

Hello Audit
--frontier--
", + "operationId": "assets-upload-v3_bot", + "requestBody": { + "content": { + "multipart/mixed": { + "schema": { + "$ref": "#/components/schemas/AssetSource" + } + } + }, + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server." + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset_Qualified_AssetKey_MzU1MjMxNTA5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Asset_Qualified_AssetKey_MzU1MjMxNTA5" + } + } + }, + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "incomplete-body", + "message": "HTTP content-length header does not match body size" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "incomplete-body", + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nHTTP content-length header does not match body size (label: `incomplete-body`)\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Asset too large (label: `client-error`)" + } + }, + "summary": "Upload an asset" + } + }, + "/bot/assets/{key}": { + "delete": { + "description": " [internal route ID: (\"assets-delete-v3\", bot)]\n\n", + "operationId": "assets-delete-v3_bot", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Delete an asset" + }, + "get": { + "description": " [internal route ID: (\"assets-download-v3\", bot)]\n\n", + "operationId": "assets-download-v3_bot", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` or Asset not found (label: `not-found`)" + } + }, + "summary": "Download an asset" + } + }, + "/bot/client": { + "get": { + "description": " [internal route ID: \"bot-get-client\"]\n\n", + "operationId": "bot-get-client", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + } + } + }, + "description": "Client found" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Client not found (label: `client-not-found`)\n\nClient not found (label: `client-not-found`)" + } + }, + "summary": "Get client for bot" + } + }, + "/bot/client/prekeys": { + "get": { + "description": " [internal route ID: \"bot-list-prekeys\"]\n\n", + "operationId": "bot-list-prekeys", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List prekeys for bot" + }, + "post": { + "description": " [internal route ID: \"bot-update-prekeys\"]\n\n", + "operationId": "bot-update-prekeys", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateBotPrekeys_LTg3NzYxODg0" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Client not found (label: `client-not-found`)" + } + }, + "summary": "Update prekeys for bot" + } + }, + "/bot/conversation": { + "get": { + "description": " [internal route ID: \"get-bot-conversation\"]\n\n", + "operationId": "get-bot-conversation", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/BotConvView_LTYzMjIzMjQz" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" + } + } + } + }, + "/bot/conversations/{conv}": { + "post": { + "description": " [internal route ID: \"add-bot\"]\n\n", + "operationId": "add-bot", + "parameters": [ + { + "in": "path", + "name": "conv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AddBot_NjI0ODkyODk3" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddBotResponse_ODA5MzA2NTA1" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AddBotResponse_ODA5MzA2NTA1" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "service-disabled", + "message": "The desired service is currently disabled." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "service-disabled", + "too-many-members", + "invalid-conversation", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The desired service is currently disabled. (label: `service-disabled`)\n\nMaximum number of members per conversation reached. (label: `too-many-members`)\n\nThe operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Add bot" + } + }, + "/bot/conversations/{conv}/{bot}": { + "delete": { + "description": " [internal route ID: \"remove-bot\"]\n\n", + "operationId": "remove-bot", + "parameters": [ + { + "in": "path", + "name": "conv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "bot", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoveBotResponse_LTUxNTQ4MDEy" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RemoveBotResponse_LTUxNTQ4MDEy" + } + } + }, + "description": "User found" + }, + "204": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-conversation", + "message": "The operation is not allowed in this conversation." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-conversation", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Remove bot" + } + }, + "/bot/messages": { + "post": { + "description": " [internal route ID: \"post-bot-message-unqualified\"]\n\n", + "operationId": "post-bot-message-unqualified", + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewOtrMessage_LTUyMTE5MTMw" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + } + }, + "description": "Message sent" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation not found (label: `no-conversation`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + } + }, + "description": "Missing clients" + } + } + } + }, + "/bot/self": { + "delete": { + "description": " [internal route ID: \"bot-delete-self\"]\n\n", + "operationId": "bot-delete-self", + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-bot", + "message": "The targeted user is not a bot." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-bot", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The targeted user is not a bot. (label: `invalid-bot`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Delete self" + }, + "get": { + "description": " [internal route ID: \"bot-get-self\"]\n\n", + "operationId": "bot-get-self", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserProfile_LTQzMTQxMTE1" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "User not found (label: `not-found`)" + } + }, + "summary": "Get self" + } + }, + "/bot/users": { + "get": { + "description": " [internal route ID: \"bot-list-users\"]\n\n", + "operationId": "bot-list-users", + "parameters": [ + { + "in": "query", + "name": "ids", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/BotUserView_LTE2MTkwMTcw" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List users" + } + }, + "/bot/users/prekeys": { + "post": { + "description": " [internal route ID: \"bot-claim-users-prekeys\"]\n\n", + "operationId": "bot-claim-users-prekeys", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserClients" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserClientPrekeyMap" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "missing-legalhold-consent-old-clients", + "too-many-clients", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nToo many clients (label: `too-many-clients`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Claim users prekeys" + } + }, + "/bot/users/{user}/clients": { + "get": { + "description": " [internal route ID: \"bot-get-user-clients\"]\n\n", + "operationId": "bot-get-user-clients", + "parameters": [ + { + "in": "path", + "name": "user", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/PubClient" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Get user clients" + } + }, + "/broadcast/otr/messages": { + "post": { + "description": " [internal route ID: \"post-otr-broadcast-unqualified\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts two query params:\n- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are ignored.\n - When 'false' all missing clients are reported.\n - When comma separated list of user-ids, only clients for listed users are ignored.\n- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are reported.\n - When 'false' all missing clients are ignored.\n - When comma separated list of user-ids, only clients for listed users are reported.\n\nApart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\nAll three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n- `report_missing` in the request body has highest precedence.\n- `ignore_missing` in the query param is the next.\n- `report_missing` in the query param has the lowest precedence.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "operationId": "post-otr-broadcast-unqualified", + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewOtrMessage_LTUyMTE5MTMw" + } + }, + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/NewOtrMessage_LTUyMTE5MTMw" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + } + }, + "description": "Message sent" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "too-many-users-to-broadcast", + "message": "Too many users to fan out the broadcast event to" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-users-to-broadcast" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body` or `report_missing` or `ignore_missing`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "non-binding-team", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Broadcast an encrypted message to all team members and all contacts (accepts JSON or Protobuf)" + } + }, + "/broadcast/proteus/messages": { + "post": { + "description": " [internal route ID: \"post-proteus-broadcast\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n- `ignore_all`: When set, no checks about missing clients are carried out.\n- `report_only`: Takes a list of qualified UserIDs. If any clients of the listed users are missing, the message is not sent. The missing clients are reported in the response.\n- `ignore_only`: Takes a list of qualified UserIDs. If any clients of the non-listed users are missing, the message is not sent. The missing clients are reported in the response.\n\nThe sending of messages in a federated conversation could theoretically fail partially. To make this case unlikely, the backend first gets a list of clients from all the involved backends and then tries to send a message. So, if any backend is down, the message is not propagated to anyone. But the actual message fan out to multiple backends could still fail partially. This type of failure is reported as a 201, the clients for which the message sending failed are part of the response body.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "operationId": "post-proteus-broadcast", + "requestBody": { + "content": { + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/QualifiedNewOtrMessage" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" + } + } + }, + "description": "Message sent" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "too-many-users-to-broadcast", + "message": "Too many users to fan out the broadcast event to" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-users-to-broadcast" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "non-binding-team", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Post an encrypted message to all team members and all contacts (accepts only Protobuf)" + } + }, + "/calls/config/v2": { + "get": { + "description": " [internal route ID: \"get-calls-config-v2\"]\n\n", + "operationId": "get-calls-config-v2", + "parameters": [ + { + "description": "Limit resulting list. Allowed values [1..10]", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "maximum": 10, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RTCConfiguration_LTIwOTc4OTk0" + } + } + }, + "description": "" + } + }, + "summary": "Retrieve all TURN server addresses and credentials. Clients are expected to do a DNS lookup to resolve the IP addresses of the given hostnames " + } + }, + "/clients": { + "get": { + "description": " [internal route ID: \"list-clients\"]\n\n", + "operationId": "list-clients", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + }, + "type": "array" + } + } + }, + "description": "List of clients" + } + }, + "summary": "List the registered clients" + }, + "post": { + "description": " [internal route ID: \"add-client\"]\n\n", + "operationId": "add-client", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewClient_ODg1NjY4Njgy" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + } + } + }, + "description": "Client registered", + "headers": { + "Location": { + "description": "Client ID", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "bad-request", + "message": "Malformed prekeys uploaded" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "bad-request" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMalformed prekeys uploaded (label: `bad-request`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "missing-auth", + "too-many-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nToo many clients (label: `too-many-clients`)" + } + }, + "summary": "Register a new client" + } + }, + "/clients/{cid}/access-token": { + "post": { + "description": " [internal route ID: \"create-access-token\"]\n\nCreate an JWT DPoP access token for the client CSR, given a JWT DPoP proof, specified in the `DPoP` header. The access token will be returned in the JSON response body as a JWT DPoP token.", + "operationId": "create-access-token", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "cid", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "DPoP", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DPoPAccessTokenResponse_LTgyODU5MDE3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DPoPAccessTokenResponse_LTgyODU5MDE3" + } + } + }, + "description": "Access token created", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Create a JWT DPoP access token" + } + }, + "/clients/{client}": { + "delete": { + "description": " [internal route ID: \"delete-client\"]\n\n", + "operationId": "delete-client", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RmClient_MTQ5OTI2MDY3" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Client deleted" + } + }, + "summary": "Delete an existing client" + }, + "get": { + "description": " [internal route ID: \"get-client\"]\n\n", + "operationId": "get-client", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Client_MTM1OTcwOTQ1" + } + } + }, + "description": "Client found" + }, + "404": { + "description": "`client` or Client not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a registered client by ID" + }, + "put": { + "description": " [internal route ID: \"update-client\"]\n\n", + "operationId": "update-client", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateClient_NzU5MjA4MzI1" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Client updated" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-duplicate-public-key", + "message": "MLS public key for the given signature scheme already exists" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-duplicate-public-key", + "bad-request" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMLS public key for the given signature scheme already exists (label: `mls-duplicate-public-key`)\n\nMalformed prekeys uploaded (label: `bad-request`)" + } + }, + "summary": "Update a registered client" + } + }, + "/clients/{client}/capabilities": { + "get": { + "description": " [internal route ID: \"get-client-capabilities\"]\n\n", + "operationId": "get-client-capabilities", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientCapabilityList" + } + } + }, + "description": "" + } + }, + "summary": "Read back what the client has been posting about itself" + } + }, + "/clients/{client}/nonce": { + "get": { + "description": " [internal route ID: \"get-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "operationId": "get-nonce", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + } + }, + "Replay-Nonce": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Get a new nonce for a client CSR" + }, + "head": { + "description": " [internal route ID: \"head-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "operationId": "head-nonce", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "No Content", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + } + }, + "Replay-Nonce": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Get a new nonce for a client CSR" + } + }, + "/clients/{client}/prekeys": { + "get": { + "description": " [internal route ID: \"get-client-prekeys\"]\n\n", + "operationId": "get-client-prekeys", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "List the remaining prekey IDs of a client" + } + }, + "/connections/{uid_domain}/{uid}": { + "get": { + "description": " [internal route ID: \"get-connection\"]\n\n", + "operationId": "get-connection", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + } + }, + "description": "Connection found" + }, + "404": { + "description": "`uid_domain` or `uid` or Connection not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get an existing connection to another user (local or remote)" + }, + "post": { + "description": " [internal route ID: \"create-connection\"]\n\nYou can have no more than 1000 connections in accepted or sent state", + "operationId": "create-connection", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + } + }, + "description": "Connection existed" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + } + }, + "description": "Connection was created" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-user", + "message": "Invalid user" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-user" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid user (label: `invalid-user`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified email" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "connection-limit", + "missing-legalhold-consent", + "missing-legalhold-consent-old-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The user has no verified email (label: `no-identity`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)" + } + }, + "summary": "Create a connection to another user" + }, + "put": { + "description": " [internal route ID: \"update-connection\"]\n\n", + "operationId": "update-connection", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConnectionUpdate_LTU3MTA1OTA5" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection_LTY3NzU1ODg0" + } + } + }, + "description": "Connection updated" + }, + "204": { + "description": "Connection unchanged" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-user", + "message": "Invalid user" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-user" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified email" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "bad-conn-update", + "not-connected", + "connection-limit", + "missing-legalhold-consent", + "missing-legalhold-consent-old-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The user has no verified email (label: `no-identity`)\n\nInvalid status transition (label: `bad-conn-update`)\n\nUsers are not connected (label: `not-connected`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)" + } + }, + "summary": "Update a connection to another user" + } + }, + "/conversations": { + "post": { + "description": " [internal route ID: \"create-group-conversation\"]\n\nThis returns 201 when a new conversation is created, and 200 when the conversation already existed\nOAuth scope: `write:conversations`", + "operationId": "create-group-conversation", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewConv_LTgzNTk1NDQx" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateGroupConversation_GroupConvType_MTQ3OTc4NDk3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateGroupConversation_GroupConvType_MTQ3OTc4NDk3" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "history-not-supported", + "message": "Shared history is not supported on this conversation" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "history-not-supported", + "mls-not-enabled", + "non-empty-member-list" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nShared history is not supported on this conversation (label: `history-not-supported`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nAttempting to add group members outside MLS (label: `non-empty-member-list`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "channels-not-enabled", + "message": "The channels feature is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "channels-not-enabled", + "not-mls-conversation", + "missing-legalhold-consent", + "operation-denied", + "no-team-member", + "not-connected", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The channels feature is not enabled for this team (label: `channels-not-enabled`)\n\nThis operation requires an MLS conversation (label: `not-mls-conversation`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nUsers are not connected (label: `not-connected`)\n\nConversation access denied (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Create a new conversation" + } + }, + "/conversations/code-check": { + "post": { + "description": " [internal route ID: \"code-check\"]\n\nIf the guest links team feature is disabled, this will fail with 404 CodeNotFound.Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/join` which responds with 409 GuestLinksDisabled if guest links are disabled.", + "operationId": "code-check", + "parameters": [ + { + "in": "header", + "name": "X-Forwarded-For", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCode_Mjg3OTI1NTMx" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Valid" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-conversation-password", + "message": "Invalid conversation password" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid conversation password (label: `invalid-conversation-password`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + } + }, + "summary": "Check validity of a conversation code." + } + }, + "/conversations/join": { + "get": { + "description": " [internal route ID: \"get-conversation-by-reusable-code\"]\n\n", + "operationId": "get-conversation-by-reusable-code", + "parameters": [ + { + "in": "query", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCoverView_LTMwNDkxMTA1" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "access-denied", + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid conversation password (label: `invalid-conversation-password`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Get limited conversation information by key/code pair" + }, + "post": { + "description": " [internal route ID: \"join-conversation-by-code-unqualified\"]\n\nIf the guest links team feature is disabled, this will fail with 409 GuestLinksDisabled.Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/code-check` which responds with 404 CodeNotFound if guest links are disabled.", + "operationId": "join-conversation-by-code-unqualified", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/JoinConversationByCode_NjgzMzM4Mjg5" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Conversation joined" + }, + "204": { + "description": "Conversation unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "too-many-members", + "message": "Maximum number of members per conversation reached" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-members", + "no-team-member", + "invalid-op", + "access-denied", + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Maximum number of members per conversation reached (label: `too-many-members`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid conversation password (label: `invalid-conversation-password`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Join a conversation using a reusable code" + } + }, + "/conversations/list": { + "post": { + "description": " [internal route ID: \"list-conversations\"]\n\n", + "operationId": "list-conversations", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ListConversations_MjkxMTIwODMz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationsResponse_GroupConvType_ODkxMjM2ODM0" + } + } + }, + "description": "" + } + }, + "summary": "Get conversation metadata for a list of conversation ids" + } + }, + "/conversations/list-ids": { + "post": { + "description": " [internal route ID: \"list-conversation-ids\"]\n\nThe IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", + "operationId": "list-conversation-ids", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetMuliTabPgRqs_ConvrIdLcOm10_ODU0NjQ1NDMz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MultiTabePg_ConvrsIdqf_cLORm_Q_Mzg0MjYzNzM0" + } + } + }, + "description": "" + } + }, + "summary": "Get all conversation IDs." + } + }, + "/conversations/mls-self": { + "get": { + "description": " [internal route ID: \"get-mls-self-conversation\"]\n\n", + "operationId": "get-mls-self-conversation", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/v9_OwnConversation_GroupConvType_LTU2MzYxNTg0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/v9_OwnConversation_GroupConvType_LTU2MzYxNTg0" + } + } + }, + "description": "The MLS self-conversation" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + } + }, + "summary": "Get the user's MLS self-conversation" + } + }, + "/conversations/self": { + "post": { + "description": " [internal route ID: \"create-self-conversation\"]\n\n", + "operationId": "create-self-conversation", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/v6_OwnConversation_GroupConvType_LTU2MzYxNTg0V6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/v6_OwnConversation_GroupConvType_LTU2MzYxNTg0V6" + } + } + }, + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/v6_OwnConversation_GroupConvType_LTU2MzYxNTg0V6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/v6_OwnConversation_GroupConvType_LTU2MzYxNTg0V6" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + } + }, + "summary": "Create a self-conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}": { + "get": { + "description": " [internal route ID: \"get-conversation\"]\n\n", + "operationId": "get-conversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Conversation_GroupConvType_MzQzMTQ1OTg3" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get a conversation by ID" + } + }, + "/conversations/{cnv_domain}/{cnv}/access": { + "put": { + "description": " [internal route ID: \"update-conversation-access\"]\n\n", + "operationId": "update-conversation-access", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationAccessData_MjMxMTI5ODc3" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Access updated" + }, + "204": { + "description": "Access unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid target access" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid target access (label: `invalid-op`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nInsufficient authorization (missing modify_conversation_access) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update access modes for a conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/add-permission": { + "put": { + "description": " [internal route ID: \"update-channel-add-permission\"]\n\n", + "operationId": "update-channel-add-permission", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AddPermissionUpdate_LTU3MzEwOTY4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Add permissions updated" + }, + "204": { + "description": "Add permissions unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid target access" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "not-connected", + "operation-denied", + "no-team-member", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid target access (label: `invalid-op`)\n\nUsers are not connected (label: `not-connected`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_add_permissions) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nTeam not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Update the permissions for adding members to a channel" + } + }, + "/conversations/{cnv_domain}/{cnv}/groupinfo": { + "get": { + "description": " [internal route ID: \"get-group-info\"]\n\n", + "operationId": "get-group-info", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/GroupInfoData" + } + } + }, + "description": "The group information" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-missing-group-info", + "message": "The conversation has no group information" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-missing-group-info", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nThe conversation has no group information (label: `mls-missing-group-info`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get MLS group information" + } + }, + "/conversations/{cnv_domain}/{cnv}/history": { + "put": { + "description": " [internal route ID: \"update-conversation-history\"]\n\n", + "operationId": "update-conversation-history", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationHistoryUpdate_LTg5MDQ5Nzgx" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "History updated" + }, + "204": { + "description": "History unchanged" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "history-not-supported", + "message": "Shared history is not supported on this conversation" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "history-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nShared history is not supported on this conversation (label: `history-not-supported`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "action-denied", + "message": "Insufficient authorization (missing modify_conversation_access)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "action-denied", + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient authorization (missing modify_conversation_access) (label: `action-denied`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update history settings of a conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/members": { + "post": { + "description": " [internal route ID: \"add-members-to-conversation\"]\n\n", + "operationId": "add-members-to-conversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InviteQualified_ODYyODIyNjYz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Conversation updated" + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-group-id-not-supported", + "message": "The group ID version of the conversation is not supported by one of the federated backends" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-group-id-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "not-connected", + "no-team-member", + "access-denied", + "too-many-members", + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nMaximum number of members per conversation reached (label: `too-many-members`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInsufficient authorization (missing add_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Add qualified members to an existing conversation." + }, + "put": { + "description": " [internal route ID: \"replace-members-in-conversation\"]\n\nThis will add any members not already in the conversation, and remove any members not in the provided list except users that are associated via a user group. The given role in the request body will be applied to all added members. The roles of already existing members will not be changed even if these members are included in the request body and their role differs from the role provided in this request.", + "operationId": "replace-members-in-conversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InviteQualified_ODYyODIyNjYz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Conversation members replaced" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-group-id-not-supported", + "message": "The group ID version of the conversation is not supported by one of the federated backends" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-group-id-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "not-connected", + "no-team-member", + "access-denied", + "too-many-members", + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nMaximum number of members per conversation reached (label: `too-many-members`)\n\nInvalid operation (label: `invalid-op`)\n\nThe conversation would be left without an admin\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nInsufficient authorization (missing add_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Replace the members of a conversation." + } + }, + "/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}": { + "delete": { + "description": " [internal route ID: \"remove-member\"]\n\n", + "operationId": "remove-member", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Target User ID", + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Member removed" + }, + "204": { + "description": "No change" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "eligible_members": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "type": "array" + } + }, + "required": [ + "eligible_members" + ], + "type": "object" + } + } + }, + "description": "The conversation would be left without an admin\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `usr_domain` or `usr` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Remove a member from a conversation" + }, + "put": { + "description": " [internal route ID: \"update-other-member\"]\n\n**Note**: at least one field has to be provided.", + "operationId": "update-other-member", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Target User ID", + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OtherMemberUpdate_LTM1MjYzOTU0" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Membership updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInvalid target (label: `invalid-op`)\n\nInsufficient authorization (missing modify_other_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation-member", + "message": "Conversation member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation-member", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `usr_domain` or `usr` not found\n\nConversation member not found (label: `no-conversation-member`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update membership of the specified user" + } + }, + "/conversations/{cnv_domain}/{cnv}/message-timer": { + "put": { + "description": " [internal route ID: \"update-conversation-message-timer\"]\n\n", + "operationId": "update-conversation-message-timer", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationMessageTimerUpdate_LTcxMjUwNzQ4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Message timer updated" + }, + "204": { + "description": "Message timer unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_message_timer) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update the message timer for a conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/name": { + "put": { + "description": " [internal route ID: \"update-conversation-name\"]\n\n\nOAuth scope: `write:conversations_name`", + "operationId": "update-conversation-name", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRename_ODkwODg1MzQ0" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Name unchanged" + }, + "204": { + "description": "Name updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update conversation name" + } + }, + "/conversations/{cnv_domain}/{cnv}/proteus/messages": { + "post": { + "description": " [internal route ID: \"post-proteus-message\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n- `ignore_all`: When set, no checks about missing clients are carried out.\n- `report_only`: Takes a list of qualified UserIDs. If any clients of the listed users are missing, the message is not sent. The missing clients are reported in the response.\n- `ignore_only`: Takes a list of qualified UserIDs. If any clients of the non-listed users are missing, the message is not sent. The missing clients are reported in the response.\n\nThe sending of messages in a federated conversation could theoretically fail partially. To make this case unlikely, the backend first gets a list of clients from all the involved backends and then tries to send a message. So, if any backend is down, the message is not propagated to anyone. But the actual message fan out to multiple backends could still fail partially. This type of failure is reported as a 201, the clients for which the message sending failed are part of the response body.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "operationId": "post-proteus-message", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/QualifiedNewOtrMessage" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" + } + } + }, + "description": "Message sent" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or Conversation not found (label: `no-conversation`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus_ODg0NDgyNDk4" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Post an encrypted message to a conversation (accepts only Protobuf)" + } + }, + "/conversations/{cnv_domain}/{cnv}/protocol": { + "put": { + "description": " [internal route ID: \"update-conversation-protocol\"]\n\n**Note**: Only proteus->mixed upgrade is supported.", + "operationId": "update-conversation-protocol", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ProtocolUpdate_NzY1ODgxNDQy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Conversation updated" + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-migration-criteria-not-satisfied", + "message": "The migration criteria for mixed to MLS protocol transition are not satisfied for this conversation" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-migration-criteria-not-satisfied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nThe migration criteria for mixed to MLS protocol transition are not satisfied for this conversation (label: `mls-migration-criteria-not-satisfied`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "invalid-op", + "action-denied", + "invalid-protocol-transition" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nProtocol transition is invalid (label: `invalid-protocol-transition`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nTeam not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update the protocol of the conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/receipt-mode": { + "put": { + "description": " [internal route ID: \"update-conversation-receipt-mode\"]\n\n", + "operationId": "update-conversation-receipt-mode", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationReceiptModeUpdate_NDE4MzUzNTU3" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Receipt mode updated" + }, + "204": { + "description": "Receipt mode unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-receipts-not-allowed", + "message": "Read receipts on MLS conversations are not allowed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-receipts-not-allowed", + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Read receipts on MLS conversations are not allowed (label: `mls-receipts-not-allowed`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_receipt_mode) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update receipt mode for a conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/self": { + "get": { + "description": " [internal route ID: \"get-conversation-self\"]\n\n", + "operationId": "get-conversation-self", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Member_OTA5OTgyNzcw" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get self membership properties" + }, + "put": { + "description": " [internal route ID: \"update-conversation-self\"]\n\n**Note**: at least one field has to be provided.", + "operationId": "update-conversation-self", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MemberUpdate_LTg4NTQ0OTYz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Update successful" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update self membership properties" + } + }, + "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}": { + "delete": { + "description": " [internal route ID: \"delete-subconversation\"]\n\n", + "operationId": "delete-subconversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSReset_NzgwODA3ODc4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "Deletion successful" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-stale-message", + "message": "The conversation epoch in a message is too old" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-stale-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" + } + }, + "summary": "Delete an MLS subconversation" + }, + "get": { + "description": " [internal route ID: \"get-subconversation\"]\n\n", + "operationId": "get-subconversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicSubConversation_MjI2NTIxMzU4" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PublicSubConversation_MjI2NTIxMzU4" + } + } + }, + "description": "Subconversation" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-subconv-unsupported-convtype", + "message": "MLS subconversations are only supported for regular conversations" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-subconv-unsupported-convtype", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS subconversations are only supported for regular conversations (label: `mls-subconv-unsupported-convtype`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get information about an MLS subconversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}/groupinfo": { + "get": { + "description": " [internal route ID: \"get-subconversation-group-info\"]\n\n", + "operationId": "get-subconversation-group-info", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/GroupInfoData" + } + } + }, + "description": "The group information" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-missing-group-info", + "message": "The conversation has no group information" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-missing-group-info", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nThe conversation has no group information (label: `mls-missing-group-info`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get MLS group information of subconversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}/self": { + "delete": { + "description": " [internal route ID: \"leave-subconversation\"]\n\n", + "operationId": "leave-subconversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled", + "mls-protocol-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nMLS protocol error (label: `mls-protocol-error`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-stale-message", + "message": "The conversation epoch in a message is too old" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-stale-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" + } + }, + "summary": "Leave an MLS subconversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/typing": { + "post": { + "description": " [internal route ID: \"member-typing-qualified\"]\n\n", + "operationId": "member-typing-qualified", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TypingStatus_LTg5MzcyNDMy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Notification sent" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Sending typing notifications" + } + }, + "/conversations/{cnv}/code": { + "delete": { + "description": " [internal route ID: \"remove-code-unqualified\"]\n\n", + "operationId": "remove-code-unqualified", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Conversation code deleted." + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Delete conversation code" + }, + "get": { + "description": " [internal route ID: \"get-code\"]\n\n\nOAuth scope: `write:conversations_code`", + "operationId": "get-code", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo_LTc5MzgzNjg3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo_LTc5MzgzNjg3" + } + } + }, + "description": "Conversation Code" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Get existing conversation code" + }, + "post": { + "description": " [internal route ID: \"create-conversation-code-unqualified\"]\n\n\nOAuth scope: `write:conversations_code`", + "operationId": "create-conversation-code-unqualified", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateConversationCodeRequest_NTYzMTA1NDYz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo_LTc5MzgzNjg3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo_LTc5MzgzNjg3" + } + } + }, + "description": "Conversation code already exists." + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event_LTMwMTMyODM5" + } + } + }, + "description": "Conversation code created." + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "create-conv-code-conflict", + "message": "Conversation code already exists with a different password setting than the requested one." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "create-conv-code-conflict", + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation code already exists with a different password setting than the requested one. (label: `create-conv-code-conflict`)\n\nThe guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Create or recreate a conversation code" + } + }, + "/conversations/{cnv}/features/conversationGuestLinks": { + "get": { + "description": " [internal route ID: \"get-conversation-guest-links-status\"]\n\n", + "operationId": "get-conversation-guest-links-status", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_GuestLinksConfig_LTcwNjU0NDMw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get the status of the guest links feature for a conversation that potentially has been created by someone from another team." + } + }, + "/conversations/{cnv}/otr/messages": { + "post": { + "description": " [internal route ID: \"post-otr-message-unqualified\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts two query params:\n- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are ignored.\n - When 'false' all missing clients are reported.\n - When comma separated list of user-ids, only clients for listed users are ignored.\n- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are reported.\n - When 'false' all missing clients are ignored.\n - When comma separated list of user-ids, only clients for listed users are reported.\n\nApart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\nAll three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n- `report_missing` in the request body has highest precedence.\n- `ignore_missing` in the query param is the next.\n- `report_missing` in the query param has the lowest precedence.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "operationId": "post-otr-message-unqualified", + "parameters": [ + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "ignore_missing", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewOtrMessage_LTUyMTE5MTMw" + } + }, + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/NewOtrMessage_LTUyMTE5MTMw" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + } + }, + "description": "Message sent" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` or Conversation not found (label: `no-conversation`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch_ODUyODM0MDQ0" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Post an encrypted message to a conversation (accepts JSON or Protobuf)" + } + }, + "/conversations/{cnv}/roles": { + "get": { + "description": " [internal route ID: \"get-conversation-roles\"]\n\n", + "operationId": "get-conversation-roles", + "parameters": [ + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRolesList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get existing roles available for the given conversation" + } + }, + "/cookies": { + "get": { + "description": " [internal route ID: \"list-cookies\"]\n\n", + "operationId": "list-cookies", + "parameters": [ + { + "description": "Filter by label (comma-separated list)", + "in": "query", + "name": "labels", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CookieList_LTM4MzYwNzAz" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CookieList_LTM4MzYwNzAz" + } + } + }, + "description": "List of cookies" + } + }, + "summary": "Retrieve the list of cookies currently stored for the user" + } + }, + "/cookies/remove": { + "post": { + "description": " [internal route ID: \"remove-cookies\"]\n\n", + "operationId": "remove-cookies", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RemoveCookies_OTYwMTI0NDMy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Cookies revoked" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Revoke stored cookies" + } + }, + "/custom-backend/by-domain/{domain}": { + "get": { + "description": " [internal route ID: \"get-custom-backend-by-domain\"]\n\n", + "operationId": "get-custom-backend-by-domain", + "parameters": [ + { + "description": "URL-encoded email domain", + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CustomBackend_LTQxODI0MjQ0" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "custom-backend-not-found", + "message": "Custom backend not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "custom-backend-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`domain` not found\n\nCustom backend not found (label: `custom-backend-not-found`)" + } + }, + "summary": "Shows information about custom backends related to a given email domain" + } + }, + "/delete": { + "post": { + "description": " [internal route ID: \"verify-delete\"]\n\n", + "operationId": "verify-delete", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/VerifyDeleteUser_Njc1NDQ1MDIy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Deletion is initiated." + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-code", + "message": "Invalid verification code" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid verification code (label: `invalid-code`)" + } + }, + "summary": "Verify account deletion with a code." + } + }, + "/domain-verification/{domain}/authorize-team": { + "post": { + "description": " [internal route ID: \"domain-verification-authorize-team\"]\n\n", + "operationId": "domain-verification-authorize-team", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainOwnershipToken_NTU0ODc1NDE5" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Authorized" + }, + "401": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" + }, + "properties": { + "code": { + "enum": [ + 401 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-auth-failure" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" + }, + "402": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" + }, + "properties": { + "code": { + "enum": [ + 402 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-payment-required" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-forbidden-for-domain-registration-state" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Authorize a team to operate on a verified domain" + } + }, + "/domain-verification/{domain}/backend": { + "post": { + "description": " [internal route ID: \"update-domain-redirect\"]\n\n", + "operationId": "update-domain-redirect", + "parameters": [ + { + "in": "header", + "name": "Authorization", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainRedirectConfig_NTI5NDE5MDQy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Updated" + }, + "401": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" + }, + "properties": { + "code": { + "enum": [ + 401 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-auth-failure" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-forbidden-for-domain-registration-state" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Update the domain redirect configuration" + } + }, + "/domain-verification/{domain}/challenges": { + "post": { + "description": " [internal route ID: \"domain-verification-challenge\"]\n\n", + "operationId": "domain-verification-challenge", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainVerificationChallenge_NjIwMzA1MjE5" + } + } + }, + "description": "" + } + }, + "summary": "Get a DNS verification challenge" + } + }, + "/domain-verification/{domain}/challenges/{challengeId}": { + "post": { + "description": " [internal route ID: \"verify-challenge\"]\n\n", + "operationId": "verify-challenge", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "challengeId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChallengeToken_Mzk3NTcwOTM3" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainOwnershipToken_NTU0ODc1NDE5" + } + } + }, + "description": "" + }, + "401": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" + }, + "properties": { + "code": { + "enum": [ + 401 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-auth-failure" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "domain-verification-failed", + "message": "Domain verification failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-verification-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain verification failed (label: `domain-verification-failed`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "challenge-not-found", + "message": "Challenge not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "challenge-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`domain` or `challengeId` not found\n\nChallenge not found (label: `challenge-not-found`)" + } + }, + "summary": "Verify a DNS verification challenge" + } + }, + "/domain-verification/{domain}/team": { + "post": { + "description": " [internal route ID: \"update-team-invite\"]\n\n", + "operationId": "update-team-invite", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamInviteConfig_MTg4Nzk4NzMz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Updated" + }, + "402": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" + }, + "properties": { + "code": { + "enum": [ + 402 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-payment-required" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-forbidden-for-domain-registration-state" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Update the team-invite configuration" + } + }, + "/domain-verification/{domain}/team/challenges/{challengeId}": { + "post": { + "description": " [internal route ID: \"verify-challenge-team\"]\n\n", + "operationId": "verify-challenge-team", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "challengeId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChallengeToken_Mzk3NTcwOTM3" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainOwnershipToken_NTU0ODc1NDE5" + } + } + }, + "description": "" + }, + "401": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" + }, + "properties": { + "code": { + "enum": [ + 401 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-auth-failure" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" + }, + "402": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" + }, + "properties": { + "code": { + "enum": [ + 402 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-payment-required" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-forbidden-for-domain-registration-state" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Verify a DNS verification challenge for a team" + } + }, + "/events": { + "get": { + "description": " [internal route ID: \"consume-events\"]\n\nThis is the rabbitMQ-based variant of \"await-notifications\"", + "externalDocs": { + "description": "RFC 6455", + "url": "https://datatracker.ietf.org/doc/html/rfc6455" + }, + "operationId": "consume-events", + "parameters": [ + { + "description": "Client ID", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Synchronization marker ID", + "in": "query", + "name": "sync_marker", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "101": { + "description": "Connection upgraded." + }, + "426": { + "description": "Upgrade required." + } + }, + "summary": "Consume events over a websocket connection" + } + }, + "/feature-configs": { + "get": { + "description": " [internal route ID: \"get-all-feature-configs-for-user\"]\n\nGets feature configs for a user. If the user is a member of a team and has the required permissions, this will return the team's feature configs.If the user is not a member of a team, this will return the personal feature configs (the server defaults).\nOAuth scope: `read:feature_configs`", + "operationId": "get-all-feature-configs-for-user", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NP_LockableFtur_ghdCnfiSOVsyAvIRqExmDpB_MGwUT2Q_LTM1Mzc0ODgy" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team not found (label: `no-team`)" + } + }, + "summary": "Gets feature configs for a user" + } + }, + "/get-domain-registration": { + "post": { + "description": " [internal route ID: \"get-domain-registration\"]\n\n- `due_to_existing_account`: boolean (optional, only present if `domain_redirect` is `no-registration`)\n- `backend`: object (optional, must be present if `domain_redirect` is `backend`)\n - `config_url`: string (required)\n - `webapp_url`: string (optional)\n- `sso_code`: string (optional, must be present if `domain_redirect` is `sso`)", + "operationId": "get-domain-registration", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetDomainRegistrationRequest_LTg4NTM1MzM2" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainRedirectResponse_V10_LTEyMjI4NTM0" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-domain", + "message": "Invalid domain" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-domain" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid domain (label: `invalid-domain`)" + } + }, + "summary": "Get domain registration configuration by email" + } + }, + "/handles": { + "post": { + "description": " [internal route ID: \"check-user-handles\"]\n\n", + "operationId": "check-user-handles", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CheckHandles_LTc0OTkxMzAx" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Handle" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Handle" + }, + "type": "array" + } + } + }, + "description": "List of free handles" + } + }, + "summary": "Check availability of user handles" + } + }, + "/handles/{handle}": { + "head": { + "description": " [internal route ID: \"check-user-handle\"]\n\n", + "operationId": "check-user-handle", + "parameters": [ + { + "in": "path", + "name": "handle", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "Handle is taken" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-handle", + "message": "The given handle is invalid (less than 2 or more than 256 characters; chars not in \"a-z0-9_.-\"; or on the blocklist)" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-handle" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given handle is invalid (less than 2 or more than 256 characters; chars not in \"a-z0-9_.-\"; or on the blocklist) (label: `invalid-handle`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Handle not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`handle` not found\n\nHandle not found (label: `not-found`)" + } + }, + "summary": "Check whether a user handle can be taken" + } + }, + "/identity-providers": { + "get": { + "description": " [internal route ID: \"idp-get-all\"]\n\n", + "operationId": "idp-get-all", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPList" + } + } + }, + "description": "" + } + } + }, + "post": { + "description": " [internal route ID: \"idp-create\"]\n\nCreate a new identity provider.\n\nThe `api_version` parameter controls the uniqueness constraint for IdP issuers:\n- `v1`: IdP issuers must be globally unique across the entire backend (all teams)\n- `v2` (default): IdP issuers must be unique per team (can be reused across different teams)\n\nThese constraints apply to both, multi-ingress and standard backends.", + "operationId": "idp-create", + "parameters": [ + { + "in": "query", + "name": "replaces", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "api_version", + "required": false, + "schema": { + "default": "v2", + "enum": [ + "v1", + "v2" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "handle", + "required": false, + "schema": { + "maxLength": 32, + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP_NDA5MTE4Mjk0" + } + } + }, + "description": "" + } + } + } + }, + "/identity-providers/{id}": { + "delete": { + "description": " [internal route ID: \"idp-delete\"]\n\n", + "operationId": "idp-delete", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "purge", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "204": { + "description": "" + } + } + }, + "get": { + "description": " [internal route ID: \"idp-get\"]\n\n", + "operationId": "idp-get", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP_NDA5MTE4Mjk0" + } + } + }, + "description": "" + } + } + }, + "put": { + "description": " [internal route ID: \"idp-update\"]\n\n", + "operationId": "idp-update", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "handle", + "required": false, + "schema": { + "maxLength": 32, + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP_NDA5MTE4Mjk0" + } + } + }, + "description": "" + } + } + } + }, + "/identity-providers/{id}/raw": { + "get": { + "description": " [internal route ID: \"idp-get-raw\"]\n\n", + "operationId": "idp-get-raw", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/list-connections": { + "post": { + "description": " [internal route ID: \"list-connections\"]\n\nThe IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", + "operationId": "list-connections", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetMuliTabPgRqs_ConcLOrm501_LTIwMjA4NzYw" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MultiTabePg_ConcsLOrRmU_MTgwMzU4OTU5" + } + } + }, + "description": "" + } + }, + "summary": "List the connections to other users, including remote users" + } + }, + "/list-users": { + "post": { + "description": " [internal route ID: \"list-users-by-ids-or-handles\"]\n\nThe 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive.", + "operationId": "list-users-by-ids-or-handles", + "parameters": [ + { + "description": "Include whether each local user can currently be contacted", + "in": "query", + "name": "include-contact-status", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ListUsersQuery" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ListUsersById_LTQ5MTE3NDc0" + } + } + }, + "description": "" + } + }, + "summary": "List users" + } + }, + "/login": { + "post": { + "description": " [internal route ID: \"login\"]\n\nLogins are throttled at the server's discretion", + "operationId": "login", + "parameters": [ + { + "description": "Request a persistent cookie instead of a session cookie", + "in": "query", + "name": "persist", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Login_LTgyNTIzMTM1" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessToken_ODIyMTczMjMw" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AccessToken_ODIyMTczMjMw" + } + } + }, + "description": "OK", + "headers": { + "Set-Cookie": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "pending-activation", + "suspended", + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nAccount pending activation (label: `pending-activation`)\n\nAccount suspended (label: `suspended`)\n\nAuthentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Authenticate a user to obtain a cookie and first access token" + } + }, + "/meetings": { + "post": { + "description": " [internal route ID: \"create-meeting\"]\n\n\nOAuth scope: `write:meetings`", + "operationId": "create-meeting", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewMeeting_LTI1NTMzOTU5" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeetingWithConversation_LTMyNzA4NzU0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingWithConversation_LTMyNzA4NzU0" + } + } + }, + "description": "Meeting created" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid meeting times, empty update, or meetings feature disabled" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid meeting times, empty update, or meetings feature disabled (label: `invalid-op`)" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Create a new meeting" + } + }, + "/meetings/list": { + "get": { + "description": " [internal route ID: \"list-meetings\"]\n\n", + "operationId": "list-meetings", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Meeting_ODU0OTMzMTgw" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "List all meetings for the authenticated user" + } + }, + "/meetings/{domain}/{id}": { + "delete": { + "description": " [internal route ID: \"delete-meeting\"]\n\n", + "operationId": "delete-meeting", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Meeting deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid meeting times, empty update, or meetings feature disabled" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid meeting times, empty update, or meetings feature disabled (label: `invalid-op`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "meeting-not-found", + "message": "Meeting not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "meeting-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`domain` or `id` or Meeting not found (label: `meeting-not-found`)" + } + }, + "summary": "Delete a meeting" + }, + "get": { + "description": " [internal route ID: \"get-meeting\"]\n\n", + "operationId": "get-meeting", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Meeting_ODU0OTMzMTgw" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "meeting-not-found", + "message": "Meeting not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "meeting-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`domain` or `id` or Meeting not found (label: `meeting-not-found`)" + } + }, + "summary": "Get a single meeting by ID" + }, + "put": { + "description": " [internal route ID: \"update-meeting\"]\n\n", + "operationId": "update-meeting", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateMeeting_NTExNzYxMTcz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeetingWithConversation_LTMyNzA4NzU0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingWithConversation_LTMyNzA4NzU0" + } + } + }, + "description": "Meeting updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid meeting times, empty update, or meetings feature disabled" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid meeting times, empty update, or meetings feature disabled (label: `invalid-op`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "meeting-not-found", + "message": "Meeting not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "meeting-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`domain` or `id` or Meeting not found (label: `meeting-not-found`)" + } + }, + "summary": "Update an existing meeting" + } + }, + "/meetings/{domain}/{id}/invitations": { + "post": { + "description": " [internal route ID: \"add-meeting-invitation\"]\n\n", + "operationId": "add-meeting-invitation", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingEmailsInvitation_NzgyNzUzMzcz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Invitation added" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid meeting times, empty update, or meetings feature disabled" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid meeting times, empty update, or meetings feature disabled (label: `invalid-op`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "meeting-not-found", + "message": "Meeting not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "meeting-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`domain` or `id` or Meeting not found (label: `meeting-not-found`)" + } + }, + "summary": "Add an email to the invited emails" + }, + "put": { + "description": " [internal route ID: \"replace-meeting-invitation\"]\n\n", + "operationId": "replace-meeting-invitation", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingEmailsInvitation_NzgyNzUzMzcz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Invitations replaced" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid meeting times, empty update, or meetings feature disabled" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid meeting times, empty update, or meetings feature disabled (label: `invalid-op`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "meeting-not-found", + "message": "Meeting not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "meeting-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`domain` or `id` or Meeting not found (label: `meeting-not-found`)" + } + }, + "summary": "Replace the invited emails" + } + }, + "/meetings/{domain}/{id}/invitations/delete": { + "post": { + "description": " [internal route ID: \"remove-meeting-invitation\"]\n\n", + "operationId": "remove-meeting-invitation", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingEmailsInvitation_NzgyNzUzMzcz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Invitations removed" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid meeting times, empty update, or meetings feature disabled" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid meeting times, empty update, or meetings feature disabled (label: `invalid-op`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "meeting-not-found", + "message": "Meeting not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "meeting-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`domain` or `id` or Meeting not found (label: `meeting-not-found`)" + } + }, + "summary": "Remove emails from the invited emails" + } + }, + "/mls/commit-bundles": { + "post": { + "description": " [internal route ID: \"mls-commit-bundle\"]\n\n\n\n**Note**: this endpoint can execute proposals, and therefore return all possible errors associated with adding or removing members to a conversation, in addition to the ones listed below. See the documentation of [POST /conversations/{cnv}/members/v2](#/default/post_conversations__cnv__members_v2) and [POST /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}](#/default/delete_conversations__cnv_domain___cnv__members__usr_domain___usr_) for more details on the possible error responses of each type of proposal.", + "operationId": "mls-commit-bundle", + "requestBody": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/CommitBundle" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus_NjA1NDA0MTE4" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus_NjA1NDA0MTE4" + } + } + }, + "description": "Commit accepted and forwarded" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-invalid-leaf-node-signature", + "message": "Invalid leaf node signature" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-invalid-leaf-node-signature", + "mls-group-id-not-supported", + "mls-welcome-mismatch", + "mls-self-removal-not-allowed", + "mls-protocol-error", + "mls-not-enabled", + "mls-invalid-leaf-node-index", + "mls-group-conversation-mismatch", + "mls-commit-missing-references", + "mls-client-sender-user-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid leaf node signature (label: `mls-invalid-leaf-node-signature`)\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)\n\nSubmitted group info is inconsistent with the backend group state (label `inconsistent-group-state`)\n\nThe list of targets of a welcome message does not match the list of new clients in a group (label: `mls-welcome-mismatch`)\n\nSelf removal from group is not allowed (label: `mls-self-removal-not-allowed`)\n\nMLS protocol error (label: `mls-protocol-error`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nA referenced leaf node index points to a blank or non-existing node (label: `mls-invalid-leaf-node-index`)\n\nConversation ID resolved from Group ID does not match submitted Conversation ID (label: `mls-group-conversation-mismatch`)\n\nThe commit is not referencing all pending proposals (label: `mls-commit-missing-references`)\n\nUser ID resolved from Client ID does not match message's sender user ID (label: `mls-client-sender-user-mismatch`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-identity-mismatch", + "message": "Leaf node signature key does not match the client's key" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-identity-mismatch", + "mls-subconv-join-parent-missing", + "missing-legalhold-consent", + "legalhold-not-enabled", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Leaf node signature key does not match the client's key (label: `mls-identity-mismatch`)\n\nMLS client cannot join the subconversation because it is not member of the parent conversation (label: `mls-subconv-join-parent-missing`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-proposal-not-found", + "message": "A proposal referenced in a commit message could not be found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-proposal-not-found", + "no-conversation", + "no-conversation-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "A proposal referenced in a commit message could not be found (label: `mls-proposal-not-found`)\n\nConversation not found (label: `no-conversation`)\n\nConversation member not found (label: `no-conversation-member`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "missing_users": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "type": "array" + } + }, + "required": [ + "missing_users" + ], + "type": "object" + } + } + }, + "description": "Group is out of sync\n\nAdding members to the conversation is not possible because the backends involved do not form a fully connected graph\n\nA user who is under legal-hold may not participate in MLS conversations (label: `mls-legal-hold-not-allowed`)\n\nThe conversation epoch in a message is too old (label: `mls-stale-message`)\n\nA proposal of type Add or Remove does not apply to the full list of clients for a user (label: `mls-client-mismatch`)" + }, + "422": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 422, + "label": "mls-unsupported-proposal", + "message": "Unsupported proposal type" + }, + "properties": { + "code": { + "enum": [ + 422 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-unsupported-proposal", + "mls-unsupported-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unsupported proposal type (label: `mls-unsupported-proposal`)\n\nAttempted to send a message with an unsupported combination of content type and wire format (label: `mls-unsupported-message`)" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Post a MLS CommitBundle" + } + }, + "/mls/key-packages/claim/{user_domain}/{user}": { + "post": { + "description": " [internal route ID: \"mls-key-packages-claim\"]\n\nOnly key packages for the specified ciphersuite are claimed.", + "operationId": "mls-key-packages-claim", + "parameters": [ + { + "in": "path", + "name": "user_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "user", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Ciphersuite in hex format (e.g. 0x0002)", + "in": "query", + "name": "ciphersuite", + "required": true, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyPackageBundle_MjU2MjY0MDU2" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/KeyPackageBundle_MjU2MjY0MDU2" + } + } + }, + "description": "Claimed key packages" + } + }, + "summary": "Claim one key package for each client of the given user" + } + }, + "/mls/key-packages/self/{client}": { + "delete": { + "description": " [internal route ID: \"mls-key-packages-delete\"]\n\n", + "operationId": "mls-key-packages-delete", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Ciphersuite in hex format (e.g. 0x0002)", + "in": "query", + "name": "ciphersuite", + "required": true, + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteKeyPackages_LTQxNTcxNjY3" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "OK" + } + }, + "summary": "Delete all key packages for a given ciphersuite and client" + }, + "post": { + "description": " [internal route ID: \"mls-key-packages-upload\"]\n\nThe request body should be a json object containing a list of base64-encoded key packages.", + "operationId": "mls-key-packages-upload", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/KeyPackageUpload_NTQ2Mjk2NzEx" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Key packages uploaded" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-protocol-error", + "message": "MLS protocol error" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-protocol-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMLS protocol error (label: `mls-protocol-error`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-identity-mismatch", + "message": "Key package credential does not match qualified client ID" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-identity-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Key package credential does not match qualified client ID (label: `mls-identity-mismatch`)" + } + }, + "summary": "Upload a fresh batch of key packages" + }, + "put": { + "description": " [internal route ID: \"mls-key-packages-replace\"]\n\nThe request body should be a json object containing a list of base64-encoded key packages. Use this sparingly.", + "operationId": "mls-key-packages-replace", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Comma-separated list of ciphersuites in hex format (e.g. 0x0002)", + "in": "query", + "name": "ciphersuites", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/KeyPackageUpload_NTQ2Mjk2NzEx" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Key packages replaced" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-protocol-error", + "message": "MLS protocol error" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-protocol-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body` or `ciphersuites`\n\nMLS protocol error (label: `mls-protocol-error`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-identity-mismatch", + "message": "Key package credential does not match qualified client ID" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-identity-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Key package credential does not match qualified client ID (label: `mls-identity-mismatch`)" + } + }, + "summary": "Upload a fresh batch of key packages and replace the old ones" + } + }, + "/mls/key-packages/self/{client}/count": { + "get": { + "description": " [internal route ID: \"mls-key-packages-count\"]\n\n", + "operationId": "mls-key-packages-count", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Ciphersuite in hex format (e.g. 0x0002)", + "in": "query", + "name": "ciphersuite", + "required": true, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyPackageCount_LTYwNDg5MDcz" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/KeyPackageCount_LTYwNDg5MDcz" + } + } + }, + "description": "Number of key packages" + } + }, + "summary": "Return the number of unclaimed key packages for a given ciphersuite and client" + } + }, + "/mls/messages": { + "post": { + "description": " [internal route ID: \"mls-message\"]\n\n\n\n**Note**: this endpoint can execute proposals, and therefore return all possible errors associated with adding or removing members to a conversation, in addition to the ones listed below. See the documentation of [POST /conversations/{cnv}/members/v2](#/default/post_conversations__cnv__members_v2) and [POST /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}](#/default/delete_conversations__cnv_domain___cnv__members__usr_domain___usr_) for more details on the possible error responses of each type of proposal.", + "operationId": "mls-message", + "requestBody": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/MLSMessage" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus_NjA1NDA0MTE4" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus_NjA1NDA0MTE4" + } + } + }, + "description": "Message sent" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-invalid-leaf-node-signature", + "message": "Invalid leaf node signature" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-invalid-leaf-node-signature", + "mls-self-removal-not-allowed", + "mls-protocol-error", + "mls-not-enabled", + "mls-invalid-leaf-node-index", + "mls-group-conversation-mismatch", + "mls-commit-missing-references", + "mls-client-sender-user-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid leaf node signature (label: `mls-invalid-leaf-node-signature`)\n\nSubmitted group info is inconsistent with the backend group state (label `inconsistent-group-state`)\n\nSelf removal from group is not allowed (label: `mls-self-removal-not-allowed`)\n\nMLS protocol error (label: `mls-protocol-error`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nA referenced leaf node index points to a blank or non-existing node (label: `mls-invalid-leaf-node-index`)\n\nConversation ID resolved from Group ID does not match submitted Conversation ID (label: `mls-group-conversation-mismatch`)\n\nThe commit is not referencing all pending proposals (label: `mls-commit-missing-references`)\n\nUser ID resolved from Client ID does not match message's sender user ID (label: `mls-client-sender-user-mismatch`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-subconv-join-parent-missing", + "message": "MLS client cannot join the subconversation because it is not member of the parent conversation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-subconv-join-parent-missing", + "missing-legalhold-consent", + "legalhold-not-enabled", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS client cannot join the subconversation because it is not member of the parent conversation (label: `mls-subconv-join-parent-missing`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-proposal-not-found", + "message": "A proposal referenced in a commit message could not be found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-proposal-not-found", + "no-conversation", + "no-conversation-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "A proposal referenced in a commit message could not be found (label: `mls-proposal-not-found`)\n\nConversation not found (label: `no-conversation`)\n\nConversation member not found (label: `no-conversation-member`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "missing_users": { + "items": { + "$ref": "#/components/schemas/Qualified_Id_IdTag_User_LTQ1NTIwNDM1" + }, + "type": "array" + } + }, + "required": [ + "missing_users" + ], + "type": "object" + } + } + }, + "description": "Group is out of sync\n\nAdding members to the conversation is not possible because the backends involved do not form a fully connected graph\n\nThe conversation epoch in a message is too old (label: `mls-stale-message`)\n\nA proposal of type Add or Remove does not apply to the full list of clients for a user (label: `mls-client-mismatch`)" + }, + "422": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 422, + "label": "mls-unsupported-proposal", + "message": "Unsupported proposal type" + }, + "properties": { + "code": { + "enum": [ + 422 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-unsupported-proposal", + "mls-unsupported-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unsupported proposal type (label: `mls-unsupported-proposal`)\n\nAttempted to send a message with an unsupported combination of content type and wire format (label: `mls-unsupported-message`)" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Post an MLS message" + } + }, + "/mls/public-keys": { + "get": { + "description": " [internal route ID: \"mls-public-keys\"]\n\nThe format of the returned key is determined by the `format` query parameter:\n - raw (default): base64-encoded raw public keys\n - jwk: keys are nested objects in JWK format.", + "operationId": "mls-public-keys", + "parameters": [ + { + "in": "query", + "name": "format", + "required": false, + "schema": { + "enum": [ + "raw", + "jwk" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSKeysByPurpose_MLSKeys_SomeKey_NjkxMjk0MTAx" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSKeysByPurpose_MLSKeys_SomeKey_NjkxMjk0MTAx" + } + } + }, + "description": "Public keys" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `format`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + } + }, + "summary": "Get public keys used by the backend to sign external proposals" + } + }, + "/mls/reset-conversation": { + "post": { + "description": " [internal route ID: \"mls-reset-conversation\"]\n\n", + "operationId": "mls-reset-conversation", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSReset_NzgwODA3ODc4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Conversation reset" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-protocol-error", + "message": "MLS protocol error" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-protocol-error", + "mls-group-id-not-supported", + "mls-federated-reset-not-supported", + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS protocol error (label: `mls-protocol-error`)\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)\n\nReset is not supported by the owning backend of the conversation (label: `mls-federated-reset-not-supported`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`) or `body`" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "action-denied", + "message": "Insufficient authorization (missing leave_conversation)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "action-denied", + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-stale-message", + "message": "The conversation epoch in a message is too old" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-stale-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" + } + }, + "summary": "Reset an MLS conversation to epoch 0" + } + }, + "/notifications": { + "get": { + "description": " [internal route ID: \"get-notifications\"]\n\nSee also: GET /teams/notifications", + "operationId": "get-notifications", + "parameters": [ + { + "description": "Only return notifications more recent than this", + "in": "query", + "name": "since", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Maximum number of notifications to return", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 10000, + "minimum": 100, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueuedNotificationList_MTU0ODEyNTQ2" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotificationList_MTU0ODEyNTQ2" + } + } + }, + "description": "Notification list" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Some notifications not found (label: `not-found`)" + } + }, + "summary": "Fetch notifications" + } + }, + "/notifications/last": { + "get": { + "description": " [internal route ID: \"get-last-notification\"]\n\n", + "operationId": "get-last-notification", + "parameters": [ + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification_NTY2NzY2MTU2" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification_NTY2NzY2MTU2" + } + } + }, + "description": "Notification found" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Some notifications not found (label: `not-found`)" + } + }, + "summary": "Fetch the last notification" + } + }, + "/notifications/{id}": { + "get": { + "description": " [internal route ID: \"get-notification-by-id\"]\n\n", + "operationId": "get-notification-by-id", + "parameters": [ + { + "description": "Notification ID", + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification_NTY2NzY2MTU2" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification_NTY2NzY2MTU2" + } + } + }, + "description": "Notification found" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`id` or Some notifications not found (label: `not-found`)" + } + }, + "summary": "Fetch a notification by ID" + } + }, + "/oauth/applications": { + "get": { + "description": " [internal route ID: \"get-oauth-applications\"]\n\nGet all OAuth applications with active account access for a user.", + "operationId": "get-oauth-applications", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/OAuthApplication_Mjk5NTUxNjA1" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/OAuthApplication_Mjk5NTUxNjA1" + }, + "type": "array" + } + } + }, + "description": "OAuth applications found" + } + }, + "summary": "Get OAuth applications with account access" + } + }, + "/oauth/applications/{OAuthClientId}/sessions": { + "delete": { + "description": " [internal route ID: \"revoke-oauth-account-access\"]\n\n", + "operationId": "revoke-oauth-account-access", + "parameters": [ + { + "description": "The ID of the OAuth client", + "in": "path", + "name": "OAuthClientId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordReqBody_LTcxMzE3ODE3" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "OAuth application access revoked" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Revoke account access from an OAuth application" + } + }, + "/oauth/applications/{OAuthClientId}/sessions/{RefreshTokenId}": { + "delete": { + "description": " [internal route ID: \"delete-oauth-refresh-token\"]\n\nRevoke an active OAuth session by providing the refresh token ID.", + "operationId": "delete-oauth-refresh-token", + "parameters": [ + { + "description": "The ID of the OAuth client", + "in": "path", + "name": "OAuthClientId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "The ID of the refresh token", + "in": "path", + "name": "RefreshTokenId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordReqBody_LTcxMzE3ODE3" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`OAuthClientId` or `RefreshTokenId` not found\n\nOAuth client not found (label: `not-found`)" + } + }, + "summary": "Revoke an active OAuth session" + } + }, + "/oauth/authorization/codes": { + "post": { + "description": " [internal route ID: \"create-oauth-auth-code\"]\n\nCurrently only supports the 'code' response type, which corresponds to the authorization code flow.", + "operationId": "create-oauth-auth-code", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateOAuthAuthorizationCodeRequest_LTQ3NzI1Mjkz" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 400, + "label": "redirect-url-miss-match", + "message": "The redirect URL does not match the one registered with the client" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "redirect-url-miss-match" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "redirect-url-miss-match", + "message": "The redirect URL does not match the one registered with the client" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "redirect-url-miss-match" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Bad Request\n\nThe redirect URL does not match the one registered with the client (label: `redirect-url-miss-match`) or `body`", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "Forbidden", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Create an OAuth authorization code" + } + }, + "/oauth/clients/{OAuthClientId}": { + "get": { + "description": " [internal route ID: \"get-oauth-client\"]\n\n", + "operationId": "get-oauth-client", + "parameters": [ + { + "description": "The ID of the OAuth client", + "in": "path", + "name": "OAuthClientId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClient_NzExMTI5NTIy" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OAuthClient_NzExMTI5NTIy" + } + } + }, + "description": "OAuth client found" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "forbidden", + "message": "OAuth is disabled" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "OAuth is disabled (label: `forbidden`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`OAuthClientId` or OAuth client not found (label: `not-found`)\n\nOAuth client not found (label: `not-found`)" + } + }, + "summary": "Get OAuth client information" + } + }, + "/oauth/revoke": { + "post": { + "description": " [internal route ID: \"revoke-oauth-refresh-token\"]\n\nRevoke an access token.", + "operationId": "revoke-oauth-refresh-token", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OAuthRevokeRefreshTokenRequest_MjA1MDg4MzQ4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "forbidden", + "message": "Invalid refresh token" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid refresh token (label: `forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "OAuth client not found (label: `not-found`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "jwt-error", + "message": "Internal error while handling JWT token" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "jwt-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Internal error while handling JWT token (label: `jwt-error`)" + } + }, + "summary": "Revoke an OAuth refresh token" + } + }, + "/oauth/token": { + "post": { + "description": " [internal route ID: \"create-oauth-access-token\"]\n\nObtain a new access token from an authorization code or a refresh token.", + "operationId": "create-oauth-access-token", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Either_OAuthAccessTokenRequest_OAuthRefreshAccessTokenRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OAuthAccessTokenResponse_NzEwOTI4NjQ0" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid_grant", + "message": "Invalid grant" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid_grant", + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid grant (label: `invalid_grant`)\n\nInvalid client credentials (label: `forbidden`)\n\nInvalid grant type (label: `forbidden`)\n\nInvalid refresh token (label: `forbidden`)\n\nOAuth is disabled (label: `forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "OAuth client not found (label: `not-found`)\n\nOAuth authorization code not found (label: `not-found`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "jwt-error", + "message": "Internal error while handling JWT token" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "jwt-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Internal error while handling JWT token (label: `jwt-error`)" + } + }, + "summary": "Create an OAuth access token" + } + }, + "/one2one-conversations": { + "post": { + "description": " [internal route ID: \"create-one-to-one-conversation\"]\n\n", + "operationId": "create-one-to-one-conversation", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewOne2OneConv_LTI3OTc4NDAz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/v3_OwnConversation_GroupConvType_LTU2MzYxNTg0V3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/v3_OwnConversation_GroupConvType_LTU2MzYxNTg0V3" + } + } + }, + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/v3_OwnConversation_GroupConvType_LTU2MzYxNTg0V3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/v3_OwnConversation_GroupConvType_LTU2MzYxNTg0V3" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "operation-denied", + "not-connected", + "no-team-member", + "non-binding-team-members", + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nBoth users must be members of the same binding team (label: `non-binding-team-members`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "non-binding-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team not found (label: `no-team`)\n\nNot a member of a binding team (label: `non-binding-team`)" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Create a 1:1 conversation" + } + }, + "/one2one-conversations/{usr_domain}/{usr}": { + "get": { + "description": " [internal route ID: \"get-one-to-one-mls-conversation\"]\n\n", + "operationId": "get-one-to-one-mls-conversation", + "parameters": [ + { + "in": "path", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "format", + "required": false, + "schema": { + "enum": [ + "raw", + "jwk" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSOne2OneConversation_SomeKey_GroupConvType_LTg2MTcwMjY3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSOne2OneConversation_SomeKey_GroupConvType_LTg2MTcwMjY3" + } + } + }, + "description": "MLS 1-1 conversation" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `format`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "not-connected", + "message": "Users are not connected" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-connected" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Users are not connected (label: `not-connected`)" + } + }, + "summary": "Get an MLS 1:1 conversation" + } + }, + "/password-reset": { + "post": { + "description": " [internal route ID: \"post-password-reset\"]\n\n", + "operationId": "post-password-reset", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewPasswordReset_LTEyNzAxMTcy" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Password reset code created and sent by email." + } + }, + "summary": "Initiate a password reset." + } + }, + "/password-reset/complete": { + "post": { + "description": " [internal route ID: \"post-password-reset-complete\"]\n\n", + "operationId": "post-password-reset-complete", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CompletePasswordReset_NDcyMjY5OTc4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Password reset successful." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)" + } + }, + "summary": "Complete a password reset." + } + }, + "/properties": { + "delete": { + "description": " [internal route ID: \"clear-properties\"]\n\n", + "operationId": "clear-properties", + "responses": { + "200": { + "description": "Properties cleared" + } + }, + "summary": "Clear all properties" + }, + "get": { + "description": " [internal route ID: \"list-property-keys\"]\n\n", + "operationId": "list-property-keys", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "type": "array" + } + } + }, + "description": "List of property keys" + } + }, + "summary": "List all property keys" + } + }, + "/properties-values": { + "get": { + "description": " [internal route ID: \"list-properties\"]\n\n", + "operationId": "list-properties", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PropertyKeysAndValues" + } + } + }, + "description": "" + } + }, + "summary": "List all properties with key and value" + } + }, + "/properties/{key}": { + "delete": { + "description": " [internal route ID: \"delete-property\"]\n\n", + "operationId": "delete-property", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "printable", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Property deleted" + } + }, + "summary": "Delete a property" + }, + "get": { + "description": " [internal route ID: \"get-property\"]\n\n", + "operationId": "get-property", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "printable", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PropertyValue" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PropertyValue" + } + } + }, + "description": "The property value" + }, + "404": { + "description": "`key` or Property not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a property value" + }, + "put": { + "description": " [internal route ID: \"set-property\"]\n\n", + "operationId": "set-property", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "printable", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PropertyValue" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Property set" + } + }, + "summary": "Set a user property" + } + }, + "/provider": { + "delete": { + "description": " [internal route ID: \"provider-delete\"]\n\n", + "operationId": "provider-delete", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteProvider_MzYxMzM3Mjg2" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "invalid-provider", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Delete a provider" + }, + "get": { + "description": " [internal route ID: \"provider-get-account\"]\n\n", + "operationId": "provider-get-account", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Provider_NDIyMzQ3ODIy" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Provider_NDIyMzQ3ODIy" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Provider not found. (label: `not-found`)\n\nProvider not found. (label: `not-found`)" + } + }, + "summary": "Get account" + }, + "put": { + "description": " [internal route ID: \"provider-update\"]\n\n", + "operationId": "provider-update", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateProvider_LTQwMjY4MDgy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-provider", + "message": "The provider does not exist." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-provider", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Update a provider" + } + }, + "/provider/activate": { + "get": { + "description": " [internal route ID: \"provider-activate\"]\n\n", + "operationId": "provider-activate", + "parameters": [ + { + "in": "query", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderActivationResponse_LTgzNTU3MzA5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ProviderActivationResponse_LTgzNTU3MzA5" + } + } + }, + "description": "" + }, + "204": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-code", + "message": "Invalid verification code" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid verification code (label: `invalid-code`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Activate a provider" + } + }, + "/provider/assets": { + "post": { + "description": " [internal route ID: (\"assets-upload-v3\", provider)]\n\n

Construct the request as multipart/mixed; set header Content-Type: multipart/mixed; boundary=<boundary>.

Use exactly two parts in this order:

  1. application/json metadata (AssetSettings)
  2. application/octet-stream asset bytes

Each part must include Content-Type and Content-Length; the second part may include Content-MD5. Use CRLF between headers and bodies.

When asset audit logging is enabled, the JSON metadata must include:

  • convId: object { id: UUID, domain: String } (qualified conversation ID)
  • filename: String
  • filetype: String MIME type (e.g. image/png, application/pdf)

Optional metadata: public (Bool, default false), retention (one of eternal, persistent, volatile, eternal-infrequent_access, expiring).

For profile pictures or team icons without a conversation, set convId.id to 00000000-0000-0000-0000-000000000000 and convId.domain to the tenant’s domain; use any reasonable filename.

Note: the server treats the asset bytes as application/octet-stream; filetype is used for auditing only.

Example body (boundary=frontier):

Content-Type: multipart/mixed; boundary=frontier

--frontier
Content-Type: application/json
Content-Length: 191

{\"public\":false,\"retention\":\"volatile\",\"convId\":{\"id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"domain\":\"example.com\"},\"filename\":\"report.pdf\",\"filetype\":\"application/pdf\"}
--frontier
Content-Type: application/octet-stream
Content-Length: 11

Hello Audit
--frontier--
", + "operationId": "assets-upload-v3_provider", + "requestBody": { + "content": { + "multipart/mixed": { + "schema": { + "$ref": "#/components/schemas/AssetSource" + } + } + }, + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server." + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset_Qualified_AssetKey_MzU1MjMxNTA5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Asset_Qualified_AssetKey_MzU1MjMxNTA5" + } + } + }, + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "incomplete-body", + "message": "HTTP content-length header does not match body size" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "incomplete-body", + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nHTTP content-length header does not match body size (label: `incomplete-body`)\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Asset too large (label: `client-error`)" + } + }, + "summary": "Upload an asset" + } + }, + "/provider/assets/{key}": { + "delete": { + "description": " [internal route ID: (\"assets-delete-v3\", provider)]\n\n", + "operationId": "assets-delete-v3_provider", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Delete an asset" + }, + "get": { + "description": " [internal route ID: (\"assets-download-v3\", provider)]\n\n", + "operationId": "assets-download-v3_provider", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` or Asset not found (label: `not-found`)" + } + }, + "summary": "Download an asset" + } + }, + "/provider/email": { + "put": { + "description": " [internal route ID: \"provider-update-email\"]\n\n", + "operationId": "provider-update-email", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EmailUpdate_LTYwODE0ODQ5" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-provider", + "message": "The provider does not exist." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-provider", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Too many request to generate a verification code." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Too many request to generate a verification code. (label: `too-many-requests`)" + } + }, + "summary": "Update a provider email" + } + }, + "/provider/login": { + "post": { + "description": " [internal route ID: \"provider-login\"]\n\n", + "operationId": "provider-login", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ProviderLogin_LTE2MTk2NTM5" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "headers": { + "Set-Cookie": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Login as a provider" + } + }, + "/provider/password": { + "put": { + "description": " [internal route ID: \"provider-update-password\"]\n\n", + "operationId": "provider-update-password", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordChange_NDI0ODgwNDU0" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)" + } + }, + "summary": "Update a provider password" + } + }, + "/provider/password-reset": { + "post": { + "description": " [internal route ID: \"provider-password-reset\"]\n\n", + "operationId": "provider-password-reset", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordReset_LTYzNDYxNTQ3" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code", + "invalid-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)\n\nInvalid email or mobile number for password reset. (label: `invalid-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ", + "code-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)\n\nA password reset is already in progress. (label: `code-exists`)\n\nA password reset is already in progress. (label: `code-exists`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Too many request to generate a verification code." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Too many request to generate a verification code. (label: `too-many-requests`)" + } + }, + "summary": "Begin a password reset" + } + }, + "/provider/password-reset/complete": { + "post": { + "description": " [internal route ID: \"provider-password-reset-complete\"]\n\n", + "operationId": "provider-password-reset-complete", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CompletePasswordReset_LTYzMDAxNDA1" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "invalid-code", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nInvalid verification code (label: `invalid-code`)\n\nAccess denied. (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)" + } + }, + "summary": "Complete a password reset" + } + }, + "/provider/register": { + "post": { + "description": " [internal route ID: \"provider-register\"]\n\n", + "operationId": "provider-register", + "parameters": [ + { + "in": "header", + "name": "X-Forwarded-For", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewProvider_LTEyMTY5MjYy" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewProviderResponse_OTE0ODI2NjU0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewProviderResponse_OTE0ODI2NjU0" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body` or `X-Forwarded-For`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Too many request to generate a verification code." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Too many request to generate a verification code. (label: `too-many-requests`)" + } + }, + "summary": "Register a new provider" + } + }, + "/provider/services": { + "get": { + "description": " [internal route ID: \"get-provider-services\"]\n\n", + "operationId": "get-provider-services", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Service_MjcyOTA5NjQx" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List provider services" + }, + "post": { + "description": " [internal route ID: \"post-provider-services\"]\n\n", + "operationId": "post-provider-services", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewService_LTYwOTU1MDQ3" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewServiceResponse_LTExMzcwMjg5" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewServiceResponse_LTExMzcwMjg5" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-service-key", + "message": "Invalid service key." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-service-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid service key. (label: `invalid-service-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Create a new service" + } + }, + "/provider/services/{service-id}": { + "delete": { + "description": " [internal route ID: \"delete-provider-services-by-service-id\"]\n\n", + "operationId": "delete-provider-services-by-service-id", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteService_LTY2NzY5NzMz" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Delete service" + }, + "get": { + "description": " [internal route ID: \"get-provider-services-by-service-id\"]\n\n", + "operationId": "get-provider-services-by-service-id", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Service_MjcyOTA5NjQx" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Get provider service by service id" + }, + "put": { + "description": " [internal route ID: \"put-provider-services-by-service-id\"]\n\n", + "operationId": "put-provider-services-by-service-id", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateService_MjAxNzQ2Njkz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Provider service updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nProvider not found. (label: `not-found`)\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Update provider service" + } + }, + "/provider/services/{service-id}/connection": { + "put": { + "description": " [internal route ID: \"put-provider-services-connection-by-service-id\"]\n\n", + "operationId": "put-provider-services-connection-by-service-id", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateServiceConn_LTQ1OTYwNjIz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Provider service connection updated" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-service-key", + "message": "Invalid service key." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-service-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid service key. (label: `invalid-service-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Update provider service connection" + } + }, + "/providers/{pid}": { + "get": { + "description": " [internal route ID: \"provider-get-profile\"]\n\n", + "operationId": "provider-get-profile", + "parameters": [ + { + "in": "path", + "name": "pid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Provider_NDIyMzQ3ODIy" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Provider_NDIyMzQ3ODIy" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`pid` or Provider not found. (label: `not-found`)" + } + }, + "summary": "Get profile" + } + }, + "/providers/{provider-id}/services": { + "get": { + "description": " [internal route ID: \"get-provider-services-by-provider-id\"]\n\n", + "operationId": "get-provider-services-by-provider-id", + "parameters": [ + { + "in": "path", + "name": "provider-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/ServiceProfile_LTc2MDQzNTk3" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Get provider services by provider id" + } + }, + "/providers/{provider-id}/services/{service-id}": { + "get": { + "description": " [internal route ID: \"get-provider-services-by-provider-id-and-service-id\"]\n\n", + "operationId": "get-provider-services-by-provider-id-and-service-id", + "parameters": [ + { + "in": "path", + "name": "provider-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceProfile_LTc2MDQzNTk3" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`provider-id` or `service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Get provider service by provider id and service id" + } + }, + "/proxy/giphy/v1/gifs": {}, + "/proxy/googlemaps/api/staticmap": {}, + "/proxy/googlemaps/maps/api/geocode": {}, + "/proxy/soundcloud/resolve": {}, + "/proxy/soundcloud/stream": {}, + "/proxy/spotify/api/token": {}, + "/proxy/youtube/v3": {}, + "/push/tokens": { + "get": { + "description": " [internal route ID: \"get-push-tokens\"]\n\n", + "operationId": "get-push-tokens", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PushTokenList_NDI0Mjc3MzY3" + } + } + }, + "description": "" + } + }, + "summary": "List the user's registered push tokens" + }, + "post": { + "description": " [internal route ID: \"register-push-token\"]\n\n", + "operationId": "register-push-token", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PushToken_ODYzMDYzOTA4" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PushToken_ODYzMDYzOTA4" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PushToken_ODYzMDYzOTA4" + } + } + }, + "description": "Push token registered", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 400, + "label": "apns-voip-not-supported", + "message": "Adding APNS_VOIP tokens is not supported" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "apns-voip-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "apns-voip-not-supported", + "message": "Adding APNS_VOIP tokens is not supported" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "apns-voip-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Adding APNS_VOIP tokens is not supported (label: `apns-voip-not-supported`) or `body`" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "app-not-found", + "message": "App does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "app-not-found", + "invalid-token" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "app-not-found", + "message": "App does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "app-not-found", + "invalid-token" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "App does not exist (label: `app-not-found`)\n\nInvalid push token (label: `invalid-token`)" + }, + "413": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 413, + "label": "sns-thread-budget-reached", + "message": "Too many concurrent calls to SNS; is SNS down?" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "sns-thread-budget-reached", + "token-too-long", + "metadata-too-long" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 413, + "label": "sns-thread-budget-reached", + "message": "Too many concurrent calls to SNS; is SNS down?" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "sns-thread-budget-reached", + "token-too-long", + "metadata-too-long" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Too many concurrent calls to SNS; is SNS down? (label: `sns-thread-budget-reached`)\n\nPush token length must be < 8192 for GCM or 400 for APNS (label: `token-too-long`)\n\nTried to add token to endpoint resulting in metadata length > 2048 (label: `metadata-too-long`)" + } + }, + "summary": "Register a native push token" + } + }, + "/push/tokens/{pid}": { + "delete": { + "description": " [internal route ID: \"delete-push-token\"]\n\n", + "operationId": "delete-push-token", + "parameters": [ + { + "description": "The push token to delete", + "in": "path", + "name": "pid", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Push token unregistered" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Push token not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Push token not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`pid` or Push token not found (label: `not-found`)" + } + }, + "summary": "Unregister a native push token" + } + }, + "/register": { + "post": { + "description": " [internal route ID: \"register\"]\n\nIf the environment where the registration takes place is private and a registered email address is not whitelisted, a 403 error is returned.", + "operationId": "register", + "parameters": [ + { + "in": "header", + "name": "X-Forwarded-For", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewUser_PlainTextPassword_8_LTI4MzI5NzQx" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User_NjA4OTQwMTQ4" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/User_NjA4OTQwMTQ4" + } + } + }, + "description": "User created and pending activation", + "headers": { + "Location": { + "description": "UserId", + "schema": { + "format": "uuid", + "type": "string" + } + }, + "Set-Cookie": { + "description": "Cookie", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code", + "invalid-email", + "invalid-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code", + "invalid-email", + "invalid-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid invitation code. (label: `invalid-invitation-code`)\n\nInvalid e-mail address. (label: `invalid-email`)\n\nInvalid mobile phone number (label: `invalid-phone`) or `body` or `X-Forwarded-For`" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unauthorized", + "message": "Unauthorized e-mail address" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorized", + "missing-identity", + "blacklisted-email", + "too-many-team-members", + "user-creation-restricted", + "ephemeral-user-creation-disabled", + "managed-by-scim" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorized", + "message": "Unauthorized e-mail address" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorized", + "missing-identity", + "blacklisted-email", + "too-many-team-members", + "user-creation-restricted", + "ephemeral-user-creation-disabled", + "managed-by-scim" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorized e-mail address (label: `unauthorized`)\n\nUsing an invitation code requires registering the given email. (label: `missing-identity`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nToo many members in this team. (label: `too-many-team-members`)\n\nThis instance does not allow creation of personal users or teams. (label: `user-creation-restricted`)\n\nEphemeral user creation is disabled on this instance. (label: `ephemeral-user-creation-disabled`)\n\nUpdating name is not allowed, because it is managed by SCIM, or E2EId is enabled (label: `managed-by-scim`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "User does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "User does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "User does not exist (label: `invalid-code`)\n\nInvalid activation code (label: `invalid-code`)" + }, + "409": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Register a new user." + } + }, + "/scim/auth-tokens": { + "delete": { + "description": " [internal route ID: \"auth-tokens-delete\"]\n\n", + "operationId": "auth-tokens-delete", + "parameters": [ + { + "in": "query", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" + } + } + }, + "get": { + "description": " [internal route ID: \"auth-tokens-list\"]\n\n", + "operationId": "auth-tokens-list", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ScimTokenList_NjQwNTYxOTAw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" + } + } + }, + "post": { + "description": " [internal route ID: \"auth-tokens-create\"]\n\n", + "operationId": "auth-tokens-create", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateScimToken_OTY0NjYxMDQ2" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateScimTokenResponse_LTIzOTU2NDU4" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" + } + } + } + }, + "/scim/auth-tokens/{id}": { + "put": { + "description": " [internal route ID: \"auth-tokens-put-name\"]\n\n", + "operationId": "auth-tokens-put-name", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ScimTokenName_LTgzOTM2OTI4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" + } + } + } + }, + "/search/contacts": { + "get": { + "description": " [internal route ID: \"search-contacts\"]\n\n", + "operationId": "search-contacts", + "parameters": [ + { + "description": "Search query

The search query is normalized: lower-cased and diacritics are removed ('Björn' becomes 'bjorn'). The normalized search query matches accounts that, in this order of priority:

  • are equal to the normalized full handle;
  • are equal to the normalized full user display name;
  • prefix-match the normalized handle;
  • prefix-match the normalized user display name.

NB: '@' Does NOT do anything special, ignoring user display names.

See also: [authoritative ElasticSearch query](https://github.com/wireapp/wire-server/blob/83c25cca6a5e9d2205c102410b452eb78fc50a00/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs#L251-L288)

", + "in": "query", + "name": "q", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Searched domain. Note: This is optional only for backwards compatibility, future versions will mandate this.", + "in": "query", + "name": "domain", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Number of results to return (min: 1, max: 500, default 15)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Only user types. Omitted or empty (type=) means no filtering.", + "in": "query", + "name": "type", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchResult_Contact_OTExNzg4MTE0" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `insufficient-permissions`)" + } + }, + "summary": "Search for users" + } + }, + "/self": { + "delete": { + "description": " [internal route ID: \"delete-self\"]\n\nif the account has a verified identity, a verification code is sent and needs to be confirmed to authorise the deletion. if the account has no verified identity but a password, it must be provided. if password is correct, or if neither a verified identity nor a password exists, account deletion is scheduled immediately.", + "operationId": "delete-self", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteUser_NjE0MjE2Mjkz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Deletion is initiated." + }, + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletionCodeTimeout_LTU1MTk0NDI3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeletionCodeTimeout_LTU1MTk0NDI3" + } + } + }, + "description": "Deletion is pending verification with a code." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-user", + "message": "Invalid user" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-user" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-self-delete-for-team-owner", + "message": "Team owners are not allowed to delete themselves; ask a fellow owner" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-self-delete-for-team-owner", + "pending-delete", + "missing-auth", + "invalid-credentials", + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team owners are not allowed to delete themselves; ask a fellow owner (label: `no-self-delete-for-team-owner`)\n\nA verification code for account deletion is still pending (label: `pending-delete`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nAuthentication failed (label: `invalid-credentials`)\n\nInvalid verification code (label: `invalid-code`)" + } + }, + "summary": "Initiate account deletion." + }, + "get": { + "description": " [internal route ID: \"get-self\"]\n\n\nOAuth scope: `read:self`", + "operationId": "get-self", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/User_NjA4OTQwMTQ4" + } + } + }, + "description": "" + } + }, + "summary": "Get your own profile" + }, + "put": { + "description": " [internal route ID: \"put-self\"]\n\n", + "operationId": "put-self", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserUpdate_MjQ4NTEwOTQz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User updated" + } + }, + "summary": "Update your profile." + } + }, + "/self/email": { + "delete": { + "description": " [internal route ID: \"remove-email\"]\n\nYour email address can only be removed if you also have a phone number.", + "operationId": "remove-email", + "responses": { + "200": { + "description": "Identity Removed" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "last-identity", + "message": "The last user identity cannot be removed." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "last-identity", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "last-identity", + "message": "The last user identity cannot be removed." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "last-identity", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The last user identity cannot be removed. (label: `last-identity`)\n\nThe user has no verified email (label: `no-identity`)" + } + }, + "summary": "Remove your email address." + } + }, + "/self/handle": { + "put": { + "description": " [internal route ID: \"change-handle\"]\n\n", + "operationId": "change-handle", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/HandleUpdate_NTI4NDk1OTAx" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Handle Changed" + } + }, + "summary": "Change your handle." + } + }, + "/self/locale": { + "put": { + "description": " [internal route ID: \"change-locale\"]\n\n", + "operationId": "change-locale", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LocaleUpdate_LTgzNjgyOTEw" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Local Changed" + } + }, + "summary": "Change your locale." + } + }, + "/self/password": { + "head": { + "description": " [internal route ID: \"check-password-exists\"]\n\n", + "operationId": "check-password-exists", + "responses": { + "200": { + "description": "Password is set" + }, + "404": { + "description": "Password is not set" + } + }, + "summary": "Check that your password is set." + }, + "put": { + "description": " [internal route ID: \"change-password\"]\n\n", + "operationId": "change-password", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordChange_MTgzMDM2NTY2" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Password Changed" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe user has no verified email (label: `no-identity`)" + }, + "409": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password change, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password change, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "For password change, new and old password must be different. (label: `password-must-differ`)" + } + }, + "summary": "Change your password." + } + }, + "/self/supported-protocols": { + "put": { + "description": " [internal route ID: \"change-supported-protocols\"]\n\n", + "operationId": "change-supported-protocols", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SupportedProtocolUpdate_LTE3Njk3MDM4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Supported protocols changed" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-protocol-error", + "message": "MLS protocol cannot be removed" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-protocol-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS protocol cannot be removed (label: `mls-protocol-error`)" + } + }, + "summary": "Change your supported protocols" + } + }, + "/services": { + "get": { + "description": " [internal route ID: \"get-services\"]\n\n", + "operationId": "get-services", + "parameters": [ + { + "in": "query", + "name": "tags", + "required": false, + "schema": { + "enum": [ + "audio", + "books", + "business", + "design", + "education", + "entertainment", + "finance", + "fitness", + "food-drink", + "games", + "graphics", + "health", + "integration", + "lifestyle", + "media", + "medical", + "movies", + "music", + "news", + "photography", + "poll", + "productivity", + "quiz", + "rating", + "shopping", + "social", + "sports", + "travel", + "tutorial", + "video", + "weather" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "start", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 100, + "minimum": 10, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceProfilePage_Njg1NDQ5Njc4" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List services" + } + }, + "/services/tags": { + "get": { + "description": " [internal route ID: \"get-services-tags\"]\n\n", + "operationId": "get-services-tags", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceTagList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Get services tags" + } + }, + "/sso/finalize-login": { + "post": { + "deprecated": true, + "description": " [internal route ID: \"auth-resp-legacy\"]\n\nDEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "operationId": "auth-resp-legacy", + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/finalize-login/{team}": { + "post": { + "description": " [internal route ID: \"auth-resp\"]\n\n", + "operationId": "auth-resp", + "parameters": [ + { + "in": "path", + "name": "team", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/get-by-email": { + "post": { + "description": " [internal route ID: \"sso-get-by-email\"]\n\n", + "operationId": "sso-get-by-email", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetByEmailReq_LTY4MzE3Njgy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetByEmailResp_LTMxNTY3MjA0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetByEmailResp_LTMxNTY3MjA0" + } + } + }, + "description": "SSO code found" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetByEmailResp_LTMxNTY3MjA0" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetByEmailResp_LTMxNTY3MjA0" + } + } + }, + "description": "SSO code not found or feature disabled" + } + } + } + }, + "/sso/initiate-login/{idp}": { + "get": { + "description": " [internal route ID: \"auth-req\"]\n\n", + "operationId": "auth-req", + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "label", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "idp", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/html": { + "schema": { + "$ref": "#/components/schemas/FormRedirect" + } + } + }, + "description": "" + } + } + }, + "head": { + "description": " [internal route ID: \"auth-req-precheck\"]\n\n", + "operationId": "auth-req-precheck", + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "label", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "idp", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": {} + }, + "description": "" + } + } + } + }, + "/sso/metadata": { + "get": { + "deprecated": true, + "description": " [internal route ID: \"sso-metadata\"]\n\nDEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "operationId": "sso-metadata", + "responses": { + "200": { + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/metadata/{team}": { + "get": { + "description": " [internal route ID: \"sso-team-metadata\"]\n\n", + "operationId": "sso-team-metadata", + "parameters": [ + { + "in": "path", + "name": "team", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/settings": { + "get": { + "description": " [internal route ID: \"sso-settings\"]\n\n", + "operationId": "sso-settings", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SsoSettings" + } + } + }, + "description": "" + } + } + } + }, + "/system/settings": { + "get": { + "description": " [internal route ID: \"get-system-settings\"]\n\n", + "operationId": "get-system-settings", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SystemSettings_ODU3MDk5MTA3" + } + } + }, + "description": "" + } + }, + "summary": "Returns a curated set of system configuration settings for authorized users." + } + }, + "/system/settings/unauthorized": { + "get": { + "description": " [internal route ID: \"get-system-settings-unauthorized\"]\n\n", + "operationId": "get-system-settings-unauthorized", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SystemSettingsPublic_LTgwNTMxNjU2" + } + } + }, + "description": "" + } + }, + "summary": "Returns a curated set of system configuration settings." + } + }, + "/teams/invitations/accept": { + "post": { + "description": " [internal route ID: \"accept-team-invitation\"]\n\n", + "operationId": "accept-team-invitation", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AcceptTeamInvitation_Nzg5NzI3MjA2" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Team invitation accepted." + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-auth", + "message": "Re-authentication via password required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-auth", + "invalid-credentials", + "missing-identity", + "too-many-team-members" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Re-authentication via password required (label: `missing-auth`)\n\nAuthentication failed (label: `invalid-credentials`)\n\nUsing an invitation code requires registering the given email. (label: `missing-identity`)\n\nToo many members in this team. (label: `too-many-team-members`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code", + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)\n\nNo pending invitations exists. (label: `not-found`)" + } + }, + "summary": "Accept a team invitation, changing a personal account into a team member account." + } + }, + "/teams/invitations/by-email": { + "head": { + "description": " [internal route ID: \"head-team-invitations\"]\n\n", + "operationId": "head-team-invitations", + "parameters": [ + { + "description": "Email address", + "in": "query", + "name": "email", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Pending invitation exists." + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "No pending invitations exists." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "No pending invitations exists." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "No pending invitations exists. (label: `not-found`)" + }, + "409": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 409, + "label": "conflicting-invitations", + "message": "Multiple conflicting invitations to different teams exists." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "conflicting-invitations" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "conflicting-invitations", + "message": "Multiple conflicting invitations to different teams exists." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "conflicting-invitations" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Multiple conflicting invitations to different teams exists. (label: `conflicting-invitations`)" + } + }, + "summary": "Check if there is an invitation pending given an email address." + } + }, + "/teams/invitations/info": { + "get": { + "description": " [internal route ID: \"get-team-invitation-info\"]\n\n", + "operationId": "get-team-invitation-info", + "parameters": [ + { + "description": "Invitation code", + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationUserView_LTUyMTE3Nzkz" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InvitationUserView_LTUyMTE3Nzkz" + } + } + }, + "description": "Invitation info" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `code`\n\nInvalid invitation code. (label: `invalid-invitation-code`)" + } + }, + "summary": "Get invitation info given a code." + } + }, + "/teams/notifications": { + "get": { + "description": " [internal route ID: \"get-team-notifications\"]\n\n

This is a work-around for scalability issues with gundeck user event fan-out. It does not track all team-wide events, but only `member-join`.

Note that `/teams/notifications` behaves differently from `/notifications`:

  • If there is a gap between the notification id requested with `since` and the available data, team queues respond with 200 and the data that could be found. They do NOT respond with status 404, but valid data in the body.\n
  • The notification with the id given via `since` is included in the response if it exists. You should remove this and only use it to decide whether there was a gap between your last request and this one.\n
  • If the notification id does *not* exist, you get the more recent events from the queue (instead of all of them). This can be done because a notification id is a UUIDv1, which is essentially a time stamp.\n
  • There is no corresponding `/last` end-point to get only the most recent event. That end-point was only useful to avoid having to pull the entire queue. In team queues, if you have never requested the queue before and have no prior notification id, just pull with timestamp 'now'.

See also: GET /notifications

", + "operationId": "get-team-notifications", + "parameters": [ + { + "description": "Notification id to start with in the response (UUIDv1)", + "in": "query", + "name": "since", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Maximum number of events to return (1..10000; default: 1000)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 10000, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotificationList_MTU0ODEyNTQ2" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-notification-id", + "message": "Could not parse notification id (must be UUIDv1)." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-notification-id" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `size` or `since`\n\nCould not parse notification id (must be UUIDv1). (label: `invalid-notification-id`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team not found (label: `no-team`)" + } + }, + "summary": "Read recently added team members from team queue" + } + }, + "/teams/{team-id}/services/whitelist": { + "post": { + "description": " [internal route ID: \"post-team-whitelist-by-team-id\"]\n\n", + "operationId": "post-team-whitelist-by-team-id", + "parameters": [ + { + "in": "path", + "name": "team-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateServiceWhitelist_LTU5MDAwMTIw" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "UpdateServiceWhitelistRespChanged" + }, + "204": { + "description": "UpdateServiceWhitelistRespUnchanged" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-services-not-allowed", + "message": "Services not allowed in MLS" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-services-not-allowed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Services not allowed in MLS (label: `mls-services-not-allowed`)" + } + }, + "summary": "Update service whitelist" + } + }, + "/teams/{team-id}/services/whitelisted": { + "get": { + "description": " [internal route ID: \"get-whitelisted-services-by-team-id\"]\n\n", + "operationId": "get-whitelisted-services-by-team-id", + "parameters": [ + { + "in": "path", + "name": "team-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "prefix", + "required": false, + "schema": { + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + { + "in": "query", + "name": "filter_disabled", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 100, + "minimum": 10, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceProfilePage_Njg1NDQ5Njc4" + } + } + }, + "description": "" + } + }, + "summary": "Get whitelisted services by team id" + } + }, + "/teams/{teamId}/registered-domains": { + "get": { + "description": " [internal route ID: \"get-all-registered-domains\"]\n\n", + "operationId": "get-all-registered-domains", + "parameters": [ + { + "in": "path", + "name": "teamId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RegisteredDomains_V10_NDYwNzYyMTMy" + } + } + }, + "description": "" + } + }, + "summary": "Get all registered domains" + } + }, + "/teams/{teamId}/registered-domains/{domain}": { + "delete": { + "description": " [internal route ID: \"delete-registered-domain\"]\n\n", + "operationId": "delete-registered-domain", + "parameters": [ + { + "in": "path", + "name": "teamId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "402": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" + }, + "properties": { + "code": { + "enum": [ + 402 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-payment-required" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-forbidden-for-domain-registration-state" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Delete a registered domain" + } + }, + "/teams/{tid}": { + "delete": { + "description": " [internal route ID: \"delete-team\"]\n\n", + "operationId": "delete-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamDeleteData_ODI5NTU0ODE5" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Team is scheduled for removal" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Verification code required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "access-denied", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Verification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (missing DeleteTeam) (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Please try again later. (label: `too-many-requests`)" + }, + "503": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 503, + "label": "queue-full", + "message": "The delete queue is full; no further delete requests can be processed at the moment" + }, + "properties": { + "code": { + "enum": [ + 503 + ], + "type": "integer" + }, + "label": { + "enum": [ + "queue-full" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The delete queue is full; no further delete requests can be processed at the moment (label: `queue-full`)" + } + }, + "summary": "Delete a team" + }, + "get": { + "description": " [internal route ID: \"get-team\"]\n\n", + "operationId": "get-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Team_NDg4MjQwOTIw" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get a team by ID" + }, + "put": { + "description": " [internal route ID: \"update-team\"]\n\n", + "operationId": "update-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamUpdateData_LTE0NTM2NTU5" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Team updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions (missing SetTeamData)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (missing SetTeamData) (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Update team properties" + } + }, + "/teams/{tid}/apps": { + "get": { + "description": " [internal route ID: \"get-apps\"]\n\n", + "operationId": "get-apps", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/UserProfile_LTQzMTQxMTE1" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "Get all apps owned by the given team (not including collaborators)" + }, + "post": { + "description": " [internal route ID: \"create-app\"]\n\n", + "operationId": "create-app", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewApp_LTQwODMwMzQ4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreatedApp_LTM3NjUxOTY1" + } + } + }, + "description": "" + } + }, + "summary": "Create a new app" + } + }, + "/teams/{tid}/apps/{app}": { + "put": { + "description": " [internal route ID: \"put-app\"]\n\n", + "operationId": "put-app", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "app", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PutApp_LTE4MDc1OTM4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "Update metadata of an existing app" + } + }, + "/teams/{tid}/apps/{app}/cookies": { + "post": { + "description": " [internal route ID: \"refresh-app-cookie\"]\n\n", + "operationId": "refresh-app-cookie", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "app", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RefreshAppCookieRequest_MjEyMDMyMTk5" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RefreshAppCookieResponse_LTQ0MjU1NTIw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-auth", + "message": "Re-authentication via password required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-auth" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Re-authentication via password required (label: `missing-auth`)" + } + }, + "summary": "Get a new app authentication token" + } + }, + "/teams/{tid}/channels/search": { + "get": { + "description": " [internal route ID: \"search-channels\"]\n\n", + "operationId": "search-channels", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Search string", + "in": "query", + "name": "q", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "sort_order", + "required": false, + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "page_size", + "required": false, + "schema": { + "description": "integer from [1..500]", + "type": "number" + } + }, + { + "description": "`name` of the last seen channel of the current page, used to get the next page.", + "in": "query", + "name": "last_seen_name", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "`id` of the last seen channel, used to get the next page, used as a tie breaker. **Must** be sent to get the next page.", + "in": "query", + "name": "last_seen_id", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "discoverable", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationPage_LTIwMDU2NDI3" + } + } + }, + "description": "" + } + }, + "summary": "Search channels" + } + }, + "/teams/{tid}/collaborators": { + "get": { + "description": " [internal route ID: \"get-team-collaborators\"]\n\n", + "operationId": "get-team-collaborators", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/TeamCollaborator_LTI3MzM1MTYz" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/TeamCollaborator_LTI3MzM1MTYz" + }, + "type": "array" + } + } + }, + "description": "Return collaborators" + } + }, + "summary": "Get all collaborators of the team." + }, + "post": { + "description": " [internal route ID: \"add-team-collaborator\"]\n\n", + "operationId": "add-team-collaborator", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewTeamCollaborator_LTIxNjEzMTYw" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + } + }, + "summary": "Add a collaborator to the team." + } + }, + "/teams/{tid}/collaborators/{uid}": { + "delete": { + "description": " [internal route ID: \"remove-team-collaborator\"]\n\n", + "operationId": "remove-team-collaborator", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + } + }, + "summary": "Remove a collaborator from the team." + }, + "put": { + "description": " [internal route ID: \"update-team-collaborator\"]\n\n", + "operationId": "update-team-collaborator", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/CollaboratorPermission_NDg5NTg2ODgy" + }, + "type": "array", + "uniqueItems": true + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + } + }, + "summary": "Update a collaborator permissions from the team." + } + }, + "/teams/{tid}/conversations": { + "get": { + "description": " [internal route ID: \"get-team-conversations\"]\n\n", + "operationId": "get-team-conversations", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamConversationList_OTI3MzY3NzY0" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + } + }, + "summary": "Get team conversations" + } + }, + "/teams/{tid}/conversations/roles": { + "get": { + "description": " [internal route ID: \"get-team-conversation-roles\"]\n\n", + "operationId": "get-team-conversation-roles", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRolesList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get existing roles available for the given team" + } + }, + "/teams/{tid}/conversations/{cid}": { + "delete": { + "description": " [internal route ID: \"delete-team-conversation\"]\n\n", + "operationId": "delete-team-conversation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "cid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Conversation deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing delete_conversation) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `cid` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Remove a team conversation" + }, + "get": { + "description": " [internal route ID: \"get-team-conversation\"]\n\n", + "operationId": "get-team-conversation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "cid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamConversation_LTIwNzgyNTEz" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `cid` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get one team conversation" + } + }, + "/teams/{tid}/features": { + "get": { + "description": " [internal route ID: \"get-all-feature-configs-for-team\"]\n\nGets feature configs for a team. User must be a member of the team and have permission to view team features.", + "operationId": "get-all-feature-configs-for-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NP_LockableFtur_ghdCnfiSOVsyAvIRqExmDpB_MGwUT2Q_LTM1Mzc0ODgy" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Gets feature configs for a team" + } + }, + "/teams/{tid}/features/allowedGlobalOperations": { + "get": { + "description": " [internal route ID: (\"get\", AllowedGlobalOperationsConfig)]\n\n", + "operationId": "get_AllowedGlobalOperationsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_AllowedGlobalOperationsConfig_NjQ1MjA5MDYw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for allowedGlobalOperations" + } + }, + "/teams/{tid}/features/appLock": { + "get": { + "description": " [internal route ID: (\"get\", AppLockConfigB)]\n\n", + "operationId": "get_AppLockConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_AppLockConfigB_Bare_Identity_ODgzNDI0OTU5" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for appLock" + }, + "put": { + "description": " [internal route ID: (\"put\", AppLockConfigB)]\n\n", + "operationId": "put_AppLockConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_AppLockConfigB_Bare_Identity_MTAzMjI5NDYy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_AppLockConfigB_Bare_Identity_ODgzNDI0OTU5" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for appLock" + } + }, + "/teams/{tid}/features/apps": { + "get": { + "description": " [internal route ID: (\"get\", AppsConfig)]\n\n", + "operationId": "get_AppsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_AppsConfig_MzQyNTMxNTk5" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for apps" + } + }, + "/teams/{tid}/features/assetAuditLog": { + "get": { + "description": " [internal route ID: (\"get\", AssetAuditLogConfig)]\n\n", + "operationId": "get_AssetAuditLogConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_AssetAuditLogConfig_NDQ3MzcyMzk2" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for assetAuditLog" + } + }, + "/teams/{tid}/features/cells": { + "get": { + "description": " [internal route ID: (\"get\", CellsConfigB)]\n\n", + "operationId": "get_CellsConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_CellsConfigB_Bare_Identity_LTgzMDA1NjI3" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for cells" + }, + "put": { + "description": " [internal route ID: (\"put\", CellsConfigB)]\n\n", + "operationId": "put_CellsConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_CellsConfigB_Bare_Identity_NzU2NjgxNzEw" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_CellsConfigB_Bare_Identity_LTgzMDA1NjI3" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for cells" + } + }, + "/teams/{tid}/features/cellsInternal": { + "get": { + "description": " [internal route ID: (\"get\", CellsInternalConfigB)]\n\n", + "operationId": "get_CellsInternalConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_CsInfigBdy_LTQ3MTU3Mjg0" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for cellsInternal" + } + }, + "/teams/{tid}/features/channels": { + "get": { + "description": " [internal route ID: (\"get\", ChannelsConfigB)]\n\n", + "operationId": "get_ChannelsConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_ChannelsConfigB_Bare_Identity_NzA2NDEyMDEw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for channels" + }, + "put": { + "description": " [internal route ID: (\"put\", ChannelsConfigB)]\n\n", + "operationId": "put_ChannelsConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_ChannelsConfigB_Bare_Identity_LTU4NTE4MTgx" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_ChannelsConfigB_Bare_Identity_NzA2NDEyMDEw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for channels" + } + }, + "/teams/{tid}/features/chatBubbles": { + "get": { + "description": " [internal route ID: (\"get\", ChatBubblesConfig)]\n\n", + "operationId": "get_ChatBubblesConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_ChatBubblesConfig_NDgwMTQzNjI2" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for chatBubbles" + } + }, + "/teams/{tid}/features/classifiedDomains": { + "get": { + "description": " [internal route ID: (\"get\", ClassifiedDomainsConfig)]\n\n", + "operationId": "get_ClassifiedDomainsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_ClassifiedDomainsConfig_LTY1ODQwODg1" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for classifiedDomains" + } + }, + "/teams/{tid}/features/conferenceCalling": { + "get": { + "description": " [internal route ID: (\"get\", ConferenceCallingConfigB)]\n\n", + "operationId": "get_ConferenceCallingConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_CnfigBIdy_NzY1NDU5MDAy" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for conferenceCalling" + }, + "put": { + "description": " [internal route ID: (\"put\", ConferenceCallingConfigB)]\n\n", + "operationId": "put_ConferenceCallingConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_ConferenceCallingConfigB_Bare_Identity_Njc2NTcxNTI3" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_CnfigBIdy_NzY1NDU5MDAy" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for conferenceCalling" + } + }, + "/teams/{tid}/features/consumableNotifications": { + "get": { + "description": " [internal route ID: (\"get\", ConsumableNotificationsConfig)]\n\n", + "operationId": "get_ConsumableNotificationsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_ConsumableNotificationsConfig_MjUxMjczMjM0" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for consumableNotifications" + } + }, + "/teams/{tid}/features/conversationGuestLinks": { + "get": { + "description": " [internal route ID: (\"get\", GuestLinksConfig)]\n\n", + "operationId": "get_GuestLinksConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_GuestLinksConfig_LTcwNjU0NDMw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for conversationGuestLinks" + }, + "put": { + "description": " [internal route ID: (\"put\", GuestLinksConfig)]\n\n", + "operationId": "put_GuestLinksConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_GuestLinksConfig_NjQyMDMxNjg3" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_GuestLinksConfig_LTcwNjU0NDMw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for conversationGuestLinks" + } + }, + "/teams/{tid}/features/digitalSignatures": { + "get": { + "description": " [internal route ID: (\"get\", DigitalSignaturesConfig)]\n\n", + "operationId": "get_DigitalSignaturesConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_DigitalSignaturesConfig_ODM2MDA2NDU4" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for digitalSignatures" + } + }, + "/teams/{tid}/features/domainRegistration": { + "get": { + "description": " [internal route ID: (\"get\", DomainRegistrationConfig)]\n\n", + "operationId": "get_DomainRegistrationConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_DomainRegistrationConfig_NzU0NjczNTE0" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for domainRegistration" + } + }, + "/teams/{tid}/features/enforceFileDownloadLocation": { + "get": { + "description": " [internal route ID: (\"get\", EnforceFileDownloadLocationConfigB)]\n\n

Custom feature: only supported on some dedicated on-prem systems.

", + "operationId": "get_EnforceFileDownloadLocationConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_EnfiDwdCgBIy_ODA5OTA5MTQ4" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for enforceFileDownloadLocation" + }, + "put": { + "description": " [internal route ID: (\"put\", EnforceFileDownloadLocationConfigB)]\n\n

Custom feature: only supported on some dedicated on-prem systems.

", + "operationId": "put_EnforceFileDownloadLocationConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Featur_EnfocilDwdLCgBIy_MjcxMzc1NDA5" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_EnfiDwdCgBIy_ODA5OTA5MTQ4" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for enforceFileDownloadLocation" + } + }, + "/teams/{tid}/features/exposeInvitationURLsToTeamAdmin": { + "get": { + "description": " [internal route ID: (\"get\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "operationId": "get_ExposeInvitationURLsToTeamAdminConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_ExpsInviURTmAdCfg_LTQzMzU2OTY1" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for exposeInvitationURLsToTeamAdmin" + }, + "put": { + "description": " [internal route ID: (\"put\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "operationId": "put_ExposeInvitationURLsToTeamAdminConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_ExposeInvitationURLsToTeamAdminConfig_LTY5NzY2Mzg5" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_ExpsInviURTmAdCfg_LTQzMzU2OTY1" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for exposeInvitationURLsToTeamAdmin" + } + }, + "/teams/{tid}/features/fileSharing": { + "get": { + "description": " [internal route ID: (\"get\", FileSharingConfig)]\n\n", + "operationId": "get_FileSharingConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_FileSharingConfig_MjgwNjIzODEz" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for fileSharing" + }, + "put": { + "description": " [internal route ID: (\"put\", FileSharingConfig)]\n\n", + "operationId": "put_FileSharingConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_FileSharingConfig_LTUyNjkxMzM4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_FileSharingConfig_MjgwNjIzODEz" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for fileSharing" + } + }, + "/teams/{tid}/features/legalhold": { + "get": { + "description": " [internal route ID: (\"get\", LegalholdConfig)]\n\n", + "operationId": "get_LegalholdConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_LegalholdConfig_LTc5MTk5OTIw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for legalhold" + }, + "put": { + "description": " [internal route ID: (\"put\", LegalholdConfig)]\n\n", + "operationId": "put_LegalholdConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_LegalholdConfig_NjM3MTkxNjYw" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_LegalholdConfig_LTc5MTk5OTIw" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "legalhold-disable-unimplemented", + "message": "legal hold cannot be disabled for whitelisted teams" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-disable-unimplemented", + "legalhold-not-enabled", + "too-large-team-for-legalhold", + "action-denied", + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nCannot enable legalhold on large teams (reason: for removing LH from team, we need to iterate over all members, which is only supported for teams with less than 2k members) (label: `too-large-team-for-legalhold`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + } + }, + "summary": "Put config for legalhold" + } + }, + "/teams/{tid}/features/limitedEventFanout": { + "get": { + "description": " [internal route ID: (\"get\", LimitedEventFanoutConfig)]\n\n", + "operationId": "get_LimitedEventFanoutConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_LimitedEventFanoutConfig_MTg3ODM0NzU0" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for limitedEventFanout" + } + }, + "/teams/{tid}/features/meetings": { + "get": { + "description": " [internal route ID: (\"get\", MeetingsConfig)]\n\n", + "operationId": "get_MeetingsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_MeetingsConfig_LTE0OTQ5Nzcw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for meetings" + }, + "put": { + "description": " [internal route ID: (\"put\", MeetingsConfig)]\n\n", + "operationId": "put_MeetingsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_MeetingsConfig_NDc2MzM0MDE1" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_MeetingsConfig_LTE0OTQ5Nzcw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for meetings" + } + }, + "/teams/{tid}/features/mls": { + "get": { + "description": " [internal route ID: (\"get\", MLSConfigB)]\n\n", + "operationId": "get_MLSConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_MLSConfigB_Bare_Identity_MTg1MTc0NTEw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for mls" + }, + "put": { + "description": " [internal route ID: (\"put\", MLSConfigB)]\n\n", + "operationId": "put_MLSConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_MLSConfigB_Bare_Identity_LTI5MjA3MDYy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_MLSConfigB_Bare_Identity_MTg1MTc0NTEw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for mls" + } + }, + "/teams/{tid}/features/mlsE2EId": { + "get": { + "description": " [internal route ID: (\"get\", MlsE2EIdConfigB)]\n\n", + "operationId": "get_MlsE2EIdConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_MlsE2EIdConfigB_Bare_Identity_MTU2ODkyMDc4" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for mlsE2EId" + }, + "put": { + "description": " [internal route ID: (\"put\", MlsE2EIdConfigB)]\n\n", + "operationId": "put_MlsE2EIdConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_MlsE2EIdConfigB_Bare_Identity_LTUxODYzODEx" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_MlsE2EIdConfigB_Bare_Identity_MTU2ODkyMDc4" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for mlsE2EId" + } + }, + "/teams/{tid}/features/mlsMigration": { + "get": { + "description": " [internal route ID: (\"get\", MlsMigrationConfigB)]\n\n", + "operationId": "get_MlsMigrationConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_MsignCfBIdy_LTE1NjAxNjU2" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for mlsMigration" + }, + "put": { + "description": " [internal route ID: (\"put\", MlsMigrationConfigB)]\n\n", + "operationId": "put_MlsMigrationConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_MlsMigrationConfigB_Bare_Identity_LTQyMDAxMTkz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_MsignCfBIdy_LTE1NjAxNjU2" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for mlsMigration" + } + }, + "/teams/{tid}/features/outlookCalIntegration": { + "get": { + "description": " [internal route ID: (\"get\", OutlookCalIntegrationConfig)]\n\n", + "operationId": "get_OutlookCalIntegrationConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_OutlookCalIntegrationConfig_NjQ0MzMyMzY0" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for outlookCalIntegration" + }, + "put": { + "description": " [internal route ID: (\"put\", OutlookCalIntegrationConfig)]\n\n", + "operationId": "put_OutlookCalIntegrationConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_OutlookCalIntegrationConfig_LTg0MjIxNTMx" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_OutlookCalIntegrationConfig_NjQ0MzMyMzY0" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for outlookCalIntegration" + } + }, + "/teams/{tid}/features/preventAdminlessGroups": { + "get": { + "description": " [internal route ID: (\"get\", PreventAdminlessGroupsConfigB)]\n\n", + "operationId": "get_PreventAdminlessGroupsConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_PvnAdmisGpCfgBIy_MTQxOTU4Mzgw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for preventAdminlessGroups" + }, + "put": { + "description": " [internal route ID: \"put-PreventAdminlessGroupsConfig@v18\"]\n\n

For API version 18, use duration strings for the timeout fields. The request body must have the following shape:

{\n  "config": {\n    "deletionTimeoutDuration": "7d",\n    "promotionStrategy": "alphabetical",\n    "reminderTimeoutDurations": [\n      "2d",\n      "4d",\n      "6d"\n    ]\n  },\n  "status": "enabled"\n}

Older API versions use the legacy numeric fields deletionTimeout and reminderTimeouts.

", + "operationId": "put-PreventAdminlessGroupsConfig@v18", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Featur_Vsiond_18PvAmlGpCfgBIy_MjUwNjk2MjQwV18" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_PvnAdmisGpCfgBIy_MTQxOTU4Mzgw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for preventAdminlessGroups" + } + }, + "/teams/{tid}/features/searchVisibility": { + "get": { + "description": " [internal route ID: (\"get\", SearchVisibilityAvailableConfig)]\n\n", + "operationId": "get_SearchVisibilityAvailableConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_SearchVisibilityAvailableConfig_LTkxMTA5ODk5" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for searchVisibility" + }, + "put": { + "description": " [internal route ID: (\"put\", SearchVisibilityAvailableConfig)]\n\n", + "operationId": "put_SearchVisibilityAvailableConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_SearchVisibilityAvailableConfig_LTMzNTkxODI1" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_SearchVisibilityAvailableConfig_LTkxMTA5ODk5" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for searchVisibility" + } + }, + "/teams/{tid}/features/searchVisibilityInbound": { + "get": { + "description": " [internal route ID: (\"get\", SearchVisibilityInboundConfig)]\n\n", + "operationId": "get_SearchVisibilityInboundConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_SearchVisibilityInboundConfig_NzA5NzczNTgy" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for searchVisibilityInbound" + }, + "put": { + "description": " [internal route ID: (\"put\", SearchVisibilityInboundConfig)]\n\n", + "operationId": "put_SearchVisibilityInboundConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_SearchVisibilityInboundConfig_MTI1NzQxODY2" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_SearchVisibilityInboundConfig_NzA5NzczNTgy" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for searchVisibilityInbound" + } + }, + "/teams/{tid}/features/selfDeletingMessages": { + "get": { + "description": " [internal route ID: (\"get\", SelfDeletingMessagesConfigB)]\n\n", + "operationId": "get_SelfDeletingMessagesConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_SfDingMsCBIdy_LTg5MTEwNTA2" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for selfDeletingMessages" + }, + "put": { + "description": " [internal route ID: (\"put\", SelfDeletingMessagesConfigB)]\n\n", + "operationId": "put_SelfDeletingMessagesConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Featur_SlfDingMsCoBIdy_LTg2MzYzNjc2" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_SfDingMsCBIdy_LTg5MTEwNTA2" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for selfDeletingMessages" + } + }, + "/teams/{tid}/features/simplifiedUserConnectionRequestQRCode": { + "get": { + "description": " [internal route ID: (\"get\", SimplifiedUserConnectionRequestQRCodeConfig)]\n\n", + "operationId": "get_SimplifiedUserConnectionRequestQRCodeConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_SimpfdUsCnRqQg_Njk5NjU4OTgy" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for simplifiedUserConnectionRequestQRCode" + } + }, + "/teams/{tid}/features/sndFactorPasswordChallenge": { + "get": { + "description": " [internal route ID: (\"get\", SndFactorPasswordChallengeConfig)]\n\n", + "operationId": "get_SndFactorPasswordChallengeConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_SndFactorPasswordChallengeConfig_MjQ3NzQ2ODgx" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for sndFactorPasswordChallenge" + }, + "put": { + "description": " [internal route ID: (\"put\", SndFactorPasswordChallengeConfig)]\n\n", + "operationId": "put_SndFactorPasswordChallengeConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Feature_SndFactorPasswordChallengeConfig_NDc0MzUyMzQz" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_SndFactorPasswordChallengeConfig_MjQ3NzQ2ODgx" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for sndFactorPasswordChallenge" + } + }, + "/teams/{tid}/features/sso": { + "get": { + "description": " [internal route ID: (\"get\", SSOConfig)]\n\n", + "operationId": "get_SSOConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_SSOConfig_NjcyMjU4MDY2" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for sso" + } + }, + "/teams/{tid}/features/stealthUsers": { + "get": { + "description": " [internal route ID: (\"get\", StealthUsersConfig)]\n\n", + "operationId": "get_StealthUsersConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFeature_StealthUsersConfig_LTE1MTk2NzIz" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for stealthUsers" + } + }, + "/teams/{tid}/features/validateSAMLemails": { + "get": { + "description": " [internal route ID: (\"get\", RequireExternalEmailVerificationConfig)]\n\n

Controls whether externally managed email addresses (from SAML or SCIM) must be verified by the user, or are auto-activated.

The external feature name is kept as validateSAMLemails for backward compatibility. That name is misleading because the feature also applies to SCIM-managed users, and it controls email ownership verification rather than generic email validation.

", + "operationId": "get_RequireExternalEmailVerificationConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LockableFtur_RqiExnmVfCg_NjUyMzgzNzY5" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for validateSAMLemails" + } + }, + "/teams/{tid}/get-members-by-ids-using-post": { + "post": { + "description": " [internal route ID: \"get-team-members-by-ids\"]\n\nThe `has_more` field in the response body is always `false`.", + "operationId": "get-team-members-by-ids", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Maximum results to be returned", + "in": "query", + "name": "maxResults", + "required": false, + "schema": { + "format": "int32", + "maximum": 2000, + "minimum": 1, + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserIdList_MzA1MTI1Njgx" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMemberList_Optional_LTM1ODE2MzM0" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "too-many-uids", + "message": "Can only process 2000 user ids per request." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-uids" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body` or `maxResults`\n\nCan only process 2000 user ids per request. (label: `too-many-uids`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get team members by user id list" + } + }, + "/teams/{tid}/invitations": { + "get": { + "description": " [internal route ID: \"get-team-invitations\"]\n\n", + "operationId": "get-team-invitations", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Invitation id to start from (ascending).", + "in": "query", + "name": "start", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Number of results to return (default 100, max 500).", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationList_ODk4NTQxODc3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InvitationList_ODk4NTQxODc3" + } + } + }, + "description": "List of sent invitations" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + } + }, + "summary": "List the sent team invitations" + }, + "post": { + "description": " [internal route ID: \"send-team-invitation\"]\n\nInvitations are sent by email. The maximum allowed number of pending team invitations is equal to the team size.", + "operationId": "send-team-invitation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InvitationRequest_LTcyMDIzNDc0" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invitation_NTkzMDYwODc1" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Invitation_NTkzMDYwODc1" + } + } + }, + "description": "Invitation was created and sent.", + "headers": { + "Location": { + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code", + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid invitation code. (label: `invalid-invitation-code`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions", + "too-many-team-invitations", + "blacklisted-email", + "no-identity", + "no-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)\n\nToo many team invitations for this team (label: `too-many-team-invitations`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nThe user has no verified email (label: `no-identity`)\n\nThis operation requires the user to have a verified email address. (label: `no-email`)" + } + }, + "summary": "Create and send a new team invitation." + } + }, + "/teams/{tid}/invitations/{iid}": { + "delete": { + "description": " [internal route ID: \"delete-team-invitation\"]\n\n", + "operationId": "delete-team-invitation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "iid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Invitation deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + } + }, + "summary": "Delete a pending team invitation by ID." + }, + "get": { + "description": " [internal route ID: \"get-team-invitation\"]\n\n", + "operationId": "get-team-invitation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "iid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invitation_NTkzMDYwODc1" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Invitation_NTkzMDYwODc1" + } + } + }, + "description": "Invitation" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Notification not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Notification not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `iid` or Notification not found. (label: `not-found`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "duplicate-entry", + "message": "Entry already exists" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "duplicate-entry" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Entry already exists (label: `duplicate-entry`)" + } + }, + "summary": "Get a pending team invitation by ID." + } + }, + "/teams/{tid}/legalhold/consent": { + "post": { + "description": " [internal route ID: \"consent-to-legal-hold\"]\n\n", + "operationId": "consent-to-legal-hold", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Grant consent successful" + }, + "204": { + "description": "Consent already granted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam member not found (label: `no-team-member`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + } + }, + "summary": "Consent to legal hold" + } + }, + "/teams/{tid}/legalhold/settings": { + "delete": { + "description": " [internal route ID: \"delete-legal-hold-settings\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientRemoved event to members with a legalhold client (via brig)\n- UserLegalHoldDisabled event to contacts of members with a legalhold client (via brig)", + "operationId": "delete-legal-hold-settings", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RemoveLegalHoldSettingsRequest_NzMzMzMyMDAz" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Legal hold service settings deleted" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "legalhold-disable-unimplemented", + "message": "legal hold cannot be disabled for whitelisted teams" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-disable-unimplemented", + "legalhold-not-enabled", + "invalid-op", + "action-denied", + "no-team-member", + "operation-denied", + "code-authentication-required", + "code-authentication-failed", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Please try again later. (label: `too-many-requests`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + } + }, + "summary": "Delete legal hold service settings" + }, + "get": { + "description": " [internal route ID: \"get-legal-hold-settings\"]\n\n", + "operationId": "get-legal-hold-settings", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ViewLegalHoldService_LTE3MzQzNDkw" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get legal hold service settings" + }, + "post": { + "description": " [internal route ID: \"create-legal-hold-settings\"]\n\n", + "operationId": "create-legal-hold-settings", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewLegalHoldService_Mzg0ODQ5NDU1" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewLegalHoldService_LTE3MzQzNDkw" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ViewLegalHoldService_LTE3MzQzNDkw" + } + } + }, + "description": "Legal hold service settings created" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-status-bad", + "message": "legal hold service: invalid response" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-status-bad", + "legalhold-invalid-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)\n\nlegal hold service pubkey is invalid (label: `legalhold-invalid-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Create legal hold service settings" + } + }, + "/teams/{tid}/legalhold/{uid}": { + "delete": { + "description": " [internal route ID: \"disable-legal-hold-for-user\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientRemoved event to the user owning the client (via brig)\n- UserLegalHoldDisabled event to contacts of the user owning the client (via brig)", + "operationId": "disable-legal-hold-for-user", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DisableLegalHoldForUserRequest_LTYyMDYxOTEy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Disable legal hold successful" + }, + "204": { + "description": "Legal hold was not enabled" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "action-denied", + "code-authentication-required", + "code-authentication-failed", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Please try again later. (label: `too-many-requests`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + } + }, + "summary": "Disable legal hold for user" + }, + "get": { + "description": " [internal route ID: \"get-legal-hold\"]\n\n", + "operationId": "get-legal-hold", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserLegalHoldStatusResponse_LTQ1MzUxMTE3" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + } + }, + "summary": "Get legal hold status" + }, + "post": { + "description": " [internal route ID: \"request-legal-hold-device\"]\n\nThis endpoint can lead to the following events being sent:\n- LegalHoldClientRequested event to contacts of the user the device is requested for, if they didn't already have a legalhold client (via brig)", + "operationId": "request-legal-hold-device", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Request device successful" + }, + "204": { + "description": "Request device already pending" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered", + "legalhold-status-bad" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service has not been registered for this team (label: `legalhold-not-registered`)\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "operation-denied", + "no-team-member", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-legal-hold-not-allowed", + "message": "A user who is under legal-hold may not participate in MLS conversations" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-legal-hold-not-allowed", + "legalhold-no-consent", + "legalhold-already-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "A user who is under legal-hold may not participate in MLS conversations (label: `mls-legal-hold-not-allowed`)\n\nuser has not given consent to using legal hold (label: `legalhold-no-consent`)\n\nlegal hold is already enabled for this user (label: `legalhold-already-enabled`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-illegal-op", + "message": "internal server error: inconsistent change of user's legalhold state" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-illegal-op", + "legalhold-internal" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "internal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)\n\nlegal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)" + } + }, + "summary": "Request legal hold device" + } + }, + "/teams/{tid}/legalhold/{uid}/approve": { + "put": { + "description": " [internal route ID: \"approve-legal-hold-device\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientAdded event to the user owning the client (via brig)\n- UserLegalHoldEnabled event to contacts of the user owning the client (via brig)\n- ClientRemoved event to the user, if removing old client due to max number (via brig)", + "operationId": "approve-legal-hold-device", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ApproveLegalHoldForUserRequest_NjEyNzYyMTIx" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Legal hold approved" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "no-team-member", + "action-denied", + "access-denied", + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "legalhold-no-device-allocated", + "message": "no legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-no-device-allocated" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `uid` not found\n\nno legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow. (label: `legalhold-no-device-allocated`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "legalhold-already-enabled", + "message": "legal hold is already enabled for this user" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-already-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold is already enabled for this user (label: `legalhold-already-enabled`)" + }, + "412": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 412, + "label": "legalhold-not-pending", + "message": "legal hold cannot be approved without being in a pending state" + }, + "properties": { + "code": { + "enum": [ + 412 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-pending" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold cannot be approved without being in a pending state (label: `legalhold-not-pending`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Please try again later. (label: `too-many-requests`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + } + }, + "summary": "Approve legal hold device" + } + }, + "/teams/{tid}/members": { + "get": { + "description": " [internal route ID: \"get-team-members\"]\n\n", + "operationId": "get-team-members", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Maximum results to be returned", + "in": "query", + "name": "maxResults", + "required": false, + "schema": { + "format": "int32", + "maximum": 2000, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Optional, when not specified, the first page will be returned.Every returned page contains a `pagingState`, this should be supplied to retrieve the next page.", + "in": "query", + "name": "pagingState", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMembersPage_NzYwNDIxODgx" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get team members" + }, + "put": { + "description": " [internal route ID: \"update-team-member\"]\n\n", + "operationId": "update-team-member", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewTeamMember_Required_LTg2NjU5OTI2" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "too-many-team-admins", + "invalid-permissions", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nMaximum number of admins per team reached (label: `too-many-team-admins`)\n\nThe specified permissions are invalid (label: `invalid-permissions`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam member not found (label: `no-team-member`)\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Update an existing team member" + } + }, + "/teams/{tid}/members/csv": { + "get": { + "description": " [internal route ID: \"get-team-members-csv\"]\n\nThe endpoint returns data in chunked transfer encoding. Internal server errors might result in a failed transfer instead of a 500 response.", + "operationId": "get-team-members-csv", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/csv": {} + }, + "description": "CSV of team members" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "You do not have permission to access this resource" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "You do not have permission to access this resource (label: `access-denied`)" + } + }, + "summary": "Get all members of the team as a CSV file" + } + }, + "/teams/{tid}/members/{uid}": { + "delete": { + "description": " [internal route ID: \"delete-team-member\"]\n\n", + "operationId": "delete-team-member", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMemberDeleteData_LTg2OTEyOTI4" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "202": { + "description": "Team member scheduled for deletion" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "access-denied", + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam not found (label: `no-team`)\n\nTeam member not found (label: `no-team-member`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Please try again later. (label: `too-many-requests`)" + } + }, + "summary": "Remove an existing team member" + }, + "get": { + "description": " [internal route ID: \"get-team-member\"]\n\n", + "operationId": "get-team-member", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMember_Optional_NTU0MDcyNzI1" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + } + }, + "summary": "Get single team member" + } + }, + "/teams/{tid}/search": { + "get": { + "description": " [internal route ID: \"browse-team\"]\n\n", + "operationId": "browse-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Search expression", + "in": "query", + "name": "q", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Role filter, eg. `member,partner`. Empty list means do not filter.", + "in": "query", + "name": "frole", + "required": false, + "schema": { + "items": { + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "type": "array" + } + }, + { + "description": "Can be one of name, handle, email, saml_idp, managed_by, role, created_at.", + "in": "query", + "name": "sortby", + "required": false, + "schema": { + "enum": [ + "name", + "handle", + "email", + "saml_idp", + "managed_by", + "role", + "created_at" + ], + "type": "string" + } + }, + { + "description": "Can be one of asc, desc.", + "in": "query", + "name": "sortorder", + "required": false, + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "description": "Number of results to return (min: 1, max: 500, default: 15)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "maximum": 500, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Optional, when not specified, the first page will be returned. Every returned page contains a `paging_state`, this should be supplied to retrieve the next page.", + "in": "query", + "name": "pagingState", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Filter for (un-)verified email", + "in": "query", + "name": "email", + "required": false, + "schema": { + "enum": [ + "unverified", + "verified" + ], + "type": "string" + } + }, + { + "description": "Optional, return only non-searchable members when false.", + "in": "query", + "name": "searchable", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchResult_TeamContact_LTE0NjQ0NzMw" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchResult_TeamContact_LTE0NjQ0NzMw" + } + } + }, + "description": "Search results" + } + }, + "summary": "Browse team for members (requires add-user permission)" + } + }, + "/teams/{tid}/search-visibility": { + "get": { + "description": " [internal route ID: \"get-search-visibility\"]\n\n", + "operationId": "get-search-visibility", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamSearchVisibilityView_Mzg3MzMzMTk3" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Shows the value for search visibility" + }, + "put": { + "description": " [internal route ID: \"set-search-visibility\"]\n\n", + "operationId": "set-search-visibility", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamSearchVisibilityView_Mzg3MzMzMTk3" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Search visibility set" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "team-search-visibility-not-enabled", + "message": "Custom search is not available for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "team-search-visibility-not-enabled", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Custom search is not available for this team (label: `team-search-visibility-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Sets the search visibility for the whole team" + } + }, + "/teams/{tid}/size": { + "get": { + "description": " [internal route ID: \"get-team-size\"]\n\nCan be out of sync by roughly the `refresh_interval` of the ES index.", + "operationId": "get-team-size", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamSize_LTMzMzk2MTk1" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamSize_LTMzMzk2MTk1" + } + } + }, + "description": "Number of team members" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid invitation code. (label: `invalid-invitation-code`)" + } + }, + "summary": "Get the number of team members as an integer" + } + }, + "/time": { + "get": { + "description": " [internal route ID: \"get-server-time\"]\n\nReturns the current server time in UTC with seconds precision.", + "operationId": "get-server-time", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServerTime_LTM4NTI3MzIx" + } + } + }, + "description": "" + } + }, + "summary": "Get the current server time" + } + }, + "/upgrade-personal-to-team": { + "post": { + "description": " [internal route ID: \"upgrade-personal-to-team\"]\n\n", + "operationId": "upgrade-personal-to-team", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/BindingNewTeamUser_LTY0MDQxMDEw" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserTeam_MzI4NDQ1Mzkw" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateUserTeam_MzI4NDQ1Mzkw" + } + } + }, + "description": "Team created" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "user-already-in-a-team", + "message": "Switching teams is not allowed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-already-in-a-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-already-in-a-team", + "message": "Switching teams is not allowed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-already-in-a-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Switching teams is not allowed (label: `user-already-in-a-team`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "User not found (label: `not-found`)" + } + }, + "summary": "Upgrade personal user to team owner" + } + }, + "/user-groups": { + "get": { + "description": " [internal route ID: \"get-user-groups\"]\n\n", + "operationId": "get-user-groups", + "parameters": [ + { + "description": "Search string", + "in": "query", + "name": "q", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "required": false, + "schema": { + "enum": [ + "name", + "created_at" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "sort_order", + "required": false, + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "page_size", + "required": false, + "schema": { + "description": "integer from [1..500]", + "type": "number" + } + }, + { + "description": "`name` of the last seen user group, used to get the next page when sorting by name.", + "in": "query", + "name": "last_seen_name", + "required": false, + "schema": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + { + "description": "`created_at` field of the last seen user group, used to get the next page when sorting by created_at.", + "in": "query", + "name": "last_seen_created_at", + "required": false, + "schema": { + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + } + }, + { + "description": "`id` of the last seen group, used to get the next page. **Must** be sent to get the next page.", + "in": "query", + "name": "last_seen_id", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "include_channels", + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "include_member_count", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroupPage_UserGroup_Const_LTMxNDg5MDAy" + } + } + }, + "description": "" + } + }, + "summary": "Fetch groups accessible to the logged-in user" + }, + "post": { + "description": " [internal route ID: \"create-user-group\"]\n\n", + "operationId": "create-user-group", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewUserGroup_MzYxODU0OTU1" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroup_Identity_NTg4MTY1MjEx" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-invalid" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nOnly team members of the same team can be added to a user group. (label: `user-group-invalid`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + } + } + } + }, + "/user-groups/check-name": { + "post": { + "description": " [internal route ID: \"check-user-group-name-available\"]\n\n", + "operationId": "check-user-group-name-available", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CheckUserGroupName_LTg0ODU1OTk1" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserGroupNameAvailability_LTYzMDE1NTk4" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroupNameAvailability_LTYzMDE1NTk4" + } + } + }, + "description": "OK" + } + }, + "summary": "[STUB] Check if a user group name is available" + } + }, + "/user-groups/{gid}": { + "delete": { + "description": " [internal route ID: \"delete-user-group\"]\n\n", + "operationId": "delete-user-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "User group deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } + }, + "get": { + "description": " [internal route ID: \"get-user-group\"]\n\n", + "operationId": "get-user-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "include_channels", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserGroup_Identity_NTg4MTY1MjEx" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroup_Identity_NTg4MTY1MjEx" + } + } + }, + "description": "User Group Found" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` or User group not found (label: `user-group-not-found`)\n\nUser group not found (label: `user-group-not-found`)" + } + }, + "summary": "Fetch a group accessible to the logged-in user" + }, + "put": { + "description": " [internal route ID: \"update-user-group\"]\n\n", + "operationId": "update-user-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroupUpdate_MjUyNTA3Mjgy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User added updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } + } + }, + "/user-groups/{gid}/channels": { + "put": { + "description": " [internal route ID: \"update-user-group-channels\"]\n\n", + "operationId": "update-user-group-channels", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "append_only", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateUserGroupChannels_LTIyMjcwMTMx" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User group channels updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + }, + "summary": "Replaces the channels with the given list." + } + }, + "/user-groups/{gid}/users": { + "post": { + "description": " [internal route ID: \"add-users-to-group-bulk\"]\n\n", + "operationId": "add-users-to-group-bulk", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroupAddUsers_LTgzOTYzNzk0" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Users added to group" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-invalid" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nOnly team members of the same team can be added to a user group. (label: `user-group-invalid`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } + }, + "put": { + "description": " [internal route ID: \"update-user-group-members\"]\n\n", + "operationId": "update-user-group-members", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateUserGroupMembers_LTg1MzQ2NDY3" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User group members updated" + } + }, + "summary": "[STUB] Update user group members. Replaces the users with the given list." + } + }, + "/user-groups/{gid}/users/{uid}": { + "delete": { + "description": " [internal route ID: \"remove-user-from-group\"]\n\n", + "operationId": "remove-user-from-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "User removed from group" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-invalid" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team members of the same team can be added to a user group. (label: `user-group-invalid`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` or `uid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } + }, + "post": { + "description": " [internal route ID: \"add-user-to-group\"]\n\n", + "operationId": "add-user-to-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "User added to group" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-invalid" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team members of the same team can be added to a user group. (label: `user-group-invalid`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` or `uid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } + } + }, + "/users/list-clients": { + "post": { + "description": " [internal route ID: \"list-clients-bulk@v2\"]\n\nIf a backend is unreachable, the clients from that backend will be omitted from the response", + "operationId": "list-clients-bulk@v2", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LimitedQualifiedUserIdList_500" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "qualified_user_map": { + "$ref": "#/components/schemas/QualifiedUserMap_Set_PubClient" + } + }, + "type": "object" + } + } + }, + "description": "" + } + }, + "summary": "List all clients for a set of user ids" + } + }, + "/users/list-prekeys": { + "post": { + "description": " [internal route ID: \"get-multi-user-prekey-bundle-qualified\"]\n\nYou can't request information for more users than maximum conversation size.", + "operationId": "get-multi-user-prekey-bundle-qualified", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QualifiedUserClients" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QualifiedUserClientPrekeyMapV4_MzcxOTAxMTYy" + } + } + }, + "description": "" + } + }, + "summary": "(deprecated) Given a map of user IDs to client IDs return a prekey for each one." + } + }, + "/users/{uid_domain}/{uid}": { + "get": { + "description": " [internal route ID: \"get-user-qualified\"]\n\n", + "operationId": "get-user-qualified", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserProfile_LTQzMTQxMTE1" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserProfile_LTQzMTQxMTE1" + } + } + }, + "description": "User found" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`uid_domain` or `uid` or User not found (label: `not-found`)" + } + }, + "summary": "Get a user by Domain and UserId" + } + }, + "/users/{uid_domain}/{uid}/clients/{client}": { + "get": { + "description": " [internal route ID: \"get-user-client-qualified\"]\n\nHint: to list all clients of one or more users, use POST /users/list-clients.", + "operationId": "get-user-client-qualified", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PubClient" + } + } + }, + "description": "" + } + }, + "summary": "Get a specific client of a user" + } + }, + "/users/{uid_domain}/{uid}/prekeys": { + "get": { + "description": " [internal route ID: \"get-users-prekey-bundle-qualified\"]\n\n", + "operationId": "get-users-prekey-bundle-qualified", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PrekeyBundle_MzgzOTk4MjYz" + } + } + }, + "description": "" + } + }, + "summary": "Get a prekey for each client of a user." + } + }, + "/users/{uid_domain}/{uid}/prekeys/{client}": { + "get": { + "description": " [internal route ID: \"get-users-prekeys-client-qualified\"]\n\n", + "operationId": "get-users-prekeys-client-qualified", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientPrekey_LTcyODUzMTcw" + } + } + }, + "description": "" + } + }, + "summary": "Get a prekey for a specific client of a user." + } + }, + "/users/{uid_domain}/{uid}/supported-protocols": { + "get": { + "description": " [internal route ID: \"get-supported-protocols\"]\n\n", + "operationId": "get-supported-protocols", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/BaseProtocolTag_LTM0MDE1NTEx" + }, + "type": "array", + "uniqueItems": true + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/BaseProtocolTag_LTM0MDE1NTEx" + }, + "type": "array", + "uniqueItems": true + } + } + }, + "description": "Protocols supported by the user" + } + }, + "summary": "Get a user's supported protocols" + } + }, + "/users/{uid}/email": { + "put": { + "description": " [internal route ID: \"update-user-email\"]\n\nIf the user has a pending email validation, the validation email will be resent.", + "operationId": "update-user-email", + "parameters": [ + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EmailUpdate_NjQ5MDg1OTY0" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "Resend email address validation email." + } + }, + "/users/{uid}/rich-info": { + "get": { + "description": " [internal route ID: \"get-rich-info\"]\n\n", + "operationId": "get-rich-info", + "parameters": [ + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RichInfoAssocList" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RichInfoAssocList" + } + } + }, + "description": "Rich info about the user" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + } + }, + "summary": "Get a user's rich info" + } + }, + "/users/{uid}/searchable": { + "post": { + "description": " [internal route ID: \"set-user-searchable\"]\n\n", + "operationId": "set-user-searchable", + "parameters": [ + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SetSearchable_NDAxODAxODI5" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "Set user's visibility in search" + } + }, + "/verification-code/send": { + "post": { + "description": " [internal route ID: \"send-verification-code\"]\n\n", + "operationId": "send-verification-code", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SendVerificationCode_MjgxNDgxODE2" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Verification code sent." + } + }, + "summary": "Send a verification code to a given email address." + } + }, + "/websocket": { + "get": { + "description": " [internal route ID: \"websocket\"]\n\nThis is a temporary copy of await, please do not use it", + "externalDocs": { + "description": "RFC 6455", + "url": "https://datatracker.ietf.org/doc/html/rfc6455" + }, + "operationId": "websocket", + "parameters": [ + { + "description": "Client ID", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "101": { + "description": "Connection upgraded." + }, + "426": { + "description": "Upgrade required." + } + }, + "summary": "Establish websocket connection" + } + } + }, + "security": [ + { + "ZAuth": [] + } + ], + "servers": [ + { + "url": "/v18" + } + ] +} diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index dd5471760b1..a78805c7c03 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -236,10 +236,11 @@ internalEndpointsSwaggerDocsAPIs = -- -- Dual to `internalEndpointsSwaggerDocsAPI`. versionedSwaggerDocsAPI :: Servant.Server VersionedSwaggerDocsAPI -versionedSwaggerDocsAPI (Just (VersionNumber V18)) = +versionedSwaggerDocsAPI (Just (VersionNumber V19)) = swaggerSchemaUIServer $ devVersionSwagger & S.info . S.description ?~ $((unTypeCode . embedText) =<< makeRelativeToProject "docs/swagger.md") +versionedSwaggerDocsAPI (Just (VersionNumber V18)) = swaggerPregenUIServer $(pregenSwagger V18) versionedSwaggerDocsAPI (Just (VersionNumber V17)) = swaggerPregenUIServer $(pregenSwagger V17) versionedSwaggerDocsAPI (Just (VersionNumber V16)) = swaggerPregenUIServer $(pregenSwagger V16) versionedSwaggerDocsAPI (Just (VersionNumber V15)) = swaggerPregenUIServer $(pregenSwagger V15) From 109ec41de2f986a40366d1b94f77fb8993433897 Mon Sep 17 00:00:00 2001 From: VeryMilkyJoe Date: Wed, 9 Sep 2026 19:01:48 +0200 Subject: [PATCH 15/29] Update developer docs (#5523) * Remove instruction for unneeded cabal update from developer build guideline --- changelog.d/4-docs/update-developer-docs | 1 + docs/src/developer/developer/api-versioning.md | 2 +- docs/src/developer/developer/building.md | 16 +++------------- .../developer/developer/coding-conventions.md | 2 +- docs/src/developer/developer/how-to.md | 4 ++-- docs/src/developer/developer/open-telemetry.md | 2 +- docs/src/developer/developer/pr-guidelines.md | 4 ++-- 7 files changed, 11 insertions(+), 20 deletions(-) create mode 100644 changelog.d/4-docs/update-developer-docs diff --git a/changelog.d/4-docs/update-developer-docs b/changelog.d/4-docs/update-developer-docs new file mode 100644 index 00000000000..9197d6b34bc --- /dev/null +++ b/changelog.d/4-docs/update-developer-docs @@ -0,0 +1 @@ +Remove cabal update from build steps and fix some typos in developer docs \ No newline at end of file diff --git a/docs/src/developer/developer/api-versioning.md b/docs/src/developer/developer/api-versioning.md index c6f70a08e71..7904fcf87a7 100644 --- a/docs/src/developer/developer/api-versioning.md +++ b/docs/src/developer/developer/api-versioning.md @@ -282,7 +282,7 @@ When invoking an endpoint as a federation client, we need to make sure that all supported versions are covered. The `FederatorClient` monad has an `Alternative` instance which can be useful for this purpose: an action will fail (before even performing any request) if it refers to an endpoint whose version -range does not contain the version that was negotiatted. +range does not contain the version that was negotiated. For example, suppose that `get-user-clients` disappears in version 2, and clients are now supposed to use an endpoint called `get-clients-ng`, with diff --git a/docs/src/developer/developer/building.md b/docs/src/developer/developer/building.md index 845053fbf5f..4cbdba83822 100644 --- a/docs/src/developer/developer/building.md +++ b/docs/src/developer/developer/building.md @@ -45,16 +45,6 @@ and go and grab a coffee. ☕ Your system will likely not build much, but it will definitely spend some time fetching things from different caches. -### initializing the cabal mirrors - -There are a few dependencies that are not provided by the nix env, for these, please run - -```bash -cabal update -``` - -now that you’re in the devshell. - ### building wire-server #### with cabal @@ -154,7 +144,7 @@ make full-clean ### Cabal can’t read index (Did you call checkForUpdates?) -Sometimes abording cabal mid-update can corrupt its index. Deleting `~/.cabal/packages/hackage.haskell.org` will usually do the trick. +Sometimes aborting cabal mid-update can corrupt its index. Deleting `~/.cabal/packages/hackage.haskell.org` will usually do the trick. As a side-note: `make c` doesn’t run `cabal update`, but `make` does, so keep that in mind. @@ -181,7 +171,7 @@ Setting up these real, but in-memory internal and “fake” external dependenci deploy/dockerephemeral/run.sh ``` -Also make sure your system is able to resolve the fully qualified domain `localhost.` (note the trailing dot). This is surprisingly not trivial, because of limitations in how libc parses `/etc/hosts`. You can check that with, for example, `ping localhost.`. If you get a name resolution error, you need to add `localhost.` explictly to your `/etc/hosts` file. +Also make sure your system is able to resolve the fully qualified domain `localhost.` (note the trailing dot). This is surprisingly not trivial, because of limitations in how libc parses `/etc/hosts`. You can check that with, for example, `ping localhost.`. If you get a name resolution error, you need to add `localhost.` explicitly to your `/etc/hosts` file. After all containers are up you can use these Makefile targets to run the tests locally: @@ -219,7 +209,7 @@ After all containers are up you can use these Makefile targets to run the tests ## How to run the webapp locally against locally running backend 1. Clone the webapp from: https://github.com/wireapp/wire-webapp -2. Install these depedencies needed for the webapp: +2. Install these dependencies needed for the webapp: 1. nodejs 2. yarn 3. mkcert diff --git a/docs/src/developer/developer/coding-conventions.md b/docs/src/developer/developer/coding-conventions.md index 5df624aac44..22d6a4ce2ed 100644 --- a/docs/src/developer/developer/coding-conventions.md +++ b/docs/src/developer/developer/coding-conventions.md @@ -17,7 +17,7 @@ convert from and to `String` and `Text` which are nowadays typically `utf8` enco and a `String` or `Text` can be different; e.g. we could decode a `ByteString` as ASCII-Chars or as utf8, just to name a few. -There’s another inherent problem to `cs` in that context, namely **readability**; a `TL.fromStict` immediately tells +There’s another inherent problem to `cs` in that context, namely **readability**; a `TL.fromStrict` immediately tells you what the code does; `cs`, however, says nothing; you know there’s *some* conversion going on but not which. We have hence decided to not use the error-prone and hard-to-read `cs` in production code, i.e., in all libraries diff --git a/docs/src/developer/developer/how-to.md b/docs/src/developer/developer/how-to.md index 8e8f97fc128..440bba4bcc8 100644 --- a/docs/src/developer/developer/how-to.md +++ b/docs/src/developer/developer/how-to.md @@ -21,7 +21,7 @@ Terminal 2: * Build and start wire-server services: ` make c && ./dist/run-services` Open your browser at: -[http://localhost:8080/api/swagger-ui](http://localhost:8080/api/swagger-ui) for a list of API verions. +[http://localhost:8080/api/swagger-ui](http://localhost:8080/api/swagger-ui) for a list of API versions. Also check out the docs for swagger in our staging environment: [Swagger / OpenAPI documentation](../../understand/api-client-perspective/swagger.md#swagger-api-docs). Replace the staging domain by @@ -96,7 +96,7 @@ This will create two full installations of wire-server on the kubernetes cluster Check CI for the latest tag that has been created on your PR (expect this to take at least 30-60 minutes from the last time you pushed to your branch). Example: -Look at a successful job in the `wire-server-pr` pipeline from a job bruild matching your desired PR and commit hash. Then, find the actual docker tag used. +Look at a successful job in the `wire-server-pr` pipeline from a job build matching your desired PR and commit hash. Then, find the actual docker tag used. ![concourse-pr-version-circled](https://user-images.githubusercontent.com/2112744/114410146-69b34000-9bab-11eb-863c-106fb661ca82.png) diff --git a/docs/src/developer/developer/open-telemetry.md b/docs/src/developer/developer/open-telemetry.md index 240d5b4daaf..0d1750604ea 100644 --- a/docs/src/developer/developer/open-telemetry.md +++ b/docs/src/developer/developer/open-telemetry.md @@ -11,7 +11,7 @@ The following components have been instrumented: ## Known Issues and future work -- Proper HTTP/2 instrumentation is missing for federator & co - this is related to http/2 outobj in the http2 libraray throwing away all structured information +- Proper HTTP/2 instrumentation is missing for federator & co - this is related to http/2 outobj in the http2 library throwing away all structured information - Some parts of the service, such as background jobs, may need additional instrumentation. It’s currently unclear if these are appearing in the tracing data. - we need to ingest the data into grafana tempo diff --git a/docs/src/developer/developer/pr-guidelines.md b/docs/src/developer/developer/pr-guidelines.md index d8bc1cffd37..00210ff82a6 100644 --- a/docs/src/developer/developer/pr-guidelines.md +++ b/docs/src/developer/developer/pr-guidelines.md @@ -62,7 +62,7 @@ For customer support access to an internal endpoint, instead update code in [ste ### Demo nginz configuration -New entris should include `common_response_no_zauth.conf;` for public endpoints without authentication and `common_response_with_zauth.conf;` for regular (authenticated) endpoints. Browse the file to see examples. +New entries should include `common_response_no_zauth.conf;` for public endpoints without authentication and `common_response_with_zauth.conf;` for regular (authenticated) endpoints. Browse the file to see examples. ### Example @@ -109,7 +109,7 @@ Remove them with the PR from wire-server `./charts` folder, as charts are linked ### Renaming configuration flags -Avoid doing this, it’s usually viable to introduce an at-least-equally-good name and remove the old one, that admins can first add the new options, then uprade the software, then remove the old ones. +Avoid doing this, it’s usually viable to introduce an at-least-equally-good name and remove the old one, that admins can first add the new options, then upgrade the software, then remove the old ones. If you must, see Removing/adding sections above. But please note that all people who have an installation of wire also may have overridden any of the configuration option you may wish to change the name of. As this is not type checked, it’s very error prone and people may find themselves with default configuration values being used instead of their intended configuration settings. Guideline: only rename for good reasons, not for aesthetics; or be prepared to spend a significant amount on documenting and communication about this change. From 946e277b743823c4becce84e3a30dae64402a5b6 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Wed, 9 Sep 2026 20:38:38 +0200 Subject: [PATCH 16/29] WPB-28246: expose ssoIdpChangeDetectionEnabled via GET /system/settings (#5527) --- changelog.d/2-features/WPB-28246 | 1 + .../wire-server/templates/brig/configmap.yaml | 11 +++++ .../src/developer/reference/config-options.md | 31 ++++++++++++ integration/test/Test/SystemSettings.hs | 38 +++++++++++++++ libs/wire-api/src/Wire/API/SystemSettings.hs | 11 ++++- .../src/Wire/JobSubsystem/Migrations.hs | 2 +- .../wire-subsystems/src/Wire/MigrationLock.hs | 10 ++-- .../src/Wire/PostgresMigrations.hs | 2 +- services/brig/brig.cabal | 1 + services/brig/src/Brig/API/Public.hs | 7 ++- services/brig/src/Brig/Options.hs | 29 +++++++++++ services/brig/test/unit/Run.hs | 4 +- services/brig/test/unit/Test/Brig/Options.hs | 48 +++++++++++++++++++ 13 files changed, 184 insertions(+), 11 deletions(-) create mode 100644 changelog.d/2-features/WPB-28246 create mode 100644 services/brig/test/unit/Test/Brig/Options.hs diff --git a/changelog.d/2-features/WPB-28246 b/changelog.d/2-features/WPB-28246 new file mode 100644 index 00000000000..fd0b80b12d4 --- /dev/null +++ b/changelog.d/2-features/WPB-28246 @@ -0,0 +1 @@ +Add `ssoIdpChangeDetectionEnabled` to `GET /system/settings`. diff --git a/charts/wire-server/templates/brig/configmap.yaml b/charts/wire-server/templates/brig/configmap.yaml index ad9ee08bf8f..7a43c8244ce 100644 --- a/charts/wire-server/templates/brig/configmap.yaml +++ b/charts/wire-server/templates/brig/configmap.yaml @@ -396,5 +396,16 @@ data: setNomadProfiles: {{ index . "setNomadProfiles" }} {{- end }} setConsumableNotifications: false + {{- /* Raw spar multi-ingress inputs (schema: spar.config in values.yaml). + Brig derives ssoIdpChangeDetectionEnabled from them at runtime + (see Brig.Options.deriveSsoIdpChangeDetectionEnabled). Keep the + rendered keys in sync with its FromJSON instance. */}} + {{- with $.Values.spar }} + {{- with .config }} + setSsoIdpChangeDetectionInputs: + multiIngressDomainConfigs: {{ .domainConfigs | default (dict) | toJson }} + idpCertFingerprintAllowlist: {{ .idpCertFingerprintAllowlist | default (list) | toJson }} + {{- end }} + {{- end }} {{- end }} {{- end }} diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index b788c123722..9154c97ea73 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -1320,6 +1320,37 @@ brig: stored. The challenge (`StoredDomainVerificationChallenge`) will be deleted after this period. +### SSO Settings + +#### `setSsoIdpChangeDetectionInputs` + +When the reported `ssoIdpChangeDetectionEnabled` is `true`, the authenticated +`GET /system/settings` endpoint tells clients to compare the stored SSO IdP ID +with the IdP ID used for the current login and to keep existing locally +decrypted messages when they match. When false or absent, clients treat an IdP +change as before. Only the authenticated endpoint reports this field; the +public `/system/settings/unauthorized` endpoint does not. + +Brig derives `ssoIdpChangeDetectionEnabled` from these raw spar inputs: it +reports `true` iff `multiIngressDomainConfigs` and +`idpCertFingerprintAllowlist` are both non-empty. The wire-server Helm chart +renders them from `spar.config`; deployments outside the chart should mirror +their spar multi-ingress configuration, and the absence of this setting +disables the feature. + +```default +# [brig.yaml] +optSettings: + setSsoIdpChangeDetectionInputs: + multiIngressDomainConfigs: + nginz-https.example.com: + appUri: https://webapp.example.com + ssoUri: https://nginz-https.example.com/sso + idpCertFingerprintAllowlist: + - "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD" +``` + + ## Settings in cargohold AWS S3 (or an alternative provider / service) is used to upload and download diff --git a/integration/test/Test/SystemSettings.hs b/integration/test/Test/SystemSettings.hs index 43071765b46..ff6c07fcdea 100644 --- a/integration/test/Test/SystemSettings.hs +++ b/integration/test/Test/SystemSettings.hs @@ -47,3 +47,41 @@ testGetSettingsInternal (MkTagged enableMls) = do getSystemSettingsInternal user `bindResponse` \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "setEnableMls" `shouldMatch` fromMaybe False enableMls + +testGetSettingsInternalSsoIdpChangeDetection :: + (HasCallStack) => + Tagged "sso-idp-change-detection" (Maybe Bool) -> + App () +testGetSettingsInternalSsoIdpChangeDetection (MkTagged multiIngress) = do + let conf = + def + { brigCfg = + maybe + (removeField "optSettings.setSsoIdpChangeDetectionInputs") + (setField "optSettings.setSsoIdpChangeDetectionInputs" . sparInputs) + multiIngress + } + withModifiedBackend conf \domain -> do + user <- randomUser domain def + getSystemSettingsInternal user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + -- Brig derives the flag in Haskell from the raw inputs: true iff both the + -- multi-ingress domain configs and the IdP cert fingerprint allowlist are + -- non-empty (Brig.Options.deriveSsoIdpChangeDetectionEnabled); absent + -- inputs mean disabled. The tag enumerates Nothing/Just False/Just True + -- (GEnum), so `fromMaybe` — not `fromJust`. + resp.json %. "ssoIdpChangeDetectionEnabled" `shouldMatch` fromMaybe False multiIngress + getSystemSettingsPublic domain `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + lookupField resp.json "ssoIdpChangeDetectionEnabled" `shouldMatch` (Nothing :: Maybe Value) + +-- | Minimal mirror of @spar.config@: 'True' is a multi-ingress spar (non-empty +-- @domainConfigs@ and allowlist); 'False' is single-ingress spar (both empty). +sparInputs :: Bool -> Value +sparInputs multiIngress = + object + [ "multiIngressDomainConfigs" + .= if multiIngress then object ["example.com" .= object []] else object [], + "idpCertFingerprintAllowlist" + .= if multiIngress then ["00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33" :: String] else ([] :: [String]) + ] diff --git a/libs/wire-api/src/Wire/API/SystemSettings.hs b/libs/wire-api/src/Wire/API/SystemSettings.hs index b41a17acc59..a0ce3c67a76 100644 --- a/libs/wire-api/src/Wire/API/SystemSettings.hs +++ b/libs/wire-api/src/Wire/API/SystemSettings.hs @@ -56,7 +56,8 @@ settingsPublicObjectSchema = ) data SystemSettingsInternal = SystemSettingsInternal - { ssiSetEnableMls :: !Bool + { ssiSetEnableMls :: !Bool, + ssiSetSsoIdpChangeDetectionEnabled :: !Bool } deriving (Eq, Show, Generic) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema SystemSettingsInternal @@ -70,6 +71,14 @@ settingsInternalObjectSchema :: ObjectSchema SwaggerDoc SystemSettingsInternal settingsInternalObjectSchema = SystemSettingsInternal <$> ssiSetEnableMls .= fieldWithDocModifier "setEnableMls" (description ?~ "Whether MLS is enabled or not") schema + <*> ssiSetSsoIdpChangeDetectionEnabled + .= fieldWithDocModifier + "ssoIdpChangeDetectionEnabled" + ( description + ?~ "Whether clients should compare the stored SSO IdP ID with the IdP ID of the current \ + \login and keep existing locally decrypted messages when they match." + ) + schema data SystemSettings = SystemSettings { ssPublic :: !SystemSettingsPublic, diff --git a/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs b/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs index cd74664b8a6..2410cb98c65 100644 --- a/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs +++ b/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs @@ -174,4 +174,4 @@ withArbiterMigrationLock connStr schemaName action = do releaseArbiterMigrationLock :: HasqlStatement.Statement Int64 () releaseArbiterMigrationLock = - [resultlessStatement|SELECT (1 :: integer) FROM (SELECT pg_advisory_unlock($1 :: bigint))|] + [resultlessStatement|SELECT (1 :: integer) FROM (SELECT pg_advisory_unlock($1 :: bigint)) AS t|] diff --git a/libs/wire-subsystems/src/Wire/MigrationLock.hs b/libs/wire-subsystems/src/Wire/MigrationLock.hs index 9befa82247c..c75bec048cb 100644 --- a/libs/wire-subsystems/src/Wire/MigrationLock.hs +++ b/libs/wire-subsystems/src/Wire/MigrationLock.hs @@ -157,11 +157,11 @@ withMigrationLocks lockType maxWait lockables action = do LockExclusive -> [resultlessStatement|SELECT (1 :: int) FROM (SELECT pg_advisory_lock(lockId) - FROM (SELECT UNNEST($1 :: bigint[]) as lockId))|] + FROM (SELECT UNNEST($1 :: bigint[]) as lockId) AS t) AS t2|] LockShared -> [resultlessStatement|SELECT (1 :: int) FROM (SELECT pg_advisory_lock_shared(lockId) - FROM (SELECT UNNEST($1 :: bigint[]) as lockId))|] + FROM (SELECT UNNEST($1 :: bigint[]) as lockId) AS t) AS t2|] releaseLocks :: Hasql.Statement [Int64] () releaseLocks = @@ -170,15 +170,13 @@ withMigrationLocks lockType maxWait lockables action = do LockExclusive -> [resultlessStatement|SELECT (1 :: int) FROM (SELECT pg_advisory_unlock(lockId) - FROM (SELECT UNNEST($1 :: bigint[]) as lockId))|] + FROM (SELECT UNNEST($1 :: bigint[]) as lockId) AS t) AS t2|] LockShared -> [resultlessStatement|SELECT (1 :: int) FROM (SELECT pg_advisory_unlock_shared(lockId) - FROM (SELECT UNNEST($1 :: bigint[]) as lockId))|] + FROM (SELECT UNNEST($1 :: bigint[]) as lockId) AS t) AS t2|] -------------------------------------------------------------------------------- --- INSTANCES - -- Combines team id and feature name into one lock key to keep per-feature locks distinct within a team -- without introducing a separate lock table; rotate+xor mixes the two hashes to reduce collisions. instance MigrationLockable (TeamId, Text) where diff --git a/libs/wire-subsystems/src/Wire/PostgresMigrations.hs b/libs/wire-subsystems/src/Wire/PostgresMigrations.hs index cc0ccefa378..75b696f8e74 100644 --- a/libs/wire-subsystems/src/Wire/PostgresMigrations.hs +++ b/libs/wire-subsystems/src/Wire/PostgresMigrations.hs @@ -90,7 +90,7 @@ runAllMigrations pool logger = do unlockNonTransactionMigration :: Hasql.Statement Int64 () unlockNonTransactionMigration = - [resultlessStatement|SELECT (1 :: integer) FROM (SELECT pg_advisory_unlock($1 :: bigint))|] + [resultlessStatement|SELECT (1 :: integer) FROM (SELECT pg_advisory_unlock($1 :: bigint)) AS t|] -- We don't have to use 'bracket' here because failing in the session should -- cause the session to drop and any acquired locks get automatically diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 794b137836c..613e931c9ef 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -490,6 +490,7 @@ test-suite brig-tests Test.Brig.Effects.Delay Test.Brig.InternalNotification Test.Brig.MLS + Test.Brig.Options hs-source-dirs: test/unit ghc-options: diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index a78805c7c03..60cc7a53bf6 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -1674,7 +1674,12 @@ getSystemSettingsInternal = do { setRestrictUserCreation = fromMaybe False optSettings.restrictUserCreation, nomadProfiles = optSettings.nomadProfiles } - iSettings = SystemSettingsInternal $ fromMaybe False optSettings.enableMLS + iSettings = + SystemSettingsInternal + { ssiSetEnableMls = fromMaybe False optSettings.enableMLS, + ssiSetSsoIdpChangeDetectionEnabled = + deriveSsoIdpChangeDetectionEnabled optSettings.ssoIdpChangeDetectionInputs + } pure $ SystemSettings pSettings iSettings authorizeTeam :: diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index df05f68fd7d..e69f515a391 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -35,6 +35,7 @@ import Data.Default import Data.Domain (Domain (..)) import Data.Id import Data.LanguageCodes (ISO639_1 (EN)) +import Data.Map.Strict qualified as Map import Data.Misc (HttpsUrl) import Data.Nonce import Data.Range @@ -549,11 +550,39 @@ data Settings = Settings ephemeralUserCreationEnabled :: !Bool, -- | Determines if this backend supports nomad profiles. nomadProfiles :: !(Maybe Bool), + -- | Raw spar multi-ingress inputs as rendered by the wire-server chart + -- (from @spar.config@; brig only needs their presence, not their contents). + -- 'deriveSsoIdpChangeDetectionEnabled' turns these into the value served + -- by @GET /system/settings@. + ssoIdpChangeDetectionInputs :: !(Maybe SsoIdpChangeDetectionInputs), -- | Determines if consumable notifications are enabled consumableNotifications :: !Bool } deriving (Show, Generic) +-- | Mirror of the multi-ingress-relevant parts of @spar.config@. Keys match +-- what @charts/wire-server/templates/brig/configmap.yaml@ renders from +-- @$.Values.spar.config@; contents are opaque to brig. +data SsoIdpChangeDetectionInputs = SsoIdpChangeDetectionInputs + { multiIngressDomainConfigs :: !(Map Text Value), + idpCertFingerprintAllowlist :: ![Text] + } + deriving (Eq, Show, Generic) + +instance FromJSON SsoIdpChangeDetectionInputs + +-- | @ssoIdpChangeDetectionEnabled@ mirrors spar's multi-ingress +-- configuration: enabled iff inline multi-ingress domain configs AND a +-- non-empty IdP cert fingerprint allowlist are configured (the criterion the +-- wire-server chart previously computed in the template; cf. +-- SAML.WebSSO.Config.isMultiIngressConfig). +deriveSsoIdpChangeDetectionEnabled :: Maybe SsoIdpChangeDetectionInputs -> Bool +deriveSsoIdpChangeDetectionEnabled = \case + Nothing -> False + Just inputs -> + not (Map.null inputs.multiIngressDomainConfigs) + && not (null inputs.idpCertFingerprintAllowlist) + newtype ImplicitNoFederationRestriction = ImplicitNoFederationRestriction {federationDomainConfig :: FederationDomainConfig} deriving (Show, Eq, Generic) diff --git a/services/brig/test/unit/Run.hs b/services/brig/test/unit/Run.hs index a371d3130cc..3fffbfbfdbe 100644 --- a/services/brig/test/unit/Run.hs +++ b/services/brig/test/unit/Run.hs @@ -25,6 +25,7 @@ import Test.Brig.Calling qualified import Test.Brig.Calling.Internal qualified import Test.Brig.InternalNotification qualified import Test.Brig.MLS qualified +import Test.Brig.Options qualified import Test.Tasty main :: IO () @@ -35,5 +36,6 @@ main = [ Test.Brig.Calling.tests, Test.Brig.Calling.Internal.tests, Test.Brig.MLS.tests, - Test.Brig.InternalNotification.tests + Test.Brig.InternalNotification.tests, + Test.Brig.Options.tests ] diff --git a/services/brig/test/unit/Test/Brig/Options.hs b/services/brig/test/unit/Test/Brig/Options.hs new file mode 100644 index 00000000000..ebac8cc7ad9 --- /dev/null +++ b/services/brig/test/unit/Test/Brig/Options.hs @@ -0,0 +1,48 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Brig.Options (tests) where + +import Brig.Options +import Data.Aeson (eitherDecode, object) +import Data.Map.Strict qualified as Map +import Imports +import Test.Tasty +import Test.Tasty.HUnit + +tests :: TestTree +tests = + testGroup + "Brig.Options" + [ testGroup + "deriveSsoIdpChangeDetectionEnabled" + [ testCase "absent inputs disable" $ + deriveSsoIdpChangeDetectionEnabled Nothing @?= False, + testCase "empty inputs disable" $ + deriveSsoIdpChangeDetectionEnabled (Just (SsoIdpChangeDetectionInputs Map.empty [])) @?= False, + testCase "domains without allowlist disable" $ + deriveSsoIdpChangeDetectionEnabled (Just (SsoIdpChangeDetectionInputs (Map.singleton "example.com" (object [])) [])) @?= False, + testCase "allowlist without domains disable" $ + deriveSsoIdpChangeDetectionEnabled (Just (SsoIdpChangeDetectionInputs Map.empty ["AA:BB"])) @?= False, + testCase "domains and allowlist enable" $ + deriveSsoIdpChangeDetectionEnabled (Just (SsoIdpChangeDetectionInputs (Map.singleton "example.com" (object [])) ["AA:BB"])) @?= True, + testCase "chart-rendered JSON parses" $ + eitherDecode + "{\"multiIngressDomainConfigs\":{\"example.com\":{}},\"idpCertFingerprintAllowlist\":[\"AA:BB\"]}" + @?= Right (SsoIdpChangeDetectionInputs (Map.singleton "example.com" (object [])) ["AA:BB"]) + ] + ] From 02557c5e14db643849615b03f7f913a545c106ad Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 10 Sep 2026 12:09:25 +0200 Subject: [PATCH 17/29] WPB-28422 reconcile stale local memberships for deleted remote conversations (#5504) --- changelog.d/6-federation/WPB-28422 | 1 + integration/test/Test/Conversation.hs | 192 +++++++++++++++++- .../src/Wire/ConversationSubsystem/Query.hs | 62 ++++-- 3 files changed, 240 insertions(+), 15 deletions(-) create mode 100644 changelog.d/6-federation/WPB-28422 diff --git a/changelog.d/6-federation/WPB-28422 b/changelog.d/6-federation/WPB-28422 new file mode 100644 index 00000000000..7ca489ce3bf --- /dev/null +++ b/changelog.d/6-federation/WPB-28422 @@ -0,0 +1 @@ +Remove stale local memberships when a remote conversation is definitively reported as not found. diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 681df7f59fa..e3a859df46f 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -582,6 +582,189 @@ testGetOneOnOneConvInStatusSentFromRemote domain = do resp <- getConversation d1User d2ConvId resp.status `shouldMatchInt` 200 +testReconcileStaleLocalMembershipsForDeletedRemoteConversation :: (HasCallStack) => App () +testReconcileStaleLocalMembershipsForDeletedRemoteConversation = do + owner <- randomUser OwnDomain def + alice <- randomUser OtherDomain def + charlie <- randomUser OtherDomain def + for_ [alice, charlie] $ connectTwoUsers owner + + conv <- registerMissingRemoteConversation owner [alice, charlie] + + eventually $ do + assertConversationMembership alice conv True + assertConversationMembership charlie conv True + + let isSystemDeleteFor conversation event = + fieldEquals event "payload.0.type" "conversation.system.delete" + &&~ isNotifConv conversation event + + -- A successful response from the owning backend that omits the conversation + -- proves that Alice's locally stored membership is stale. + withWebSockets [alice, charlie] $ \[wsAlice, wsCharlie] -> do + getConversation alice conv >>= assertLabel 404 "no-conversation" + void $ awaitMatch (isSystemDeleteFor conv) wsAlice + assertConversationMembership alice conv False + assertConversationMembership charlie conv True + + -- The bulk endpoint performs the same reconciliation for Charlie. + bindResponse (listConversations charlie [conv]) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "found" `shouldMatch` ([] :: [Value]) + resp.json %. "not_found" `shouldMatch` [conv] + resp.json %. "failed" `shouldMatch` ([] :: [Value]) + void $ awaitMatch (isSystemDeleteFor conv) wsCharlie + + assertConversationMembership alice conv False + assertConversationMembership charlie conv False + + -- Reconciliation is idempotent once the local membership has been removed. + getConversation alice conv >>= assertLabel 404 "no-conversation" + +-- | Fetching stale remote conversations from two different domains reconciles +-- both independently. A response for one domain must not remove memberships +-- belonging to another domain. +testReconcileStaleMembershipsMultipleDomains :: (HasCallStack) => App () +testReconcileStaleMembershipsMultipleDomains = do + resourcePool <- asks resourcePool + runCodensity (acquireResources 1 resourcePool) $ \[remoteBackend] -> + runCodensity (startDynamicBackend remoteBackend mempty) $ \_ -> do + alice <- randomUser OwnDomain def + ownerStatic <- randomUser OtherDomain def + ownerDynamic <- randomUser remoteBackend.berDomain def + connectTwoUsers ownerStatic alice + connectTwoUsers ownerDynamic alice + convStatic <- registerMissingRemoteConversation ownerStatic [alice] + convDynamic <- registerMissingRemoteConversation ownerDynamic [alice] + + eventually $ do + assertConversationMembership alice convStatic True + assertConversationMembership alice convDynamic True + + bindResponse (listConversations alice [convStatic, convDynamic]) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "found" `shouldMatch` ([] :: [Value]) + resp.json %. "failed" `shouldMatch` ([] :: [Value]) + notFound <- resp.json %. "not_found" & asList + for_ [convStatic, convDynamic] $ \conv -> + (notFound :: [Value]) `shouldContain` [conv] + + assertConversationMembership alice convStatic False + assertConversationMembership alice convDynamic False + +-- | Conversations the remote still returns are preserved; only omitted ones +-- are reconciled within the same request. +testReconcileOnlyMissingConversations :: (HasCallStack) => App () +testReconcileOnlyMissingConversations = do + alice <- randomUser OwnDomain def + owner <- randomUser OtherDomain def + connectTwoUsers owner alice + + alive <- + postConversation owner (defProteus {qualifiedUsers = [alice]}) + >>= getJSON 201 + aliveQid <- objQidObject alive + stale <- registerMissingRemoteConversation owner [alice] + + eventually $ do + assertConversationMembership alice aliveQid True + assertConversationMembership alice stale True + + bindResponse (listConversations alice [aliveQid, stale]) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "failed" `shouldMatch` ([] :: [Value]) + found <- resp.json %. "found" & asList + length (found :: [Value]) `shouldMatchInt` 1 + notFound <- resp.json %. "not_found" & asList + (notFound :: [Value]) `shouldContain` [stale] + + assertConversationMembership alice aliveQid True + assertConversationMembership alice stale False + +testPreserveRemoteMembershipOnFederationFailure :: (HasCallStack) => App () +testPreserveRemoteMembershipOnFederationFailure = do + resourcePool <- asks resourcePool + runCodensity (acquireResources 1 resourcePool) $ \[remoteBackend] -> do + (alice, convQid) <- runCodensity (startDynamicBackend remoteBackend mempty) $ \_ -> do + owner <- randomUser remoteBackend.berDomain def + alice <- randomUser OwnDomain def + connectTwoUsers owner alice + conv <- + postConversation owner (defProteus {qualifiedUsers = [alice]}) + >>= getJSON 201 + convQid <- objQidObject conv + eventually $ assertConversationMembership alice convQid True + pure (alice, convQid) + + bindResponse (listConversations alice [convQid]) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "failed" `shouldMatch` [convQid] + assertConversationMembership alice convQid True + +-- | This function only submits the on-conversation-created event +-- to the remote backend without actually having created a local conversation. +registerMissingRemoteConversation :: (HasCallStack) => Value -> [Value] -> App Value +registerMissingRemoteConversation owner members = do + originDomain <- objDomain owner + originUserId <- objId owner + convId <- randomId + targetDomain <- case members of + [] -> assertFailure "A remote conversation needs at least one local member" + firstMember : remainingMembers -> do + domain <- objDomain firstMember + for_ remainingMembers $ \remoteMember -> do + memberDomain <- objDomain remoteMember + memberDomain `shouldMatch` domain + pure domain + memberPayloads <- for members $ \remoteMember -> do + memberId <- objId remoteMember + memberQid <- objQidObject remoteMember + pure + $ object + [ "id" .= memberId, + "qualified_id" .= memberQid, + "status" .= (0 :: Int), + "conversation_role" .= ("wire_member" :: String) + ] + req <- + rawBaseRequest + originDomain + FederatorInternal + Unversioned + (joinHttpPath ["rpc", targetDomain, "galley", "on-conversation-created"]) + bindResponse + ( submit "POST" + $ req + & addHeader "Wire-Origin-Domain" originDomain + & addJSONObject + [ "time" .= ("2026-01-01T00:00:00.000Z" :: String), + "orig_user_id" .= originUserId, + "cnv_id" .= convId, + "cnv_type" .= (0 :: Int), + "cnv_access" .= ["invite" :: String, "code"], + "cnv_access_roles" .= ["team_member" :: String, "non_team_member"], + "cnv_name" .= Aeson.Null, + "non_creator_members" .= memberPayloads, + "message_timer" .= Aeson.Null, + "receipt_mode" .= Aeson.Null, + "protocol" .= object ["protocol" .= ("proteus" :: String)], + "group_conv_type" .= ("group_conversation" :: String), + "channel_add_permission" .= Aeson.Null, + "history" .= Aeson.Null + ] + ) + $ \resp -> resp.status `shouldMatchInt` 200 + pure $ object ["domain" .= originDomain, "id" .= convId] + +assertConversationMembership :: (HasCallStack) => Value -> Value -> Bool -> App () +assertConversationMembership user conv expected = + bindResponse (listConversationIds user def) $ \resp -> do + resp.status `shouldMatchInt` 200 + conversationIds <- resp.json %. "qualified_conversations" & asList + if expected + then conversationIds `shouldContain` [conv] + else conversationIds `shouldNotContain` [conv] + testAddingUserNonFullyConnectedFederation :: (HasCallStack) => StaticDomain -> App () testAddingUserNonFullyConnectedFederation domain = do let overrides = @@ -1029,8 +1212,13 @@ testOnUserDeletedConversations = do do -- Bob is not in the one-to-one conversation with Alice any more - conv <- getConversation alice ooConvId >>= getJSON 200 - shouldBeEmpty $ conv %. "members.others" + resp <- getConversation alice ooConvId + case resp.status of + 200 -> do + conv <- getJSON 200 resp + shouldBeEmpty $ conv %. "members.others" + 404 -> resp.json %. "label" `shouldMatch` ("no-conversation" :: String) + status -> assertFailure $ "Unexpected status while fetching one-to-one conversation: " <> show status do -- Bob is not in the main conversation any more mainConvAfter <- getConversation alice (mainConvBefore %. "qualified_id") >>= getJSON 200 diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Query.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Query.hs index c181a9c0866..3f020ae6f39 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Query.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Query.hs @@ -84,6 +84,7 @@ import Wire.API.Conversation.Role import Wire.API.Conversation.Role qualified as Public import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.Event.Conversation (SystemEvent (..), SystemEventData (EdSystemConvDelete)) import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Client (FederatorClient, getNegotiatedVersion) @@ -106,12 +107,16 @@ import Wire.ConversationSubsystem.Fetch (getConversationIdsImpl) import Wire.ConversationSubsystem.MLS import Wire.ConversationSubsystem.MLS.Enabled (assertMLSEnabled, getMLSPrivateKeys, isMLSEnabled) import Wire.ConversationSubsystem.MLS.One2One (localMLSOne2OneConversation, remoteMLSOne2OneConversation) +import Wire.ConversationSubsystem.Notify qualified as Notify import Wire.ConversationSubsystem.One2One import Wire.ConversationSubsystem.Util import Wire.FeaturesConfigSubsystem import Wire.FederationAPIAccess qualified as E import Wire.HashPassword (HashPassword) +import Wire.NotificationSubsystem import Wire.RateLimit +import Wire.Sem.Now (Now) +import Wire.Sem.Now qualified as Now import Wire.Sem.Paging.Cassandra import Wire.StoredConversation import Wire.StoredConversation qualified as Data @@ -184,6 +189,8 @@ getConversation :: Member (Error FederationError) r, Member (E.FederationAPIAccess FederatorClient) r, Member P.TinyLog r, + Member Now r, + Member NotificationSubsystem r, Member TeamSubsystem r ) => Local UserId -> @@ -205,6 +212,8 @@ getOwnConversation :: Member (Error InternalError) r, Member (E.FederationAPIAccess FederatorClient) r, Member P.TinyLog r, + Member Now r, + Member NotificationSubsystem r, Member TeamSubsystem r ) => Local UserId -> @@ -222,7 +231,9 @@ getRemoteConversation :: Member (ErrorS ConvNotFound) r, Member (Error FederationError) r, Member TinyLog r, - Member (E.FederationAPIAccess FederatorClient) r + Member (E.FederationAPIAccess FederatorClient) r, + Member Now r, + Member NotificationSubsystem r ) => Local UserId -> Remote ConvId -> @@ -239,7 +250,9 @@ getRemoteConversations :: Member (Error FederationError) r, Member (ErrorS 'ConvNotFound) r, Member (E.FederationAPIAccess FederatorClient) r, - Member P.TinyLog r + Member P.TinyLog r, + Member Now r, + Member NotificationSubsystem r ) => Local UserId -> [Remote ConvId] -> @@ -308,7 +321,9 @@ partitionGetConversationFailures = bimap concat concat . partitionEithers . map getRemoteConversationsWithFailures :: ( Member ConversationStore.ConversationStore r, Member (E.FederationAPIAccess FederatorClient) r, - Member P.TinyLog r + Member P.TinyLog r, + Member Now r, + Member NotificationSubsystem r ) => Local UserId -> [Remote ConvId] -> @@ -346,18 +361,38 @@ getRemoteConversationsWithFailures lusr convs = do rpc $ GetConversationsRequest (tUnqualified lusr) (tUnqualified someConvs) bimap (localFailures <>) (map remoteView . concat) . partitionEithers - <$> traverse handleFailure resp + <$> traverse (handleRequest locallyFound) resp where - handleFailure :: - (Member P.TinyLog r) => + handleRequest :: + ( Member ConversationStore.ConversationStore r, + Member P.TinyLog r, + Member Now r, + Member NotificationSubsystem r + ) => + [Remote ConvId] -> Either (Remote [ConvId], FederationError) (Remote GetRemoteConversationViewsResponse) -> Sem r (Either FailedGetConversation [Remote RemoteConversationView]) - handleFailure (Left (rcids, e)) = do + handleRequest _ (Left (rcids, e)) = do P.warn $ Logger.msg ("Error occurred while fetching remote conversations" :: ByteString) . Logger.field "error" (displayException e) pure . Left $ failedGetConversationRemotely (sequenceA rcids) e - handleFailure (Right c) = pure . Right . traverse (.convs) $ c + handleRequest locallyFound (Right response) = do + let locallyFoundForDomain = Set.fromList $ filter ((== tDomain response) . tDomain) locallyFound + returnedIds = Set.fromList $ map (qualifyAs response . (.id)) (tUnqualified response).convs + missingConversations = Set.toList $ locallyFoundForDomain `Set.difference` returnedIds + unless (null missingConversations) $ do + now <- Now.get + for_ missingConversations $ \conv -> do + ConversationStore.deleteMembersInRemoteConversation conv [tUnqualified lusr] + Notify.pushSystemEvent + Nothing + (SystemEvent (tUntagged conv) Nothing now Nothing EdSystemConvDelete) + (Set.singleton $ tUnqualified lusr) + P.info $ + Logger.msg ("Removed stale local memberships for remote conversations" :: ByteString) + . Logger.field "convIds" (show $ map tUntagged missingConversations) + pure . Right . traverse (.convs) $ response getConversationRoles :: ( Member ConversationStore.ConversationStore r, @@ -505,7 +540,9 @@ listConversations :: ( Member ConversationStore.ConversationStore r, Member (Error InternalError) r, Member (E.FederationAPIAccess FederatorClient) r, - Member P.TinyLog r + Member P.TinyLog r, + Member Now r, + Member NotificationSubsystem r ) => Local UserId -> Public.ListConversations -> @@ -529,9 +566,6 @@ listConversations luser (Public.ListConversations ids) = do fetchedOrFailedRemoteIds = Set.fromList $ map Public.cnvQualifiedId remoteConversations <> failedConvs remoteNotFoundRemoteIds = filter (`Set.notMember` fetchedOrFailedRemoteIds) $ map tUntagged remoteIds unless (null remoteNotFoundRemoteIds) $ - -- FUTUREWORK: This implies that the backends are out of sync. Maybe the - -- current user should be considered removed from this conversation at this - -- point. P.warn $ Logger.msg ("Some locally found conversation ids were not returned by remotes" :: ByteString) . Logger.field "convIds" (show remoteNotFoundRemoteIds) @@ -591,7 +625,9 @@ getSelfMember :: Member (ErrorS ConvNotFound) r, Member (Error FederationError) r, Member TinyLog r, - Member (E.FederationAPIAccess FederatorClient) r + Member (E.FederationAPIAccess FederatorClient) r, + Member Now r, + Member NotificationSubsystem r ) => Local UserId -> Qualified ConvId -> From ca0f6b6e83cb60ba54a3ec1d82b70b4645073717 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 10 Sep 2026 12:17:33 +0200 Subject: [PATCH 18/29] WPB-28421 add an opt in policy for dropping unsupported federated notifications (#5501) * implement new unsupported version policy * changelog * roundtrip and golden test added * moved spec to correct place, ranemed tests according to lint rule --- changelog.d/6-federation/WPB-28421 | 1 + .../API/Federation/BackendNotifications.hs | 47 +++++- .../API/BackendNotificationsSpec.hs | 28 ++++ .../Wire/API/Federation/Golden/GoldenSpec.hs | 5 + .../Golden/UnsupportedVersionPolicy.hs | 26 ++++ ...portedVersionPolicy_DropIfUnsupported.json | 1 + ...t_UnsupportedVersionPolicy_KeepQueued.json | 1 + .../wire-api-federation.cabal | 2 + .../src/Wire/BackendNotificationPusher.hs | 21 ++- .../src/Wire/BackgroundWorker/Env.hs | 2 + .../Wire/BackendNotificationPusherSpec.hs | 147 ++++++++++++++++++ 11 files changed, 273 insertions(+), 8 deletions(-) create mode 100644 changelog.d/6-federation/WPB-28421 create mode 100644 libs/wire-api-federation/test/Test/Wire/API/Federation/API/BackendNotificationsSpec.hs create mode 100644 libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/UnsupportedVersionPolicy.hs create mode 100644 libs/wire-api-federation/test/golden/testObject_UnsupportedVersionPolicy_DropIfUnsupported.json create mode 100644 libs/wire-api-federation/test/golden/testObject_UnsupportedVersionPolicy_KeepQueued.json diff --git a/changelog.d/6-federation/WPB-28421 b/changelog.d/6-federation/WPB-28421 new file mode 100644 index 00000000000..6425a4060cd --- /dev/null +++ b/changelog.d/6-federation/WPB-28421 @@ -0,0 +1 @@ +Add an opt-in policy for dropping queued federation notifications when the target backend supports no compatible API version. diff --git a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs index ac1e0e03cd9..9e80eafe1ff 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs @@ -36,6 +36,7 @@ import Network.AMQP qualified as Q import Network.AMQP.Types qualified as Q import Servant import Servant.Client.Core +import Test.QuickCheck (Arbitrary (arbitrary), elements) import Wire.API.Federation.API.Common import Wire.API.Federation.Client import Wire.API.Federation.Component @@ -75,6 +76,29 @@ instance ToSchema BackendNotification where <*> bodyVersions .= maybe_ (optField "bodyVersions" schema) <*> (.requestId) .= maybe_ (optField "requestId" schema) +data UnsupportedVersionPolicy + = KeepQueued + | DropIfUnsupported + deriving stock (Eq, Show) + deriving (A.ToJSON, A.FromJSON) via (Schema UnsupportedVersionPolicy) + +instance Arbitrary UnsupportedVersionPolicy where + arbitrary = elements [KeepQueued, DropIfUnsupported] + +instance ToSchema UnsupportedVersionPolicy where + schema = + enum @Text $ + mconcat + [ element "keep_queued" KeepQueued, + element "drop_if_unsupported" DropIfUnsupported + ] + +-- Keeping the notification queued is the safe choice if representations +-- configured with different policies are accidentally combined. +instance Semigroup UnsupportedVersionPolicy where + DropIfUnsupported <> DropIfUnsupported = DropIfUnsupported + _ <> _ = KeepQueued + -- | Convert a federation endpoint to a backend notification to be enqueued to a -- RabbitMQ queue. fedNotifToBackendNotif :: @@ -104,17 +128,29 @@ fedNotifToBackendNotif rid ownDomain payload = requestId = Just rid } -newtype PayloadBundle (c :: Component) = PayloadBundle - { notifications :: NE.NonEmpty BackendNotification +data PayloadBundle (c :: Component) = PayloadBundle + { notifications :: NE.NonEmpty BackendNotification, + unsupportedVersionPolicy :: UnsupportedVersionPolicy } deriving (A.ToJSON, A.FromJSON) via (Schema (PayloadBundle c)) - deriving newtype (Semigroup) + deriving stock (Eq, Show) + +instance Semigroup (PayloadBundle c) where + bundle1 <> bundle2 = + PayloadBundle + { notifications = bundle1.notifications <> bundle2.notifications, + unsupportedVersionPolicy = bundle1.unsupportedVersionPolicy <> bundle2.unsupportedVersionPolicy + } instance (Typeable c) => ToSchema (PayloadBundle c) where schema = object $ PayloadBundle <$> notifications .= field "notifications" (nonEmptyArray schema) + <*> unsupportedVersionPolicy + .= fmap + (fromMaybe KeepQueued) + (optField "unsupportedVersionPolicy" schema) toBundle :: forall {k} (tag :: k). @@ -130,7 +166,10 @@ toBundle :: PayloadBundle (NotificationComponent k) toBundle reqId originDomain payload = let notif = fedNotifToBackendNotif @tag reqId originDomain payload - in PayloadBundle . pure $ notif + in PayloadBundle + { notifications = pure notif, + unsupportedVersionPolicy = KeepQueued + } makeBundle :: forall {k} (tag :: k) c. diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/API/BackendNotificationsSpec.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/API/BackendNotificationsSpec.hs new file mode 100644 index 00000000000..2bb12c26503 --- /dev/null +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/API/BackendNotificationsSpec.hs @@ -0,0 +1,28 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2026 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) +-- any later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Federation.API.BackendNotificationsSpec where + +import Imports +import Test.Hspec +import Test.Wire.API.Federation.API.Util (jsonRoundTrip) +import Wire.API.Federation.BackendNotifications (UnsupportedVersionPolicy (..)) + +spec :: Spec +spec = describe "UnsupportedVersionPolicy" $ do + describe "roundtrip" $ do + jsonRoundTrip @UnsupportedVersionPolicy diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs index 038f98b0d1e..80be3edb0a1 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs @@ -29,6 +29,7 @@ import Test.Wire.API.Federation.Golden.MessageSendResponse qualified as MessageS import Test.Wire.API.Federation.Golden.NewConnectionRequest qualified as NewConnectionRequest import Test.Wire.API.Federation.Golden.NewConnectionResponse qualified as NewConnectionResponse import Test.Wire.API.Federation.Golden.Runner (testObjects) +import Test.Wire.API.Federation.Golden.UnsupportedVersionPolicy qualified as UnsupportedVersionPolicy spec :: Spec spec = @@ -85,3 +86,7 @@ spec = (GetOne2OneConversationResponse.testObject_GetOne2OneConversationResponseBackendMismatch, "testObject_GetOne2OneConversationResponseBackendMismatch.json"), (GetOne2OneConversationResponse.testObject_GetOne2OneConversationResponseNotConnected, "testObject_GetOne2OneConversationResponseNotConnected.json") ] + testObjects + [ (UnsupportedVersionPolicy.testObjectUnsupportedVersionPolicyKeepQueued, "testObject_UnsupportedVersionPolicy_KeepQueued.json"), + (UnsupportedVersionPolicy.testObjectUnsupportedVersionPolicyDropIfUnsupported, "testObject_UnsupportedVersionPolicy_DropIfUnsupported.json") + ] diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/UnsupportedVersionPolicy.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/UnsupportedVersionPolicy.hs new file mode 100644 index 00000000000..bd868c808d7 --- /dev/null +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/UnsupportedVersionPolicy.hs @@ -0,0 +1,26 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2026 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Federation.Golden.UnsupportedVersionPolicy where + +import Wire.API.Federation.BackendNotifications (UnsupportedVersionPolicy (..)) + +testObjectUnsupportedVersionPolicyKeepQueued :: UnsupportedVersionPolicy +testObjectUnsupportedVersionPolicyKeepQueued = KeepQueued + +testObjectUnsupportedVersionPolicyDropIfUnsupported :: UnsupportedVersionPolicy +testObjectUnsupportedVersionPolicyDropIfUnsupported = DropIfUnsupported diff --git a/libs/wire-api-federation/test/golden/testObject_UnsupportedVersionPolicy_DropIfUnsupported.json b/libs/wire-api-federation/test/golden/testObject_UnsupportedVersionPolicy_DropIfUnsupported.json new file mode 100644 index 00000000000..ae02d02074a --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_UnsupportedVersionPolicy_DropIfUnsupported.json @@ -0,0 +1 @@ +"drop_if_unsupported" diff --git a/libs/wire-api-federation/test/golden/testObject_UnsupportedVersionPolicy_KeepQueued.json b/libs/wire-api-federation/test/golden/testObject_UnsupportedVersionPolicy_KeepQueued.json new file mode 100644 index 00000000000..82b45e1fb25 --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_UnsupportedVersionPolicy_KeepQueued.json @@ -0,0 +1 @@ +"keep_queued" diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index a451e2f01c0..5bb39296c38 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -135,6 +135,7 @@ test-suite spec -- cabal-fmt: expand test other-modules: Main + Test.Wire.API.Federation.API.BackendNotificationsSpec Test.Wire.API.Federation.API.BrigSpec Test.Wire.API.Federation.API.GalleySpec Test.Wire.API.Federation.API.Util @@ -149,6 +150,7 @@ test-suite spec Test.Wire.API.Federation.Golden.NewConnectionRequest Test.Wire.API.Federation.Golden.NewConnectionResponse Test.Wire.API.Federation.Golden.Runner + Test.Wire.API.Federation.Golden.UnsupportedVersionPolicy hs-source-dirs: test default-extensions: diff --git a/services/background-worker/src/Wire/BackendNotificationPusher.hs b/services/background-worker/src/Wire/BackendNotificationPusher.hs index cadce5c0270..0654dbd3344 100644 --- a/services/background-worker/src/Wire/BackendNotificationPusher.hs +++ b/services/background-worker/src/Wire/BackendNotificationPusher.hs @@ -207,10 +207,23 @@ pushNotification runningFlag targetDomain (msg, envelope) = do -- compute the best usable version in a notification let bestVersion = bodyVersions >=> flip latestCommonVersion remoteVersions case pairedMaximumOn bestVersion (toList (notifications bundle)) of - (_, Nothing) -> - Log.fatal $ - Log.msg (Log.val "No federation API version in common, the notification will be ignored") - . Log.field "domain" (domainText targetDomain) + (_, Nothing) -> do + metrics <- asks backendNotificationMetrics + case bundle.unsupportedVersionPolicy of + KeepQueued -> do + Log.fatal $ + Log.msg (Log.val "No federation API version in common; the notification will remain queued") + . Log.field "domain" (domainText targetDomain) + . Log.field "paths" (Text.intercalate "," $ map (.path) $ toList bundle.notifications) + withLabel metrics.stuckQueuesGauge (domainText targetDomain) (flip setGauge 1) + DropIfUnsupported -> do + Log.warn $ + Log.msg (Log.val "Dropping notification because the target backend supports no compatible federation API version") + . Log.field "domain" (domainText targetDomain) + . Log.field "paths" (Text.intercalate "," $ map (.path) $ toList bundle.notifications) + lift $ ack envelope + withLabel metrics.droppedUnsupportedVersionCounter (domainText targetDomain) incCounter + withLabel metrics.stuckQueuesGauge (domainText targetDomain) (flip setGauge 0) (notif, cveVersion) -> do ceFederator <- asks (.federatorInternal) ceHttp2Manager <- asks http2Manager diff --git a/services/background-worker/src/Wire/BackgroundWorker/Env.hs b/services/background-worker/src/Wire/BackgroundWorker/Env.hs index ed784a33db9..10886fd8332 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Env.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Env.hs @@ -118,6 +118,7 @@ data Env = Env data BackendNotificationMetrics = BackendNotificationMetrics { pushedCounter :: Vector Text Counter, + droppedUnsupportedVersionCounter :: Vector Text Counter, errorCounter :: Vector Text Counter, stuckQueuesGauge :: Vector Text Gauge } @@ -130,6 +131,7 @@ mkBackendNotificationMetrics :: IO BackendNotificationMetrics mkBackendNotificationMetrics = BackendNotificationMetrics <$> register (vector "targetDomain" $ counter $ Prometheus.Info "wire_backend_notifications_pushed" "Number of notifications pushed") + <*> register (vector "targetDomain" $ counter $ Prometheus.Info "wire_backend_notifications_dropped_unsupported_version" "Number of notifications dropped because the target backend supports no compatible federation API version") <*> register (vector "targetDomain" $ counter $ Prometheus.Info "wire_backend_notifications_errors" "Number of errors that occurred while pushing notifications") <*> register (vector "targetDomain" $ gauge $ Prometheus.Info "wire_backend_notifications_stuck_queues" "Set to 1 when pushing notifications is stuck") diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index 7222120d93a..be46a03c648 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -66,6 +66,7 @@ import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley import Wire.API.Federation.BackendNotifications +import Wire.API.Federation.Version import Wire.API.RawJson import Wire.API.Team.FeatureFlags import Wire.BackendNotificationPusher @@ -77,6 +78,17 @@ import Wire.RateLimit.Interpreter (newRateLimitEnv) spec :: Spec spec = do + describe "PayloadBundle" $ do + it "should default to keeping a notification queued when decoding a bundle without a policy" $ do + let bundle = testBundle KeepQueued (rangeFromVersion V1) + oldBundle = Aeson.object ["notifications" .= bundle.notifications] + Aeson.fromJSON @(PayloadBundle 'Brig) oldBundle `shouldBe` Aeson.Success bundle + + it "should use the safe keep-queued policy when combining bundles with different policies" $ do + let keepQueuedBundle = testBundle KeepQueued (rangeFromVersion V1) + dropBundle = testBundle DropIfUnsupported (rangeFromVersion V1) + (keepQueuedBundle <> dropBundle).unsupportedVersionPolicy `shouldBe` KeepQueued + describe "pushNotification" $ do it "should push notifications" $ do let origDomain = Domain "origin.example.com" @@ -253,6 +265,119 @@ spec = do getVectorWith env.backendNotificationMetrics.pushedCounter getCounter `shouldReturn` [(domainText targetDomain, 1)] + it "should leave an unsupported notification queued by default" $ do + let targetDomain = Domain "target.example.com" + bundle = testBundle KeepQueued (rangeFromVersion V1) + envelope <- newMockEnvelope + let msg = + Q.newMsg + { Q.msgBody = Aeson.encode bundle, + Q.msgContentType = Just "application/json" + } + runningFlag <- newMVar () + (env, fedReqs) <- + withTempMockFederator def {versions = [0]} . runTestAppT $ do + wait =<< pushNotification runningFlag targetDomain (msg, envelope) + ask + + readIORef envelope.acks `shouldReturn` 0 + readIORef envelope.rejections `shouldReturn` [] + fedReqs `shouldBe` [] + getVectorWith env.backendNotificationMetrics.droppedUnsupportedVersionCounter getCounter + `shouldReturn` [] + getVectorWith env.backendNotificationMetrics.stuckQueuesGauge getGauge + `shouldReturn` [(domainText targetDomain, 1)] + + it "should drop an unsupported notification when configured" $ do + let targetDomain = Domain "target.example.com" + bundle = testBundle DropIfUnsupported (rangeFromVersion V1) + envelope <- newMockEnvelope + let msg = + Q.newMsg + { Q.msgBody = Aeson.encode bundle, + Q.msgContentType = Just "application/json" + } + runningFlag <- newMVar () + (env, fedReqs) <- + withTempMockFederator def {versions = [0]} . runTestAppT $ do + wait =<< pushNotification runningFlag targetDomain (msg, envelope) + ask + + readIORef envelope.acks `shouldReturn` 1 + readIORef envelope.rejections `shouldReturn` [] + fedReqs `shouldBe` [] + getVectorWith env.backendNotificationMetrics.droppedUnsupportedVersionCounter getCounter + `shouldReturn` [(domainText targetDomain, 1)] + getVectorWith env.backendNotificationMetrics.stuckQueuesGauge getGauge + `shouldReturn` [(domainText targetDomain, 0)] + + it "should deliver a compatible notification with the drop policy" $ do + let targetDomain = Domain "target.example.com" + bundle = testBundle DropIfUnsupported (rangeFromVersion V1) + envelope <- newMockEnvelope + let msg = + Q.newMsg + { Q.msgBody = Aeson.encode bundle, + Q.msgContentType = Just "application/json" + } + runningFlag <- newMVar () + (env, fedReqs) <- + withTempMockFederator def {versions = [1]} . runTestAppT $ do + wait =<< pushNotification runningFlag targetDomain (msg, envelope) + ask + + readIORef envelope.acks `shouldReturn` 1 + readIORef envelope.rejections `shouldReturn` [] + fedReqs + `shouldBe` [ FederatedRequest + { frTargetDomain = targetDomain, + frOriginDomain = testOriginDomain, + frComponent = Brig, + frRPC = "unsupported-version-test", + frBody = Aeson.encode testNotificationBody + } + ] + getVectorWith env.backendNotificationMetrics.droppedUnsupportedVersionCounter getCounter + `shouldReturn` [] + + it "should retry delivery failures instead of applying the drop policy" $ do + isRemoteBrokenRef <- newIORef True + fedCalls <- newIORef (0 :: Int) + let mockRemote :: req -> IO MockResponse + mockRemote _ = do + isRemoteBroken <- readIORef isRemoteBrokenRef + atomicModifyIORef fedCalls $ \c -> (c + 1, ()) + pure $ + if isRemoteBroken + then MockResponse status200 "text/html" "down for maintenance" + else MockResponse status200 "application/json" (Aeson.encode EmptyResponse) + targetDomain = Domain "target.example.com" + bundle = testBundle DropIfUnsupported (rangeFromVersion V1) + envelope <- newMockEnvelope + let msg = + Q.newMsg + { Q.msgBody = Aeson.encode bundle, + Q.msgContentType = Just "application/json" + } + runningFlag <- newMVar () + env <- testEnv + pushThread <- + async $ withTempMockFederator def {handler = mockRemote, versions = [1]} . runTestAppTWithEnv env $ do + wait =<< pushNotification runningFlag targetDomain (msg, envelope) + + untilM $ (>= 2) <$> readIORef fedCalls + readIORef envelope.acks `shouldReturn` 0 + getVectorWith env.backendNotificationMetrics.droppedUnsupportedVersionCounter getCounter + `shouldReturn` [] + + writeIORef isRemoteBrokenRef False + void $ wait pushThread + + readIORef envelope.acks `shouldReturn` 1 + readIORef envelope.rejections `shouldReturn` [] + getVectorWith env.backendNotificationMetrics.droppedUnsupportedVersionCounter getCounter + `shouldReturn` [] + it "should reject invalid notifications" $ do envelope <- newMockEnvelope let msg = @@ -487,6 +612,28 @@ spec = do calls `shouldSatisfy` (\c -> length c >= 2) mapM_ (\vhost -> vhost `shouldBe` rabbitmqVHost) calls +testOriginDomain :: Domain +testOriginDomain = Domain "origin.example.com" + +testNotificationBody :: Aeson.Value +testNotificationBody = Aeson.object ["foo" .= ("bar" :: Text)] + +testBundle :: UnsupportedVersionPolicy -> VersionRange -> PayloadBundle 'Brig +testBundle policy versions = + PayloadBundle + { notifications = + pure + BackendNotification + { targetComponent = Brig, + ownDomain = testOriginDomain, + path = "/unsupported-version-test", + body = RawJson $ Aeson.encode testNotificationBody, + bodyVersions = Just versions, + requestId = Just $ RequestId defRequestId + }, + unsupportedVersionPolicy = policy + } + untilM :: (Monad m) => m Bool -> m () untilM action = do b <- action From d7dd619452b53224153702e9b6f3dc2301fcaa97 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 10 Sep 2026 14:46:50 +0200 Subject: [PATCH 19/29] WPB-26650 federate senderless adminless events (#5525) * implement federation support for adminless * some fixes in the implementation * formatting * updated tests with remote notification cases * add legacy federation test * changelog * filter already present users to prevent spam * cache fedaration supported state during scan * used bucketed recipients * avoid redundant per-domain grouping for system notifications * only search bounded fed api versions * simplified by removing deconstruction * replace Set.from/toList with nubOrd * removed unused parameter --- changelog.d/2-features/WPB-26650 | 1 + integration/test/Notifications.hs | 3 + integration/test/Test/AdminlessGroups.hs | 102 +++++++---- .../src/Wire/API/Federation/API.hs | 3 + .../Federation/API/Galley/Notifications.hs | 57 +++++++ .../src/Wire/API/Federation/API/Util.hs | 12 ++ .../API/Federation/HasNotificationEndpoint.hs | 4 + .../src/Wire/API/Federation/Version.hs | 13 +- .../src/Wire/ConversationSubsystem.hs | 12 ++ .../Wire/ConversationSubsystem/Federation.hs | 93 +++++++++++ .../Wire/ConversationSubsystem/Interpreter.hs | 6 + .../src/Wire/ConversationSubsystem/Notify.hs | 46 ++++- .../src/Wire/ConversationSubsystem/Update.hs | 158 ++++++++++++------ .../src/Wire/FederationAPIAccess.hs | 22 +++ services/galley/src/Galley/API/Federation.hs | 3 + 15 files changed, 454 insertions(+), 81 deletions(-) create mode 100644 changelog.d/2-features/WPB-26650 diff --git a/changelog.d/2-features/WPB-26650 b/changelog.d/2-features/WPB-26650 new file mode 100644 index 00000000000..0d4e172ee34 --- /dev/null +++ b/changelog.d/2-features/WPB-26650 @@ -0,0 +1 @@ +Add federation support for `preventAdminlessGroups` system notifications, with capability-aware handling for older remote backends. diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs index 144b9902870..339922d117a 100644 --- a/integration/test/Notifications.hs +++ b/integration/test/Notifications.hs @@ -230,6 +230,9 @@ isConvDeleteNotif n = fieldEquals n "payload.0.type" "conversation.delete" ||~ fieldEquals n "payload.0.type" "conversation.system.delete" +isConvSystemDeleteNotif :: (HasCallStack, MakesValue a) => a -> App Bool +isConvSystemDeleteNotif n = fieldEquals n "payload.0.type" "conversation.system.delete" + isNotifTeamConvDelete :: (HasCallStack, MakesValue conv, MakesValue a) => conv -> a -> App Bool isNotifTeamConvDelete conv n = isNotifConv conv n diff --git a/integration/test/Test/AdminlessGroups.hs b/integration/test/Test/AdminlessGroups.hs index 139d5439357..01884aa9d60 100644 --- a/integration/test/Test/AdminlessGroups.hs +++ b/integration/test/Test/AdminlessGroups.hs @@ -26,6 +26,7 @@ import MLS.Util import Notifications import SetupHelpers hiding (deleteUser) import Testlib.Prelude +import Testlib.VersionedFed (FedDomain) testOnLastAdminLeaveReturnEligibleMembers :: (HasCallStack) => App () testOnLastAdminLeaveReturnEligibleMembers = do @@ -349,7 +350,7 @@ testAdminlessSetupDeletesWithOriginAndRemoteMembers = do conversationIds <- resp.json %. "qualified_conversations" & asList conversationIds `shouldContain` [convQid] - withWebSockets [remoteUser] $ \[wsRemoteUser] -> do + withWebSocket remoteUser $ \wsRemoteUser -> do setTeamFeatureConfigVersioned (ExplicitVersion 18) alice tid "preventAdminlessGroups" (mkAdminlessFeature "enabled" "1s" []) >>= assertSuccess deleteNotif <- awaitMatchFor 20 isConvDeleteNotif wsRemoteUser @@ -360,10 +361,9 @@ testAdminlessSetupDeletesWithOriginAndRemoteMembers = do conversationIds <- resp.json %. "qualified_conversations" & asList conversationIds `shouldNotContain` [convQid] -testAdminlessSetupSkipsDeletionForRemoteMembers :: (HasCallStack) => App () -testAdminlessSetupSkipsDeletionForRemoteMembers = do - -- Senderless deletion is skipped when remote members are present because - -- remote backends do not support the system delete event yet. +testAdminlessSetupDeletesWithSystemEventAndRemoteMembers :: (HasCallStack) => App () +testAdminlessSetupDeletesWithSystemEventAndRemoteMembers = do + -- The integration backends support the senderless system-delete event. (alice, tid, _) <- createTeam OwnDomain 1 remoteUser <- randomUser OtherDomain def connectTwoUsers alice remoteUser @@ -375,29 +375,66 @@ testAdminlessSetupSkipsDeletionForRemoteMembers = do traverse_ (uploadNewKeyPackage def) [alice1, remoteUser1] conv <- createTeamMLSConversation alice tid alice1 [remoteUser] + convQid <- objQidObject conv -- Create an adminless conversation while the feature is disabled. Enabling -- the feature later exercises the system-triggered setup path. removeMember alice conv alice >>= assertSuccess - configureAdminlessGroupsFeature OwnDomain tid "enabled" "1s" ["1s"] + withWebSocket remoteUser $ \wsRemoteUser -> do + configureAdminlessGroupsFeature OwnDomain tid "enabled" "1s" [] - -- The setup job must not schedule deletion or reminders for this - -- conversation because it contains a remote member. - liftIO $ threadDelay 2_000_000 - bindResponse (GalleyI.getConversation conv) $ \resp -> do - resp.status `shouldMatchInt` 200 + void $ awaitMatchFor 20 isConvSystemDeleteNotif wsRemoteUser + + eventually $ bindResponse (GalleyI.getConversation conv) $ \resp -> do + resp.status `shouldMatchInt` 404 + + eventually $ bindResponse (listConversationIds remoteUser def) $ \resp -> do + resp.status `shouldMatchInt` 200 + conversationIds <- resp.json %. "qualified_conversations" & asList + conversationIds `shouldNotContain` [convQid] + +testAdminlessSetupSkipsDeletionForUnsupportedRemote :: (HasCallStack) => FedDomain 2 -> App () +testAdminlessSetupSkipsDeletionForUnsupportedRemote fedDomain = do + (alice, tid, _) <- createTeam OwnDomain 1 + remoteUser <- randomUser fedDomain def + connectTwoUsers alice remoteUser + + configureAdminlessGroupsFeature OwnDomain tid "disabled" "1s" [] + + alice1 <- createMLSClient def alice + remoteUser1 <- createMLSClient def remoteUser + traverse_ (uploadNewKeyPackage def) [alice1, remoteUser1] + + conv <- createTeamMLSConversation alice tid alice1 [remoteUser] + + -- Create an adminless conversation while the feature is disabled. The + -- setup scan must skip it because the remote backend does not support the + -- senderless system-delete notification. + removeMember alice conv alice >>= assertSuccess + + withWebSocket remoteUser $ \wsRemoteUser -> do + configureAdminlessGroupsFeature OwnDomain tid "enabled" "1s" [] + + -- Allow the setup scan and deletion worker to run, and assert that neither + -- senderless notification is emitted for the unsupported backend. + deleteResult <- awaitNMatchesResultFor 8 1 isConvSystemDeleteNotif wsRemoteUser + deleteResult.success `shouldMatch` False + reminderResult <- awaitNMatchesResultFor 8 1 isConvSystemAdminlessReminderNotif wsRemoteUser + reminderResult.success `shouldMatch` False -testAdminlessSetupSkipsReminderForRemoteMembers :: (HasCallStack) => App () -testAdminlessSetupSkipsReminderForRemoteMembers = do - -- A remote member prevents senderless deletion. The remaining local app is - -- not eligible for promotion, but would receive a system reminder if one - -- were emitted. + bindResponse (GalleyI.getConversation conv) $ \resp -> do + resp.status `shouldMatchInt` 200 + +testAdminlessSetupSendsReminderWithRemoteMembers :: (HasCallStack) => App () +testAdminlessSetupSendsReminderWithRemoteMembers = do + -- The remaining local app is not eligible for promotion and receives the + -- senderless reminder even though the conversation has a remote member. (alice, tid, _) <- createTeam OwnDomain 1 remoteUser <- randomUser OtherDomain def connectTwoUsers alice remoteUser - configureAdminlessGroupsFeature OwnDomain tid "disabled" "5s" ["4s"] + configureAdminlessGroupsFeature OwnDomain tid "disabled" "10s" ["1s"] alice1 <- createMLSClient def alice remoteUser1 <- createMLSClient def remoteUser @@ -411,11 +448,14 @@ testAdminlessSetupSkipsReminderForRemoteMembers = do -- it through the internal path runs senderless setup cleanup. removeMember alice conv alice >>= assertSuccess - withWebSockets [app] $ \[wsApp] -> do - configureAdminlessGroupsFeature OwnDomain tid "enabled" "2s" ["1s"] + withWebSockets [app, remoteUser] $ \[wsApp, wsRemoteUser] -> do + configureAdminlessGroupsFeature OwnDomain tid "enabled" "10s" ["1s"] - reminderResult <- awaitNMatchesResultFor 5 1 isConvSystemAdminlessReminderNotif wsApp - reminderResult.success `shouldMatch` False + reminder <- awaitMatchFor 20 isConvSystemAdminlessReminderNotif wsApp + reminder %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + + remoteReminder <- awaitMatchFor 20 isConvSystemAdminlessReminderNotif wsRemoteUser + remoteReminder %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv bindResponse (GalleyI.getConversation conv) $ \resp -> do resp.status `shouldMatchInt` 200 @@ -423,8 +463,8 @@ testAdminlessSetupSkipsReminderForRemoteMembers = do testAdminlessSetupAutopromotesWithRemoteMembers :: (HasCallStack) => App () testAdminlessSetupAutopromotesWithRemoteMembers = do -- Autopromotion is safe with remote members because the owning backend is - -- authoritative for roles, even though remote clients do not receive the - -- senderless system member-update event yet. + -- authoritative for roles. The remote member receives the senderless + -- system member-update event. (alice, tid, [bob]) <- createTeam OwnDomain 2 remoteUser <- randomUser OtherDomain def connectTwoUsers alice remoteUser @@ -442,12 +482,18 @@ testAdminlessSetupAutopromotesWithRemoteMembers = do -- the feature later promotes Bob through a system action. removeMember alice conv alice >>= assertSuccess - configureAdminlessGroupsFeature OwnDomain tid "enabled" "1s" [] + withWebSocket remoteUser $ \wsRemoteUser -> do + configureAdminlessGroupsFeature OwnDomain tid "enabled" "1s" [] - liftIO $ threadDelay 2_000_000 - bindResponse (getConversation bob conv) $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "members.self.conversation_role" `shouldMatch` "wire_admin" + liftIO $ threadDelay 2_000_000 + bindResponse (getConversation bob conv) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "members.self.conversation_role" `shouldMatch` "wire_admin" + + memberUpdate <- awaitMatchFor 20 isConvSystemMemberUpdateNotif wsRemoteUser + memberUpdate %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + memberUpdate %. "payload.0.data.qualified_target" `shouldMatch` objQidObject bob + memberUpdate %. "payload.0.data.conversation_role" `shouldMatch` "wire_admin" testAdminlessJobsCancelledOnFeatureDisable :: (HasCallStack) => App () testAdminlessJobsCancelledOnFeatureDisable = do diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API.hs b/libs/wire-api-federation/src/Wire/API/Federation/API.hs index 704d8a3a0a0..e0934edc118 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API.hs @@ -34,6 +34,9 @@ module Wire.API.Federation.API -- * Re-exports Component (..), makeConversationUpdateBundle, + makeSystemMemberUpdateBundle, + makeSystemDeleteBundle, + makeSystemAdminlessReminderBundle, ) where diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs index 9fa5cd6b8f4..0e91ac2790a 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs @@ -32,6 +32,7 @@ import Imports import Servant.API import Wire.API.Conversation import Wire.API.Conversation.Action +import Wire.API.Event.Conversation (AdminlessReminder, MemberUpdateData) import Wire.API.Federation.Component import Wire.API.Federation.Endpoint import Wire.API.Federation.HasNotificationEndpoint @@ -49,6 +50,9 @@ data GalleyNotificationTag | OnConversationUpdatedTagV0 | OnConversationUpdatedTag | OnUserDeletedConversationsTag + | OnSystemMemberUpdateTag + | OnSystemDeleteTag + | OnSystemAdminlessReminderTag deriving (Show, Eq, Generic, Bounded, Enum) instance IsNotificationTag GalleyNotificationTag where @@ -85,6 +89,21 @@ instance HasNotificationEndpoint 'OnUserDeletedConversationsTag where type Payload 'OnUserDeletedConversationsTag = UserDeletedConversationsNotification type NotificationPath 'OnUserDeletedConversationsTag = "on-user-deleted-conversations" +instance HasNotificationEndpoint 'OnSystemMemberUpdateTag where + type Payload 'OnSystemMemberUpdateTag = SystemMemberUpdateNotification + type NotificationPath 'OnSystemMemberUpdateTag = "on-conversation-system-member-update" + type NotificationMods 'OnSystemMemberUpdateTag = '[From 'V4] + +instance HasNotificationEndpoint 'OnSystemDeleteTag where + type Payload 'OnSystemDeleteTag = SystemDeleteNotification + type NotificationPath 'OnSystemDeleteTag = "on-conversation-system-delete" + type NotificationMods 'OnSystemDeleteTag = '[From 'V4] + +instance HasNotificationEndpoint 'OnSystemAdminlessReminderTag where + type Payload 'OnSystemAdminlessReminderTag = SystemAdminlessReminderNotification + type NotificationPath 'OnSystemAdminlessReminderTag = "on-conversation-system-adminless-reminder" + type NotificationMods 'OnSystemAdminlessReminderTag = '[From 'V4] + -- | All the notification endpoints return an 'EmptyResponse'. type GalleyNotificationAPI = NotificationFedEndpoint 'OnClientRemovedTag @@ -93,6 +112,9 @@ type GalleyNotificationAPI = :<|> NotificationFedEndpoint 'OnConversationUpdatedTagV0 :<|> NotificationFedEndpoint 'OnConversationUpdatedTag :<|> NotificationFedEndpoint 'OnUserDeletedConversationsTag + :<|> NotificationFedEndpoint 'OnSystemMemberUpdateTag + :<|> NotificationFedEndpoint 'OnSystemDeleteTag + :<|> NotificationFedEndpoint 'OnSystemAdminlessReminderTag data ClientRemovedRequest = ClientRemovedRequest { user :: UserId, @@ -190,6 +212,41 @@ instance FromJSON ConversationUpdate instance ToSchema ConversationUpdate +data SystemMemberUpdateNotification = SystemMemberUpdateNotification + { time :: UTCTime, + conversation :: ConvId, + update :: MemberUpdateData, + alreadyPresentUsers :: [UserId] + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform SystemMemberUpdateNotification) + deriving (ToJSON, FromJSON) via (CustomEncoded SystemMemberUpdateNotification) + +instance ToSchema SystemMemberUpdateNotification + +data SystemDeleteNotification = SystemDeleteNotification + { time :: UTCTime, + conversation :: ConvId, + alreadyPresentUsers :: [UserId] + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform SystemDeleteNotification) + deriving (ToJSON, FromJSON) via (CustomEncoded SystemDeleteNotification) + +instance ToSchema SystemDeleteNotification + +data SystemAdminlessReminderNotification = SystemAdminlessReminderNotification + { time :: UTCTime, + conversation :: ConvId, + reminder :: AdminlessReminder, + alreadyPresentUsers :: [UserId] + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform SystemAdminlessReminderNotification) + deriving (ToJSON, FromJSON) via (CustomEncoded SystemAdminlessReminderNotification) + +instance ToSchema SystemAdminlessReminderNotification + conversationUpdateToV0 :: ConversationUpdate -> ConversationUpdateV0 conversationUpdateToV0 cu = ConversationUpdateV0 diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Util.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Util.hs index d855c2abb01..61afbcf27f2 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Util.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Util.hs @@ -27,3 +27,15 @@ makeConversationUpdateBundle :: FedQueueClient 'Galley (PayloadBundle 'Galley) makeConversationUpdateBundle update = (<>) <$> makeBundle update <*> makeBundle (conversationUpdateToV0 update) + +makeSystemMemberUpdateBundle :: SystemMemberUpdateNotification -> FedQueueClient 'Galley (PayloadBundle 'Galley) +makeSystemMemberUpdateBundle = + fmap (\bundle -> bundle {unsupportedVersionPolicy = DropIfUnsupported}) . makeBundle @'OnSystemMemberUpdateTag + +makeSystemDeleteBundle :: SystemDeleteNotification -> FedQueueClient 'Galley (PayloadBundle 'Galley) +makeSystemDeleteBundle = + fmap (\bundle -> bundle {unsupportedVersionPolicy = DropIfUnsupported}) . makeBundle @'OnSystemDeleteTag + +makeSystemAdminlessReminderBundle :: SystemAdminlessReminderNotification -> FedQueueClient 'Galley (PayloadBundle 'Galley) +makeSystemAdminlessReminderBundle = + fmap (\bundle -> bundle {unsupportedVersionPolicy = DropIfUnsupported}) . makeBundle @'OnSystemAdminlessReminderTag diff --git a/libs/wire-api-federation/src/Wire/API/Federation/HasNotificationEndpoint.hs b/libs/wire-api-federation/src/Wire/API/Federation/HasNotificationEndpoint.hs index cbc16a0d769..63410850c85 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/HasNotificationEndpoint.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/HasNotificationEndpoint.hs @@ -21,6 +21,7 @@ module Wire.API.Federation.HasNotificationEndpoint HasFedPath, HasVersionRange, fedPath, + supportsNotificationVersion, versionRange, ) where @@ -90,3 +91,6 @@ instance {-# OVERLAPPABLE #-} (MkVersionRange mods) => MkVersionRange (m ': mods -- | The federation API version range this endpoint is supported in. versionRange :: forall t. (HasVersionRange t) => VersionRange versionRange = mkVersionRange @(NotificationMods t) + +supportsNotificationVersion :: forall t. (HasVersionRange t) => VersionInfo -> Bool +supportsNotificationVersion = supportsVersionRange (versionRange @t) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Version.hs b/libs/wire-api-federation/src/Wire/API/Federation/Version.hs index 5e2f016901a..f9a6136f393 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Version.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Version.hs @@ -24,9 +24,11 @@ module Wire.API.Federation.Version V1Sym0, V2Sym0, V3Sym0, + V4Sym0, intToVersion, versionInt, versionText, + supportsVersionRange, supportedVersions, VersionInfo (..), versionInfo, @@ -54,7 +56,7 @@ import Imports import Servant.API (ToHttpApiData (..)) import Wire.API.MLS.Group.Serialisation -data Version = V0 | V1 | V2 | V3 +data Version = V0 | V1 | V2 | V3 | V4 deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) deriving (FromJSON, ToJSON) via (Schema Version) @@ -67,15 +69,19 @@ versionInt V0 = 0 versionInt V1 = 1 versionInt V2 = 2 versionInt V3 = 3 +versionInt V4 = 4 versionText :: Version -> Text versionText = ("v" <>) . Text.pack . show . versionInt +supportsVersionRange :: VersionRange -> VersionInfo -> Bool +supportsVersionRange range = any (maybe False (inVersionRange range) . intToVersion) . (.vinfoSupported) + versionByteString :: Version -> ByteString versionByteString = ("v" <>) . BS.pack . show . versionInt intToVersion :: Int -> Maybe Version -intToVersion intV = find (\v -> versionInt v == intV) [minBound ..] +intToVersion intV = find (\v -> versionInt v == intV) [minBound .. maxBound] instance ToSchema Version where schema = @@ -83,7 +89,8 @@ instance ToSchema Version where [ element 0 V0, element 1 V1, element 2 V2, - element 3 V3 + element 3 V3, + element 4 V4 ] supportedVersions :: Set Version diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs index 56b01784b8f..3002ff3b013 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs @@ -304,6 +304,18 @@ data ConversationSubsystem m a where Domain -> ConversationUpdate -> ConversationSubsystem m EmptyResponse + FederationOnSystemMemberUpdate :: + Domain -> + SystemMemberUpdateNotification -> + ConversationSubsystem m EmptyResponse + FederationOnSystemDelete :: + Domain -> + SystemDeleteNotification -> + ConversationSubsystem m EmptyResponse + FederationOnSystemAdminlessReminder :: + Domain -> + SystemAdminlessReminderNotification -> + ConversationSubsystem m EmptyResponse FederationOnUserDeleted :: Domain -> UserDeletedConversationsNotification -> diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs index df14feaa080..04143201e53 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs @@ -87,6 +87,7 @@ import Wire.ConversationSubsystem.MLS.SubConversation hiding (leaveSubConversati import Wire.ConversationSubsystem.MLS.Util import Wire.ConversationSubsystem.MLS.Welcome import Wire.ConversationSubsystem.Message +import Wire.ConversationSubsystem.Notify (pushSystemEvent) import Wire.ConversationSubsystem.Util import Wire.ExternalAccess (ExternalAccess) import Wire.FeaturesConfigSubsystem @@ -215,6 +216,98 @@ onConversationUpdated requestingDomain cu = do void $ updateLocalStateOfRemoteConv rcu Nothing pure EmptyResponse +onSystemMemberUpdate :: + ( Member E.ConversationStore r, + Member NotificationSubsystem r, + Member P.TinyLog r + ) => + Domain -> + SystemMemberUpdateNotification -> + Sem r EmptyResponse +onSystemMemberUpdate requestingDomain e = do + localMembers <- filterSystemNotificationRecipients requestingDomain e.conversation e.alreadyPresentUsers + pushSystemEvent + Nothing + ( SystemEvent + (Qualified e.conversation requestingDomain) + Nothing + e.time + Nothing + (EdSystemMemberUpdate e.update) + ) + (Set.fromList localMembers) + pure EmptyResponse + +onSystemDelete :: + ( Member E.ConversationStore r, + Member NotificationSubsystem r, + Member P.TinyLog r + ) => + Domain -> + SystemDeleteNotification -> + Sem r EmptyResponse +onSystemDelete requestingDomain e = do + let rconvId = toRemoteUnsafe requestingDomain e.conversation + localMembers <- filterSystemNotificationRecipients requestingDomain e.conversation e.alreadyPresentUsers + E.deleteMembersInRemoteConversation rconvId localMembers + pushSystemEvent + Nothing + ( SystemEvent + (Qualified e.conversation requestingDomain) + Nothing + e.time + Nothing + EdSystemConvDelete + ) + (Set.fromList localMembers) + pure EmptyResponse + +onSystemAdminlessReminder :: + ( Member E.ConversationStore r, + Member NotificationSubsystem r, + Member P.TinyLog r + ) => + Domain -> + SystemAdminlessReminderNotification -> + Sem r EmptyResponse +onSystemAdminlessReminder requestingDomain notification = do + localMembers <- + filterSystemNotificationRecipients + requestingDomain + notification.conversation + notification.alreadyPresentUsers + pushSystemEvent + Nothing + ( SystemEvent + (Qualified notification.conversation requestingDomain) + Nothing + notification.time + Nothing + (EdSystemAdminlessReminder notification.reminder) + ) + (Set.fromList localMembers) + pure EmptyResponse + +filterSystemNotificationRecipients :: + ( Member E.ConversationStore r, + Member P.TinyLog r + ) => + Domain -> + ConvId -> + [UserId] -> + Sem r [UserId] +filterSystemNotificationRecipients requestingDomain conversation users = do + let rconvId = toRemoteUnsafe requestingDomain conversation + (members, allMembers) <- E.selectRemoteMembers users rconvId + unless allMembers $ + P.warn $ + Log.field "conversation" (toByteString' conversation) + Log.~~ Log.field "domain" (toByteString' requestingDomain) + Log.~~ Log.field "users" (show users) + Log.~~ Log.msg + ("Federated system notification contained users that are not members of the conversation" :: ByteString) + pure members + -- as of now this will not generate the necessary events on the leaver's domain leaveConversation :: ( Member BackendNotificationQueueAccess r, diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs index 651d6c775e8..06f9c8a5ae5 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs @@ -201,6 +201,12 @@ interpretConversationSubsystem = interpret $ \case mapErrors $ Federation.onMLSMessageSent domain rmm FederationOnConversationUpdated domain cu -> mapErrors $ Federation.onConversationUpdated domain cu + FederationOnSystemMemberUpdate domain notification -> + mapErrors $ Federation.onSystemMemberUpdate domain notification + FederationOnSystemDelete domain notification -> + mapErrors $ Federation.onSystemDelete domain notification + FederationOnSystemAdminlessReminder domain notification -> + mapErrors $ Federation.onSystemAdminlessReminder domain notification FederationOnUserDeleted domain udcn -> mapErrors $ Federation.onUserDeleted domain udcn PostOtrMessageUnqualified lusr con cnv ignore report msg -> diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Notify.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Notify.hs index e40ea62e46f..2e3f65227b1 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Notify.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Notify.hs @@ -18,6 +18,9 @@ module Wire.ConversationSubsystem.Notify ( notifyConversationActionImpl, pushSystemEvent, + sendSystemMemberUpdate, + sendSystemDelete, + sendSystemAdminlessReminder, ) where @@ -34,8 +37,8 @@ import Wire.API.Conversation hiding (Member) import Wire.API.Conversation qualified as Public import Wire.API.Conversation.Action import Wire.API.Event.Conversation -import Wire.API.Federation.API (makeConversationUpdateBundle, sendBundle) -import Wire.API.Federation.API.Galley.Notifications (ConversationUpdate (..)) +import Wire.API.Federation.API +import Wire.API.Federation.API.Galley.Notifications import Wire.API.Federation.Error import Wire.BackendNotificationQueueAccess (BackendNotificationQueueAccess, enqueueNotificationsConcurrently) import Wire.ConversationSubsystem.Util @@ -120,3 +123,42 @@ pushSystemEvent con event targets = do isCellsEvent = True } ] + +sendSystemMemberUpdate :: + ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r + ) => + Set (Remote UserId) -> + SystemMemberUpdateNotification -> + Sem r () +sendSystemMemberUpdate targets n = void $ + enqueueNotificationsConcurrently Q.Persistent (toList targets) $ \ruids -> + makeSystemMemberUpdateBundle + (SystemMemberUpdateNotification n.time n.conversation n.update (tUnqualified ruids)) + >>= sendBundle + +sendSystemDelete :: + ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r + ) => + Set (Remote UserId) -> + SystemDeleteNotification -> + Sem r () +sendSystemDelete targets n = void $ + enqueueNotificationsConcurrently Q.Persistent (toList targets) $ \ruids -> + makeSystemDeleteBundle + (SystemDeleteNotification n.time n.conversation (tUnqualified ruids)) + >>= sendBundle + +sendSystemAdminlessReminder :: + ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r + ) => + Set (Remote UserId) -> + SystemAdminlessReminderNotification -> + Sem r () +sendSystemAdminlessReminder targets n = void $ + enqueueNotificationsConcurrently Q.Persistent (toList targets) $ \ruids -> + makeSystemAdminlessReminderBundle + (SystemAdminlessReminderNotification n.time n.conversation n.reminder (tUnqualified ruids)) + >>= sendBundle diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs index 78474964d8f..b6d079602e9 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs @@ -83,6 +83,7 @@ import Data.Code import Data.Default import Data.Id import Data.Json.Util +import Data.List.Extra (nubOrd) import Data.List.NonEmpty (NonEmpty (..), appendList, nonEmpty) import Data.Map.Strict qualified as Map import Data.Misc @@ -96,6 +97,7 @@ import Imports hiding (forkIO) import Polysemy import Polysemy.Error import Polysemy.Input +import Polysemy.State (evalState, get, modify) import Polysemy.TinyLog import System.Logger qualified as Log import Wire.API.Bot hiding (addBot) @@ -1196,17 +1198,32 @@ isAdminlessCheckCandidate conv = conv.metadata.cnvmType == RegularConv && maybe True (== GroupConversation) conv.metadata.cnvmGroupConvType -shouldSkipSystemAdminlessDeletion :: Maybe (Local UserId) -> StoredConversation -> Bool -shouldSkipSystemAdminlessDeletion mlusr conv = - isNothing mlusr - && not (null conv.remoteMembers) +systemAdminlessDeletionSupported :: + (Member (E.FederationAPIAccess FederatorClient) r) => + StoredConversation -> + Sem r Bool +systemAdminlessDeletionSupported conv = + E.allRemoteBackendsSupportNotification @'OnSystemDeleteTag + (remoteBackendsForConversation conv) + +remoteBackendsForConversation :: StoredConversation -> [Remote ()] +remoteBackendsForConversation conv = + nubOrd $ + [ toRemoteUnsafe (tDomain member.id_) () + | member <- conv.remoteMembers + ] -logSkippedSystemAdminlessDeletion :: (Member TinyLog r) => Text -> StoredConversation -> Sem r () +logSkippedSystemAdminlessDeletion :: + (Member TinyLog r) => + Text -> + StoredConversation -> + Sem r () logSkippedSystemAdminlessDeletion action conv = info $ - Log.msg (Log.val "Skipping senderless adminless deletion for conversation with remote members") + Log.msg (Log.val "Skipping system-triggered adminless deletion") . Log.field "conversation_id" (idToText conv.id_) . Log.field "action" action + . Log.field "reason" (Log.val "remote backend does not support system delete") setupAdminlessGroupsCleanup :: ( Member ConversationStore r, @@ -1221,19 +1238,40 @@ setupAdminlessGroupsCleanup :: Member FeaturesConfigSubsystem r, Member (Input (Local ())) r, Member JobSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (E.FederationAPIAccess FederatorClient) r ) => Maybe (Local UserId) -> TeamId -> Sem r () setupAdminlessGroupsCleanup mUsr tid = do - teamConvIds <- E.getTeamConversations tid - for_ teamConvIds $ \cnv -> do - lcnv <- qualifyLocal cnv - adminlessTryAutopromote mUsr lcnv $ \conv feature _ -> - if shouldSkipSystemAdminlessDeletion mUsr conv - then logSkippedSystemAdminlessDeletion "schedule_for_deletion" conv - else scheduleDeletion lcnv mUsr tid feature + evalState mempty $ do + teamConvIds <- E.getTeamConversations tid + for_ teamConvIds $ \cnv -> do + lcnv <- qualifyLocal cnv + adminlessTryAutopromote mUsr lcnv $ \conv feature -> do + supported <- + if isNothing mUsr + then systemAdminlessDeletionSupportedCached conv + else pure True + if supported + then scheduleDeletion lcnv mUsr tid feature + else logSkippedSystemAdminlessDeletion "scan" conv + where + systemAdminlessDeletionSupportedCached conv = + and + <$> for + (remoteBackendsForConversation conv) + ( \remoteBackend -> do + cached <- get + case Map.lookup (tDomain remoteBackend) cached of + Just supported -> pure supported + Nothing -> do + supported <- + E.allRemoteBackendsSupportNotification @'OnSystemDeleteTag [remoteBackend] + modify (Map.insert (tDomain remoteBackend) supported) + pure supported + ) guardPreventAdminlessGroups :: ( Member ConversationStore r, @@ -1370,7 +1408,7 @@ adminlessTryAutopromote :: ) => Maybe (Local UserId) -> Local ConvId -> - (StoredConversation -> LockableFeature PreventAdminlessGroupsConfig -> [(Qualified UserId, User.Name)] -> Sem r ()) -> + (StoredConversation -> LockableFeature PreventAdminlessGroupsConfig -> Sem r ()) -> Sem r () adminlessTryAutopromote mlusr lcnv altAction = do conv <- getConversationWithError lcnv @@ -1400,6 +1438,14 @@ adminlessTryAutopromote mlusr lcnv altAction = do def Nothing -> do now <- Now.get + Notify.sendSystemMemberUpdate + (Set.fromList (map (.id_) conv.remoteMembers)) + SystemMemberUpdateNotification + { time = now, + conversation = tUnqualified lcnv, + update = memberUpdateData candidate update, + alreadyPresentUsers = [] + } Notify.pushSystemEvent Nothing ( SystemEvent @@ -1410,7 +1456,7 @@ adminlessTryAutopromote mlusr lcnv altAction = do (EdSystemMemberUpdate (memberUpdateData candidate update)) ) (Set.fromList (map (.id_) conv.localMembers)) - [] -> altAction conv feature eligibleMembers + [] -> altAction conv feature where memberUpdateData candidate memberUpdate' = MemberUpdateData @@ -1437,6 +1483,7 @@ adminlessAutopromoteOrDelete :: Member FeaturesConfigSubsystem r, Member ProposalStore r, Member CodeStore r, + Member (E.FederationAPIAccess FederatorClient) r, Member TinyLog r ) => Maybe (Local UserId) -> @@ -1444,10 +1491,13 @@ adminlessAutopromoteOrDelete :: Sem r () adminlessAutopromoteOrDelete mlusr lcnv = adminlessTryAutopromote mlusr lcnv orAlternativelyDeleteConv where - orAlternativelyDeleteConv conv _ _ = - if shouldSkipSystemAdminlessDeletion mlusr conv - then logSkippedSystemAdminlessDeletion "deletion" conv - else do + orAlternativelyDeleteConv conv _ = do + canDelete <- + if isNothing mlusr && not (null conv.remoteMembers) + then systemAdminlessDeletionSupported conv + else pure True + if canDelete + then do removeConversation (qualifyAs lcnv conv) case mlusr of Just lusr -> @@ -1463,10 +1513,18 @@ adminlessAutopromoteOrDelete mlusr lcnv = adminlessTryAutopromote mlusr lcnv orA def Nothing -> do now <- Now.get + Notify.sendSystemDelete + (Set.fromList (map (.id_) conv.remoteMembers)) + SystemDeleteNotification + { time = now, + conversation = tUnqualified lcnv, + alreadyPresentUsers = [] + } Notify.pushSystemEvent Nothing (SystemEvent (tUntagged lcnv) Nothing now conv.metadata.cnvmTeam EdSystemConvDelete) (Set.fromList (map (.id_) conv.localMembers)) + else logSkippedSystemAdminlessDeletion "deletion" conv adminlessAutopromoteOrSendReminder :: ( Member ConversationStore r, @@ -1478,8 +1536,7 @@ adminlessAutopromoteOrSendReminder :: Member Now r, Member E.ExternalAccess r, Member BackendNotificationQueueAccess r, - Member FeaturesConfigSubsystem r, - Member TinyLog r + Member FeaturesConfigSubsystem r ) => Maybe (Local UserId) -> Local ConvId -> @@ -1487,33 +1544,38 @@ adminlessAutopromoteOrSendReminder :: Sem r () adminlessAutopromoteOrSendReminder mlusr lcnv deletionScheduledFor = adminlessTryAutopromote mlusr lcnv orAlternativelySendReminder where - orAlternativelySendReminder conv _ _ = - if shouldSkipSystemAdminlessDeletion mlusr conv - then logSkippedSystemAdminlessDeletion "reminder" conv - else do - now <- Now.get - case mlusr of - Just lusr -> do - let event = - Event - (tUntagged lcnv) - Nothing - (EventFromUser (tUntagged lusr)) - now - (conv.metadata.cnvmTeam) - (EdAdminlessReminder (AdminlessReminder deletionScheduledFor)) - pushConversationEvent Nothing conv event (qualifyAs lcnv (map (.id_) conv.localMembers)) [] - Nothing -> - Notify.pushSystemEvent + orAlternativelySendReminder conv _ = do + now <- Now.get + case mlusr of + Just lusr -> do + let event = + Event + (tUntagged lcnv) + Nothing + (EventFromUser (tUntagged lusr)) + now + (conv.metadata.cnvmTeam) + (EdAdminlessReminder (AdminlessReminder deletionScheduledFor)) + pushConversationEvent Nothing conv event (qualifyAs lcnv (map (.id_) conv.localMembers)) [] + Nothing -> do + Notify.sendSystemAdminlessReminder + (Set.fromList (map (.id_) conv.remoteMembers)) + SystemAdminlessReminderNotification + { time = now, + conversation = tUnqualified lcnv, + reminder = AdminlessReminder deletionScheduledFor, + alreadyPresentUsers = [] + } + Notify.pushSystemEvent + Nothing + ( SystemEvent + (tUntagged lcnv) Nothing - ( SystemEvent - (tUntagged lcnv) - Nothing - now - conv.metadata.cnvmTeam - (EdSystemAdminlessReminder (AdminlessReminder deletionScheduledFor)) - ) - (Set.fromList (map (.id_) conv.localMembers)) + now + conv.metadata.cnvmTeam + (EdSystemAdminlessReminder (AdminlessReminder deletionScheduledFor)) + ) + (Set.fromList (map (.id_) conv.localMembers)) -- Use eight random bytes and fold them into a big-endian Word64. This keeps -- the helper small, deterministic under tests, and free of extra Random API. diff --git a/libs/wire-subsystems/src/Wire/FederationAPIAccess.hs b/libs/wire-subsystems/src/Wire/FederationAPIAccess.hs index 6476cc08de5..2fc3d3d60f8 100644 --- a/libs/wire-subsystems/src/Wire/FederationAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/FederationAPIAccess.hs @@ -35,6 +35,7 @@ import Servant.Client.Core.RunClient (RunClient) import Wire.API.Federation.API import Wire.API.Federation.Component import Wire.API.Federation.Error +import Wire.API.Federation.HasNotificationEndpoint type HasBrigFederationAccess m r = ( Member (FederationAPIAccess m) r, @@ -88,3 +89,24 @@ runFederatedConcurrently :: runFederatedConcurrently rx c = do results <- runFederatedConcurrentlyEither rx c fromEither $ mapLeft snd $ sequence results + +allRemoteBackendsSupportNotification :: + forall tag fedM f x r. + ( Member (FederationAPIAccess fedM) r, + HasVersionRange tag, + RunClient (fedM 'Brig), + FederationMonad fedM, + Typeable fedM, + Foldable f, + Functor f + ) => + f (Remote x) -> + Sem r Bool +allRemoteBackendsSupportNotification remoteBackends = do + results <- + runFederatedConcurrentlyEither remoteBackends $ \_ -> + fedClient @'Brig @"api-version" () + pure $ all supports results + where + supports (Right versionInfo) = supportsNotificationVersion @tag (tUnqualified versionInfo) + supports (Left _) = False diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index d1e47cae92a..a3d98e02464 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -60,6 +60,9 @@ federationSitemap = :<|> Named @(Versioned 'V0 "on-conversation-updated") onConversationUpdatedV0 :<|> Named @"on-conversation-updated" federationOnConversationUpdated :<|> Named @"on-user-deleted-conversations" federationOnUserDeleted + :<|> Named @"on-conversation-system-member-update" federationOnSystemMemberUpdate + :<|> Named @"on-conversation-system-delete" federationOnSystemDelete + :<|> Named @"on-conversation-system-adminless-reminder" federationOnSystemAdminlessReminder onConversationUpdatedV0 :: (Member ConversationSubsystem r) => From f57865f042ef16d2e7097e8548893da0939da939 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Thu, 10 Sep 2026 18:41:02 +0200 Subject: [PATCH 20/29] WPB-28483: remove serial per-user round trips from MLS commit-bundle path (#5528) --- changelog.d/5-internal/WPB-28483 | 1 + libs/galley-types/src/Galley/Types/Error.hs | 2 +- .../Wire/ConversationSubsystem/Federation.hs | 6 +- .../Wire/ConversationSubsystem/Interpreter.hs | 4 +- .../ConversationSubsystem/MLS/CheckClients.hs | 43 ++++- .../ConversationSubsystem/MLS/Commit/Core.hs | 45 +++-- .../MLS/Commit/InternalCommit.hs | 43 +++-- .../Wire/ConversationSubsystem/MLS/Message.hs | 13 +- .../ConversationSubsystem/MLS/Proposal.hs | 18 +- .../Wire/ConversationSubsystem/MLS/Welcome.hs | 14 +- .../MLS/CheckClientsSpec.hs | 169 ++++++++++++++++++ libs/wire-subsystems/wire-subsystems.cabal | 1 + 12 files changed, 300 insertions(+), 59 deletions(-) create mode 100644 changelog.d/5-internal/WPB-28483 create mode 100644 libs/wire-subsystems/test/unit/Wire/ConversationSubsystem/MLS/CheckClientsSpec.hs diff --git a/changelog.d/5-internal/WPB-28483 b/changelog.d/5-internal/WPB-28483 new file mode 100644 index 00000000000..99c53983960 --- /dev/null +++ b/changelog.d/5-internal/WPB-28483 @@ -0,0 +1 @@ +MLS commit-bundles are processed with less sequential I/O: proposal references are resolved from a single store read, client and client-store updates fan out concurrently, and welcome pushes no longer block the response. diff --git a/libs/galley-types/src/Galley/Types/Error.hs b/libs/galley-types/src/Galley/Types/Error.hs index 51a7223c868..2c63d7fa28a 100644 --- a/libs/galley-types/src/Galley/Types/Error.hs +++ b/libs/galley-types/src/Galley/Types/Error.hs @@ -42,7 +42,7 @@ data InternalError | NoPrekeyForUser | CannotCreateManagedConv | InternalErrorWithDescription LText - deriving (Eq) + deriving (Eq, Show) internalErrorDescription :: InternalError -> LText internalErrorDescription = message . internalErrorToWai diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs index 04143201e53..93e4aa44d97 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs @@ -41,6 +41,7 @@ import Galley.Types.Error import Imports import Network.Wai.Utilities.Exception import Polysemy +import Polysemy.Async (Async) import Polysemy.Error import Polysemy.Input import Polysemy.Internal.Kind (Append) @@ -696,7 +697,8 @@ sendMLSCommitBundle :: Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, Member FeaturesConfigSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member (Input ConversationSubsystemConfig) r, + Member Async r ) => Domain -> MLSMessageSendRequest -> @@ -984,8 +986,6 @@ onMLSMessageSent domain rmm = mlsSendWelcome :: ( Member (Error InternalError) r, Member NotificationSubsystem r, - Member ExternalAccess r, - Member P.TinyLog r, Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member (Input (Local ())) r, Member Now r diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs index 06f9c8a5ae5..5adbbcb9305 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs @@ -29,6 +29,7 @@ import Data.Qualified import Imports import Network.Wai.Utilities.JSONResponse (JSONResponse) import Polysemy +import Polysemy.Async (Async) import Polysemy.Error import Polysemy.Input import Polysemy.Resource (Resource) @@ -123,7 +124,8 @@ interpretConversationSubsystem :: Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member UserClientIndexStore r, Member (Input FanoutLimit) r, - Member TinyLog r + Member TinyLog r, + Member Async r ) => InterpreterFor ConversationSubsystem r interpretConversationSubsystem = interpret $ \case diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/CheckClients.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/CheckClients.hs index 388d6a16422..0581fec3bb9 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/CheckClients.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/CheckClients.hs @@ -29,8 +29,11 @@ import Data.Map qualified as Map import Data.Qualified import Data.Set qualified as Set import Data.Tuple.Extra +import Galley.Types.Error (InternalError (..)) import Imports import Polysemy +import Polysemy.Async (Async) +import Polysemy.Async qualified as P import Polysemy.Error import Wire.API.Error import Wire.API.Error.Galley @@ -50,7 +53,9 @@ checkClients :: Member (FederationAPIAccess FederatorClient) r, Member (ErrorS MLSClientMismatch) r, Member (ErrorS MLSIdentityMismatch) r, - Member (Error MLSProtocolError) r + Member (Error MLSProtocolError) r, + Member (Error InternalError) r, + Member Async r ) => Local ConvOrSubConv -> CipherSuiteTag -> @@ -59,9 +64,35 @@ checkClients :: checkClients lConvOrSub ciphersuite newCM = do let convOrSub = tUnqualified lConvOrSub cm = convOrSub.members - fmap catMaybes . forM (Map.assocs (unClientMap newCM)) $ - \(qtarget, newclients) -> do - mClientData <- getClientData lConvOrSub ciphersuite qtarget + assocs = Map.assocs (unClientMap newCM) + -- Fetch client data from brig concurrently. getClientData is total with + -- respect to 'FederationError' (hushed inside getClientData): an inner + -- 'Nothing' is a legitimate "user unreachable" result. + -- + -- sequenceConcurrently attaches an outer 'Maybe' to every child result. + -- Under galley's production stack (asyncToIOFinal below pure + -- runError/mapError interpreters, cf. Galley.App), an 'Error'-effect + -- thrown inside a spawned child (e.g. RpcException/ParseException from + -- interpretBrigAccess) is forwarded by the in-thread mapError handlers + -- to the residual error, whose interpreter sits outside the async + -- boundary; Polysemy collapses the child result to 'Nothing'. That is a + -- crashed child, not "no client data", and must abort the commit with + -- an internal error instead of being conflated with the unreachable + -- case. + -- + -- Validation below runs serially so that Error-effect throws abort the + -- whole commit exactly as in the fully serial implementation. + mClientDatas <- + P.sequenceConcurrently $ + flip fmap assocs $ \(qtarget, _) -> + getClientData lConvOrSub ciphersuite qtarget + clientDatas <- + forM mClientDatas $ + maybe + (throw (InternalErrorWithDescription "Concurrent brig client-data fetch failed while processing commit")) + pure + fmap catMaybes . forM (zip assocs clientDatas) $ + \((qtarget, newclients), mClientData) -> do unreachable <- case (mClientData, cmLookup qtarget cm) of -- user is already present, skip check in this case (_, Just existingClients) -> do @@ -103,9 +134,9 @@ checkClients lConvOrSub ciphersuite newCM = do pure False -- Check that new leaf nodes are using the registered signature keys. - for_ mClientData $ \clientData -> + for_ mClientData $ \cd -> for_ (Map.assocs newclients) $ \(cid, (_, mKp)) -> - checkSignatureKey (fmap (.leafNode) mKp) (Map.lookup cid clientData.infoMap) + checkSignatureKey (fmap (.leafNode) mKp) (Map.lookup cid cd.infoMap) pure $ guard unreachable $> qtarget diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Commit/Core.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Commit/Core.hs index c54c5474cf8..64d13f432bd 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Commit/Core.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Commit/Core.hs @@ -18,6 +18,7 @@ module Wire.ConversationSubsystem.MLS.Commit.Core ( getCommitData, incrementEpoch, + incrementEpochNoRead, getClientInfo, getSingleClientInfo, checkSignatureKey, @@ -70,7 +71,7 @@ import Wire.ExternalAccess import Wire.FederationAPIAccess import Wire.LegalHoldStore (LegalHoldStore) import Wire.NotificationSubsystem -import Wire.ProposalStore (ProposalStore) +import Wire.ProposalStore (ProposalStore, StoredProposal, getAllPendingProposals) import Wire.Sem.Now (Now) import Wire.Sem.Random (Random) import Wire.TeamCollaboratorsSubsystem @@ -114,22 +115,27 @@ getCommitData :: Epoch -> CipherSuiteTag -> IncomingBundle -> - Sem r (IndexMap, ProposalAction) + Sem r (IndexMap, ProposalAction, [StoredProposal]) getCommitData senderIdentity lConvOrSub epoch ciphersuite bundle = do let convOrSub = tUnqualified lConvOrSub groupId = cnvmlsGroupId convOrSub.mlsMeta - runState convOrSub.indexMap $ do - creatorAction <- - if epoch == Epoch 0 - then addProposedClient (Left . RegularClient $ senderIdentity.client) - else mempty - proposals <- - traverse - (derefOrCheckProposal epoch ciphersuite groupId) - bundle.commit.value.proposals - action <- applyProposals ciphersuite proposals - pure (creatorAction <> action) + -- Fetch all pending proposals once: used both for dereferencing commit + -- proposal refs and by checkReferences downstream. + storedProposals <- getAllPendingProposals groupId epoch + (newIndexMap, combinedAction) <- + runState convOrSub.indexMap $ do + creatorAction <- + if epoch == Epoch 0 + then addProposedClient (Left . RegularClient $ senderIdentity.client) + else mempty + proposals <- + traverse + (derefOrCheckProposalFrom storedProposals ciphersuite) + bundle.commit.value.proposals + action <- applyProposals ciphersuite proposals + pure (creatorAction <> action) + pure (newIndexMap, combinedAction, storedProposals) incrementEpoch :: ( Member ConversationStore r, @@ -149,6 +155,19 @@ incrementEpoch (SubConv c s) = do getSubConversation (mcId c) (scSubConvId s) >>= noteS @'ConvNotFound pure (SubConv c subconv) +-- | Bump the MLS epoch without re-reading the conversation afterwards. +-- Use when the caller discards the result; avoids 2-3 CQL round trips. +incrementEpochNoRead :: + (Member ConversationStore r) => + ConvOrSubConv -> + Sem r () +incrementEpochNoRead = + \case + Conv c -> + setConversationEpoch (mcId c) (succ (cnvmlsEpoch (mcMLSData c))) + SubConv _c s -> + setSubConversationEpoch (scParentConvId s) (scSubConvId s) (succ (cnvmlsEpoch (scMLSData s))) + getClientInfo :: ( Member BrigAPIAccess r, Member (FederationAPIAccess FederatorClient) r, diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Commit/InternalCommit.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Commit/InternalCommit.hs index d904f7396d5..68bb12c1ec0 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Commit/InternalCommit.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Commit/InternalCommit.hs @@ -32,6 +32,8 @@ import Data.Tuple.Extra import Galley.Types.Error import Imports import Polysemy +import Polysemy.Async (Async) +import Polysemy.Async qualified as P import Polysemy.Error import Polysemy.Input (Input) import Polysemy.Resource (Resource) @@ -78,6 +80,7 @@ processInternalCommit :: Member (ErrorS 'MissingLegalholdConsent) r, Member (ErrorS 'GroupIdVersionNotSupported) r, Member Resource r, + Member Async r, Member Random r, Member (ErrorS MLSInvalidLeafNodeSignature) r, Member MLSCommitLockStore r, @@ -93,14 +96,14 @@ processInternalCommit :: Epoch -> ProposalAction -> Commit -> + [StoredProposal] -> Codensity (Sem r) [LocalConversationUpdate] -processInternalCommit senderIdentity con lConvOrSub ciphersuite ciphersuiteUpdate epoch action commit = do +processInternalCommit senderIdentity con lConvOrSub ciphersuite ciphersuiteUpdate epoch action commit storedProposals = do let convOrSub = tUnqualified lConvOrSub qusr = cidQualifiedUser senderIdentity.client cm = convOrSub.members newUserClients = Map.assocs (unClientMap (paAdd action)) - - lift $ checkReferences convOrSub epoch commit + lift $ checkReferences storedProposals commit -- check update path lift $ traverse_ (checkUpdatePath lConvOrSub senderIdentity ciphersuite) commit.path @@ -244,9 +247,25 @@ processInternalCommit senderIdentity con lConvOrSub ciphersuite ciphersuiteUpdat removeMLSClients gid qtarget (Map.keysSet clients) -- add clients to the conversation state - for_ newUserClients $ \(qtarget, newClients) -> do - addMLSClients gid qtarget $ - Set.fromList [(cid, idx) | (cid, (idx, _)) <- Map.assocs newClients] + -- Note: safe to run concurrently because the children only perform store + -- writes on disjoint rows. The store children fail via IO exceptions + -- (addMLSClients runs through embedClient, a pure IO embed), which the + -- Async interpretation rethrows. An 'Error'-effect throw in a child is + -- forwarded by the in-thread mapError interpreters (cf. Galley.App) to + -- the residual error, whose interpreter sits outside asyncToIOFinal; + -- Polysemy collapses the child result to 'Nothing'. The 'Nothing' guard + -- below turns that into a hard commit failure instead of a silently + -- dropped write. + results <- + P.sequenceConcurrently $ + flip fmap newUserClients $ \(qtarget, newClients) -> + addMLSClients gid qtarget $ + Set.fromList [(cid, idx) | (cid, (idx, _)) <- Map.assocs newClients] + when (Nothing `elem` results) $ + throw + ( InternalErrorWithDescription + "A concurrent client-store write failed while processing commit" + ) for_ action.paHistoryClientAdd $ uncurry (addHistoryClient gid) @@ -256,9 +275,8 @@ processInternalCommit senderIdentity con lConvOrSub ciphersuite ciphersuiteUpdat when ciphersuiteUpdate $ case convOrSub.id of Conv cid -> setConversationCipherSuite cid ciphersuite SubConv cid sub -> setSubConversationCipherSuite cid sub ciphersuite - -- increment epoch number - for_ lConvOrSub incrementEpoch + for_ lConvOrSub incrementEpochNoRead pure events @@ -330,12 +348,9 @@ existingMembers :: Local StoredConversation -> Set (Qualified UserId) existingMembers lconv = existingLocalMembers lconv <> existingRemoteMembers lconv checkReferences :: - ( Member ProposalStore r, - Member (ErrorS MLSCommitMissingReferences) r - ) => - ConvOrSubConv -> Epoch -> Commit -> Sem r () -checkReferences convOrSub epoch commit = do - allPendingProposals <- getAllPendingProposals (cnvmlsGroupId convOrSub.mlsMeta) epoch + (Member (ErrorS MLSCommitMissingReferences) r) => + [StoredProposal] -> Commit -> Sem r () +checkReferences allPendingProposals commit = do let referencedProposals = Set.fromList $ mapMaybe (\x -> preview _Ref x) commit.proposals let (includedProposals, missingProposals) = partition diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Message.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Message.hs index b5cc036c2c6..760bcd63e22 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Message.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Message.hs @@ -43,6 +43,7 @@ import Data.Tuple.Extra import Galley.Types.Error import Imports import Polysemy +import Polysemy.Async (Async) import Polysemy.Error import Polysemy.Input import Polysemy.Output @@ -183,7 +184,8 @@ postMLSCommitBundle :: Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r, - Member FeaturesConfigSubsystem r + Member FeaturesConfigSubsystem r, + Member Async r ) => Local x -> Qualified UserId -> @@ -220,7 +222,8 @@ postMLSCommitBundleFromLocalUser :: Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r, - Member FeaturesConfigSubsystem r + Member FeaturesConfigSubsystem r, + Member Async r ) => Version -> Local UserId -> @@ -257,7 +260,8 @@ postMLSCommitBundleToLocalConv :: Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r, - Member FeaturesConfigSubsystem r + Member FeaturesConfigSubsystem r, + Member Async r ) => Qualified UserId -> ClientId -> @@ -325,7 +329,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do (events, newClients) <- case senderIdentity.index of Just _ -> do -- extract added/removed clients from bundle - (newIndexMap, action) <- + (newIndexMap, action, storedProposals) <- lift $ getCommitData senderIdentity lConvOrSub bundle.epoch ciphersuite bundle @@ -358,6 +362,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do bundle.epoch action bundle.commit.value + storedProposals -- the sender client is included in the Add action on the first commit, -- but it doesn't need to get a welcome message, so we filter it out here let newClients = cmRemoveClient senderIdentity.client (paAdd action) diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Proposal.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Proposal.hs index 0edaaa345a3..c4d9497c148 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Proposal.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Proposal.hs @@ -17,7 +17,7 @@ module Wire.ConversationSubsystem.MLS.Proposal ( -- * Proposal processing - derefOrCheckProposal, + derefOrCheckProposalFrom, checkProposal, processProposal, proposalProcessingStage, @@ -145,24 +145,24 @@ type HasProposalEffects r = Member TeamCollaboratorsSubsystem r ) -derefOrCheckProposal :: +-- | Dereference a commit proposal, looking up refs in a prefetched list of +-- pending proposals instead of issuing one point-read per ref. +derefOrCheckProposalFrom :: ( Member (Error MLSProtocolError) r, Member (ErrorS 'MLSInvalidLeafNodeIndex) r, Member (ErrorS 'MLSUnsupportedProposal) r, - Member ProposalStore r, Member (State IndexMap) r, Member (ErrorS 'MLSProposalNotFound) r, Member (ErrorS 'MLSInvalidLeafNodeSignature) r ) => - Epoch -> + [StoredProposal] -> CipherSuiteTag -> - GroupId -> ProposalOrRef -> Sem r Proposal -derefOrCheckProposal epoch _ciphersuite groupId (Ref ref) = do - p <- getProposal groupId epoch ref >>= noteS @'MLSProposalNotFound - pure p.value -derefOrCheckProposal _epoch ciphersuite _ (Inline p) = do +derefOrCheckProposalFrom stored _ciphersuite (Ref ref) = + noteS @'MLSProposalNotFound $ + (.proposal.value) <$> find ((== ref) . (.ref)) stored +derefOrCheckProposalFrom _stored ciphersuite (Inline p) = do im <- get checkProposal ciphersuite im p pure p diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Welcome.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Welcome.hs index b16bb87e1af..9ebe0ff0e85 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Welcome.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Welcome.hs @@ -48,7 +48,6 @@ import Wire.API.MLS.SubConversation import Wire.API.MLS.Welcome import Wire.API.Message import Wire.API.Push.V2 (RecipientClients (..)) -import Wire.ExternalAccess import Wire.FederationAPIAccess import Wire.NotificationSubsystem import Wire.Sem.Now (Now) @@ -56,7 +55,6 @@ import Wire.Sem.Now qualified as Now sendWelcomes :: ( Member (FederationAPIAccess FederatorClient) r, - Member ExternalAccess r, Member P.TinyLog r, Member Now r, Member NotificationSubsystem r @@ -79,10 +77,7 @@ sendWelcomes loc qusr con cids welcome = do convFrom (SubConv c _) = c sendLocalWelcomes :: - ( Member P.TinyLog r, - Member ExternalAccess r, - Member NotificationSubsystem r - ) => + (Member NotificationSubsystem r) => Qualified ConvId -> Qualified UserId -> Maybe ConnId -> @@ -100,8 +95,11 @@ sendLocalWelcomes qcnv qusr con now welcome lclients = do mempty $ tUnqualified lclients let e = Event qcnv Nothing (EventFromUser qusr) now Nothing $ EdMLSWelcome welcome.raw - runMessagePush lclients (Just qcnv) $ - newMessagePush mempty con defMessageMetadata rcpts e + -- Fire-and-forget: delivery is asynchronous downstream of gundeck anyway; + -- blocking the commit-bundle response on the fan-out is wasted latency. + void $ + pushNotificationAsync + (toPush (newMessagePush mempty con defMessageMetadata rcpts e)) sendRemoteWelcomes :: ( Member (FederationAPIAccess FederatorClient) r, diff --git a/libs/wire-subsystems/test/unit/Wire/ConversationSubsystem/MLS/CheckClientsSpec.hs b/libs/wire-subsystems/test/unit/Wire/ConversationSubsystem/MLS/CheckClientsSpec.hs new file mode 100644 index 00000000000..addded975bd --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/ConversationSubsystem/MLS/CheckClientsSpec.hs @@ -0,0 +1,169 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2026 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it +-- under the terms of the GNU Affero General Public License as published by the +-- Free Software Foundation, either version 3 of the License, or (at your +-- option) any later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +-- FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License +-- for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +module Wire.ConversationSubsystem.MLS.CheckClientsSpec (spec) where + +import Data.Domain (Domain (..)) +import Data.Id +import Data.Map qualified as Map +import Data.Qualified +import Data.UUID qualified as UUID +import Galley.Types.Error (InternalError (..)) +import Imports +import Polysemy +import Polysemy.Async (asyncToIOFinal) +import Polysemy.Error (Error, runError, throw) +import Test.Hspec +import Wire.API.Conversation +import Wire.API.Conversation.Protocol +import Wire.API.Error (runErrorS) +import Wire.API.Error.Galley (GalleyError (MLSClientMismatch, MLSIdentityMismatch), MLSProtocolError) +import Wire.API.Federation.Client (FederatorClient) +import Wire.API.Federation.Error (FederationError (..)) +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.KeyPackage (KeyPackage) +import Wire.API.MLS.LeafNode (LeafIndex) +import Wire.API.MLS.SubConversation (ConvOrSubChoice (..)) +import Wire.BrigAPIAccess (BrigAPIAccess (..)) +import Wire.ConversationStore.MLS.Types +import Wire.ConversationSubsystem.MLS.CheckClients +import Wire.FederationAPIAccess (FederationAPIAccess (..)) +import Wire.StoredConversation (MLSMigrationState (..)) + +data TestBrigFailure = TestBrigFailure + deriving stock (Show) + +data BrigBehavior + = BrigCrashes + | BrigUnused + +spec :: Spec +spec = describe "Wire.ConversationSubsystem.MLS.CheckClients" do + it "aborts the commit when a concurrent brig client-data fetch crashes" $ do + result <- runCheckClients BrigCrashes + case result of + Right (Left (InternalErrorWithDescription _)) -> pure () + _ -> + expectationFailure $ + "expected InternalErrorWithDescription, got: " <> show result + + it "still treats a hushed FederationError as user-unreachable" $ do + result <- runCheckClients BrigUnused + case result of + Right (Right unreachable) -> + fmap qUnqualified unreachable `shouldBe` [userId] + _ -> + expectationFailure $ + "expected unreachable user, got: " <> show result + +-- | Runs 'checkClients' with a layering that mirrors galley's production +-- stack (cf. Galley.App): all pure error interpreters ('runError' and +-- 'runErrorS') sit outside 'asyncToIOFinal', so an 'Error'-effect throw +-- inside a spawned child collapses to 'Nothing' instead of propagating. +runCheckClients :: + BrigBehavior -> + IO (Either TestBrigFailure (Either InternalError [Qualified UserId])) +runCheckClients behavior = + runFinal @IO + . runError @TestBrigFailure + . runError @InternalError + . (fmap (fromMaybe (error "unexpected MLSIdentityMismatch")) . runErrorS @'MLSIdentityMismatch) + . (fmap (fromMaybe (error "unexpected MLSClientMismatch")) . runErrorS @'MLSClientMismatch) + . (fmap (either (error "unexpected MLSProtocolError") id) . runError @MLSProtocolError) + . asyncToIOFinal + . embedToFinal @IO + . interpretTestBrig behavior + . interpretTestFederation + $ checkClients lConv csSuite (newCM (qUserId behavior)) + +interpretTestBrig :: + (Polysemy.Member (Error TestBrigFailure) r) => + BrigBehavior -> + Sem (BrigAPIAccess ': r) a -> + Sem r a +interpretTestBrig behavior = + interpret $ \case + GetLocalMLSClients {} -> case behavior of + BrigCrashes -> throw TestBrigFailure + BrigUnused -> error "unexpected GetLocalMLSClients call in test" + _ -> error "unexpected BrigAPIAccess call in test" + +interpretTestFederation :: + Sem (FederationAPIAccess FederatorClient ': r) a -> + Sem r a +interpretTestFederation = + interpret $ \case + -- Mirrors the production federation failure path: getRemoteMLSClients + -- throws the returned FederationError in client code, where + -- getClientData hushes it into an inner 'Nothing'. + RunFederatedEither _ _ -> pure (Left FederationNotImplemented) + _ -> error "unexpected FederationAPIAccess call in test" + +ownDomain :: Domain +ownDomain = Domain "example.com" + +-- | The added user is remote, so the client-data fetch goes through the +-- federation path in both scenarios. +remoteDomain :: Domain +remoteDomain = Domain "other.example.com" + +lConv :: Local ConvOrSubConv +lConv = toLocalUnsafe ownDomain convOrSub + +convOrSub :: ConvOrSubConv +convOrSub = + Conv + MLSConversation + { mcId = convId, + mcMetadata = defConversationMetadata Nothing, + mcMLSData = ConversationMLSData {cnvmlsGroupId = groupId, cnvmlsActiveData = Nothing}, + mcLocalMembers = [], + mcRemoteMembers = [], + mcMembers = mempty, + mcIndexMap = mempty, + mcMigrationState = MLSMigrationMLS + } + +-- | One user (not a conversation member) adding one client. With 'Nothing' +-- client data this user is classified unreachable. The user domain selects +-- the client-data fetch path: local via BrigAPIAccess, remote via the +-- federation path. +newCM :: Qualified UserId -> ClientMap (LeafIndex, Maybe KeyPackage) +newCM quser = ClientMap (Map.singleton quser (Map.singleton testClientId (0, Nothing))) + +mkUuid :: String -> UUID.UUID +mkUuid = fromJust . UUID.fromString + +userId :: UserId +userId = Id (mkUuid "00000000-0000-0000-0000-000000000001") + +convId :: ConvId +convId = Id (mkUuid "00000000-0000-0000-0000-000000000002") + +testClientId :: ClientId +testClientId = ClientId 3 + +groupId :: GroupId +groupId = GroupId "check-clients-spec" + +csSuite :: CipherSuiteTag +csSuite = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + +qUserId :: BrigBehavior -> Qualified UserId +qUserId = \case + BrigCrashes -> Qualified userId ownDomain + BrigUnused -> Qualified userId remoteDomain diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 220d4252fb5..66662926e3d 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -636,6 +636,7 @@ test-suite wire-subsystems-tests Wire.ConversationSubsystem.AdminlessGroupsSpec Wire.ConversationSubsystem.InterpreterSpec Wire.ConversationSubsystem.MessageSpec + Wire.ConversationSubsystem.MLS.CheckClientsSpec Wire.ConversationSubsystem.One2OneSpec Wire.EmailSubsystem.TemplateFixtures Wire.EmailSubsystem.TemplateSpec From 47a8f020fd70dd24257755a369bb2f7f2c97f490 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 11 Sep 2026 10:48:38 +0200 Subject: [PATCH 21/29] WPB-27964 use CSP header of account pages (#5534) --- changelog.d/3-bug-fixes/WPB-27964 | 1 + .../nginx-ingress-services/templates/ingress.yaml | 15 +++++++++++++-- charts/wire-ingress/README.md | 10 +++++++--- charts/wire-ingress/templates/_helpers.tpl | 8 +++++++- .../templates/httproute-account-pages.yaml | 13 +++++-------- .../wire-ingress/templates/httproute-webapp.yaml | 13 +++++-------- charts/wire-ingress/values.yaml | 8 +++++--- .../nginx-ingress-services/values.yaml.gotmpl | 4 ++++ 8 files changed, 47 insertions(+), 25 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-27964 diff --git a/changelog.d/3-bug-fixes/WPB-27964 b/changelog.d/3-bug-fixes/WPB-27964 new file mode 100644 index 00000000000..b2e78cf94e0 --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-27964 @@ -0,0 +1 @@ +Account pages now use the correct backend URL and CSP header on each multi-ingress domain. This applies to both ingress charts: `nginx-ingress-services` no longer includes the account-pages host in its generic CSP snippet, and `wire-ingress` (envoy-gateway) no longer injects a Content-Security-Policy response header on the account-pages route. The same fix is applied to the webapp route in `wire-ingress`, which had the same problem (`nginx-ingress-services` already skipped it). diff --git a/charts/nginx-ingress-services/templates/ingress.yaml b/charts/nginx-ingress-services/templates/ingress.yaml index c0f1635efdc..77d07967fe9 100644 --- a/charts/nginx-ingress-services/templates/ingress.yaml +++ b/charts/nginx-ingress-services/templates/ingress.yaml @@ -1,3 +1,7 @@ +{{- $accountPagesDns := "" -}} +{{- if .Values.accountPages.enabled -}} +{{- $accountPagesDns = required "Need a 'config.dns.accountPages' name when accountPages.enabled is true." .Values.config.dns.accountPages -}} +{{- end -}} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: @@ -46,6 +50,13 @@ metadata: if ($http_host = "{{ .Values.config.dns.webapp }}") { set $skip_csp 1; } + {{ if .Values.accountPages.enabled }} + # Like the webapp, account-pages provides multi-ingress aware CSP headers + # itself, so the approximation below must not overwrite them. + if ($http_host = "{{ $accountPagesDns }}") { + set $skip_csp 1; + } + {{ end }} if ($uri ~ "^(/v[0-9]+)?/sso/(finalize-login|initiate-login)(/[a-zA-Z0-9-]*)?$|^/favicon\.ico$") { set $skip_csp 1; } @@ -91,7 +102,7 @@ spec: - {{ .Values.config.dns.teamSettings }} {{- end }} {{- if .Values.accountPages.enabled }} - - {{ .Values.config.dns.accountPages }} + - {{ $accountPagesDns }} {{- end }} secretName: {{ include "nginx-ingress-services.getCertificateSecretName" . | quote }} rules: @@ -142,7 +153,7 @@ spec: number: {{ .Values.service.teamSettings.externalPort }} {{- end }} {{- if .Values.accountPages.enabled }} - - host: {{ .Values.config.dns.accountPages }} + - host: {{ $accountPagesDns }} http: paths: - path: / diff --git a/charts/wire-ingress/README.md b/charts/wire-ingress/README.md index 73433734acc..7b31b398477 100644 --- a/charts/wire-ingress/README.md +++ b/charts/wire-ingress/README.md @@ -103,7 +103,7 @@ name overrides, etc.) can be found in `values.yaml`. | `config.ingressClass` | | | `ingressName` | Replaced by `config.domains[].name` — see [Multi-ingress (multiple backend domains)](#multi-ingress-multiple-backend-domains) | | `config.isAdditionalIngress` | Implicit — every `config.domains` entry after the first is an additional ingress | -| `config.renderCSPInIngress` | CSP is injected automatically on additional domains; opt out per-domain with `config.domains[].renderCSP: false` | +| `config.renderCSPInIngress` | CSP is injected automatically on additional domains (team-settings route only); opt out per-domain with `config.domains[].renderCSP: false` | | `config.dns.base` | Replaced by `config.domains[].base` (used for the per-domain CSP wildcard) | | `tls.verify_depth` | Envoy Gateway `ClientTrafficPolicy` does not expose a direct verify-depth knob; the CA chain itself controls this | | `tls.enabled` | Removed — had no effect; all routes are always TLS-terminated | @@ -250,8 +250,12 @@ config: First entry = primary (listener `https`, un-suffixed names, no injected CSP — apps set their own). Each additional entry gets its own listener `https-`, cert/secret, suffixed routes, and an -injected per-domain CSP header on the webapp/team-settings/account-pages routes (opt out with -`renderCSP: false`). +injected per-domain CSP header on the team-settings route (opt out with `renderCSP: false`). + +The webapp and account-pages routes never get an injected CSP, on any domain: both apps emit +correct per-domain headers themselves, and the injected header would replace them with a weaker +approximation. This matches the hosts the legacy `nginx-ingress-services` chart skips in its CSP +snippet. Team-settings does not yet support this, hence the approximation there. Multi-ingress is mutually exclusive with federation: `config.domains` cannot be combined with `federator.enabled: true`. Use federation with a single backend diff --git a/charts/wire-ingress/templates/_helpers.tpl b/charts/wire-ingress/templates/_helpers.tpl index f780f7f35c5..8990fcc5011 100644 --- a/charts/wire-ingress/templates/_helpers.tpl +++ b/charts/wire-ingress/templates/_helpers.tpl @@ -86,7 +86,9 @@ Multi-domain: `config.domains` is a list; the FIRST entry is the primary (its resources keep the un-suffixed names, and its frontend apps set their own CSP so no CSP is injected). Every additional entry gets a `-` suffix, its own Gateway listener (`https-`), its own certificate/secret, and — being -an "additional ingress" — a per-domain CSP header injected on the app routes. +an "additional ingress" — a per-domain CSP header injected on the team-settings +route. The webapp and account-pages routes never get an injected CSP: those +apps set multi-ingress aware headers themselves (see the httproute templates). Each entry has: suffix, section, hostname, https, ssl, webapp, teamSettings, accountPages, fakeS3, base, secretName, certName, issuerName, issuerKind, @@ -183,6 +185,10 @@ for multi-ingress domains (charts/nginx-ingress-services/templates/ingress.yaml) where the primary domain's frontend apps set CSP themselves but additional domains need the header set at the front door. +Only the team-settings route uses this. The webapp and account-pages routes are +excluded, matching the `$skip_csp` hosts in the nginx chart's snippet, because +those apps emit correct per-domain headers on their own. + Call with a dict: {https, ssl, base, websockets (bool)}. */}} {{- define "wire-ingress.cspHeader" -}} diff --git a/charts/wire-ingress/templates/httproute-account-pages.yaml b/charts/wire-ingress/templates/httproute-account-pages.yaml index 385876a2e31..c1115985b91 100644 --- a/charts/wire-ingress/templates/httproute-account-pages.yaml +++ b/charts/wire-ingress/templates/httproute-account-pages.yaml @@ -30,14 +30,11 @@ spec: - path: type: PathPrefix value: / - {{- if $domain.csp }} - filters: - - type: ResponseHeaderModifier - responseHeaderModifier: - set: - - name: Content-Security-Policy - value: {{ include "wire-ingress.cspHeader" (dict "https" $domain.https "ssl" $domain.ssl "base" $domain.base "websockets" $root.Values.websockets.enabled) | quote }} - {{- end }} + {{/* No CSP header is injected here. Unlike team-settings, account-pages + sets multi-ingress aware CSP headers itself, and a ResponseHeaderModifier + "set" filter would replace them with the weaker approximation from + wire-ingress.cspHeader. This mirrors the legacy nginx-ingress-services + chart, which skips its CSP snippet for this host. */}} backendRefs: - name: account-pages-http port: {{ $root.Values.service.accountPages.externalPort }} diff --git a/charts/wire-ingress/templates/httproute-webapp.yaml b/charts/wire-ingress/templates/httproute-webapp.yaml index 07844ec80a9..f66cb1b8c2d 100644 --- a/charts/wire-ingress/templates/httproute-webapp.yaml +++ b/charts/wire-ingress/templates/httproute-webapp.yaml @@ -30,14 +30,11 @@ spec: - path: type: PathPrefix value: / - {{- if $domain.csp }} - filters: - - type: ResponseHeaderModifier - responseHeaderModifier: - set: - - name: Content-Security-Policy - value: {{ include "wire-ingress.cspHeader" (dict "https" $domain.https "ssl" $domain.ssl "base" $domain.base "websockets" $root.Values.websockets.enabled) | quote }} - {{- end }} + {{/* No CSP header is injected here. Unlike team-settings, the webapp + sets multi-ingress aware CSP headers itself, and a ResponseHeaderModifier + "set" filter would replace them with the weaker approximation from + wire-ingress.cspHeader. This mirrors the legacy nginx-ingress-services + chart, which skips its CSP snippet for this host. */}} backendRefs: - name: webapp-http port: {{ $root.Values.service.webapp.externalPort }} diff --git a/charts/wire-ingress/values.yaml b/charts/wire-ingress/values.yaml index bcd14ac911b..b4d665a54d7 100644 --- a/charts/wire-ingress/values.yaml +++ b/charts/wire-ingress/values.yaml @@ -125,8 +125,10 @@ gateway: # the un-suffixed names, and its frontend apps set their own CSP). Every # additional entry gets its own Gateway HTTPS listener (`https-`), its own # certificate/secret, and — being an "additional ingress" — a per-domain -# Content-Security-Policy header injected on the webapp/team-settings/ -# account-pages routes (mirrors the legacy nginx-ingress-services behaviour). +# Content-Security-Policy header injected on the team-settings route (mirrors +# the legacy nginx-ingress-services behaviour). The webapp and account-pages +# routes are left alone: those apps set multi-ingress aware CSP headers +# themselves, and injecting here would overwrite them. # # config.dns and config.domains are mutually exclusive; config.domains wins. # @@ -153,7 +155,7 @@ gateway: # https: nginz-https.red.example.org # ssl: nginz-ssl.red.example.org # webapp: webapp.red.example.org -# # renderCSP: false # optional: disable the injected CSP for this domain +# # renderCSP: false # optional: disable the injected CSP (team-settings) for this domain # tls: # # secretName: "" # optional TLS secret name override (defaults to a per-domain name) # issuer: # optional per-domain cert-manager issuer override (defaults to tls.issuer) diff --git a/hack/helm_vars/nginx-ingress-services/values.yaml.gotmpl b/hack/helm_vars/nginx-ingress-services/values.yaml.gotmpl index 958ba24ac9b..e7d01c05688 100644 --- a/hack/helm_vars/nginx-ingress-services/values.yaml.gotmpl +++ b/hack/helm_vars/nginx-ingress-services/values.yaml.gotmpl @@ -15,7 +15,11 @@ tls: config: ingressClass: "nginx-{{ .Release.Namespace }}" + # Exercise the in-ingress CSP header path (off by default in the chart), so + # that changes to the 'configuration-snippet' annotation get test coverage. + renderCSPInIngress: true dns: + base: "{{ .Release.Namespace }}-integration.example.com" https: "nginz-https.{{ .Release.Namespace }}-integration.example.com" ssl: "nginz-ssl.{{ .Release.Namespace }}-integration.example.com" webapp: "webapp.{{ .Release.Namespace }}-integration.example.com" From c6136fc11bed4ec3bbb32301f4f06f70791377a7 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 11 Sep 2026 12:20:42 +0200 Subject: [PATCH 22/29] WPB-28565: rebump api v18 swagger docs (#5536) --------- Co-authored-by: Leif Battermann --- .../1-api-changes/WPB-28565-finalize-api-version-v18 | 2 +- services/brig/docs/swagger-v18.json | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/changelog.d/1-api-changes/WPB-28565-finalize-api-version-v18 b/changelog.d/1-api-changes/WPB-28565-finalize-api-version-v18 index ebcce755966..ab10369d5f0 100644 --- a/changelog.d/1-api-changes/WPB-28565-finalize-api-version-v18 +++ b/changelog.d/1-api-changes/WPB-28565-finalize-api-version-v18 @@ -1 +1 @@ -Finalize API version v18 and create development version v19. +Finalize API version v18 and create development version v19. (#5531, #5536) diff --git a/services/brig/docs/swagger-v18.json b/services/brig/docs/swagger-v18.json index 555fae62371..e0f697799f4 100644 --- a/services/brig/docs/swagger-v18.json +++ b/services/brig/docs/swagger-v18.json @@ -7549,11 +7549,16 @@ "setRestrictUserCreation": { "description": "Do not allow certain user creation flows", "type": "boolean" + }, + "ssoIdpChangeDetectionEnabled": { + "description": "Whether clients should compare the stored SSO IdP ID with the IdP ID of the current login and keep existing locally decrypted messages when they match.", + "type": "boolean" } }, "required": [ "setRestrictUserCreation", - "setEnableMls" + "setEnableMls", + "ssoIdpChangeDetectionEnabled" ], "type": "object" }, From da4e4baafd00fe3c5fd5f03d60c806bddca923f3 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 11 Sep 2026 16:06:22 +0200 Subject: [PATCH 23/29] WPB-28645: presence cleanup must not swallow async exceptions (#5535) --- changelog.d/3-bug-fixes/WPB-28645 | 4 ++++ services/gundeck/src/Gundeck/Run.hs | 11 +++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-28645 diff --git a/changelog.d/3-bug-fixes/WPB-28645 b/changelog.d/3-bug-fixes/WPB-28645 new file mode 100644 index 00000000000..570d00f42f0 --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-28645 @@ -0,0 +1,4 @@ +Gundeck's presence cleanup background thread no longer swallows asynchronous +exceptions. On shutdown this removes the spurious "presence cleanup failed" +(AsyncCancelled) error log line and lets the thread terminate promptly instead +of lingering for up to an hour. diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index 6f4b388d643..590fe96e1ce 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -41,8 +41,8 @@ import Cassandra (runClient, shutdown) import Cassandra.Schema (versionCheck) import Control.Error (ExceptT (ExceptT)) import Control.Exception (finally) +import Control.Exception.Safe (catchAny) import Control.Lens ((.~), (^.)) -import Control.Monad.Catch (catchAll) import Control.Monad.Extra import Data.Map qualified as Map import Data.Metrics.AWS (gaugeTokenRemaing) @@ -183,15 +183,18 @@ collectAuthMetrics env = do -- | Hourly janitor replacing the redis key TTL: deletes presence rows older -- than a week (leak guard for abnormally dead pods). Never let a transient DB --- error kill the thread — log and retry next hour. +-- error kill the thread — log and retry next hour. Async exceptions (e.g. +-- 'AsyncCancelled' from 'Async.cancel' during shutdown) propagate because +-- 'Control.Exception.Safe.catchAny' rethrows asynchronously-delivered +-- exceptions and only handles synchronous ones. cleanupPresenceLoop :: Log.Logger -> Gundeck () cleanupPresenceLoop logger = forever $ (PresenceData.cleanup >> threadDelay cleanupInterval) - `catchAll` \e -> do + `catchAny` \e -> do liftIO . Log.err logger $ Log.msg (Log.val "presence cleanup failed") - . Log.field "error" (displayException (e :: SomeException)) + . Log.field "error" (displayException e) threadDelay cleanupInterval cleanupInterval :: Int From 628bd2cf855dfad46298b4466971429a5eeb6477 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 11 Sep 2026 18:01:29 +0200 Subject: [PATCH 24/29] WPB-28685 :Deliver meeting events via native push (APNs/FCM) (#5537) --- changelog.d/2-features/WPB-28685 | 1 + .../src/Wire/MeetingNotifier/Interpreter.hs | 8 ++++---- .../src/Wire/MeetingsSubsystem/Notification.hs | 4 +++- .../wire-subsystems/test/unit/Wire/MeetingNotifierSpec.hs | 2 ++ .../test/unit/Wire/MeetingsSubsystem/InterpreterSpec.hs | 7 ++++--- 5 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 changelog.d/2-features/WPB-28685 diff --git a/changelog.d/2-features/WPB-28685 b/changelog.d/2-features/WPB-28685 new file mode 100644 index 00000000000..fc6a13cd4bb --- /dev/null +++ b/changelog.d/2-features/WPB-28685 @@ -0,0 +1 @@ +Meeting events (`meeting.create`, `meeting.update`, `meeting.delete`, `meeting.member-add`) are now delivered to all push channels, including native push (APNs/FCM), so offline or backgrounded clients learn about meeting changes via native push instead of waiting for the next foreground sync. diff --git a/libs/wire-subsystems/src/Wire/MeetingNotifier/Interpreter.hs b/libs/wire-subsystems/src/Wire/MeetingNotifier/Interpreter.hs index 53ee3bbed38..8659a0b5b36 100644 --- a/libs/wire-subsystems/src/Wire/MeetingNotifier/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/MeetingNotifier/Interpreter.hs @@ -70,10 +70,10 @@ notifyMeetingMembersAddedImpl qUser qConvId mTeamId users = do TinyLog.warn $ Log.msg ("alive meeting not found for meeting member-add event" :: ByteString) . Log.field "conversationId" (toByteString' (qUnqualified qConvId)) - -- `users` are the members added by the commit; the commit creator is already - -- a member and never in `users`, so mkMeetingEventPush (which no longer - -- filters the originator by UserId) does not echo member-add back to them. - -- conn is Nothing: every client connection of each added user should be notified. + -- Recipients are exactly the added users: the commit creator is already a + -- member, so it is structurally absent from `users` and member-add is never + -- echoed back to them. conn is Nothing: every client connection of each + -- added user should be notified. for_ meetings $ \meeting -> pushNotificationAsync $ mkMeetingEventPush diff --git a/libs/wire-subsystems/src/Wire/MeetingsSubsystem/Notification.hs b/libs/wire-subsystems/src/Wire/MeetingsSubsystem/Notification.hs index 7eb3ad27faa..2fa1e7de2a8 100644 --- a/libs/wire-subsystems/src/Wire/MeetingsSubsystem/Notification.hs +++ b/libs/wire-subsystems/src/Wire/MeetingsSubsystem/Notification.hs @@ -31,6 +31,8 @@ import Wire.API.Push.V2 qualified as PushV2 import Wire.NotificationSubsystem -- | Build the common push event structure used by all meeting lifecycle events. +-- Delivered to all channels including native push (APNs/FCM), matching the +-- regular-conversation event convention (RouteAny). mkMeetingEventPush :: UTCTime -> Qualified UserId -> @@ -55,6 +57,6 @@ mkMeetingEventPush now qUser conn recipients qConvId mTeamId meetingType qMeetin evtTeam = mTeamId }, recipients = recipients, - route = PushV2.RouteDirect, + route = PushV2.RouteAny, conn } diff --git a/libs/wire-subsystems/test/unit/Wire/MeetingNotifierSpec.hs b/libs/wire-subsystems/test/unit/Wire/MeetingNotifierSpec.hs index eb5ac1d066e..2c78b199681 100644 --- a/libs/wire-subsystems/test/unit/Wire/MeetingNotifierSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/MeetingNotifierSpec.hs @@ -32,6 +32,7 @@ import Polysemy.TinyLog (TinyLog) import Test.Hspec import Wire.API.Event.Meeting qualified as MeetingEvent import Wire.API.Meeting qualified as API +import Wire.API.Push.V2 qualified as PushV2 import Wire.MeetingNotifier import Wire.MeetingNotifier.Interpreter import Wire.MeetingsStore qualified as Store @@ -72,6 +73,7 @@ spec = do length pushes `shouldBe` 1 let push = head pushes push.recipients `shouldBe` [userRecipient addedUser] + push.route `shouldBe` PushV2.RouteAny case fromJSON (Object push.json) :: Result MeetingEvent.Event of Error err -> expectationFailure err Success event -> event `shouldBe` expectedEvent diff --git a/libs/wire-subsystems/test/unit/Wire/MeetingsSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/MeetingsSubsystem/InterpreterSpec.hs index 145bf63ba8b..532711007f3 100644 --- a/libs/wire-subsystems/test/unit/Wire/MeetingsSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/MeetingsSubsystem/InterpreterSpec.hs @@ -49,6 +49,7 @@ import Wire.API.Error.Galley (GalleyError (TeamMemberNotFound, TeamNotFound), In import Wire.API.Event.Meeting qualified as MeetingEvent import Wire.API.Meeting qualified as API import Wire.API.PostgresMarshall (PostgresUnmarshall (postgresUnmarshall)) +import Wire.API.Push.V2 qualified as PushV2 import Wire.API.Team.Feature import Wire.API.Team.Member (TeamMember, mkTeamMember) import Wire.API.Team.Permission (fullPermissions) @@ -1629,11 +1630,11 @@ spec = describe "MeetingsSubsystem.Interpreter" $ do Left err -> fail $ "Error: " <> show err Right pushes -> do let push = head pushes - -- Order is deterministic: 'members' is a literal list, 'map - -- localMemberToRecipient' preserves order, and mkMeetingEventPush - -- no longer filters or reorders recipients. + -- Recipient order is preserved: 'members' is a literal list and + -- mkMeetingEventPush maps 'localMemberToRecipient' over it. map (.recipientUserId) push.recipients `shouldBe` [uid1, uid2] push.conn `shouldBe` Just originConn + push.route `shouldBe` PushV2.RouteAny describe "V16 operations" $ do let now = UTCTime (fromGregorian 2026 1 1) 0 From 8e9310fad342f93aff4125b8e4e49091679abfbe Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Mon, 14 Sep 2026 15:33:08 +0200 Subject: [PATCH 25/29] [WPB-27169] Script listing all commits and releases in which given files have been touched. (#5509) --- ...ses-in-which-given-files-have-been-touched | 1 + hack/bin/super-blame.sh | 101 ++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 changelog.d/5-internal/WPB-27169-script-listing-all-commits-and-releases-in-which-given-files-have-been-touched create mode 100755 hack/bin/super-blame.sh diff --git a/changelog.d/5-internal/WPB-27169-script-listing-all-commits-and-releases-in-which-given-files-have-been-touched b/changelog.d/5-internal/WPB-27169-script-listing-all-commits-and-releases-in-which-given-files-have-been-touched new file mode 100644 index 00000000000..5e69990bc55 --- /dev/null +++ b/changelog.d/5-internal/WPB-27169-script-listing-all-commits-and-releases-in-which-given-files-have-been-touched @@ -0,0 +1 @@ +Script listing all commits and releases in which given files have been touched. diff --git a/hack/bin/super-blame.sh b/hack/bin/super-blame.sh new file mode 100755 index 00000000000..fb9293afd9f --- /dev/null +++ b/hack/bin/super-blame.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <[@] [file ...] + +List commits that changed the given file(s) in chronological order, +annotated with the release in which each commit landed on master. + +A commit's release is the oldest chart/X.Y.0 tag on master whose +tagged commit is not older than the commit itself. + +Line ranges limit output to commits that changed those lines. +Syntax: file.hs@1,15-18 (single lines and ranges, comma-separated) +If no line ranges are given, all commits for the file are shown. + +Options: + -h Show this help +EOF + exit 0 +} + +if [ $# -eq 0 ]; then + echo "Usage: $0 [-h] [@] [file ...]" >&2 + exit 1 +fi + +if [ "$1" = "-h" ]; then + usage +fi + +git fetch --tags + +releases_tmp=$(mktemp) +trap 'rm -f "$releases_tmp"' EXIT + +while read -r tag; do + commit=$(git rev-list -1 "$tag") + ts=$(git log -1 --format="%ct" "$commit") + date=$(git log -1 --format="%ai" "$commit") + echo "${ts} ${tag} ${date}" +done < <(git tag --merged origin/master 'chart/*.0' | sort -V) > "$releases_tmp" + +annotate() { + while read -r commit_ts rest; do + commit=$(echo "$rest" | awk '{print $4}') + release="" + release_date="" + while read -r ts tag rdate; do + if [ "$ts" -ge "$commit_ts" ]; then + release="$tag" + release_date="$rdate" + break + fi + done < <(sort -n "$releases_tmp") + if [ -n "$release" ]; then + echo "$rest [$release, $release_date]" + else + echo "$rest [unreleased]" + fi + done +} + +for arg in "$@"; do + file="${arg%@*}" + lines="${arg##*@}" + if [ "$file" = "$arg" ]; then + lines="" + fi + + if [ ! -f "$file" ]; then + echo "Error: file not found: $file" >&2 + exit 1 + fi + + echo "=== $arg ===" + + if [ -z "$lines" ]; then + git log --follow --format="%ct %ai %H %s" -- "$file" | annotate + else + commits_tmp=$(mktemp) + IFS=',' read -ra ranges <<< "$lines" + for r in "${ranges[@]}"; do + if [[ "$r" == *"-"* ]]; then + start="${r%-*}" + end="${r#*-}" + else + start="$r" + end="$r" + fi + git log -L "${start},${end}:${file}" -s --format="%ct %ai %H %s" >> "$commits_tmp" + done + # Each line-range query may return the same commit. `sort -u` + # deduplicates and sorts by timestamp (field 1). + sort -u -k1,1n "$commits_tmp" | annotate + rm -f "$commits_tmp" + fi + + echo +done From caa2b39704a7404367454094c4f4308772ca24b2 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 14 Sep 2026 15:51:37 +0200 Subject: [PATCH 26/29] WPB-28697 add federated reminder events with origin user (#5540) --- changelog.d/2-features/WPB-26650 | 2 +- integration/test/Test/AdminlessGroups.hs | 41 +++++++++++++++++++ .../src/Wire/API/Federation/API.hs | 1 + .../Federation/API/Galley/Notifications.hs | 20 +++++++++ .../src/Wire/API/Federation/API/Util.hs | 4 ++ .../src/Wire/ConversationSubsystem.hs | 4 ++ .../Wire/ConversationSubsystem/Federation.hs | 32 +++++++++++++++ .../Wire/ConversationSubsystem/Interpreter.hs | 2 + .../src/Wire/ConversationSubsystem/Notify.hs | 14 +++++++ .../src/Wire/ConversationSubsystem/Update.hs | 13 ++++++ services/galley/src/Galley/API/Federation.hs | 1 + 11 files changed, 133 insertions(+), 1 deletion(-) diff --git a/changelog.d/2-features/WPB-26650 b/changelog.d/2-features/WPB-26650 index 0d4e172ee34..3c13d53b4f0 100644 --- a/changelog.d/2-features/WPB-26650 +++ b/changelog.d/2-features/WPB-26650 @@ -1 +1 @@ -Add federation support for `preventAdminlessGroups` system notifications, with capability-aware handling for older remote backends. +Add federation support for `preventAdminlessGroups` system notifications, with capability-aware handling for older remote backends. (#5525, #5540) diff --git a/integration/test/Test/AdminlessGroups.hs b/integration/test/Test/AdminlessGroups.hs index 01884aa9d60..094aa9ebbc3 100644 --- a/integration/test/Test/AdminlessGroups.hs +++ b/integration/test/Test/AdminlessGroups.hs @@ -19,6 +19,7 @@ module Test.AdminlessGroups where import API.Brig import API.Galley +import qualified API.Galley as Public import API.GalleyInternal hiding (getConversation) import qualified API.GalleyInternal as GalleyI import Control.Concurrent (threadDelay) @@ -460,6 +461,46 @@ testAdminlessSetupSendsReminderWithRemoteMembers = do bindResponse (GalleyI.getConversation conv) $ \resp -> do resp.status `shouldMatchInt` 200 +testAdminlessSetupSendsReminderWithOriginUserAndRemoteMembers :: (HasCallStack) => App () +testAdminlessSetupSendsReminderWithOriginUserAndRemoteMembers = do + -- Enabling the feature through the public API preserves the origin user. + (alice, tid, _) <- createTeam OwnDomain 1 + remoteUser <- randomUser OtherDomain def + connectTwoUsers alice remoteUser + + configureAdminlessGroupsFeature OwnDomain tid "disabled" "10s" ["1s"] + + alice1 <- createMLSClient def alice + remoteUser1 <- createMLSClient def remoteUser + traverse_ (uploadNewKeyPackage def) [alice1, remoteUser1] + + conv <- createTeamMLSConversation alice tid alice1 [remoteUser] + let newApp = def {name = "adminless-federated-origin-reminder-app", description = "not eligible for promotion"} + (app, _) <- createAndAddAppMember alice tid alice1 conv newApp + + removeMember alice conv alice >>= assertSuccess + + withWebSockets [app, remoteUser] $ \[wsApp, wsRemoteUser] -> do + Public.setTeamFeatureConfig alice tid "preventAdminlessGroups" (mkAdminlessFeature "enabled" "10s" ["1s"]) + >>= assertSuccess + + reminder <- awaitMatchFor 20 isConvAdminlessReminderNotif wsApp + reminder %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + reminder %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + + remoteReminder <- awaitMatchFor 20 isConvAdminlessReminderNotif wsRemoteUser + remoteReminder %. "payload.0.qualified_conversation" `shouldMatch` objQidObject conv + remoteReminder %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + + deleteNotif <- awaitMatchFor 20 isConvDeleteNotif wsApp + deleteNotif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + + remoteDeleteNotif <- awaitMatchFor 20 isConvDeleteNotif wsRemoteUser + remoteDeleteNotif %. "payload.0.qualified_from" `shouldMatch` objQidObject alice + + bindResponse (GalleyI.getConversation conv) $ \resp -> do + resp.status `shouldMatchInt` 404 + testAdminlessSetupAutopromotesWithRemoteMembers :: (HasCallStack) => App () testAdminlessSetupAutopromotesWithRemoteMembers = do -- Autopromotion is safe with remote members because the owning backend is diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API.hs b/libs/wire-api-federation/src/Wire/API/Federation/API.hs index e0934edc118..552783fb69a 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API.hs @@ -37,6 +37,7 @@ module Wire.API.Federation.API makeSystemMemberUpdateBundle, makeSystemDeleteBundle, makeSystemAdminlessReminderBundle, + makeAdminlessReminderBundle, ) where diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs index 0e91ac2790a..f02f1fdf7a6 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs @@ -52,6 +52,7 @@ data GalleyNotificationTag | OnUserDeletedConversationsTag | OnSystemMemberUpdateTag | OnSystemDeleteTag + | OnAdminlessReminderTag | OnSystemAdminlessReminderTag deriving (Show, Eq, Generic, Bounded, Enum) @@ -104,6 +105,11 @@ instance HasNotificationEndpoint 'OnSystemAdminlessReminderTag where type NotificationPath 'OnSystemAdminlessReminderTag = "on-conversation-system-adminless-reminder" type NotificationMods 'OnSystemAdminlessReminderTag = '[From 'V4] +instance HasNotificationEndpoint 'OnAdminlessReminderTag where + type Payload 'OnAdminlessReminderTag = AdminlessReminderNotification + type NotificationPath 'OnAdminlessReminderTag = "on-conversation-adminless-reminder" + type NotificationMods 'OnAdminlessReminderTag = '[From 'V4] + -- | All the notification endpoints return an 'EmptyResponse'. type GalleyNotificationAPI = NotificationFedEndpoint 'OnClientRemovedTag @@ -114,6 +120,7 @@ type GalleyNotificationAPI = :<|> NotificationFedEndpoint 'OnUserDeletedConversationsTag :<|> NotificationFedEndpoint 'OnSystemMemberUpdateTag :<|> NotificationFedEndpoint 'OnSystemDeleteTag + :<|> NotificationFedEndpoint 'OnAdminlessReminderTag :<|> NotificationFedEndpoint 'OnSystemAdminlessReminderTag data ClientRemovedRequest = ClientRemovedRequest @@ -247,6 +254,19 @@ data SystemAdminlessReminderNotification = SystemAdminlessReminderNotification instance ToSchema SystemAdminlessReminderNotification +data AdminlessReminderNotification = AdminlessReminderNotification + { time :: UTCTime, + conversation :: ConvId, + origUserId :: Qualified UserId, + reminder :: AdminlessReminder, + alreadyPresentUsers :: [UserId] + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform AdminlessReminderNotification) + deriving (ToJSON, FromJSON) via (CustomEncoded AdminlessReminderNotification) + +instance ToSchema AdminlessReminderNotification + conversationUpdateToV0 :: ConversationUpdate -> ConversationUpdateV0 conversationUpdateToV0 cu = ConversationUpdateV0 diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Util.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Util.hs index 61afbcf27f2..fbcf1facb14 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Util.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Util.hs @@ -39,3 +39,7 @@ makeSystemDeleteBundle = makeSystemAdminlessReminderBundle :: SystemAdminlessReminderNotification -> FedQueueClient 'Galley (PayloadBundle 'Galley) makeSystemAdminlessReminderBundle = fmap (\bundle -> bundle {unsupportedVersionPolicy = DropIfUnsupported}) . makeBundle @'OnSystemAdminlessReminderTag + +makeAdminlessReminderBundle :: AdminlessReminderNotification -> FedQueueClient 'Galley (PayloadBundle 'Galley) +makeAdminlessReminderBundle = + fmap (\bundle -> bundle {unsupportedVersionPolicy = DropIfUnsupported}) . makeBundle @'OnAdminlessReminderTag diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs index 3002ff3b013..89d9267388a 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs @@ -316,6 +316,10 @@ data ConversationSubsystem m a where Domain -> SystemAdminlessReminderNotification -> ConversationSubsystem m EmptyResponse + FederationOnAdminlessReminder :: + Domain -> + AdminlessReminderNotification -> + ConversationSubsystem m EmptyResponse FederationOnUserDeleted :: Domain -> UserDeletedConversationsNotification -> diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs index 93e4aa44d97..5cf578c7341 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs @@ -289,6 +289,38 @@ onSystemAdminlessReminder requestingDomain notification = do (Set.fromList localMembers) pure EmptyResponse +onAdminlessReminder :: + ( Member E.ConversationStore r, + Member NotificationSubsystem r, + Member ExternalAccess r, + Member (Input (Local ())) r, + Member P.TinyLog r + ) => + Domain -> + AdminlessReminderNotification -> + Sem r EmptyResponse +onAdminlessReminder requestingDomain notification = do + loc <- qualifyLocal () + localMembers <- + filterSystemNotificationRecipients + requestingDomain + notification.conversation + notification.alreadyPresentUsers + pushConversationEvent + Nothing + () + ( Event + (Qualified notification.conversation requestingDomain) + Nothing + (EventFromUser notification.origUserId) + notification.time + Nothing + (EdAdminlessReminder notification.reminder) + ) + (qualifyAs loc localMembers) + [] + pure EmptyResponse + filterSystemNotificationRecipients :: ( Member E.ConversationStore r, Member P.TinyLog r diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs index 5adbbcb9305..c4265369a46 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs @@ -209,6 +209,8 @@ interpretConversationSubsystem = interpret $ \case mapErrors $ Federation.onSystemDelete domain notification FederationOnSystemAdminlessReminder domain notification -> mapErrors $ Federation.onSystemAdminlessReminder domain notification + FederationOnAdminlessReminder domain notification -> + mapErrors $ Federation.onAdminlessReminder domain notification FederationOnUserDeleted domain udcn -> mapErrors $ Federation.onUserDeleted domain udcn PostOtrMessageUnqualified lusr con cnv ignore report msg -> diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Notify.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Notify.hs index 2e3f65227b1..d76ea3ff58e 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Notify.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Notify.hs @@ -21,6 +21,7 @@ module Wire.ConversationSubsystem.Notify sendSystemMemberUpdate, sendSystemDelete, sendSystemAdminlessReminder, + sendAdminlessReminder, ) where @@ -162,3 +163,16 @@ sendSystemAdminlessReminder targets n = void $ makeSystemAdminlessReminderBundle (SystemAdminlessReminderNotification n.time n.conversation n.reminder (tUnqualified ruids)) >>= sendBundle + +sendAdminlessReminder :: + ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r + ) => + Set (Remote UserId) -> + AdminlessReminderNotification -> + Sem r () +sendAdminlessReminder targets n = void $ + enqueueNotificationsConcurrently Q.Persistent (toList targets) $ \ruids -> + makeAdminlessReminderBundle + (AdminlessReminderNotification n.time n.conversation n.origUserId n.reminder (tUnqualified ruids)) + >>= sendBundle diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs index b6d079602e9..caa7f328b3e 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs @@ -1444,6 +1444,7 @@ adminlessTryAutopromote mlusr lcnv altAction = do { time = now, conversation = tUnqualified lcnv, update = memberUpdateData candidate update, + -- Filled per remote backend by Notify.sendSystemMemberUpdate. alreadyPresentUsers = [] } Notify.pushSystemEvent @@ -1518,6 +1519,7 @@ adminlessAutopromoteOrDelete mlusr lcnv = adminlessTryAutopromote mlusr lcnv orA SystemDeleteNotification { time = now, conversation = tUnqualified lcnv, + -- Filled per remote backend by Notify.sendSystemDelete. alreadyPresentUsers = [] } Notify.pushSystemEvent @@ -1556,6 +1558,16 @@ adminlessAutopromoteOrSendReminder mlusr lcnv deletionScheduledFor = adminlessTr now (conv.metadata.cnvmTeam) (EdAdminlessReminder (AdminlessReminder deletionScheduledFor)) + Notify.sendAdminlessReminder + (Set.fromList (map (.id_) conv.remoteMembers)) + AdminlessReminderNotification + { time = now, + conversation = tUnqualified lcnv, + reminder = AdminlessReminder deletionScheduledFor, + origUserId = tUntagged lusr, + -- Filled per remote backend by Notify.sendAdminlessReminder. + alreadyPresentUsers = [] + } pushConversationEvent Nothing conv event (qualifyAs lcnv (map (.id_) conv.localMembers)) [] Nothing -> do Notify.sendSystemAdminlessReminder @@ -1564,6 +1576,7 @@ adminlessAutopromoteOrSendReminder mlusr lcnv deletionScheduledFor = adminlessTr { time = now, conversation = tUnqualified lcnv, reminder = AdminlessReminder deletionScheduledFor, + -- Filled per remote backend by Notify.sendSystemAdminlessReminder. alreadyPresentUsers = [] } Notify.pushSystemEvent diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index a3d98e02464..239d6757533 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -62,6 +62,7 @@ federationSitemap = :<|> Named @"on-user-deleted-conversations" federationOnUserDeleted :<|> Named @"on-conversation-system-member-update" federationOnSystemMemberUpdate :<|> Named @"on-conversation-system-delete" federationOnSystemDelete + :<|> Named @"on-conversation-adminless-reminder" federationOnAdminlessReminder :<|> Named @"on-conversation-system-adminless-reminder" federationOnSystemAdminlessReminder onConversationUpdatedV0 :: From 435d3439ecd5e66af93c1b50570e74dc0d49546d Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 18 Sep 2026 13:13:53 +0200 Subject: [PATCH 27/29] WPB-28709 additional logging on failed mls commits (#5544) --- changelog.d/5-internal/WPB-28709 | 1 + .../Wire/ConversationSubsystem/Interpreter.hs | 25 +++++++-- .../Wire/ConversationSubsystem/MLS/Util.hs | 51 +++++++++++++------ 3 files changed, 58 insertions(+), 19 deletions(-) create mode 100644 changelog.d/5-internal/WPB-28709 diff --git a/changelog.d/5-internal/WPB-28709 b/changelog.d/5-internal/WPB-28709 new file mode 100644 index 00000000000..5d6b3dc8eb7 --- /dev/null +++ b/changelog.d/5-internal/WPB-28709 @@ -0,0 +1 @@ +Add diagnostic logging for failed MLS commit-bundle operations, including typed failures and exceptions during commit-lock handling. diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs index c4265369a46..70428536c30 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs @@ -25,15 +25,18 @@ module Wire.ConversationSubsystem.Interpreter ) where +import Data.Aeson qualified as A +import Data.Aeson.Types qualified as AT import Data.Qualified +import Data.Text qualified as Text import Imports -import Network.Wai.Utilities.JSONResponse (JSONResponse) +import Network.Wai.Utilities.JSONResponse (JSONResponse (..)) import Polysemy import Polysemy.Async (Async) import Polysemy.Error import Polysemy.Input import Polysemy.Resource (Resource) -import Polysemy.TinyLog (TinyLog) +import Polysemy.TinyLog (TinyLog, logErrors) import Wire.API.Conversation.Config import Wire.API.Error import Wire.API.Federation.Client (FederatorClient) @@ -87,6 +90,14 @@ import Wire.TeamSubsystem (TeamSubsystem) import Wire.UserClientIndexStore (UserClientIndexStore) import Wire.UserGroupStore (UserGroupStore) +renderConversationSubsystemError :: ConversationSubsystemError -> Text +renderConversationSubsystemError errorValue = + let response = toResponse errorValue + label = case response.value of + A.Object object -> fromMaybe "unknown" (AT.parseMaybe (A..: "label") object) + _ -> "unknown" + in "status=" <> Text.pack (show response.status) <> " label=" <> label + interpretConversationSubsystem :: ( Member MeetingNotifier r, Member (Error ConversationSubsystemError) r, @@ -152,9 +163,15 @@ interpretConversationSubsystem = interpret $ \case InternalGetLocalMember cid uid -> mapErrors $ ConvStore.getLocalMember cid uid PostMLSCommitBundle loc qusr c ctype qConvOrSub conn oosCheck bundle -> - mapErrors $ MLSMessage.postMLSCommitBundle loc qusr c ctype qConvOrSub conn oosCheck bundle + logErrors @_ @ConversationSubsystemError + renderConversationSubsystemError + "MLS commit bundle failed" + (mapErrors $ MLSMessage.postMLSCommitBundle loc qusr c ctype qConvOrSub conn oosCheck bundle) PostMLSCommitBundleFromLocalUser v lusr c conn bundle -> - mapErrors $ MLSMessage.postMLSCommitBundleFromLocalUser v lusr c conn bundle + logErrors @_ @ConversationSubsystemError + renderConversationSubsystemError + "MLS commit bundle failed" + (mapErrors $ MLSMessage.postMLSCommitBundleFromLocalUser v lusr c conn bundle) PostMLSMessage loc qusr c ctype qconvOrSub con oosCheck msg -> mapErrors $ MLSMessage.postMLSMessage loc qusr c ctype qconvOrSub con oosCheck msg PostMLSMessageFromLocalUser v lusr c conn smsg -> diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Util.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Util.hs index b862e488b09..e5ff6133bc9 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Util.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Util.hs @@ -28,7 +28,7 @@ import Data.Text qualified as T import Imports import Polysemy import Polysemy.Error -import Polysemy.Resource (Resource, bracket) +import Polysemy.Resource (Resource, bracket, onException) import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as TinyLog import System.Logger qualified as Log @@ -123,25 +123,46 @@ withCommitLock lConvOrSubId gid epoch = Nothing throwS @'MLSStaleMessage ) - (const $ releaseCommitLock gid epoch) ( const $ do - actualEpoch <- - fromMaybe (Epoch 0) <$> case tUnqualified lConvOrSubId of - Conv cnv -> getConversationEpoch cnv - SubConv cnv sub -> getSubConversationEpoch cnv sub - unless (actualEpoch == epoch) $ do - logStaleCommitLock - "commit-lock-epoch-mismatch" - lConvOrSubId - gid - epoch - (Just actualEpoch) - throwS @'MLSStaleMessage - k () + releaseCommitLock gid epoch + `onException` (logCommitLockFailure "release" lConvOrSubId gid epoch) + ) + ( const $ + ( do + actualEpoch <- + fromMaybe (Epoch 0) <$> case tUnqualified lConvOrSubId of + Conv cnv -> getConversationEpoch cnv + SubConv cnv sub -> getSubConversationEpoch cnv sub + unless (actualEpoch == epoch) $ do + logStaleCommitLock + "commit-lock-epoch-mismatch" + lConvOrSubId + gid + epoch + (Just actualEpoch) + throwS @'MLSStaleMessage + k () + ) + `onException` logCommitLockFailure "operation" lConvOrSubId gid epoch ) where ttl = fromIntegral (600 :: Int) -- 10 minutes +logCommitLockFailure :: + (Member TinyLog r) => + ByteString -> + Local ConvOrSubConvId -> + GroupId -> + Epoch -> + Sem r () +logCommitLockFailure phase lConvOrSubId gid epoch = + TinyLog.warn $ + Log.msg ("MLS commit lock operation failed" :: ByteString) + . Log.field "phase" phase + . Log.field "groupId" ("0x" <> hex (unGroupId gid)) + . Log.field "epoch" (epochNumber epoch) + . Log.field "convOrSubConvId" (toByteString' (show (tUnqualified lConvOrSubId))) + logStaleCommitLock :: (Member TinyLog r) => ByteString -> From 1dc34ef44646a25a97e10d8fb39499b787d5c5c4 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 18 Sep 2026 16:51:25 +0200 Subject: [PATCH 28/29] revert redis retirement (#5545) * Revert "WPB-28645: presence cleanup must not swallow async exceptions (#5535)" This reverts commit da4e4baafd00fe3c5fd5f03d60c806bddca923f3. * Revert "WPB-28377: migrate gundeck presence from redis to PostGreSQL (#5493)" This reverts commit 9a39b9cb4a87f21de51d3e6bcd3eea5c6008aedf. * Make gundeck config compatible with Redis and Postgres --- Makefile | 4 +- .../0-release-notes/WPB-28377-remove-redis | 17 -- changelog.d/3-bug-fixes/WPB-28645 | 4 - .../WPB-28377-gundeck-presence-postgres | 1 - charts/databases-ephemeral/requirements.yaml | 7 + .../databases-ephemeral/templates/NOTES.txt | 1 + .../templates/integration-integration.yaml | 27 +- charts/integration/templates/secret.yaml | 6 + charts/reaper/.helmignore | 21 ++ charts/reaper/Chart.yaml | 10 + charts/reaper/README.md | 71 +++++ charts/reaper/scripts/reaper.sh | 89 +++++++ charts/reaper/templates/_helpers.tpl | 26 ++ charts/reaper/templates/configmap.yaml | 10 + charts/reaper/templates/deployment.yaml | 81 ++++++ charts/reaper/templates/rbac.yaml | 38 +++ charts/reaper/values.yaml | 45 ++++ charts/redis-ephemeral/Chart.yaml | 4 + charts/redis-ephemeral/requirements.yaml | 5 + charts/redis-ephemeral/values.yaml | 60 +++++ charts/wire-server/templates/_helpers.tpl | 40 +++ .../templates/cannon/statefulset.yaml | 2 +- .../templates/gundeck/configmap.yaml | 24 ++ .../templates/gundeck/deployment.yaml | 49 +++- .../templates/gundeck/redis-ca-secret.yaml | 30 +++ .../wire-server/templates/gundeck/secret.yaml | 12 + .../templates/gundeck/tests/configmap.yaml | 7 + .../gundeck/tests/gundeck-integration.yaml | 37 +++ .../templates/gundeck/tests/secret.yaml | 6 + charts/wire-server/values.yaml | 40 ++- deploy/dockerephemeral/docker-compose.yaml | 142 ++++++++++ .../docker/redis-master-mode.conf | 1 + .../docker/redis-node-1-cert.pem | 19 ++ .../docker/redis-node-1-key.pem | 28 ++ .../dockerephemeral/docker/redis-node-1.conf | 17 ++ .../docker/redis-node-2-cert.pem | 19 ++ .../docker/redis-node-2-key.pem | 28 ++ .../dockerephemeral/docker/redis-node-2.conf | 17 ++ .../docker/redis-node-3-cert.pem | 19 ++ .../docker/redis-node-3-key.pem | 28 ++ .../dockerephemeral/docker/redis-node-3.conf | 17 ++ .../docker/redis-node-4-cert.pem | 19 ++ .../docker/redis-node-4-key.pem | 28 ++ .../dockerephemeral/docker/redis-node-4.conf | 17 ++ .../docker/redis-node-5-cert.pem | 19 ++ .../docker/redis-node-5-key.pem | 28 ++ .../dockerephemeral/docker/redis-node-5.conf | 17 ++ .../docker/redis-node-6-cert.pem | 19 ++ .../docker/redis-node-6-key.pem | 28 ++ .../dockerephemeral/docker/redis-node-6.conf | 17 ++ docs/src/developer/developer/building.md | 1 + .../src/developer/reference/config-options.md | 94 ++++++- .../install/infrastructure-configuration.md | 5 +- docs/src/how-to/install/troubleshooting.md | 4 +- hack/bin/gen-certs.sh | 13 + hack/helm_vars/certs/values.yaml.gotmpl | 53 ++++ hack/helm_vars/redis-ephemeral/values.yaml | 47 ++++ hack/helm_vars/wire-server/values.yaml.gotmpl | 20 +- hack/helmfile.yaml.gotmpl | 36 +++ libs/wire-api/src/Wire/API/Presence.hs | 7 +- .../Test/Wire/API/Golden/Manual/Presence.hs | 3 + .../20260828093750-gundeck-presence.sql | 12 - .../src/Wire/JobSubsystem/Migrations.hs | 4 +- libs/wire-subsystems/src/Wire/Postgres.hs | 1 - postgres-schema.sql | 30 --- services/brig/src/Brig/Run.hs | 5 - services/gundeck/default.nix | 17 +- services/gundeck/gundeck.cabal | 14 +- services/gundeck/gundeck.integration.yaml | 23 +- services/gundeck/src/Gundeck/Env.hs | 81 +++++- services/gundeck/src/Gundeck/Monad.hs | 73 +++++ services/gundeck/src/Gundeck/Options.hs | 32 ++- services/gundeck/src/Gundeck/Presence.hs | 4 +- services/gundeck/src/Gundeck/Presence/Data.hs | 252 ++++++++---------- services/gundeck/src/Gundeck/Push.hs | 2 +- .../gundeck/src/Gundeck/Push/Websocket.hs | 6 +- services/gundeck/src/Gundeck/Redis.hs | 127 +++++++++ services/gundeck/src/Gundeck/Run.hs | 35 +-- services/gundeck/src/Gundeck/Util/Redis.hs | 61 +++++ services/gundeck/test/integration/API.hs | 67 ++++- services/gundeck/test/integration/Main.hs | 9 +- .../gundeck/test/integration/TestSetup.hs | 8 +- services/gundeck/test/integration/Util.hs | 119 +++++++++ services/gundeck/test/unit/MockGundeck.hs | 1 + services/integration.yaml | 6 + 85 files changed, 2244 insertions(+), 329 deletions(-) delete mode 100644 changelog.d/0-release-notes/WPB-28377-remove-redis delete mode 100644 changelog.d/3-bug-fixes/WPB-28645 delete mode 100644 changelog.d/5-internal/WPB-28377-gundeck-presence-postgres create mode 100644 charts/reaper/.helmignore create mode 100644 charts/reaper/Chart.yaml create mode 100644 charts/reaper/README.md create mode 100755 charts/reaper/scripts/reaper.sh create mode 100644 charts/reaper/templates/_helpers.tpl create mode 100644 charts/reaper/templates/configmap.yaml create mode 100644 charts/reaper/templates/deployment.yaml create mode 100644 charts/reaper/templates/rbac.yaml create mode 100644 charts/reaper/values.yaml create mode 100644 charts/redis-ephemeral/Chart.yaml create mode 100644 charts/redis-ephemeral/requirements.yaml create mode 100644 charts/redis-ephemeral/values.yaml create mode 100644 charts/wire-server/templates/gundeck/redis-ca-secret.yaml create mode 100644 deploy/dockerephemeral/docker/redis-master-mode.conf create mode 100644 deploy/dockerephemeral/docker/redis-node-1-cert.pem create mode 100644 deploy/dockerephemeral/docker/redis-node-1-key.pem create mode 100644 deploy/dockerephemeral/docker/redis-node-1.conf create mode 100644 deploy/dockerephemeral/docker/redis-node-2-cert.pem create mode 100644 deploy/dockerephemeral/docker/redis-node-2-key.pem create mode 100644 deploy/dockerephemeral/docker/redis-node-2.conf create mode 100644 deploy/dockerephemeral/docker/redis-node-3-cert.pem create mode 100644 deploy/dockerephemeral/docker/redis-node-3-key.pem create mode 100644 deploy/dockerephemeral/docker/redis-node-3.conf create mode 100644 deploy/dockerephemeral/docker/redis-node-4-cert.pem create mode 100644 deploy/dockerephemeral/docker/redis-node-4-key.pem create mode 100644 deploy/dockerephemeral/docker/redis-node-4.conf create mode 100644 deploy/dockerephemeral/docker/redis-node-5-cert.pem create mode 100644 deploy/dockerephemeral/docker/redis-node-5-key.pem create mode 100644 deploy/dockerephemeral/docker/redis-node-5.conf create mode 100644 deploy/dockerephemeral/docker/redis-node-6-cert.pem create mode 100644 deploy/dockerephemeral/docker/redis-node-6-key.pem create mode 100644 deploy/dockerephemeral/docker/redis-node-6.conf create mode 100644 hack/helm_vars/redis-ephemeral/values.yaml delete mode 100644 libs/wire-subsystems/postgres-migrations/20260828093750-gundeck-presence.sql create mode 100644 services/gundeck/src/Gundeck/Redis.hs create mode 100644 services/gundeck/src/Gundeck/Util/Redis.hs create mode 100644 services/gundeck/test/integration/Util.hs diff --git a/Makefile b/Makefile index 0e2e771c983..2a3250275e6 100644 --- a/Makefile +++ b/Makefile @@ -13,11 +13,11 @@ CHARTS_INTEGRATION := wire-server databases-ephemeral rabbitmq fake-aws ingre # (e.g. move charts/brig to charts/wire-server/brig) # this list could be generated from the folder names under ./charts/ like so: # CHARTS_RELEASE := $(shell find charts/ -maxdepth 1 -type d | xargs -n 1 basename | grep -v charts) -CHARTS_RELEASE := wire-server rabbitmq rabbitmq-external databases-ephemeral \ +CHARTS_RELEASE := wire-server redis-ephemeral rabbitmq rabbitmq-external databases-ephemeral \ fake-aws fake-aws-s3 fake-aws-sqs aws-ingress fluent-bit kibana backoffice \ calling-test demo-smtp elasticsearch-curator elasticsearch-external \ elasticsearch-ephemeral minio-external cassandra-external \ -ingress-nginx-controller nginx-ingress-services \ +ingress-nginx-controller nginx-ingress-services reaper \ k8ssandra-test-cluster ldap-scim-bridge wire-server-enterprise \ wire-ingress KIND_CLUSTER_NAME := wire-server diff --git a/changelog.d/0-release-notes/WPB-28377-remove-redis b/changelog.d/0-release-notes/WPB-28377-remove-redis deleted file mode 100644 index b7f75b78e85..00000000000 --- a/changelog.d/0-release-notes/WPB-28377-remove-redis +++ /dev/null @@ -1,17 +0,0 @@ -Gundeck no longer uses redis: presence tracking is stored in PostgreSQL. - -Operators must: - -- Remove redis deployments that were only used by gundeck, and the gundeck - `redis:` and `redisAdditionalWrite:` configuration, the `REDIS_USERNAME`, - `REDIS_PASSWORD`, `REDIS_ADDITIONAL_WRITE_USERNAME` and - `REDIS_ADDITIONAL_WRITE_PASSWORD` environment variables, and gundeck redis - TLS secrets (`redisUsername`/`redisPassword`/`redisAdditionalWrite*` secrets - and the redis CA certificates). -- Add the new required configuration `gundeck.config.postgresql` (plus - `postgresqlPool`, and optionally `secrets.pgPassword` for the password file), - following the same format as brig's postgresql settings. -- Restart all cannons after deployment is successful. Presence data does - not carry over, this will make sure all clients reconnect after the - presence data is being written to PostgreSQL. -- Stop deploying `redis-ephemeral` and `reaper`, these have been removed. diff --git a/changelog.d/3-bug-fixes/WPB-28645 b/changelog.d/3-bug-fixes/WPB-28645 deleted file mode 100644 index 570d00f42f0..00000000000 --- a/changelog.d/3-bug-fixes/WPB-28645 +++ /dev/null @@ -1,4 +0,0 @@ -Gundeck's presence cleanup background thread no longer swallows asynchronous -exceptions. On shutdown this removes the spurious "presence cleanup failed" -(AsyncCancelled) error log line and lets the thread terminate promptly instead -of lingering for up to an hour. diff --git a/changelog.d/5-internal/WPB-28377-gundeck-presence-postgres b/changelog.d/5-internal/WPB-28377-gundeck-presence-postgres deleted file mode 100644 index 3862322c3a1..00000000000 --- a/changelog.d/5-internal/WPB-28377-gundeck-presence-postgres +++ /dev/null @@ -1 +0,0 @@ -Gundeck presence tracking moved from redis to postgres; redis and the gundeck redis configuration options are gone. (WPB-28377) diff --git a/charts/databases-ephemeral/requirements.yaml b/charts/databases-ephemeral/requirements.yaml index fff1534c387..74dbd7594d8 100644 --- a/charts/databases-ephemeral/requirements.yaml +++ b/charts/databases-ephemeral/requirements.yaml @@ -13,6 +13,13 @@ dependencies: # since cassandra-migrations did not yet run; but the cassandra-migrations hook # requires all pods to be in a 'Ready' state before starting (condition for post-install); this is impossible. ##################################################### +- name: redis-ephemeral + version: "0.0.42" + repository: "file://../redis-ephemeral" + tags: + - redis-ephemeral + - databases-ephemeral + - demo - name: elasticsearch-ephemeral version: "0.0.42" repository: "file://../elasticsearch-ephemeral" diff --git a/charts/databases-ephemeral/templates/NOTES.txt b/charts/databases-ephemeral/templates/NOTES.txt index 7f07b9ed7ca..2e2ad5b0592 100644 --- a/charts/databases-ephemeral/templates/NOTES.txt +++ b/charts/databases-ephemeral/templates/NOTES.txt @@ -2,6 +2,7 @@ You now have an in-memory, non-persistent, non-highly-available set of databases * cassandra-ephemeral * elasticsearch-ephemeral +* redis-ephemeral !! WARNING WARNING !! This is fine for testing and demo purposes, but NOT for a production use case. diff --git a/charts/integration/templates/integration-integration.yaml b/charts/integration/templates/integration-integration.yaml index 4841ef372b7..9fea9fbde3e 100644 --- a/charts/integration/templates/integration-integration.yaml +++ b/charts/integration/templates/integration-integration.yaml @@ -41,10 +41,6 @@ spec: configMap: name: "gundeck" - - name: "gundeck-secrets" - secret: - secretName: "gundeck" - - name: "cargohold-config" configMap: name: "cargohold" @@ -97,6 +93,9 @@ spec: secret: secretName: {{ .Values.config.elasticsearch.tlsCaSecretRef.name }} + - name: redis-ca + secret: + secretName: {{ .Values.config.redis.tlsCaSecretRef.name }} - name: rabbitmq-ca secret: @@ -238,9 +237,6 @@ spec: - name: gundeck-config mountPath: /etc/wire/gundeck/conf - - name: gundeck-secrets - mountPath: /etc/wire/gundeck/secrets - - name: cargohold-config mountPath: /etc/wire/cargohold/conf @@ -280,6 +276,9 @@ spec: - name: elasticsearch-ca mountPath: /etc/wire/brig/elasticsearch-ca + - name: redis-ca + mountPath: /etc/wire/gundeck/redis-ca + - name: rabbitmq-ca mountPath: /etc/wire/brig/rabbitmq-ca @@ -344,6 +343,20 @@ spec: - name: ENABLE_FEDERATION_V{{$version}} value: "1" {{- end }} + {{- if hasKey .Values.secrets "redisUsername" }} + - name: REDIS_USERNAME + valueFrom: + secretKeyRef: + name: integration + key: redisUsername + {{- end }} + {{- if hasKey .Values.secrets "redisPassword" }} + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: integration + key: redisPassword + {{- end }} - name: TEST_XML value: /tmp/result.xml {{- if .Values.config.uploadXml }} diff --git a/charts/integration/templates/secret.yaml b/charts/integration/templates/secret.yaml index 34e6698ed2f..32f6085176e 100644 --- a/charts/integration/templates/secret.yaml +++ b/charts/integration/templates/secret.yaml @@ -16,4 +16,10 @@ data: {{- if hasKey . "uploadXmlAwsSecretAccessKey" }} uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} {{- end }} + {{- if hasKey . "redisUsername" }} + redisUsername: {{ .redisUsername | b64enc | quote }} + {{- end }} + {{- if hasKey . "redisPassword" }} + redisPassword: {{ .redisPassword | b64enc | quote }} + {{- end }} {{- end }} diff --git a/charts/reaper/.helmignore b/charts/reaper/.helmignore new file mode 100644 index 00000000000..f0c13194444 --- /dev/null +++ b/charts/reaper/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/charts/reaper/Chart.yaml b/charts/reaper/Chart.yaml new file mode 100644 index 00000000000..131654fa443 --- /dev/null +++ b/charts/reaper/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +version: 0.0.42 +name: reaper +appVersion: 0.1.0 +description: A helm charts to restart cannons if redis-ephemeal has died +annotations: + # must conform to https://github.com/helm/community/blob/main/hips/hip-0015.md + helm.sh/images: | + - name: kubectl + image: docker.io/alpine/kubectl:1.36.3 diff --git a/charts/reaper/README.md b/charts/reaper/README.md new file mode 100644 index 00000000000..f4b73e4e670 --- /dev/null +++ b/charts/reaper/README.md @@ -0,0 +1,71 @@ +Reaper +------ + +This pod is useful in the following scenario: You run wire-server alongside a single +redis-ephemeral (part of databases-ephemeral). If you have a different setup for redis, +do not use this chart. + +Due to the nature of pods and their ephemerality, there might be situations where a +redis-ephemeral pod is restarted. In such cases, wire clients will have stale +connections (they will have an active websocket connection, but gundeck (responsible for +sending messages) will be unaware of this (as the record of who is connected where is +gone with a redis-ephemeral restart). So these stale clients will not receive any +messages. Here, this reaper will check that the `redis-ephemeral` pod is older than any +other `cannon`; if that is not the case, it kills the `cannon`s forcing clients to +reconnect. + +Image +----- + +The reaper runs `scripts/reaper.sh` through `kubectl`, so `image` must point at a +kubectl image that **contains a POSIX shell** at `/bin/sh`. Distroless kubectl images +do not ship one and the pod will fail to start. The script itself is POSIX sh, so +busybox `ash` is enough, bash not required. + +The image is fully configurable: + +```yaml +image: + registry: docker.io # set to "" for an unqualified repository + repository: alpine/kubectl + tag: 1.36.3 + digest: "" # e.g. "sha256:..."; takes precedence over tag + pullPolicy: IfNotPresent +imagePullSecrets: + - name: my-pull-secret +``` + +RBAC +---- + +The chart creates a namespaced `Role`/`RoleBinding` granting `get`, `list`, `watch` and +`delete` on pods, bound to a `-reaper` ServiceAccount. + +`watch` is required even though the script never watches anything explicitly: +`kubectl delete pod` blocks until the pod is gone and opens a watch to do so. Without it +the reaper deletes the first cannon and then hangs, without crashing. + +Earlier versions bound the ServiceAccount to `cluster-admin` through a fixed-name +`ClusterRoleBinding`, which gave the pod read access to every Secret in the cluster. +`helm upgrade` removes that binding and the old `reaper-role` ServiceAccount. Because +nothing is cluster-scoped any more and all names are release-scoped, several reaper +releases can now coexist in one cluster; previously a second release failed to install +with a `ClusterRoleBinding` ownership conflict. + +Runtime +------- + +The container runs as uid/gid 65534 with a read-only root filesystem and has resource +requests and limits. `nodeSelector`, `tolerations` and `affinity` are honoured. + +`checkIntervalSeconds` (default `15`) controls how long the script waits between checks. +Earlier versions listed pods once per second. + +Logs distinguish a failure to reach the API from "there are no matching pods", and +include the underlying error: + + Failed to list pods: Error from server (Forbidden): ... Skipping this iteration... + No cannon pods found. Doing nothing... + +Both cases previously printed `Failed to list pods. Skipping this iteration...`, so a +reaper that could not list pods at all looked exactly like an idle one. diff --git a/charts/reaper/scripts/reaper.sh b/charts/reaper/scripts/reaper.sh new file mode 100755 index 00000000000..f67049e76a6 --- /dev/null +++ b/charts/reaper/scripts/reaper.sh @@ -0,0 +1,89 @@ +#!/bin/sh + +# See the readme of the reaper chart. +# +# This is POSIX sh on purpose: the only actively maintained kubectl images that +# ship busybox ash, not bash. + +# we loop forever, and on transient errors sleep and try again. +# setting -e would crash the pod on transient e.g. network errors, which isn't useful. +set -u +# shellcheck disable=SC3040 # busybox ash supports pipefail +set -o pipefail + +USAGE="$0 [INTERVAL_SECONDS]" +NAMESPACE="${1:?$USAGE}" +INTERVAL="${2:-15}" + +echo "Using namespace: $NAMESPACE, check interval: ${INTERVAL}s" + +kill_all_cannons() { + echo "Killing all cannons" + RAW_PODS=$(kubectl -n "$NAMESPACE" get pods 2>&1) || { + echo "Failed to list cannon pods: $RAW_PODS. Skipping this iteration..." + return + } + CANNON_PODS=$(echo "$RAW_PODS" | grep -e "cannon" | awk '{ print $1 }') || CANNON_PODS="" + + # A here-document rather than a pipeline, so the loop runs in the current + # shell and the `exit 1` below actually terminates the script. + while IFS= read -r cannon; do + if [ -n "$cannon" ]; then + echo "Deleting $cannon" + # If a single delete fails, we skip it but keep going. + kubectl -n "$NAMESPACE" delete pod "$cannon" || { + echo "Failed to delete pod $cannon, crash reaper and try again" + exit 1 + } + fi + done <&1) || { + echo "Failed to list pods: $RAW_PODS. Skipping this iteration..." + sleep "$INTERVAL" + continue + } + + # Gather all pods that contain "cannon" or "redis-ephemeral", sorted by creation time + ALL_PODS=$(echo "$RAW_PODS" | grep -e "cannon" -e "redis-ephemeral") || ALL_PODS="" + + # Check if we have any cannon pods at all + if ! echo "$ALL_PODS" | grep -q "cannon"; then + echo "No cannon pods found. Doing nothing..." + sleep "$INTERVAL" + continue + fi + + # Check if we have any redis-ephemeral pods at all + if ! echo "$ALL_PODS" | grep -q "redis-ephemeral"; then + echo "No redis-ephemeral pod found. Doing nothing..." + sleep "$INTERVAL" + continue + fi + + # At this point, we have both cannon and redis-ephemeral pods in ALL_PODS + # Check which is oldest + FIRST_POD=$(echo "$ALL_PODS" | head -n 1 | awk '{ print $1 }') + + if [ -z "$FIRST_POD" ]; then + echo "Could not determine the oldest pod from the list. Doing nothing..." + sleep "$INTERVAL" + continue + fi + + case "$FIRST_POD" in + *redis-ephemeral*) + echo "redis-ephemeral is the oldest pod, all good." + ;; + *) + kill_all_cannons + ;; + esac + + sleep "$INTERVAL" +done diff --git a/charts/reaper/templates/_helpers.tpl b/charts/reaper/templates/_helpers.tpl new file mode 100644 index 00000000000..47fc05fa161 --- /dev/null +++ b/charts/reaper/templates/_helpers.tpl @@ -0,0 +1,26 @@ +{{/* Allow KubeVersion to be overridden. */}} +{{- define "kubeVersion" -}} + {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} +{{- end -}} + +{{- define "includeSecurityContext" -}} + {{- (semverCompare ">= 1.24-0" (include "kubeVersion" .)) -}} +{{- end -}} + +{{/* Fully qualified image reference, digest taking precedence over tag. */}} +{{- define "reaper.image" -}} +{{- $repository := .Values.image.repository -}} +{{- if .Values.image.registry -}} +{{- $repository = printf "%s/%s" .Values.image.registry .Values.image.repository -}} +{{- end -}} +{{- if .Values.image.digest -}} +{{- printf "%s@%s" $repository .Values.image.digest -}} +{{- else -}} +{{- printf "%s:%s" $repository (.Values.image.tag | toString) -}} +{{- end -}} +{{- end -}} + +{{/* Release-scoped name for the ServiceAccount, Role and RoleBinding. */}} +{{- define "reaper.serviceAccountName" -}} +{{- printf "%s-reaper" .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/charts/reaper/templates/configmap.yaml b/charts/reaper/templates/configmap.yaml new file mode 100644 index 00000000000..571e81a1f4a --- /dev/null +++ b/charts/reaper/templates/configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: reaper-script + labels: + app: reaper +data: + reaper.sh: |- + {{- .Files.Get "scripts/reaper.sh" | nindent 4 }} + diff --git a/charts/reaper/templates/deployment.yaml b/charts/reaper/templates/deployment.yaml new file mode 100644 index 00000000000..9d50439dc5c --- /dev/null +++ b/charts/reaper/templates/deployment.yaml @@ -0,0 +1,81 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: reaper + labels: + app: reaper + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: 1 + selector: + matchLabels: + app: reaper + release: {{ .Release.Name }} + template: + metadata: + labels: + app: reaper + release: {{ .Release.Name }} + annotations: + # Ensure changes to the script cause a redeployment upon `helm upgrade` + checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} + spec: + serviceAccountName: {{ include "reaper.serviceAccountName" . }} + automountServiceAccountToken: true + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: reaper + containers: + - name: reaper + image: {{ include "reaper.image" . | quote }} + imagePullPolicy: {{ default "" .Values.image.pullPolicy | quote }} + command: ["/bin/sh", "/app/reaper.sh", "{{ .Release.Namespace }}", "{{ .Values.checkIntervalSeconds }}"] + {{- if eq (include "includeSecurityContext" .) "true" }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 12 }} + {{- end }} + env: + # kubectl writes its discovery cache below $HOME; the root + # filesystem is read-only, so point it at the emptyDir. + - name: HOME + value: /tmp + volumeMounts: + - name: reaper-script + mountPath: /app + readOnly: true + - name: tmp + mountPath: /tmp + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: reaper-script + configMap: + name: reaper-script + defaultMode: 0755 + items: + - key: reaper.sh + path: reaper.sh + - name: tmp + emptyDir: {} diff --git a/charts/reaper/templates/rbac.yaml b/charts/reaper/templates/rbac.yaml new file mode 100644 index 00000000000..5e4caafb6f2 --- /dev/null +++ b/charts/reaper/templates/rbac.yaml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "reaper.serviceAccountName" . }} + labels: + app: reaper + release: {{ .Release.Name }} +--- +# The reaper only ever lists and deletes pods in its own namespace, so a +# namespaced Role is sufficient. +# `watch` is required even though the script never watches anything explicitly. +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ include "reaper.serviceAccountName" . }} + labels: + app: reaper + release: {{ .Release.Name }} +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch", "delete"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ include "reaper.serviceAccountName" . }} + labels: + app: reaper + release: {{ .Release.Name }} +roleRef: + kind: Role + name: {{ include "reaper.serviceAccountName" . }} + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: {{ include "reaper.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/charts/reaper/values.yaml b/charts/reaper/values.yaml new file mode 100644 index 00000000000..cf2f56cf9e4 --- /dev/null +++ b/charts/reaper/values.yaml @@ -0,0 +1,45 @@ +image: + # The reaper executes a shell script through kubectl, so this image must + # contain a POSIX shell at /bin/sh. Distroless kubectl images do not + # ship one and the pod will fail to start with them. + # + # Set `registry` to "" to use an unqualified repository (e.g. when mirroring + # into a registry configured as the daemon default). + registry: docker.io + repository: alpine/kubectl + tag: 1.36.3 + # Optional: pin by digest (e.g. "sha256:abc..."). Takes precedence over `tag`. + digest: "" + pullPolicy: IfNotPresent + +imagePullSecrets: [] + +# How long to wait between two checks, in seconds. The condition this chart +# watches for (a redis-ephemeral restart) is rare, so there is no reason to poll +# the API server aggressively. +checkIntervalSeconds: 15 + +resources: + requests: + memory: 32Mi + cpu: 10m + limits: + memory: 64Mi + +nodeSelector: {} +tolerations: [] +affinity: {} + +# Applied as the container securityContext. runAsUser/runAsGroup are set +# explicitly because alpine/kubectl runs as root by default. +podSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + seccompProfile: + type: RuntimeDefault diff --git a/charts/redis-ephemeral/Chart.yaml b/charts/redis-ephemeral/Chart.yaml new file mode 100644 index 00000000000..c907a999576 --- /dev/null +++ b/charts/redis-ephemeral/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: Wrapper chart for https://github.com/groundhog2k/helm-charts/tree/redis-1.3.8/charts/redis +name: redis-ephemeral +version: 0.0.42 diff --git a/charts/redis-ephemeral/requirements.yaml b/charts/redis-ephemeral/requirements.yaml new file mode 100644 index 00000000000..0cdad0dfbc4 --- /dev/null +++ b/charts/redis-ephemeral/requirements.yaml @@ -0,0 +1,5 @@ +dependencies: +- name: redis + version: 1.3.8 + repository: https://groundhog2k.github.io/helm-charts/ + alias: redis-ephemeral diff --git a/charts/redis-ephemeral/values.yaml b/charts/redis-ephemeral/values.yaml new file mode 100644 index 00000000000..aafcf440e40 --- /dev/null +++ b/charts/redis-ephemeral/values.yaml @@ -0,0 +1,60 @@ +redis-ephemeral: + image: + tag: "7.4.6" + + haMode: + enabled: false + + redisConfig: | + # dont write rdb to emptyDir disk for an ephemeral setup + save "" + +# To add a password add the following to redisConfig: +# requirepass my-plaintext-password + + +# How to enable SSL connections: +# +# Add the following lines to redisConfig: +# +# port 0 +# tls-port 6379 +# tls-cert-file /data/ssl/tls.crt +# tls-key-file /data/ssl/tls.key +# tls-ca-cert-file /data/ssl/ca.crt +# tls-auth-clients no +# +# Mount the certificate and adjust probes to use SSL +# +# redis-ephemeral: +# extraRedisSecrets: +# - name: redis-certificate +# mountPath: /data/ssl +# +# livenessProbe: +# enabled: false +# customLivenessProbe: +# exec: +# command: +# - sh +# - -c +# - redis-cli --tls --cacert /data/ssl/ca.crt ping +# +# readinessProbe: +# enabled: false +# customReadinessProbe: +# exec: +# command: +# - sh +# - -c +# - redis-cli --tls --cacert /data/ssl/ca.crt ping +# +# startupProbe: +# enabled: false +# customStartupProbe: +# exec: +# command: +# - sh +# - -c +# - redis-cli --tls --cacert /data/ssl/ca.crt ping +# diff --git a/charts/wire-server/templates/_helpers.tpl b/charts/wire-server/templates/_helpers.tpl index 94aca197dc7..5edb0251456 100644 --- a/charts/wire-server/templates/_helpers.tpl +++ b/charts/wire-server/templates/_helpers.tpl @@ -106,6 +106,46 @@ {{- end -}} {{- end -}} +{{- define "gundeck.configureRedisCa" -}} +{{ or (hasKey .redis "tlsCa") (hasKey .redis "tlsCaSecretRef") }} +{{- end -}} + +{{- define "gundeck.redisTlsSecretName" -}} +{{- if .redis.tlsCaSecretRef -}} +{{ .redis.tlsCaSecretRef.name }} +{{- else }} +{{- print "gundeck-redis-ca" -}} +{{- end -}} +{{- end -}} + +{{- define "gundeck.redisTlsSecretKey" -}} +{{- if .redis.tlsCaSecretRef -}} +{{ .redis.tlsCaSecretRef.key }} +{{- else }} +{{- print "ca.pem" -}} +{{- end -}} +{{- end -}} + +{{- define "gundeck.configureAdditionalRedisCa" -}} +{{ and (hasKey . "redisAdditionalWrite") (or (hasKey .redis "additionalTlsCa") (hasKey .redis "additionalTlsCaSecretRef")) }} +{{- end -}} + +{{- define "gundeck.additionalRedisTlsSecretName" -}} +{{- if .redis.additionalTlsCaSecretRef -}} +{{ .redis.additionalTlsCaSecretRef.name }} +{{- else }} +{{- print "gundeck-additional-redis-ca" -}} +{{- end -}} +{{- end -}} + +{{- define "gundeck.additionalRedisTlsSecretKey" -}} +{{- if .redis.additionalTlsCaSecretRef -}} +{{ .redis.additionalTlsCaSecretRef.key }} +{{- else }} +{{- print "ca.pem" -}} +{{- end -}} +{{- end -}} + {{/* SPAR */}} {{- define "spar.tlsSecretRef" -}} {{- if .cassandra.tlsCaSecretRef -}} diff --git a/charts/wire-server/templates/cannon/statefulset.yaml b/charts/wire-server/templates/cannon/statefulset.yaml index f60d6586453..00103604bf8 100644 --- a/charts/wire-server/templates/cannon/statefulset.yaml +++ b/charts/wire-server/templates/cannon/statefulset.yaml @@ -2,7 +2,7 @@ # Specific pods can be accessed within the cluster at cannon-.cannon. # (the second 'cannon' is the name of the headless service) # Note: In fact, cannon-.cannon can also be used to access the service but assuming -# that we can have multiple namespaces accessing the same cannon cluster, appending `.` +# that we can have multiple namespaces accessing the same redis cluster, appending `.` # makes the service unambiguous apiVersion: apps/v1 kind: StatefulSet diff --git a/charts/wire-server/templates/gundeck/configmap.yaml b/charts/wire-server/templates/gundeck/configmap.yaml index 6aae6aa47d8..7e844043675 100644 --- a/charts/wire-server/templates/gundeck/configmap.yaml +++ b/charts/wire-server/templates/gundeck/configmap.yaml @@ -41,11 +41,35 @@ data: {{- end }} {{- end }} + redis: + host: {{ .redis.host }} + port: {{ .redis.port }} + connectionMode: {{ .redis.connectionMode }} + enableTls: {{ .redis.enableTls }} + insecureSkipVerifyTls: {{ .redis.insecureSkipVerifyTls }} + {{- if eq (include "gundeck.configureRedisCa" .) "true" }} + tlsCa: /etc/wire/gundeck/redis-ca/{{ include "gundeck.redisTlsSecretKey" . }} + {{- end }} + + {{- if .redisAdditionalWrite }} + redisAdditionalWrite: + host: {{ .redisAdditionalWrite.host }} + port: {{ .redisAdditionalWrite.port }} + connectionMode: {{ .redisAdditionalWrite.connectionMode }} + enableTls: {{ .redisAdditionalWrite.enableTls }} + insecureSkipVerifyTls: {{ .redisAdditionalWrite.insecureSkipVerifyTls }} + {{- if eq (include "gundeck.configureAdditionalRedisCa" .) "true" }} + tlsCa: /etc/wire/gundeck/additional-redis-ca/{{ include "gundeck.additionalRedisTlsSecretKey" . }} + {{- end }} + {{- end }} + + {{- if .postgresql }} postgresql: {{ toYaml .postgresql | nindent 6 }} postgresqlPool: {{ toYaml .postgresqlPool | nindent 6 }} {{- if hasKey $.Values.gundeck.secrets "pgPassword" }} postgresqlPassword: /etc/wire/gundeck/secrets/pgPassword {{- end }} + {{- end }} # Gundeck uses discovery for AWS access key / secrets # For more details, check amazonka's documentation at: diff --git a/charts/wire-server/templates/gundeck/deployment.yaml b/charts/wire-server/templates/gundeck/deployment.yaml index ff7a457fc6e..bc46a53ec0d 100644 --- a/charts/wire-server/templates/gundeck/deployment.yaml +++ b/charts/wire-server/templates/gundeck/deployment.yaml @@ -50,9 +50,16 @@ spec: secret: secretName: {{ (include "gundeck.tlsSecretRef" .Values.gundeck.config | fromYaml).name }} {{- end }} - - name: "gundeck-secrets" + {{- if eq (include "gundeck.configureRedisCa" .Values.gundeck.config) "true" }} + - name: "redis-ca" secret: - secretName: "gundeck" + secretName: {{ include "gundeck.redisTlsSecretName" .Values.gundeck.config }} + {{- end }} + {{- if eq (include "gundeck.configureAdditionalRedisCa" .Values.gundeck.config) "true" }} + - name: "additional-redis-ca" + secret: + secretName: {{ include "gundeck.additionalRedisTlsSecretName" .Values.gundeck.config }} + {{- end }} containers: - name: gundeck image: "{{ .Values.gundeck.image.repository }}:{{ .Values.gundeck.image.tag }}" @@ -68,8 +75,14 @@ spec: - name: "gundeck-cassandra" mountPath: "/etc/wire/gundeck/cassandra" {{- end }} - - name: "gundeck-secrets" - mountPath: "/etc/wire/gundeck/secrets" + {{- if eq (include "gundeck.configureRedisCa" .Values.gundeck.config) "true" }} + - name: "redis-ca" + mountPath: "/etc/wire/gundeck/redis-ca/" + {{- end }} + {{- if eq (include "gundeck.configureAdditionalRedisCa" .Values.gundeck.config) "true" }} + - name: "additional-redis-ca" + mountPath: "/etc/wire/gundeck/additional-redis-ca/" + {{- end }} {{- if and .Values.gundeck.config.rabbitmq .Values.gundeck.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" mountPath: "/etc/wire/gundeck/rabbitmq-ca/" @@ -97,6 +110,34 @@ spec: name: gundeck key: awsSecretKey {{- end }} + {{- if hasKey .Values.gundeck.secrets "redisUsername" }} + - name: REDIS_USERNAME + valueFrom: + secretKeyRef: + name: gundeck + key: redisUsername + {{- end }} + {{- if hasKey .Values.gundeck.secrets "redisPassword" }} + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: gundeck + key: redisPassword + {{- end }} + {{- if hasKey .Values.gundeck.secrets "redisAdditionalWriteUsername" }} + - name: REDIS_ADDITIONAL_WRITE_USERNAME + valueFrom: + secretKeyRef: + name: gundeck + key: redisAdditionalWriteUsername + {{- end }} + {{- if hasKey .Values.gundeck.secrets "redisAdditionalWritePassword" }} + - name: REDIS_ADDITIONAL_WRITE_PASSWORD + valueFrom: + secretKeyRef: + name: gundeck + key: redisAdditionalWritePassword + {{- end }} - name: AWS_REGION value: "{{ .Values.gundeck.config.aws.region }}" {{- with .Values.gundeck.config.proxy }} diff --git a/charts/wire-server/templates/gundeck/redis-ca-secret.yaml b/charts/wire-server/templates/gundeck/redis-ca-secret.yaml new file mode 100644 index 00000000000..a82eab555cb --- /dev/null +++ b/charts/wire-server/templates/gundeck/redis-ca-secret.yaml @@ -0,0 +1,30 @@ +--- +{{- if not (empty .Values.gundeck.config.redis.tlsCa) }} +apiVersion: v1 +kind: Secret +metadata: + name: "gundeck-redis-ca" + labels: + app: gundeck + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + ca.pem: {{ .Values.gundeck.config.redis.tlsCa | b64enc | quote }} +{{- end }} +--- +{{- if not (empty .Values.gundeck.config.redis.additionalTlsCa) }} +apiVersion: v1 +kind: Secret +metadata: + name: "gundeck-additional-redis-ca" + labels: + app: gundeck + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + ca.pem: {{ .Values.gundeck.config.redis.additionalTlsCa | b64enc | quote }} +{{- end }} diff --git a/charts/wire-server/templates/gundeck/secret.yaml b/charts/wire-server/templates/gundeck/secret.yaml index a17529f4c77..000255ba0a6 100644 --- a/charts/wire-server/templates/gundeck/secret.yaml +++ b/charts/wire-server/templates/gundeck/secret.yaml @@ -19,6 +19,18 @@ data: {{- if hasKey . "awsSecretKey" }} awsSecretKey: {{ .awsSecretKey | b64enc | quote }} {{- end }} + {{- if hasKey . "redisUsername" }} + redisUsername: {{ .redisUsername | b64enc | quote }} + {{- end }} + {{- if hasKey . "redisPassword" }} + redisPassword: {{ .redisPassword | b64enc | quote }} + {{- end }} + {{- if hasKey . "redisAdditionalWriteUsername" }} + redisAdditionalWriteUsername: {{ .redisAdditionalWriteUsername | b64enc | quote }} + {{- end }} + {{- if hasKey . "redisAdditionalWritePassword" }} + redisAdditionalWritePassword: {{ .redisAdditionalWritePassword | b64enc | quote }} + {{- end }} {{- if hasKey . "pgPassword" }} pgPassword: {{ .pgPassword | b64enc | quote }} {{- end }} diff --git a/charts/wire-server/templates/gundeck/tests/configmap.yaml b/charts/wire-server/templates/gundeck/tests/configmap.yaml index 28d76773076..c8c23ce5185 100644 --- a/charts/wire-server/templates/gundeck/tests/configmap.yaml +++ b/charts/wire-server/templates/gundeck/tests/configmap.yaml @@ -39,3 +39,10 @@ data: host: brig port: 8080 + # a "redis migration" test in gundeck makes use of a second (distinct) redis + redis2: + host: redis-ephemeral-2 + port: 6379 + connectionMode: master + enableTls: false + insecureSkipVerifyTls: false diff --git a/charts/wire-server/templates/gundeck/tests/gundeck-integration.yaml b/charts/wire-server/templates/gundeck/tests/gundeck-integration.yaml index 60b5d26e3d5..f1a661b4a58 100644 --- a/charts/wire-server/templates/gundeck/tests/gundeck-integration.yaml +++ b/charts/wire-server/templates/gundeck/tests/gundeck-integration.yaml @@ -18,6 +18,11 @@ spec: secret: secretName: {{ (include "gundeck.tlsSecretRef" .Values.gundeck.config | fromYaml).name }} {{- end }} + {{- if eq (include "gundeck.configureRedisCa" .Values.gundeck.config) "true" }} + - name: "redis-ca" + secret: + secretName: {{ include "gundeck.redisTlsSecretName" .Values.gundeck.config }} + {{- end }} {{- if .Values.gundeck.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" secret: @@ -68,6 +73,10 @@ spec: - name: "gundeck-cassandra" mountPath: "/etc/wire/gundeck/cassandra" {{- end }} + {{- if eq (include "gundeck.configureRedisCa" .Values.gundeck.config) "true" }} + - name: "redis-ca" + mountPath: "/etc/wire/gundeck/redis-ca/" + {{- end }} {{- if .Values.gundeck.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" mountPath: "/etc/wire/gundeck/rabbitmq-ca/" @@ -87,6 +96,34 @@ spec: value: "guest" - name: RABBITMQ_PASSWORD value: "guest" + {{- if hasKey .Values.gundeck.secrets "redisUsername" }} + - name: REDIS_USERNAME + valueFrom: + secretKeyRef: + name: gundeck + key: redisUsername + {{- end }} + {{- if hasKey .Values.gundeck.secrets "redisPassword" }} + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: gundeck + key: redisPassword + {{- end }} + {{- if and (hasKey .Values.gundeck.tests "secrets") (hasKey .Values.gundeck.tests.secrets "redisAdditionalWriteUsername") }} + - name: REDIS_ADDITIONAL_WRITE_USERNAME + valueFrom: + secretKeyRef: + name: gundeck-integration + key: redisAdditionalWriteUsername + {{- end }} + {{- if and (hasKey .Values.gundeck.tests "secrets") (hasKey .Values.gundeck.tests.secrets "redisAdditionalWritePassword") }} + - name: REDIS_ADDITIONAL_WRITE_PASSWORD + valueFrom: + secretKeyRef: + name: gundeck-integration + key: redisAdditionalWritePassword + {{- end }} {{- if .Values.gundeck.tests.config.uploadXml }} - name: UPLOAD_XML_S3_BASE_URL value: {{ .Values.gundeck.tests.config.uploadXml.baseUrl }} diff --git a/charts/wire-server/templates/gundeck/tests/secret.yaml b/charts/wire-server/templates/gundeck/tests/secret.yaml index df7b82695fe..60aed14a3a4 100644 --- a/charts/wire-server/templates/gundeck/tests/secret.yaml +++ b/charts/wire-server/templates/gundeck/tests/secret.yaml @@ -17,5 +17,11 @@ data: {{- if hasKey . "uploadXmlAwsSecretAccessKey" }} uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} {{- end }} + {{- if hasKey . "redisAdditionalWriteUsername" }} + redisAdditionalWriteUsername: {{ .redisAdditionalWriteUsername | b64enc | quote }} + {{- end }} + {{- if hasKey . "redisAdditionalWritePassword" }} + redisAdditionalWritePassword: {{ .redisAdditionalWritePassword | b64enc | quote }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/wire-server/values.yaml b/charts/wire-server/values.yaml index 9a98be5d528..d2a7deafdc5 100644 --- a/charts/wire-server/values.yaml +++ b/charts/wire-server/values.yaml @@ -744,19 +744,35 @@ gundeck: # tlsCaSecretRef: # name: # key: - # Postgres connection settings for presence tracking. + redis: + host: redis-ephemeral + port: 6379 + connectionMode: "master" # master | cluster + enableTls: false + insecureSkipVerifyTls: false + # To configure custom TLS CA, please provide one of these: + # tlsCa: + # + # Or refer to an existing secret (containing the CA): + # tlsCaSecretRef: + # name: + # key: + + # To enable additional writes during a migration: + # redisAdditionalWrite: + # host: redis-two + # port: 6379 + # connectionMode: master + # enableTls: false + # insecureSkipVerifyTls: false # - # Values are described in https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS - # To set the password via a gundeck secret see `secrets.pgPassword`. - postgresql: - host: postgresql # DNS name without protocol - port: "5432" - user: wire-server - dbname: wire-server - postgresqlPool: - size: 100 - acquisitionTimeout: 10s - idlenessTimeout: 10m + # # To configure custom TLS CA, please provide one of these: + # # tlsCa: + # # + # # Or refer to an existing secret (containing the CA): + # # tlsCaSecretRef: + # # name: + # # key: aws: region: "eu-west-1" proxy: {} diff --git a/deploy/dockerephemeral/docker-compose.yaml b/deploy/dockerephemeral/docker-compose.yaml index a8a9ab66d5c..fb2a4801ebc 100644 --- a/deploy/dockerephemeral/docker-compose.yaml +++ b/deploy/dockerephemeral/docker-compose.yaml @@ -1,4 +1,10 @@ networks: + redis: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/24 + coredns: driver: bridge ipam: @@ -72,6 +78,134 @@ services: networks: - demo_wire + redis-master: + container_name: demo_wire_redis + image: redis:7.2-alpine + command: redis-server /usr/local/etc/redis/redis.conf + ports: + - "127.0.0.1:6379:6379" + volumes: + - ./docker/redis-master-mode.conf:/usr/local/etc/redis/redis.conf + networks: + - demo_wire + + redis-cluster: + image: "redis:7.2-alpine" + command: + - redis-cli + - --cluster + - create + - 172.20.0.31:6373 + - 172.20.0.32:6374 + - 172.20.0.33:6375 + - 172.20.0.34:6376 + - 172.20.0.35:6377 + - 172.20.0.36:6378 + - --cluster-replicas + - "1" + - --cluster-yes + - -a + - very-secure-redis-cluster-password + - --cacert + - /usr/local/etc/redis/ca.pem + - --tls + volumes: + - ./docker/redis-ca.pem:/usr/local/etc/redis/ca.pem + networks: + redis: + ipv4_address: 172.20.0.30 + depends_on: + - redis-node-1 + - redis-node-2 + - redis-node-3 + - redis-node-4 + - redis-node-5 + - redis-node-6 + redis-node-1: + image: "redis:7.2-alpine" + command: redis-server /usr/local/etc/redis/redis.conf + ports: + - "127.0.0.1:6373:6373" + volumes: + - redis-node-1-data:/var/lib/redis + - ./docker/redis-node-1.conf:/usr/local/etc/redis/redis.conf + - ./docker/redis-node-1-cert.pem:/usr/local/etc/redis/cert.pem + - ./docker/redis-node-1-key.pem:/usr/local/etc/redis/key.pem + - ./docker/redis-ca.pem:/usr/local/etc/redis/ca.pem + networks: + redis: + ipv4_address: 172.20.0.31 + redis-node-2: + image: "redis:7.2-alpine" + command: redis-server /usr/local/etc/redis/redis.conf + ports: + - "127.0.0.1:6374:6374" + volumes: + - redis-node-2-data:/var/lib/redis + - ./docker/redis-node-2.conf:/usr/local/etc/redis/redis.conf + - ./docker/redis-node-2-cert.pem:/usr/local/etc/redis/cert.pem + - ./docker/redis-node-2-key.pem:/usr/local/etc/redis/key.pem + - ./docker/redis-ca.pem:/usr/local/etc/redis/ca.pem + networks: + redis: + ipv4_address: 172.20.0.32 + redis-node-3: + image: "redis:7.2-alpine" + command: redis-server /usr/local/etc/redis/redis.conf + ports: + - "127.0.0.1:6375:6375" + volumes: + - redis-node-3-data:/var/lib/redis + - ./docker/redis-node-3.conf:/usr/local/etc/redis/redis.conf + - ./docker/redis-node-3-cert.pem:/usr/local/etc/redis/cert.pem + - ./docker/redis-node-3-key.pem:/usr/local/etc/redis/key.pem + - ./docker/redis-ca.pem:/usr/local/etc/redis/ca.pem + networks: + redis: + ipv4_address: 172.20.0.33 + redis-node-4: + image: "redis:7.2-alpine" + command: redis-server /usr/local/etc/redis/redis.conf + ports: + - "127.0.0.1:6376:6376" + volumes: + - redis-node-4-data:/var/lib/redis + - ./docker/redis-node-4.conf:/usr/local/etc/redis/redis.conf + - ./docker/redis-node-4-cert.pem:/usr/local/etc/redis/cert.pem + - ./docker/redis-node-4-key.pem:/usr/local/etc/redis/key.pem + - ./docker/redis-ca.pem:/usr/local/etc/redis/ca.pem + networks: + redis: + ipv4_address: 172.20.0.34 + redis-node-5: + image: "redis:7.2-alpine" + command: redis-server /usr/local/etc/redis/redis.conf + ports: + - "127.0.0.1:6377:6377" + volumes: + - redis-node-5-data:/var/lib/redis + - ./docker/redis-node-5.conf:/usr/local/etc/redis/redis.conf + - ./docker/redis-node-5-cert.pem:/usr/local/etc/redis/cert.pem + - ./docker/redis-node-5-key.pem:/usr/local/etc/redis/key.pem + - ./docker/redis-ca.pem:/usr/local/etc/redis/ca.pem + networks: + redis: + ipv4_address: 172.20.0.35 + redis-node-6: + image: "redis:7.2-alpine" + command: redis-server /usr/local/etc/redis/redis.conf + ports: + - "127.0.0.1:6378:6378" + volumes: + - redis-node-6-data:/var/lib/redis + - ./docker/redis-node-6.conf:/usr/local/etc/redis/redis.conf + - ./docker/redis-node-6-cert.pem:/usr/local/etc/redis/cert.pem + - ./docker/redis-node-6-key.pem:/usr/local/etc/redis/key.pem + - ./docker/redis-ca.pem:/usr/local/etc/redis/ca.pem + networks: + redis: + ipv4_address: 172.20.0.36 + elasticsearch: container_name: demo_wire_elasticsearch image: elasticsearch:6.8.23 @@ -284,3 +418,11 @@ services: # - DNS_SERVER_RECURSION_DENIED_NETWORKS=1.1.1.0/24 #Comma separated list of IP addresses or network addresses to deny recursion. Valid only for `UseSpecifiedNetworkACL` recursion option. This option is obsolete and DNS_SERVER_RECURSION_NETWORK_ACL should be used instead. # - DNS_SERVER_RECURSION_ALLOWED_NETWORKS=127.0.0.1, 192.168.1.0/24 #Comma separated list of IP addresses or network addresses to allow recursion. Valid only for `UseSpecifiedNetworkACL` recursion option. This option is obsolete and DNS_SERVER_RECURSION_NETWORK_ACL should be used instead. # - DNS_SERVER_ENABLE_BLOCKING=false #Sets the DNS server to block domain names using Blocked Zone and Block List Zone. + +volumes: + redis-node-1-data: + redis-node-2-data: + redis-node-3-data: + redis-node-4-data: + redis-node-5-data: + redis-node-6-data: diff --git a/deploy/dockerephemeral/docker/redis-master-mode.conf b/deploy/dockerephemeral/docker/redis-master-mode.conf new file mode 100644 index 00000000000..d71dbc51c97 --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-master-mode.conf @@ -0,0 +1 @@ +requirepass very-secure-redis-master-password \ No newline at end of file diff --git a/deploy/dockerephemeral/docker/redis-node-1-cert.pem b/deploy/dockerephemeral/docker/redis-node-1-cert.pem new file mode 100644 index 00000000000..7756f82bbd0 --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-1-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTtp3U0VPwBVJNULQF +S4BBlWBNf/8NMidOq23IsTcjkIFWO1XL+HFZoa1AArUSA/TaLBYyz9WmX7eLWvAU +ADM6mfAf2V6whmIs2H9ZRnY89bFWO2hzLWWp1qq3dXK1ywTLpw7DqU4OT0rtYZbp +QHeVY0mKKspF+YJTZzWB1hs8IX9355wXRlYBLPNQ5oHRb4/16J/UUFPIJjpUyHsq +T1LWmVREqisrq9u50FnNPeLXE6SDnHGRkYGQXzQOM/yAI75/QUOOqo5rt3Et52t5 +pkOT45R0PbAC2UpR1usew0zVjRoQfFk9n38tXUSHKw/tW+ZY1xJqEKEiLGfnhhza +t4kjAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy0xhwSsFAAfMB0GA1UdDgQWBBQsOxsq4X8dS/Ddl9l1 +TWDb8Q5KKzAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAaByeD08xOZCV0ZKsx7lHtiem5/XG01rMcDxNVrVguSD+mqhR +/j8ciTW2CruJ2X8ReTjNrI4X1nWLbh4rsrA56q4xkjkgJIfWQAdKCibXTrHOWfk5 +dcYG1pqVdpD5bvsxAsY95jxqoVJHXHGN8ynC+lV39HbDJQFOdHLAP66NUrphp76a +OZKiuzUS6naeiHWoA9eIANFRz/JoQvyp109gdce5MH0iFwGFqNJU2rwilOpzQVc7 +qldx7MHMnW5UYSTqryTOr8PS+xo24TSdHjIXmnOO3Ov0Pw7iPpGVGj56dAKgEisG +yGOAWYto8UBWKLox1vSSlfdkhAoDXluvE8EwRw== +-----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-1-key.pem b/deploy/dockerephemeral/docker/redis-node-1-key.pem new file mode 100644 index 00000000000..6d8b29bbdee --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-1-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDTtp3U0VPwBVJN +ULQFS4BBlWBNf/8NMidOq23IsTcjkIFWO1XL+HFZoa1AArUSA/TaLBYyz9WmX7eL +WvAUADM6mfAf2V6whmIs2H9ZRnY89bFWO2hzLWWp1qq3dXK1ywTLpw7DqU4OT0rt +YZbpQHeVY0mKKspF+YJTZzWB1hs8IX9355wXRlYBLPNQ5oHRb4/16J/UUFPIJjpU +yHsqT1LWmVREqisrq9u50FnNPeLXE6SDnHGRkYGQXzQOM/yAI75/QUOOqo5rt3Et +52t5pkOT45R0PbAC2UpR1usew0zVjRoQfFk9n38tXUSHKw/tW+ZY1xJqEKEiLGfn +hhzat4kjAgMBAAECggEAFqMmoixVxMrU34Z7ETve9WRC/VZrz53mvQ8weG6WfjuD +0NQcWuhwOkzCyR7g/JGmuzNOllVJu3Xtmr15ATJ6R9BQ8B7edJKR6cimaUXS+7ar +pRRKGVKn1a6p517sCoswMpRkzEAMpBQPZ21xZPRrNPJ+WQM1SKEiscdN3dmmZNng +MvroH1dPVbyZ49xkjMQ0NaOtk4rvopzdKKZea2qz41w/vXR9hShnfVDs/q86clmx +5mnEvXcEdguioAfUWz+qQ7dXlWsASKa/gAMjUN9GW9uOn4LclFsVCD2MW+IUJMxe ++JtFM0xiQ3HaK0Fem8+XR8mG3BB5a/06ZHBfcv/lsQKBgQD4WSJjZwMBG80uGidR +ls+VhhFjysxm5qrF34MWziLczi1nAStc/PzVcA7tHapKX5JiKYT6d8Ptngz6FLIo +/72OshmLzctxRprlpihWxMIYOqwb2PLB0//ghuUE81Zbxj1MQ6k9WbTGHBwUbaiv +PSzclhmMubypfLLcmMEnHeZFswKBgQDaPIWmyax3Eft8DzC3Om7X3WMN0NXE96z2 +6hUAon5tqinMuWUWa2cyWzPsdBgFM8mCynoiIu08YFpZQivoB6QSal4x2mLg4R+u +aLm3h9f6NS4/VvpWPL5wMUAqeCCbP/2PVKk///0mtQGixUOxeQftTncQeLtfXOXd +4gDJHjfW0QKBgQDND7xnW42Ngsk+wfWpVt981UDSp4dziA+GZ3I0iG0c6Vlv7fVC +SNrz2h1ZCN+tnZCfYS0eK3oqYBDTBfe+Br0ccE7Ls1fC5svLyBES5FBn9TpbnB2G +kmh7mqbMGak7CktfB5dcww+TbW56J7nbSKYcVgwuuMbhI8gEglUq2XNkJQKBgQDV +VojIzSmdlKSlWCwlUif9OdyVKutuizg4gAhcAH1bMxd9nFbnncLaBTIzGiJJI6EA +DHNsX3xOo1pvGzLUtnN71SOT1IsIjsprstCqS0+ktswo+xvppaP9BQhW++vUGLAE +p5x0hgixCA07U1+jZE+NekEGhx+UT7oeN8rQ0IuBoQKBgQC4PF4WwqashYHkYW2j +4LaMu5kWY/0OI9Vh/h1iOcKPzVUn61aabjsx1wF9rummIdxP03/bs7ZpkwPypcVR +v7XnNbi+hDZFEN6s/+Gl4S6RfAbWXs3sgnhVlctlkzzwG8UHCef4DWMPxFI1JQI8 +X+SdDfpmB/ayQb8TlYvke/s8cQ== +-----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-1.conf b/deploy/dockerephemeral/docker/redis-node-1.conf new file mode 100644 index 00000000000..aa772f502fe --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-1.conf @@ -0,0 +1,17 @@ +port 0 +tls-port 6373 +tls-cert-file /usr/local/etc/redis/cert.pem +tls-key-file /usr/local/etc/redis/key.pem +tls-ca-cert-file /usr/local/etc/redis/ca.pem +tls-auth-clients no +tls-cluster yes +tls-replication yes + +cluster-enabled yes +cluster-config-file nodes.conf +cluster-node-timeout 5000 + +appendonly yes + +requirepass very-secure-redis-cluster-password +masterauth very-secure-redis-cluster-password diff --git a/deploy/dockerephemeral/docker/redis-node-2-cert.pem b/deploy/dockerephemeral/docker/redis-node-2-cert.pem new file mode 100644 index 00000000000..ea4b4507d6b --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-2-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4mm9GWOVh0ttlzXaJ +11/rQQX3vYQ3zyeMdz/KGTKArOF+pxVCxbETlI3ZufO8Ht9zqa7Doh5R86iNtVMR +LoQZVWeXjQsMATwNUZT3lEOezpDE0ZI8d5JyU946Z+7s0VjIMbXOzjTSjTNSi57N +li59/1NTG5CW9EtgnnYoP5SOrYTpK+fzawXD18tD8kq/VBLt8OoG7xn6DIpGsFr9 +h1Ot/yrUejvrHg2KIi3av/cnqA8twzFpkdvGSEarjRuYG6fHGL67dgSpLvzh/v7h +QiJDFFB8fHnUc5ioZXFw88P4Oq7UlzBhnkC8nhUi1X1vWoF9Xz4FXXJ1P4WkZfWB +Vui7AgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy0yhwSsFAAgMB0GA1UdDgQWBBQQK2od431iWKznJEQz +zy5GXgt1DDAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAhvNbLzlY4sS/xmn8alzIYjY/uIc5c0PaUaXc7SSjeoChRfNQ +tE5YLmOo86WYNThtaNmiRLFv3yNBXCcqdVgNdL78EIQlKvPxHwzZXxkKDmOcfIZS +nUa4w+OmKJLsdNjphBGmR94h8WycwoFMThw55vnTJ2+AnCFPsLDfjtHiKB8AsW8u +gtSTtVyu+QyvGTDxEFDgqFgyFjJpVp37bOakRuzuZZ8VUssQbb11YHyhnNGTcL3a +hLXeGVSRA7SyDXxxRs5PmmJVsUOWkgbIjguvZK5APpqaGEYwBYo036DFSgt6DTOu +8YsCTeSOmue0xNlPDiVPSP8HUGfq3tTBKMXbUQ== +-----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-2-key.pem b/deploy/dockerephemeral/docker/redis-node-2-key.pem new file mode 100644 index 00000000000..fba9118998e --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-2-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4mm9GWOVh0ttl +zXaJ11/rQQX3vYQ3zyeMdz/KGTKArOF+pxVCxbETlI3ZufO8Ht9zqa7Doh5R86iN +tVMRLoQZVWeXjQsMATwNUZT3lEOezpDE0ZI8d5JyU946Z+7s0VjIMbXOzjTSjTNS +i57Nli59/1NTG5CW9EtgnnYoP5SOrYTpK+fzawXD18tD8kq/VBLt8OoG7xn6DIpG +sFr9h1Ot/yrUejvrHg2KIi3av/cnqA8twzFpkdvGSEarjRuYG6fHGL67dgSpLvzh +/v7hQiJDFFB8fHnUc5ioZXFw88P4Oq7UlzBhnkC8nhUi1X1vWoF9Xz4FXXJ1P4Wk +ZfWBVui7AgMBAAECggEAONB+8r2lSygkEf7cPqwkfzjx5z9SlAKTf22sGj0LCAMt +G1e8+WHyj74msh3C3+D4kJZmjRs2Da7Z71MhD6arTUi1qzTjc3xlyQuUt2XQMe4N +LCX7xdRfJASf3oXiSMxdcK+r7swUAcEnTH5gD5HrGSgdsvRG2c6x7DiY0OZQiGBk +2rPHQDUKeb7Z0YLWc8nldzlnOe2OeWpFVEraAOzmANnV5FVJZP8RNoiKviZnB77O +qbA6Xtpg4ytVhMaymUmjkjdFuxm5XcMCOIz4W9SZVJ9uSzjZqATzgjsiOWYozB7q +2xb1yOyCVPgf+dZj32D8DvqSrwwRBR3LcNhnj2wUIQKBgQDqL4VNp54Lrf+oZ0ZF +h3s6lL2NquY0xHs91YvoO187VetyUlNjOcGXt8ROhSSAf6qTLQvrreVdjYHr52xr +smCohhQ9QDm3d+Inh3ARgr75O577aPwJHBmo0fnu9h6OkDr8nx05SthW4XenHqoE +iWQ9FnibAFz5KLBSYC7x9wfGaQKBgQDJzI3UC6AqQS8ILbcqHm7ZmnpUUjn7vPUm +lkB3/YtV7ewWJhFzdPdaKHKe2YO9WXQTCF7iPRK3+gWt8uh4DWCrSObBkmSUlF66 +wbRof3lsYiWDPed9OTgoDHRwbMPeYrJ3A0TMrGJQsbedljneaat+DM3kNgjgChfW +JiL0g9c5gwKBgQDBi8zMRT/lv0SQVepKBJLf85ZFw3zHF6wTiq46nPcz/uq8bTXl +yBIr5gEkM/3bBahgQtabTflGvHEoGvgMejxQi5+mj7Ij47zRlqoUjs5vBct7VWUX +0lWSpRe/W0Id6S4XIxnwA9+Qzn8pa7pwTWy+4BeFY2NzuSEgs8WYzOVsIQKBgHbI +IPOfpDc7ByQZRKdWIomTlE3t2JOFNgfwiSIX69w4n66p2bvMLYy0IkO+ZP0fmmNZ +mgAxUsNYN9+cC5oexbgMwUdPlESg0OG9AyQ/ZImXe900ov3ioFtyeVdzrhdIoSPM +mMKg9X3qHdp0gruYF4mqn8akx7SYPE+hQxIKSLVhAoGAJP+TshJj8xAeE1Uroyc/ +yIWThbp0Q/EFaXkpS6aJqBjdcLfh2U+Zo9ZaTn9OBlzXHk9WttzeWuMY9PrINodJ +8DSg5f0PslYxJ5DQuKnDWUeqX3zCnXkgnymlvh78t6wWp+BUAEjI8qH5IgKVwKd+ +VJbPX4mzhAl/0kIablU6SqM= +-----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-2.conf b/deploy/dockerephemeral/docker/redis-node-2.conf new file mode 100644 index 00000000000..de7687558b3 --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-2.conf @@ -0,0 +1,17 @@ +port 0 +tls-port 6374 +tls-cert-file /usr/local/etc/redis/cert.pem +tls-key-file /usr/local/etc/redis/key.pem +tls-ca-cert-file /usr/local/etc/redis/ca.pem +tls-auth-clients no +tls-cluster yes +tls-replication yes + +cluster-enabled yes +cluster-config-file nodes.conf +cluster-node-timeout 5000 + +appendonly yes + +requirepass very-secure-redis-cluster-password +masterauth very-secure-redis-cluster-password diff --git a/deploy/dockerephemeral/docker/redis-node-3-cert.pem b/deploy/dockerephemeral/docker/redis-node-3-cert.pem new file mode 100644 index 00000000000..e550d0e30f9 --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-3-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCd72omHFn1mEFw/GBp +gkPM5BkF7giGx7GOLyijCoi4NLNVKJn6mOJt9vX2PbBYedy1OcskObLbEwqUwcZr +7fVim34xrE4AmdJqBWTkcMFnhbjzYIynfvejej/05kWlzp3JuhTpi7i2W+nnZjqb +S6UHgeTwF/iENA1oysuq0jC4oaVGNa2ZCoz3W+uAEbpUYNjN7/uQeEwRyZjSEJUY +KyG69Wrl9KnzBX0mkltq8rJiCqaG+qOZwP+XH7TxjYM1SlAxLHrnjDQHWyZXJzPY +fikRk2Zf8nDobA5thXVR/2PicDxUs1VyGYSg/vK1EMwOIHIZdxalo0x75vFjBJ9T +l+HFAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy0zhwSsFAAhMB0GA1UdDgQWBBQyljx2OR3L7yZLVax4 +MLTDhj4xPjAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAUv3JN0ip/LWmtWHyqzPuq9tbVFs2M5waRO2ZZtEp6Pzudr9x +JKrmtz7IlnwK2E3eqw1Hh3kZYiM5XT2GzqFjPn+Na32i3IsR/S1Y4ZDq6T1WOjht +u+3EjrUvpTXAcLfaO60gJ7DrfC4PsuNuaRr23BiF3lIb7A693hnESg3EnUqGvAvA +ikR/Cv48kAvxpFlXZfnGApFEP49svj676emodRUlk4aCOjIniPByLF318Dl+MwzW +KbnjynzjnOqfcXeD67axFqIBAhZPBDWIDOLNo/ASAROkPntycBGFPUL+Wgdq75vs +8WnftwfCzYtKcASNVSeoSFtJhVy2cAqHK1bd/g== +-----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-3-key.pem b/deploy/dockerephemeral/docker/redis-node-3-key.pem new file mode 100644 index 00000000000..d7be5cf147d --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-3-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCd72omHFn1mEFw +/GBpgkPM5BkF7giGx7GOLyijCoi4NLNVKJn6mOJt9vX2PbBYedy1OcskObLbEwqU +wcZr7fVim34xrE4AmdJqBWTkcMFnhbjzYIynfvejej/05kWlzp3JuhTpi7i2W+nn +ZjqbS6UHgeTwF/iENA1oysuq0jC4oaVGNa2ZCoz3W+uAEbpUYNjN7/uQeEwRyZjS +EJUYKyG69Wrl9KnzBX0mkltq8rJiCqaG+qOZwP+XH7TxjYM1SlAxLHrnjDQHWyZX +JzPYfikRk2Zf8nDobA5thXVR/2PicDxUs1VyGYSg/vK1EMwOIHIZdxalo0x75vFj +BJ9Tl+HFAgMBAAECggEABYejI9UiS+MaMiaOtE2x/16NMb6f4Hg600umFJoDJ3qm +PM5rIHHHRn+7JPVhU00RA+y+HB/uZJVKGDigsJloWhzaUkrs1ZXiiYEe2JDKH3cj +KVexamabrRxUA53RxSMdizlPZM4A7axSMvP1YV1IrfadBCW9Ydj2DzvqiFShDWst +asKPAa6MAU63zfZZaBQvicswd1nJUvc8ZNp1p0JiVcwWPWVTYH9d2c+0WZLlfCHm +GxUurHwyVc6b7T4OSrsiDaQN0kdLJDAYowp+T94JDBCH3m4e/NF9W6gkoO2UGXTH +6A9HVDI3FwUBzXdT9rL/Wmp4kKXB4xO2TU/yeZoYAQKBgQDK2Q2vG+BucY2aJxGw +7HNeXov2lLma2Vn4TRr+cyzcXH7Jmc8J/h9RMU7AEfg3CQwMbXE60P561/1q1e0Z +fD55x9ka3FZ2dG+a5CDzjkqnUgnLYOK1bxx5UUq+Sf6IeNjGPikejRPcPBmvFVuu +NvoPU0HwWLm67BnantJIpUFvRQKBgQDHUaPa6SIMGAWHasI6EvZMGgBAy5iJa4s3 +o+DuESF+6lD989ZnOltsPFeYhwbIzm14EzhK/y4MVR46gXLMZ9FwlGCGdXE7LWiN +VKCm9kRcxcH9Sak70LkZ9yv08Nl45f9vTOzBcKzu6bgZ2LOeSJ0oTmiVEb98pL7N +w6XxD2iQgQKBgAVVPYncBsOAksN5wXpQTRwvCij6cgLDMh1YEZyc9JH6kI7GT24o +0zP0QujD0C3KPBnbir2MHxSltxDm/OvNm2riOS/+mPtWRlThKIiethG+E2nYaz1v +5WS/IWLtWRbHbpOPsM8P0HTa06YJvrZO1bYvby1dd8yVRny77jVgut6tAoGAHpMK +ZHkgjORebMBWnNvtxgyy/z1735CMoXNU/I/KKJK+68WsnNcZ0QeMlEwaIVFw/1tL +Zk2wfZnM8kKLHonKWc+Y4uc/AEnd4NgbcKEUKXr4X+cdu5wv2KjOqFsNsPru7N7K +7n1fOaLGZ8iS/PO8j8M/TaaUTgVjc2LQoKKxcoECgYAtPzq1Y0yc22M+m1m6nK/W +L7rsUI0zDs0VZcJ5mrJg8nahOM/f+BsFYN5oAHYxuXPUyynZyD2nPtdsES75DGOH +PEqr9DhgSig4JmHS/6SEBnWql+zyNdn1/FaYOkKRHiY7jNhjTayiDObJrXg0g4OT +BmzY39BABb52ogQbjWslow== +-----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-3.conf b/deploy/dockerephemeral/docker/redis-node-3.conf new file mode 100644 index 00000000000..7f406d72324 --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-3.conf @@ -0,0 +1,17 @@ +port 0 +tls-port 6375 +tls-cert-file /usr/local/etc/redis/cert.pem +tls-key-file /usr/local/etc/redis/key.pem +tls-ca-cert-file /usr/local/etc/redis/ca.pem +tls-auth-clients no +tls-cluster yes +tls-replication yes + +cluster-enabled yes +cluster-config-file nodes.conf +cluster-node-timeout 5000 + +appendonly yes + +requirepass very-secure-redis-cluster-password +masterauth very-secure-redis-cluster-password diff --git a/deploy/dockerephemeral/docker/redis-node-4-cert.pem b/deploy/dockerephemeral/docker/redis-node-4-cert.pem new file mode 100644 index 00000000000..185f8f97014 --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-4-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKxOmFo5c4ae38qC3z +I89R1F6xjaMyR6jjd6k5qsW7eU/y8+trgY4HV/jbzD3CDOjMkj70la5EiV+GTA0i +GeJH/BkKjqEPsIy/vAPux9xt2ZpIO9ieO2BF75ojrcM7tAbeOLQNAgYA7zAyIpQk +J2P8IyOYSJ31ujLJCR7d0zudAbXJXfAAyPUWqUrmmRHIY7hRi1tUv74JARqnU2tH +ZhFgGyBCaLROK69S/Wy+xPKo5w9Ol5L9eIccrK2/JwNpfsFAxJqXawNm1l1M9gGk +2MpQXzZeTg/hlusqCtPieOPUQKoEDXAgYArQy8iYkLuZzOtg2WwcPOhtfsgVRLNE +wXihAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy00hwSsFAAiMB0GA1UdDgQWBBQrI/peejY55qjXOc6W +XUU+/q6R+TAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAdf1N+gPpnkEHzDAMnK4kUCHq2ymLBBWJVAPDcmmtcMjEiEVC +/9BU+hcdqgLXxonEqiA4kEs9Mkj8AcUk0Dzl5Gfk2haZO6yzVEp97zwto+3Tgzya +0l6bvRv4OSfdVeSTYx8T48h23O8FBD/Gp9l5sFOZgc1TCWrb7ReJQS+XThAksIdW +DLvwbOU1I2qRL3ZbT49FAhmVcrMkHJjzkugXDoGG3Rgdzx/HePUjXWdWC1L+7/Kn +U/7w72ymW1mC5PbjoW9zzkVKesj++mhzSb5+sXa/is3hUJ17zy4Bqc71Mb2q8tqM +G/uMrdwfPeoad3qRVPRsK8QlVnJ0eIpiUDk+Ow== +-----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-4-key.pem b/deploy/dockerephemeral/docker/redis-node-4-key.pem new file mode 100644 index 00000000000..355661d6a99 --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-4-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDKxOmFo5c4ae38 +qC3zI89R1F6xjaMyR6jjd6k5qsW7eU/y8+trgY4HV/jbzD3CDOjMkj70la5EiV+G +TA0iGeJH/BkKjqEPsIy/vAPux9xt2ZpIO9ieO2BF75ojrcM7tAbeOLQNAgYA7zAy +IpQkJ2P8IyOYSJ31ujLJCR7d0zudAbXJXfAAyPUWqUrmmRHIY7hRi1tUv74JARqn +U2tHZhFgGyBCaLROK69S/Wy+xPKo5w9Ol5L9eIccrK2/JwNpfsFAxJqXawNm1l1M +9gGk2MpQXzZeTg/hlusqCtPieOPUQKoEDXAgYArQy8iYkLuZzOtg2WwcPOhtfsgV +RLNEwXihAgMBAAECggEABRxG5XEc0dVro9tKQy9DHaUcWN3Av/bp5QfCSluJPcMe +Nnma1JwQjBNVyJZidRZVtLg34Xq3SG9s6qnWh+Y+m4FZUTiMiyRwO7HdqII9hkA+ +gPUPLdfBwql6CU2rFsFgDfBAa3aCV7ovjQftk2axwKxTDJbB8mxFtObnsgANp9SU +c+MTlNTs1IQ4ev4u1i9ntR8SlFMcYQUA2AxvOiEDu7b4x/Ph9TEGuR6wLxdImRq/ +7hXcPtGAJKYgZLzAwCrZrjGjHILSskTxdii+Tr52Aq75SA3tLYGkJfSxHTJjFe0u +1k4Ot4uSEjRf4DIwohbSFFbK/ZXG2uscn36OphtbUQKBgQDwQY263RPJ/M5mKvME +15DK1JW3DOLWCBiV0XzwXsS+QpE8pKs2YLeyrY7sV/w1tdnfNdfINCknuzC4tG7Y +I+QzCQGhyKrP2nj4K3SsKUcFk6OWxgiPF5CRmlWySJ+H6+yITKcSJt/ZjUvvGQyQ +TV+IQ8s4RbKII9Pvifai6SLJ2QKBgQDYDn4bqIfZKR0I46//AycGXAUl55Yfgeog +8CR5MatNz26crrmDzjnDgsRbKUxK+UZLl/zEXY5Npn06sOG1G0bO/t7wQqcPsXZt +rZTx58lKvW7LQhEBAz48y9QeK3WUvT1E3JMJ6rt+6IfHvbvCLIu9DwyGJ7Zc7N+6 +k5GduC9gCQKBgQC4Zdfd3+hcUwgnKjezM7ARvO/buqwvEa+s7UgzRMlELdtC7C/s +YHcdUFAt3anZn2VFCBJBuqcLs4RFf1bD1WhEM1lpTparSUcnUlMN//Beu14HTp8r +FC8FUasMVuj6bXzxb8ObDvMoCmaJcHRQHNKBx2amHfhUvQrhAsalasIkoQKBgAFo +XsP5XiE5FlpXeW8U6y0sblAn6R99bjQWvHYZr78LCfJ1ZPoJ3vB6KqNZaojWhPG7 +JMd2wJWa7xfxzRar/dMdcABqvsHoaxgd2GmXFAWrpEwouwmhpscooNItgE+eyAZp +1X9sCxqxkyjnAJEsTyDFN1Ssb5C9blu92GYJrC1ZAoGASChIICMp0HWrDSRxRCen +Fddf993aEI4e46NTWY54u2p0Ga62XUcaw5eND9QX6craD8nd7mMhwvdvQ5vuORBk +m+dqt0oU5cloVp0srHDA861CO8topJFaNGWdF4wDgLU8YKRzd6hNX8X0/CCRl1vd +z/YmtxfgU56SaqExe0X65eA= +-----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-4.conf b/deploy/dockerephemeral/docker/redis-node-4.conf new file mode 100644 index 00000000000..55b360f9f90 --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-4.conf @@ -0,0 +1,17 @@ +port 0 +tls-port 6376 +tls-cert-file /usr/local/etc/redis/cert.pem +tls-key-file /usr/local/etc/redis/key.pem +tls-ca-cert-file /usr/local/etc/redis/ca.pem +tls-auth-clients no +tls-cluster yes +tls-replication yes + +cluster-enabled yes +cluster-config-file nodes.conf +cluster-node-timeout 5000 + +appendonly yes + +requirepass very-secure-redis-cluster-password +masterauth very-secure-redis-cluster-password diff --git a/deploy/dockerephemeral/docker/redis-node-5-cert.pem b/deploy/dockerephemeral/docker/redis-node-5-cert.pem new file mode 100644 index 00000000000..e1221b9df77 --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-5-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCSXrnTzrNfHudXjC0A +h0CiFRe3yp4j6cN3Hfv4snS6tPWXM4MH9Dka8zMLZvzRVQZK3PxDh/R/DQYBZhpy +LEvT7wYCDsS+F+tie2sPjzSAbdM5dolD8fGwACOqobI4vPz0QrwDqHde/OdVWAZl +h5Pzw5rDUu84CdfPSWRN1pomCFWG7gVkpuFzIcBfz+smPodyw3BfU8969q6tFACE +pjGPF/RufmHoIaHe2q/c+3HBY06ro0oTqTtRe36v4Jp2HLE/jE8wc+YggTmHE670 +uEXIR9N3fF3AbPVnhimEwcQ5fpJtMonUvfj5Z/4KfKo/0Yrh0wljeRiz/tZgTwwb +h5ATAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy01hwSsFAAjMB0GA1UdDgQWBBRkbb1LScfQthztQJ3l +R+QFCKjXUTAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAn8TOFqomU30SmIDIHYBKMRGq3bVDLkDDC2yy6LCCwwG2rpoO +UtnUMig2w3iNQ6nvqR4LJB1ha0hLK5FP3iX/JcqZiO0NaucOTe7aJlt9taCADgAw +4vRW/pDuxtq7H1hc2pOue6i05UtGqy2E12jYowQc8a/5hylfEO3b5t5Z7xoQzyAZ +1ov7sYatBinwhqyDI5qNvZCuyT7SMx7H10T7cPrEec4uq55AJ0ReXnAAy1MLhpGd +nW5FX3F4gnyJcK2xL/V+ScL4NTzA8qWT+qOK33KxU1qrGripAkFaF6Z110nuIDiP +Z2tneIovCKKChgFsmZjy2spRpDw6R3Am6rXjpA== +-----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-5-key.pem b/deploy/dockerephemeral/docker/redis-node-5-key.pem new file mode 100644 index 00000000000..467778629cd --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-5-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCSXrnTzrNfHudX +jC0Ah0CiFRe3yp4j6cN3Hfv4snS6tPWXM4MH9Dka8zMLZvzRVQZK3PxDh/R/DQYB +ZhpyLEvT7wYCDsS+F+tie2sPjzSAbdM5dolD8fGwACOqobI4vPz0QrwDqHde/OdV +WAZlh5Pzw5rDUu84CdfPSWRN1pomCFWG7gVkpuFzIcBfz+smPodyw3BfU8969q6t +FACEpjGPF/RufmHoIaHe2q/c+3HBY06ro0oTqTtRe36v4Jp2HLE/jE8wc+YggTmH +E670uEXIR9N3fF3AbPVnhimEwcQ5fpJtMonUvfj5Z/4KfKo/0Yrh0wljeRiz/tZg +Twwbh5ATAgMBAAECggEAFEPciJsaseBueUGQItLsZkRzVzVMtdOHW4uhjOpFnRVc +LLCrbe4opeGRaf0P+HpEIm38LewPNDP9ETPYv4FV3PmVTwhKbGNAFLovtXocnmzA +4jjWLRESEaMYombmwlJFghq8kJPCNeWKsIHnyNDU8YVd0mM+JE0V6GjUjq5YA0x4 +co87wiNxAtjdNuAmI8elOqH3YhCwCQjYO1NeEJwIhWz5tgb2J9Rvn85LOh65cU17 +FaxiTBNSrMW44yk+uhEyj8IDbcZax0s8gLbCSLIj/MnuSbm74VkGKXme0U3mJSmn +dY2tpO3DnNZ+qvakpUk50e/LXYFofsJH9cs1BlZ7AQKBgQDJufGOp07KcIG4N3ei +YxH1IRZ8vThOHksbKVnzQLRcJcEY6SHrL3DgxS+kO3IfjkKZGw5vZgV4/jfTfWQP +eDXwIl0t/YVCEDECppfAIN7fyvIVI14quRogbIrn0jn5ijhVzPI8SWvi/viFbFvn +2O/8KUaHudv9yQ6zKItZ1zHAkwKBgQC5wBLKYdeQT5EfvfXT+rHoioUyywFxTpOF +em14JfNwKLdhqEVB99MzGEdRs6HNz88YbhKQpuEQjwJkbBXZUpAXyYPLDN5uQtV7 +Xw1MY7d8O7U5qNevos+Yti8rrv4w8Cb8ppOX0DJ2SD7J4OQjuyiRYx6sE+tQH6p+ +6N2Gt9YigQKBgQCqpnt7s3uK9Aw42+t/2xFo7lnIooYMR8I/swaeKsGpJmMpAKep +/pMeApHf/E359e3O+b2HbaX5ig2OAwhvscDnaRqsekiN74aWeHntlaEVbujGCwpx +V++LOGd13zkeKdiodN0DNRVojUuOC3HgO3whNIWu8gLxuXGPDCB+mvZCswKBgH+I +vh4QgZYG22iE37U0ylQUT5HpSktGnQknXuQAgp1+hzJY+3xosKzDPax9/lk2FkX6 +xWpl+d+JoSXcBEBbbK24YXHXmxzvbG4xfAr36DI3OJ2nLLfdvFVouQhwNPza1pnf +sTSp8Qu/XMT1UQ6rYRY5jQSvBIDVzRUnw3nM3QyBAoGAXs5Mg1jcQme6X56e0Db0 +zDCcEJuYL+nWXSkClsQCaDwafi4PQVP/V351Qruw0n98grD5vacz1HdXosvCaACJ +8P8e4sFJmSGu8SQt4zbReq8DHNTWZyPC8muurnMSKtfg3XulY8SFsoog7dlMzGGY +IMDiEb5jIb6DFcpNxjigXsM= +-----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-5.conf b/deploy/dockerephemeral/docker/redis-node-5.conf new file mode 100644 index 00000000000..ba2cfde9c65 --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-5.conf @@ -0,0 +1,17 @@ +port 0 +tls-port 6377 +tls-cert-file /usr/local/etc/redis/cert.pem +tls-key-file /usr/local/etc/redis/key.pem +tls-ca-cert-file /usr/local/etc/redis/ca.pem +tls-auth-clients no +tls-cluster yes +tls-replication yes + +cluster-enabled yes +cluster-config-file nodes.conf +cluster-node-timeout 5000 + +appendonly yes + +requirepass very-secure-redis-cluster-password +masterauth very-secure-redis-cluster-password diff --git a/deploy/dockerephemeral/docker/redis-node-6-cert.pem b/deploy/dockerephemeral/docker/redis-node-6-cert.pem new file mode 100644 index 00000000000..c176eae043d --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-6-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmN9ktdBsuxTOPFUsU +qAjMnQSyBz/BpYDGMagy9e7PbtniVGTHHOvGgoq5VvPdtiVTerwefNAQaL3nLLvg +24hOEWBlQuBgK0gW48NPZJAbzYvNdF2jOzIzsDu8edEz4TcI8oKvw2WS5HQGl213 +06f2tMN1Ng0O07WoW8cxOYISsKVT9EyQJX4M/Oq5/nzHkXvS97ayFT0OvVdIRzPU +A6VsSyr/X1LgVmZEGfWcdv+cxJGBiXRsiWdW+Y+n6qvRBC2WpTEhCXomtbbDtuSH +e+8EXk9eKSc5QYFNCDWEMk25JuEQpXIMfdiHbMmK+9BgdRTUh8Pm94yD3hkMO6z5 +N5e3AgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy02hwSsFAAkMB0GA1UdDgQWBBRyt96xEM6o5VkG9JV5 +vLVxnBELSjAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAmxFmsjenSgrrI1sE7DJahX1CaNVGodx4CwVc2etEq5PBWC6r +DpfCcYDW+Hg64Ac+NiPaLxFaG/8aM7JSePbAa71AQN+2hJpsV3/ANvSUaJfbHSFx +xfTRr8m5l33IV7ynjvZCPXWK4Gc5o7/shPKObHjwb03DLJjW0rvD5SYIjfCLjlOk +na2ufQnrmEP0XO77EvP4G/sHBjUaXrthsYTISO3lBTnGoKWNj8YwTFtXILC3O1to +sKWKYe5A6FB6xathUVBfS+Drp0PIYdAU9N3adymv4tZf52ofMsbJNkDqY3JaWmcO +dYHuYTeYg6ZiVhzZeasd3V+wc/CKAD8U5UfD5A== +-----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-6-key.pem b/deploy/dockerephemeral/docker/redis-node-6-key.pem new file mode 100644 index 00000000000..0bc3f366189 --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-6-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCmN9ktdBsuxTOP +FUsUqAjMnQSyBz/BpYDGMagy9e7PbtniVGTHHOvGgoq5VvPdtiVTerwefNAQaL3n +LLvg24hOEWBlQuBgK0gW48NPZJAbzYvNdF2jOzIzsDu8edEz4TcI8oKvw2WS5HQG +l21306f2tMN1Ng0O07WoW8cxOYISsKVT9EyQJX4M/Oq5/nzHkXvS97ayFT0OvVdI +RzPUA6VsSyr/X1LgVmZEGfWcdv+cxJGBiXRsiWdW+Y+n6qvRBC2WpTEhCXomtbbD +tuSHe+8EXk9eKSc5QYFNCDWEMk25JuEQpXIMfdiHbMmK+9BgdRTUh8Pm94yD3hkM +O6z5N5e3AgMBAAECggEAQ088YB6zX0Y2McvyooPFRG6VVy5+UAGgWyICtdhHg7Kl +AvUf9k2s4K8+U/11NaQsC1kZUtNCQlLYDARedJkR4mNBAOCLEgaU48gJ8F2NyeR7 +p5Bm1tIC61GDbzh5UiPycGocJ+bdfBWNMpohlzObwdjDifSAZy+uUWYRDMr39G7x +9SH6aLL7ZHBg0Oc4dw6K4GQMrU7sdomQcSqNyi5sn6PN8FsuO1wMp8C8V+U6y5sb +36Y1rz90ZOFqmOBnG/IdPR8tFbdql1Yy31tzy/I4thK+1v4QN6JVLPvyw4H0RzFe +j347k5IsNehRdwltplhckeAUzWGGNiTx0zhQPAchuQKBgQDmyGB055GCtRoEIpqN +ANNa8PxTp2sCH+/J7KZma6gSJ9WY73xtGSVXX/Ubz4l8FHiGoA0CQCElARJ9zff/ +tAiNXqvcQeBPVC23CMJL3hxeHLNs0ipoD8qvdQpGit3DAZMjdjtt5jd48CulEmfP +/rVmeHKChZaPPR1EgrMnIytaCwKBgQC4YWygHnDjW9zekpsDRMKkvK5QMIey9ygB +LqXlXw6GANhVDGSr7zOHBtF1aBc6FA1FKlVRXz3Fag4pPZLd2HbEaKnzfCNPH5PL +UTX8fukftrzY03bvpYcr+/YabPO8H5hkeUqHyH9EyIgdj5hOhKEVj9kJkqENt3el +GvohkgdwhQKBgG0itPqTx6wYGIV8F7o2eby32Zt1wJTwpWTIFKi6oHB1hf0cw6qU +CaSYLEFKk6mpxJVlesFlskbdivETRgQWDzVLX9p5DKp3FGdKLRfToXaf+/mqKYOs +dB0lLAbQBK8DP6G1d8Uw6Wq3qOwXGCC0QvSCYSR4KAr0y7JqXG5Vo1qhAoGATLCh +GNxwgfDEpoL+HNbtys18B3iYCLVKm2tGr2fhR5V0ZbOY7/a3TPNmDdp0xsBuYJVi +FU1zCPi62SZ2PvX5OGp8Pf0lRpTQyWGG/fXfi0RbuigCsVz9IytSyt0EZ/wQS8Iz +YNThMr/h9cGzTP1Xbvt8/8FQYb8s8ayN24a8t20CgYBDyjVifJHw6iVl3vu/O+R9 ++AdSe5bEGGDuIKZRDJbEj2ScgD3Nwqdst7X5wC+rcUuyJNW22GihyLiC/+OCaJPl +9fyaRpWWjEUkzpvR+3GhzzykDnemw1z39AJrg3ewSaBdbw9Bvq0ebrGaDF+uCReY +V+yVEYFsBaK0JrbkIffXbA== +-----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-6.conf b/deploy/dockerephemeral/docker/redis-node-6.conf new file mode 100644 index 00000000000..2989c5550ea --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-node-6.conf @@ -0,0 +1,17 @@ +port 0 +tls-port 6378 +tls-cert-file /usr/local/etc/redis/cert.pem +tls-key-file /usr/local/etc/redis/key.pem +tls-ca-cert-file /usr/local/etc/redis/ca.pem +tls-auth-clients no +tls-cluster yes +tls-replication yes + +cluster-enabled yes +cluster-config-file nodes.conf +cluster-node-timeout 5000 + +appendonly yes + +requirepass very-secure-redis-cluster-password +masterauth very-secure-redis-cluster-password diff --git a/docs/src/developer/developer/building.md b/docs/src/developer/developer/building.md index 4cbdba83822..bcbca56502c 100644 --- a/docs/src/developer/developer/building.md +++ b/docs/src/developer/developer/building.md @@ -156,6 +156,7 @@ These services require most of the deployment dependencies as seen in the archit - Required internal dependencies: - cassandra (with the correct schema) - elasticsearch (with the correct schema) + - redis - Required external dependencies are the following configured AWS services (or “fake” replacements providing the same API): - SES - SQS diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 9154c97ea73..062f891f03c 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -1983,6 +1983,96 @@ elasticsearch-index: insecureSkipVerifyTls: true ``` +## Configure Redis authentication + +If the redis used needs authentication with either username and password or just +password (legacy auth), it can be configured like this: + +```yaml +gundeck: + secrets: + redisUsername: + redisPassword: +``` + +**NOTE**: When using redis < 6, the `redisUsername` must not be set at all (not +even set to `null` or empty string, the key must be absent from the config). +When using redis >= 6 and using legacy auth, the `redisUsername` must either be +not set at all or set to `"default"`. + +While doing migrations to another redis instance, the credentials for the +addtional redis can be set as follows: + +```yaml +gundeck: + secrets: + redisAdditionalWriteUsername: # Do not set this at all when using legacy auth + redisAdditionalWritePassword: +``` + +**NOTE**: `redisAddtiionalWriteUsername` follows same restrictions as +`redisUsername` when using legacy auth. + +## Configure TLS for Redis + +If the redis instance requires TLS, it can be configured like this: + +```yaml +gundeck: + config: + redis: + enableTls: true +``` + +In case a custom CA certificate is required it can be provided like this: + +```yaml +gundeck: + config: + redis: + tlsCa: +``` + +There is another way to provide this, in case there already exists a kubernetes +secret containing the CA certificate(s): + +```yaml +gundeck: + config: + redis: + tlsCaSecretRef: + name: + key: +``` + +For configuring `redisAdditionalWrite` in gundeck (this is required during a +migration from one redis instance to another), the settings need to be like +this: + +```yaml +gundeck: + config: + redisAdditionalWrite: + enableTls: true + # One or none of these: + # tlsCa: + # tlsCaSecretRef: +``` + +**WARNING:** Please do this only if you know what you’re doing. + +In case it is not possible to verify TLS certificate of the redis +server, it can be turned off without tuning off TLS like this: + +```yaml +gundeck: + config: + redis: + insecureSkipVerifyTls: true + redisAdditionalWrite: + insecureSkipVerifyTls: true +``` + ## Configure RabbitMQ RabbitMQ authentication must be configured on brig, galley and background-worker. For example: @@ -2015,7 +2105,7 @@ server, verification can be turned off by settings `insecureSkipVerifyTls` to ## Configure PostgreSQL -`brig`, `galley`, `gundeck`, and `background-worker` require a PostgreSQL database. The configured user needs to +`brig`, `galley`, and `background-worker` require a PostgreSQL database. The configured user needs to be able to write data and change the schema (e.g. create and alter tables.) The internal configuration YAML file format and the Helm charts for `brig` and @@ -2072,7 +2162,7 @@ The `port` needs to be a number provided as string. Besides the password file (`postgresqlPassword`), the fields correspond to [libpq-connect parameters](https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS). -The `postgresqlPassword` file is read by `brig`, `galley`, `gundeck`, and `background-worker`. Its content is +The `postgresqlPassword` file is read by `brig`, `galley`, and `background-worker`. Its content is used as `password` field. ### Using PostgreSQL for storing Cassandra-backed data diff --git a/docs/src/how-to/install/infrastructure-configuration.md b/docs/src/how-to/install/infrastructure-configuration.md index 06ef7a218d1..34e1eb2c19d 100644 --- a/docs/src/how-to/install/infrastructure-configuration.md +++ b/docs/src/how-to/install/infrastructure-configuration.md @@ -27,6 +27,7 @@ gundeck: - "10.0.0.0/8" - "elasticsearch-external" - "cassandra-external" + - "redis-ephemeral" - "fake-aws-sqs" - "fake-aws-dynamodb" - "fake-aws-sns" @@ -414,8 +415,8 @@ cassandra cannot reliably be installed on kubernetes. Some people have tried, e.g. [this project](https://github.com/instaclustr/cassandra-operator) though at the time of writing (Nov 2018), this does not yet work as advertised. We -recommend therefore to install cassandra, (possibly also elasticsearch) -separately, i.e. outside of kubernetes (using 3 nodes each). +recommend therefore to install cassandra, (possibly also elasticsearch +and redis) separately, i.e. outside of kubernetes (using 3 nodes each). For further higher-availability: diff --git a/docs/src/how-to/install/troubleshooting.md b/docs/src/how-to/install/troubleshooting.md index 3703500a9fd..ecb0ffb636a 100644 --- a/docs/src/how-to/install/troubleshooting.md +++ b/docs/src/how-to/install/troubleshooting.md @@ -252,7 +252,7 @@ These are some steps you can take to debug what is going on when the installatio As an example, we’ll take a case where we try installing `wire-server` with `helm`, but it fails due to `cassandra` being broken in some way. -This guide, while focusing on a `cassandra` related issue, will also provide general steps to debug problems that could be related to other components like `rabbitmq`, etc. +This guide, while focusing on a `cassandra` related issue, will also provide general steps to debug problems that could be related to other components like `rabbitmq`, `redis`, etc. Our first step is to identify and isolate which component is causing the issue. @@ -266,6 +266,7 @@ fake-aws-sns-76fb45cf4f-t6mg6 2/2 Running 0 75m fake-aws-sqs-6495cd7c98-w8f8w 2/2 Running 0 75m rabbitmq-external-0 0/1 Pending 0 78m reaper-84cfbf746d-wk8nc 1/1 Running 0 75m +redis-ephemeral-master-0 1/1 Running 0 76m ``` We then run the `wire-server` helm installation command: @@ -293,6 +294,7 @@ fake-aws-sns-76fb45cf4f-t6mg6 2/2 Running 0 95m fake-aws-sqs-6495cd7c98-w8f8w 2/2 Running 0 95m rabbitmq-external-0 0/1 Pending 0 98m reaper-84cfbf746d-wk8nc 1/1 Running 0 95m +redis-ephemeral-master-0 1/1 Running 0 96m ``` (You can also do `d kubectl get pods -o wide` to get more details though that’s not necessary here) diff --git a/hack/bin/gen-certs.sh b/hack/bin/gen-certs.sh index d4840f8af6d..f995a238aaa 100755 --- a/hack/bin/gen-certs.sh +++ b/hack/bin/gen-certs.sh @@ -81,6 +81,19 @@ install_certs "$TEMP/es" "$ROOT_DIR/deploy/dockerephemeral/docker" \ install_certs "$TEMP/es" "$ROOT_DIR/hack/helm_vars/certs" \ elasticsearch-ca elasticsearch-ca-key +# redis +mkdir -p "$TEMP/redis" +gen_ca "$TEMP/redis" redis.ca.example.com +REDIS="$ROOT_DIR/deploy/dockerephemeral/docker" +cp "$TEMP/redis/ca.pem" "$REDIS/redis-ca.pem" +for redis_node in $(seq 1 6); do + gen_cert "$TEMP/redis" "DNS:redis-${redis_node}, IP:172.20.0.3${redis_node}" + chmod 0644 "$TEMP/redis/key.pem" + install_certs "$TEMP/redis" "$REDIS" "" "" \ + "redis-node-${redis_node}-cert" \ + "redis-node-${redis_node}-key" +done + # rabbitmq RABBITMQ="$ROOT_DIR/deploy/dockerephemeral/rabbitmq-config/certificates" gen_ca "$RABBITMQ" rabbitmq.ca.example.com diff --git a/hack/helm_vars/certs/values.yaml.gotmpl b/hack/helm_vars/certs/values.yaml.gotmpl index 7cd8a633653..307d50fa48a 100644 --- a/hack/helm_vars/certs/values.yaml.gotmpl +++ b/hack/helm_vars/certs/values.yaml.gotmpl @@ -16,6 +16,59 @@ resources: ca: secretName: elasticsearch-ca + # redis CA and certificate + - apiVersion: cert-manager.io/v1 + kind: Issuer + metadata: + name: redis-ca-issuer + namespace: '{{ .Release.Namespace }}' + spec: + selfSigned: {} + - apiVersion: cert-manager.io/v1 + kind: Certificate + metadata: + name: redis-ca + namespace: '{{ .Release.Namespace }}' + spec: + secretName: redis-ca-certificate + isCA: true + duration: 2160h # 90d + renewBefore: 360h # 15d + commonName: redis.example.com + privateKey: + algorithm: RSA + encoding: PKCS1 + size: 2048 + issuerRef: + name: redis-ca-issuer + kind: Issuer + - apiVersion: cert-manager.io/v1 + kind: Issuer + metadata: + name: redis-issuer + namespace: '{{ .Release.Namespace }}' + spec: + ca: + secretName: redis-ca-certificate + - apiVersion: cert-manager.io/v1 + kind: Certificate + metadata: + name: redis + namespace: '{{ .Release.Namespace }}' + spec: + secretName: redis-certificate + isCA: false + duration: 2160h # 90d + renewBefore: 360h # 15d + commonName: redis-ephemeral + privateKey: + algorithm: RSA + encoding: PKCS1 + size: 2048 + issuerRef: + name: redis-issuer + kind: Issuer + # RabbitMQ CA and certificate - apiVersion: cert-manager.io/v1 kind: Issuer diff --git a/hack/helm_vars/redis-ephemeral/values.yaml b/hack/helm_vars/redis-ephemeral/values.yaml new file mode 100644 index 00000000000..996dc30e45c --- /dev/null +++ b/hack/helm_vars/redis-ephemeral/values.yaml @@ -0,0 +1,47 @@ +redis-ephemeral: + image: + registry: public.ecr.aws + repository: docker/library/redis + + redisConfig: | + requirepass very-secure-redis-master-password + + # ephemeral + save "" + + port 0 + tls-port 6379 + tls-cert-file /data/ssl/tls.crt + tls-key-file /data/ssl/tls.key + tls-ca-cert-file /data/ssl/ca.crt + tls-auth-clients no + + extraRedisSecrets: + - name: redis-certificate + mountPath: /data/ssl + + livenessProbe: + enabled: false + customLivenessProbe: + exec: + command: + - sh + - -c + - redis-cli --tls --cacert /data/ssl/ca.crt ping + readinessProbe: + enabled: false + customReadinessProbe: + exec: + command: + - sh + - -c + - redis-cli --tls --cacert /data/ssl/ca.crt ping + startupProbe: + enabled: false + customStartupProbe: + exec: + command: + - sh + - -c + - redis-cli --tls --cacert /data/ssl/ca.crt ping + diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 43373b1cf2e..f05d42a98e4 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -492,11 +492,13 @@ gundeck: tlsCaSecretRef: name: "rabbitmq-certificate" key: "ca.crt" - postgresql: - host: "postgresql" - port: "5432" - user: wire-server - dbname: wire-server + redis: + host: redis-ephemeral + connectionMode: master + enableTls: true + tlsCaSecretRef: + name: "redis-certificate" + key: "ca.crt" aws: account: "123456789012" region: eu-west-1 @@ -512,7 +514,7 @@ gundeck: secrets: awsKeyId: dummykey awsSecretKey: dummysecret - pgPassword: posty-the-gres + redisPassword: very-secure-redis-master-password rabbitmq: username: {{ .Values.rabbitmqUsername }} password: {{ .Values.rabbitmqPassword }} @@ -527,6 +529,7 @@ gundeck: uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} {{- end }} + redisAdditionalWritePassword: very-secure-redis-master-password-2 nginz: replicaCount: 1 @@ -726,6 +729,10 @@ integration: tlsCaSecretRef: name: {{ .Values.elasticsearch.caSecretName }} key: "ca.crt" + redis: + tlsCaSecretRef: + name: "redis-certificate" + key: "ca.crt" rabbitmq: tlsCaSecretRef: name: "rabbitmq-certificate" @@ -739,6 +746,7 @@ integration: uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} {{- end }} + redisPassword: very-secure-redis-master-password tls: caNamespace: wire-federation-v0 diff --git a/hack/helmfile.yaml.gotmpl b/hack/helmfile.yaml.gotmpl index f28ed995af5..09c825f9c31 100644 --- a/hack/helmfile.yaml.gotmpl +++ b/hack/helmfile.yaml.gotmpl @@ -114,6 +114,15 @@ releases: values: - './helm_vars/certs/values.yaml.gotmpl' + - name: 'redis-ephemeral' + namespace: '{{ .Values.namespace1 }}' + chart: '../.local/charts/redis-ephemeral' + values: + - './helm_vars/wire-image-mirror.yaml' + - './helm_vars/redis-ephemeral/values.yaml' + needs: + - certs + - name: 'cassandra-ephemeral' namespace: '{{ .Values.namespace1 }}' chart: '../.local/charts/cassandra-ephemeral' @@ -138,6 +147,15 @@ releases: name: elasticsearch kind: Issuer + - name: 'redis-ephemeral' + namespace: '{{ .Values.namespace2 }}' + chart: '../.local/charts/redis-ephemeral' + values: + - './helm_vars/wire-image-mirror.yaml' + - './helm_vars/redis-ephemeral/values.yaml' + needs: + - certs + - name: 'cassandra-ephemeral' namespace: '{{ .Values.namespace2 }}' chart: '../.local/charts/cassandra-ephemeral' @@ -198,6 +216,22 @@ releases: values: - './helm_vars/opensearch/values.yaml.gotmpl' + # Required for testing redis migration + - name: redis-ephemeral-2 + namespace: '{{ .Values.namespace1 }}' + chart: '../.local/charts/redis-ephemeral' + values: + - redis-ephemeral: + image: + registry: public.ecr.aws + repository: docker/library/redis + + redisConfig: | + requirepass very-secure-redis-master-password-2 + + # ephemeral + save "" + - name: 'certs' namespace: '{{ .Values.namespace2 }}' chart: bedag/raw @@ -324,6 +358,7 @@ releases: value: true needs: - 'cassandra-ephemeral' + - 'redis-ephemeral' - 'postgresql' - name: 'wire-server' @@ -341,6 +376,7 @@ releases: value: {{ .Values.federationDomain2 }} needs: - 'cassandra-ephemeral' + - 'redis-ephemeral' - 'postgresql' - name: wire-server-enterprise diff --git a/libs/wire-api/src/Wire/API/Presence.hs b/libs/wire-api/src/Wire/API/Presence.hs index 9ec538f064f..427e0a0f8af 100644 --- a/libs/wire-api/src/Wire/API/Presence.hs +++ b/libs/wire-api/src/Wire/API/Presence.hs @@ -23,6 +23,7 @@ import Data.Aeson.Types qualified as A import Data.Attoparsec.ByteString (takeByteString) import Data.ByteString.Char8 qualified as Bytes import Data.ByteString.Conversion +import Data.ByteString.Lazy qualified as Lazy import Data.Id import Data.Misc (Milliseconds) import Data.OpenApi qualified as S @@ -34,6 +35,7 @@ import Imports import Network.URI qualified as Net import Servant.API (ToHttpApiData (toUrlPiece)) +-- FUTUREWORK: use Network.URI and toss this newtype. servant should have all these instances for us these days. newtype URI = URI { fromURI :: Net.URI } @@ -75,7 +77,9 @@ data Presence = Presence -- operating the team settings pages without the need for -- end-to-end crypto. clientId :: !(Maybe ClientId), - createdAt :: !Milliseconds + createdAt :: !Milliseconds, + -- | REFACTOR: temp. addition to ease migration + __field :: !Lazy.ByteString } deriving (Eq, Ord, Show) deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema Presence) @@ -90,6 +94,7 @@ instance ToSchema Presence where <*> clientId .= optField "client_id" (maybeWithDefault A.Null schema) -- keep null for backwards compat <*> createdAt .= (fromMaybe 0 <$> (optField "created_at" schema)) ) + <&> ($ ("" :: Lazy.ByteString)) uriSchema :: ValueSchema NamedSwaggerDoc URI uriSchema = mkSchema desc uriFromJSON (Just . uriToJSON) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Presence.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Presence.hs index adb5f582d44..97005af0a0d 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Presence.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Presence.hs @@ -35,6 +35,7 @@ testObject_Presence_1 = (fromJust $ parse "http://example.com/") Nothing 0 + "" testObject_Presence_2 :: Presence testObject_Presence_2 = @@ -44,6 +45,7 @@ testObject_Presence_2 = (fromJust $ parse "http://example.com/3") (Just (ClientId 1)) 12323 + "" -- __field always has to be "", see ToSchema instance. testObject_Presence_3 :: Presence testObject_Presence_3 = @@ -53,3 +55,4 @@ testObject_Presence_3 = (fromJust $ parse "http://example.com/3") (Just (ClientId 1)) 0 + "" -- __field always has to be "", see ToSchema instance. diff --git a/libs/wire-subsystems/postgres-migrations/20260828093750-gundeck-presence.sql b/libs/wire-subsystems/postgres-migrations/20260828093750-gundeck-presence.sql deleted file mode 100644 index eb84c4ed7f1..00000000000 --- a/libs/wire-subsystems/postgres-migrations/20260828093750-gundeck-presence.sql +++ /dev/null @@ -1,12 +0,0 @@ --- WPB-28377: gundeck presence (replaces redis presence hashes) -CREATE TABLE IF NOT EXISTS presence ( - user_id uuid NOT NULL, - conn_id text NOT NULL, - resource text NOT NULL, - client_id text, - created_at timestamptz NOT NULL, - PRIMARY KEY (user_id, conn_id) -); - --- index for cleanup deletes like `DELETE ... WHERE created_at < now() - interval '7 days'` -CREATE INDEX presence_created_at_idx ON presence (created_at); diff --git a/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs b/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs index 2410cb98c65..920782cc856 100644 --- a/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs +++ b/libs/wire-subsystems/src/Wire/JobSubsystem/Migrations.hs @@ -19,13 +19,11 @@ -- with this program. If not, see . module Wire.JobSubsystem.Migrations - ( defaultSchemaName, - mkArbiterConnectionString, + ( mkArbiterConnectionString, runJobMigrations, ) where -import Arbiter.Core (defaultSchemaName) import Arbiter.Migrations qualified as ArbiterMigrations import Control.Exception (bracket, bracket_, throwIO) import Data.Hashable qualified as Hashable diff --git a/libs/wire-subsystems/src/Wire/Postgres.hs b/libs/wire-subsystems/src/Wire/Postgres.hs index a0210c21cf8..f91f3637efe 100644 --- a/libs/wire-subsystems/src/Wire/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/Postgres.hs @@ -43,7 +43,6 @@ module Wire.Postgres runTransaction, runTransactionWithRetry, runPipeline, - useWithResetAndRetry, parseCount, PGConstraints, diff --git a/postgres-schema.sql b/postgres-schema.sql index 2d3df5fb27f..de4a57a0f2e 100644 --- a/postgres-schema.sql +++ b/postgres-schema.sql @@ -1511,21 +1511,6 @@ CREATE TABLE public.mls_history_client ( ALTER TABLE public.mls_history_client OWNER TO "wire-server"; --- --- Name: presence; Type: TABLE; Schema: public; Owner: wire-server --- - -CREATE TABLE public.presence ( - user_id uuid NOT NULL, - conn_id text NOT NULL, - resource text NOT NULL, - client_id text, - created_at timestamp with time zone NOT NULL -); - - -ALTER TABLE public.presence OWNER TO "wire-server"; - -- -- Name: remote_conversation_local_member; Type: TABLE; Schema: public; Owner: wire-server -- @@ -1982,14 +1967,6 @@ ALTER TABLE ONLY public.mls_history_client ADD CONSTRAINT mls_history_client_pkey PRIMARY KEY (group_id, id); --- --- Name: presence presence_pkey; Type: CONSTRAINT; Schema: public; Owner: wire-server --- - -ALTER TABLE ONLY public.presence - ADD CONSTRAINT presence_pkey PRIMARY KEY (user_id, conn_id); - - -- -- Name: remote_conversation_local_member remote_conversation_local_member_pkey; Type: CONSTRAINT; Schema: public; Owner: wire-server -- @@ -2462,13 +2439,6 @@ CREATE INDEX idx_meetings_recurrence_eff_end ON public.meetings USING btree (GRE CREATE INDEX idx_meetings_start_time ON public.meetings USING btree (start_time); --- --- Name: presence_created_at_idx; Type: INDEX; Schema: public; Owner: wire-server --- - -CREATE INDEX presence_created_at_idx ON public.presence USING btree (created_at); - - -- -- Name: user_group_member_user_id_idx; Type: INDEX; Schema: public; Owner: wire-server -- diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 20365d1ebb9..fd170bdbaff 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -69,7 +69,6 @@ import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai import Wire.API.User (AccountStatus (PendingInvitation)) import Wire.DeleteQueue -import Wire.JobSubsystem.Migrations (defaultSchemaName, mkArbiterConnectionString, runJobMigrations) import Wire.OpenTelemetry (withTracer) import Wire.PostgresMigrations import Wire.Sem.Paging qualified as P @@ -118,10 +117,6 @@ migratePostgres opts resetFirst = do pool <- (.rawPool) <$> initPostgresPool opts.postgresqlPool opts.postgresql opts.postgresqlPassword when resetFirst $ resetSchema pool logger runAllMigrations pool logger - -- Also create the arbiter job schema, so that this command yields the full - -- database schema (e.g. for `make postgres-schema`). - arbiterConnStr <- mkArbiterConnectionString opts.postgresql opts.postgresqlPassword - runJobMigrations arbiterConnStr defaultSchemaName flush logger mkApp :: Opts -> IO (Wai.Application, Env) diff --git a/services/gundeck/default.nix b/services/gundeck/default.nix index 5de25d7fff5..2e4f8b69d5f 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -22,14 +22,14 @@ , conduit , containers , criterion +, crypton-x509-store , data-timeout , errors , exceptions , extended , extra , foldl -, hasql -, hasql-th +, hedis , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk , HsOpenSSL @@ -37,6 +37,7 @@ , http-client-tls , http-types , imports +, kan-extensions , lens , lens-aeson , lib @@ -45,6 +46,7 @@ , MonadRandom , mtl , multiset +, network , network-uri , optparse-applicative , prometheus-client @@ -77,7 +79,6 @@ , unliftio , unordered-containers , uuid -, vector , wai , wai-extra , wai-middleware-gunzip @@ -85,7 +86,6 @@ , websockets , wire-api , wire-otel -, wire-subsystems , yaml }: mkDerivation { @@ -110,14 +110,14 @@ mkDerivation { bytestring-conversion cassandra-util containers + crypton-x509-store data-timeout errors exceptions extended extra foldl - hasql - hasql-th + hedis hs-opentelemetry-instrumentation-wai hs-opentelemetry-sdk http-client @@ -148,14 +148,12 @@ mkDerivation { unliftio unordered-containers uuid - vector wai wai-extra wai-middleware-gunzip wai-utilities wire-api wire-otel - wire-subsystems yaml ]; executableHaskellDepends = [ @@ -175,8 +173,10 @@ mkDerivation { http-client http-client-tls imports + kan-extensions lens lens-aeson + network network-uri optparse-applicative random @@ -191,6 +191,7 @@ mkDerivation { tinylog types-common uuid + wai-utilities websockets wire-api yaml diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index 9ad88362bc4..06bf1b5024f 100644 --- a/services/gundeck/gundeck.cabal +++ b/services/gundeck/gundeck.cabal @@ -39,6 +39,7 @@ library Gundeck.Push.Native.Types Gundeck.Push.Websocket Gundeck.React + Gundeck.Redis Gundeck.Run Gundeck.Schema.Run Gundeck.Schema.V1 @@ -57,6 +58,7 @@ library Gundeck.ThreadBudget.Internal Gundeck.Util Gundeck.Util.DelayQueue + Gundeck.Util.Redis other-modules: Paths_gundeck hs-source-dirs: src @@ -124,14 +126,14 @@ library , bytestring-conversion >=0.2 , cassandra-util >=0.16.2 , containers >=0.5 + , crypton-x509-store , data-timeout , errors >=2.0 , exceptions >=0.4 , extended , extra >=1.1 , foldl - , hasql - , hasql-th + , hedis >=0.14.0 , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk , http-client >=0.7 @@ -162,14 +164,12 @@ library , unliftio >=0.2 , unordered-containers >=0.2 , uuid >=1.3 - , vector , wai >=3.2 , wai-extra >=3.0 , wai-middleware-gunzip >=0.0.2 , wai-utilities >=0.16 , wire-api , wire-otel - , wire-subsystems , yaml >=0.8 default-language: GHC2021 @@ -244,6 +244,7 @@ executable gundeck-integration Metrics Paths_gundeck TestSetup + Util hs-source-dirs: test/integration default-extensions: @@ -296,7 +297,7 @@ executable gundeck-integration build-depends: , aeson , async - , base >=4 && <5 + , base >=4 && <5 , base16-bytestring >=0.1 , bilge , bytestring @@ -309,8 +310,10 @@ executable gundeck-integration , http-client , http-client-tls , imports + , kan-extensions , lens , lens-aeson + , network , network-uri , optparse-applicative , random @@ -324,6 +327,7 @@ executable gundeck-integration , tinylog , types-common , uuid + , wai-utilities >=0.16 , websockets >=0.8 , wire-api , yaml diff --git a/services/gundeck/gundeck.integration.yaml b/services/gundeck/gundeck.integration.yaml index bb19ab89eb7..00c80574794 100644 --- a/services/gundeck/gundeck.integration.yaml +++ b/services/gundeck/gundeck.integration.yaml @@ -13,17 +13,18 @@ cassandra: keyspace: gundeck_test # filterNodesByDatacentre: datacenter1 -postgresql: - host: 127.0.0.1 - port: "5432" - user: wire-server - dbname: backendA - password: posty-the-gres - -postgresqlPool: - size: 20 - acquisitionTimeout: 10s - idlenessTimeout: 10m +redis: + host: 172.20.0.31 + port: 6373 + connectionMode: cluster # master | cluster + enableTls: true + tlsCa: ../../deploy/dockerephemeral/docker/redis-ca.pem + insecureSkipVerifyTls: false + +# redisAdditionalWrite: +# host: 127.0.0.1 +# port: 6379 +# connectionMode: master aws: queueName: integration-gundeck-events diff --git a/services/gundeck/src/Gundeck/Env.hs b/services/gundeck/src/Gundeck/Env.hs index a7f233fb2c0..39f6f98bda7 100644 --- a/services/gundeck/src/Gundeck/Env.hs +++ b/services/gundeck/src/Gundeck/Env.hs @@ -23,19 +23,30 @@ import Bilge hiding (host, port) import Cassandra (ClientState) import Cassandra.Util (initCassandraForService) import Control.AutoUpdate +import Control.Concurrent.Async (Async) import Control.Lens (makeLenses, (^.)) +import Control.Retry (capDelay, exponentialBackoff) +import Data.ByteString.Char8 qualified as BSChar8 import Data.Id import Data.Misc (Milliseconds (..)) +import Data.Text qualified as Text +import Data.Time.Clock import Data.Time.Clock.POSIX +import Data.X509.CertificateStore as CertStore +import Database.Redis qualified as Redis import Gundeck.Aws qualified as Aws -import Gundeck.Options +import Gundeck.Options as Opt hiding (host, port) +import Gundeck.Options qualified as O +import Gundeck.Redis qualified as Redis import Gundeck.ThreadBudget -import Hasql.Pool.Extended qualified as HasqlPoolExt import Imports import Network.AMQP (Channel) import Network.AMQP.Extended qualified as Q import Network.HTTP.Client (responseTimeoutMicro) import Network.HTTP.Client.TLS (tlsManagerSettings) +import Network.TLS as TLS +import Network.TLS.Extra qualified as TLS +import System.Logger qualified as Log import System.Logger.Extended qualified as Logger data Env = Env @@ -44,7 +55,8 @@ data Env = Env _applog :: !Logger.Logger, _manager :: !Manager, _cstate :: !ClientState, - _hasqlPool :: !HasqlPoolExt.Pool, + _rstate :: !Redis.RobustConnection, + _rstateAdditionalWrite :: !(Maybe Redis.RobustConnection), _awsEnv :: !Aws.Env, _time :: !(IO Milliseconds), _threadBudgetState :: !(Maybe ThreadBudgetState), @@ -53,7 +65,7 @@ data Env = Env makeLenses ''Env -createEnv :: Opts -> IO Env +createEnv :: Opts -> IO ([Async ()], Env) createEnv o = do l <- Logger.mkLogger (o ^. logLevel) (o ^. logNetStrings) (o ^. logFormat) n <- @@ -64,7 +76,17 @@ createEnv o = do managerResponseTimeout = responseTimeoutMicro 5000000 } - pgPool <- HasqlPoolExt.initPostgresPool (o ^. postgresqlPool) (o ^. postgresql) (o ^. postgresqlPassword) + redisUsername <- BSChar8.pack <$$> lookupEnv "REDIS_USERNAME" + redisPassword <- BSChar8.pack <$$> lookupEnv "REDIS_PASSWORD" + (rThread, r) <- createRedisPool l (o ^. redis) redisUsername redisPassword "main-redis" + + (rAdditionalThreads, rAdditional) <- case o ^. redisAdditionalWrite of + Nothing -> pure ([], Nothing) + Just additionalRedis -> do + additionalRedisUsername <- BSChar8.pack <$$> lookupEnv "REDIS_ADDITIONAL_WRITE_USERNAME" + addtionalRedisPassword <- BSChar8.pack <$$> lookupEnv "REDIS_ADDITIONAL_WRITE_PASSWORD" + (rAddThread, rAdd) <- createRedisPool l additionalRedis additionalRedisUsername addtionalRedisPassword "additional-write-redis" + pure ([rAddThread], Just rAdd) p <- initCassandraForService @@ -82,8 +104,55 @@ createEnv o = do } mtbs <- mkThreadBudgetState `mapM` (o ^. settings . maxConcurrentNativePushes) rabbitMqChannelMVar <- Q.mkRabbitMqChannelMVar l (Just "gundeck") (o ^. rabbitmq) - pure $! Env (RequestId defRequestId) o l n p pgPool a io mtbs rabbitMqChannelMVar + pure $! (rThread : rAdditionalThreads,) $! Env (RequestId defRequestId) o l n p r rAdditional a io mtbs rabbitMqChannelMVar reqIdMsg :: RequestId -> Logger.Msg -> Logger.Msg reqIdMsg = ("request" Logger..=) . unRequestId {-# INLINE reqIdMsg #-} + +createRedisPool :: Logger.Logger -> RedisEndpoint -> Maybe ByteString -> Maybe ByteString -> ByteString -> IO (Async (), Redis.RobustConnection) +createRedisPool l ep username password identifier = do + customCertStore <- case ep._tlsCa of + Nothing -> pure Nothing + Just caPath -> CertStore.readCertificateStore caPath + let defClientParams = defaultParamsClient (Text.unpack ep._host) "" + tlsParams = + guard ep._enableTls + $> defClientParams + { clientHooks = + if ep._insecureSkipVerifyTls + then defClientParams.clientHooks {onServerCertificate = \_ _ _ _ -> pure []} + else defClientParams.clientHooks, + clientShared = + case customCertStore of + Nothing -> defClientParams.clientShared + Just sharedCAStore -> defClientParams.clientShared {sharedCAStore}, + clientSupported = + defClientParams.clientSupported + { supportedVersions = [TLS.TLS13, TLS.TLS12], + supportedCiphers = TLS.ciphersuite_strong + } + } + let redisConnInfo = + Redis.defaultConnectInfo + { Redis.connectAddr = Redis.ConnectAddrHostPort (Text.unpack ep._host) (fromIntegral ep._port), + Redis.connectUsername = username, + Redis.connectAuth = password, + Redis.connectTimeout = Just (secondsToNominalDiffTime 5), + Redis.connectMaxConnections = 100, + Redis.connectTLSParams = tlsParams + } + + Log.info l $ + Log.msg (Log.val $ "starting connection to " <> identifier <> "...") + . Log.field "connectionMode" (show $ ep ^. O.connectionMode) + . Log.field "connInfo" (safeShowConnInfo redisConnInfo) + let connectWithRetry = Redis.connectRobust l (capDelay 1000000 (exponentialBackoff 50000)) + r <- case ep ^. O.connectionMode of + Master -> connectWithRetry $ Redis.checkedConnect redisConnInfo + Cluster -> connectWithRetry $ Redis.checkedConnectCluster redisConnInfo + Log.info l $ Log.msg (Log.val $ "Established connection to " <> identifier <> ".") + pure r + +safeShowConnInfo :: Redis.ConnectInfo -> String +safeShowConnInfo connInfo = show $ connInfo {Redis.connectAuth = "[REDACTED]" <$ Redis.connectAuth connInfo} diff --git a/services/gundeck/src/Gundeck/Monad.hs b/services/gundeck/src/Gundeck/Monad.hs index db3eda96184..832bff5d890 100644 --- a/services/gundeck/src/Gundeck/Monad.hs +++ b/services/gundeck/src/Gundeck/Monad.hs @@ -33,6 +33,10 @@ module Gundeck.Monad runGundeck, posixTime, getRabbitMqChan, + + -- * Select which redis to target + runWithDefaultRedis, + runWithAdditionalRedis, msToUTCSecs, ) where @@ -49,7 +53,9 @@ import Data.Time (UTCTime) import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import Data.UUID as UUID import Data.UUID.V4 as UUID +import Database.Redis qualified as Redis import Gundeck.Env +import Gundeck.Redis qualified as Redis import Imports import Network.AMQP import Network.HTTP.Types @@ -61,6 +67,7 @@ import System.Logger (Logger) import System.Logger qualified as Logger import System.Logger.Class qualified as Log import System.Timeout +import UnliftIO (async) -- | TODO: 'Client' already has an 'Env'. Why do we need two? How does this even work? We should -- probably explain this here. @@ -84,6 +91,72 @@ newtype Gundeck a = Gundeck instance MonadMonitor Gundeck where doIO = liftIO +-- | 'Gundeck' doesn't have an instance for 'MonadRedis' because it contains two +-- connections to two redis instances. When using 'WithDefaultRedis', any redis +-- operation will only target the default redis instance (configured under +-- 'redis:' in the gundeck config). To write to both redises use +-- 'WithAdditionalRedis'. +newtype WithDefaultRedis a = WithDefaultRedis {runWithDefaultRedis :: Gundeck a} + deriving newtype + ( Functor, + Applicative, + Monad, + MonadIO, + MonadThrow, + MonadCatch, + MonadMask, + MonadReader Env, + MonadClient, + MonadUnliftIO, + Log.MonadLogger + ) + +instance Redis.MonadRedis WithDefaultRedis where + liftRedis action = do + defaultConn <- view rstate + Redis.runRobust defaultConn action + +instance Redis.RedisCtx WithDefaultRedis (Either Redis.Reply) where + returnDecode :: (Redis.RedisResult a) => Redis.Reply -> WithDefaultRedis (Either Redis.Reply a) + returnDecode = Redis.liftRedis . Redis.returnDecode + +-- | 'Gundeck' doesn't have an instance for 'MonadRedis' because it contains two +-- connections to two redis instances. When using 'WithAdditionalRedis', any +-- redis operation will target both redis instances (configured under 'redis:' +-- and 'redisAddtionalWrite:' in the gundeck config). To write to only the +-- default redis use 'WithDefaultRedis'. +newtype WithAdditionalRedis a = WithAdditionalRedis {runWithAdditionalRedis :: Gundeck a} + deriving newtype + ( Functor, + Applicative, + Monad, + MonadIO, + MonadThrow, + MonadCatch, + MonadMask, + MonadReader Env, + MonadClient, + MonadUnliftIO, + Log.MonadLogger + ) + +instance Redis.MonadRedis WithAdditionalRedis where + liftRedis action = do + defaultConn <- view rstate + ret <- Redis.runRobust defaultConn action + + mAdditionalRedisConn <- view rstateAdditionalWrite + for_ mAdditionalRedisConn $ \additionalRedisConn -> + -- We just fire and forget this call, as there is not much we can do if + -- this fails. + async $ Redis.runRobust additionalRedisConn action + + pure ret + +instance Redis.RedisCtx WithAdditionalRedis (Either Redis.Reply) where + returnDecode :: (Redis.RedisResult a) => Redis.Reply -> WithAdditionalRedis (Either Redis.Reply a) + returnDecode = Redis.liftRedis . Redis.returnDecode + instance Log.MonadLogger Gundeck where log l m = do e <- ask diff --git a/services/gundeck/src/Gundeck/Options.hs b/services/gundeck/src/Gundeck/Options.hs index 5222248da27..d70bbc4f91d 100644 --- a/services/gundeck/src/Gundeck/Options.hs +++ b/services/gundeck/src/Gundeck/Options.hs @@ -24,7 +24,6 @@ import Control.Lens hiding (Level) import Data.Aeson.TH import Data.Yaml (FromJSON) import Gundeck.Aws.Arn -import Hasql.Pool.Extended (PoolConfig) import Imports import Network.AMQP.Extended import System.Logger.Extended (Level, LogFormat) @@ -103,6 +102,30 @@ deriveFromJSON toOptionFieldName ''MaxConcurrentNativePushes makeLenses ''MaxConcurrentNativePushes +data RedisConnectionMode + = Master + | Cluster + deriving (Show, Generic) + +deriveJSON defaultOptions {constructorTagModifier = map toLower} ''RedisConnectionMode + +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) + +deriveFromJSON toOptionFieldName ''RedisEndpoint + +makeLenses ''RedisEndpoint + makeLenses ''Settings deriveFromJSON toOptionFieldName ''Settings @@ -112,11 +135,8 @@ data Opts = Opts _gundeck :: !Endpoint, _brig :: !Endpoint, _cassandra :: !CassandraOpts, - -- | 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, + _redis :: !RedisEndpoint, + _redisAdditionalWrite :: !(Maybe RedisEndpoint), _aws :: !AWSOpts, _rabbitmq :: !AmqpEndpoint, _discoUrl :: !(Maybe Text), diff --git a/services/gundeck/src/Gundeck/Presence.hs b/services/gundeck/src/Gundeck/Presence.hs index 6c6b757ef59..aa8fb778095 100644 --- a/services/gundeck/src/Gundeck/Presence.hs +++ b/services/gundeck/src/Gundeck/Presence.hs @@ -33,10 +33,10 @@ import Wire.API.CannonId import Wire.API.Presence listH :: UserId -> Gundeck [Presence] -listH = Data.list +listH = runWithDefaultRedis . Data.list listAllH :: CommaSeparatedList UserId -> Gundeck [Presence] -listAllH uids = concat <$> Data.listAll (fromCommaSeparatedList uids) +listAllH uids = concat <$> runWithDefaultRedis (Data.listAll (fromCommaSeparatedList uids)) addH :: Presence -> Gundeck (Headers '[Header "Location" URI] NoContent) addH p = do diff --git a/services/gundeck/src/Gundeck/Presence/Data.hs b/services/gundeck/src/Gundeck/Presence/Data.hs index 622dba9329b..6173ace303d 100644 --- a/services/gundeck/src/Gundeck/Presence/Data.hs +++ b/services/gundeck/src/Gundeck/Presence/Data.hs @@ -20,156 +20,128 @@ module Gundeck.Presence.Data list, listAll, deleteAll, - cleanup, ) where -import Control.Lens (view) -import Control.Monad.Catch (throwM) -import Data.ByteString.Conversion (fromByteString, toByteString') +import Control.Monad.Catch +import Data.Aeson as Aeson +import Data.ByteString qualified as Strict +import Data.ByteString.Builder (byteString) +import Data.ByteString.Char8 qualified as StrictChars +import Data.ByteString.Conversion hiding (fromList) +import Data.ByteString.Lazy qualified as Lazy import Data.Id -import Data.Map.Strict qualified as Map -import Data.Misc (Milliseconds (..)) -import Data.Text (pack, unpack) -import Data.Text.Encoding (decodeUtf8, encodeUtf8) -import Data.Time (UTCTime) -import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds) -import Data.UUID (UUID) -import Data.Vector qualified as Vector -import Gundeck.Env (hasqlPool) -import Gundeck.Monad -import Hasql.Session (Session, statement) -import Hasql.Statement (Statement) -import Hasql.TH +import Data.List.NonEmpty qualified as NonEmpty +import Data.Misc (Milliseconds) +import Database.Redis +import Gundeck.Monad (Gundeck, posixTime, runWithAdditionalRedis) +import Gundeck.Util.Redis import Imports -import System.Logger.Class qualified as Log +import System.Logger.Class (MonadLogger) import Wire.API.Presence -import Wire.Postgres qualified as Postgres --- | Register (or refresh) a presence. The server-side timestamp is stamped --- here, the 'Presence'\'s own 'createdAt' value is ignored (as in the redis --- implementation before). +-- Note [Migration] --------------------------------------------------------- +-- +-- Previous redis schema: user:=@= +-- New redis schema: user:= = +-- +-- The previous redis schema encodes cannon's ID in the subkey. The migration +-- proceeds as follows: +-- +-- 1. When adding new entries, we only use the connection as subkey. +-- 2. When listing entries (which does not use the subkey fortunately) we +-- store the original field name in the `Presence` record property `__field`. +-- 3. When deleting entries, we use this `Presence`'s `__field` value. +-- 4. Eventually `__field` can be removed from the `Presence` type and the +-- connection can be used directly instead. +-- + add :: Presence -> Gundeck () add p = do - nowMs <- posixTime - runPool $ - statement - (toUUID (userId p), connIdText (connId p), uriText (resource p), clientToText <$> clientId p, msToUtc (fromIntegral (ms nowMs))) - upsertPresence - --- | Read all presences of a single user. -list :: UserId -> Gundeck [Presence] -list u = fromMaybe [] . listToMaybe <$> listAll [u] - --- | Read all presences of the given users, one list per user (input order, --- empty list for users without presences). Single round trip. -listAll :: [UserId] -> Gundeck [[Presence]] -listAll [] = pure [] -listAll uu = do - rows <- runPool $ statement (Vector.fromList (toUUID <$> uu)) selectByUsers - presencesByUser <- - foldM - ( \acc (u, c, r, cl, t) -> case readPresenceRow u c r cl t of - Just p -> pure $! Map.insertWith (<>) (userId p) [p] acc - Nothing -> do - Log.warn $ - Log.msg (Log.val "ignoring unreadable presence row") - . Log.field "user_id" (show u) - . Log.field "conn_id" (show c) - pure acc - ) - Map.empty - (Vector.toList rows) - pure [Map.findWithDefault [] u presencesByUser | u <- uu] - --- | Compare-and-delete: only delete the stored presence if it is not newer --- than the given one (a newer re-registration with the same conn id must not --- be deleted by a stale disconnect). -deleteAll :: [Presence] -> Gundeck () -deleteAll [] = pure () -deleteAll pp = - runPool . statement params $ deleteMany + now <- posixTime + let k = toKey (userId p) + let v = toField (connId p) + let d = Lazy.toStrict $ Aeson.encode $ PresenceData p.resource p.clientId now + runWithAdditionalRedis . retry x3 $ do + void . fromTxResult <=< (liftRedis . multiExec) $ do + void $ hset k (NonEmpty.singleton (v, d)) + -- nb. All presences of a user are expired 'maxIdleTime' after the + -- last presence was registered. A client who keeps a presence + -- (i.e. websocket) connected for longer than 'maxIdleTime' will be + -- silently dropped and receives no more notifications. + expire k maxIdleTime where - params = - ( Vector.fromList (toUUID . userId <$> pp), - Vector.fromList (connIdText . connId <$> pp), - Vector.fromList (msToUtc . fromIntegral . ms . createdAt <$> pp) - ) - --- | Delete presences older than a week. Normal disconnects delete their --- presence rows; this only guards against leaks from abnormally dead pods --- (replaces the redis key TTL). -cleanup :: Gundeck () -cleanup = runPool $ statement () deleteStale - --- Helpers ------------------------------------------------------------------- + maxIdleTime = 7 * 24 * 60 * 60 -- 7 days in seconds --- | Millis <-> UTC. Exact (milliseconds nest inside timestamptz's microseconds); --- do NOT reuse 'Gundeck.Monad.msToUTCSecs', it truncates to whole seconds. -msToUtc :: Int64 -> UTCTime -msToUtc p = posixSecondsToUTCTime (fromRational (fromIntegral p / 1000 :: Rational)) - -utcToMs :: UTCTime -> Int64 -utcToMs = floor . (* 1000) . utcTimeToPOSIXSeconds - -newtype PresenceDbError = PresenceDbError Text deriving (Show) - -instance Exception PresenceDbError - -runPool :: Session a -> Gundeck a -runPool sess = do - pool <- view hasqlPool - liftIO (Postgres.useWithResetAndRetry pool sess) >>= either (throwM . PresenceDbError . pack . show) pure - -connIdText :: ConnId -> Text -connIdText = decodeUtf8 . fromConnId +deleteAll :: (MonadMask m, MonadIO m, RedisCtx m (Either Reply), MonadLogger m) => [Presence] -> m () +deleteAll [] = pure () +deleteAll pp = for_ pp $ \p -> do + let k = toKey (userId p) + let f = Lazy.toStrict $ __field p + void . retry x3 $ do + void . liftRedis $ watch (pure k) + value <- either (throwM . RedisSimpleError) id <$> hget k f + void . liftRedis . multiExec $ do + case value of + Nothing -> pure $ pure () + Just v -> do + let p' = readPresence (userId p) (f, v) + if Just p == p' + then void <$> hdel k (pure f) + else pure $ pure () + +list :: (MonadRedis m, MonadThrow m) => UserId -> m [Presence] +list u = do + ePresenses <- liftRedis $ list' u + case ePresenses of + Left r -> throwM $ RedisSimpleError r + Right ps -> pure ps + +list' :: (RedisCtx m f, Functor f) => UserId -> m (f [Presence]) +list' u = mapMaybe (readPresence u) <$$> hgetall (toKey u) + +-- FUTUREWORK: Make this not fail if it fails only for a few users. +listAll :: (MonadRedis m, MonadThrow m) => [UserId] -> m [[Presence]] +listAll [] = pure [] +listAll uu = mapM list uu -uriText :: URI -> Text -uriText = decodeUtf8 . toByteString' +-- Helpers ------------------------------------------------------------------- -readPresenceRow :: UUID -> Text -> Text -> Maybe Text -> UTCTime -> Maybe Presence -readPresenceRow u c r cl t = do - uri <- parse (unpack r) - cid <- traverse parseClient cl - pure (Presence (Id u) (ConnId (encodeUtf8 c)) uri cid (Ms (fromIntegral (utcToMs t)))) - where - parseClient = fromByteString . encodeUtf8 - -upsertPresence :: Statement (UUID, Text, Text, Maybe Text, UTCTime) () -upsertPresence = - [resultlessStatement| - INSERT INTO presence (user_id, conn_id, resource, client_id, created_at) - VALUES ($1 :: uuid, $2 :: text, $3 :: text, $4 :: text?, $5 :: timestamptz) - ON CONFLICT (user_id, conn_id) DO UPDATE - SET resource = EXCLUDED.resource, - client_id = EXCLUDED.client_id, - created_at = EXCLUDED.created_at - |] - -selectByUsers :: Statement (Vector.Vector UUID) (Vector.Vector (UUID, Text, Text, Maybe Text, UTCTime)) -selectByUsers = - [vectorStatement| - SELECT user_id :: uuid, conn_id :: text, resource :: text, client_id :: text?, created_at :: timestamptz - FROM presence - WHERE user_id = ANY ($1 :: uuid[]) - |] - --- | Compare-and-delete, in one round trip: only delete each stored presence --- if it is not newer than the given one (a newer re-registration with the --- same conn id must not be deleted by a stale disconnect). -deleteMany :: Statement (Vector.Vector UUID, Vector.Vector Text, Vector.Vector UTCTime) () -deleteMany = - [resultlessStatement| - DELETE FROM presence p - USING unnest($1 :: uuid[], $2 :: text[], $3 :: timestamptz[]) AS d (user_id, conn_id, created_at) - WHERE p.user_id = d.user_id - AND p.conn_id = d.conn_id - AND p.created_at <= d.created_at - |] - -deleteStale :: Statement () () -deleteStale = - [resultlessStatement| - DELETE FROM presence - WHERE created_at < now() - interval '7 days' - |] +data PresenceData = PresenceData !URI !(Maybe ClientId) !Milliseconds + deriving (Eq) + +instance ToJSON PresenceData where + toJSON (PresenceData r c t) = + object + [ "r" .= r, + "c" .= c, + "t" .= t + ] + +instance FromJSON PresenceData where + parseJSON = withObject "PresenceData" $ \o -> + PresenceData + <$> o + .: "r" + <*> o + .:? "c" + <*> o + .:? "t" + .!= 0 + +toKey :: UserId -> ByteString +toKey u = Lazy.toStrict $ runBuilder (byteString "user:" <> builder u) + +toField :: ConnId -> ByteString +toField (ConnId con) = con + +fromField :: ByteString -> ConnId +fromField = ConnId . StrictChars.takeWhile (/= '@') + +readPresence :: UserId -> (ByteString, ByteString) -> Maybe Presence +readPresence u (f, b) = do + PresenceData uri clt tme <- + if "http" `Strict.isPrefixOf` b + then PresenceData <$> fromByteString b <*> pure Nothing <*> pure 0 + else decodeStrict' b + pure (Presence u (fromField f) uri clt tme (Lazy.fromStrict f)) diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index 77149b6efef..a6cdf759062 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -122,7 +122,7 @@ instance MonadPushAll Gundeck where mpaNotificationTTL = view (options . settings . notificationTTL) mpaCellsEventQueue = view (options . settings . cellsEventQueue) mpaMkNotificationId = mkNotificationId - mpaListAllPresences = Presence.listAll + mpaListAllPresences = runWithDefaultRedis . Presence.listAll mpaBulkPush = Web.bulkPush mpaStreamAdd = Data.add mpaPushNative = pushNative diff --git a/services/gundeck/src/Gundeck/Push/Websocket.hs b/services/gundeck/src/Gundeck/Push/Websocket.hs index 721dadd8eaf..562bcb10730 100644 --- a/services/gundeck/src/Gundeck/Push/Websocket.hs +++ b/services/gundeck/src/Gundeck/Push/Websocket.hs @@ -64,7 +64,7 @@ class (Monad m, MonadThrow m, Log.MonadLogger m) => MonadBulkPush m where instance MonadBulkPush Gundeck where mbpBulkSend = bulkSend - mbpDeleteAllPresences = Presence.deleteAll + mbpDeleteAllPresences = runWithAdditionalRedis . Presence.deleteAll mbpPosixTime = posixTime mbpMapConcurrently = mapConcurrently mbpMonitorBadCannons = monitorBadCannons @@ -315,7 +315,7 @@ push :: push notif (toList -> tgts) originUser originConn conns = do pp <- handleAny noPresences listPresences (ok, gone) <- foldM onResult ([], []) =<< send notif pp - Presence.deleteAll gone + runWithAdditionalRedis $ Presence.deleteAll gone pure ok where listPresences = @@ -324,7 +324,7 @@ push notif (toList -> tgts) originUser originConn conns = do . concat . filterByClient . zip tgts - <$> Presence.listAll (view targetUser <$> tgts) + <$> runWithDefaultRedis (Presence.listAll (view targetUser <$> tgts)) noPresences exn = do Log.err $ Log.field "error" (displayException exn) diff --git a/services/gundeck/src/Gundeck/Redis.hs b/services/gundeck/src/Gundeck/Redis.hs new file mode 100644 index 00000000000..e9bf1affafe --- /dev/null +++ b/services/gundeck/src/Gundeck/Redis.hs @@ -0,0 +1,127 @@ +{-# LANGUAGE NumDecimals #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Gundeck.Redis + ( RobustConnection, + connectRobust, + runRobust, + PingException, + ) +where + +import Control.Concurrent.Async (Async, async) +import Control.Monad.Catch qualified as Catch +import Control.Retry +import Database.Redis +import Database.Redis.Connection (ClusterDownError) +import Imports +import System.Logger qualified as Log +import System.Logger.Class (MonadLogger) +import System.Logger.Class qualified as LogClass +import System.Logger.Extended +import UnliftIO.Exception + +-- | Connection to Redis which allows reconnecting. +type RobustConnection = MVar Connection + +-- | Connection to Redis which can be reestablished on connection errors. +-- +-- Reconnecting even when Redis IPs change as long as the DNS name remains +-- constant. The server type (cluster or not) and the connection information of +-- the initial connection are used when reconnecting. +-- +-- Throws 'ConnectError', 'ConnectTimeout', 'ConnectionLostException', +-- 'PingException', or 'IOException' if retry policy is finite. +connectRobust :: + Logger -> + -- | e. g., @exponentialBackoff 50000@ + RetryPolicy -> + -- | action returning a fresh initial 'Connection', e. g., @(checkedConnect connInfo)@ or @(checkedConnectCluster connInfo)@ + IO Connection -> + IO (Async (), RobustConnection) +connectRobust l retryStrategy connectLowLevel = do + robustConnection <- newEmptyMVar @IO @Connection + thread <- + async $ safeForever l $ do + Log.info l $ Log.msg (Log.val "connecting to Redis") + conn <- retry connectLowLevel + Log.info l $ Log.msg (Log.val "successfully connected to Redis") + putMVar robustConnection conn + catch + ( forever $ do + _ <- runRedis conn ping + threadDelay 1e6 + ) + $ \(_ :: SomeException) -> void $ takeMVar robustConnection + pure (thread, robustConnection) + where + retry = + recovering -- retry connecting, e. g., with exponential back-off + retryStrategy + [ const $ Catch.Handler (\(e :: ClusterDownError) -> logEx (Log.err l) e "Redis cluster down" >> pure True), + const $ Catch.Handler (\(e :: ConnectError) -> logEx (Log.err l) e "Redis not in cluster mode" >> pure True), + const $ Catch.Handler (\(e :: ConnectTimeout) -> logEx (Log.err l) e "timeout when connecting to Redis" >> pure True), + const $ Catch.Handler (\(e :: ConnectionLostException) -> logEx (Log.err l) e "Redis connection lost during request" >> pure True), + const $ Catch.Handler (\(e :: PingException) -> logEx (Log.err l) e "pinging Redis failed" >> pure True), + const $ Catch.Handler (\(e :: IOException) -> logEx (Log.err l) e "network error when connecting to Redis" >> pure True) + ] + . const -- ignore RetryStatus + logEx :: (Exception e) => ((Msg -> Msg) -> IO ()) -> e -> ByteString -> IO () + logEx lLevel e description = lLevel $ Log.msg (Log.val description) . Log.field "error" (displayException e) + +-- | Run a 'Redis' action through a 'RobustConnection'. +-- +-- Blocks on connection errors as long as the connection is not reestablished. +-- Without externally enforcing timeouts, this may lead to leaking threads. +runRobust :: (MonadUnliftIO m, MonadLogger m, Catch.MonadMask m) => RobustConnection -> Redis a -> m a +runRobust mvar action = retry $ do + robustConnection <- readMVar mvar + liftIO $ runRedis robustConnection action + where + retryStrategy = capDelay 1000000 (exponentialBackoff 50000) + retry = + recovering -- retry connecting, e. g., with exponential back-off + retryStrategy + [ logAndHandle $ Catch.Handler (\(_ :: ConnectionLostException) -> pure True), + logAndHandle $ Catch.Handler (\(_ :: IOException) -> pure True) + ] + . const -- ignore RetryStatus + logAndHandle (Handler handler) _ = + Handler $ \e -> do + LogClass.err $ Log.msg (Log.val "Redis connection failed") . Log.field "error" (displayException e) + handler e + +data PingException = PingException Reply deriving (Show) + +instance Exception PingException + +safeForever :: + forall m. + (MonadUnliftIO m) => + Logger -> + m () -> + m () +safeForever l action = + forever $ + action `catchAny` \e -> do + Log.err l $ Log.msg (Log.val "Uncaught exception while connecting to redis") . Log.field "error" (displayException e) + threadDelay 1e6 -- pause to keep worst-case noise in logs manageable diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index 590fe96e1ce..89e4c9f8ef2 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -41,7 +41,6 @@ import Cassandra (runClient, shutdown) import Cassandra.Schema (versionCheck) import Control.Error (ExceptT (ExceptT)) import Control.Exception (finally) -import Control.Exception.Safe (catchAny) import Control.Lens ((.~), (^.)) import Control.Monad.Extra import Data.Map qualified as Map @@ -49,18 +48,17 @@ import Data.Metrics.AWS (gaugeTokenRemaing) import Data.Metrics.Servant qualified as Metrics import Data.Proxy (Proxy (Proxy)) import Data.Text (unpack) +import Database.Redis qualified as Redis import Gundeck.API.Internal as Internal (InternalAPI, servantSitemap) import Gundeck.API.Public as Public (servantSitemap) import Gundeck.Aws qualified as Aws import Gundeck.Env import Gundeck.Env qualified as Env import Gundeck.Monad -import Gundeck.Options -import Gundeck.Presence.Data qualified as PresenceData +import Gundeck.Options hiding (host, port) import Gundeck.React import Gundeck.Schema.Run (lastSchemaVersion) import Gundeck.ThreadBudget -import Hasql.Pool.Extended (Pool (rawPool)) import Imports import Network.AMQP import Network.AMQP.Types @@ -83,13 +81,11 @@ import Wire.API.Routes.Public.Gundeck (GundeckAPI) import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai import Wire.OpenTelemetry -import Wire.PostgresMigrations qualified as PostgresMigrations run :: Opts -> IO () run opts = withTracer \tracer -> do - env <- createEnv opts + (rThreads, env) <- createEnv opts let logger = env ^. applog - PostgresMigrations.runAllMigrations (env ^. hasqlPool).rawPool logger runDirect env setUpRabbitMqExchangesAndQueues @@ -97,10 +93,10 @@ run opts = withTracer \tracer -> do versionCheck lastSchemaVersion let s = newSettings $ defaultServer (unpack . host $ opts ^. gundeck) (port $ opts ^. gundeck) logger let throttleMillis = fromMaybe defSqsThrottleMillis $ opts ^. (settings . sqsThrottleMillis) + lst <- Async.async $ Aws.execute (env ^. awsEnv) (Aws.listen throttleMillis (runDirect env . onEvent)) wtbs <- forM (env ^. threadBudgetState) $ \tbs -> Async.async $ runDirect env $ watchThreadBudgetState tbs 10 wCollectAuth <- Async.async (collectAuthMetrics (Aws._awsEnv (Env._awsEnv env))) - pcleanup <- Async.async $ runDirect env $ cleanupPresenceLoop logger app <- middleware env <*> pure (mkApp env) inSpan tracer "gundeck" defaultSpanArguments {kind = Otel.Server} (runSettingsWithShutdown s app Nothing) `finally` do @@ -108,8 +104,10 @@ run opts = withTracer \tracer -> do shutdown (env ^. cstate) Async.cancel lst Async.cancel wCollectAuth - Async.cancel pcleanup forM_ wtbs Async.cancel + forM_ rThreads Async.cancel + Redis.disconnect =<< takeMVar (env ^. rstate) + whenJust (env ^. rstateAdditionalWrite) $ (=<<) Redis.disconnect . takeMVar Log.close (env ^. applog) where setUpRabbitMqExchangesAndQueues :: Gundeck () @@ -180,22 +178,3 @@ collectAuthMetrics env = do mbRemaining <- readAuthExpiration env gaugeTokenRemaing mbRemaining threadDelay 1_000_000 - --- | Hourly janitor replacing the redis key TTL: deletes presence rows older --- than a week (leak guard for abnormally dead pods). Never let a transient DB --- error kill the thread — log and retry next hour. Async exceptions (e.g. --- 'AsyncCancelled' from 'Async.cancel' during shutdown) propagate because --- 'Control.Exception.Safe.catchAny' rethrows asynchronously-delivered --- exceptions and only handles synchronous ones. -cleanupPresenceLoop :: Log.Logger -> Gundeck () -cleanupPresenceLoop logger = - forever $ - (PresenceData.cleanup >> threadDelay cleanupInterval) - `catchAny` \e -> do - liftIO . Log.err logger $ - Log.msg (Log.val "presence cleanup failed") - . Log.field "error" (displayException e) - threadDelay cleanupInterval - -cleanupInterval :: Int -cleanupInterval = 3_600_000_000 -- one hour, in microseconds diff --git a/services/gundeck/src/Gundeck/Util/Redis.hs b/services/gundeck/src/Gundeck/Util/Redis.hs new file mode 100644 index 00000000000..d125d04baca --- /dev/null +++ b/services/gundeck/src/Gundeck/Util/Redis.hs @@ -0,0 +1,61 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Gundeck.Util.Redis where + +import Control.Monad.Catch +import Control.Retry +import Data.ByteString qualified as BS +import Database.Redis +import Imports +import System.Logger.Class (MonadLogger) +import System.Logger.Class qualified as Log +import System.Logger.Message + +retry :: (MonadIO m, MonadMask m, MonadLogger m) => RetryPolicyM m -> m a -> m a +retry x = recovering x handlers . const + +x3 :: RetryPolicy +x3 = limitRetries 3 <> exponentialBackoff 100000 + +handlers :: (MonadLogger m) => [a -> Handler m Bool] +handlers = + [ const . Handler $ \case + RedisSimpleError (Error err) -> pure $ "READONLY" `BS.isPrefixOf` err + RedisTxError err -> pure $ "READONLY" `isPrefixOf` err + err -> do + Log.warn $ + Log.msg (Log.val "Redis error; not retrying.") + ~~ "redis.errMsg" .= show err + pure False + ] + +-- Error ------------------------------------------------------------------- + +data RedisError + = RedisSimpleError Reply + | RedisTxAborted + | RedisTxError String + deriving (Show) + +instance Exception RedisError + +fromTxResult :: (MonadThrow m) => TxResult a -> m a +fromTxResult = \case + TxSuccess a -> pure a + TxAborted -> throwM RedisTxAborted + TxError e -> throwM $ RedisTxError e diff --git a/services/gundeck/test/integration/API.hs b/services/gundeck/test/integration/API.hs index 6346d07a438..27de5b8602d 100644 --- a/services/gundeck/test/integration/API.hs +++ b/services/gundeck/test/integration/API.hs @@ -28,6 +28,7 @@ import Bilge hiding (head) import Bilge.Assert import Control.Arrow ((&&&)) import Control.Concurrent.Async (Async, async, concurrently_, wait) +import Control.Concurrent.Async qualified as Async import Control.Lens (view, (%~), (.~), (?~), (^.), (^?), _2) import Control.Retry (constantDelay, limitRetries, recoverAll, retrying) import Data.Aeson @@ -46,6 +47,8 @@ import Data.Set qualified as Set import Data.Text.Encoding qualified as T import Data.UUID qualified as UUID import Data.UUID.V4 +import Gundeck.Options +import Gundeck.Options qualified as O import Imports import Network.HTTP.Client qualified as Http import Network.URI (parseURI) @@ -56,6 +59,7 @@ import System.Timeout (timeout) import Test.Tasty import Test.Tasty.HUnit import TestSetup +import Util (runRedisProxy, withEnvOverrides, withSettingsOverrides) import Wire.API.Event.Gundeck import Wire.API.Internal.Notification import Wire.API.Presence @@ -72,7 +76,8 @@ tests s = test s "Remove stale presence" removeStalePresence, test s "Single user push" singleUserPush, test s "Single user push with large message" singleUserPushLargeMessage, - test s "Send a push, ensure origin does not receive it" sendSingleUserNoPiggyback + test s "Send a push, ensure origin does not receive it" sendSingleUserNoPiggyback, + test s "Store notifications even when redis is down" storeNotificationsEvenWhenRedisIsDown ], testGroup "Notifications" @@ -103,6 +108,10 @@ tests s = test s "control pings with payload produce pongs with the same payload" testControlPingPongWithData, test s "data non-pings are ignored" testNoPingNoPong ], + testGroup + "Redis migration" + [ test s "redis migration should work" testRedisMigration + ], -- TODO: The following tests require (at the moment), the usage real AWS -- services so they are kept in a separate group to simplify testing testGroup @@ -126,8 +135,8 @@ replacePresence = do con <- randomConnId let localhost8080 = URI . fromJust $ parseURI "http://localhost:8080" let localhost8081 = URI . fromJust $ parseURI "http://localhost:8081" - let pres1 = Presence uid (ConnId "dummy_dev") localhost8080 Nothing 0 - let pres2 = Presence uid (ConnId "dummy_dev") localhost8081 Nothing 0 + let pres1 = Presence uid (ConnId "dummy_dev") localhost8080 Nothing 0 "" + let pres2 = Presence uid (ConnId "dummy_dev") localhost8081 Nothing 0 "" void $ connectUser ca uid con setPresence gu pres1 !!! const 201 === statusCode sendPush (push uid [uid]) @@ -260,6 +269,28 @@ sendMultipleUsers = do pevent = KeyMap.fromList ["foo" .= (42 :: Int)] push u us = newPush (Just u) (toRecipients us) pload & pushOriginConnection ?~ ConnId "dev" +storeNotificationsEvenWhenRedisIsDown :: TestM () +storeNotificationsEvenWhenRedisIsDown = do + ally <- randomId + origRedisEndpoint <- view $ tsOpts . redis + let proxyPort = 10112 + redisProxyServer <- liftIO . async $ runRedisProxy (origRedisEndpoint ^. O.host) (origRedisEndpoint ^. O.port) proxyPort + withSettingsOverrides + ( \gundeckSettings -> + gundeckSettings + & redis . Gundeck.Options.host .~ "localhost" + & redis . Gundeck.Options.port .~ proxyPort + ) + $ do + let pload = textPayload "hello" + push = buildPush ally [(ally, RecipientClientsAll)] pload + gu <- view tsGundeck + liftIO $ Async.cancel redisProxyServer + post (runGundeckR gu . path "i/push/v2" . json [push]) !!! const 200 === statusCode + + ns <- listNotifications ally Nothing + liftIO $ assertEqual ("Expected 1 notification, got: " <> show ns) 1 (length ns) + ----------------------------------------------------------------------------- -- Notifications @@ -698,6 +729,36 @@ testLongPushToken = do tkn4 <- randomToken clt gcmToken {tSize = 5000} registerPushTokenRequest uid tkn4 !!! const 413 === statusCode +-- * Redis Migration + +testRedisMigration :: TestM () +testRedisMigration = do + uid <- randomUser + con <- randomConnId + cannonURI <- Wire.API.Presence.parse "http://cannon.example" + let presence = Presence uid con cannonURI Nothing 1 "" + redis2 <- view tsRedis2 + + withSettingsOverrides (redisAdditionalWrite ?~ redis2) $ do + g <- view tsGundeck + setPresence g presence + !!! const 201 + === statusCode + retrievedPresence <- + map resource . decodePresence <$> (getPresence g (toByteString' uid) lookupEnv "REDIS_ADDITIONAL_WRITE_USERNAME" + password <- ("REDIS_PASSWORD",) <$$> lookupEnv "REDIS_ADDITIONAL_WRITE_PASSWORD" + pure $ catMaybes [username, password] + + withEnvOverrides redis2CredsAsRedis1Creds $ withSettingsOverrides (redis .~ redis2) $ do + g <- view tsGundeck + retrievedPresence <- + map resource . decodePresence <$> (getPresence g (toByteString' uid) UserId -> Int -> TestM () diff --git a/services/gundeck/test/integration/Main.hs b/services/gundeck/test/integration/Main.hs index 05a385e40b4..767f28a4ae4 100644 --- a/services/gundeck/test/integration/Main.hs +++ b/services/gundeck/test/integration/Main.hs @@ -30,7 +30,7 @@ import Data.Proxy import Data.Tagged import Data.Text.Encoding (encodeUtf8) import Data.Yaml (decodeFileEither) -import Gundeck.Options +import Gundeck.Options hiding (host, port) import Imports hiding (local) import Metrics qualified import Network.HTTP.Client (responseTimeoutMicro) @@ -52,7 +52,8 @@ data IntegrationConfig = IntegrationConfig { gundeck :: Endpoint, cannon :: Endpoint, cannon2 :: Endpoint, - brig :: Endpoint + brig :: Endpoint, + redis2 :: RedisEndpoint } deriving (Show, Generic) @@ -113,6 +114,6 @@ main = withOpenSSL $ runTests go b = BrigR $ mkRequest iConf.brig lg <- Logger.new Logger.defSettings db <- defInitCassandra (gConf ^. cassandra) lg - pure $ TestSetup m g c c2 b db lg - mkRequest (Endpoint h p) = Bilge.host (encodeUtf8 h) . Bilge.port p + pure $ TestSetup m g c c2 b db lg gConf (redis2 iConf) releaseOpts _ = pure () + mkRequest (Endpoint h p) = Bilge.host (encodeUtf8 h) . Bilge.port p diff --git a/services/gundeck/test/integration/TestSetup.hs b/services/gundeck/test/integration/TestSetup.hs index 70e8cd77dca..ea49d1b3222 100644 --- a/services/gundeck/test/integration/TestSetup.hs +++ b/services/gundeck/test/integration/TestSetup.hs @@ -28,6 +28,8 @@ module TestSetup tsBrig, tsCass, tsLogger, + tsOpts, + tsRedis2, TestM (..), TestSetup (..), BrigR (..), @@ -40,6 +42,8 @@ import Bilge (HttpT (..), Manager, MonadHttp, Request, runHttpT) import Cassandra qualified as Cql import Control.Lens (makeLenses, (^.)) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow) +import Gundeck.Options (RedisEndpoint) +import Gundeck.Options qualified as Gundeck import Imports import System.Logger qualified as Log import Test.Tasty (TestName, TestTree) @@ -75,7 +79,9 @@ data TestSetup = TestSetup _tsCannon2 :: CannonR, _tsBrig :: BrigR, _tsCass :: Cql.ClientState, - _tsLogger :: Log.Logger + _tsLogger :: Log.Logger, + _tsOpts :: Gundeck.Opts, + _tsRedis2 :: RedisEndpoint } makeLenses ''TestSetup diff --git a/services/gundeck/test/integration/Util.hs b/services/gundeck/test/integration/Util.hs new file mode 100644 index 00000000000..d6790424b2f --- /dev/null +++ b/services/gundeck/test/integration/Util.hs @@ -0,0 +1,119 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Util where + +import Bilge qualified +import Control.Concurrent (forkFinally) +import Control.Concurrent.Async (race_) +import Control.Exception qualified as E +import Control.Lens +import Control.Monad.Catch +import Control.Monad.Codensity +import Data.ByteString qualified as S +import Data.Text qualified as Text +import Gundeck.Env (createEnv) +import Gundeck.Options +import Gundeck.Run (mkApp) +import Imports +import Network.Socket hiding (openSocket) +import Network.Socket.ByteString (recv, sendAll) +import Network.Wai.Utilities.MockServer (withMockServer) +import TestSetup + +withSettingsOverrides :: (Opts -> Opts) -> TestM a -> TestM a +withSettingsOverrides f action = do + ts <- ask + let opts = f (view tsOpts ts) + (_rThreads, env) <- liftIO $ createEnv opts + liftIO . lowerCodensity $ do + let app = mkApp env + p <- withMockServer app + liftIO $ Bilge.runHttpT (ts ^. tsManager) $ runReaderT (runTestM action) $ ts & tsGundeck .~ GundeckR (mkRequest p) + where + mkRequest p = Bilge.host "127.0.0.1" . Bilge.port p + +withEnvOverrides :: forall m a. (MonadIO m, MonadMask m) => [(String, String)] -> m a -> m a +withEnvOverrides envOverrides action = do + bracket (setEnvVars envOverrides) (resetEnvVars) $ const action + where + setEnvVars :: [(String, String)] -> m [(String, Maybe String)] + setEnvVars newVars = liftIO $ do + oldVars <- mapM (\(k, _) -> (k,) <$> lookupEnv k) newVars + mapM_ (uncurry setEnv) newVars + pure oldVars + + resetEnvVars :: [(String, Maybe String)] -> m () + resetEnvVars = + mapM_ (\(k, mV) -> maybe (unsetEnv k) (setEnv k) mV) + +runRedisProxy :: Text -> Word16 -> Word16 -> IO () +runRedisProxy redisHost redisPort proxyPort = do + (servAddr : _) <- getAddrInfo Nothing (Just $ Text.unpack redisHost) (Just $ show redisPort) + runTCPServer Nothing (show proxyPort) $ \client -> do + server <- getServerSocket servAddr + client <~~> server + where + getServerSocket servAddr = do + server <- socket (addrFamily servAddr) Stream defaultProtocol + connect server (addrAddress servAddr) >> pure server + p1 <~~> p2 = finally (race_ (p1 `mapData` p2) (p2 `mapData` p1)) (close p1 >> close p2) + mapData f t = do + content <- recv f 4096 + unless (S.null content) $ sendAll t content >> mapData f t + +-- Forked from network-run, added logic to cleanup clients when server is closed + +-- | Running a TCP server with an accepted socket and its peer name. +runTCPServer :: Maybe HostName -> ServiceName -> (Socket -> IO a) -> IO b +runTCPServer mhost port' server = withSocketsDo $ do + addr <- resolve Stream mhost port' True + clientThreads <- newTVarIO [] + E.bracket (open addr) (cleanupClients clientThreads) (loop clientThreads) + where + open addr = E.bracketOnError (openServerSocket addr) close $ \sock -> do + listen sock 1024 + pure sock + loop clientThreads sock = forever $ do + E.bracketOnError (accept sock) (close . fst) $ + \(conn, _peer) -> do + thread <- forkFinally (server conn) (const $ gracefulClose conn 5000) + atomically $ modifyTVar clientThreads (thread :) + cleanupClients :: TVar [ThreadId] -> Socket -> IO () + cleanupClients clientThreads sock = do + close sock + mapM_ killThread =<< readTVarIO clientThreads + +resolve :: SocketType -> Maybe HostName -> ServiceName -> Bool -> IO AddrInfo +resolve socketType mhost port' passive = + head <$> getAddrInfo (Just hints) mhost (Just port') + where + hints = + defaultHints + { addrSocketType = socketType, + addrFlags = [AI_PASSIVE | passive] + } + +openServerSocket :: AddrInfo -> IO Socket +openServerSocket addr = E.bracketOnError (openSocket addr) close $ \sock -> do + setSocketOption sock ReuseAddr 1 + withFdSocket sock $ setCloseOnExecIfNeeded + bind sock $ addrAddress addr + pure sock + +openSocket :: AddrInfo -> IO Socket +openSocket addr = socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr) diff --git a/services/gundeck/test/unit/MockGundeck.hs b/services/gundeck/test/unit/MockGundeck.hs index 647e30f376d..6e4f27df53d 100644 --- a/services/gundeck/test/unit/MockGundeck.hs +++ b/services/gundeck/test/unit/MockGundeck.hs @@ -770,6 +770,7 @@ fakePresence userId clientId_ = Presence {..} connId = fakeConnId clientId_ resource = URI . fromJust $ URI.parseURI "http://127.0.0.1:8080" createdAt = 0 + __field = mempty -- | See also: 'fakePresence'. fakeConnId :: ClientId -> ConnId diff --git a/services/integration.yaml b/services/integration.yaml index acb0e595b23..2da7e194e1f 100644 --- a/services/integration.yaml +++ b/services/integration.yaml @@ -128,6 +128,12 @@ backendTwo: originDomain: b.example.com +redis2: + host: 127.0.0.1 + port: 6379 + connectionMode: master + enableTls: false + insecureSkipVerifyTls: false dynamicBackends: dynamic-backend-1: From bdf8e663bb9f8b3e306f7382d5c8bcc071bf9811 Mon Sep 17 00:00:00 2001 From: Zebot Date: Fri, 18 Sep 2026 15:15:49 +0000 Subject: [PATCH 29/29] Add changelog for Release 2026-09-18 --- CHANGELOG.md | 71 +++++++++++++++++++ changelog.d/0-release-notes/WPB-28237 | 1 - .../WPB-28565-finalize-api-version-v18 | 1 - changelog.d/2-features/WPB-26650 | 1 - changelog.d/2-features/WPB-28246 | 1 - ...hange-default-total-limit-bytes-value-to-1 | 1 - changelog.d/2-features/WPB-28685 | 1 - changelog.d/2-features/user-pg-migration | 1 - changelog.d/3-bug-fixes/WPB-18929 | 3 - changelog.d/3-bug-fixes/WPB-23427 | 1 - changelog.d/3-bug-fixes/WPB-27964 | 1 - ...pending-invitations-for-deleted-SCIM-users | 2 - ...postgresql-connection-string-parse-failure | 3 - changelog.d/4-docs/update-developer-docs | 1 - ...ses-in-which-given-files-have-been-touched | 1 - changelog.d/5-internal/WPB-28483 | 1 - changelog.d/5-internal/WPB-28709 | 1 - changelog.d/6-federation/WPB-28421 | 1 - changelog.d/6-federation/WPB-28422 | 1 - 19 files changed, 71 insertions(+), 23 deletions(-) delete mode 100644 changelog.d/0-release-notes/WPB-28237 delete mode 100644 changelog.d/1-api-changes/WPB-28565-finalize-api-version-v18 delete mode 100644 changelog.d/2-features/WPB-26650 delete mode 100644 changelog.d/2-features/WPB-28246 delete mode 100644 changelog.d/2-features/WPB-28484-change-default-total-limit-bytes-value-to-1 delete mode 100644 changelog.d/2-features/WPB-28685 delete mode 100644 changelog.d/2-features/user-pg-migration delete mode 100644 changelog.d/3-bug-fixes/WPB-18929 delete mode 100644 changelog.d/3-bug-fixes/WPB-23427 delete mode 100644 changelog.d/3-bug-fixes/WPB-27964 delete mode 100644 changelog.d/3-bug-fixes/pending-invitations-for-deleted-SCIM-users delete mode 100644 changelog.d/3-bug-fixes/postgresql-connection-string-parse-failure delete mode 100644 changelog.d/4-docs/update-developer-docs delete mode 100644 changelog.d/5-internal/WPB-27169-script-listing-all-commits-and-releases-in-which-given-files-have-been-touched delete mode 100644 changelog.d/5-internal/WPB-28483 delete mode 100644 changelog.d/5-internal/WPB-28709 delete mode 100644 changelog.d/6-federation/WPB-28421 delete mode 100644 changelog.d/6-federation/WPB-28422 diff --git a/CHANGELOG.md b/CHANGELOG.md index 093e56294e3..347b162dabd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,74 @@ +# [2026-09-18] (Chart Release 5.36.0) + +## Release notes + + +* `preventAdminlessGroups` is unlocked by default (#5496) + + +## API changes + + +* Finalize API version v18 and create development version v19. (#5531, #5536) + + +## Features + + +* Add federation support for `preventAdminlessGroups` system notifications, with capability-aware handling for older remote backends. (#5525, #5540) + +* Add `ssoIdpChangeDetectionEnabled` to `GET /system/settings`. (#5527) + +* Change the default value of totalLimitBytes from one terrabyte to unlimited (#5519) + +* Meeting events (`meeting.create`, `meeting.update`, `meeting.delete`, `meeting.member-add`) are now delivered to all push channels, including native push (APNs/FCM), so offline or backgrounded clients learn about meeting changes via native push instead of waiting for the next foreground sync. (#5537) + +* Support migrating user data to postgresql from cassandra (#5324) + + +## Bug fixes and other updates + + +* Revoking a pending SCIM invitation now removes the associated Brig account and + Spar SCIM metadata synchronously, allowing the same SCIM user to be invited + again. (#5510) + +* MLS message validation has been hardened. (#5517) + +* Account pages now use the correct backend URL and CSP header on each multi-ingress domain. This applies to both ingress charts: `nginx-ingress-services` no longer includes the account-pages host in its generic CSP snippet, and `wire-ingress` (envoy-gateway) no longer injects a Content-Security-Policy response header on the account-pages route. The same fix is applied to the webapp route in `wire-ingress`, which had the same problem (`nginx-ingress-services` already skipped it). (#5534) + +* Deleted SCIM users could still have pending team invitations. These are now + deleted (invalidated) with the SCIM user. (#5492) + +* Postgresql connection strings with mismatched host/port counts in service + configurations now lead to immediate failure with a clear error message instead + of silently producing an erroneous connection. (#5494) + + +## Documentation + + +* Remove cabal update from build steps and fix some typos in developer docs (#5523) + + +## Internal changes + + +* Script listing all commits and releases in which given files have been touched. (#5509) + +* MLS commit-bundles are processed with less sequential I/O: proposal references are resolved from a single store read, client and client-store updates fan out concurrently, and welcome pushes no longer block the response. (#5528) + +* Add diagnostic logging for failed MLS commit-bundle operations, including typed failures and exceptions during commit-lock handling. (#5544) + + +## Federation changes + + +* Add an opt-in policy for dropping queued federation notifications when the target backend supports no compatible API version. (#5501) + +* Remove stale local memberships when a remote conversation is definitively reported as not found. (#5504) + + # [2026-08-27] (Chart Release 5.35.0) ## Release notes diff --git a/changelog.d/0-release-notes/WPB-28237 b/changelog.d/0-release-notes/WPB-28237 deleted file mode 100644 index c30460e5cdd..00000000000 --- a/changelog.d/0-release-notes/WPB-28237 +++ /dev/null @@ -1 +0,0 @@ -`preventAdminlessGroups` is unlocked by default diff --git a/changelog.d/1-api-changes/WPB-28565-finalize-api-version-v18 b/changelog.d/1-api-changes/WPB-28565-finalize-api-version-v18 deleted file mode 100644 index ab10369d5f0..00000000000 --- a/changelog.d/1-api-changes/WPB-28565-finalize-api-version-v18 +++ /dev/null @@ -1 +0,0 @@ -Finalize API version v18 and create development version v19. (#5531, #5536) diff --git a/changelog.d/2-features/WPB-26650 b/changelog.d/2-features/WPB-26650 deleted file mode 100644 index 3c13d53b4f0..00000000000 --- a/changelog.d/2-features/WPB-26650 +++ /dev/null @@ -1 +0,0 @@ -Add federation support for `preventAdminlessGroups` system notifications, with capability-aware handling for older remote backends. (#5525, #5540) diff --git a/changelog.d/2-features/WPB-28246 b/changelog.d/2-features/WPB-28246 deleted file mode 100644 index fd0b80b12d4..00000000000 --- a/changelog.d/2-features/WPB-28246 +++ /dev/null @@ -1 +0,0 @@ -Add `ssoIdpChangeDetectionEnabled` to `GET /system/settings`. diff --git a/changelog.d/2-features/WPB-28484-change-default-total-limit-bytes-value-to-1 b/changelog.d/2-features/WPB-28484-change-default-total-limit-bytes-value-to-1 deleted file mode 100644 index d5e72d2730b..00000000000 --- a/changelog.d/2-features/WPB-28484-change-default-total-limit-bytes-value-to-1 +++ /dev/null @@ -1 +0,0 @@ -Change the default value of totalLimitBytes from one terrabyte to unlimited \ No newline at end of file diff --git a/changelog.d/2-features/WPB-28685 b/changelog.d/2-features/WPB-28685 deleted file mode 100644 index fc6a13cd4bb..00000000000 --- a/changelog.d/2-features/WPB-28685 +++ /dev/null @@ -1 +0,0 @@ -Meeting events (`meeting.create`, `meeting.update`, `meeting.delete`, `meeting.member-add`) are now delivered to all push channels, including native push (APNs/FCM), so offline or backgrounded clients learn about meeting changes via native push instead of waiting for the next foreground sync. diff --git a/changelog.d/2-features/user-pg-migration b/changelog.d/2-features/user-pg-migration deleted file mode 100644 index 2abecf732fc..00000000000 --- a/changelog.d/2-features/user-pg-migration +++ /dev/null @@ -1 +0,0 @@ -Support migrating user data to postgresql from cassandra \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/WPB-18929 b/changelog.d/3-bug-fixes/WPB-18929 deleted file mode 100644 index 29da72e51fd..00000000000 --- a/changelog.d/3-bug-fixes/WPB-18929 +++ /dev/null @@ -1,3 +0,0 @@ -Revoking a pending SCIM invitation now removes the associated Brig account and -Spar SCIM metadata synchronously, allowing the same SCIM user to be invited -again. diff --git a/changelog.d/3-bug-fixes/WPB-23427 b/changelog.d/3-bug-fixes/WPB-23427 deleted file mode 100644 index 2a20346bd27..00000000000 --- a/changelog.d/3-bug-fixes/WPB-23427 +++ /dev/null @@ -1 +0,0 @@ -MLS message validation has been hardened. diff --git a/changelog.d/3-bug-fixes/WPB-27964 b/changelog.d/3-bug-fixes/WPB-27964 deleted file mode 100644 index b2e78cf94e0..00000000000 --- a/changelog.d/3-bug-fixes/WPB-27964 +++ /dev/null @@ -1 +0,0 @@ -Account pages now use the correct backend URL and CSP header on each multi-ingress domain. This applies to both ingress charts: `nginx-ingress-services` no longer includes the account-pages host in its generic CSP snippet, and `wire-ingress` (envoy-gateway) no longer injects a Content-Security-Policy response header on the account-pages route. The same fix is applied to the webapp route in `wire-ingress`, which had the same problem (`nginx-ingress-services` already skipped it). diff --git a/changelog.d/3-bug-fixes/pending-invitations-for-deleted-SCIM-users b/changelog.d/3-bug-fixes/pending-invitations-for-deleted-SCIM-users deleted file mode 100644 index 1f472f2c805..00000000000 --- a/changelog.d/3-bug-fixes/pending-invitations-for-deleted-SCIM-users +++ /dev/null @@ -1,2 +0,0 @@ -Deleted SCIM users could still have pending team invitations. These are now -deleted (invalidated) with the SCIM user. diff --git a/changelog.d/3-bug-fixes/postgresql-connection-string-parse-failure b/changelog.d/3-bug-fixes/postgresql-connection-string-parse-failure deleted file mode 100644 index c4f4e02752f..00000000000 --- a/changelog.d/3-bug-fixes/postgresql-connection-string-parse-failure +++ /dev/null @@ -1,3 +0,0 @@ -Postgresql connection strings with mismatched host/port counts in service -configurations now lead to immediate failure with a clear error message instead -of silently producing an erroneous connection. diff --git a/changelog.d/4-docs/update-developer-docs b/changelog.d/4-docs/update-developer-docs deleted file mode 100644 index 9197d6b34bc..00000000000 --- a/changelog.d/4-docs/update-developer-docs +++ /dev/null @@ -1 +0,0 @@ -Remove cabal update from build steps and fix some typos in developer docs \ No newline at end of file diff --git a/changelog.d/5-internal/WPB-27169-script-listing-all-commits-and-releases-in-which-given-files-have-been-touched b/changelog.d/5-internal/WPB-27169-script-listing-all-commits-and-releases-in-which-given-files-have-been-touched deleted file mode 100644 index 5e69990bc55..00000000000 --- a/changelog.d/5-internal/WPB-27169-script-listing-all-commits-and-releases-in-which-given-files-have-been-touched +++ /dev/null @@ -1 +0,0 @@ -Script listing all commits and releases in which given files have been touched. diff --git a/changelog.d/5-internal/WPB-28483 b/changelog.d/5-internal/WPB-28483 deleted file mode 100644 index 99c53983960..00000000000 --- a/changelog.d/5-internal/WPB-28483 +++ /dev/null @@ -1 +0,0 @@ -MLS commit-bundles are processed with less sequential I/O: proposal references are resolved from a single store read, client and client-store updates fan out concurrently, and welcome pushes no longer block the response. diff --git a/changelog.d/5-internal/WPB-28709 b/changelog.d/5-internal/WPB-28709 deleted file mode 100644 index 5d6b3dc8eb7..00000000000 --- a/changelog.d/5-internal/WPB-28709 +++ /dev/null @@ -1 +0,0 @@ -Add diagnostic logging for failed MLS commit-bundle operations, including typed failures and exceptions during commit-lock handling. diff --git a/changelog.d/6-federation/WPB-28421 b/changelog.d/6-federation/WPB-28421 deleted file mode 100644 index 6425a4060cd..00000000000 --- a/changelog.d/6-federation/WPB-28421 +++ /dev/null @@ -1 +0,0 @@ -Add an opt-in policy for dropping queued federation notifications when the target backend supports no compatible API version. diff --git a/changelog.d/6-federation/WPB-28422 b/changelog.d/6-federation/WPB-28422 deleted file mode 100644 index 7ca489ce3bf..00000000000 --- a/changelog.d/6-federation/WPB-28422 +++ /dev/null @@ -1 +0,0 @@ -Remove stale local memberships when a remote conversation is definitively reported as not found.