Skip to content

Add server-to-server subscription status check (STATUS_KEY) - #3

Merged
wachtelhund merged 1 commit into
mainfrom
nilsson/subscription-status-1d5f
Nov 6, 2026
Merged

wachtelhund merged 1 commit into
mainfrom
nilsson/subscription-status-1d5f

Conversation

@wachtelhund

Copy link
Copy Markdown
Contributor

Fredrik approved a status lookup for the web-app (srcfl/web-app.sourceful.energy on Vercel) so /api/newsletter GET can show whether the signed-in user is on a list such as app-users. Two hard conditions are in the code:

  1. Only the web-app server may call it, with a secret STATUS_KEY. A form key is refused (401 form_key_not_allowed), even if STATUS_KEY was accidentally set to the same value. Form keys stay for POST /subscribe and POST /unsubscribe only.
  2. One address per request; the answer is only { "subscribed": true } or { "subscribed": false }. No name, consent, source, or suppression reason. false covers never signed up, left, and bounced.

Endpoint contract (for the web-app)

Method POST (GET is 405: an address in the URL would land in access logs)
URL https://news.sourceful.energy/subscribe/status
Auth Authorization: Bearer <STATUS_KEY>
Content-Type application/json (2 KB max)
Body { "email": "<logged-in user only>", "list": "app-users" }
Success 200 { "subscribed": true | false } with Cache-Control: no-store
No CORS browser / app must not call this

Errors: key_required, invalid_key, form_key_not_allowed, invalid_email, invalid_list, invalid_body, too_large, unsupported_media_type, rate_limited (Retry-After: 60), status_unavailable, method_not_allowed.

Env var (Bitwarden)

Worker secret name: STATUS_KEY

Generate with openssl rand -hex 32 (32+ characters). Store in Bitwarden as STATUS_KEY, next to ADMIN_TOKEN and FORM_KEYS. It must be a different value from every form key.

On Vercel, keep the same value under a name that cannot be mixed up with a form key, e.g. UTSKICK_STATUS_KEY. Do not put it in FORM_KEYS.

This PR does not create or print a real key. The next Worker deploy needs STATUS_KEY in the secrets file (required binding, like FORM_KEYS). config/sourceful.json already names status.lists: ["app-users"] and statusLimiterNamespace: "7002". No deploy from this PR.

Other design

  • status.lists in config is the allowlist (Sourceful: app-users only). Empty/absent → 503 status_unavailable.
  • Rate limit: STATUS_LIMITER, 120/min per calling server (CF-Connecting-IP). X-Client-IP is ignored so a stolen key cannot mint a new budget per guessed address. Failed auth does not count.
  • Logs: { event: "subscription_status", list, subscribed } on 200, { event: "form_key_rejected", path: "/subscribe/status" } when a form key is used. No address, no key.
  • Lookup is by email hash. Tests in test/status.test.ts (225 tests pass).

Docs: docs/signup.md section 4, plus setup/README/SECURITY.

Open in Web Open in Cursor 

…t from form keys

POST /subscribe/status is for the web-app's own server. It authenticates with STATUS_KEY,
refuses form keys (they must not reveal who subscribes), and answers only { subscribed }
for one address and one configured list.

Co-authored-by: Nilsson <wachtelhund@users.noreply.github.com>
@wachtelhund
wachtelhund merged commit 7e52659 into main Nov 6, 2026
1 check 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.

2 participants