Commit graph

16 commits

Author SHA1 Message Date
Agent
4b43664411 extract shared brandingVars() helper to eliminate duplication
All checks were successful
CI / checks (pull_request) Successful in 35s
The { brandName, brandAccent, brandLogo, settingsUrl } object was built
identically 3 times in the /confirm handler. Extract a brandingVars()
helper so it is defined once and reused via spread.

Closes mailship-90c
2026-09-16 09:56:13 -04:00
Agent
dbde1ec02d Normalize EVENT_VIEWER_URL trailing slash once in env.ts, remove 6 ad-hoc strips
All checks were successful
CI / checks (pull_request) Successful in 35s
EVENT_VIEWER_URL.replace(/\/$/, '') was duplicated across:
- env.ts (BRAND_LOGO, 1x)
- server.ts (settingsUrl in confirm routes, 3x)
- mailer.ts (settingsUrl in sendConfirm/sendDigest, 2x)

Now trailing-slash normalization happens at the export source in env.ts,
so all consumers get a clean URL without ad-hoc stripping.
2026-09-16 09:28:03 -04:00
Agent
a0a284b0a3 Route BASE_URL through env module instead of reading process.env directly
All checks were successful
CI / checks (pull_request) Successful in 33s
server.ts was building callback URLs from raw process.env.BASE_URL at
lines 175 and 217, while env.ts already validates and exports BASE_URL
as a typed constant. This adds BASE_URL to the import from ./env.js and
replaces both direct process.env references so the callback URL cannot
drift from the validated value.
2026-09-14 16:08:40 -04:00
Agent
b54fb4f891 POST /notify/🆔 standardize response shape — always include stored field
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
2026-09-14 13:42:11 -04:00
Agent
40ac047629 Implement NIP-98 HTTP auth for GET/PUT/DELETE /subscription/email
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).
2026-09-14 13:04:26 -04:00
Agent
a4463fdab4 Merge remote-tracking branch 'origin/main' into mailship-32x-cors-is-not-production-safe-wildcard-ori-52b 2026-09-10 12:25:26 -04:00
Agent
fdc4579aa7 fix: scope CORS to browser routes only, require CORS_ORIGIN (fail closed)
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
2026-09-10 11:29:18 -04:00
Agent
75f5908039 Strip out the web UI (login + subscription filter management)
Remove the browser admin SPA under web/ that let users log in via
a Nostr signer and manage subscription filters. The server is now
a headless API only.

Changes:
- Delete web/ directory entirely (SPA source, config, deps)
- Remove express.static('web/dist') serving from server.ts
- Simplify GET / handler to always return JSON (no fallback)
- Remove build:web from package.json build pipeline
- Remove web build steps from Dockerfile
- Remove web references from build-in-production.sh

Kept: core subscription/unsubscribe/confirm/notify backend and
transactional src/pages/*.html (part of email flow, not the UI).
2026-09-10 10:08:14 -04:00
mplorentz
5abec46cb7 Make subscription upsert idempotent via PUT
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.
2026-09-03 11:47:21 -04:00
mplorentz
d143a563ba Process nostr events included directly in the callback. 2026-08-27 11:28:03 -04:00
mplorentz
4e8918c1f3 Add branding to email confirmation screen 2026-08-27 10:30:46 -04:00
mplorentz
77e35c26c7 log errors 2026-08-26 17:21:00 -04:00
mplorentz
a799e1ba1c add permissive CORS headers 2026-08-20 12:42:31 -04:00
Agent
f220301154 Add integration test script, fix root handler, add DATA_DIR support
- 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
2026-08-18 12:35:20 -04:00
Agent
25645e1d0e Fix unsubscribe page path, remove pool override, handle Postmark error gracefully
- Fix unsubscribe page path (unsubscribe-success.html → unsubscribe.html)
- Remove netContext.pool.get override that crashed load()
- Handle Postmark registration error gracefully by returning subscription from DB
- Add getSubscriptionByPubkey import to server.ts
2026-08-18 12:26:07 -04:00
Agent
21e7058862 Initial mailship fork from anchor
Fork anchor, strip all push notification code (APNs, FCM, WebPush),
rename from anchor to mailship, add new database schema for
subscriptions + events tables, and add HTTP API for email
notification registration and NIP-9a relay push callbacks.

- POST /subscription/email — register for email digests
- DELETE /subscription/:key — unsubscribe
- POST /notify/:id — NIP-9a relay push callback
- GET /confirm?token=... — confirm email
- GET /unsubscribe?token=... — unsubscribe

Co-authored-by: mplorentz
2026-08-18 12:21:22 -04:00