mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-01 20:08:57 +00:00
core, web: correctly process name verification for channels, show names in directory
This commit is contained in:
@@ -70,6 +70,7 @@ $(JQ.deriveJSON defaultJSON ''PublicLink)
|
||||
data DirectoryEntry = DirectoryEntry
|
||||
{ entryType :: DirectoryEntryType,
|
||||
displayName :: Text,
|
||||
simplexName :: Maybe Text,
|
||||
groupLink :: PublicLink,
|
||||
shortDescr :: Maybe MarkdownList,
|
||||
welcomeMessage :: Maybe MarkdownList,
|
||||
@@ -97,7 +98,7 @@ recentRoundedTime roundTo now t
|
||||
in Just $ systemToUTCTime $ MkSystemTime secs 0
|
||||
|
||||
groupDirectoryEntry :: UTCTime -> GroupInfo -> Maybe GroupLink -> Maybe (DirectoryEntry, Maybe (FilePath, ImageFileData))
|
||||
groupDirectoryEntry now GroupInfo {groupProfile, chatTs, createdAt, groupSummary} gLink_ =
|
||||
groupDirectoryEntry now g@GroupInfo {groupProfile, chatTs, createdAt, groupSummary} gLink_ =
|
||||
let GroupProfile {displayName, shortDescr, description, image, memberAdmission, publicGroup} = groupProfile
|
||||
gt = (\PublicGroupProfile {groupType} -> groupType) <$> publicGroup
|
||||
entryType = DETGroup gt memberAdmission groupSummary
|
||||
@@ -112,6 +113,7 @@ groupDirectoryEntry now GroupInfo {groupProfile, chatTs, createdAt, groupSummary
|
||||
DirectoryEntry
|
||||
{ entryType,
|
||||
displayName,
|
||||
simplexName = shortNameInfoStr . SimplexNameInfo NTPublicGroup <$> verifiedGroupDomain g,
|
||||
groupLink,
|
||||
shortDescr = toFormattedText <$> shortDescr,
|
||||
welcomeMessage = toFormattedText <$> description',
|
||||
|
||||
@@ -64,11 +64,12 @@ import Simplex.Chat.Terminal.Main (simplexChatCLI')
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.Chat.View (serializeChatError, serializeChatResponse, simplexChatContact, viewContactName, viewGroupName)
|
||||
import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ACreatedConnLink (..), AgentErrorType (..), ConnectionLink (..), CreatedConnLink (..), SConnectionMode (..), sameConnReqContact, sameShortLinkContact)
|
||||
import Simplex.Chat.View (groupSimplexDomain, serializeChatError, serializeChatResponse, simplexChatContact, viewContactName, viewGroupName)
|
||||
import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ACreatedConnLink (..), AgentErrorType (..), ConnectionLink (..), CreatedConnLink (..), SConnectionMode (..), SimplexDomain, sameConnReqContact, sameShortLinkContact)
|
||||
import qualified Simplex.Messaging.Crypto.File as CF
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (ErrorType (..))
|
||||
import Simplex.Messaging.SimplexName (SimplexNameInfo (..), SimplexNameType (..), shortNameInfoStr)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (eitherToMaybe, raceAny_, safeDecodeUtf8, tshow, unlessM, (<$$>))
|
||||
@@ -361,7 +362,17 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
let msg = "Error: " <> err <> ", group: " <> tshow groupId <> " " <> localDisplayName <> ", " <> T.pack e
|
||||
notifyAdminUsers msg
|
||||
logError msg
|
||||
groupInfoText p@GroupProfile {description = d, publicGroup} = groupNameDescr p <> maybe "" ("\nWelcome message:\n" <>) d <> linkToJoin
|
||||
-- validates that resolving the claimed name leads to the link in the group profile,
|
||||
-- updating the group's verification status; leaves it unchanged on network errors
|
||||
verifyGroupDomain_ :: GroupInfo -> IO GroupInfo
|
||||
verifyGroupDomain_ g@GroupInfo {groupId}
|
||||
| isJust (groupSimplexDomain g) =
|
||||
sendChatCmd cc (APIVerifyGroupDomain groupId) >>= \case
|
||||
Right CRGroupDomainVerified {groupInfo = g'} -> pure g'
|
||||
Right r -> g <$ logError ("verifyGroupDomain_: unexpected response " <> tshow r)
|
||||
Left e -> g <$ logInfo ("verifyGroupDomain_: error " <> tshow e)
|
||||
| otherwise = pure g
|
||||
groupInfoText simplexName_ p@GroupProfile {description = d, publicGroup} = groupNameDescr p <> maybe "" ("\nSimpleX name: " <>) simplexName_ <> maybe "" ("\nWelcome message:\n" <>) d <> linkToJoin
|
||||
where
|
||||
linkToJoin = case publicGroup of
|
||||
Just pg@PublicGroupProfile {groupLink} ->
|
||||
@@ -800,13 +811,18 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
alwaysCaptcha || useMemberFilter image (passCaptcha a)
|
||||
|
||||
sendToApprove :: GroupInfo -> GroupReg -> GroupApprovalId -> IO ()
|
||||
sendToApprove GroupInfo {groupId, groupProfile = p@GroupProfile {displayName, image = image', publicGroup = pg_}, groupSummary} GroupReg {dbContactId, promoted} gaId = do
|
||||
sendToApprove g0 GroupReg {dbContactId, promoted} gaId = do
|
||||
-- verify the claimed name first, so admins see its actual state
|
||||
g@GroupInfo {groupId, groupProfile = p@GroupProfile {displayName, image = image', publicGroup = pg_}, groupSummary} <-
|
||||
if groupDomainVerified g0 /= Just True then verifyGroupDomain_ g0 else pure g0
|
||||
ct_ <- getContact' cc user dbContactId
|
||||
let gt = maybe "group" groupTypeStr' pg_
|
||||
-- admins see an unverified claim, marked; users never do
|
||||
nameStr_ = (\d -> simplexNameStr d <> (if groupDomainVerified g == Just True then "" else " (NOT verified - will not be shown)")) <$> groupSimplexDomain g
|
||||
membersStr = "_" <> membersCountStr p groupSummary <> "_\n"
|
||||
text =
|
||||
either (\_ -> "The " <> gt <> " ID " <> tshow groupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the " <> gt <> " ID " <> tshow groupId <> ": ") ct_
|
||||
<> ("\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:")
|
||||
<> ("\n" <> groupInfoText nameStr_ p <> "\n" <> membersStr <> "\nTo approve send:")
|
||||
msg = maybe (MCText text) (\image -> MCImage {text, image}) image'
|
||||
withAdminUsers $ \cId -> do
|
||||
let approveCmd = MCText $ "/approve " <> tshow groupId <> ":" <> viewName displayName <> " " <> tshow gaId <> if promoted then " promote=on" else ""
|
||||
@@ -821,8 +837,11 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) PRMAllGroups Nothing) >>= \case
|
||||
Right (CRConnectionPlan _ _ _ _ (CPGroupLink (GLPKnown {groupInfo = g', groupUpdated, linkOwners = ListDef owners}))) ->
|
||||
checkValidOwner dbOwnerMemberId owners $ do
|
||||
when groupUpdated $ reapprove pg gr groupRegStatus g'
|
||||
when (groupUpdated || summary /= groupSummary g') $ listingsUpdated env
|
||||
-- re-verified every cycle: the directory re-publishes the name, so it must
|
||||
-- know the name still resolves to the link in the (just refreshed) profile
|
||||
g'' <- verifyGroupDomain_ g'
|
||||
when groupUpdated $ reapprove pg gr groupRegStatus g''
|
||||
when (groupUpdated || summary /= groupSummary g'' || groupDomainVerified g'' /= groupDomainVerified gInfo) $ listingsUpdated env
|
||||
Left (ChatErrorAgent {agentError = SMP _ err}) | linkDeleted err ->
|
||||
setGroupStatus logError st env cc groupId GRSRemoved $ \gr' ->
|
||||
notifyOwner gr' "The channel link is no longer valid.\nThe channel is removed from the directory."
|
||||
@@ -1202,9 +1221,10 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
Just PCAll -> "_enabled_"
|
||||
Just PCNoImage -> "_enabled for profiles without image_"
|
||||
DCShowUpgradeGroupLink gId gName_ ->
|
||||
(if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \GroupInfo {groupId, groupProfile = GroupProfile {publicGroup = pg_}, localDisplayName = gName} _ -> case pg_ of
|
||||
(if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g@GroupInfo {groupId, groupProfile = GroupProfile {publicGroup = pg_}, localDisplayName = gName} _ -> case pg_ of
|
||||
Just pg@PublicGroupProfile {groupLink} ->
|
||||
sendReply $ "The link to join the " <> groupTypeStr' pg <> " " <> groupReference' gId gName <> ":\n" <> strEncodeTxt groupLink
|
||||
<> maybe "" (("\nSimpleX name: " <>) . simplexNameStr) (verifiedGroupDomain g)
|
||||
Nothing -> do
|
||||
let groupRef = groupReference' gId gName
|
||||
withGroupLinkResult groupRef (sendChatCmd cc $ APIGetGroupLink groupId) $
|
||||
@@ -1289,10 +1309,10 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
where
|
||||
msgs = replyMsg :| map foundGroup gs <> [moreMsg | moreGroups > 0]
|
||||
replyMsg = (Just ciId, MCText reply)
|
||||
foundGroup (GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_, memberAdmission}, groupSummary}, _) =
|
||||
foundGroup (g@GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_, memberAdmission}, groupSummary}, _) =
|
||||
let membersStr = "_" <> membersCountStr p groupSummary <> "_"
|
||||
showId = if isAdmin then tshow groupId <> ". " else ""
|
||||
text = T.unlines $ [showId <> groupInfoText p, membersStr] ++ knockingStr memberAdmission
|
||||
text = T.unlines $ [showId <> groupInfoText (simplexNameStr <$> verifiedGroupDomain g) p, membersStr] ++ knockingStr memberAdmission
|
||||
in (Nothing, maybe (MCText text) (\image -> MCImage {text, image}) image_)
|
||||
moreMsg = (Nothing, MCText $ "Send /next for " <> tshow moreGroups <> " more result(s).")
|
||||
|
||||
@@ -1490,7 +1510,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
membersStr = "_" <> membersCountStr p groupSummary <> "_"
|
||||
cmds = "/'role " <> tshow useGroupId <> "', /'filter " <> tshow useGroupId <> "'"
|
||||
ownerStr = maybe "" (("Owner: " <>) . either (("getContact error: " <>) . T.pack) localDisplayName') ct_
|
||||
text = T.unlines $ [tshow useGroupId <> ". " <> groupInfoText p] ++ [ownerStr | isAdmin] ++ [membersStr, statusStr] ++ knockingStr memberAdmission ++ [cmds]
|
||||
text = T.unlines $ [tshow useGroupId <> ". " <> groupInfoText (simplexNameStr <$> verifiedGroupDomain g) p] ++ [ownerStr | isAdmin] ++ [membersStr, statusStr] ++ knockingStr memberAdmission ++ [cmds]
|
||||
msg = maybe (MCText text) (\image -> MCImage {text, image}) image_
|
||||
in (Nothing, msg)
|
||||
|
||||
@@ -1563,3 +1583,6 @@ unexpectedError err = "Unexpected error: " <> err <> ", please notify the develo
|
||||
|
||||
strEncodeTxt :: StrEncoding a => a -> Text
|
||||
strEncodeTxt = safeDecodeUtf8 . strEncode
|
||||
|
||||
simplexNameStr :: SimplexDomain -> Text
|
||||
simplexNameStr = shortNameInfoStr . SimplexNameInfo NTPublicGroup
|
||||
|
||||
@@ -43,6 +43,7 @@ module Directory.Store
|
||||
getAllListedGroups,
|
||||
getAllListedGroups_,
|
||||
searchListedGroups,
|
||||
verifiedGroupDomain,
|
||||
groupRegStatusText,
|
||||
pendingApproval,
|
||||
groupRemoved,
|
||||
@@ -86,11 +87,13 @@ import Data.Time.Clock.System (systemEpochDay)
|
||||
import Directory.Search
|
||||
import Directory.Util
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Names (claimDomain)
|
||||
import Simplex.Chat.Options.DB (FromField (..), ToField (..))
|
||||
import Simplex.Chat.Store
|
||||
import Simplex.Chat.Store.Groups
|
||||
import Simplex.Chat.Store.Shared (groupInfoQueryFields, groupInfoQueryFrom)
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Messaging.Agent.Protocol (SimplexDomain)
|
||||
import Simplex.Messaging.Agent.Store.DB (BoolInt (..), fromTextField_)
|
||||
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
import Simplex.Messaging.Encoding.String
|
||||
@@ -222,6 +225,13 @@ grDirectoryStatus = \case
|
||||
GRSRemoved -> DSRemoved
|
||||
_ -> DSRegistered
|
||||
|
||||
-- the group's claimed SimpleX name, only when its verification status is recorded as
|
||||
-- verified - the display criterion for all user-facing directory surfaces
|
||||
verifiedGroupDomain :: GroupInfo -> Maybe SimplexDomain
|
||||
verifiedGroupDomain GroupInfo {groupProfile = GroupProfile {publicGroup}, groupDomainVerified}
|
||||
| groupDomainVerified == Just True = claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim)
|
||||
| otherwise = Nothing
|
||||
|
||||
$(JQ.deriveJSON (enumJSON $ dropPrefix "PC") ''ProfileCondition)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''DirectoryMemberAcceptance)
|
||||
@@ -385,15 +395,20 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa
|
||||
orderBy = " ORDER BY r.created_at DESC, r.group_reg_id ASC "
|
||||
STSearch search -> case lastGroup_ of
|
||||
Nothing -> do
|
||||
gs <- groups currentTs $ DB.query db (listedGroupQuery <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, s, s, s, s, pageSize)
|
||||
n <- count $ DB.query db (countQuery' <> searchCond) (GRSActive, s, s, s, s)
|
||||
gs <- groups currentTs $ DB.query db (listedGroupQuery <> searchCond <> orderBy <> " LIMIT ?") ((userId, userContactId, GRSActive, s, s, s, s) :. (sDomain, pageSize))
|
||||
n <- count $ DB.query db (countQuery' <> searchCond) (GRSActive, s, s, s, s, sDomain)
|
||||
pure (gs, n)
|
||||
Just gId -> do
|
||||
gs <- groups currentTs $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, s, s, s, s, pageSize)
|
||||
n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> searchCond) (GRSActive, gId, s, s, s, s)
|
||||
gs <- groups currentTs $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> searchCond <> orderBy <> " LIMIT ?") ((userId, userContactId, GRSActive, gId, s, s, s, s) :. (sDomain, pageSize))
|
||||
n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> searchCond) (GRSActive, gId, s, s, s, s, sDomain)
|
||||
pure (gs, n)
|
||||
where
|
||||
s = T.toLower search
|
||||
-- names are searched without the # / @ prefix; "#" (empty after stripping) cannot
|
||||
-- match any stored domain, so a bare prefix does not match every named group
|
||||
sDomain = case T.uncons s of
|
||||
Just (c, rest) | c == '#' || c == '@' -> if T.null rest then "#" else rest
|
||||
_ -> s
|
||||
countQuery' = countQuery <> " JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id WHERE r.group_reg_status = ? "
|
||||
orderBy = " ORDER BY g.summary_current_members_count DESC, r.group_reg_id ASC "
|
||||
where
|
||||
@@ -407,6 +422,7 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa
|
||||
OR LOWER(gp.full_name) LIKE '%' || ? || '%'
|
||||
OR LOWER(gp.short_descr) LIKE '%' || ? || '%'
|
||||
OR LOWER(gp.description) LIKE '%' || ? || '%'
|
||||
OR (LOWER(gp.group_domain) LIKE '%' || ? || '%' AND g.group_domain_verified = 1)
|
||||
)
|
||||
|]
|
||||
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
# Directory service: SimpleX domain names
|
||||
|
||||
Add SimpleX domain names (`#name`) of listed channels to the directory: show them in the
|
||||
bot's chat output, include them in the generated web listing and on the directory web page,
|
||||
and make both bot search and web search match them.
|
||||
|
||||
Prerequisites (part A, core):
|
||||
|
||||
1. `updateGroupFromLinkData` must stop marking unresolved claims as verified — today the
|
||||
link-refresh paths mark self-claimed names verified, which would make the directory an
|
||||
impersonation amplifier. It must also NOT resolve names itself: automatic resolution
|
||||
from a member's device is a metadata leak (the resolver learns which names the
|
||||
device's channels claim — membership metadata, on a refresh cadence).
|
||||
2. `APIVerifyGroupDomain` must be fixed to do the verification actually needed:
|
||||
validate that the group name and the group link are **consistent** — resolving the
|
||||
claimed name leads to the link declared in the profile (`publicGroup.groupLink`).
|
||||
The link may differ from the one we joined through, and that is fine: link rotation
|
||||
is a supported direction, so no join-link comparison. Verification is explicit:
|
||||
users trigger it manually (or via the client's own policy); the directory — a public
|
||||
bot whose resolver queries reveal nothing personal — calls it directly.
|
||||
|
||||
Line refs are to the working tree at planning time; re-check before editing.
|
||||
|
||||
## 1. Current state (facts, with references)
|
||||
|
||||
### Names in core
|
||||
|
||||
- A channel's name is a claim on its group profile:
|
||||
`PublicGroupAccess.groupDomainClaim :: Maybe SimplexDomainClaim` (`Types.hs:847-853`),
|
||||
reachable as `publicGroup >>= publicGroupAccess >>= groupDomainClaim`. Only public,
|
||||
relay-backed groups (channels) have `publicGroup`; legacy p2p groups cannot have names.
|
||||
- `SimplexDomainClaim = { domain :: StrJSON SimplexDomain, proof :: Maybe SimplexDomainProof }`
|
||||
(`Names.hs:49-53`). `SimplexDomain` (simplexmq `SimplexName.hs:40`) stores as TEXT via
|
||||
`fullDomainName` — e.g. `team.simplex` — in `group_profiles.group_domain`
|
||||
(row mapping `Store/Shared.hs:688,720-727`).
|
||||
- Display form: `shortNameInfoStr (SimplexNameInfo NTPublicGroup domain)` → `#team` for a
|
||||
plain `.simplex` name, otherwise `#`+full domain (simplexmq `SimplexName.hs:118-125`).
|
||||
`groupSimplexDomain :: GroupInfo -> Maybe SimplexDomain` exists (`View.hs:1131-1133`;
|
||||
the module has no export list, so it is importable).
|
||||
- Local status: `GroupInfo.groupDomainVerified :: Maybe Bool` (`Types.hs:502`), column
|
||||
`groups.group_domain_verified`, included in `groupInfoQueryFields`
|
||||
(`Store/Shared.hs:799-805`) — every directory store query already returns claim + status.
|
||||
`setGroupDomainVerified` (`Store/Groups.hs:2754`) writes it; `updateGroupProfile`
|
||||
already resets it to NULL when (and only when) the claim changes
|
||||
(`Store/Groups.hs:2712-2731`).
|
||||
- The owner sets the name with `/public group access #g domain=...`
|
||||
(`Commands.hs:3143-3152`): the client resolves the domain and requires
|
||||
`nameResolvesTo groupLink` (`Commands.hs:4837-4838`) before storing. Client-side
|
||||
enforcement only — a modified client can claim any name.
|
||||
- Core's own by-name lookup criterion: claim + `group_domain_verified = 1`
|
||||
(`getGroupToConnect`, `Store/Groups.hs:1086-1104`).
|
||||
- Connect-by-name creates the group with the domain set and marked verified
|
||||
(`Store/Groups.hs:2759-2765`) — sound: the user's explicit action resolved the name.
|
||||
|
||||
### Defect 1: link-data refresh marks claims verified without resolution
|
||||
|
||||
`updateGroupFromLinkData` (`Internal.hs:1466-1488`) marks **any** claim present in
|
||||
fetched link data as verified (`verifyChanged`, `:1488`). Call sites:
|
||||
|
||||
| # | Site | Reached by | Name resolved first? | Claim=name checked? |
|
||||
|---|------|-----------|----------------------|---------------------|
|
||||
| 1 | `APIGetUpdatedGroupLinkData`, `Commands.hs:1858` | refresh of the channel's own link data | NO (pure link fetch, `:1855`) | n/a |
|
||||
| 2 | name-plan branch, `Commands.hs:4326` | `APIConnectPlan`, NAME target, group known by link | YES (`resolveNameLink` `:4303,4357-4363`) — explicit user action | yes, but AFTER the mark — `:4335` throws after `:4326` wrote the flag |
|
||||
| 3 | `resolveKnownGroup` `Commands.hs:4346-4355`, LINK target | known group + `PRMAllGroups` — the directory's `deGroupLinkCheck` (`Service.hs:820-821`) and app link taps | NO (`resolveSLink` = `pure l'`, `:4290-4292`) | n/a |
|
||||
| 4 | `resolveKnownGroup`, NAME target | reachable only under `PRMAllGroups`, which no caller sends with a name target (the directory sends link targets only, `Service.hs:563,821,957`; user connects use `PRMUnknown`, `Commands.hs:2288`) — currently unexercised | YES | n/a — the group was found by the verified-name lookup, so it is already verified |
|
||||
|
||||
The **contact** twin has sound semantics (`Commands.hs:4246-4253`):
|
||||
`updateContactFromLinkData` runs only on the name path (`planDomain` guard, `:4249-4250`)
|
||||
and the claim-equality check throws **before** it runs (`:4252-4253`). Note sites 1 and 3
|
||||
are still needed for profile / member-count refresh — the fix is the verification rule
|
||||
inside, not the calls.
|
||||
|
||||
### Defect 2: `APIVerifyGroupDomain` does not do the needed verification
|
||||
|
||||
`APIVerifyGroupDomain` (`Commands.hs:2301-2307`) delegates to `verifyEntityDomain`
|
||||
(`:4840-4864`), which requires `claim.proof` and verifies a proof signature. For a public
|
||||
group this is the wrong check: the claim arrives owner-signed (link data or signed
|
||||
`XGrpInfo`), so claim authenticity is already given; what needs verification is
|
||||
**consistency** — that resolving the claimed name leads to the link the profile
|
||||
declares (not to the link we joined through, which may have rotated). The proof requirement
|
||||
also makes the command inoperative today (nothing in this tree generates
|
||||
`SimplexDomainProof`; it always returns "no name proof to verify").
|
||||
`APIVerifyContactDomain` (`:2294-2300`) is a separate case (contact claims are not
|
||||
link-bound the same way) and is out of scope.
|
||||
|
||||
### Directory service integration points
|
||||
|
||||
- Bot output text: `groupInfoText` (`Service.hs:364-370`) — used by `sendToApprove`
|
||||
(`:802-813`, admin approval message), `sendFoundGroups` (`:1287-1297`, search results),
|
||||
and `sendGroupsInfo` (`:1478-1495`, `/list`, `/last`, `/pending`). All call sites also
|
||||
have the `GroupReg`. The `/link` command's public-group branch prints the channel's
|
||||
link — the profile-declared `groupLink` (`:1204-1207`).
|
||||
- Bot search: `searchListedGroups` (`Store.hs:357-411`); `searchCond` (`:404-411`) matches
|
||||
`gp.display_name`, `gp.full_name`, `gp.short_descr`, `gp.description` with `LIKE`.
|
||||
- Web listing: `generateListing` (`Listing.hs:146-168`) writes `listing.json` /
|
||||
`promoted.json`; entries built by `groupDirectoryEntry` (`:99-144`) into
|
||||
`DirectoryEntry` (`:70-81`), JSON via `defaultJSON` — an optional field is
|
||||
wire-compatible.
|
||||
- Web page: `website/src/js/directory.jsc` — display in `displayEntries` (`:212-341`),
|
||||
client-side search in `filterEntries` (`:98-112`); search input
|
||||
`website/src/directory.html:273`.
|
||||
- Periodic link check: `linkCheckThread_` (`Service.hs:199-212`), default 1800s
|
||||
(`Options.hs:175-182`); `deGroupLinkCheck` (`:816-854`) sends a link-target
|
||||
`APIConnectPlan … PRMAllGroups` per active/pending group — targeting the
|
||||
profile-declared `groupLink` (`:818-820`) — and triggers `listingsUpdated` when
|
||||
`groupUpdated || summary /= groupSummary g'` (`:825`).
|
||||
|
||||
## 2. Design decisions (alternatives considered)
|
||||
|
||||
1. **Verification = name ↔ profile-link consistency.** Resolving the claimed name must
|
||||
lead to the link the profile declares (`nameResolvesTo (publicGroup.groupLink)`,
|
||||
helper at `Commands.hs:4837-4838`) — the same check the owner's client runs at set
|
||||
time (`APISetPublicGroupAccess`, `:3149-3150`), so the rule is uniform. The join
|
||||
link is NOT compared: link rotation is supported, a name may legitimately resolve to
|
||||
a link we never joined through. This is also exactly the property the directory
|
||||
publishes — the listing's join link IS the profile-declared link
|
||||
(`Listing.hs:124-126`), so a verified name is one that leads users to the same place
|
||||
as the entry itself. The profile is trusted (owner-signed provenance: link data or
|
||||
signed `XGrpInfo`; a profile update carrying a different `publicGroupId` is rejected —
|
||||
`Subscriber.hs:3684-3685` for `XGrpInfo`, extended to link-data-borne updates by
|
||||
A1's mirror guard). No proof is involved. Rejected alternatives:
|
||||
(a) comparing against the join link — breaks on every rotation; (b) fetching the
|
||||
resolved link's data and comparing `publicGroupId` — an extra network round-trip
|
||||
for a binding the consistency rule does not need.
|
||||
2. **Verification is explicit, never automatic on a user's device.** The distinction
|
||||
is the request payload, not the destination — both operations are served via SMP
|
||||
servers: a link-data fetch references an opaque link ID (the serving host learns
|
||||
only that some client fetched that link — nothing name-shaped), while a resolution
|
||||
query carries the **readable name**, so whoever serves it learns which name this
|
||||
device is interested in. Automatic resolution on profile/link refresh would
|
||||
therefore leak membership metadata on a cadence; scheduled link-data fetches (the
|
||||
UI's chat-open refresh, the directory's link checks) are fine. So: claim change
|
||||
drops the status locally (already the case, `Groups.hs:2717`); resolution happens
|
||||
only when the user acts (verify command / client's own verify policy, or connecting
|
||||
by name, where resolution is the action itself). The directory, a public service,
|
||||
calls the verify API explicitly — its resolver queries reveal nothing personal.
|
||||
3. **Connect-by-name paths still record verified** (sites 2 and 4 with resolved-domain
|
||||
context): the resolution already happened as the user's explicit action; storing the
|
||||
result adds no resolver call and no leak, and matches the existing
|
||||
created-as-verified behavior (`Groups.hs:2759-2765`).
|
||||
4. **The directory displays claim + `group_domain_verified = 1`** — the identical
|
||||
criterion core uses for by-name lookup (`Groups.hs:1103`). User-facing surfaces
|
||||
(search results, `/list`, `/link`, listing JSON, web page) show only verified names;
|
||||
the admin approval message additionally shows an unverified claim, marked, because
|
||||
admins decide approval.
|
||||
5. **Directory triggers**: call `APIVerifyGroupDomain` from the existing periodic link
|
||||
check for **every group with a claim, every cycle** — the directory re-publishes the
|
||||
name, so it must know the name is still working, not only that it worked once. One
|
||||
trigger covers the initial claim, claim changes, resolver-failure retries, an owner
|
||||
fixing the registry after a failed check, and a registry entry lapsing or being
|
||||
re-pointed (the name disappears within one link-check interval). No staleness state
|
||||
or timestamps needed. Clients do NOT re-verify on a schedule — only the directory.
|
||||
Plus a synchronous check when an approval request is composed (`sendToApprove`, B2)
|
||||
so admins always see the actual state. Cost: one resolver query per claimed group
|
||||
per cycle (default 30 min), plus one per approval request.
|
||||
6. **No directory schema change, no directory-log change.**
|
||||
7. **`groupInfoText` gains a `Maybe Text` name parameter** (the already-formatted line
|
||||
content), keeping the admin/user display policy visible at the call sites.
|
||||
Alternative: pass `(GroupInfo, GroupReg)` and decide inside — rejected: hides the
|
||||
policy in a text builder.
|
||||
|
||||
## 3. Part A — core fixes
|
||||
|
||||
### A1. `updateGroupFromLinkData` (`Internal.hs:1466-1488`): no marking without resolution, no resolving
|
||||
|
||||
The contract, on any group profile update:
|
||||
|
||||
1. name unchanged → verification status retained (including `Just False`);
|
||||
2. name changed or removed → status reset to unknown (NULL);
|
||||
3. name added → status remains unknown (NULL).
|
||||
|
||||
The store layer already enforces all three: `updateGroupProfile` compares old vs new
|
||||
claim domain and resets to NULL exactly when it changed (`Groups.hs:2712-2717`,
|
||||
in-memory copy at `:2714`). Every caller is clean — `xGrpInfo` (`Subscriber.hs:3690`,
|
||||
with `publicGroupId` immutability guards `:3684-3686`), the relay accept path
|
||||
(`Subscriber.hs:4403`, `publicGroupId` check `:4396-4399`), `runUpdateGroupProfile`
|
||||
(`Commands.hs:3913`), `createGroupInvitation` (`Groups.hs:474`), the member-update path
|
||||
(`Groups.hs:815-816`) — **except** `updateGroupFromLinkData` (`Internal.hs:1478`), which
|
||||
overrides the reset and marks `Just True` whenever a claim is present (`verifyChanged`
|
||||
`:1488`). It thereby violates all three invariants; the worst case is invariant 1: an
|
||||
unchanged claim with status `Just False` is overwritten to `Just True` — apps call
|
||||
`APIGetUpdatedGroupLinkData` on every chat open (`ChatView.swift:773`,
|
||||
`ChatView.kt:216`), so a failed manual verification is silently erased to "verified"
|
||||
the next time the user opens the channel.
|
||||
|
||||
Fix: add a parameter `resolvedDomain_ :: Maybe SimplexDomain` — the domain the caller
|
||||
just resolved (by explicit user action) to reach this link data; `Nothing` otherwise.
|
||||
Rule:
|
||||
|
||||
- set verified (`Just True`) exactly when `resolvedDomain_` equals the incoming claim's
|
||||
domain (the connect-by-name exception, decision 3 — same class as prepared-group
|
||||
creation, `Groups.hs:671-674`). Do NOT also gate on the current status being
|
||||
not-`Just True`: `updateGroupProfile` runs first and, on a claim rotation, resets the
|
||||
status to NULL, so a stale pre-update `Just True` would wrongly suppress the re-mark of
|
||||
the newly-resolved name — leaving it NULL. The write is idempotent, so re-marking an
|
||||
already-verified same-name group is a harmless no-op;
|
||||
- this marking condition is part of the function's **entry guard**
|
||||
(`profileChanged || countChanged || verifyResolved`) — a name-plan hit on an unchanged
|
||||
profile with a NULL status must still mark, even though nothing else changed;
|
||||
- otherwise never touch the status — the store layer's three invariants then hold on
|
||||
every path;
|
||||
- **no resolver calls in this function** (decision 2 — metadata leak).
|
||||
|
||||
Call sites:
|
||||
|
||||
The whole call-site diff is one added argument in three places:
|
||||
|
||||
- `Commands.hs:1858` (`APIGetUpdatedGroupLinkData`): pass `Nothing` — pure refresh.
|
||||
- `Commands.hs:4326` (name-plan branch): pass `Just nameDomain` (bind it in the case
|
||||
alternative that currently discards it, `(Just _, …)` → `(Just nameDomain, …)`).
|
||||
No reordering: the existing claim check at `:4328-4335` stays where it is — the
|
||||
marking rule itself prevents the status write when the fresh claim differs from the
|
||||
resolved name, so a mismatched claim throws with no status written. The profile
|
||||
refresh that runs before the throw is the group's own link data and is legitimate.
|
||||
- `Commands.hs:4353` (`resolveKnownGroup`): pass `Nothing` — a link-refresh path.
|
||||
(With a name target it is reachable only under `PRMAllGroups`, which no caller sends
|
||||
with a name — see §6; and a group found by name is already verified,
|
||||
`Groups.hs:1103`, so marking would be a no-op there anyway.)
|
||||
|
||||
Nothing moves, nothing reorders; lookup semantics (name → local claim+verified match,
|
||||
link → stored link match, miss → resolve → OK plan) are untouched.
|
||||
|
||||
Behavior change: members who joined via link no longer see
|
||||
"SimpleX name: #x (verified)" (`simplexDomainLine`, `View.hs:1145-1154`) from the
|
||||
self-claim alone; the status is NULL (shown unverified) until the user verifies — or
|
||||
the client's verify policy does on opening group info (`SimplexNameView.kt:62-63`).
|
||||
|
||||
### A2. Fix `APIVerifyGroupDomain` (`Commands.hs:2301-2307`)
|
||||
|
||||
Applies to public groups (channels) only — claims exist only on `publicGroup` profiles;
|
||||
p2p groups cannot carry one (`xGrpInfo` rejects it, `Subscriber.hs:3686`); business
|
||||
groups use the separate address-anchored mechanism (`setPreparedGroupDomain`,
|
||||
`Groups.hs:2758-2769`, domain on `businessChat.businessDomain`); contacts keep
|
||||
`APIVerifyContactDomain` untouched.
|
||||
|
||||
The check is name ↔ profile-link consistency (decision 1); no join-link comparison and
|
||||
no `preparedGroup` involvement — the flow works identically for the owner, a member,
|
||||
and the directory bot, before or after link rotation:
|
||||
|
||||
1. Require a claim (`publicGroup >>= publicGroupAccess >>= groupDomainClaim`) — as now.
|
||||
2. `resolveSimplexName` (agent) on the claimed domain →
|
||||
`nameResolvesTo (publicGroup.groupLink) nrSimplexChannel`
|
||||
(helper at `Commands.hs:4837-4838`; `groupLink` is a mandatory field of
|
||||
`PublicGroupProfile`, `Types.hs:855-861`) — the same consistency check the owner's
|
||||
client runs at set time (`:3149-3150`).
|
||||
3. Holds → `Just True`, else `Just False`, via `setGroupDomainVerified`; respond
|
||||
`CRGroupDomainVerified user g' reason` as now. A definite NAME NOT_FOUND resolver
|
||||
answer (cf. `tests/ChatTests/Names.hs:100-102`) is `Just False`; network/transport
|
||||
errors remain thrown `ChatErrorAgent` (retryable, status unchanged).
|
||||
4. No proof involvement for groups. (`verifyEntityDomain`'s proof logic remains for
|
||||
`APIVerifyContactDomain`, untouched.)
|
||||
|
||||
Division of labor — the verify API itself performs only the resolver query; it does
|
||||
NOT fetch link data, because the profile it checks against is kept fresh by the
|
||||
existing link-data fetches, which continue on their own cadence:
|
||||
|
||||
- the directory fetches link data on every link-check cycle (`deGroupLinkCheck` →
|
||||
connect plan, `Service.hs:820-821`), refreshing the profile (claim and `groupLink`)
|
||||
before the verify call runs in the same cycle;
|
||||
- the UI fetches on chat open (`APIGetUpdatedGroupLinkData` — `ChatView.swift:773`,
|
||||
`ChatView.kt:216`), so by the time group info's auto/manual verify fires
|
||||
(`SimplexNameView.kt:62-63`) the profile is current;
|
||||
- per A1, none of those fetches touch the verification status — they only refresh the
|
||||
data that verification is checked against.
|
||||
|
||||
The verify API is called only on user interaction (manual, or the client's verify
|
||||
policy) and from the directory (B2) — never on a schedule from a user's device.
|
||||
|
||||
### A3. Owner set-name marks verified (`APISetPublicGroupAccess`, `Commands.hs:3143-3152`)
|
||||
|
||||
Setting a new/changed domain already resolves it and requires
|
||||
`nameResolvesTo groupLink` as a precondition (`:3147-3150`) — but the subsequent
|
||||
`runUpdateGroupProfile` resets the status to NULL (claim changed), so the owner's own
|
||||
channel reads as unverified right after a successful set. Fix: in the
|
||||
`APISetPublicGroupAccess` handler, after the profile update, set the status `Just True`
|
||||
— only on the branch where the resolution check ran (new/changed domain). Setting the
|
||||
same domain skips resolution and retains the status (invariant 1); clearing the domain
|
||||
leaves NULL (invariant 2). This is the same trust event as connect-by-name: resolution
|
||||
by explicit user action, persisted immediately. The UI sets channel names through this
|
||||
command (`GroupChatInfoView.kt:192`), so the fix covers the app flow. Note the returned
|
||||
`CRGroupUpdated` must carry the updated status (the UI reads the flag off the returned
|
||||
`GroupInfo`) — set the flag after `runUpdateGroupProfile` and patch or re-read the
|
||||
group for the response.
|
||||
|
||||
### A4. Core tests (`tests/ChatTests/Names.hs`, resolver stub `tests/NameResolver.hs`)
|
||||
|
||||
1. Link-target plan / link-data refresh does NOT change the verification status and does
|
||||
NOT query the resolver (register a counter in the `NameResolver` stub registry, or
|
||||
assert status stays NULL after refresh) — regression for sites 1 and 3 and for the
|
||||
no-auto-resolution rule.
|
||||
2. The three invariants across a profile update cycle: unchanged claim retains the
|
||||
status — specifically `Just False` survives a link-data refresh / chat open
|
||||
(regression for the erased-failure defect); changed claim resets to NULL; added
|
||||
claim stays NULL.
|
||||
3. `/_verify domain #<gId>`: name resolving to the profile-declared link → verified
|
||||
(this is also the rotation case — the check never touches the join link); resolving
|
||||
to a different link → failed; NOT_FOUND → failed; resolver unreachable → error,
|
||||
status unchanged.
|
||||
4. Claim change resets the status; the by-name lookup (`getGroupToConnect`) stops
|
||||
matching until re-verified.
|
||||
5. Name-plan mismatch: link data claiming a different name than the resolved one throws
|
||||
`CESimplexDomainNotReady` and the status is not written (the marking rule requires
|
||||
the fresh claim to equal the resolved name).
|
||||
6. Owner sets the name (`/public group access ... domain=`): status is `Just True`
|
||||
after a successful set of a new domain; re-sending the same domain retains the
|
||||
status; clearing it resets to NULL.
|
||||
|
||||
## 4. Part B — directory service
|
||||
|
||||
### B1. Verified-name accessor
|
||||
|
||||
`verifiedGroupDomain :: GroupInfo -> Maybe SimplexDomain` — the claim
|
||||
(`groupSimplexDomain`) when `groupDomainVerified == Just True`; in `Directory.Store`
|
||||
(used by both `Service.hs` and `Listing.hs`). Display string:
|
||||
`shortNameInfoStr . SimplexNameInfo NTPublicGroup`.
|
||||
|
||||
### B2. Verification trigger (`Service.hs`)
|
||||
|
||||
- In `deGroupLinkCheck` (`:816-854`), after the existing owner check: when the group
|
||||
profile has a claim, send `APIVerifyGroupDomain groupId` — every cycle, regardless of
|
||||
the current status (decision 5: the directory must know the name is still working).
|
||||
The ordering matters: the verify call runs after the cycle's connect-plan call has
|
||||
fetched fresh link data, so consistency is checked against the current claim and
|
||||
`groupLink`, not day-old state.
|
||||
On `CRGroupDomainVerified {groupInfo = g'}` with a status different from before and
|
||||
the group listed → `listingsUpdated env`. Errors (`Left` network/timeout) → log,
|
||||
status unchanged, retry next cycle.
|
||||
- Also extend the existing `listingsUpdated` condition (`:825`) with
|
||||
`groupDomainVerified g' /= groupDomainVerified gInfo` for the plan-driven updates.
|
||||
- Verify **synchronously in `sendToApprove`** (`:802-813`), before composing the admin
|
||||
message: when the group has a claim and the status is not `Just True`, run
|
||||
`APIVerifyGroupDomain` and use the returned `GroupInfo` for the message. This is the
|
||||
single choke point through which every approval request flows — registration
|
||||
(`deMemberUpdated` `:1067-1070`), re-registration (`deReregistration` →
|
||||
`pendingApprovalTransition` `:1051-1056`), profile changes
|
||||
(`publicGroupProfileChange` `:552-577`), and link-check reapprovals (`:843-853`) — so
|
||||
admins always see the actual verification state. A registration-time
|
||||
`DEGroupLinkCheck` nudge would NOT work: at `joinAndRegisterPublicGroup` the group is
|
||||
still connecting (the link-target plan returns `GLPConnectingProhibit`, and
|
||||
`deGroupLinkCheck` only covers `GRSActive`/pending-approval states, `:819`), and an
|
||||
async verify would race the approval message. The event loop is already sequential
|
||||
and network-bearing; one added round-trip per approval is in pattern.
|
||||
|
||||
### B3. Bot output (`Service.hs`)
|
||||
|
||||
- `groupInfoText` (`:364`): add `Maybe Text` name parameter; when present, a line
|
||||
`SimpleX name: #team` after `groupNameDescr` (wording mirrors core `View.hs:1175`).
|
||||
- User-facing call sites pass the display string of `verifiedGroupDomain`:
|
||||
`sendFoundGroups` (`:1292-1296`), `sendGroupsInfo` (`:1486-1494`).
|
||||
- Admin call site `sendToApprove` (`:802-813`): verified → `#team`; claimed but not
|
||||
verified → `#team (NOT verified - will not be shown)`.
|
||||
- `/link` command public branch (`:1204-1207`): add the verified-name line under the link.
|
||||
|
||||
### B4. Bot search (`Store.hs`)
|
||||
|
||||
`searchListedGroups` `STSearch` (`:386-398`): normalize the query — after `T.toLower`,
|
||||
strip one leading `#` or `@` (aligning with the web page's normalization) — and extend
|
||||
`searchCond` (`:404-411`) with a parenthesized branch:
|
||||
`OR (LOWER(gp.group_domain) LIKE '%' || ? || '%' AND g.group_domain_verified = 1)`.
|
||||
The stored form is the full domain (`team.simplex`), so `team`, `#team`, and
|
||||
`team.simplex` all match. Parameter tuples in all four query/count variants gain one
|
||||
param (the `STSearch` count query already joins `group_profiles`; `g` is in scope in
|
||||
both — the main query via `groupInfoQueryFrom`, `Store/Shared.hs:816-823`). Guard the
|
||||
edge case: when the normalized domain query is empty (the search was just `#`),
|
||||
`LIKE '%%'` would match every named group — pass a value that cannot match (or skip the
|
||||
branch) instead.
|
||||
|
||||
### B5. Web listing (`Listing.hs`)
|
||||
|
||||
- `DirectoryEntry` (`:70-81`): add `simplexName :: Maybe Text` (display form `#team`);
|
||||
`defaultJSON` omits `Nothing` (simplexmq `Parsers.hs:153`), so the field is
|
||||
wire-compatible with existing consumers. `promoted.json` inherits.
|
||||
- `groupDirectoryEntry` (`:99`): no new parameter — it already takes `GroupInfo`, and
|
||||
`verifiedGroupDomain` needs only `GroupInfo` (claim and status both live there);
|
||||
compute the field inside. `Listing.hs` already imports `Directory.Store` (`:34`).
|
||||
|
||||
### B6. Web page (`website/src/js/directory.jsc`, `website/src/directory.html`)
|
||||
|
||||
- `displayEntries` (`:225-228`): when `entry.simplexName` is set, render it under the
|
||||
`h2` display name (small accent-styled line, same pattern as other meta lines). Use
|
||||
`textContent`, not `innerHTML` — same as `displayName` (`:226`); domains are
|
||||
parser-constrained to ASCII (`nameLabelP`, simplexmq `SimplexName.hs:59-71`), but the
|
||||
listing file is an external input to the page.
|
||||
- `filterEntries` (`:98-112`): add a `simplexName` match, normalizing both sides:
|
||||
lowercase, strip leading `#`/`@`, strip trailing `.simplex` — so `#team`, `team`, and
|
||||
`team.simplex` all match an entry displaying `#team`.
|
||||
- Optional: `#search` placeholder copy in `directory.html`.
|
||||
|
||||
### B7. Directory tests (`tests/Bots/DirectoryTests.hs`)
|
||||
|
||||
Directory name tests run under `withSmpServerAndNames` (`tests/ChatClient.hs:596`) — the
|
||||
directory harness uses the ambient SMP server, so they are a **separate spec**
|
||||
(`directoryNameTests`) wired in `Test.hs` under the names-capable `tmpTestBracket`
|
||||
(mirroring `chatNamesTests`), not folded into `directoryServiceTests` (which runs under a
|
||||
plain SMP server). Channel setup follows `testRegisterChannelViaCard` (`:2042`).
|
||||
|
||||
Implemented:
|
||||
1. **Verified name shown** (`testDirectoryChannelName`): resolver maps `news` to the
|
||||
channel's link → owner sets `domain=news.simplex` → register via card → the directory
|
||||
verifies name↔link consistency and the admin approval message shows `SimpleX name: #news`.
|
||||
2. **Unverified claim marked** (`testDirectoryChannelNameNotVerified`): the registry entry
|
||||
is re-pointed to a different link after the owner set the name → the directory's
|
||||
verification fails → the admin sees `SimpleX name: #news (NOT verified - will not be shown)`.
|
||||
|
||||
These exercise the security-critical properties end-to-end: verification (A2), the
|
||||
`sendToApprove` trigger (B2), the display line (B3), and the verified-only display gating.
|
||||
|
||||
Not yet written (lower-risk; the underlying code is covered by the core tests A4 and the
|
||||
two above): search-by-name (B4, `#team`/`team`/`team.simplex`), a `listing.json`
|
||||
`simplexName` assertion (B5, extend `checkListings` `:1208-1217`), and re-verification
|
||||
across a `deGroupLinkCheck` cycle (B2).
|
||||
|
||||
## 5. Sequencing (one logical change per commit)
|
||||
|
||||
1. Core: `updateGroupFromLinkData` — resolved-domain parameter (one added argument at
|
||||
three call sites), no marking without it, no auto-resolution
|
||||
(+ core tests A4 1, 2, 5).
|
||||
2. Core: `APIVerifyGroupDomain` — name ↔ profile-link consistency (resolve +
|
||||
`nameResolvesTo (publicGroup.groupLink)`) (+ core tests A4 3, 4).
|
||||
3. Core: owner set-name marks verified (A3) (+ core test A4 6).
|
||||
4. Directory: every-cycle verification in `deGroupLinkCheck` + approval-time
|
||||
verification in `sendToApprove` (B2).
|
||||
5. Directory: bot output (B3) + test 1 bot assertions.
|
||||
6. Directory: search (B4) + tests 2, 3.
|
||||
7. Directory: listing JSON (B5) + `checkListings` extension, test 4.
|
||||
8. Website (B6) — manual verification; no JS test harness exists.
|
||||
|
||||
## 6. Open points / follow-ups
|
||||
|
||||
- Client UX for manual verification (verify button / client-side auto-verify policy per
|
||||
`plans/2026-06-27-namespace-ui-display-set.md`) is separate UI work; part A only makes
|
||||
the API do the right check. The UI in this tree already renders the 3-state indicator
|
||||
and auto-verifies on open when the status is NULL (`SimplexNameView.kt:62-63`,
|
||||
toggle default ON) — so after A1, link-joined members get a real verification on
|
||||
opening channel info instead of today's spurious "(verified)".
|
||||
- `APIVerifyContactDomain` and contact names (`@name`) are out of scope — the directory
|
||||
lists only groups, and contact claims are not bound to a joined link the same way.
|
||||
- Observed but NOT in scope (no change requested; decide separately if ever relevant):
|
||||
(a) `resolveKnownGroup` with a name target would apply the registry-resolved link's
|
||||
data to the locally-found group without checking the resolved link is that group's —
|
||||
the path is currently unexercised (defect table row 4); (b) link-data-borne profile
|
||||
updates lack the `publicGroupId` mismatch rejection that `xGrpInfo` applies
|
||||
(`Subscriber.hs:3684-3685`).
|
||||
|
||||
Resolved decisions (were open points):
|
||||
|
||||
- Owner's own status after setting the name — fix in scope (A3): mark verified after a
|
||||
successful set, since resolution ran as its precondition.
|
||||
- Invariant 3 ("added → unknown") admits exactly one class of exception — resolution by
|
||||
explicit user action, persisted where the group happens to live: at creation for an
|
||||
unknown group (`APIPrepareGroup domain=`, `Groups.hs:671-674`), on the existing row
|
||||
for a known group (A1's `resolvedDomain_` on the name-plan paths), and at set time
|
||||
for the owner (A3). All confirmed.
|
||||
- Stale verified names: the directory re-verifies every claimed group on every link
|
||||
check (decision 5), so a lapsed or re-pointed registry entry drops the name within
|
||||
one interval. Clients never re-verify on a schedule — the freshness requirement
|
||||
belongs to the publisher, not the member.
|
||||
- **Link rotation is supported** — no link-immutability invariants anywhere: joining
|
||||
does not require the profile link to equal the joining link, `xGrpInfo` does not
|
||||
freeze `groupLink`, and verification never compares against the join link. The
|
||||
earlier rule "verification fails when the profile link differs from the join link"
|
||||
is superseded: verification is name ↔ profile-link consistency only. The only link
|
||||
ever compared is the profile-declared one; the join link is never used. Incoming
|
||||
profile updates remain subject to the existing `publicGroupId` mismatch rejection
|
||||
(`xGrpInfo`), mirrored for link data by A1.
|
||||
@@ -111,7 +111,8 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), patt
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (base64P)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NameRecord (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), ErrorType (NAME), MsgFlags (..), NameRecord (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport.Client (defaultSocksProxyWithAuth)
|
||||
@@ -1855,7 +1856,7 @@ processChatCommand cxt nm = \case
|
||||
(_, cData@(ContactLinkData _ UserContactData {relays = currentRelayLinks})) <- getShortLinkConnReq' nm user sLnk
|
||||
groupSLinkData_ <- liftIO $ decodeLinkUserData cData
|
||||
gInfo' <- case groupSLinkData_ of
|
||||
Just sLinkData -> fst <$> updateGroupFromLinkData user gInfo sLinkData
|
||||
Just sLinkData -> fst <$> updateGroupFromLinkData user gInfo sLinkData Nothing
|
||||
_ -> pure gInfo
|
||||
when (memberRole' (membership gInfo) /= GROwner && memberCurrent (membership gInfo)) $
|
||||
withGroupLock "syncSubscriberRelays" groupId $
|
||||
@@ -2299,11 +2300,20 @@ processChatCommand cxt nm = \case
|
||||
ct' <- maybe (pure ct) (\v -> withFastStore' $ \db -> setContactDomainVerified db user ct v) verified
|
||||
pure $ CRContactDomainVerified user ct' reason
|
||||
APIVerifyGroupDomain groupId -> withUser $ \user -> do
|
||||
g@GroupInfo {groupProfile = GroupProfile {publicGroup}, preparedGroup} <- withFastStore $ \db -> getGroupInfo db cxt user groupId
|
||||
let connLink_ = preparedGroup >>= \PreparedGroup {connLinkToConnect = CCLink _ sLnk_} -> ACSL SCMContact <$> sLnk_
|
||||
domain <- maybe (throwCmdError "group has no name to verify") pure $ publicGroup >>= publicGroupAccess >>= groupDomainClaim
|
||||
(verified, reason) <- verifyEntityDomain user nm NTPublicGroup domain connLink_
|
||||
g' <- maybe (pure g) (\v -> withFastStore' $ \db -> setGroupDomainVerified db user g v) verified
|
||||
g@GroupInfo {groupProfile = GroupProfile {publicGroup}} <- withFastStore $ \db -> getGroupInfo db cxt user groupId
|
||||
PublicGroupProfile {groupLink, publicGroupAccess} <- maybe (throwCmdError "not a public group") pure publicGroup
|
||||
claim <- maybe (throwCmdError "group has no name to verify") pure $ publicGroupAccess >>= groupDomainClaim
|
||||
-- name <-> link consistency: resolving the claimed name must lead to the link in the group
|
||||
-- profile - the same check the owner's client runs when setting the name; the link used
|
||||
-- to join is not compared, links may rotate
|
||||
(verified, reason) <-
|
||||
tryAllErrors (withAgent $ \a -> resolveSimplexName a nm (aUserId user) (claimDomain claim)) >>= \case
|
||||
Right NameRecord {nrSimplexChannel}
|
||||
| nameResolvesTo groupLink nrSimplexChannel -> pure (True, Nothing)
|
||||
| otherwise -> pure (False, Just "the name does not resolve to the link in the group profile")
|
||||
Left (ChatErrorAgent {agentError = SMP _ (NAME SMP.NOT_FOUND)}) -> pure (False, Just "the name is not registered")
|
||||
Left e -> throwError e
|
||||
g' <- withFastStore' $ \db -> setGroupDomainVerified db user g verified
|
||||
pure $ CRGroupDomainVerified user g' reason
|
||||
APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do
|
||||
ct@Contact {profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db cxt user contactId
|
||||
@@ -3144,11 +3154,19 @@ processChatCommand cxt nm = \case
|
||||
gInfo@GroupInfo {groupProfile = p@GroupProfile {publicGroup}} <- withStore $ \db -> getGroupInfo db cxt user gId
|
||||
case publicGroup of
|
||||
Just pg@PublicGroupProfile {groupLink, publicGroupAccess = existingAccess} -> do
|
||||
let domainChanged = (claimDomain <$> newClaim) /= (claimDomain <$> (existingAccess >>= groupDomainClaim))
|
||||
forM_ (claimDomain <$> newClaim) $ \newDomain ->
|
||||
when (Just newDomain /= (claimDomain <$> (existingAccess >>= groupDomainClaim))) $ do
|
||||
when domainChanged $ do
|
||||
NameRecord {nrSimplexChannel} <- withAgent $ \a -> resolveSimplexName a nm (aUserId user) newDomain
|
||||
unless (nameResolvesTo groupLink nrSimplexChannel) $ throwChatError $ CESimplexDomainNotReady newDomain SDENoValidLink
|
||||
runUpdateGroupProfile user gInfo p {publicGroup = Just pg {publicGroupAccess = Just access}}
|
||||
r <- runUpdateGroupProfile user gInfo p {publicGroup = Just pg {publicGroupAccess = Just access}}
|
||||
-- the resolution above proved name <-> link consistency; record it (the profile
|
||||
-- update resets the status on a claim change)
|
||||
case r of
|
||||
CRGroupUpdated {fromGroup, toGroup, member_, msgSigned} | isJust newClaim && domainChanged -> do
|
||||
toGroup' <- withFastStore' $ \db -> setGroupDomainVerified db user toGroup True
|
||||
pure CRGroupUpdated {user, fromGroup, toGroup = toGroup', member_, msgSigned}
|
||||
_ -> pure r
|
||||
Nothing -> throwChatError $ CECommandError "not a public group"
|
||||
APICreateGroupLink groupId mRole -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do
|
||||
gInfo@GroupInfo {groupProfile} <- withFastStore $ \db -> getGroupInfo db cxt user groupId
|
||||
@@ -4322,8 +4340,8 @@ processChatCommand cxt nm = \case
|
||||
-- e.g. an un-upgraded relay dropped the claim); refresh its profile from the fresh link
|
||||
-- data and mark it verified, so the check below passes and future by-name lookups match
|
||||
plan <- case (planDomain, plan0, groupSLinkData_) of
|
||||
(Just _, CPGroupLink (GLPKnown g u o os), Just sLinkData) ->
|
||||
(\(g', _) -> CPGroupLink (GLPKnown g' u o os)) <$> updateGroupFromLinkData user g sLinkData
|
||||
(Just nameDomain, CPGroupLink (GLPKnown g u o os), Just sLinkData) ->
|
||||
(\(g', _) -> CPGroupLink (GLPKnown g' u o os)) <$> updateGroupFromLinkData user g sLinkData (Just nameDomain)
|
||||
_ -> pure plan0
|
||||
forM_ planDomain $ \nameDomain ->
|
||||
let domain_ = (\GroupProfile {publicGroup} -> claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim)) =<< case plan of
|
||||
@@ -4350,7 +4368,7 @@ processChatCommand cxt nm = \case
|
||||
let ov = verifyLinkOwner rk owners l' sig_
|
||||
glOwners = map (\OwnerAuth {ownerId, ownerKey} -> GroupLinkOwner {memberId = MemberId ownerId, memberKey = ownerKey}) owners
|
||||
(g', updated) <- case groupSLinkData_ of
|
||||
Just sLinkData -> updateGroupFromLinkData user g sLinkData
|
||||
Just sLinkData -> updateGroupFromLinkData user g sLinkData Nothing
|
||||
_ -> pure (g, False)
|
||||
pure (con l' (linkConnReq fd), CPGroupLink (GLPKnown g' updated ov (ListDef glOwners)))
|
||||
-- resolve a name to its first contact/channel short link
|
||||
|
||||
@@ -1463,9 +1463,12 @@ updatePublicGroupData user gInfo
|
||||
withStore $ \db -> updatePublicMemberCount db cxt user gInfo
|
||||
| otherwise = pure gInfo
|
||||
|
||||
updateGroupFromLinkData :: User -> GroupInfo -> GroupShortLinkData -> CM (GroupInfo, Bool)
|
||||
updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupDomainVerified, groupSummary = GroupSummary {publicMemberCount = localCount}} GroupShortLinkData {groupProfile, publicGroupData}
|
||||
| profileChanged || countChanged || verifyChanged = do
|
||||
-- resolvedDomain_ is the name the caller just resolved, by explicit user action, to reach
|
||||
-- this link data; a link-data refresh alone (resolvedDomain_ = Nothing) never marks a
|
||||
-- claim verified, and names are never resolved here.
|
||||
updateGroupFromLinkData :: User -> GroupInfo -> GroupShortLinkData -> Maybe SimplexDomain -> CM (GroupInfo, Bool)
|
||||
updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} GroupShortLinkData {groupProfile, publicGroupData} resolvedDomain_
|
||||
| profileChanged || countChanged || verifyResolved = do
|
||||
cxt <- chatStoreCxt
|
||||
withStore $ \db -> do
|
||||
g <- if profileChanged then updateGroupProfile db user gInfo groupProfile else pure gInfo
|
||||
@@ -1473,9 +1476,9 @@ updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupDomainVerif
|
||||
Just PublicGroupData {publicMemberCount} | countChanged ->
|
||||
setPublicMemberCount db cxt user g publicMemberCount
|
||||
_ -> pure g
|
||||
-- the group's own link is authoritative for its domain claim, so a claim in the link profile is
|
||||
-- verified; updateGroupProfile above clears verification on a claim change, so set it afterwards
|
||||
g'' <- if verifyChanged then liftIO $ setGroupDomainVerified db user g' True else pure g'
|
||||
-- the claim is marked verified only when the caller resolved this name to reach this link
|
||||
-- data; updateGroupProfile above clears verification on a claim change, so set it afterwards
|
||||
g'' <- if verifyResolved then liftIO $ setGroupDomainVerified db user g' True else pure g'
|
||||
pure (g'', profileChanged)
|
||||
| otherwise = pure (gInfo, False)
|
||||
where
|
||||
@@ -1485,7 +1488,9 @@ updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupDomainVerif
|
||||
_ -> False
|
||||
groupClaim GroupProfile {publicGroup} = claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim)
|
||||
newClaim = groupClaim groupProfile
|
||||
verifyChanged = isJust newClaim && (groupDomainVerified /= Just True || groupClaim p /= newClaim)
|
||||
-- mark verified whenever the caller resolved exactly this claim (connect-by-name); idempotent,
|
||||
-- so no need to special-case an already-verified status (which updateGroupProfile may just have reset)
|
||||
verifyResolved = isJust resolvedDomain_ && resolvedDomain_ == newClaim
|
||||
|
||||
updateContactFromLinkData :: User -> Contact -> Profile -> CM Contact
|
||||
updateContactFromLinkData user ct@Contact {profile = profile@LocalProfile {contactDomain = prevClaim, contactDomainVerified}} linkProfile@Profile {contactDomain = newClaim}
|
||||
|
||||
@@ -30,7 +30,9 @@ import Simplex.Chat.Options.DB
|
||||
import Simplex.Chat.Protocol (memberSupportVoiceVersion)
|
||||
import Simplex.Chat.Types (ChatPeerType (..), Profile (..))
|
||||
import Simplex.Chat.Types.Shared (GroupMemberRole (..))
|
||||
import Simplex.Messaging.SimplexName (SimplexDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..))
|
||||
import Simplex.Messaging.Version
|
||||
import NameResolver
|
||||
import System.FilePath ((</>))
|
||||
import Test.Hspec hiding (it)
|
||||
|
||||
@@ -100,6 +102,13 @@ directoryServiceTests = do
|
||||
it "should handle re-registration when already listed" testReregistrationAlreadyListed
|
||||
it "should update subscriber count periodically" testLinkCheckUpdatesCount
|
||||
|
||||
-- these run under a names-enabled SMP server (withSmpServerAndNames), so they are a
|
||||
-- separate spec from directoryServiceTests (which runs under a plain SMP server)
|
||||
directoryNameTests :: SpecWith TestParams
|
||||
directoryNameTests = do
|
||||
it "should verify and show a channel's SimpleX name" testDirectoryChannelName
|
||||
it "should mark an inconsistent SimpleX name as not verified" testDirectoryChannelNameNotVerified
|
||||
|
||||
directoryProfile :: Profile
|
||||
directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing, contactDomain = Nothing}
|
||||
|
||||
@@ -2117,6 +2126,89 @@ testRegisterChannelViaCard ps =
|
||||
superUser <# "'SimpleX Directory'> The channel ID 1 (news) is de-listed (channel owner left)."
|
||||
relay <## "#news: 'SimpleX Directory' left the group (signed)"
|
||||
|
||||
-- the owner sets a SimpleX name on a channel; the directory verifies name<->link consistency
|
||||
-- against the resolver and shows the verified name in the admin approval message
|
||||
testDirectoryChannelName :: HasCallStack => TestParams -> IO ()
|
||||
testDirectoryChannelName ps = withSmpServerAndNames $ \reg ->
|
||||
withDirectoryServiceCfg ps testCfg $ \superUser dsLink ->
|
||||
withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob ->
|
||||
withRelay ps $ \relay -> do
|
||||
bob `connectVia` dsLink
|
||||
(shortLink, _fullLink) <- prepareChannel1Relay "news" bob relay
|
||||
registerName reg newsName (channelNameRecord "news" (T.pack shortLink))
|
||||
bob ##> "/public group access #news domain=news.simplex"
|
||||
bob <## "updated public group access: domain=news.simplex"
|
||||
relay <## "bob updated group #news: (signed)"
|
||||
relay <## "updated public group access: domain=news.simplex"
|
||||
bob ##> "/share chat #news @'SimpleX Directory'"
|
||||
bob <# "@'SimpleX Directory' link to join channel #news (signed):"
|
||||
_ <- getTermLine bob -- short link
|
||||
_ <- getTermLine bob -- ownerSig JSON
|
||||
bob <# "'SimpleX Directory'> Joining the channel news…"
|
||||
concurrentlyN_
|
||||
[ do
|
||||
relay <## "'SimpleX Directory': accepting request to join group #news..."
|
||||
relay <## "#news: 'SimpleX Directory' joined the group",
|
||||
bob <## "#news: relay introduced 'SimpleX Directory_1' in the channel"
|
||||
]
|
||||
bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours."
|
||||
bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them."
|
||||
bob <## "Captcha verification is enabled. Use /'filter 1' to change it."
|
||||
-- the directory verified the name against the channel link and shows it to the admin
|
||||
superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:"
|
||||
superUser <## "news"
|
||||
superUser <## "SimpleX name: #news"
|
||||
superUser <##. "Link to join channel: "
|
||||
superUser <## "You need SimpleX Chat app v6.5 to join."
|
||||
superUser <## "1 subscribers"
|
||||
superUser <## ""
|
||||
superUser <## "To approve send:"
|
||||
superUser <# "'SimpleX Directory'> /approve 1:news 1"
|
||||
where
|
||||
newsName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "news" [])
|
||||
|
||||
-- after the owner set the name, the registry entry is re-pointed to a different link; the
|
||||
-- directory's verification fails, so the admin sees the name marked as not verified
|
||||
testDirectoryChannelNameNotVerified :: HasCallStack => TestParams -> IO ()
|
||||
testDirectoryChannelNameNotVerified ps = withSmpServerAndNames $ \reg ->
|
||||
withDirectoryServiceCfg ps testCfg $ \superUser dsLink ->
|
||||
withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob ->
|
||||
withRelay ps $ \relay -> do
|
||||
bob `connectVia` dsLink
|
||||
(shortLink, _fullLink) <- prepareChannel1Relay "news" bob relay
|
||||
registerName reg newsName (channelNameRecord "news" (T.pack shortLink))
|
||||
bob ##> "/public group access #news domain=news.simplex"
|
||||
bob <## "updated public group access: domain=news.simplex"
|
||||
relay <## "bob updated group #news: (signed)"
|
||||
relay <## "updated public group access: domain=news.simplex"
|
||||
-- the name is re-pointed to a different link after the owner set it
|
||||
registerName reg newsName (channelNameRecord "news" "https://simplex.chat/other")
|
||||
bob ##> "/share chat #news @'SimpleX Directory'"
|
||||
bob <# "@'SimpleX Directory' link to join channel #news (signed):"
|
||||
_ <- getTermLine bob -- short link
|
||||
_ <- getTermLine bob -- ownerSig JSON
|
||||
bob <# "'SimpleX Directory'> Joining the channel news…"
|
||||
concurrentlyN_
|
||||
[ do
|
||||
relay <## "'SimpleX Directory': accepting request to join group #news..."
|
||||
relay <## "#news: 'SimpleX Directory' joined the group",
|
||||
bob <## "#news: relay introduced 'SimpleX Directory_1' in the channel"
|
||||
]
|
||||
bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours."
|
||||
bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them."
|
||||
bob <## "Captcha verification is enabled. Use /'filter 1' to change it."
|
||||
superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:"
|
||||
superUser <## "news"
|
||||
superUser <## "SimpleX name: #news (NOT verified - will not be shown)"
|
||||
superUser <##. "Link to join channel: "
|
||||
superUser <## "You need SimpleX Chat app v6.5 to join."
|
||||
superUser <## "1 subscribers"
|
||||
superUser <## ""
|
||||
superUser <## "To approve send:"
|
||||
superUser <# "'SimpleX Directory'> /approve 1:news 1"
|
||||
where
|
||||
newsName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "news" [])
|
||||
|
||||
testLinkAsTextSearch :: HasCallStack => TestParams -> IO ()
|
||||
testLinkAsTextSearch ps =
|
||||
withDirectoryServiceCfg ps testCfg $ \_superUser dsLink ->
|
||||
|
||||
@@ -5,7 +5,7 @@ module ChatTests.Names where
|
||||
|
||||
import ChatClient
|
||||
import ChatTests.DBUtils
|
||||
import ChatTests.Groups (prepareChannel1Relay)
|
||||
import ChatTests.Groups (memberJoinChannel, prepareChannel1Relay)
|
||||
import ChatTests.Utils
|
||||
import Control.Concurrent.Async (concurrently_)
|
||||
import qualified Data.Text as T
|
||||
@@ -20,6 +20,8 @@ chatNamesTests = do
|
||||
it "connect by name to a known contact not claimed in profile is rejected" testConnectByNameKnownContactNotClaimed
|
||||
it "connect by unregistered name fails to resolve" testConnectByNameNotFound
|
||||
it "set name not resolving to own address is rejected" testSetNameNotOwnAddress
|
||||
it "channel name is not verified just by joining via link" testChannelDomainLinkJoinUnverified
|
||||
it "verify channel name, fail on re-point, retain status on refresh" testChannelDomainVerify
|
||||
it "connect by channel name" testConnectByChannelName
|
||||
it "connect by name resolving to channel (primary) and direct contact" testConnectByNameChannelAndContact
|
||||
it "connect by name resolving to direct contact (primary) and channel" testConnectByNameContactAndChannel
|
||||
@@ -115,6 +117,59 @@ testSetNameNotOwnAddress ps = withSmpServerAndNames $ \reg ->
|
||||
alice ##> "/_set domain 1 alice.simplex"
|
||||
alice <## "SimpleX name alice.simplex has no valid connection link"
|
||||
|
||||
-- A member who joined via link does not get the claimed name verified, even after a link-data
|
||||
-- refresh - the group's self-claim is not proof of name ownership.
|
||||
testChannelDomainLinkJoinUnverified :: HasCallStack => TestParams -> IO ()
|
||||
testChannelDomainLinkJoinUnverified ps = withSmpServerAndNames $ \reg ->
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath ->
|
||||
withNewTestChat ps "bob" bobProfile $ \bob -> do
|
||||
(shortLink, fullLink) <- prepareChannel1Relay "team" alice cath
|
||||
registerName reg teamName (channelNameRecord "team" (T.pack shortLink))
|
||||
alice ##> "/public group access #team domain=team.simplex"
|
||||
alice <## "updated public group access: domain=team.simplex"
|
||||
cath <## "alice updated group #team: (signed)"
|
||||
cath <## "updated public group access: domain=team.simplex"
|
||||
memberJoinChannel "team" [cath] [alice] shortLink fullLink bob
|
||||
-- a link-data refresh must not mark the self-claimed name verified
|
||||
bob ##> ("/_connect plan 1 " <> shortLink <> " resolve=allGroups")
|
||||
bob <## "group link: known group #team"
|
||||
bob <## "use #team <message> to send messages" -- no "SimpleX name" line: status stays unknown
|
||||
where
|
||||
teamName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "team" [])
|
||||
|
||||
-- A2 (verify by name<->profile-link consistency), A3 (owner verified on set), and A1 (a failed
|
||||
-- status is retained across a link-data refresh, not erased to verified).
|
||||
testChannelDomainVerify :: HasCallStack => TestParams -> IO ()
|
||||
testChannelDomainVerify ps = withSmpServerAndNames $ \reg ->
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath ->
|
||||
withNewTestChat ps "bob" bobProfile $ \bob -> do
|
||||
(shortLink, fullLink) <- prepareChannel1Relay "team" alice cath
|
||||
registerName reg teamName (channelNameRecord "team" (T.pack shortLink))
|
||||
alice ##> "/public group access #team domain=team.simplex"
|
||||
alice <## "updated public group access: domain=team.simplex"
|
||||
cath <## "alice updated group #team: (signed)"
|
||||
cath <## "updated public group access: domain=team.simplex"
|
||||
-- A3: setting the name resolved it, so the owner's channel is verified
|
||||
alice ##> "/_verify domain #1"
|
||||
alice <## "SimpleX name #team verified"
|
||||
memberJoinChannel "team" [cath] [alice] shortLink fullLink bob
|
||||
-- A2: resolving the name leads to the profile's link
|
||||
bob ##> "/_verify domain #1"
|
||||
bob <## "SimpleX name #team verified"
|
||||
-- the name is re-pointed to a different link: verification fails
|
||||
registerName reg teamName (channelNameRecord "team" "https://simplex.chat/other")
|
||||
bob ##> "/_verify domain #1"
|
||||
bob <## "SimpleX name #team not verified: the name does not resolve to the link in the group profile"
|
||||
-- A1: a link-data refresh keeps the failed status (it does not overwrite it with verified)
|
||||
bob ##> ("/_connect plan 1 " <> shortLink <> " resolve=allGroups")
|
||||
bob <## "group link: known group #team"
|
||||
bob <## "SimpleX name: #team (verification failed)"
|
||||
bob <## "use #team <message> to send messages"
|
||||
where
|
||||
teamName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "team" [])
|
||||
|
||||
testConnectByChannelName :: HasCallStack => TestParams -> IO ()
|
||||
testConnectByChannelName ps = withSmpServerAndNames $ \reg ->
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
|
||||
@@ -74,6 +74,7 @@ main = do
|
||||
describe "Random servers" randomServersTests
|
||||
#if !defined(dbPostgres)
|
||||
around (tmpTestBracket chatQueryStats agentQueryStats) $ describe "names tests" chatNamesTests
|
||||
around (tmpTestBracket chatQueryStats agentQueryStats) $ xdescribe'' "SimpleX Directory names" directoryNameTests
|
||||
#endif
|
||||
#if defined(dbPostgres)
|
||||
createdDropDb . around testBracket
|
||||
|
||||
@@ -107,10 +107,25 @@ function filterEntries(mode, s) {
|
||||
|| (entry.displayName || '').toLowerCase().includes(query)
|
||||
|| includesQuery(entry.shortDescr, query)
|
||||
|| includesQuery(entry.welcomeMessage, query)
|
||||
|| simplexNameIncludesQuery(entry.simplexName, query)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// matches '#team', 'team' and 'team.simplex' against an entry displaying '#team'
|
||||
function simplexNameIncludesQuery(name, query) {
|
||||
if (!name) return false;
|
||||
const q = normalizeSimplexName(query);
|
||||
return q !== '' && normalizeSimplexName(name).includes(q);
|
||||
}
|
||||
|
||||
function normalizeSimplexName(s) {
|
||||
s = s.toLowerCase();
|
||||
if (s.startsWith('#') || s.startsWith('@')) s = s.slice(1);
|
||||
if (s.endsWith('.simplex')) s = s.slice(0, -'.simplex'.length);
|
||||
return s;
|
||||
}
|
||||
|
||||
function includesQuery(field, query) {
|
||||
return field
|
||||
&& Array.isArray(field)
|
||||
@@ -215,7 +230,7 @@ function displayEntries(entries) {
|
||||
|
||||
for (let entry of entries) {
|
||||
try {
|
||||
const { entryType, displayName, groupLink, shortDescr, welcomeMessage, imageFile } = entry;
|
||||
const { entryType, displayName, simplexName, groupLink, shortDescr, welcomeMessage, imageFile } = entry;
|
||||
const entryDiv = document.createElement('div');
|
||||
entryDiv.className = 'entry w-full flex flex-col items-start md:flex-row rounded-[4px] overflow-hidden shadow-[0px_20px_30px_rgba(0,0,0,0.12)] dark:shadow-none bg-white dark:bg-[#0B2A59] mb-8';
|
||||
|
||||
@@ -227,6 +242,13 @@ function displayEntries(entries) {
|
||||
nameElement.className = 'text-grey-black dark:text-white !text-lg md:!text-xl font-bold';
|
||||
textContainer.appendChild(nameElement);
|
||||
|
||||
if (simplexName) {
|
||||
const simplexNameElement = document.createElement('p');
|
||||
simplexNameElement.textContent = simplexName;
|
||||
simplexNameElement.className = 'text-sm font-medium';
|
||||
textContainer.appendChild(simplexNameElement);
|
||||
}
|
||||
|
||||
const welcomeMessageHTML = welcomeMessage ? renderMarkdown(welcomeMessage) : undefined;
|
||||
const shortDescrHTML = shortDescr ? renderMarkdown(shortDescr) : undefined;
|
||||
if (shortDescrHTML && welcomeMessageHTML?.includes(shortDescrHTML) !== true) {
|
||||
|
||||
Reference in New Issue
Block a user