Merge branch 'main' into cron-minutely

This commit is contained in:
mplorentz 2026-08-27 11:28:12 -04:00
commit 2644310bfa

View file

@ -1,12 +1,13 @@
import { instrument } from 'succinct-async' import { instrument } from 'succinct-async'
import express, { Request, Response, NextFunction } from 'express' import express, { Request, Response, NextFunction } from 'express'
import rateLimit from 'express-rate-limit' 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 { render } from './templates.js'
import { confirmSubscriptionAction, unsubscribeAction, registerSubscription, ActionError } from './actions.js' import { confirmSubscriptionAction, unsubscribeAction, registerSubscription, ActionError } from './actions.js'
import { getSubscriptionById, insertEvent, getSubscriptionByKey, getSubscriptionByPubkey } from './database.js' import { getSubscriptionById, insertEvent, getSubscriptionByKey, getSubscriptionByPubkey } from './database.js'
import { load } from '@welshman/net' import { load } from '@welshman/net'
import { getIdFilters } from '@welshman/util' import { getIdFilters } from '@welshman/util'
import { verifyEvent } from 'nostr-tools/pure'
// Endpoints // Endpoints
@ -147,7 +148,7 @@ addRoute('delete', '/subscription/:key', async (req: Request, res: Response) =>
// NIP-9a relay push callback // NIP-9a relay push callback
addRoute('post', '/notify/:id', async (req: Request, res: Response) => { addRoute('post', '/notify/:id', async (req: Request, res: Response) => {
const { id, relay } = req.body const { id, relay, event } = req.body
if (!id || !relay) { if (!id || !relay) {
return res.status(400).json({ error: 'id and relay are required' }) 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' }) return res.status(404).json({ error: 'Subscription not active' })
} }
// Fetch the full event from the relay
try { try {
const [event] = await load({ let storedEvent = event
relays: [relay],
filters: getIdFilters([id]),
})
if (!event) { if (storedEvent) {
// Event not found at relay — don't 404, just skip // If the subscription requested include_event, verify and use it directly
return res.json({ ok: true, skipped: true }) 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 }) return res.json({ ok: true, stored })
} catch (error) { } catch (error) {
@ -198,7 +210,12 @@ addRoute('get', '/confirm', async (req: Request, res: Response) => {
try { try {
await confirmSubscriptionAction({ token: req.query.token }) 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) { } catch (error) {
const isActionError = error instanceof ActionError const isActionError = error instanceof ActionError
const message = isActionError ? String(error) : 'Oops, something went wrong on our end!' 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 { } else {
next() next()
} }
}) })
// 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)
}