- 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>
128 lines
3.4 KiB
TypeScript
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' };
|
|
}
|
|
}
|