Skip to content

fix(whatsapp): keep the stored verify_token when config is saved without one - #554

Open
Guru35 wants to merge 2 commits into
ArnasDon:mainfrom
Guru35:fix/keep-stored-verify-token
Open

fix(whatsapp): keep the stored verify_token when config is saved without one#554
Guru35 wants to merge 2 commits into
ArnasDon:mainfrom
Guru35:fix/keep-stored-verify-token

Conversation

@Guru35

@Guru35 Guru35 commented Sep 6, 2026

Copy link
Copy Markdown

Summary

Saving the WhatsApp settings form clears whatsapp_config.verify_token unless
the token field happens to be filled in — which it never is, because the form
does not render the stored value. Every save of any other field (phone
number id, WABA id, access token…) silently nulls the verify token, and Meta's
webhook GET handshake then returns 403. From the outside this is
indistinguishable from "you typed the wrong token".

Root cause is one expression in the save path:

verify_token: incoming ? encrypt(incoming) : null

An empty field means "keep what is stored", not "clear it" — the settings
GET endpoint deliberately never returns the token (it is encrypted at rest),
so empty is the normal state of that input.

Measured on a live self-hosted instance (2026-09-05): the row held
verify_token = NULL, Meta's verification GET returned 403, inbound message
delivery stopped. Restoring the token fixed it; saving any other field broke
it again.

What changed

  • src/lib/whatsapp/verify-token.ts — new resolveVerifyTokenForSave(incoming, existingEncrypted):
    a non-empty value is encrypted and stored, an empty/whitespace value keeps
    the existing ciphertext. Clearing a token on its own is not a supported
    operation; Reset Configuration already wipes the whole row.
  • src/app/api/whatsapp/config/route.ts — save path reads the existing row
    and delegates to the helper instead of inlining the ternary.
  • src/components/settings/whatsapp-config.tsx — the field now says the token
    is kept when left blank, so the behaviour is discoverable rather than
    implied.
  • messages/en.json, messages/ko.json — that one hint string.

Behaviour is unchanged when a token is typed.

Test plan

  • src/lib/whatsapp/verify-token.test.ts7 passed, written
    red-first (the test commit precedes the fix commit in this PR).
  • Negative control: replacing return existingEncrypted ?? null with
    return null (the old behaviour) turns 4 of the 7 red; reverting makes
    them green again. The suite genuinely pins this behaviour rather than
    passing vacuously.
  • npm run typecheck — clean.
  • npx eslint on the four touched files — 0 errors, 1 warning, and
    that warning (react-hooks/exhaustive-deps in whatsapp-config.tsx) is
    pre-existing: it reports on upstream/main too, at line 204 vs 210 here
    — the shift is just the added lines.
  • npm run build — not run locally; left to CI.

Related

Found while self-hosting; no existing issue covers it (searched open + closed
for verify_token). Happy to split the i18n hint into its own commit or drop
it if you would rather keep the fix minimal.

Guru35 and others added 2 commits September 7, 2026 02:24
…e stored one

Regression cover for the settings page silently dropping the webhook
verify token: the form never shows the stored (encrypted) token, so a
save of any other field posted it empty and the route wrote NULL over it.
Measured live 2026-09-05: whatsapp_config.verify_token was NULL and every
Meta GET handshake got 403 — indistinguishable from a wrong token.

resolveVerifyTokenForSave is a skeleton mirroring the current route
behaviour so the test goes red on the assertion, not on the import:

  Tests  5 failed | 2 passed (7)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The settings form never shows the stored webhook verify token (encrypted
at rest, not returned by GET), so the field arrived empty on every save
that touched any other setting — and the route wrote NULL over the stored
value (verify_token ? encrypt(verify_token) : null). Measured live on
2026-09-05: verify_token was NULL and every Meta GET handshake got 403,
indistinguishable from a wrong token from the outside.

Server: resolveVerifyTokenForSave() — a non-empty incoming token is
trimmed and re-encrypted; empty/undefined keeps the stored ciphertext.
The pre-existing row lookup now also selects verify_token. Clearing the
token on its own stays unsupported; Reset Configuration wipes the row.

Client: mirror the access-token treatment — show a mask when a token is
stored, clear it on focus, and omit verify_token from the payload unless
the user actually typed one (undefined is dropped from JSON, so the
server keeps what it has). Hint text explains the mask (en + ko).

Tests 829 -> 836 (7 new), typecheck clean, lint 0 errors (37 pre-existing
warnings untouched). Red-first commit: efec1df.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Guru35
Guru35 requested a review from ArnasDon as a code owner September 6, 2026 23:32
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