From 8eaf1523f2451497fe88da2aa4030573a40d466e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 12 Jul 2026 09:47:10 +0100 Subject: [PATCH] remove plan --- plans/2026-07-10-directory-domain-names.md | 465 --------------------- 1 file changed, 465 deletions(-) delete mode 100644 plans/2026-07-10-directory-domain-names.md diff --git a/plans/2026-07-10-directory-domain-names.md b/plans/2026-07-10-directory-domain-names.md deleted file mode 100644 index 1f43f1c15a..0000000000 --- a/plans/2026-07-10-directory-domain-names.md +++ /dev/null @@ -1,465 +0,0 @@ -# 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 #`: 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.