Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
157 commits
Select commit Hold shift + click to select a range
f382b1e
docs(fork): state that this is an unofficial fork, and how it is changed
AlmogCohen Sep 27, 2026
fadd8bd
build(deps): pin baileys 7.0.0-rc14
AlmogCohen Sep 27, 2026
a52aaf0
test: a vitest harness that runs Evolution's source against the real …
AlmogCohen Sep 27, 2026
c5d815b
ci: run the fork's tests on every commit
AlmogCohen Sep 27, 2026
fa3d836
docs(fork): the working rules, test first
AlmogCohen Sep 27, 2026
eb94ff7
test: app-state sync keys survive a reload in every auth store
AlmogCohen Sep 27, 2026
4ffaf1e
fix(auth-state): reload app-state sync keys with fromObject
AlmogCohen Sep 27, 2026
22b0eda
test: a webhook subscribed to GROUP_UPDATE receives group updates
AlmogCohen Sep 27, 2026
b0edded
fix(webhook): accept both spellings of the group update event
AlmogCohen Sep 27, 2026
b588821
test: getMessage answers a miss with nothing, not an empty message
AlmogCohen Sep 27, 2026
26b9bfa
fix(baileys): answer a getMessage miss with undefined
AlmogCohen Sep 27, 2026
e98168a
test: forward every @lid to phone mapping Baileys learns, on contacts…
AlmogCohen Sep 27, 2026
71ba3e8
feat(baileys): forward @lid to phone mappings on contacts.upsert
AlmogCohen Sep 27, 2026
9e958c1
test: say on contacts.upsert whether a name is the one the owner saved
AlmogCohen Sep 27, 2026
f7b5606
feat(baileys): mark saved contact names on contacts.upsert, only when…
AlmogCohen Sep 27, 2026
dd11ebe
test: nothing a message carries reaches the logs under LOG_LEVEL=ERRO…
AlmogCohen Sep 27, 2026
c52cab4
fix(baileys): keep message content, numbers and names out of the logs
AlmogCohen Sep 27, 2026
5e2059e
test: media downloads leave through the instance's proxy
AlmogCohen Sep 27, 2026
2c2193f
fix(baileys): download media through the instance's proxy
AlmogCohen Sep 27, 2026
37775a9
test: media uploads work on an instance with a proxy, and leave throu…
AlmogCohen Sep 27, 2026
c5435ef
fix(baileys): give Baileys an http agent for uploads through the proxy
AlmogCohen Sep 27, 2026
5187ea2
test: the WhatsApp Web version fetch leaves through the instance's proxy
AlmogCohen Sep 27, 2026
7435f41
fix(baileys): fetch the WhatsApp Web version through the instance's p…
AlmogCohen Sep 27, 2026
bc3567e
test: a connect never starts before the instance's proxy is loaded
AlmogCohen Sep 27, 2026
1876a9b
fix(baileys): wait for the proxy before connecting
AlmogCohen Sep 27, 2026
6f895ce
test: a connect uses the instance's stored settings, not defaults
AlmogCohen Sep 27, 2026
452402e
fix(baileys): wait for settings before connecting
AlmogCohen Sep 27, 2026
3ec7c20
test: a database error never replaces a linked session's credentials
AlmogCohen Sep 27, 2026
b3e91ff
fix(auth-state): fail on a database error instead of starting a fresh…
AlmogCohen Sep 27, 2026
aead400
test: a failed settings read does not drop an event batch
AlmogCohen Sep 27, 2026
3235303
fix(baileys): keep processing a batch when the settings read fails
AlmogCohen Sep 27, 2026
fb23a15
test: unlinking an instance whose connection is down still unlinks it
AlmogCohen Sep 27, 2026
3355775
fix(instance): wipe the session on logout and delete even when the so…
AlmogCohen Sep 27, 2026
7068a11
test: profile pictures are fetched once per contact, not per event
AlmogCohen Sep 27, 2026
fa79a62
fix(baileys): cache profile picture lookups and bound their concurrency
AlmogCohen Sep 27, 2026
0050af5
test: one pairing code per connect attempt, and a fresh QR budget per…
AlmogCohen Sep 27, 2026
192605e
fix(baileys): request the pairing code once per attempt and reset the…
AlmogCohen Sep 27, 2026
053d26d
test: a group listing does not query every group's metadata again
AlmogCohen Sep 27, 2026
4e34298
fix(baileys): fill the group metadata cache from groups.update
AlmogCohen Sep 27, 2026
b1a88c0
test: history and live messages never wait on profile picture lookups
AlmogCohen Sep 27, 2026
5b4c3f5
fix(baileys): look up history contacts' pictures off the event path
AlmogCohen Sep 27, 2026
018d200
test: reconnects back off instead of spinning
AlmogCohen Sep 27, 2026
ae8ad21
fix(baileys): back off between reconnects, and time out the version f…
AlmogCohen Sep 27, 2026
f9e3cfb
test: one instance never runs two sockets
AlmogCohen Sep 27, 2026
5434ac4
fix(baileys): end the old socket and serialise connects
AlmogCohen Sep 27, 2026
420b9bc
test: a logout that cannot reach WhatsApp is delivered when the conne…
AlmogCohen Sep 27, 2026
1d15929
fix(instance): keep the session until WhatsApp accepts the logout
AlmogCohen Sep 27, 2026
0f2a0c4
test: a pending logout is never undone by a lost marker file
AlmogCohen Sep 27, 2026
df633ca
fix(instance): record a pending logout on the instance row too
AlmogCohen Sep 27, 2026
0baf1d4
docs(fork): list what the fork changes
AlmogCohen Sep 27, 2026
2d9aeea
test: pin that sends to @lid chats go through (guard, not a bug)
AlmogCohen Sep 27, 2026
2493e11
test: getMessage answers nothing when the lookup fails
AlmogCohen Sep 27, 2026
078d6c9
fix(baileys): answer nothing when the getMessage lookup fails
AlmogCohen Sep 27, 2026
c559cb1
docs(fork): correct the issue references
AlmogCohen Sep 27, 2026
a559dc4
test: group participant updates carry each participant's jid and phon…
AlmogCohen Sep 27, 2026
8fd9004
fix(baileys): read Baileys 7 participant objects in group participant…
AlmogCohen Sep 27, 2026
94d76b2
test: chat updates and the history chat list carry the archive state
AlmogCohen Sep 27, 2026
1e79cf1
feat(baileys): forward archive state on chat updates and the history …
AlmogCohen Sep 27, 2026
c029b76
test: a media download says whether it asked the phone to re-upload
AlmogCohen Sep 27, 2026
873d901
feat(baileys): record whether a media download asked for a re-upload
AlmogCohen Sep 27, 2026
c7e7e3b
test: a media download can skip asking the phone to re-upload
AlmogCohen Sep 27, 2026
c6860fa
feat(chat): let a media download skip the phone re-upload
AlmogCohen Sep 27, 2026
8c95840
test: an expired media download asks the phone to re-upload, once
AlmogCohen Sep 27, 2026
48bb6f0
fix(baileys): re-upload expired media when Baileys' own check misses it
AlmogCohen Sep 27, 2026
8cb0049
test: a message's webhook key keeps its original @lid address
AlmogCohen Sep 27, 2026
f3d1052
fix(baileys): keep the original @lid in remoteJidAlt when showing the…
AlmogCohen Sep 27, 2026
999d2bb
test: a media re-upload names the chat by the address WhatsApp stores…
AlmogCohen Sep 27, 2026
257ebde
fix(baileys): ask for a re-upload with the message's original address
AlmogCohen Sep 27, 2026
dbde828
test: a refused re-upload says why
AlmogCohen Sep 27, 2026
d12de02
feat(baileys): record why the phone refused a re-upload
AlmogCohen Sep 27, 2026
0d4ce58
test: a 403 from the media servers also asks the phone to re-upload
AlmogCohen Sep 27, 2026
c09c604
fix(baileys): ask the phone to re-upload after a 403 from the media s…
AlmogCohen Sep 27, 2026
c147db1
docs(fork): list the @lid webhook key and the re-upload address
AlmogCohen Sep 27, 2026
cc5241f
test: a re-upload works when the media key arrives as text
AlmogCohen Sep 27, 2026
d149189
fix(baileys): turn the media key back into bytes before asking the phone
AlmogCohen Sep 27, 2026
7b09c47
test: a 403 asks the phone only when the media link has expired
AlmogCohen Sep 27, 2026
431ebb8
fix(baileys): ask the phone after a 403 only when the media link has …
AlmogCohen Sep 27, 2026
3167ff7
test: a failed media download never logs the media URL
AlmogCohen Sep 27, 2026
db182c8
fix(baileys): keep media URLs out of the logs
AlmogCohen Sep 27, 2026
c8fdc03
test: a failed reconnect says why
AlmogCohen Sep 27, 2026
7eb95c9
fix(baileys): log why a reconnect attempt failed
AlmogCohen Sep 27, 2026
ce65a3f
test: a failed incoming media conversion never logs the media URL
AlmogCohen Sep 27, 2026
6780ae8
fix(baileys): keep the media URL out of the base64 conversion log
AlmogCohen Sep 27, 2026
639ab6e
test: a failed sent media conversion never logs the media URL
AlmogCohen Sep 27, 2026
6850a9b
fix(baileys): keep the media URL out of the sent media conversion log
AlmogCohen Sep 27, 2026
63c5170
test: a failed connect for a pending logout says why
AlmogCohen Sep 27, 2026
a24f928
fix(baileys): log why a connect for a pending logout failed
AlmogCohen Sep 27, 2026
f20c85b
test: a failed connection reload says why, scrubbed
AlmogCohen Sep 27, 2026
59a7b82
fix(baileys): log why a connection reload failed, scrubbed
AlmogCohen Sep 27, 2026
96e45fc
test: the live-record codec round-trips bytes, Longs, undefined, prot…
AlmogCohen Sep 27, 2026
88c15df
feat(live-record): a tagged JSON codec that keeps bytes, Longs, undef…
AlmogCohen Sep 27, 2026
70a3df9
test: a live check records its events, webhooks and a manifest under …
AlmogCohen Sep 27, 2026
ad048e6
feat(live-record): record a live check's events, webhooks and manifes…
AlmogCohen Sep 27, 2026
acce1ee
test: the live-check scrubber replaces identities consistently and it…
AlmogCohen Sep 27, 2026
938ad85
feat(live-record): a scrubber that turns a raw recording into a fixtu…
AlmogCohen Sep 27, 2026
03fc07a
test: a scrubbed live-check fixture replays through the real buffer a…
AlmogCohen Sep 27, 2026
00735ff
feat(live-record): replay a scrubbed fixture through the real buffer …
AlmogCohen Sep 27, 2026
8ccda96
test: a guard finds personal data in live fixtures and names only the…
AlmogCohen Sep 27, 2026
5372d71
feat(live-record): a guard that scans live fixtures for personal data…
AlmogCohen Sep 27, 2026
797e52e
style: lint and prettier on the live-record files
AlmogCohen Sep 27, 2026
71b5ae0
docs: the live-check protocol, catalogue and results log, and the rul…
AlmogCohen Sep 27, 2026
96ab1aa
test: the scrubber keeps a numeric message id a message id, never a p…
AlmogCohen Sep 27, 2026
bf53c7c
fix(live-record): the scrubber fakes a numeric message id as an id
AlmogCohen Sep 27, 2026
d4d1e29
test: the fixture guard passes identifiers and the scrubber's numeric…
AlmogCohen Sep 27, 2026
ec02f6f
fix(live-record): the guard passes identifiers and the scrubber's num…
AlmogCohen Sep 27, 2026
356384e
test(live): a contact renamed on the phone after a restart, replayed …
AlmogCohen Sep 27, 2026
584077c
test(live): archive and unarchive on the phone, replayed from a live …
AlmogCohen Sep 27, 2026
e38b506
test(live): a group renamed and a member removed and added back, repl…
AlmogCohen Sep 27, 2026
bc6ee78
test(live): a DM addressed by @lid keeps its @lid, replayed from a li…
AlmogCohen Sep 27, 2026
c804f38
style(live-record): prettier on the scrubber's numeric id, and a type…
AlmogCohen Sep 27, 2026
2f1c5a7
docs(live-checks): log the 2026-09-27 rig session
AlmogCohen Sep 27, 2026
b86b102
test: a 403 is judged by the link that actually failed
AlmogCohen Sep 27, 2026
6e605a4
fix(baileys): judge a 403 by the link that actually failed
AlmogCohen Sep 27, 2026
9e4780c
test: deleting an instance with its logout pending keeps the creds an…
AlmogCohen Sep 27, 2026
136690d
fix: a deleted instance keeps its row until its pending logout is del…
AlmogCohen Sep 27, 2026
837ab36
test: the scrubber, its leak gate and the fixture guard let a usernam…
AlmogCohen Sep 27, 2026
f63ff5c
fix: the scrubber keeps a string only by field and known value, stops…
AlmogCohen Sep 27, 2026
d82761e
test: a recorded link writes the QR payload, its image and the pairin…
AlmogCohen Sep 27, 2026
4d5b98a
fix: the recorder writes a marker, not the QR, its image or the pairi…
AlmogCohen Sep 27, 2026
529c4b9
test: a 202 PENDING logout is undone by losing the instances volume, …
AlmogCohen Sep 27, 2026
2d5c296
fix: a pending logout is recorded on the instance's row before 202, s…
AlmogCohen Sep 27, 2026
54a3892
test: a second logout answers 'logged out' while the first is still i…
AlmogCohen Sep 27, 2026
dba9bc9
fix: concurrent logouts share one run and answer with its outcome
AlmogCohen Sep 27, 2026
37f49ba
test: a logout WhatsApp never confirmed wipes the session, on the soc…
AlmogCohen Sep 27, 2026
a1c69d2
fix: a logout finishes on WhatsApp's answer, not on the socket closin…
AlmogCohen Sep 27, 2026
a20d2d9
test: a reload, and a socket built across a shutdown, escape the one-…
AlmogCohen Sep 27, 2026
f147f02
fix: a reload goes through the one-at-a-time connect, and nothing is …
AlmogCohen Sep 27, 2026
166ff8d
test: batches queued before an instance is shut down still run after it
AlmogCohen Sep 27, 2026
4e0c39e
fix: an instance shut down runs no queued batch and forwards nothing …
AlmogCohen Sep 27, 2026
465d874
test: a late picture lookup brings back an old name, and a removed pi…
AlmogCohen Sep 27, 2026
caf5fa4
fix: a late picture lookup carries the contact's newest name, and nev…
AlmogCohen Sep 27, 2026
d2e163c
test: a proxied instance can still reach WhatsApp from the server add…
AlmogCohen Sep 27, 2026
d65032f
fix: a proxied instance fails a connect or a download rather than lea…
AlmogCohen Sep 27, 2026
88102c7
test: errorFields keeps a quoted text and a formatted phone number fr…
AlmogCohen Sep 27, 2026
90041f3
fix: log scrubbing masks quoted text and phone numbers written with s…
AlmogCohen Sep 27, 2026
3d2907a
test: a DM addressed by phone whose key carries its @lid is asked for…
AlmogCohen Sep 27, 2026
e435411
fix: a re-upload the phone refuses under the @lid is asked again unde…
AlmogCohen Sep 27, 2026
9ef82e6
test: an unanswered re-upload leaves Baileys' waiters behind, and a l…
AlmogCohen Sep 27, 2026
2a86f76
fix: an unanswered re-upload ends Baileys' wait for the answer
AlmogCohen Sep 27, 2026
3fc6f59
test: a boot connect that fails on a database read leaves the instanc…
AlmogCohen Sep 27, 2026
26622dc
fix: a boot connect that fails keeps the instance in the API and retr…
AlmogCohen Sep 27, 2026
a69530d
test: a failed creds write reads as saved, unreadable creds are overw…
AlmogCohen Sep 27, 2026
b8df08a
fix: a creds write that fails is reported and retried, and unreadable…
AlmogCohen Sep 27, 2026
219e79c
test: a recording directory that cannot be created fails the connect,…
AlmogCohen Sep 27, 2026
2ed4afe
fix: a recording that cannot start or write its manifest costs the re…
AlmogCohen Sep 27, 2026
b3be753
test: a replay splits a batch the live buffer delivered whole
AlmogCohen Sep 27, 2026
eb9bc27
fix(test): a replay flushes the buffer only where the live buffer did
AlmogCohen Sep 27, 2026
4931519
docs(fork): state what the fork's guarantees cover, and what they do not
AlmogCohen Sep 27, 2026
fbd558c
test: a failed media download answers with the signed media link
AlmogCohen Sep 29, 2026
458226b
fix: a failed media download answers without the media link
AlmogCohen Sep 29, 2026
ac5a424
test: a signal key file is left torn by a crash, a failed write or a …
AlmogCohen Sep 29, 2026
d955eb7
fix: signal key files are replaced whole, never written in place
AlmogCohen Sep 29, 2026
2ad6311
test: a pending-logout marker write that fails partway leaves half a …
AlmogCohen Sep 29, 2026
ddb4e2a
fix: the pending-logout marker is replaced whole, never written in place
AlmogCohen Sep 29, 2026
95c844b
docs(fork): list media errors without the link, and session files rep…
AlmogCohen Sep 29, 2026
5daf1fa
docs(fork): the trademark policy lives upstream, not in this tree
AlmogCohen Sep 29, 2026
34979cd
docs(fork): record the 2026-09-29 live results (logout on an open soc…
AlmogCohen Sep 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .github/workflows/fork-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# The fork's tests. Every commit on a fork/ branch runs them, so a test:
# commit shows its red run and the fix: commit after it shows green (FORK.md).
name: fork tests

on:
push:
branches: ['fork/**']
pull_request:
schedule:
# Weekly, against the newest Baileys release, so a new version is a
# diff to read before anyone bumps the pin.
- cron: '0 6 * * 1'
workflow_dispatch:

permissions:
contents: read

jobs:
test:
name: typecheck and tests (pinned Baileys)
runs-on: ubuntu-latest
# A few tests need what only a real database does (foreign keys, cascades):
# they create and drop their own database on this server (test/helpers/real-postgres.ts).
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports: ['5432:5432']
options: >-
--health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10
env:
FORK_TEST_PG_URL: postgresql://postgres:postgres@127.0.0.1:5432/postgres
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run db:generate
env:
DATABASE_PROVIDER: postgresql
- run: npx tsc --noEmit
- name: tests
run: |
expected=$(node -p "require('./package.json').dependencies.baileys")
BAILEYS_EXPECT="$expected" npx vitest run

baileys-latest:
name: tests against the newest Baileys
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
# A few tests need what only a real database does (foreign keys, cascades):
# they create and drop their own database on this server (test/helpers/real-postgres.ts).
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports: ['5432:5432']
options: >-
--health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10
env:
FORK_TEST_PG_URL: postgresql://postgres:postgres@127.0.0.1:5432/postgres
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm install --no-save baileys@latest
- run: npm run db:generate
env:
DATABASE_PROVIDER: postgresql
- name: tests
run: |
installed=$(node -p "require('baileys/package.json').version")
echo "Testing baileys $installed"
BAILEYS_EXPECT="$installed" npx vitest run
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ lerna-debug.log*
# Project related
/instances/*
!/instances/.gitkeep
/test/

/src/env.yml
/store
*.env

/temp/*

# Raw live-check recordings (docs/LIVE-CHECKS.md): never committed
/live-records/

.DS_Store
*.DS_Store
.tool-versions
Expand Down
64 changes: 64 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,67 @@
# Working in this fork (read first)

This is an unofficial fork of Evolution API 2.3.7 (`FORK.md`).
These rules are this fork's own standard. They are not Evolution's, and they
do not go into pull requests offered to evolution-foundation/evolution-api.

## Test first, always: red, then green

1. Write the test that reproduces the bug or specifies the behaviour, and run
it against the code as it is. It must FAIL, for the reason the bug gives.
A test that passes before the fix proves nothing: rewrite it.
2. Commit the test alone: `test: <what it proves>`. CI runs on that commit,
and its red run is the evidence.
3. Make the smallest change that turns it green, with the rest of the suite
still green, and commit it: `fix: ...` or `feat: ...`. Push the two commits
one at a time, so CI records each.

Never change a test to make it pass, and never skip or delete a failing test
without saying why in the commit.

## How the tests run

- `npm test` runs vitest over `test/**/*.test.ts`. The tests run Evolution's
TypeScript source, not the bundle, against the Baileys that `package.json`
pins (`BAILEYS_DIR` points them at another build).
- A fixture is a WhatsApp-side input (a `HistorySync` proto, an app-state
action, an encrypted patch), turned into events by the Baileys version under
test. Hand-written events are allowed only where Baileys has no builder.
- `test/helpers/baileys-service.ts` builds the real `BaileysStartupService`
with an in-memory Prisma and the real Baileys event buffer; `deliver()`
sends a batch the way the socket does (buffered).
- Every test runs under a configuration profile (`test/helpers/profiles.ts`).
The default is `minimal`, a production configuration that stores only the instance; a test
of storage uses `stored`. A behaviour that depends on a flag is tested
under both.
- Assert what a consumer observes: the webhook payload, the stored row, the
socket call, the HTTP answer. Assert exact fields, not substrings.
- No test touches WhatsApp, a real account or the network beyond localhost.

## Recordings and fixtures (live checks)

A live check records a real session (`LIVE_RECORD_DIR`, `docs/LIVE-CHECKS.md`).
Raw recordings hold real people's numbers, names and messages. Any agent or
person working here follows these rules, with no exception:

- Never commit, stage or copy anything from `LIVE_RECORD_DIR` (or
`live-records/`). Only the scrubber's output (`scripts/live-scrub.ts`) goes
into `test/fixtures/live/`.
- Before committing a fixture, run `npx tsx scripts/live-guard.ts` and read
`scrub-report.json` (`"leakGate": "pass"`, counts that fit the check).
- Open and skim every new fixture file yourself. The guard cannot recognise a
name or a message text; you can.
- If anything looks like a phone number, a user part of an `@lid` or
`@s.whatsapp.net` address that is not a scrubber fake, a name, a message
text, a signed media URL (`mmg.whatsapp.net`, `oh=` / `oe=` parameters), an
email, a token or a key: stop. Fix the scrubber and scrub again. Never edit
a fixture by hand.
- Never paste a raw recording, or any part of one, into a commit, an issue, a
pull request or a chat.
- A fixture's replay test must fail when the behaviour it covers is broken:
run it once against the code before the fix and say so in the commit.

---

# Evolution API - AI Agent Guidelines

This document provides comprehensive guidelines for AI agents (Claude, GPT, Cursor, etc.) working with the Evolution API codebase.
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
**This is a fork: read the first section of `AGENTS.md` (test first, red then green) before changing anything.**

# CLAUDE.md

This file provides comprehensive guidance to Claude AI when working with the Evolution API codebase.
Expand Down
84 changes: 84 additions & 0 deletions FORK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# This fork

An unofficial fork of [Evolution API](https://github.com/evolution-foundation/evolution-api)
2.3.7. It is not endorsed by Evolution Foundation, and
the image it builds is not an official Evolution API build: it carries its own
name of its own, not Evolution's.

## Why it exists

Evolution API has no test suite. Every bug below shipped because nothing could
catch it, and a consumer that needed a fix could only patch the minified bundle
with string replacements. This fork shows the other way: the same code, a test
harness that runs Evolution's own TypeScript source against the real Baileys,
and every change made test first.

## The rule: red, then green

Every change in this fork is two commits, in this order:

1. `test: ...`, a test that reproduces the bug and **fails** on the code as it
is. CI runs on that commit, so its red run is the proof that the test sees
the bug.
2. `fix: ...` (or `feat:`), the smallest change that makes that test pass,
with the rest of the suite still green.

A fix without a failing test first is not merged here. See `AGENTS.md`.

## Base

- Source: the `2.3.7` tag (`cd800f29`). Upstream `main` has the same code.
Upstream `develop` (2.4.0) is not used: it requires licence activation
against Evolution Foundation's server and breaks `POST /instance/create`.
- Baileys pinned to `7.0.0-rc14` (2.3.7 ships rc.9, which is inside the range
of CVE-2026-48063).

## Changes from 2.3.7

Each line is a pair of commits: the test that failed on the code before the
fix, then the fix. For most, that code is 2.3.7 itself; several need what the
fork added first (the test harness, the Baileys 7.0.0-rc14 pin), and a red commit
that only fails to import what its fix adds proves nothing about behaviour.
`git log 2.3.7..` is the source of truth, and `git diff 2.3.7 --stat` lists
every file modified from the original. Upstream issues and pull requests are
named where one exists.

**Contacts, names and groups**
- App-state sync keys are reloaded with `fromObject` in all three auth stores, so saved names, labels, mutes and archives keep syncing after a restart (#2576, #2384; fixes also offered in #2685 and #2610).
- Every @lid to phone mapping Baileys learns is forwarded on `contacts.upsert`, captured before Baileys' event buffer drops it.
- Every `contacts.upsert` item for a contact says whether its name is the one the owner saved (`saved`), and only when that is certain. The @lid mapping items above carry `pushName: null` and no `saved`: a consumer that replaces a whole contact with an item, rather than merging its fields, loses the name.
- Group updates reach subscriptions stored as `GROUP_UPDATE`, in all seven transports and in the global configurations (#2652).
- Group metadata is filled from `groups.update` instead of queried again for every group on every listing.
- `chats.update`, `chats.set` and `chats.upsert` items carry the chat's archive, pin and mute state (`archived`, `pinned`, `muteEndTime`) when Baileys has it, and omit a field it does not have rather than guess.

**Messages and privacy**
- A `getMessage` miss or failed lookup answers nothing, so Baileys no longer relays an empty message on a retry and uses it up (#2705, and #2706 for groups; fixes also offered in #2728 and #2623).
- The fork's own log lines at `LOG_LEVEL=ERROR,WARN`, and Baileys' error logs, carry bounded fields, not message text, phone numbers, JIDs or push names; an exception's message is kept only scrubbed of URLs, JIDs, quoted parts and phone numbers (also as a person writes them). This is not a guarantee for every line: inherited code still logs some raw errors (integrations such as S3, Chatwoot and the chatbots are not covered by the tests), and a name inside an exception's unquoted words is not recognisable.
- The `messages.upsert` webhook key of a DM WhatsApp addresses by @lid shows the phone JID as `remoteJid`, as 2.3.7 does, but keeps the original @lid in `remoteJidAlt` (2.3.7 copied the phone there too and lost it), with `addressingMode: 'pn'`: upstream develop's swap.
- A media re-upload request names the message by the address the phone stores it under: the @lid from `remoteJidAlt` or `participantAlt` when the key shows the phone, or, for a key stored from 2.3.7 (the phone twice, `addressingMode: 'lid'`), the @lid Baileys' LID mapping has for the phone. The phone refuses a request that names an @lid chat by its phone JID. A key with the phone and its @lid beside it cannot say which of the two the phone keeps (a DM WhatsApp addresses by phone looks the same), so a refusal under the @lid is asked again once under the key as given. A request the phone does not answer in time ends Baileys' own wait for it, so no listener is left and a late answer changes nothing.
- A media download records whether it asked the phone to re-upload an expired file and how that ended (a bounded log line, and `reupload` on the download's error), and when the phone refused, why (`reason=` on the log line, `reuploadReason` on the error and the HTTP answer: the phone's result such as `NOT_FOUND`, `error_<code>`, `missing_ciphertext`, or `no_answer`). Baileys 7.0.0-rc14 never asks on a CDN 404 or 410 (its check misses the status), so Evolution asks the phone itself, once per download, for at most 60s. A CDN 403 counts as expired only when the link that actually failed (the one on Baileys' error, which is the directPath when the message has one, else the url) carries exactly one `oe` query parameter, in hex unix seconds, that has passed by the local clock. This is a conservative heuristic: on 84 history-sync attachments, 403 answered 34 of 34 expired links and 0 of 50 valid ones, where a valid link to a dropped file answered 404 or 410.
- A media download turns the media key back into bytes (from a base64 string, or the object JSON makes of a Uint8Array or a Buffer) before it asks the phone to re-upload, since Baileys 7.0.0-rc14 derives the re-upload's key from the value as given and then cannot decrypt the phone's answer (Baileys #2729 proposed the same fix there).
- `POST /chat/getBase64FromMediaMessage` takes an optional `reupload` (boolean, default true). With `false` the phone is never asked to re-upload, and a file gone from the CDN (404, 410, or 403 on an expired link) fails at once with a 400 that says so; the HTTP error carries `reupload` for any failed download.
- A failed media download never answers with the media link. The error's own text (Baileys' "Failed to fetch stream from" and the signed CDN link, whose `oh`, `oe` and `_nc_*` parameters let anyone holding it fetch the file, and whose directPath alone names it) used to be the message of the HTTP answer, of what the error handler posts to the errors webhook, and of the S3 upload's error log. The answer now says what failed, never where: `The media could not be downloaded (HTTP <status>)`, or the kind of error and its network code when there is no status, with `reupload` and `reuploadReason` as before; a text Evolution threw itself (it names no link) stands.

**Proxy**
- Media downloads, media uploads and the WhatsApp Web version fetch leave through the instance's proxy (uploads failed outright on a proxied instance).
- A connect waits for the instance's proxy and stored settings, so it never starts from the server's own address or with default settings. Reloading the proxy keeps the one in force until the new one is read; a proxyscrape list that cannot be fetched fails the connect (and it is retried), and a proxyscrape download with no socket exit to share fails, rather than either going out directly. Tested with a local HTTP and SOCKS5 proxy; other transports a deployment adds are not covered.

**Sessions and connections**
- A database error never replaces a linked session's credentials with fresh ones, and a failed settings read no longer drops an event batch.
- A logout or delete while the socket is down keeps the session until WhatsApp is told, so the device leaves the phone's Linked devices; it answers `202 PENDING` meanwhile and forwards nothing (#2520, #2508 in part). The pending logout is recorded on the instance's row (`disconnectionObject.logoutPending`) as well as in a file next to the session keys, so a restart that lost the instances volume still finishes it; when it cannot be recorded, the logout or delete answers 500 instead of 202. A logout is finished only on WhatsApp's word: its answer to remove-companion-device, or its refusal of the device on the next connection; the socket's own close after sending the request (all Baileys' `logout()` waits for) is not one. Concurrent logouts and deletes share one run and answer with its outcome. A deleted instance keeps its database row until then (Session and Proxy cascade from it), out of the API, its name taken and its token cleared.
- Reconnects back off from 1s to 60s instead of spinning, the version fetch times out after 10s, and an instance never runs two sockets: a reload after a profile or privacy change joins a connect under way, and once an instance is removed nothing builds a socket for it, runs a batch it had queued, or forwards anything (#2134, #2184, #2430; ideas from #2732). A connect that fails at boot keeps the instance in the API and is retried on the same backoff.
- A creds write that fails is logged and tried again until it lands, and stored creds that cannot be parsed fail the connect instead of being replaced by fresh ones.
- Session files on disk are replaced whole, never written in place: each signal key file of the Prisma auth store (keys live under INSTANCE_DIR next to creds in the database) and the pending-logout marker go to a temp file in the same directory, are flushed, renamed over the target and the directory flushed. A process killed mid-write, a full disk or two writes of one key at once used to leave a torn or empty file, and a key file that does not parse reads as no key; tested by SIGKILLing a writer at random moments (half the files torn or empty before, none after). A failed write keeps the previous file and rejects; writes of one file run in call order; a temp file a killed writer left is removed when the store next opens. The provider-files store writes on its own server and Redis holds no files, so neither is covered.
- Profile pictures are looked up once per contact per hour, at most four at a time, and history never waits on them (#1883). The picture update a history batch sends carries each contact's newest name, and a lookup that finishes after WhatsApp said the picture changed or was removed is dropped.
- One pairing code per connect attempt, and a fresh QR budget per attempt (#2100, #2696).

**Live checks**
- A live check against a real phone can be recorded (`LIVE_RECORD_DIR`; the QR, its image and pairing codes are never written), scrubbed into a fixture, and replayed through the real event buffer and service in a test. The scrubber keeps a value as written only under a field it lists with a value that field is known to take, and stops on a field it does not know; its leak gate searches the output for every raw value that is not structure; a guard scans every committed fixture. None of them recognises a name the scrubber mistook for structure, so a person still reads every new fixture. A replay compares the webhooks' content, not their order or the pictures, and runs on events already decoded: it shows what Evolution does with what WhatsApp sent, not the encryption or the timing around it. The protocol, the check catalogue and the results log are in `docs/LIVE-CHECKS.md`.

## Licence

Evolution API is licensed under the Apache License 2.0 with additional
conditions (`LICENSE`), which this fork keeps unchanged. `NOTICE` carries
Evolution Foundation's attribution and states the modifications.
27 changes: 27 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Evolution API
Copyright 2026 Evolution Foundation

This product includes software developed by Evolution Foundation
(https://evolutionfoundation.com.br).

Trademark notice
"Evolution Foundation", "Evolution" and "Evolution API" are trademarks of
Evolution Foundation. The Evolution API logo, wordmark, and visual identity
are governed by Evolution Foundation's Trademark and Brand Assets Policy
(https://github.com/evolution-foundation/evolution-api/blob/main/TRADEMARKS.md).
This fork does not change the Evolution API user interface or its brand assets.

Third-party attributions

This product includes software derived from CodeChat WhatsApp API
(https://github.com/code-chat-br/whatsapp-api), originally licensed under MIT.
The CodeChat project implemented the Baileys library
(https://github.com/WhiskeySockets/Baileys), which Evolution API also uses for
its WhatsApp Web integration.

Modifications

This is an unofficial fork of Evolution API 2.3.7, maintained by Almog Cohen
(https://github.com/AlmogCohen/evolution-api). It is not endorsed
by Evolution Foundation. The changes are described in FORK.md, and each one is
a separate commit with the test that proves it.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
> **Unofficial fork** of Evolution API 2.3.7, made test first. Not endorsed by Evolution Foundation. See [FORK.md](FORK.md) for what changed and why.

<h1 align="center">Evolution Api</h1>

<div align="center">
Expand Down
Loading
Loading