From f7e0c99764d5292985762929120ce72b229354ca Mon Sep 17 00:00:00 2001 From: mplorentz Date: Thu, 27 Aug 2026 10:17:06 -0400 Subject: [PATCH 1/3] Update subscription confirmation email template --- src/mailer.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/mailer.ts b/src/mailer.ts index b8e65a0..55889a8 100644 --- a/src/mailer.ts +++ b/src/mailer.ts @@ -29,12 +29,13 @@ const transporter = nodemailer.createTransport({ export const sendConfirm = (sub: Subscription) => { const href = `${BASE_URL}/confirm?token=${sub.key}` + const settingsUrl = `${EVENT_VIEWER_URL.replace(/\/$/, '')}/settings/alerts` return transporter .sendMail({ from: SMTP_FROM, to: sub.email, - subject: `Confirm your ${BRAND_NAME} digest`, + subject: `Confirm your email for ${BRAND_NAME} notifications`, html: ` @@ -42,18 +43,17 @@ export const sendConfirm = (sub: Subscription) => {
-
- ${BRAND_LOGO ? `${BRAND_NAME}` : `

${BRAND_NAME}

`} -

Digest notifications

+ ${BRAND_LOGO ? `${BRAND_NAME}` : ''} +

${BRAND_NAME}

-

Welcome to ${BRAND_NAME}!

-

Please confirm that you would like to receive ${sub.frequency} digests by clicking the button below:

+
+

Please click below to confirm your email address. We'll send occasional emails with updates from your communities on ${BRAND_NAME}.

- Confirm Digest + Confirm

-

Or visit: ${EVENT_VIEWER_URL}

+

To disable notifications, ignore this email. Or update your settings at ${EVENT_VIEWER_URL.replace(/^https?:\/\//, '')}/settings/alerts.

@@ -61,7 +61,7 @@ export const sendConfirm = (sub: Subscription) => { `, - text: `Please confirm that you would like to receive ${sub.frequency} digests by visiting: ${href}`, + text: `Please click below to receive emails for updates from your communities on ${BRAND_NAME}.\n\nConfirm: ${href}\n\nTo disable notifications, ignore this email. Or update your settings at: ${settingsUrl}`, }) .catch(error => { console.error('mailer: confirmation email failed', { From 4e8918c1f35b29249b2a7d3f8189464a337a988e Mon Sep 17 00:00:00 2001 From: mplorentz Date: Thu, 27 Aug 2026 10:30:46 -0400 Subject: [PATCH 2/3] Add branding to email confirmation screen --- src/server.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/server.ts b/src/server.ts index 204fb19..9be5d7f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,7 +1,7 @@ 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' @@ -198,7 +198,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!' From d143a563ba31f2a6392d2779300643d0b942cb50 Mon Sep 17 00:00:00 2001 From: mplorentz Date: Thu, 27 Aug 2026 11:28:03 -0400 Subject: [PATCH 3/3] Process nostr events included directly in the callback. --- src/server.ts | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) 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