Skip to content

feat: attachments, webhooks, annual contrast fix, inbox auth probe#5

Merged
mqmalagris merged 7 commits into
mainfrom
dev
Apr 30, 2026
Merged

feat: attachments, webhooks, annual contrast fix, inbox auth probe#5
mqmalagris merged 7 commits into
mainfrom
dev

Conversation

@mqmalagris
Copy link
Copy Markdown
Owner

@mqmalagris mqmalagris commented Apr 30, 2026

Summary

  • Email attachments — processor extracts attachment metadata (filename, content-type, size, sha256, index) and stores it on the message; new GET /inbox/{addr}/messages/{id}/attachments/{idx} (and admin twin) re-parses the raw .eml already in S3 and streams the indexed file with Content-Disposition: attachment + nosniff so browsers always download. Hard limits at parse time (10 MB per file, 25 MB per message) and an extension blocklist (exe/bat/scr/msi/dll/js/vbs/...). Lambda now base64-encodes binary responses for API Gateway v2.
  • Webhooks for premiumPUT/GET/DELETE /user/webhook with HMAC-SHA256 signing. Processor fires synchronously after StoreMessage with a 5 s timeout, no retry; metadata-only payload (no email body) so consumers fetch via the API. Account page exposes URL input + secret reveal; /docs documents the headers, payload, and verification recipe (printf '%s' + openssl dgst, since echo -n adds CR/LF on Git Bash).
  • Annual price contrast fixtext-ghost was nearly invisible on the dark slab and text-white/70 had no light-theme override; switched to text-muted and text-white/80.
  • Inbox auth hardening — visiting /inbox/{addr} without a saved token would render the shell as if the visitor owned the inbox while polls silently 401'd. Now the page probes the API once before rendering and falls back to the user's API key for premium users navigating from their account; backend authenticateInbox accepts the user api key when inbox.UserID == user.UserID.

Test plan

  • pnpm build succeeds and go test ./... passes
  • Send an email with a PDF or image attachment to a registered inbox → it appears under "Attachments" with size; click Download → file saves with the right filename, never renders inline
  • Send an email with a .exe → silently dropped from the metadata
  • Premium: set webhook URL on /account → submit email → POST hits the configured URL within seconds with X-Ephemask-Signature: sha256=...; recompute HMAC locally and confirm match
  • Free user attempting to set webhook → 403
  • Visit /inbox/random@ephemask.com as anon or as a different user → redirects to home (no shell flash)
  • Click an inbox from /account → Active Inboxes → loads even without sessionStorage token (api-key fallback)
  • Pricing card on the landing renders \$4/mo and \$40/yr · Save 17% with readable contrast in both dark and light themes

text-ghost (#2a3a4e) was nearly invisible against the dark slab background;
switch annual price line and plan picker labels to text-muted/text-white-70.
text-white/70 has no theme-clean override, rendering as 70% white on the
light background (invisible). Switch to /80 which has #3a352f override.
Processor now walks the MIME tree and stores attachment metadata (filename,
content type, size, sha256, index) alongside the message. The bytes stay in
the raw .eml that SES already drops in S3 with a 1-day lifecycle, so no
duplicate storage.

Adds GET /inbox/{addr}/messages/{id}/attachments/{idx} (and the admin twin)
which re-parses the .eml on demand, returns the indexed attachment with
Content-Disposition: attachment + Content-Type: application/octet-stream +
X-Content-Type-Options: nosniff so browsers always download (never render
inline).

Hard limits applied at parse time: 10MB per attachment, 25MB total per
message, plus an extension blocklist (exe/bat/scr/msi/dll/js/vbs/...).

API Lambda now base64-encodes binary responses for API Gateway v2; Terraform
gains the new routes.

Frontend lists attachments under the message body with per-file size and a
download button that calls the new endpoint.
Premium users can register an https URL via PUT /user/webhook. Each delivery
is signed with HMAC-SHA256 over the JSON body using a per-user secret
generated server-side; receivers verify with the X-Ephemask-Signature
header.

Processor fires the webhook synchronously after StoreMessage with a 5s
timeout, no retries (best-effort) — the inbox always has the message
regardless of delivery success. Payload carries metadata only
(inbox_address, message_id, from, subject, received_at, attachment_count);
the email body is never included so consumers fetch via the API.

Adds GET/PUT/DELETE /user/webhook endpoints (premium-gated), webhook UI in
the account page (URL input, secret reveal with copy, remove button), and
docs section explaining the headers and signature verification.

CORS allow-methods now includes PUT.
The /inbox/{address} page would render the layout for any address typed
into the URL. The poll fetch silently failed because no inbox token was
stored, but the UI still showed the address as if it belonged to the
visitor — confusing at best, dangerous if someone shared the link.

Now: when there is no saved token for the address, fall back to the
logged-in user's API key. The backend authenticateInbox accepts the user
api key when the inbox belongs to that user, so premium users navigating
to their own inboxes from the account page still work. With neither
token nor matching user key, redirect to home where the visitor can
create their own inbox.
The previous fix fell back to user.apiKey without verifying it actually
authorized for this inbox. Anyone with any saved api key would see the
inbox UI render while the polls silently 401'd in the background.

Now: do a single getInbox call with the candidate token (saved inbox
token or user api key) before showing the UI. On 401 / any error,
redirect home; on success, seed messages from the probe response so
the first poll already has data.
echo -n adds CR/LF on Git Bash and several other Windows shells, which
silently breaks the signature comparison.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ephemask-web Ready Ready Preview, Comment Apr 30, 2026 11:49pm

@mqmalagris mqmalagris self-assigned this Apr 30, 2026
@mqmalagris mqmalagris merged commit 5246ded into main Apr 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant