mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-30 19:46:17 +00:00
docs: explain NIP-05 identifier resolution in nostr-expert skill
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bb8txetygxSnLXHJhPZSwH
This commit is contained in:
@@ -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<K,V>` 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 <number>` | - |
|
||||
|
||||
## Common Event Kinds
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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).
|
||||
Reference in New Issue
Block a user