feat(cordn): embedded coordinator on the relay, crews default to it, disclosure

Newlay runs its embedded Cordn coordinator (identity cordn.key, uid 10001). SKATESPOTS_COORDINATOR is the provided default: Settings -> Coordinator shows a 'Use SkateSpots coordinator' block and CrewForm prefills it for new crews; BYO coordinator stays client-side. Disclosure copy (coordinators see ciphertext + metadata, never contents) in socialUi.coordinator.ourDefault* + existing privacyNote.
This commit is contained in:
SkateSpots 2026-10-12 16:36:56 +07:00
parent b0c4684b4a
commit a8e411faa0
6 changed files with 45 additions and 2 deletions

View file

@ -2123,6 +2123,18 @@ The community relay moved off the hosted relay.tools instance
(`lib/imageFallback.ts`) includes it so reads fall back across all three.
Verified end-to-end (signed PUT /upload → 201, GET /<sha> → 200). To fully
self-host media later, drop the two public hosts from both lists.
- **Cordn coordinator (2026-10-12):** the relay also runs Newlay's **embedded Cordn
coordinator** for crew MLS chat (`[cordn] enabled`, `mode = "embedded"`,
`data_dir = /var/lib/newlay/data/cordn` inside the volume, `relay_url =
ws://127.0.0.1:7777`). Its identity is `cordn.key` (mounted read-only) — pubkey
`02816a05…` — ⚠️ the file must be owned by the container's `newlay` user
(uid 10001, `chown 10001:10001`), else the relay crashes on
`/etc/newlay/cordn.key: Permission denied`. BYO coordinator stays client-side.
In the app: `SKATESPOTS_COORDINATOR` (`src/lib/coordinator.ts`) is the provided
default; **Settings → Coordinator** shows a "Use SkateSpots coordinator" block
and **CrewForm** prefills it for new crews. Disclosure copy lives in
`socialUi.coordinator.ourDefault*` + the existing `privacyNote` (coordinators see
ciphertext + metadata, never contents).
- **Cutover (2026-10-12):** `SKATE_RELAY` (`src/lib/types.ts`) → the new relay; the
default pools, the DM/CVM/zap relay arrays, the `cvm-server` + `push-service`
defaults, and the archive default all point at the new relay.

View file

@ -24,6 +24,7 @@ import {
coordinatorAddUrl,
parseCoordinatorInput,
shortNpub,
SKATESPOTS_COORDINATOR,
} from '@/lib/coordinator';
import { cn } from '@/lib/utils';
@ -198,6 +199,20 @@ export function CoordinatorSettingsCard() {
</div>
)}
{/* ── SkateSpots' provided default ───────────────────────────────── */}
<div className="rounded-2xl border border-white/8 bg-white/2 p-4 space-y-2.5">
<p className="flex items-center gap-2 text-sm font-bold text-white">
<Server className="h-4 w-4 text-cyan-400" />
{t('socialUi.coordinator.ourDefaultTitle')}
</p>
<p className="text-xs leading-relaxed text-muted-foreground">
{t('socialUi.coordinator.ourDefaultDesc')}
</p>
<Button size="sm" variant="outline" onClick={() => setCoordinator(SKATESPOTS_COORDINATOR)}>
{t('socialUi.coordinator.useOurs')}
</Button>
</div>
{/* ── Add / change ────────────────────────────────────────────────── */}
<div className="rounded-2xl border border-white/8 bg-white/2 p-4 space-y-3">
<p className="text-sm font-bold text-white">{coordinator ? t('socialUi.coordinator.change') : t('socialUi.coordinator.addTitle')}</p>

View file

@ -28,7 +28,7 @@ import { useCrewGoal } from '@/hooks/useCrewGoal';
import { useCoordinator, useCoordinatorHealth } from '@/hooks/useCoordinator';
import { normalizeUrlInput, sanitizeUrl } from '@/lib/sanitizeUrl';
import { Nip05HandlesEditor } from '@/components/Nip05HandlesEditor';
import { coordinatorAddUrl, crewCoordinatorInputValue, parseCrewCoordinatorInput, shortNpub } from '@/lib/coordinator';
import { coordinatorAddUrl, crewCoordinatorInputValue, parseCrewCoordinatorInput, shortNpub, SKATESPOTS_COORDINATOR } from '@/lib/coordinator';
import { useRelays } from '@/lib/relays';
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, Film, Check, ArrowRight, ArrowLeft as ArrowLeftIcon, Server, ChevronDown, Zap } from 'lucide-react';
@ -332,7 +332,8 @@ export function CrewForm({ existing }: CrewFormProps) {
// Sponsoring shop coordinate (30017:pubkey:d) and Cordn/MLS coordinator pointer.
const [sponsorRef, setSponsorRef] = useState(existing?.sponsorRef ?? '');
const [coordinatorInput, setCoordinatorInput] = useState(
crewCoordinatorInputValue(existing?.coordinator),
crewCoordinatorInputValue(existing?.coordinator) ||
(isEditing ? '' : crewCoordinatorInputValue({ url: SKATESPOTS_COORDINATOR.relays[0], pubkey: SKATESPOTS_COORDINATOR.pubkey })),
);
// The coordinator the user set in Settings → Coordinator, for a one-tap prefill.
const { coordinator: myCoordinator } = useCoordinator();

View file

@ -3918,6 +3918,9 @@ socialUi: {
runOwnDesc: "It can live on an old phone, a Raspberry Pi, or any small box you leave on. Your group keeps working while it's up — and you can move the group whenever you like.",
onPhone: 'On a phone',
onPhoneDesc: 'Use the Cordn Coordinator app, then scan its QR here to add it.',
ourDefaultTitle: 'SkateSpots coordinator',
ourDefaultDesc: 'SkateSpots runs a coordinator at relay.skate-spots.org. Use it by default, or run your own — a coordinator only ever sees ciphertext and metadata, never your messages.',
useOurs: 'Use SkateSpots coordinator',
openCordn: 'Open Cordn',
scanItsQR: 'Scan its QR',
onServer: 'On a server',

View file

@ -3918,6 +3918,9 @@ socialUi: {
runOwnDesc: 'Puede vivir en un móvil viejo, una Raspberry Pi o cualquier cacharro que dejes encendido. Tu grupo sigue funcionando mientras esté activo — y puedes mover el grupo cuando quieras.',
onPhone: 'En un móvil',
onPhoneDesc: 'Usa la app Cordn Coordinator y escanea su QR aquí para añadirlo.',
ourDefaultTitle: 'Coordinador de SkateSpots',
ourDefaultDesc: 'SkateSpots ejecuta un coordinador en relay.skate-spots.org. Úsalo por defecto, o monta el tuyo — un coordinador solo ve texto cifrado y metadatos, nunca tus mensajes.',
useOurs: 'Usar el coordinador de SkateSpots',
openCordn: 'Abrir Cordn',
scanItsQR: 'Escanear su QR',
onServer: 'En un servidor',

View file

@ -40,6 +40,15 @@ export interface CoordinatorConfig {
addedAt: number;
}
/** SkateSpots' own coordinator — embedded in our relay at wss://relay.skate-spots.org.
* Provided as the default for crews and Settings → Coordinator; users may always
* run their own (a coordinator only ever sees ciphertext + metadata, never message
* contents). Pubkey = the relay's `cordn.key`. */
export const SKATESPOTS_COORDINATOR: Pick<CoordinatorConfig, 'pubkey' | 'relays'> = {
pubkey: '02816a05b1d4e70a7df9c712ef6f7272774269d65bd8a2f262ce0330d0885693',
relays: ['wss://relay.skate-spots.org'],
};
const HEX64 = /^[0-9a-f]{64}$/i;
function uniqueRelays(relays: Iterable<string>): string[] {