Files
plebeian-signer/projects/common/src/lib/helpers/nip05-validator.ts
mleku ebe2b695cc Release v1.0.0 - Major security upgrade with Argon2id encryption
- Upgrade vault encryption from PBKDF2 (1000 iterations) to Argon2id
  (256MB memory, 8 iterations, 4 threads, ~3 second derivation)
- Add automatic migration from v1 to v2 vault format on unlock
- Add WebAssembly CSP support for hash-wasm Argon2id implementation
- Add NIP-42 relay authentication support for auth-required relays
- Add profile edit feature with pencil icon on identity page
- Add direct NIP-05 validation (removes NDK dependency for validation)
- Add deriving modal with progress timer during key derivation
- Add client tag "plebeian-signer" to profile events
- Fix modal colors (dark theme for visibility)
- Fix NIP-05 badge styling to include check/error indicator
- Add release zip packages for Chrome and Firefox

New files:
- projects/common/src/lib/helpers/argon2-crypto.ts
- projects/common/src/lib/helpers/websocket-auth.ts
- projects/common/src/lib/helpers/nip05-validator.ts
- projects/common/src/lib/components/deriving-modal/
- projects/{chrome,firefox}/src/app/components/profile-edit/
- releases/plebeian-signer-{chrome,firefox}-v1.0.0.zip

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 12:30:10 +01:00

128 lines
3.4 KiB
TypeScript

/**
* NIP-05 Verification Helper
*
* Directly validates NIP-05 identifiers by fetching the .well-known/nostr.json
* file and comparing the pubkey.
*/
export interface Nip05ValidationResult {
valid: boolean;
pubkey?: string;
relays?: string[];
error?: string;
}
/**
* Parse a NIP-05 identifier into its components
* @param nip05 - The NIP-05 identifier (e.g., "me@mleku.dev" or "_@mleku.dev")
* @returns Object with name and domain, or null if invalid
*/
export function parseNip05(nip05: string): { name: string; domain: string } | null {
if (!nip05 || typeof nip05 !== 'string') {
return null;
}
const parts = nip05.toLowerCase().trim().split('@');
if (parts.length !== 2) {
return null;
}
const [name, domain] = parts;
if (!name || !domain) {
return null;
}
// Basic domain validation
if (!domain.includes('.') || domain.includes('/')) {
return null;
}
return { name, domain };
}
/**
* Validate a NIP-05 identifier against a pubkey
*
* @param nip05 - The NIP-05 identifier (e.g., "me@mleku.dev")
* @param expectedPubkey - The expected pubkey in hex format
* @param timeoutMs - Fetch timeout in milliseconds
* @returns Validation result with status and any discovered relays
*/
export async function validateNip05(
nip05: string,
expectedPubkey: string,
timeoutMs = 10000
): Promise<Nip05ValidationResult> {
const parsed = parseNip05(nip05);
if (!parsed) {
return { valid: false, error: 'Invalid NIP-05 format' };
}
const { name, domain } = parsed;
const url = `https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
const response = await fetch(url, {
signal: controller.signal,
headers: {
'Accept': 'application/json',
},
});
clearTimeout(timeoutId);
if (!response.ok) {
return {
valid: false,
error: `HTTP ${response.status}: ${response.statusText}`,
};
}
const data = await response.json();
// Check if the names object exists and contains the requested name
if (!data.names || typeof data.names !== 'object') {
return { valid: false, error: 'Invalid nostr.json structure: missing names' };
}
// NIP-05 names are case-insensitive
const pubkeyFromJson = data.names[name] || data.names[name.toLowerCase()];
if (!pubkeyFromJson) {
return { valid: false, error: `Name "${name}" not found in nostr.json` };
}
// Compare pubkeys (case-insensitive hex comparison)
const normalizedExpected = expectedPubkey.toLowerCase();
const normalizedFound = pubkeyFromJson.toLowerCase();
const valid = normalizedExpected === normalizedFound;
// Extract relays if present
let relays: string[] | undefined;
if (data.relays && typeof data.relays === 'object') {
const relayList = data.relays[pubkeyFromJson] || data.relays[normalizedFound];
if (Array.isArray(relayList)) {
relays = relayList;
}
}
return {
valid,
pubkey: pubkeyFromJson,
relays,
error: valid ? undefined : 'Pubkey mismatch',
};
} catch (error) {
if (error instanceof Error) {
if (error.name === 'AbortError') {
return { valid: false, error: 'Request timeout' };
}
return { valid: false, error: error.message };
}
return { valid: false, error: 'Unknown error' };
}
}