diff --git a/src/server.ts b/src/server.ts index 204fb19..1cc71de 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,12 +1,13 @@ import { instrument } from 'succinct-async' import express, { Request, Response, NextFunction } from 'express' import rateLimit from 'express-rate-limit' -import { appSigner } from './env.js' +import { appSigner, BRAND_NAME, BRAND_ACCENT, BRAND_LOGO, EVENT_VIEWER_URL } from './env.js' import { render } from './templates.js' import { confirmSubscriptionAction, unsubscribeAction, registerSubscription, ActionError } from './actions.js' 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) { @@ -198,7 +210,12 @@ addRoute('get', '/confirm', async (req: Request, res: Response) => { try { await confirmSubscriptionAction({ token: req.query.token }) - res.send(await render('pages/confirm-success.html')) + res.send(await render('pages/confirm-success.html', { + brandName: BRAND_NAME, + brandAccent: BRAND_ACCENT, + brandLogo: BRAND_LOGO, + settingsUrl: `${EVENT_VIEWER_URL.replace(/\/$/, '')}/settings/alerts`, + })) } catch (error) { const isActionError = error instanceof ActionError const message = isActionError ? String(error) : 'Oops, something went wrong on our end!' @@ -233,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