From af9f376ad00ea722fe7dcda4b7101206e7b9ad5f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 6 May 2023 22:29:41 -0500 Subject: [PATCH] Verify NIP05's with cache, fixes #1 --- src/controllers/api/accounts.ts | 8 ++--- src/nip05.ts | 57 +++++++++++++++++++++++++++++++++ src/transmute.ts | 37 ++++++++++++++++----- 3 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 src/nip05.ts diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 539b032..77effcb 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -11,7 +11,7 @@ const credentialsController: AppController = async (c) => { const event = await getAuthor(pubkey); if (event) { - return c.json(toAccount(event, { withSource: true })); + return c.json(await toAccount(event, { withSource: true })); } return c.json({ error: 'Could not find user.' }, 404); @@ -22,7 +22,7 @@ const accountController: AppController = async (c) => { const event = await getAuthor(pubkey); if (event) { - return c.json(toAccount(event)); + return c.json(await toAccount(event)); } return c.json({ error: 'Could not find user.' }, 404); @@ -37,7 +37,7 @@ const accountLookupController: AppController = async (c) => { const event = await lookupAccount(acct); if (event) { - return c.json(toAccount(event)); + return c.json(await toAccount(event)); } return c.json({ error: 'Could not find user.' }, 404); @@ -52,7 +52,7 @@ const accountSearchController: AppController = async (c) => { const event = await lookupAccount(decodeURIComponent(q)); if (event) { - return c.json([toAccount(event)]); + return c.json([await toAccount(event)]); } return c.json([]); diff --git a/src/nip05.ts b/src/nip05.ts new file mode 100644 index 0000000..4416c98 --- /dev/null +++ b/src/nip05.ts @@ -0,0 +1,57 @@ +import { nip19, z } from '@/deps.ts'; + +const NIP05_REGEX = /^(?:([\w.+-]+)@)?([\w.-]+)$/; + +interface LookupOpts { + timeout?: number; +} + +/** Adapted from nostr-tools. */ +async function lookup(value: string, opts: LookupOpts = {}): Promise { + const { timeout = 1000 } = opts; + + const match = value.match(NIP05_REGEX); + if (!match) return; + + const [_, name = '_', domain] = match; + + try { + const res = await fetch(`https://${domain}/.well-known/nostr.json?name=${name}`, { + signal: AbortSignal.timeout(timeout), + }); + + const { names, relays } = nostrJsonSchema.parse(await res.json()); + + const pubkey = names[name]; + + if (pubkey) { + return { + pubkey, + relays: relays?.[pubkey], + }; + } + } catch (_e) { + return; + } +} + +const nostrJsonSchema = z.object({ + names: z.record(z.string(), z.string()), + relays: z.record(z.string(), z.array(z.string())).optional().catch(undefined), +}); + +async function verify(value: string, pubkey: string): Promise { + try { + const result = await nip05.lookup(value); + return result?.pubkey === pubkey; + } catch (_e) { + return false; + } +} + +const nip05 = { + lookup, + verify, +}; + +export { nip05 }; diff --git a/src/transmute.ts b/src/transmute.ts index 5da97b3..54e3a29 100644 --- a/src/transmute.ts +++ b/src/transmute.ts @@ -4,6 +4,7 @@ import { type MetaContent, parseMetaContent } from '@/schema.ts'; import { LOCAL_DOMAIN } from './config.ts'; import { getAuthor } from './client.ts'; +import { nip05 } from './nip05.ts'; import { getMediaLinks, type MediaLink, parseNoteContent } from './note.ts'; import { type Nip05, parseNip05 } from './utils.ts'; @@ -14,7 +15,7 @@ interface ToAccountOpts { withSource?: boolean; } -function toAccount(event: Event<0>, opts: ToAccountOpts = {}) { +async function toAccount(event: Event<0>, opts: ToAccountOpts = {}) { const { withSource = false } = opts; const { pubkey } = event; @@ -24,7 +25,9 @@ function toAccount(event: Event<0>, opts: ToAccountOpts = {}) { let parsed05: Nip05 | undefined; try { - parsed05 = parseNip05(nip05!); + if (nip05 && await verifyNip05Cached(nip05, pubkey)) { + parsed05 = parseNip05(nip05); + } } catch (_e) { // } @@ -63,9 +66,27 @@ function toAccount(event: Event<0>, opts: ToAccountOpts = {}) { }; } +const ONE_HOUR = 60 * 60 * 1000; + +const nip05Cache = new TTLCache>({ ttl: ONE_HOUR, max: 5000 }); + +function verifyNip05Cached(value: string, pubkey: string): Promise { + const cached = nip05Cache.get(value); + if (cached !== undefined) { + console.log(`Using cached NIP-05 for ${value}`); + return cached; + } + + console.log(`Verifying NIP-05 for ${value}`); + const result = nip05.verify(value, pubkey); + nip05Cache.set(value, result); + + return result; +} + async function toMention(pubkey: string) { const profile = await getAuthor(pubkey); - const account = profile ? toAccount(profile) : undefined; + const account = profile ? await toAccount(profile) : undefined; if (account) { return { @@ -88,7 +109,7 @@ async function toMention(pubkey: string) { async function toStatus(event: Event<1>) { const profile = await getAuthor(event.pubkey); - const account = profile ? toAccount(profile) : undefined; + const account = profile ? await toAccount(profile) : undefined; if (!account) return; const replyTag = findReplyTag(event); @@ -198,14 +219,14 @@ async function unfurlCard(url: string): Promise { const TWELVE_HOURS = 12 * 60 * 60 * 1000; -const previewCardCache = new TTLCache({ ttl: TWELVE_HOURS, max: 500 }); +const previewCardCache = new TTLCache>({ ttl: TWELVE_HOURS, max: 500 }); /** Unfurl card from cache if available, otherwise fetch it. */ -async function unfurlCardCached(url: string): Promise { - const cached = previewCardCache.get(url); +function unfurlCardCached(url: string): Promise { + const cached = previewCardCache.get(url); if (cached !== undefined) return cached; - const card = await unfurlCard(url); + const card = unfurlCard(url); previewCardCache.set(url, card); return card;