Skip to content

Latest commit

 

History

History
303 lines (217 loc) · 13.7 KB

File metadata and controls

303 lines (217 loc) · 13.7 KB

AGENTS.md

Guide for AI coding agents (Claude Code, Cursor, Aider, Codex, Copilot Workspace, etc.) contributing to Baileys. Human contributors should also read this — there's nothing AI-specific in the conventions, only in the disclosure rules at the end.

If you are an AI agent driving this repo, read this file first, then CODE_OF_CONDUCT.md (specifically the AI policy section), then SECURITY.md.

What Baileys is

A TypeScript WebSocket client for the WhatsApp Web protocol. No browser, no Selenium — it speaks the binary Noise/protobuf protocol directly. Used by thousands of downstream projects, so changes to public APIs and wire-level handling have wide blast radius.

The library is dual-use: legitimate automation, bots, and integrations on one side; spam, stalkerware, and ToS-breaking automation on the other. We do not accept contributions whose primary purpose is to enable abuse (mass messaging, evasion of WhatsApp's anti-spam, scraping users without consent). See CODE_OF_CONDUCT.md.

Repository layout

src/
  Socket/          High-level socket — chats, groups, messages send/recv, newsletter, USync
  Signal/          Signal Protocol session/sender-key wrapping over libsignal-node
  Utils/           Decoding, media, auth state, retry, app-state sync, generics
  Types/           Public TypeScript types — touching these is a public-API change
  WABinary/        Binary node encoding/decoding
  WAUSync/         USync query protocols
  Defaults/        Constants (WA Web version, baileys version, default config)
  __tests__/       Jest unit + integration tests; e2e tests live in __tests__/e2e
WAProto/           Generated protobuf bindings — DO NOT hand-edit
Example/           Reference implementation in example.ts
proto-extract/     Tooling to refresh protobufs from WA Web
scripts/           Repo automation (version bumps, etc.)
.github/workflows/ CI — lint, test, e2e, build, release

WAProto/index.js, WAProto/index.d.ts are generated by WAProto/GenerateStatics.sh. If a change requires modifying them, regenerate via npm run gen:protobuf rather than editing by hand.

Setup

This repo requires Yarn 4 via Corepack. Yarn 1 / classic will fail noisily on package.json resolutions.

corepack enable
yarn install

Node ≥ 20 (enforced by engines and preinstall).

Daily commands

Task Command
Install yarn install
Build (lib + types) yarn build
Type-check + lint yarn lint
Auto-fix lint + format yarn lint:fix
Format only yarn format
Unit + integration tests yarn test
End-to-end tests yarn test:e2e (requires the bartender mock server, see below)
Run the example yarn example
Regenerate protobufs yarn gen:protobuf
Audit deps yarn npm audit --recursive

yarn lint runs tsc first, then ESLint. A green lint means no type errors.

Code style

  • TypeScript strictstrict, strictNullChecks, noUncheckedIndexedAccess, verbatimModuleSyntax are all on. Don't disable them locally.
  • Tabs for indentation, single quotes, no semicolons (Prettier-enforced).
  • No any in new code. Existing anys in tests are tolerated as warnings, not invitations.
  • No comments that restate the code. Comment the why — protocol quirks, WhatsApp-side behavior, non-obvious workarounds. Don't comment-narrate "// loop over messages".
  • No emojis in code or commit messages unless the user explicitly asks.
  • Named exports preferred. Default exports only where they already exist (e.g., makeWASocket).
  • Errors: throw Boom (@hapi/boom) for protocol/HTTP-style errors so downstream code can branch on .output.statusCode. Plain Error for everything else.
  • Logging: every code path that crosses an async boundary should accept a logger: ILogger (pino-compatible). Don't console.log.

Idiomatic patterns

These are the patterns the existing code uses. Match them. New code that does the same thing differently will get review comments asking you to align.

Errors — Boom with statusCode

import { Boom } from '@hapi/boom'

if (!sock.user) {
  throw new Boom('Not authenticated', { statusCode: 401 })
}

if (!isJidUser(jid)) {
  throw new Boom(`Invalid jid: ${jid}`, { statusCode: 400 })
}

Downstream code branches on error.output.statusCode to retry, log out, or surface to the user. Plain throw new Error(...) loses that signal. Reserve plain Error for genuinely internal invariants where no caller is expected to recover.

Logging — structured, never positional

// good: object first, message last; reads cleanly in JSON logs
logger.warn({ msgId: attrs.id, from: attrs.from }, 'error 463: account restricted')
logger.debug({ messageKey }, 'already requested resend')
logger.error({ err: error, opName }, 'failed to parse mex notification JSON')

// bad: string interpolation, untyped fields
logger.warn(`error 463 from ${attrs.from}`)
console.log('failed:', error)

Pino convention: errors go under err, not error or e. The structured object is the first argument so log processors can index it.

JIDs — always go through the helpers

import { jidDecode, jidNormalizedUser, areJidsSameUser, isJidUser } from '../WABinary'

const decoded = jidDecode(rawJid)            // { user, server, device?, agent? } | undefined
const normalized = jidNormalizedUser(rawJid) // strips device/agent, lowercases
const same = areJidsSameUser(a, b)           // compare user portions only

Never split a JID with .split('@') or compare with ===. JIDs carry device suffixes (:0, :42), agent fields, and LID/PN duality — string ops will silently miss matches and you'll ship a bug that only fires on multi-device accounts.

Binary nodes — typed accessors

import { getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString } from '../WABinary'

const groupsNode = getBinaryNodeChild(result, 'groups')
if (!groupsNode) {
  throw new Boom('missing <groups> in iq response', { statusCode: 502 })
}

const groups = getBinaryNodeChildren(groupsNode, 'group') // BinaryNode[]
const text = getBinaryNodeChildString(node, 'body')        // string | undefined
const { attrs } = node                                     // typed Record<string, string>

Don't reach into node.content as an array directly — types are loose and the shape varies by stanza. The accessors handle the missing/single/array cases.

Sending IQs — query with timeouts

const result = await sock.query({
  tag: 'iq',
  attrs: { to: S_WHATSAPP_NET, type: 'get', xmlns: 'w:profile:picture' },
  content: [{ tag: 'picture', attrs: { type: 'image', query: 'url' } }]
}, /* timeoutMs */ 15_000)

query auto-generates the stanza id, attaches a one-shot listener, and rejects on timeout. Don't write your own sock.ws.send + manual listener — you'll leak listeners on errors.

Listening for incoming stanzas — CB: prefix

sock.ws.on('CB:ib,,dirty', async (node: BinaryNode) => {
  const { attrs } = getBinaryNodeChild(node, 'dirty')!
  // ...
})

sock.ws.on('CB:notification,type:server_sync', handler)

The CB:tag,attr:value syntax routes by tag + attribute filter inside the websocket. This is how messages-recv, chats, groups listen — don't filter manually inside a generic 'message' handler.

Public events — ev.on, never call twice

sock.ev.on('messages.upsert', ({ messages, type }) => { ... })
sock.ev.on('connection.update', ({ connection, lastDisconnect }) => { ... })
sock.ev.on('creds.update', saveCreds)

saveCreds (or any handler) must be deduped — registering twice means writing twice. The harness in __tests__/e2e/helpers/test-client.ts shows the cleanup pattern (ev.off in teardown).

Async cleanup — bracket pattern

When you allocate a resource (timer, listener, ws subscription) inside a Promise, clean it up in both paths:

return new Promise<T>((resolve, reject) => {
  const timer = setTimeout(() => {
    cleanup()
    reject(new Boom('timed out', { statusCode: 408 }))
  }, timeoutMs)

  const cleanup = () => {
    clearTimeout(timer)
    sock.ev.off('event.name', handler)
  }

  const handler = (data: T) => {
    if (matches(data)) {
      cleanup()
      resolve(data)
    }
  }

  sock.ev.on('event.name', handler)
})

Half-cleanups are how this codebase grew its memory leaks. The bracket pattern (allocate → cleanup defined → both paths call it) is the fix.

Imports — verbatimModuleSyntax

import type { WAMessage, WAUrlInfo } from '../Types'
import { Boom } from '@hapi/boom'
import { type BinaryNode, getBinaryNodeChild } from '../WABinary'

Type-only imports must be marked import type or inline-prefixed type — TS strict-mode verbatimModuleSyntax will fail the build otherwise. Don't merge a value import with a type import unless you actually use both at runtime.

Optional chains over null guards

// good
const text = msg.message?.extendedTextMessage?.text ?? msg.message?.conversation
if (!sent?.key.id) return

// bad — proliferates `if (x && x.y && x.y.z)` ladders
if (msg.message && msg.message.extendedTextMessage) { ... }

noUncheckedIndexedAccess is on, so array/record indexing returns T | undefined. Don't paper over it with ! unless you have an invariant the type system can't see — and if you do, leave a one-line comment explaining the invariant.

Tests — colocate, name by behavior

// src/__tests__/Utils/decode-wa-message.test.ts
describe('SERVER_ERROR_CODES', () => {
  it('MessageAccountRestriction is 463', () => {
    expect(SERVER_ERROR_CODES.MessageAccountRestriction).toBe('463')
  })
})

Test files mirror the source path: src/Foo/bar.tssrc/__tests__/Foo/bar.test.ts. describe names the unit, it names the behavior in plain English. Avoid it('works').

Public API discipline

src/Types/** and the top-level src/index.ts re-exports define the public surface. Treat changes there as breaking unless you can prove additive-only.

For wire-level changes (Socket/messages-recv.ts, Utils/decode-wa-message.ts, WABinary/, etc.) — describe the WhatsApp-side trigger in the PR. Reviewers can't always reproduce protocol behavior, so the description does the heavy lifting.

Commits and PRs

Conventional commits, scoped where useful:

feat(socket): add support for reachout limits XWAs
fix(retry): process <keys> bundle and embed SKDM on resend
chore(deps): bump ajv from 6.12.6 to 6.15.0
test(e2e): test harness + signal/prekey fixes

PR titles follow the same convention. Squash-merge is the default.

Before opening a PR:

  1. yarn lint — must be 0 errors.
  2. yarn test — must be all green. If you can't run e2e locally, say so in the PR description.
  3. Don't commit baileys_auth_info/, .env, mitm_*.db, .superset/, or any session state. Git is configured to ignore the obvious ones; double-check.
  4. Don't commit regenerated yarn.lock from a different package manager. If your yarn.lock diff is unexpectedly large (thousands of lines for a one-line package.json change), you're using Yarn 1 — switch to Corepack.

What not to touch without coordination

  • libsignal cryptographic flows — session state, prekeys, sender-key derivation. Subtle bugs here are silent and brick downstream sessions.
  • Defaults/baileys-version.json — bumped by the update-version workflow. Manual edits race with automation.
  • WAProto/ generated files — regenerate, don't hand-edit.
  • .github/workflows/ — CI changes are reviewed separately; bundle them in their own PR when possible.
  • package.json resolutions — these patch known security advisories. Removing entries reintroduces CVEs; check yarn npm audit --recursive before pruning.

Testing expectations

  • New public API → unit test in src/__tests__/.
  • New protocol path or stanza handler → integration test mocking the binary node, not an e2e test (e2e is expensive and flaky in agent loops).
  • New crypto/auth flow → e2e against bartender if feasible, otherwise a deterministic fixture-based unit test.

Tests are colocated by area: src/__tests__/Socket/, src/__tests__/Utils/, src/__tests__/binary/. Match the existing layout.

Security-sensitive changes

If your change touches:

  • Auth state read/write, key storage, prekey/session lifecycle
  • Message decryption / signature verification
  • Any path that handles user PII (phone numbers, JIDs, message content) in logs or errors

…flag it explicitly in the PR description. See SECURITY.md for disclosure of vulnerabilities (do not file them as public issues).

AI agent etiquette

Beyond the AI policy in CODE_OF_CONDUCT.md:

  • Read before you write. This codebase has subtle protocol invariants. A grep-and-replace agent will make a mess of Socket/messages-recv.ts. Read the surrounding handler before editing.
  • Don't invent WhatsApp protocol details. If you're not sure how a stanza is structured, find a real example in the tests or in Utils/decode-wa-message.ts. Hallucinated XML attributes get merged and then break in production.
  • Stay in scope. A bug fix doesn't need a refactor pass. A type tweak doesn't need a comment cleanup PR.
  • Don't paste auth state into AI tools. baileys_auth_info/ contains long-lived Signal keys. Treat it like an SSH private key.
  • Disclose AI authorship in PRs. A one-line "drafted with [tool], reviewed by [human]" is enough. See CODE_OF_CONDUCT.md § AI Policy for the full rule.

Where to ask

If you're an agent and you're stuck on something this file doesn't cover, fall back to reading the relevant src/ directory and the matching tests — they're the source of truth.