diff --git a/src/server.ts b/src/server.ts index 9be5d7f..1cc71de 100644 --- a/src/server.ts +++ b/src/server.ts @@ -7,6 +7,7 @@ import { confirmSubscriptionAction, unsubscribeAction, registerSubscription, Act import { getSubscriptionById, insertEvent, getSubscriptionByKey, getSubscriptionByPubkey } from './database.js' import { load } from '@welshman/net' import { getIdFilters } from '@welshman/util' +import { verifyEvent } from 'nostr-tools/pure' // Endpoints @@ -147,7 +148,7 @@ addRoute('delete', '/subscription/:key', async (req: Request, res: Response) => // NIP-9a relay push callback addRoute('post', '/notify/:id', async (req: Request, res: Response) => { - const { id, relay } = req.body + const { id, relay, event } = req.body if (!id || !relay) { return res.status(400).json({ error: 'id and relay are required' }) @@ -164,19 +165,30 @@ addRoute('post', '/notify/:id', async (req: Request, res: Response) => { return res.status(404).json({ error: 'Subscription not active' }) } - // Fetch the full event from the relay try { - const [event] = await load({ - relays: [relay], - filters: getIdFilters([id]), - }) + let storedEvent = event - if (!event) { - // Event not found at relay — don't 404, just skip - return res.json({ ok: true, skipped: true }) + if (storedEvent) { + // If the subscription requested include_event, verify and use it directly + if (storedEvent.id !== id || !validEvent(storedEvent)) { + return res.status(400).json({ error: 'Invalid event' }) + } + } else { + // Otherwise fetch the full event from the relay + const [fetched] = await load({ + relays: [relay], + filters: getIdFilters([id]), + }) + + storedEvent = fetched + + if (!storedEvent) { + // Event not found at relay — don't 404, just skip + return res.json({ ok: true, skipped: true }) + } } - const stored = await insertEvent(id, sub.id, event, relay) + const stored = await insertEvent(id, sub.id, storedEvent, relay) return res.json({ ok: true, stored }) } catch (error) { @@ -238,4 +250,11 @@ server.use((err: Error, req: Request, res: Response, next: NextFunction) => { } else { next() } -}) \ No newline at end of file +}) + +// Validate an event's signature and that its id hash matches (defense against +// a malicious relay forwarding tampered content via include_event). +const validEvent = (event: any) => { + if (!event || typeof event !== 'object') return false + return verifyEvent(event) +} \ No newline at end of file