Crew chat: frame built-in E2E chat as the default, Cordn as honest opt-in
Decision (per Marmot/Concord/Cordn analysis): a semi-public, socially-verified skate crew fits Concord-style shared-key chat, not MLS. The built-in NIP-17 giftwrap group chat is already the right answer and works with zero setup — so stop implying a coordinator is required and stop over-claiming MLS guarantees the code doesn't yet provide. - CrewCoordinatorCard: default state now reassures that private E2E chat is already ON (no setup) for everyone, instead of prompting founders to "set up a coordinator". Founders get a small, honestly-labeled advanced link to the self-hosted Cordn option. Fixed the "E2E · MLS / forward secrecy" badge — the coordinator path is NIP-44 group encryption routed over ContextVM, not an MLS ratchet. - CrewForm: the coordinator field is now clearly "experimental · self-host", states you don't need it, and links to the Cordn repo. - NIP.md: reworded the crew-chat section + tag/profile descriptions to match the honest crypto seam (NIP-44 group today; MLS a future upgrade), so docs no longer claim forward secrecy that isn't shipped. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
e698e4880f
commit
739469aa1d
3 changed files with 91 additions and 55 deletions
51
NIP.md
51
NIP.md
|
|
@ -14,7 +14,7 @@ other clients, and preserved on edit:
|
|||
| `noffer` | string | CLINK Offer code (`noffer1…`) — self-custodial Bitcoin tip code. |
|
||||
| `bitcoin_silent_payment` | string | Bitcoin Silent Payment address (BIP-352, `sp1…`) — a reusable, private on-chain address. |
|
||||
| `sponsors` | string | Freeform highlight of the skater's sponsors / supporters (brands, shops, people). |
|
||||
| `cordn` | string | A Cordn (cordn.net) contact URL for private, MLS-encrypted messaging. |
|
||||
| `cordn` | string | A Cordn (cordn.net) contact URL for private, end-to-end encrypted messaging via a self-hosted coordinator. |
|
||||
|
||||
`lud16` (Lightning address) is standard NIP-01 but is also editable directly in
|
||||
the SkateSpots profile form for convenience.
|
||||
|
|
@ -2128,7 +2128,7 @@ bare `p` tag on the crew is only a claim until the member self-labels.
|
|||
| `goal` | Optional **crew treasury** — the event id of a NIP-75 Zap Goal (kind `9041`) the crew is raising funds for. See Crew Treasury below. |
|
||||
| `looking-for` | Optional **open call** — a Lab craft the crew is hiring for. Repeatable. Slugs: `filmer`, `photographer`, `editor`, `coach`, `builder`, `artist`, `musician`, `members`. Surfaces the crew's needs so matching Lab providers can reach out. |
|
||||
| `sponsor` | Optional **sponsoring shop** — a NIP-15 stall coordinate (`30017:pubkey:d`) for a local skate shop that backs the crew. Renders a "Sponsored by [Shop]" banner linking to the shop. Only shown when the shop resolves. |
|
||||
| `coordinator` | Optional **encrypted-chat coordinator** for the crew's private group. Shape: `["coordinator", <url>, <coordinatorPubkey?>, <operatorPubkey?>]`. `<url>` is an `http(s)`/`wss` endpoint of a **Cordn** coordinator (an MLS / RFC 9420 Delivery Service reached over ContextVM). The crew event stays public and discoverable; message content, membership secrets and MLS ratchets live client-side. Nothing crew-specific is published to public relays. `<coordinatorPubkey>` is the coordinator service pubkey (hex) for ContextVM addressing; `<operatorPubkey>` is the human (hex) who runs it, if delegated. |
|
||||
| `coordinator` | Optional, **advanced/self-host**: routes the crew's private chat through a founder-run **Cordn** coordinator instead of the built-in NIP-17 chat. Shape: `["coordinator", <url>, <coordinatorPubkey?>, <operatorPubkey?>]`. `<url>` is an `http(s)`/`wss` endpoint of a Cordn coordinator (a Delivery Service reached over ContextVM). The crew event stays public and discoverable; message content and keys live client-side; nothing crew-specific is published to public relays. `<coordinatorPubkey>` is the coordinator service pubkey (hex) for ContextVM addressing; `<operatorPubkey>` is the human (hex) who runs it, if delegated. Not required — every crew has E2E chat without it. |
|
||||
| `video` | Optional **crew video / montage** ("The Part") — a **YouTube** or **Instagram** URL, rendered as the crew's headline embed. Only these two sources are accepted (skate parts live on YouTube, clips on Instagram); any other URL is ignored. No native/self-hosted video. Clients must parse it to a known embed (YouTube video id / Instagram shortcode) and reject anything else rather than render an untrusted iframe. |
|
||||
| `crew-role` | Optional **skate role** assigned to a roster member. Shape: `["crew-role", <memberPubkey>, <role>]`, repeatable (a member may hold several). `<role>` ∈ the full Lab-craft set — `filmer`, `photographer`, `editor`, `coach`, `builder`, `artist`, `graffiti`, `tattoo`, `trainer`, `musician`, `judge` — plus the crew-specific `manager` and `og`. The Lab-craft roles mirror the Lab profession kinds (35001–35012), so a crew's "filmer" speaks the same language as a Lab Filmer profile. These are the *functions* on a skate team (the filmer is as vital as the skaters) and are purely a **flex/label** — they grant **no powers** (only the `admin` marker does). Only honoured for the founder or a listed member; roles on non-roster pubkeys are ignored. |
|
||||
|
||||
|
|
@ -2469,31 +2469,34 @@ by event id).
|
|||
#### Private Crew Chat (Cordn / MLS) — the `coordinator` tag
|
||||
|
||||
A crew's **public** identity (roster, spots, events, treasury, sponsor) lives on
|
||||
Nostr as the kind-`35006` event above. A crew's **private** group chat is designed
|
||||
to run over **Cordn** — an MLS ([RFC 9420](https://datatracker.ietf.org/doc/rfc9420/))
|
||||
Delivery Service coordinator, **one per group**, reached over ContextVM.
|
||||
Nostr as the kind-`35006` event above.
|
||||
|
||||
A crew advertises its coordinator with the optional `coordinator` tag (see the
|
||||
Optional Tags table). This is intentionally just a **pointer**:
|
||||
**A crew's private group chat is end-to-end encrypted and on by default** — no
|
||||
setup. The built-in transport is **NIP-17 giftwrap group DM**: every message is
|
||||
sealed to each member's key and published as gift wraps, so only the founder and
|
||||
listed members can read it and relays see only ciphertext. This is the right fit
|
||||
for a semi-public, socially-verified skate crew (cf. Concord's shared-key model
|
||||
for "large casual rooms").
|
||||
|
||||
- **On relays:** only the public crew event and a coordinator URL. No
|
||||
protocol-specific messaging events (no per-message kinds) are ever published to
|
||||
public relays, so there is nothing crew-specific to fingerprint or cluster.
|
||||
- **Client-side / under MLS:** message content, the MLS member tree, epoch keys
|
||||
and ratchets. Forward secrecy and post-compromise security hold; membership
|
||||
changes are O(log n).
|
||||
**Optionally**, a technical founder who **self-hosts a
|
||||
[Cordn](https://github.com/Cordn-msg/cordn) coordinator** (an MLS
|
||||
[RFC 9420](https://datatracker.ietf.org/doc/rfc9420/) Delivery Service reached
|
||||
over ContextVM, one per group) can advertise it with the optional `coordinator`
|
||||
tag (see the Optional Tags table) to route chat through it instead. This is a
|
||||
**pointer, not a requirement**:
|
||||
|
||||
- **On relays:** only the public crew event and a coordinator URL. With the
|
||||
coordinator path, no per-message events hit public relays at all, so there is
|
||||
nothing crew-specific to fingerprint or cluster.
|
||||
- **The coordinator** routes sealed ContextVM traffic and never sees message
|
||||
content, client IPs, or (for routine sends using ephemeral transport keys) who
|
||||
is talking. If it is unreachable, chat delivery stalls but the group, its
|
||||
membership and its history on members' devices survive; groups can migrate
|
||||
coordinators (per-group cursors preserve ordering).
|
||||
|
||||
Cordn is chosen over Marmot (fully decentralised MLS) and Concord (shared-key,
|
||||
no forward secrecy) because its **coordinator-per-group** model mirrors a crew's
|
||||
**leader / coordinator** structure, it is browser/TypeScript-native, and it keeps
|
||||
the private chat un-fingerprintable next to an already-public, geolocated crew
|
||||
identity. Until a crew configures a coordinator, the built-in NIP-17 group-DM
|
||||
chat remains the fallback.
|
||||
content or client IPs. If it is unreachable, chat delivery stalls but the
|
||||
group, its membership and its history on members' devices survive; groups can
|
||||
migrate coordinators (per-group cursors preserve ordering).
|
||||
- **Crypto status (honest):** the coordinator path currently uses the same
|
||||
**NIP-44 group encryption** as the built-in chat (see the envelope's crypto
|
||||
seam below) — real E2E confidentiality, but **not yet** an MLS ratchet, so
|
||||
forward secrecy / post-compromise security are a future upgrade, not a present
|
||||
claim.
|
||||
|
||||
##### Message envelope (kind `4497`) & delivery
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ import {
|
|||
import { useCrewGoal } from '@/hooks/useCrewGoal';
|
||||
import { useAppContext } from '@/hooks/useAppContext';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Users, Upload, X, Loader2, MapPin, Plus, ShieldCheck, AtSign, HardHat, Camera, GraduationCap, Sparkles, Target, Megaphone, Music, Palette, Dumbbell, Eye, Store, Lock, Film, Check, ArrowRight, ArrowLeft as ArrowLeftIcon } from 'lucide-react';
|
||||
import { Users, Upload, X, Loader2, MapPin, Plus, ShieldCheck, AtSign, HardHat, Camera, GraduationCap, Sparkles, Target, Megaphone, Music, Palette, Dumbbell, Eye, Store, Lock, Film, Check, ArrowRight, ArrowLeft as ArrowLeftIcon, Server } from 'lucide-react';
|
||||
|
||||
const FOCUS_ORDER: CrewFocus[] = ['mixed', 'builder', 'photo', 'coaching', 'art', 'music', 'wellness'];
|
||||
const FOCUS_ICONS: Record<CrewFocus, typeof Users> = {
|
||||
|
|
@ -1082,14 +1082,24 @@ export function CrewForm({ existing }: CrewFormProps) {
|
|||
);
|
||||
|
||||
const coordinatorBlock = (
|
||||
<div className="space-y-2 rounded-lg border border-emerald-500/30 bg-emerald-500/[0.04] p-4">
|
||||
<Label htmlFor="crew-coordinator" className="flex items-center gap-2 text-emerald-400">
|
||||
<Lock className="h-4 w-4" /> Private Chat Coordinator <span className="font-normal text-gray-500">· advanced</span>
|
||||
<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>
|
||||
</Label>
|
||||
<p className="text-xs text-gray-500">
|
||||
Most crews can skip this. If you run a <span className="font-semibold text-emerald-300">Cordn</span> coordinator
|
||||
(an MLS delivery service reached over ContextVM), paste its URL to enable end-to-end encrypted group chat —
|
||||
nothing crew-specific ever hits a public relay. Leave blank and your crew still gets a private group chat.
|
||||
<span className="font-semibold text-gray-300">You don't need this.</span> Your crew already has private,
|
||||
end-to-end encrypted group chat built in — no setup. This field is only for crews who{' '}
|
||||
<span className="font-semibold text-gray-300">run their own</span>{' '}
|
||||
<a
|
||||
href="https://github.com/Cordn-msg/cordn"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-teal-400 underline underline-offset-2 hover:text-teal-300"
|
||||
>
|
||||
Cordn coordinator
|
||||
</a>{' '}
|
||||
(a small self-hosted MLS delivery service). Paste its URL to route chat through it instead. Leave blank
|
||||
to use the built-in chat.
|
||||
</p>
|
||||
<Input
|
||||
id="crew-coordinator"
|
||||
|
|
|
|||
|
|
@ -11,11 +11,15 @@ import { nip19 } from 'nostr-tools';
|
|||
/**
|
||||
* Shows the crew's encrypted-chat coordinator (Cordn / MLS Delivery Service).
|
||||
*
|
||||
* A crew's PUBLIC identity lives on Nostr; its PRIVATE group chat is designed to
|
||||
* run over Cordn — an MLS (RFC 9420) coordinator, one per group, reached over
|
||||
* ContextVM. This card surfaces which coordinator the crew uses, explains what
|
||||
* the coordinator can and cannot see, and (for the leader) prompts to set one
|
||||
* when none is configured.
|
||||
* Every crew already has built-in end-to-end encrypted group chat (NIP-17
|
||||
* giftwrap to the roster) — no setup required. This card:
|
||||
* - by default, reassures the crew that private chat is already ON; and
|
||||
* - if a founder has opted into routing chat through their own self-hosted
|
||||
* Cordn coordinator, surfaces which coordinator is in use.
|
||||
*
|
||||
* Note: the coordinator path currently provides NIP-44 group encryption routed
|
||||
* over ContextVM (nothing crew-specific hits a public relay). It is NOT yet a
|
||||
* full MLS ratchet, so we deliberately avoid claiming forward secrecy here.
|
||||
*/
|
||||
export function CrewCoordinatorCard({
|
||||
crew,
|
||||
|
|
@ -27,28 +31,47 @@ export function CrewCoordinatorCard({
|
|||
const { coordinator } = crew;
|
||||
|
||||
if (!coordinator) {
|
||||
// Only the leader sees the "set it up" prompt; others see nothing (keeps the
|
||||
// page clean for crews that just use the built-in feed chat for now).
|
||||
if (!isFounder) return null;
|
||||
// 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
|
||||
// coordinator is required. Founders get a small, honestly-labeled link to
|
||||
// the experimental self-hosted-coordinator option.
|
||||
return (
|
||||
<Card className="border-2 border-dashed border-emerald-500/25 bg-gradient-to-br from-emerald-500/[0.05] to-transparent">
|
||||
<Card className="border-2 border-teal-500/25 bg-gradient-to-br from-teal-500/[0.06] to-black/90">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-black uppercase text-white">
|
||||
<Lock className="h-5 w-5 text-emerald-400" /> Private Crew Chat
|
||||
<Lock className="h-5 w-5 text-teal-400" /> Private Crew Chat
|
||||
<span className="ml-auto inline-flex items-center gap-1 rounded-full bg-teal-500/20 px-2 py-0.5 text-[10px] font-black uppercase tracking-wide text-teal-300">
|
||||
<ShieldCheck className="h-3 w-3" /> On
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-sm leading-relaxed text-gray-400">
|
||||
Turn on <span className="font-bold text-emerald-300">end-to-end encrypted</span> group chat
|
||||
for your crew, powered by an MLS coordinator (Cordn). Forward secrecy, post-compromise
|
||||
security, and nothing about your chat ever hits a public relay.
|
||||
The <span className="font-semibold text-gray-200">Chat</span> tab is{' '}
|
||||
<span className="font-bold text-teal-300">end-to-end encrypted</span> for the founder and all
|
||||
listed members — nothing to set up. Messages are sealed to each member's key (NIP-17), so only
|
||||
your crew can read them.
|
||||
</p>
|
||||
<Link
|
||||
to={`/edit-crew/${crew.pubkey}/${crew.identifier}`}
|
||||
className="inline-flex items-center gap-1.5 rounded-xl border border-emerald-500/40 bg-emerald-500/10 px-3 py-2 text-sm font-bold text-emerald-300 transition-colors hover:bg-emerald-500/20"
|
||||
>
|
||||
<Settings className="h-4 w-4" /> Set up a coordinator
|
||||
</Link>
|
||||
{isFounder && (
|
||||
<p className="text-[11px] leading-relaxed text-gray-600">
|
||||
Advanced: run your own{' '}
|
||||
<a
|
||||
href="https://github.com/Cordn-msg/cordn"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-400 underline underline-offset-2 hover:text-teal-300"
|
||||
>
|
||||
Cordn coordinator
|
||||
</a>
|
||||
?{' '}
|
||||
<Link
|
||||
to={`/edit-crew/${crew.pubkey}/${crew.identifier}`}
|
||||
className="inline-flex items-center gap-1 font-semibold text-gray-400 hover:text-teal-300"
|
||||
>
|
||||
<Settings className="h-3 w-3" /> Route chat through it
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
|
@ -60,7 +83,7 @@ export function CrewCoordinatorCard({
|
|||
<CardTitle className="flex items-center gap-2 text-lg font-black uppercase text-white">
|
||||
<Lock className="h-5 w-5 text-emerald-400" /> Private Crew Chat
|
||||
<span className="ml-auto inline-flex items-center gap-1 rounded-full bg-emerald-500/20 px-2 py-0.5 text-[10px] font-black uppercase tracking-wide text-emerald-300">
|
||||
<ShieldCheck className="h-3 w-3" /> E2E · MLS
|
||||
<Server className="h-3 w-3" /> Coordinator
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
@ -74,10 +97,10 @@ export function CrewCoordinatorCard({
|
|||
<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 now runs through this
|
||||
coordinator: messages are end-to-end encrypted and routed over ContextVM — the
|
||||
coordinator only ever handles sealed traffic, and nothing crew-specific is published to
|
||||
public relays.
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue