From 4147298494cb3af0b0e03bdccd4e2424baac2d0f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 20:14:06 +0000 Subject: [PATCH] docs: explain NIP-05 identifier resolution in nostr-expert skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI agents were hand-rolling their own hex/npub/nprofile/NIP-05 resolvers (e.g. a bespoke `resolveObserver` with its own well-known fetch and JSON parse) instead of using `resolveUserHexOrNull` in `quartz/nip05DnsIdentifiers/`, which already handles every identifier form. Add discoverable explainers so the canonical functions surface before an agent reaches for a hand-rolled version: - New reference `references/nip05-identifiers.md`: full API surface (`resolveUserHexOrNull`, `Nip05Client`, `Nip05Id`, `Nip05Parser`, `KeyInfoSet`, Namecoin `.bit`, `OkHttpNip05Fetcher`) plus the hand-rolled anti-pattern to avoid and the ✅ replacement. - SKILL.md: new "Resolving User Input to a Pubkey" section, a When-to-Use bullet, Bundled Resources + Quick Reference rows, and the identifier-resolution trigger added to the skill description. - nip-catalog.md: fix the stale NIP-05 entry (referenced a nonexistent `Nip05Verifier.kt`) to point at `UserHexResolver.kt` / `Nip05Client.kt`. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Bb8txetygxSnLXHJhPZSwH --- .claude/skills/nostr-expert/SKILL.md | 26 ++++- .../nostr-expert/references/nip-catalog.md | 2 +- .../references/nip05-identifiers.md | 98 +++++++++++++++++++ 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 .claude/skills/nostr-expert/references/nip05-identifiers.md diff --git a/.claude/skills/nostr-expert/SKILL.md b/.claude/skills/nostr-expert/SKILL.md index 8333b24a30..8dc31a681a 100644 --- a/.claude/skills/nostr-expert/SKILL.md +++ b/.claude/skills/nostr-expert/SKILL.md @@ -1,6 +1,6 @@ --- name: nostr-expert -description: Nostr protocol implementation patterns in Quartz (AmethystMultiplatform's KMP Nostr library). Use when working with: (1) Nostr events (creating, parsing, signing), (2) Event kinds and tags, (3) NIP implementations (80+ NIP packages in quartz/), (4) Event builders and TagArrayBuilder DSL, (5) Nostr cryptography (secp256k1, NIP-44 encryption), (6) Relay communication patterns, (7) Bech32 encoding (npub, nsec, note, nevent). Complements nostr-protocol agent (NIP specs) - this skill provides Quartz codebase patterns and implementation details. +description: Nostr protocol implementation patterns in Quartz (AmethystMultiplatform's KMP Nostr library). Use when working with: (1) Nostr events (creating, parsing, signing), (2) Event kinds and tags, (3) NIP implementations (80+ NIP packages in quartz/), (4) Event builders and TagArrayBuilder DSL, (5) Nostr cryptography (secp256k1, NIP-44 encryption), (6) Relay communication patterns, (7) Bech32 encoding (npub, nsec, note, nevent), (8) Resolving user input (hex, npub, nprofile, or NIP-05 `name@domain` internet identifiers) to a pubkey. Complements nostr-protocol agent (NIP specs) - this skill provides Quartz codebase patterns and implementation details. --- # Nostr Protocol Expert (Quartz Implementation) @@ -15,6 +15,7 @@ Practical patterns for working with Nostr in Quartz, AmethystMultiplatform's KMP - Finding NIP implementations in quartz/ codebase - Nostr cryptography (secp256k1 signing, NIP-44 encryption) - Bech32 encoding/decoding (npub, nsec, note formats) +- Resolving user input (hex / npub / nprofile / NIP-05 `name@domain`) to a pubkey - Event validation and verification **For NIP specifications** → Use `nostr-protocol` agent @@ -424,6 +425,25 @@ when (val entity = Nip19Parser.uriToRoute(input)?.entity) { } ``` +## Resolving User Input to a Pubkey (NIP-05 + NIP-19) + +**Before writing any `if (isHex) … else if (npub) … else if ("@" in s) fetchWellKnown()` logic, stop — it already exists.** `resolveUserHexOrNull` in `quartz/nip05DnsIdentifiers/` accepts every identifier form a user might type and returns a 64-hex pubkey. + +```kotlin +import com.vitorpamplona.quartz.nip05DnsIdentifiers.resolveUserHexOrNull + +// hex | npub1… | nprofile1… | nsec1… | name@domain.tld → HexKey? (null if unrecognized/lookup fails) +val pubkey = resolveUserHexOrNull(userInput, nip05Client) +``` + +- Tries the **synchronous** hex/bech32 path first (`decodePublicKeyAsHexOrNull`) — only NIP-05-shaped input hits the network. +- `suspend`; re-throws only `CancellationException`. Pass `nip05Client = null` for offline contexts. +- Build the client with `Nip05Client(fetcher = OkHttpNip05Fetcher { _ -> okHttp })` (see `cli/Context.kt`). The OkHttp fetcher already runs on IO and disables redirects per the NIP-05 spec — don't re-implement the `.well-known/nostr.json` fetch or JSON parse. +- Need only hex/bech32 (no network)? Use `decodePublicKeyAsHexOrNull(input)` directly. +- Need to *verify* a claimed identifier maps back to a pubkey? `nip05Client.verify(Nip05Id.parse(id)!!, pubkey)`. + +See `references/nip05-identifiers.md` for the full API surface (`Nip05Id`, `Nip05Client`, `Nip05Parser`, `KeyInfoSet`, Namecoin `.bit`) and the hand-rolled anti-pattern to avoid. + ## Event Validation ```kotlin @@ -552,6 +572,7 @@ Or see `references/nip-catalog.md` for complete catalog. - **references/event-hierarchy.md** - Event class hierarchy, kind classifications, common types - **references/tag-patterns.md** - Tag structure, TagArrayBuilder DSL, common tag types, parsing patterns - **references/nip19-bech32.md** - `Nip19Parser`, `Bech32Util`, `TlvBuilder`, entity types (NPub, NSec, NEvent, NAddress, NProfile, NRelay, NEmbed) +- **references/nip05-identifiers.md** - Resolving any identifier (hex/npub/nprofile/nsec/`name@domain`) to a pubkey via `resolveUserHexOrNull`; `Nip05Client`, `Nip05Id`, `Nip05Parser`, Namecoin `.bit` — and the hand-rolled anti-pattern to avoid - **references/event-factory.md** - `EventFactory` dispatch pattern and how to register a new kind - **references/crypto-and-encryption.md** - Event signing/verification, secp256k1 abstraction, NIP-44 encryption, `SharedKeyCache` - **references/large-cache.md** - `LargeCache` expect/actual + `ICacheOperations` functional API @@ -567,6 +588,9 @@ Or see `references/nip-catalog.md` for complete catalog. | Verify signature | `event.verify()` | nip01Core/core/ | | Encrypt (NIP-44) | `Nip44v2.encrypt(...)` | nip44Encryption/ | | Bech32 encode | `Nip19.npubEncode(...)` | nip19Bech32/ | +| Resolve input → pubkey | `resolveUserHexOrNull(input, nip05Client)` | nip05DnsIdentifiers/ | +| Decode bech32 → pubkey (no net) | `decodePublicKeyAsHexOrNull(input)` | nip19Bech32/ | +| Verify NIP-05 identifier | `nip05Client.verify(Nip05Id.parse(id)!!, hex)` | nip05DnsIdentifiers/ | | Find NIP | `scripts/nip-lookup.sh ` | - | ## Common Event Kinds diff --git a/.claude/skills/nostr-expert/references/nip-catalog.md b/.claude/skills/nostr-expert/references/nip-catalog.md index a18731696f..78ed47ee11 100644 --- a/.claude/skills/nostr-expert/references/nip-catalog.md +++ b/.claude/skills/nostr-expert/references/nip-catalog.md @@ -13,7 +13,7 @@ under `experimental/`**. The categorized list below may lag behind — | 02 | `nip02FollowList/` | ContactListEvent.kt | Follow/contact lists (kind 3) | | 03 | `nip03Timestamp/` | OpenTimestampsAttestation.kt | Timestamps | | 04 | `nip04Dm/` | EncryptedDmEvent.kt | Legacy encrypted DMs (deprecated for NIP-17) | -| 05 | `nip05DnsIdentifiers/` | Nip05Verifier.kt | DNS-based verification | +| 05 | `nip05DnsIdentifiers/` | UserHexResolver.kt, Nip05Client.kt | Internet identifiers; `resolveUserHexOrNull` resolves hex/npub/nprofile/`name@domain` → pubkey (see references/nip05-identifiers.md) | | 06 | `nip06KeyDerivation/` | Mnemonic-related | BIP-39 key derivation | | 09 | `nip09Deletions/` | DeletionEvent.kt | Event deletion requests (kind 5) | | 11 | `nip11RelayInfo/` | RelayInformation.kt | Relay metadata | diff --git a/.claude/skills/nostr-expert/references/nip05-identifiers.md b/.claude/skills/nostr-expert/references/nip05-identifiers.md new file mode 100644 index 0000000000..450f0e86fc --- /dev/null +++ b/.claude/skills/nostr-expert/references/nip05-identifiers.md @@ -0,0 +1,98 @@ +# NIP-05: Identifiers → Pubkey Resolution + +How Quartz turns anything a human might type — a raw hex pubkey, an `npub`/`nprofile`/`nsec`, or a NIP-05 internet identifier (`alice@domain.tld`) — into a 64-hex Nostr pubkey. **Everything below already exists in `quartz/nip05DnsIdentifiers/`. Do not hand-roll it.** + +## TL;DR — the one function you almost always want + +```kotlin +import com.vitorpamplona.quartz.nip05DnsIdentifiers.resolveUserHexOrNull + +// hex | npub1… | nprofile1… | nsec1… | name@domain.tld → 64-hex pubkey (or null) +val pubkey: HexKey? = resolveUserHexOrNull(userInput, nip05Client) +``` + +`resolveUserHexOrNull(input, nip05Client)` (in `UserHexResolver.kt`) is the canonical "accept any identifier form" resolver. It: + +- trims input, tries the **synchronous** bech32/hex path first (`decodePublicKeyAsHexOrNull`), so hex/`npub`/`nprofile`/`nsec` never touch the network; +- only issues an HTTPS fetch for genuinely NIP-05-shaped input (`name@domain.tld`), gated by a cheap `looksLikeNip05()` precheck; +- returns `null` on anything unrecognizable or on a failed NIP-05 lookup (network error / no match); +- re-throws **only** `CancellationException`, so it's safe inside structured concurrency. + +Pass `nip05Client = null` in pure-offline contexts — NIP-05-shaped inputs then fall through to `null` and no HTTP is attempted. + +## ❌ Do not write this (the hand-rolled anti-pattern) + +```kotlin +// DON'T. This re-implements resolveUserHexOrNull badly: +// - no nsec support +// - no input validation (accepts IP-literal / malformed domains → spurious fetches) +// - hand-parses JSON instead of using Nip05Parser +// - bespoke httpGet ignores the "MUST NOT follow redirects" rule +// - swallows CancellationException, breaking structured concurrency +fun resolveObserver(input: String): String? { + if (Hex.isHex64(input)) return input.lowercase() + if (input.startsWith("npub1") || input.startsWith("nprofile1")) { /* … */ } + if ("@" in input) return resolveNip05(input) // bespoke well-known fetch + return null +} +``` + +## ✅ Do this instead + +```kotlin +// CLI already exposes it — commands call Context.requireUserHex(input): +val pubHex = resolveUserHexOrNull(input, nip05Client) + ?: return Output.error("bad_args", "expected npub, nprofile, 64-hex, or name@domain.tld") +``` + +## Building an `Nip05Client` + +`resolveUserHexOrNull` takes an `INip05Client`. On JVM/Android, wire the OkHttp fetcher (mirror what `cli/Context.kt` does): + +```kotlin +import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Client +import com.vitorpamplona.quartz.nip05DnsIdentifiers.OkHttpNip05Fetcher + +val nip05Client = Nip05Client(fetcher = OkHttpNip05Fetcher { _ -> okHttpClient }) +``` + +`OkHttpNip05Fetcher` already runs on `Dispatchers.IO` and disables redirects per the NIP-05 spec ("Fetchers MUST ignore any HTTP redirects"). Don't re-implement the fetch. + +For tests / offline code, `EmptyNip05Client` is a no-op stub. + +## The pieces (all in `quartz/…/nip05DnsIdentifiers/`) + +| Type | File | Purpose | +|------|------|---------| +| `resolveUserHexOrNull(input, client?)` | `UserHexResolver.kt` | **Start here.** Any identifier form → 64-hex pubkey, or null. `suspend`. | +| `Nip05Id` | `Nip05Id.kt` | Parsed `name@domain`. `Nip05Id.parse(str)` validates (RFC 5321 local-part + hostname rules, rejects IP literals) and lowercases. `toUserUrl()` / `toDomainUrl()` build the `.well-known/nostr.json` URLs. `toDisplayValue()` collapses the `_` wildcard to just the domain. | +| `INip05Client` / `Nip05Client` | `INip05Client.kt`, `Nip05Client.kt` | Async resolver. `get(id): Nip05KeyInfo?` (pubkey + relays), `verify(id, hex): Boolean`, `load(id): KeyInfoSet?`, `list(domain): KeyInfoSet`, `loadClinkOffer(id): String?`. Auto-routes `.bit` domains to Namecoin. `EmptyNip05Client` = offline no-op. | +| `Nip05Fetcher` / `OkHttpNip05Fetcher` | `Nip05Fetcher.kt`, `OkHttpNip05Fetcher.kt` (jvmAndroid) | Transport SAM. OkHttp actual disables redirects + runs on IO. | +| `Nip05Parser` | `Nip05Parser.kt` | JSON `.well-known/nostr.json` codec: `parseHexKey`, `parseHexKeyAndRelays`, `parse` → `KeyInfoSet`, `parseClinkOffer`. | +| `Nip05KeyInfo` / `KeyInfoSet` | `Nip05KeyInfo.kt`, `KeyInfoSet.kt` | `Nip05KeyInfo(pubkey, relays)`; `KeyInfoSet(names: Map, relays: Map)` = the full domain listing. | +| `NamecoinNameResolver` | `namecoin/NamecoinNameResolver.kt` | `.bit` / `d/…` / `id/…` blockchain identifiers. `isNamecoinIdentifier(str)`, `resolve(str)`. Invoked automatically by `Nip05Client` — you rarely call it directly. | + +## When you only need the pure (synchronous, no-network) part + +If the input can only be hex/bech32 (no NIP-05), skip the client entirely: + +```kotlin +import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull + +// hex | npub1… | nprofile1… | nsec1… → 64-hex pubkey (or null). No suspend, no network. +val pubkey: HexKey? = decodePublicKeyAsHexOrNull(input) +``` + +See `references/nip19-bech32.md` for the full bech32 entity story. `resolveUserHexOrNull` is just this function plus the NIP-05 HTTP fallback. + +## Verifying a claimed identifier + +To confirm a profile's advertised `nip05` actually points back to its pubkey (NIP-05 verification), use `verify`, not `get`: + +```kotlin +val ok: Boolean = nip05Client.verify(Nip05Id.parse("alice@domain.tld")!!, profilePubkeyHex) +``` + +## Tests + +`quartz/src/commonTest/…/nip05DnsIdentifiers/Nip05Test.kt` covers parsing, URL construction, case-normalization, CLINK offers, and the validation rejects (IP literals, malformed domains).