Merge branch 'main' into cron-minutely
This commit is contained in:
commit
2644310bfa
1 changed files with 37 additions and 13 deletions
|
|
@ -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!'
|
||||||
|
|
@ -234,3 +251,10 @@ server.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||||
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)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue