fix(crew): stop overclaiming about public relays in the coordinator copy

The transport treats a crew's wss:// coordinator URL AS A RELAY, and an https://
one falls back to the public wss://relay.contextvm.org. So 'nothing crew-specific
hits a public relay' was false for https coordinators — it put ciphertext +
metadata (crew coord, timing) on a public relay.

- CrewCoordinatorCard: shows the actual relay in use, and states honestly
  whether it's the crew's own or the shared public one.
- CrewForm: the coordinator field now says a wss:// endpoint is the crew's own
  relay and an https:// one routes over the shared public relay.
- AGENTS.md: record the relay-vs-coordinator transport reality so this isn't
  re-derived (and Cordn RPC is not called).
This commit is contained in:
SkateSpots 2026-09-29 16:48:17 +07:00
parent c14b19aee1
commit 322ec58ada
3 changed files with 38 additions and 9 deletions

View file

@ -616,9 +616,17 @@ session with **no other memory of this project**. So:
Cordn's phone app (an nprofile, not a URL) was un-enterable — `CrewForm` now
uses the shared `parseCrewCoordinatorInput` / `crewCoordinatorInputValue`
(`lib/coordinator.ts`) and has a **"Use my coordinator"** button that prefills
from Settings → Coordinator. **Still future:** real MLS
(`ts-mls`) instead of group-key crypto. **Lesson: grep `src` for the feature
before building it — this was nearly duplicated.**
from Settings → Coordinator. ⚠️ **Transport reality (do not overclaim):** a
crew's **`wss://` coordinator URL is used AS A RELAY** (`resolveCordnEndpoint`
→ `relayUrl`); an `https://` one falls back to the **public**
`wss://relay.contextvm.org`. The app publishes sealed kind-4497 events and
queries them back — it does **not** call Cordn's coordinator RPC
(`FetchGroupMessages`/`SubscribeGroupMessages`), so a real Cordn coordinator is
not yet engaged, and an `https://` coordinator puts ciphertext + metadata
(crew coord, timing) on a public relay. `CrewCoordinatorCard` + `CrewForm` now
say this honestly (2026-09-29). **Still future:** real MLS
(`ts-mls`) instead of group-key crypto, plus Cordn RPC interop. **Lesson: grep
`src` for the feature before building it — this was nearly duplicated.**
- **Empty-map nudge (2026-09-28):** `MapPage` distinguishes the two empties — the
existing "No spots match" card is for *filters* (`filteredSpots.length === 0 &&
allLocations.length > 0`) and a new "No spots mapped yet — be the first" card

View file

@ -1086,7 +1086,7 @@ export function CrewForm({ existing }: CrewFormProps) {
const coordinatorBlock = (
<div className="space-y-2 rounded-lg border border-gray-700 bg-gray-900/60 p-4">
<Label htmlFor="crew-coordinator" className="flex items-center gap-2 text-gray-300">
<Server className="h-4 w-4" /> Custom Chat Coordinator <span className="font-normal text-gray-500">· experimental · self-host</span>
<Server className="h-4 w-4" /> Custom Chat Coordinator <span className="font-normal text-gray-500">· experimental · your own relay</span>
</Label>
<p className="text-xs text-gray-500">
<span className="font-semibold text-gray-300">You don't need this.</span> Your crew already has private,
@ -1101,7 +1101,10 @@ export function CrewForm({ existing }: CrewFormProps) {
Cordn coordinator
</a>{' '}
— even on an old phone. Paste its <span className="font-mono">nprofile</span>, a cordn.net link, or an
endpoint URL. Leave blank to use the built-in chat.
endpoint URL. A <span className="font-mono">wss://</span> endpoint is used as the crew's own chat relay
(point it at a private one to keep even the metadata off public relays); an{' '}
<span className="font-mono">https://</span> coordinator is addressed by its pubkey over the shared
ContextVM relay. Leave blank to use the built-in chat.
</p>
<Input
id="crew-coordinator"

View file

@ -5,6 +5,8 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import { sanitizeUrl, urlDomain } from '@/lib/sanitizeUrl';
import { resolveCordnEndpoint } from '@/lib/cordn/endpoint';
import { CORDN_RELAY_URL } from '@/lib/coordinator';
import type { CrewCoordinator, SkateCrew } from '@/lib/types';
import { nip19 } from 'nostr-tools';
@ -30,6 +32,12 @@ export function CrewCoordinatorCard({
}) {
const { coordinator } = crew;
// Which relay the messages actually travel over — a `wss://` coordinator URL
// IS the relay; an `https://` one is addressed by pubkey over the shared
// ContextVM relay (public). The copy below must not overclaim either way.
const endpoint = resolveCordnEndpoint(crew);
const overPublicRelay = endpoint?.relayUrl === CORDN_RELAY_URL;
if (!coordinator) {
// Default state: private chat is ALREADY on for every crew (built-in NIP-17
// giftwrap group chat, no setup). We say so plainly rather than implying a
@ -92,15 +100,25 @@ export function CrewCoordinatorCard({
{coordinator.operator && <CoordinatorOperator pubkey={coordinator.operator} />}
{endpoint && (
<p className="text-[11px] leading-relaxed text-gray-500">
Delivered over{' '}
<span className="font-mono text-gray-400">{urlDomain(endpoint.relayUrl) || endpoint.relayUrl}</span>
{overPublicRelay ? ' — the shared ContextVM relay.' : " — your crew's own relay."}
</p>
)}
<div className="rounded-xl border border-emerald-500/15 bg-emerald-500/[0.04] p-3">
<p className="flex items-center gap-1.5 text-xs font-bold text-emerald-300">
<Lock className="h-3.5 w-3.5" /> Private chat is live
</p>
<p className="mt-1 text-[11px] leading-relaxed text-gray-500">
The <span className="font-semibold text-gray-300">Chat</span> tab runs through this
coordinator: messages are end-to-end encrypted (NIP-44, sealed to each member) and routed
over ContextVM, so the coordinator only ever handles sealed traffic and nothing
crew-specific hits a public relay.
The <span className="font-semibold text-gray-300">Chat</span> tab routes through this
coordinator: messages are end-to-end encrypted (NIP-44, sealed to each member), so it only
ever handles sealed traffic.{' '}
{overPublicRelay
? 'Heads up: it is addressed over the shared ContextVM relay, so the ciphertext and its metadata (which crew, and when) are publicly visible — just not readable. Point the crew at a private wss:// relay to keep even that private.'
: "Nothing crew-specific leaves your crew's own relay."}
</p>
</div>