Instead of setting context = events (which made repliesByParentId always
empty), query the repository for events that #e-tag our matched event
IDs with kinds NOTE, COMMENT, or REACTION. This gives buildParameters
real reply/reaction data to count.
Fixes the bug where every event in the digest email showed
'0 replies / 0 reactions'.
Added test/digest-reply-stats.test.ts that:
- Publishes reply and reaction events into a mock repository
- Calls sendFromStoredEvents with only the parent event
- Asserts that the resulting digest parameters show Replies >= 1 and
Reactions >= 1
Bug: when the event was not found at the relay, the handler returned
{ ok: true, skipped: true }, which did not match the documented contract
{ ok: true, stored: boolean } in README.md.
Fix: change the 'skipped' response to { ok: true, stored: false }, so the
response shape is consistent across all code paths.
- src/server.ts: changed line 287 from { ok: true, skipped: true } to
{ ok: true, stored: false }, with updated comment
- README.md: added a note documenting the skip case (stored: false)
- test/notify-response-shape.test.ts: new test that asserts stored=false
and that skipped is never present
- Add vitest as dev dep (pnpm add -D vitest), create vitest.config.ts
with globals:true and setupFiles
- Migrate digest-template, normalize-relay-url, reschedule-on-frequency-change,
and event-arrival-race from raw JS + hand-rolled asserts to vitest
describe/it/expect, as .test.ts (port all assertions without weakening)
- Create test/setup.ts with env vars needed by env.ts/mailer.ts
- DELETE test/web-ui.test.js: dead test for removed web UI (PR #1)
- package.json: add test:unit script (vitest run), keep test = bash E2E
- script/checks: add pnpm test:unit after build
- README: document pnpm test:unit / pnpm test
Bug: runJob computed since = sub.last_digest_at, fetched events with
received_at > since, sent the digest (slow), then deleted ALL events
with received_at > since. Any /notify event that arrived between the
fetch and the delete was also received_at > since, so it was deleted
without ever being sent in a digest.
Two changes:
1. Delete by exact event IDs (src/database.ts, src/worker/email.ts):
Added deleteEventsByIds(subscriptionId, eventIds) which deletes
only the events that were actually fetched + sent. The old
timestamp-based delete is retained but no longer called from runJob.
2. Re-fetch subscription on each cron tick (src/worker/email.ts):
createJob's closure captured the original sub, so sub.last_digest_at
stayed stale in memory. Every subsequent tick recomputed since from
the old value, re-fetching and re-sending duplicate events. Now each
tick re-fetches the subscription from the DB via getSubscriptionById
before calling runJob.
Fixes bead mailship-091
The test was failing because src/env.ts requires CORS_ORIGIN to be set,
but the integration test never exported it. This is a pre-existing setup
gap exposed by running the test — not a NIP-98 regression.
Three browser-facing endpoints now require a kind-27235 HTTP auth event
(NIP-98) proving the caller controls the pubkey:
- GET /subscription/email — pubkey extracted from auth header instead
of query param; returns subscription for the authed pubkey.
- PUT /subscription/email — pubkey extracted from auth header instead
of trusting a client-supplied body field.
- DELETE /subscription/:key — verifies auth pubkey matches subscription
owner (returns 403 if mismatch).
Server-side: decode base64 'Nostr <b64>' Authorization header, JSON.parse,
check kind === 27235, verifyEvent (nostr-tools/pure), then check u /
method / payload tags against the request URL / method / body.
README updated to reflect 'implemented' auth (not 'planned').
Integration test updated to generate NIP-98 auth headers via a new helper
script (script/nip98-auth-header.mjs).
When a confirmed subscriber calls PUT /subscription/email with a changed
frequency, db.updateSubscription updates the row but the running CronJob
captured the original frequency in createJob and was never rescheduled.
A subscriber switching daily→weekly kept the daily cadence until restart.
Fix: in actions.ts:registerSubscription, call worker.registerSubscription(sub)
when the subscription is already confirmed, so addJob stops the old job and
creates a new one with the updated frequency.
Changes:
- src/actions.ts: add else branch calling worker.registerSubscription when
sub.confirmed_at is set
- test/reschedule-on-frequency-change.test.js: new failing-before/passing-after
test verifying the cron expression is updated after frequency change
Closes mailship-e04
Replace all Postmark references (POSTMARK_API_KEY, POSTMARK_SENDER_ADDRESS)
with SMTP configuration (SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD,
SMTP_FROM) across documentation, deployment config, template, and test.
- README.md: architecture diagram (Postmark → SMTP) and configuration table
- docker-compose.yml: POSTMARK_* env vars replaced with SMTP_* required vars
- .env.template: POSTMARK_* entries replaced with SMTP_* entries
- test/integration.sh: POSTMARK_* test exports replaced with SMTP_* test values
The server was setting Access-Control-Allow-Origin: * on every route,
including the unauthenticated GET /subscription/email which returns the
subscriber's email address. Any website could query known pubkeys and
harvest emails.
Changes:
- src/env.ts: require CORS_ORIGIN env var (fail closed, no wildcard)
- src/server.ts: scope CORS middleware to browser-facing routes only,
skip /notify (server-to-server), add Vary: Origin header, import
CORS_ORIGIN from env instead of defaulting to '*'
- .env.template: document new CORS_ORIGIN variable
- test/cors.test.sh: verify CORS on browser routes, no CORS on
server-to-server routes, Vary: Origin presence
Lines 8 and 22 of digest.mjml hardcoded #7161FF for the event-item
border-left and footer link color, while mailer.ts already passes
brandAccent into the template (used at lines 15 and 34). When
BRAND_ACCENT is customized, the border and footer links stayed the
default purple.
Drive both from {{brandAccent}} so they respect the customization.
Fixes bead mailship-hu5
Switch the subscribe endpoint from POST to an idempotent PUT so clients can
always upsert without a stateful lookup first.
- Add GET /subscription/email?pubkey= lookup so clients can avoid re-POSTing.
- insertSubscription no longer clears confirmed_at unless the email address
changes; a frequency change keeps the existing confirmation.
- registerSubscription only sends a confirmation email when the subscription
is new, unconfirmed, or the email address changed.
- Update integration test and README for the PUT endpoint.
Adds a test that validates the fix for the web UI crash when
VITE_NOTIFIER_RELAY is not set. The test exercises the guarded
pattern (ternary guard before calling normalizeRelayUrl) that
prevents the TypeError crash on undefined input.
Closes mailship-4ic
The web UI crashed on load when VITE_NOTIFIER_RELAY was not set
because normalizeRelayUrl(undefined) calls url.match(...) on the
undefined argument, producing:
TypeError: can't access property 'match', A is undefined
Fix: guard with a truthy check before calling normalizeRelayUrl,
and early-return from loadAlerts when NOTIFIER_RELAY is undefined.
Fixes bead mailship-4ic
- test/integration.sh: runs full E2E flow against fresh server
- pnpm test and pnpm test:server scripts added
- Root endpoint handles missing web UI gracefully (returns JSON)
- database.ts reads DATA_DIR env var for test isolation
- .gitignore updated for test artifacts