diff --git a/netlify/functions/nip05.mjs b/netlify/functions/nip05.mjs index 094f3cf..3629f59 100644 --- a/netlify/functions/nip05.mjs +++ b/netlify/functions/nip05.mjs @@ -112,6 +112,26 @@ function verifyAuth(header, action) { return { pubkey: ev.pubkey, name: normalizeName(tagVal(ev.tags, 'name')) }; } +// Best-effort in-memory throttle for the write endpoints. Serverless instances +// are ephemeral, so this is a blunt abuse damper (mass name-squatting), not a +// hard guarantee — the real gate is the signature on every claim. +const WINDOW_MS = 60_000; +const LIMIT = 12; +const hits = new Map(); + +function throttled(request) { + const ip = + request.headers.get('x-nf-client-connection-ip') || + request.headers.get('x-forwarded-for') || + 'unknown'; + const now = Date.now(); + const recent = (hits.get(ip) ?? []).filter((t) => now - t < WINDOW_MS); + if (recent.length >= LIMIT) return true; + recent.push(now); + hits.set(ip, recent); + return false; +} + // ── Handlers ────────────────────────────────────────────────────────────────── /** GET /.well-known/nostr.json[?name=x] — the NIP-05 document itself. */ @@ -153,6 +173,7 @@ async function handleCheck(url) { /** POST /api/nip05/claim — reserve `name` for the signing pubkey. */ async function handleClaim(request) { + if (throttled(request)) return json({ error: 'Too many requests.' }, 429); const auth = verifyAuth(request.headers.get('x-nip05-auth'), 'claim'); if (!auth) return json({ error: 'Signature required.' }, 401); const { name, pubkey } = auth; @@ -180,6 +201,7 @@ async function handleClaim(request) { /** POST /api/nip05/release — remove a name (its owner, or a platform owner). */ async function handleRelease(request) { + if (throttled(request)) return json({ error: 'Too many requests.' }, 429); const auth = verifyAuth(request.headers.get('x-nip05-auth'), 'release'); if (!auth) return json({ error: 'Signature required.' }, 401); const { name, pubkey } = auth;