import { nip19 } from 'nostr-tools'; /** * Cordn coordinator support — mostly *formats*, taken from Cordn itself so we * don't invent anything interoperable. * * - A coordinator is identified by a Nostr pubkey plus the ContextVM relays * that reach it (see `nprofile`). * - Cordn's own server prints the canonical "add me" link as * `https://cordn.net/chat/coordinators?c=` * (`packages/server/src/main.ts`), and defaults to `wss://relay.contextvm.org` * (`packages/server/src/coordinatorServer.ts`). * - Cordn is a ContextVM server, so a coordinator can be probed the same way * our own CVM server is: look for events it authored on that relay. */ /** Cordn's default ContextVM relay. */ export const CORDN_RELAY_URL = 'wss://relay.contextvm.org'; /** Cordn's "add this coordinator" URL base. */ export const CORDN_ADD_BASE = 'https://cordn.net/chat/coordinators'; /** Cordn's home — where the docs/one-liner/phone app live. */ export const CORDN_HOME = 'https://cordn.net'; /** The self-host one-liner advertised on cordn.net. */ export const CORDN_DOCKER_COMMAND = 'docker run --rm ghcr.io/cordn-msg/cordn:latest'; /** Where a user gets a coordinator they can run on a phone. */ export const CORDN_APP_URL = 'https://cordn.net'; /** Device-local: which coordinator this user points at. */ export const COORDINATOR_STORAGE_KEY = 'skatespots:coordinator'; export interface CoordinatorConfig { /** Coordinator server pubkey (hex). */ pubkey: string; /** ContextVM relays that reach it (deduped, sorted-ish). */ relays: string[]; addedAt: number; } const HEX64 = /^[0-9a-f]{64}$/i; function uniqueRelays(relays: Iterable): string[] { return [...new Set([...relays].map((r) => r.trim()).filter((r) => /^wss?:\/\//i.test(r)))]; } /** Pull the `c=` nprofile out of a cordn.net add URL. */ function extractFromUrl(input: string): string | null { try { return new URL(input).searchParams.get('c'); } catch { return null; } } /** * Parse anything a user might paste: an `nprofile`, an `npub`, a bare hex * pubkey, or a `cordn.net/chat/coordinators?c=` link. Returns null * when it isn't a usable coordinator pointer. */ export function parseCoordinatorInput(raw: string): Omit | null { const input = raw.trim(); if (!input) return null; const candidate = input.includes('://') ? extractFromUrl(input) ?? input : input; try { const decoded = nip19.decode(candidate); if (decoded.type === 'nprofile') { return { pubkey: decoded.data.pubkey, relays: uniqueRelays(decoded.data.relays ?? []) }; } if (decoded.type === 'npub') { return { pubkey: decoded.data, relays: [] }; } } catch { // Not bech32 — try a raw hex pubkey below. } if (HEX64.test(candidate)) return { pubkey: candidate.toLowerCase(), relays: [] }; return null; } /** The canonical cordn.net link that adds this coordinator. */ export function coordinatorAddUrl(config: Pick): string { const nprofile = nip19.nprofileEncode({ pubkey: config.pubkey, relays: config.relays }); return `${CORDN_ADD_BASE}?c=${nprofile}`; } /** `npub1abcd…wxyz` for display; falls back to a hex prefix. */ export function shortNpub(pubkey: string): string { try { const npub = nip19.npubEncode(pubkey); return `${npub.slice(0, 12)}…${npub.slice(-6)}`; } catch { return `${pubkey.slice(0, 12)}…`; } } /** A crew coordinator as stored on the crew event: `['coordinator', url, pubkey?, operator?]`. */ export interface CrewCoordinatorPointer { /** Endpoint: a `wss://` ContextVM relay, or an http(s) coordinator URL. */ url: string; /** Coordinator service pubkey (hex), when addressed by identity. */ pubkey?: string; } /** * Parse what a crew founder pastes into the crew's coordinator field. Accepts * everything Settings → Coordinator does (nprofile / npub / hex / cordn.net * link) **plus** a plain endpoint URL — so a coordinator run from Cordn's phone * app (which yields an nprofile, not a URL) is enterable, not just a * self-hosted endpoint. */ export function parseCrewCoordinatorInput(raw: string): CrewCoordinatorPointer | null { const input = raw.trim(); if (!input) return null; const pointer = parseCoordinatorInput(input); if (pointer) { return { url: pointer.relays[0] ?? CORDN_RELAY_URL, pubkey: pointer.pubkey }; } if (/^(https?|wss?):\/\//i.test(input)) return { url: input }; return null; } /** Rebuild a paste-able input from a stored crew coordinator (round-trips). */ export function crewCoordinatorInputValue( coordinator: { url: string; pubkey?: string } | undefined, ): string { if (!coordinator?.url) return ''; if (coordinator.pubkey && /^wss?:\/\//i.test(coordinator.url)) { return nip19.nprofileEncode({ pubkey: coordinator.pubkey, relays: [coordinator.url] }); } return coordinator.url; } /** * Best-effort coordinator display name from an announcement event. Cordn relies * on the ContextVM transport announcement, whose exact shape we don't hard-code * here — so we look in the obvious places and give up gracefully. */ export function coordinatorNameFromEvent(event: { tags: string[][]; content: string; }): string | null { const tagName = event.tags.find(([t]) => t === 'name')?.[1]; if (tagName?.trim()) return tagName.trim().slice(0, 40); try { const parsed = JSON.parse(event.content) as { name?: unknown; serverInfo?: { name?: unknown }; }; const candidates = [parsed.serverInfo?.name, parsed.name]; for (const value of candidates) { if (typeof value === 'string' && value.trim()) return value.trim().slice(0, 40); } } catch { // content isn't JSON — no name to show. } return null; }