Browser extension that signs WebAuthn passkey requests required by WhatsApp to link a device on some accounts (clients hit this when the server flags an account for passkey pairing instead of a plain QR code).
Two builds, one per browser family, because the page-to-extension messaging mechanism differs between them:
chrome/— Chromium-based browsers (Chrome, Edge, Brave, ...). Pages talk to it directly viachrome.runtime.sendMessage(extensionId, ...).firefox/— Firefox. Uses a content script bridge instead, since Firefox doesn't support page-to-extension messaging viaexternally_connectable.
See each folder's own README for the message contract, integration examples, and how to load it locally for development.
Bump version.json — the single source of truth — then:
make sync-version # writes it into both manifestsScheme is YYYY.M.PATCH (e.g. 2026.7.1); no leading zeros, so July is 7.
Stores reject a version that already exists. make release runs this for you.
make release
# .out/chrome-2026.7.1.zip (16 locales)
# .out/firefox-2026.7.1.zip (18 locales)Syncs the version and locales, validates both extensions, writes the zips.
Aborts on a manifest that drifted from version.json, a __MSG_ placeholder
with no default_locale, a missing locale key, an over-long name/description,
or a missing file.
Needs node >= 14, zip, unzip. Also: make check (validate only),
make sync-version, make locales, make version, make clean.
Developer Dashboard → the
existing item ghpdcgnjffaaekflfpcgkgpbafmjldcp (unlisted) → Package →
Upload new package → .out/chrome-<version>.zip → submit for review.
Never create a new item: a new ID breaks every integrator's
chrome.runtime.sendMessage(EXTENSION_ID, ...). Leave the listing name and
description blank — they come from _locales/.
Developer Hub → Submit a New Add-on
→ .out/firefox-<version>.zip. First upload is a new submission (the add-on ID
changed during de-branding); later ones use Upload New Version.
Pick "On your own" for a signed self-hosted .xpi, or "On this site" to publish.
Name and short description come from _locales/; the long description is edited
per-locale in the Hub. No source upload needed. Firefox won't permanently install
an unsigned extension, so self-hosted builds still need AMO signing.
The extension is available in 18 languages, matching the dashboard's language
set. Both the store listing (name + description) and the confirmation
card shown on web.whatsapp.com are translated.
The language is detected automatically from the browser's UI language and
is not configurable: neither Chrome nor Firefox exposes an API to override the
locale that i18n.getMessage() resolves against. Unknown languages fall back
to English via default_locale. Right-to-left languages (Arabic, Persian,
Hebrew, Urdu) lay the card out RTL, driven by the predefined @@bidi_dir
message.
Translations live in i18n/messages/<code>.json — the single source of
truth. chrome/_locales/ and firefox/_locales/ are generated from it and
committed (so both folders stay loadable unpacked, with no build step):
make locales # regenerate both _locales trees
make check # fail if they're out of date (also checks version.json)version.json works the same way: it's the source of truth for the version,
written into both manifests by make sync-version.
Never hand-edit a file under _locales/ — the next sync overwrites it. The
script also validates that every locale has the same key set as en.json (a
missing key would silently render as an empty string), that extName and
extDescription fit the stores' 45/132-character limits, and that they stay
vendor-neutral.
Neither store takes the bare pt or zh codes — only regional variants. Chrome
ignores an unknown locale directory silently (the upload succeeds, the language
just never appears in the listing), and AMO guesses, which filed our Brazilian
pt under "Português (Europeu)". So the sync script writes the variants
explicitly, for both browsers: pt.json → pt_BR + pt_PT, zh.json →
zh_CN. There is deliberately no zh_TW — the translation is Simplified, and
Traditional readers are better served by the en fallback than by mislabelled
Simplified text.
Beyond that, each store drops locales for its own reasons, and the two are not the same thing:
- Chrome accepts locale directories only from a
fixed table,
which has no Punjabi (
pa) or Urdu (ur). Those ship in the Firefox build only; the sync script skips them for Chrome automatically, and Chrome users with that UI language fall back toen. - AMO translates a listing only into locales in its
PROD_LANGUAGESset, which excludesar,bn,fa,hi,id,pa,ur— so the Firefox store page shows English for those. This is a listing-only limit and not a reason to remove them: Firefox reads_locales/out of the XPI at runtime, so those users still get a fully translated extension.
Both stores read the localized name and description straight out of
_locales/ at upload time, so the install page is translated with no extra
work. One exception: AMO's long listing description is edited per-locale in
the Developer Hub and cannot be checked into this repo.