feat: add @domain alias for NIP-05 domain directory resolution (#136)

Add support for @domain syntax in req and count commands to query all
users from a domain's NIP-05 directory (e.g., @habla.news).

Features:
- Fetches /.well-known/nostr.json from domain
- Extracts all pubkeys from the names object
- Works with -a (authors), -p (#p tags), and -P (#P tags) flags
- Supports mixed usage with npub, hex, NIP-05, $me, $contacts
- 5-minute caching for domain lookups
- UI display in ReqViewer query dropdown

Implementation:
- Added resolveDomainDirectory and resolveDomainDirectoryBatch to nip05.ts
- Updated req-parser and count-parser to detect @domain syntax
- Updated argParsers in man.ts to resolve domains asynchronously
- Updated ReqViewer to display queried domains in dropdown
- Added comprehensive tests for domain resolution

Examples:
- req -k 1 -a @habla.news
- req -k 7 -p @nostr.band
- count relay.damus.io -k 1 -a @getcurrent.io

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Alejandro
2026-01-18 14:49:37 +01:00
committed by GitHub
parent 4d90aab83c
commit b70eb82fea
8 changed files with 368 additions and 8 deletions

View File

@@ -5,7 +5,7 @@ import type { AppId } from "./app";
import { parseOpenCommand } from "@/lib/open-parser";
import { parseProfileCommand } from "@/lib/profile-parser";
import { parseRelayCommand } from "@/lib/relay-parser";
import { resolveNip05Batch } from "@/lib/nip05";
import { resolveNip05Batch, resolveDomainDirectoryBatch } from "@/lib/nip05";
import { parseChatCommand } from "@/lib/chat-parser";
import { parseBlossomCommand } from "@/lib/blossom-parser";
@@ -318,6 +318,50 @@ export const manPages: Record<string, ManPageEntry> = {
}
}
// Resolve domain directories if present
const allDomains = [
...(parsed.domainAuthors || []),
...(parsed.domainPTags || []),
...(parsed.domainPTagsUppercase || []),
];
if (allDomains.length > 0) {
const resolved = await resolveDomainDirectoryBatch(allDomains);
// Add resolved authors to filter
if (parsed.domainAuthors) {
for (const domain of parsed.domainAuthors) {
const pubkeys = resolved.get(domain);
if (pubkeys) {
if (!parsed.filter.authors) parsed.filter.authors = [];
parsed.filter.authors.push(...pubkeys);
}
}
}
// Add resolved #p tags to filter
if (parsed.domainPTags) {
for (const domain of parsed.domainPTags) {
const pubkeys = resolved.get(domain);
if (pubkeys) {
if (!parsed.filter["#p"]) parsed.filter["#p"] = [];
parsed.filter["#p"].push(...pubkeys);
}
}
}
// Add resolved #P tags to filter
if (parsed.domainPTagsUppercase) {
for (const domain of parsed.domainPTagsUppercase) {
const pubkeys = resolved.get(domain);
if (pubkeys) {
if (!parsed.filter["#P"]) parsed.filter["#P"] = [];
parsed.filter["#P"].push(...pubkeys);
}
}
}
}
return parsed;
},
defaultProps: { filter: { kinds: [1], limit: 50 } },
@@ -443,6 +487,47 @@ export const manPages: Record<string, ManPageEntry> = {
}
}
// Resolve domain directories if present
const allDomains = [
...(parsed.domainAuthors || []),
...(parsed.domainPTags || []),
...(parsed.domainPTagsUppercase || []),
];
if (allDomains.length > 0) {
const resolved = await resolveDomainDirectoryBatch(allDomains);
if (parsed.domainAuthors) {
for (const domain of parsed.domainAuthors) {
const pubkeys = resolved.get(domain);
if (pubkeys) {
if (!parsed.filter.authors) parsed.filter.authors = [];
parsed.filter.authors.push(...pubkeys);
}
}
}
if (parsed.domainPTags) {
for (const domain of parsed.domainPTags) {
const pubkeys = resolved.get(domain);
if (pubkeys) {
if (!parsed.filter["#p"]) parsed.filter["#p"] = [];
parsed.filter["#p"].push(...pubkeys);
}
}
}
if (parsed.domainPTagsUppercase) {
for (const domain of parsed.domainPTagsUppercase) {
const pubkeys = resolved.get(domain);
if (pubkeys) {
if (!parsed.filter["#P"]) parsed.filter["#P"] = [];
parsed.filter["#P"].push(...pubkeys);
}
}
}
}
return parsed;
},
},