Verify NIP05's with cache, fixes #1
This commit is contained in:
parent
f567acb58f
commit
af9f376ad0
|
@ -11,7 +11,7 @@ const credentialsController: AppController = async (c) => {
|
||||||
|
|
||||||
const event = await getAuthor(pubkey);
|
const event = await getAuthor(pubkey);
|
||||||
if (event) {
|
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);
|
return c.json({ error: 'Could not find user.' }, 404);
|
||||||
|
@ -22,7 +22,7 @@ const accountController: AppController = async (c) => {
|
||||||
|
|
||||||
const event = await getAuthor(pubkey);
|
const event = await getAuthor(pubkey);
|
||||||
if (event) {
|
if (event) {
|
||||||
return c.json(toAccount(event));
|
return c.json(await toAccount(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({ error: 'Could not find user.' }, 404);
|
return c.json({ error: 'Could not find user.' }, 404);
|
||||||
|
@ -37,7 +37,7 @@ const accountLookupController: AppController = async (c) => {
|
||||||
|
|
||||||
const event = await lookupAccount(acct);
|
const event = await lookupAccount(acct);
|
||||||
if (event) {
|
if (event) {
|
||||||
return c.json(toAccount(event));
|
return c.json(await toAccount(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({ error: 'Could not find user.' }, 404);
|
return c.json({ error: 'Could not find user.' }, 404);
|
||||||
|
@ -52,7 +52,7 @@ const accountSearchController: AppController = async (c) => {
|
||||||
|
|
||||||
const event = await lookupAccount(decodeURIComponent(q));
|
const event = await lookupAccount(decodeURIComponent(q));
|
||||||
if (event) {
|
if (event) {
|
||||||
return c.json([toAccount(event)]);
|
return c.json([await toAccount(event)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json([]);
|
return c.json([]);
|
||||||
|
|
|
@ -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<nip19.ProfilePointer | undefined> {
|
||||||
|
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<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await nip05.lookup(value);
|
||||||
|
return result?.pubkey === pubkey;
|
||||||
|
} catch (_e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nip05 = {
|
||||||
|
lookup,
|
||||||
|
verify,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { nip05 };
|
|
@ -4,6 +4,7 @@ import { type MetaContent, parseMetaContent } from '@/schema.ts';
|
||||||
|
|
||||||
import { LOCAL_DOMAIN } from './config.ts';
|
import { LOCAL_DOMAIN } from './config.ts';
|
||||||
import { getAuthor } from './client.ts';
|
import { getAuthor } from './client.ts';
|
||||||
|
import { nip05 } from './nip05.ts';
|
||||||
import { getMediaLinks, type MediaLink, parseNoteContent } from './note.ts';
|
import { getMediaLinks, type MediaLink, parseNoteContent } from './note.ts';
|
||||||
import { type Nip05, parseNip05 } from './utils.ts';
|
import { type Nip05, parseNip05 } from './utils.ts';
|
||||||
|
|
||||||
|
@ -14,7 +15,7 @@ interface ToAccountOpts {
|
||||||
withSource?: boolean;
|
withSource?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toAccount(event: Event<0>, opts: ToAccountOpts = {}) {
|
async function toAccount(event: Event<0>, opts: ToAccountOpts = {}) {
|
||||||
const { withSource = false } = opts;
|
const { withSource = false } = opts;
|
||||||
|
|
||||||
const { pubkey } = event;
|
const { pubkey } = event;
|
||||||
|
@ -24,7 +25,9 @@ function toAccount(event: Event<0>, opts: ToAccountOpts = {}) {
|
||||||
|
|
||||||
let parsed05: Nip05 | undefined;
|
let parsed05: Nip05 | undefined;
|
||||||
try {
|
try {
|
||||||
parsed05 = parseNip05(nip05!);
|
if (nip05 && await verifyNip05Cached(nip05, pubkey)) {
|
||||||
|
parsed05 = parseNip05(nip05);
|
||||||
|
}
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
@ -63,9 +66,27 @@ function toAccount(event: Event<0>, opts: ToAccountOpts = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ONE_HOUR = 60 * 60 * 1000;
|
||||||
|
|
||||||
|
const nip05Cache = new TTLCache<string, Promise<boolean>>({ ttl: ONE_HOUR, max: 5000 });
|
||||||
|
|
||||||
|
function verifyNip05Cached(value: string, pubkey: string): Promise<boolean> {
|
||||||
|
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) {
|
async function toMention(pubkey: string) {
|
||||||
const profile = await getAuthor(pubkey);
|
const profile = await getAuthor(pubkey);
|
||||||
const account = profile ? toAccount(profile) : undefined;
|
const account = profile ? await toAccount(profile) : undefined;
|
||||||
|
|
||||||
if (account) {
|
if (account) {
|
||||||
return {
|
return {
|
||||||
|
@ -88,7 +109,7 @@ async function toMention(pubkey: string) {
|
||||||
|
|
||||||
async function toStatus(event: Event<1>) {
|
async function toStatus(event: Event<1>) {
|
||||||
const profile = await getAuthor(event.pubkey);
|
const profile = await getAuthor(event.pubkey);
|
||||||
const account = profile ? toAccount(profile) : undefined;
|
const account = profile ? await toAccount(profile) : undefined;
|
||||||
if (!account) return;
|
if (!account) return;
|
||||||
|
|
||||||
const replyTag = findReplyTag(event);
|
const replyTag = findReplyTag(event);
|
||||||
|
@ -198,14 +219,14 @@ async function unfurlCard(url: string): Promise<PreviewCard | null> {
|
||||||
|
|
||||||
const TWELVE_HOURS = 12 * 60 * 60 * 1000;
|
const TWELVE_HOURS = 12 * 60 * 60 * 1000;
|
||||||
|
|
||||||
const previewCardCache = new TTLCache({ ttl: TWELVE_HOURS, max: 500 });
|
const previewCardCache = new TTLCache<string, Promise<PreviewCard | null>>({ ttl: TWELVE_HOURS, max: 500 });
|
||||||
|
|
||||||
/** Unfurl card from cache if available, otherwise fetch it. */
|
/** Unfurl card from cache if available, otherwise fetch it. */
|
||||||
async function unfurlCardCached(url: string): Promise<PreviewCard | null> {
|
function unfurlCardCached(url: string): Promise<PreviewCard | null> {
|
||||||
const cached = previewCardCache.get<PreviewCard | null>(url);
|
const cached = previewCardCache.get(url);
|
||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) return cached;
|
||||||
|
|
||||||
const card = await unfurlCard(url);
|
const card = unfurlCard(url);
|
||||||
previewCardCache.set(url, card);
|
previewCardCache.set(url, card);
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
|
|
Loading…
Reference in New Issue