A NIP-05 name maps to one key, so "who owns it" decides where it lives: - Crew: repeatable nip05 tags (up to 5), verified against the founder key. - Shop: same on kind 30017, verified against the owner key (new field). - Profile: kind-0 has ONE standard nip05; extra personal handles live in a separate NIP-78 record (kind 30078, d=skatespots:profile-handles) so another client re-saving the profile can't wipe them. Shared pieces: - components/Nip05Badges.tsx — generic verify-and-render chip row (any owner). - components/Nip05HandlesEditor.tsx — shared add/remove editor with live verified/mismatch status; used by profile, crew and shop forms. - CrewNip05Badge now delegates to Nip05Badges; CrewForm uses the shared editor. - Profile page shows primary + extra handles, and each crew/shop chips its own verified handle. - lib/profileHandles.ts + hooks/useProfileHandles.ts (read/publish the record). - types: SkateShop gains nip05/nip05s; parseSkateShop parses repeatable nip05s. - NIP.md + AGENTS.md + i18n (EN/ES) updated.
51 lines
2.1 KiB
TypeScript
51 lines
2.1 KiB
TypeScript
/**
|
|
* Extra personal NIP-05 handles for a profile.
|
|
*
|
|
* A kind-0 profile has ONE standard `nip05` field, and other clients editing it
|
|
* may drop anything non-standard. So the extra handles live in a **separate
|
|
* NIP-78 record** (kind 30078, `d=skatespots:profile-handles`) authored by the
|
|
* user — the primary stays in `nip05` (works everywhere), the extras ride here
|
|
* (SkateSpots-only, but durable against another client re-saving the profile).
|
|
*
|
|
* Each handle must resolve (via `/.well-known/nostr.json`) to the **owner's own**
|
|
* pubkey — verified the same way as a crew handle, just against the profile key.
|
|
*/
|
|
import { APP_DATA_KIND } from '@/lib/types';
|
|
|
|
export { APP_DATA_KIND };
|
|
|
|
/** `d` identifier for the per-user extra-handles record. */
|
|
export const PROFILE_HANDLES_D = 'skatespots:profile-handles';
|
|
|
|
export const MAX_PROFILE_HANDLES = 5;
|
|
|
|
/** `local@domain.tld` per NIP-05 (local part is `[a-z0-9-_.]`). */
|
|
export const NIP05_HANDLE_RE = /^[a-z0-9\-_.]+@[a-z0-9.-]+\.[a-z]{2,}$/;
|
|
|
|
/** Normalise a raw handle; returns null when it isn't a valid `local@domain`. */
|
|
export function normalizeHandle(raw: string): string | null {
|
|
const value = raw.trim().toLowerCase();
|
|
return NIP05_HANDLE_RE.test(value) ? value : null;
|
|
}
|
|
|
|
/** All extra handles stored on a profile-handles event (deduped, ordered, capped). */
|
|
export function parseProfileHandles(event: { tags: string[][] }): string[] {
|
|
const handles = event.tags
|
|
.filter(([name, value]) => name === 'nip05' && !!value?.trim())
|
|
.map(([, value]) => value.trim().toLowerCase())
|
|
.filter((value) => NIP05_HANDLE_RE.test(value));
|
|
return [...new Set(handles)].slice(0, MAX_PROFILE_HANDLES);
|
|
}
|
|
|
|
/** Tags for the addressable record. An empty list clears the extras (replace). */
|
|
export function profileHandlesTags(handles: string[]): string[][] {
|
|
const clean = [
|
|
...new Set(handles.map((h) => h.trim().toLowerCase()).filter((h) => NIP05_HANDLE_RE.test(h))),
|
|
].slice(0, MAX_PROFILE_HANDLES);
|
|
const tags: string[][] = [
|
|
['d', PROFILE_HANDLES_D],
|
|
['alt', 'SkateSpots profile handles'],
|
|
];
|
|
for (const handle of clean) tags.push(['nip05', handle]);
|
|
return tags;
|
|
}
|