@japofc/baileys - v2.4.7
    Preparing search index...

    @japofc/baileys - v2.4.7

    JAPofc logo

    @japofc/baileys

    Typed ESM · always-current WA Web versioning · protocol-safe tooling · 140+ production bot utilities


    npm version npm downloads MIT license

    Node >=20 ESM only TypeScript types tests passing npm audit clean

    JAPofc WhatsApp Channel

    📦 Install · 🚀 Quick start · 🔥 Features · 💡 Examples · 📘 TypeScript · ❓ FAQ · 🇮🇩 Bahasa Indonesia


    Predictable socket, auto WA-Web version resolution, reconnect + anti-ban toolkit — no hidden side effects.

    Buttons, native flows, carousel, AIRich cards, polls, channels, VoIP, and 140+ bot utilities.

    Bundled .d.ts, multi-backend auth, credential redaction, session tools — 1513 tests, 0 vulns.

    📑 Table of contents — click to expand

    Getting started  ›  Install · Quick start · Overview · Installation · Authentication · Store backends

    What's inside  ›  Comparison · Built with · Features · Exclusive J.AP enhancements · New in v2.4.5

    Messaging  ›  Usage examples · Message helpers · Channels, history & transcripts · WA 2026 catch-up

    Calls & sync  ›  Voice & video calls · User sync queries · Username management

    Toolkit  ›  Everyday utilities · Security pack · Bot framework · Utility modules

    Reference  ›  Recommended environment · TypeScript support · FAQ & troubleshooting · Contributing · Credits · Maintainer


    npm i @japofc/baileys
    

    Node.js 20+ is required. The package is ESM-only and has no install/postinstall scripts.

    import makeWASocket, { useMultiFileAuthState } from '@japofc/baileys'

    const { state, saveCreds } = await useMultiFileAuthState('auth_info')
    const sock = makeWASocket({ auth: state, printQRInTerminal: true })

    sock.ev.on('creds.update', saveCreds)
    sock.ev.on('messages.upsert', async ({ messages }) => {
    const msg = messages[0]
    if (!msg?.message || msg.key.fromMe) return
    await sock.sendMessage(msg.key.remoteJid, { text: 'pong' }, { quoted: msg })
    })

    @japofc/baileys is a production-oriented WhatsApp Web library for bot builders who need predictable socket behavior, complete TypeScript declarations, and maintained utilities without hidden side effects.

    • Current WA Web fallback version plus makeWASocketAuto() for live version resolution.
    • Full WAProto declarations and deep package exports for installed-package consumers.
    • Auth backends for files, SQLite, Redis, MongoDB, MySQL, and PostgreSQL.
    • Interactive messages, native flows, carousel, AIRich cards, channels/newsletters, status helpers, and bot modules.
    • Hardened media/link fetching, credential redaction, session tools, reconnect helpers, and protocol-capture tooling.
    • No implicit group/channel join or follow. Join/follow helpers remain available, but they only run when your code explicitly calls them.

    How @japofc/baileys stacks up against other Baileys libraries:

    Capability @japofc/baileys Other forks (typical)
    Native Flow buttons (V1/V2/V3) ✅ ⚠️ partial
    Carousel messages ✅ ❌
    Rich response cards (AIRich) ✅ ❌
    Commerce flow (catalog/order/payment) ✅ ⚠️ partial
    Voice calling (VoIP) ✅ experimental ❌
    Multi-backend store (SQL/Mongo/Redis) ✅ ⚠️ partial
    Full .d.ts TypeScript definitions ✅ ⚠️ partial
    User sync queries (WAUSync) ✅ ✅
    Username management (check/set/pin/recommend) ✅ ❌
    Bot framework (middleware, session, stats) ✅ ❌
    Built-in QR render (terminal/SVG/PNG, zero deps) ✅ ❌ needs qrcode-terminal
    Auto WA Web version resolution ✅ ❌ hardcoded
    Status mentions (notify users/groups of your status) ✅ ❌
    Anti-ban toolkit (warmup ramps, group-op ceilings, disconnect classifier, Gaussian jitter) ✅ ❌
    Crash-message shield (mention bombs, zalgo, RTLO spoofing) ✅ ❌
    Session doctor + portable/encrypted session strings ✅ ❌
    Community/bot modules (economy, games, rental, moderation — 140+ utils) ✅ ❌
    Sticker EXIF branding ✅ pure JS ⚠️ needs node-webpmux
    CLI (doctor / session / sticker / export / wa) ✅ ❌
    Credential-redacting logger ✅ ❌

    This table describes package-level features, not performance benchmarks. PRs to update or correct it are welcome via Contributing.


    Node.js JavaScript ESM WebSocket Signal Protocol Protobuf npm

    Tech stack icons


    💬 Interactive Messages 🛒 Commerce & Business 🧩 Utility Features
    • Native Flow
    • Buttons (V1 / V2 / V3)
    • Lists
    • Carousel Messages
    • Poll & Quiz Messages
    • Rich Response Messages (AIRich)
    • CTA / Reply / URL Buttons
    • Call Buttons
    • OTP Buttons
    • Authentication Buttons
    • Catalog Message
    • Order Details
    • Order Status
    • Review & Pay
    • Payment Status
    • Payment Method
    • Track Order
    • Reorder
    • Cancel Order
    • Group Status Support
    • Mention All
    • Lottie Sticker Support
    • Newsletter Support
    • ExternalAdReply Helper
    • View Once Support
    • Rich Formatting
    • Code Highlighting
    🔐 Auth & Storage 📞 Realtime 🛠 Developer Tooling
    • Multi-file auth state
    • Single-file auth state
    • SQLite auth state
    • Cache-manager auth state
    • In-memory store
    • SQLite / MongoDB / MySQL / PostgreSQL / Redis store adapters
    • Voice calling (VoIP, WASM engine)
    • User sync queries (WAUSync)
    • Presence / status / device lookup
    • Event buffer for high-volume bots
    • Full TypeScript definitions (.d.ts)
    • Anti-delete & anti-edit detection (recover deleted messages, capture pre-edit content)
    • Message search helpers
    • Auto-reply engine
    • Scheduling helpers
    • Message retry manager

    Audio status uses a more compatible implementation to avoid unsupported-version errors on older WhatsApp clients.

    A chainable builder (AIRich, also exported as AIJap / LeafRich / JapAI / JapRich) with 40+ add*()/set*() methods covering headings, formatted text (hyperlinks/citations/LaTeX), code blocks, tables, image/video/product/post cards, task & progress cards, tip banners, and quick-reply suggestions — the kind of rich card layout you'd normally only see from an official AI/assistant-style bot. See Usage Examples below.

    Buttons & Actions

    • cta_reminder
    • cta_cancel_reminder
    • otp_button
    • authentication_button
    • call_button
    • url_button
    • reply_button
    • voice_call
    • video_call_button

    Commerce Flow

    • catalog_message
    • mpm
    • card_message
    • order_details
    • order_status
    • review_and_pay
    • payment_status
    • payment_method

    Navigation & Misc

    • address_message
    • send_location
    • track_order
    • reorder
    • cancel_order
    • clear_chat
    • navigateToScreen
    • flow (payload field: flow_action)

    Messages include:

    message.isSystemNotification
    

    Useful for filtering:

    • E2E notices
    • Meta service notices
    • Other system-generated messages

    npm install @japofc/baileys
    

    Directly from GitHub:

    npm install github:JAPofc/baileys
    

    Requires Node.js 20+ — enforced at import time with a clear error (no install scripts: this package deliberately ships zero preinstall/postinstall hooks, so npm ci --ignore-scripts and strict supply-chain policies work out of the box).

    Coming from @whiskeysockets/baileys? Migration is usually a one-line import change — see MIGRATION.md.

    Everything below is optional — the socket works without any of them. Install only what the features you use need; missing ones fail with a clear install hint instead of a silent crash.

    Package Compatible versions Unlocks
    sharp any Fast image resizing/processing (used by the message builders' Toolkit.resize)
    @napi-rs/image ~1.12.x Lighter native alternative to sharp for image ops
    jimp bundled (^1.6.x) Pure-JS image fallback when neither of the above is installed — ships as a regular dependency, nothing to install
    fluent-ffmpeg ^2.1.3 Audio/video conversion for media messages
    ffmpeg-static >=5.0.0 Bundled ffmpeg binary — auto-detected, zero config (~80MB; not available for Termux/Android, use pkg install ffmpeg there)
    @ffmpeg-installer/ffmpeg >=1.1.0 Alternative bundled ffmpeg binary — also auto-detected
    audio-decode ^2.2.3 Audio waveform/duration extraction (voice notes, VoIP capture)
    link-preview-js ^3.x Rich link previews for URLs in outgoing text messages
    better-sqlite3 ^11.x (Node 20 ABI) SQLite auth state, SQLite store adapter, and the Bot Framework's SQLiteStore/StatsManager
    mongodb ^6.10+ MongoDB store adapter
    mysql2 ^3.11+ MySQL store adapter
    pg ^8.13+ PostgreSQL store adapter
    ioredis ^5.4+ Redis store adapter
    @roamhq/wrtc any (platform-dependent native bindings) Native WebRTC bindings for voice calling
    axios any (optional) Fallback HTTP client for URL thumbnails in button messages — not required: the built-in fetch (Node 18+) is used first

    The version column mirrors this package's peerDependencies ranges — those are the ranges the test suite runs against. Newer majors usually work but aren't verified; npm ls <pkg> + printEnvironmentReport() (below) will tell you what you actually have.

    Every feature that shells out to ffmpeg (video thumbnails, sticker conversion, voice-note conversion, VoIP audio feeding) resolves the binary through one central resolver, in this order:

    1. Your override — setFfmpegPath('/path/to/ffmpeg') (exported from the package) or the FFMPEG_PATH env var
    2. ffmpeg-static — if installed, its bundled binary is used automatically
    3. @ffmpeg-installer/ffmpeg — same, as an alternative
    4. System ffmpeg on your PATH

    So on a normal server you can just npm i ffmpeg-static and never think about it. On Termux/Android (where neither npm package ships a binary) install the system one instead — it's picked up automatically:

    pkg install ffmpeg
    

    If nothing is found, the feature fails with a per-platform install hint instead of a cryptic spawn ffmpeg ENOENT.

    Wondering why some feature doesn't work? One call diagnoses everything — Node version, which optional deps are installed, where ffmpeg was found, the active image backend:

    import { printEnvironmentReport } from '@japofc/baileys'
    await printEnvironmentReport() // prints a full report + returns the snapshot
    // or the raw-data version without printing:
    import { checkEnvironment } from '@japofc/baileys'
    const env = await checkEnvironment() // { ok, platform, node, ffmpeg, imageBackend, optionalDeps, warnings }

    Or straight from the terminal, no code needed:

    npx @japofc/baileys doctor    # exit code 0 = all good, 1 = warnings
    npx @japofc/baileys version
    npx @japofc/baileys export dump.json --format csv --out chat.csv # WAMessage JSON → transcript

    import { makeWASocketAuto, useMultiFileAuthState } from '@japofc/baileys'

    const { state, saveCreds } = await useMultiFileAuthState('auth_info')

    // makeWASocketAuto resolves the freshest WA Web version before connecting
    // (WA sw.js -> fork -> fallback), preventing stale-version pairing 405s.
    // Prefer sync? `makeWASocket({ auth: state })` still works exactly as before.
    const sock = await makeWASocketAuto({
    auth: state
    })

    // persist credentials whenever Baileys updates them
    sock.ev.on('creds.update', saveCreds)

    sock.ev.on('connection.update', (update) => {
    const { connection, qr } = update

    // Easiest: pass `printQRInTerminal: true` to makeWASocket and the QR is
    // drawn automatically with the built-in zero-dependency renderer.
    // Manual/custom rendering from the event also works:
    // import { renderQRToTerminal, qrToSVG, qrToPNG } from '@japofc/baileys'
    // if (qr) console.log(renderQRToTerminal(qr)) // terminal (▀▄█)
    // if (qr) fs.writeFileSync('qr.svg', qrToSVG(qr)) // for a web UI
    // if (qr) fs.writeFileSync('qr.png', qrToPNG(qr)) // raster (send anywhere)
    if (qr) console.log('Got a pairing QR')

    if (connection === 'open') console.log('🍃 Connected!')
    })

    sock.ev.on('messages.upsert', ({ messages }) => {
    const msg = messages[0]
    if (!msg.message || msg.key.fromMe) return
    console.log('New message from', msg.key.remoteJid)
    })

    Production tip — wrap the socket in autoReconnect() and disconnect handling is done for you (exponential backoff, never reconnects on loggedOut, immediate reconnect after pairing):

    import { makeWASocketAuto, autoReconnect, useMultiFileAuthState } from '@japofc/baileys'

    const { state, saveCreds } = await useMultiFileAuthState('auth_info')
    const manager = autoReconnect(() => makeWASocketAuto({ auth: state, printQRInTerminal: true }), {
    onSocket: sock => {
    sock.ev.on('creds.update', saveCreds)
    sock.ev.on('messages.upsert', handler) // re-attached on every reconnect
    },
    onLoggedOut: () => console.log('delete auth_info/ and re-pair'),
    })
    await manager.start()

    Full runnable version: examples/auto-reconnect-bot.js.

    Observability — attach createDebugMonitor() for a safe, structured snapshot (connection state, disconnect reason, uptime, message latency percentiles, send-retry counts, Signal error tallies, VoIP/WASM state, memory). It never contains QR payloads, pairing codes, auth keys, tokens, or private keys — the whole snapshot is passed through redactSecrets() before it is returned, so it's safe to log or attach to bug reports as-is:

    import { createDebugMonitor } from '@japofc/baileys'

    const monitor = createDebugMonitor(sock)
    // later — in a health endpoint, cron log, or crash handler:
    console.log(JSON.stringify(monitor.getDebugInfo(), null, 2))
    // { connection: { state, uptimeMs, lastDisconnect: { code, reason } },
    // messages: { received, decryptFailed, latencyMs: { p50, p90, p99 } },
    // sendRetries, signalErrors: { noSession, badMac, ... }, voip, memory }

    Experimental protocol capture — new WhatsApp features should not be faked from wishful thinking; first capture the consenting test account's BinaryNode shape, redact secrets, then add a high-level wrapper once the wire contract is known:

    import { bindProtocolCapture } from '@japofc/baileys'

    const capture = bindProtocolCapture(sock, { file: './wa-protocol.ndjson' })
    // trigger the feature on a paired official client/test account
    // each line is redacted JSON: { direction, summary, node }
    await capture.close()

    Four auth-state backends ship out of the box. All return the same { state, saveCreds } shape expected by makeWASocket({ auth }), so they're drop-in interchangeable.

    Function Storage Best for
    useMultiFileAuthState(folder) One JSON file per key, on disk Default choice — simple, debuggable, works everywhere
    useSingleFileAuthState(fileName) One JSON file, on disk Small bots where a single file is easier to manage/back up
    useSqliteAuthState(opts) SQLite (better-sqlite3) Bots that already use SQLite, or want auth in one embedded DB file
    useCacheManagerAuthState(store, sessionKey) Any cacheable-compatible store Multi-session hosting panels, Redis-backed setups
    useRedisAuthState(opts) Redis (ioredis or node-redis client) Multi-instance bots, fast shared session storage
    useMongoAuthState(opts) MongoDB (mongodb collection) Bots already on Mongo; one document per key
    usePostgresAuthState(opts) Postgres (pg Pool/Client) Production deployments on Postgres; table auto-created
    useMySQLAuthState(opts) MySQL/MariaDB (mysql2/promise) Shared-hosting setups; table auto-created
    makeAuthStateFromStore(store) Any key-value backend you implement Custom databases — implement 5 small methods and you're done
    import { makeWASocket, useMultiFileAuthState } from '@japofc/baileys'

    const { state, saveCreds } = await useMultiFileAuthState('auth_info')
    const sock = makeWASocket({ auth: state })
    sock.ev.on('creds.update', saveCreds)
    import { makeWASocket, useMultiFileAuthState, formatPairingCode } from '@japofc/baileys'

    const { state, saveCreds } = await useMultiFileAuthState('auth_info')
    const sock = makeWASocket({ auth: state, printQRInTerminal: false })
    sock.ev.on('creds.update', saveCreds)

    if (!state.creds.registered) {
    const code = await sock.requestPairingCode('628123456789') // full international number
    console.log('Pairing code:', formatPairingCode(code)) // "ABCD-EFGH"
    }

    // custom 8-char code (Crockford base32: 1-9, A-Z minus I/O/U)
    await sock.requestPairingCode('628123456789', 'JAPJAP12')

    Input is validated up front with clear errors instead of the classic silent server-side failures: formatting noise (+, spaces, dashes) is stripped, a leading 00 international call prefix is removed automatically, and the two big footguns throw immediately — local-format numbers (08123... instead of 628123..., the #1 cause of "the pairing code never arrives") and numbers over the 15-digit E.164 maximum. The same check is exported standalone as normalizePairingPhone(phone). For public-facing bots, rate-limit pairing attempts with withPairingGuard.

    // SQLite variant
    import { makeWASocket, useSqliteAuthState } from '@japofc/baileys'

    const { state, saveCreds } = await useSqliteAuthState({ database: './auth.db' })
    const sock = makeWASocket({ auth: state })
    sock.ev.on('creds.update', saveCreds)

    All four database adapters accept either an existing client/collection (recommended — you control pooling and lifecycle) or a uri (the driver is lazily imported only then; none are hard dependencies):

    // Redis variant — works with ioredis AND node-redis
    import { makeWASocket, useRedisAuthState } from '@japofc/baileys'

    const { state, saveCreds } = await useRedisAuthState({ client: myRedis, session: 'bot-1' })
    const sock = makeWASocket({ auth: state })
    sock.ev.on('creds.update', saveCreds)

    useMultiFileAuthState also exports pruneStaleAuthFiles(folder, options) to clean up old sender-key files on a schedule — useful for long-running bots that accumulate thousands of stale key files.


    makeInMemoryStore() from lib/Store gives you the classic in-memory chat/contact/message cache. For anything that needs to survive a restart, makePersistentStore() (from PersistentStore.js) wraps one of five backends behind the same interface:

    Backend Function
    SQLite createSqliteStoreAdapter(opts)
    MongoDB createMongoStoreAdapter(opts)
    MySQL createMysqlStoreAdapter(opts)
    PostgreSQL createPostgresStoreAdapter(opts)
    Redis createRedisStoreAdapter(opts)
    import { makeWASocket, makeInMemoryStore } from '@japofc/baileys'

    const store = makeInMemoryStore({})
    store.readFromFile('./baileys_store.json')
    setInterval(() => store.writeToFile('./baileys_store.json'), 10_000)

    const sock = makeWASocket({ /* ...auth etc */ })
    store.bind(sock.ev)

    import { Button } from '@japofc/baileys'

    await new Button(sock)
    .setTitle('Promo Spesial')
    .setBody('Diskon 20% cuma hari ini')
    .setFooter('@japofc/baileys')
    .addReply('Klaim Sekarang', 'claim_promo')
    .addUrl('Lihat Katalog', 'https://example.com/catalog')
    .addCall('Hubungi Kami', '628123456789')
    .send(jid)
    import { Poll } from '@japofc/baileys'

    await new Poll(sock)
    .setName('Mau makan apa hari ini?')
    .addOptions(['Fried Rice', 'Chicken Noodles', 'Meatballs'])
    .setSelectable(1)
    .send(jid)

    v2.4.7 — the poll builder now validates before it sends. Duplicate options are rejected (poll results are bucketed by option text, so duplicates used to merge into one result), setEndDate() throws on an unparseable date instead of wiring endTime: NaN, and setQuiz() can no longer be combined with setAnnouncementGroup() (that path emits pollCreationMessageV2, which has no correctAnswer field, so the quiz was silently dropped). Quiz polls are also finally readable: getAggregateVotesInPollMessage() now recognises pollCreationMessageV5.

    const poll = new Poll(sock)
    .setName('Quiz: ibu kota Indonesia?')
    .addOptions(['Jakarta', 'Bandung', 'Surabaya'])
    .setQuiz('Jakarta')

    poll.validate() // [] when safe to send — collects every problem at once
    poll.assertValid() // throws the first problem (build()/send() run this for you)
    poll.countOptions() // 3
    poll.getOptions() // ['Jakarta', 'Bandung', 'Surabaya'] (a copy)
    poll.removeOption('Surabaya')
    Poll.MAX_OPTIONS // 12 — enforced, like MAX_NAME_LENGTH (255) / MAX_OPTION_LENGTH (100)

    await poll.send(jid)

    Cards are usually built with Button(...).toCard() first. Media cards work as before, and v2.4.7 also allows valid text/button-only carousel cards (WhatsApp renders them) via addTextCard() or a Button(...).toCard() card with no media header.

    import { Button, Carousel } from '@japofc/baileys'

    const cardA = await new Button(sock)
    .setImage('https://example.com/a.jpg')
    .setTitle('Produk A')
    .addUrl('Lihat', 'https://example.com/a')
    .toCard()

    const cardB = await new Button(sock)
    .setImage('https://example.com/b.jpg')
    .setTitle('Produk B')
    .addUrl('Lihat', 'https://example.com/b')
    .toCard()

    await new Carousel(sock)
    .setBody('Pilih salah satu produk di bawah ini')
    .addCard([cardA, cardB])
    .send(jid)

    // Text/button-only card, no image required:
    await new Carousel(sock)
    .setBody('Quick actions')
    .addTextCard({
    title: 'Support',
    text: 'Choose an action',
    buttons: [
    { id: 'faq', text: 'FAQ' },
    { webview: 'https://example.com/help', text: 'Open Help' }
    ]
    })
    .send(jid)
    import { AIRich } from '@japofc/baileys'

    await new AIRich(sock)
    .addHeading('Order Summary')
    .addText('Your order is being processed.')
    .addTable([
    ['Item', 'Qty'],
    ['Iced Latte', '2']
    ])
    .addTip('Orders are usually ready in 15 minutes')
    .addSuggest('Track Order')
    .send(jid)

    For the common case, v2.4.7 also exports a one-liner wrapper around the same builder:

    import { sendJapRich, jap } from '@japofc/baileys'

    await sendJapRich(sock, jid, {
    title: 'Order Summary',
    markdown: '# Paid\nYour order is being processed.',
    actions: { text: 'Track', url: 'https://example.com/order/123' },
    suggestions: ['Track Order', 'Contact Support']
    })

    // Or keep chaining after the shortcut builds the card:
    await jap(sock, '# Quick Report').addTip('Generated now').send(jid)

    Neutral aliases (sendRich, Rich, createRich, buildRich) remain available, but the JAP-branded helpers (jap, japRich, createJapRich, buildJapRich, sendJapRich) are the preferred names for new code.

    AIRich supports { id, insertAt } on every add*()/set*() call, so you can insert a block relative to one you added earlier instead of always appending to the end — handy for streaming/edit-in-place style responses combined with sendEdit(jid, id).

    streamText() — the "AI typing" effect. One bubble that grows via in-place edits (exactly how Meta AI streams its answers), instead of flooding the chat:

    // sends once, then patches the same bubble until the full text is out
    await new AIRich(sock).streamText(jid, longAnswer, {
    chunkSize: 120, // ~chars per reveal (split at word boundaries)
    intervalMs: 900, // pause between edits (min 300)
    cursor: ' ▍' // shown while streaming, removed at the end ('' disables)
    })
    // works alongside other blocks too: addHeading()/addTable() first, then streamText()

    readRichMessage() — the reader half. Parse a received rich card (from any AI/assistant-style bot) back into structured blocks instead of staring at opaque proto:

    import { readRichMessage } from '@japofc/baileys'

    sock.ev.on('messages.upsert', ({ messages }) => {
    const rich = readRichMessage(messages[0])
    if (!rich.found) return
    rich.blocks // [{ type: 'heading', text }, { type: 'code', language, code },
    // { type: 'table', rows, headerRows }, { type: 'text', text, entities }, …]
    rich.suggestions // every suggestion-pill text
    rich.text // flat text rendering of the whole card
    })

    Handles both wire forms (unified-response payload + proto submessages fallback), resolves inline link entities back to [label](url), re-joins syntax-highlighted code spans, never throws — non-rich input returns { found: false }. Round-trip tested against the AIRich builder itself.

    Other builders worth knowing about: ButtonV2 (simpler quick-reply-only buttons), ButtonV3 (loadFrom(msg) to edit an existing template message in place), and Toolkit (static helpers: Toolkit.resize(), Toolkit.fetchBuffer(), Toolkit.waitAllPromises(), Toolkit.extractIE() for parsing [label](url) links/citations/LaTeX out of plain text).

    Every message builder lives in its own file under lib/Builders/, so you can trace exactly where a class comes from instead of digging through one giant MessageBuilder.js. index.js is just the barrel file that re-exports everything below (plus their aliases) for the top-level import { ... } from '@japofc/baileys' syntax.

    File Exports What it's for
    shared.js Toolkit, BaseBuilder, RowBuilder The shared foundation every other builder is built on. BaseBuilder holds the common chainable send()/context-info plumbing, RowBuilder is the shared row/section helper, and Toolkit is the static-helper grab bag (resize, fetchBuffer, waitAllPromises, extractIE, media-duration/preview helpers). Nothing here is meant to be instantiated directly in bot code — it exists so Button, Poll, Carousel, etc. don't each reimplement the same plumbing.
    Button.js Button, CardBuilder The main interactive/native-flow button builder — headers (image/video/document), CTA helpers (addUrl, addCall, addReply, and the wider native-flow set), and .toCard() to turn a button message into a Carousel card.
    ButtonV2.js ButtonV2 A lighter-weight legacy buttonsMessage variant limited to up to 3 quick-reply buttons — includes validate()/assertValid(), countButtons()/getButtons()/clearButtons(), and relays with the same canonical getBizBinaryNode() envelope as the socket path.
    ButtonV3.js ButtonV3 Built around loadFrom(msg) — loads an existing templateMessage (e.g. one you fetched or that was quoted) so you can edit it in place. Also validates hydrated quick-reply/url/call buttons and relays with the canonical biz node.
    Carousel.js Carousel Chains Button(...).toCard() results into a swipeable card carousel. Accepts media cards and text/button-only cards (addTextCard()), validates before send, and is capped at Carousel.MAX_CARDS (10) since WhatsApp silently truncates anything beyond that.
    Poll.js Poll Poll/vote message builder — question, options, single/multi-select, hidden-voter mode, correct-answer marking, expiry.
    AIRich.js AIRich (+ aliases ORich, AIJap, LeafRich, JapAI, JapRich, RichJap) The rich AI-assistant-style response builder described above — headings, formatted text, tables, media/product/post cards, task/progress cards, tip banners, quick-reply suggestions.
    Rich.js jap, japRich, createJapRich, buildJapRich, sendJapRich (+ neutral aliases Rich, createRich, buildRich, sendRich) One-liner convenience wrapper over AIRich for common cards (markdown, text, actions, suggestions, media, etc.) without replacing the full chainable builder.
    A2UI.js A2UI, sendA2UIWidget Lower-level A2UI/Bloks widget builder. Builds the flat components tree (Column/Row → children, Card/Button → child, Modal → trigger/content) that Bloks widgets expect and sends it through the same getBizBinaryNode() path as Button/ButtonV2, so the wire-level node always matches the button names actually sent.
    JapBaileys.js JapBaileys A unified builder hub — one object that wraps all the builders above behind short method names (.button(), .buttonV2(), .buttonV3(), .carousel(), .poll(), .airich(), .rich(), .a2ui()), plus PascalCase aliases (.Button(), .Carousel(), .Poll(), .AIRich(), .Rich(), .A2UI()) for developers who prefer that naming style.
    index.js everything above, plus MESSAGE_BUILDER_VERSION The barrel file — re-exports every builder and its aliases so import { Button, Poll, AIRich, JapBaileys } from '@japofc/baileys' works without reaching into individual files. MESSAGE_BUILDER_VERSION tracks the builder API surface's own version, independent of the package version.

    Unified hub example — useful if you'd rather carry one object around than import each builder individually:

    import { JapBaileys } from '@japofc/baileys'

    const vx = new JapBaileys(sock)

    await vx.button()
    .setTitle('Promo Spesial')
    .addReply('Klaim Sekarang', 'claim_promo')
    .send(jid)

    await vx.poll()
    .setName('Mau makan apa hari ini?')
    .addOptions(['Fried Rice', 'Chicken Noodles'])
    .send(jid)

    // Pin a message for 7 days (default 24h), unpin, keep / unkeep in disappearing chats
    await sock.sendPin(jid, msg.key, { durationSec: 7 * 86400 })
    await sock.sendUnpin(jid, msg.key)
    await sock.sendKeep(jid, msg.key)
    await sock.sendUnkeep(jid, msg.key)
    await sock.sendEvent(jid, {
    name: 'Weekly Meeting',
    description: 'Bahas progres bot',
    startDate: new Date('2026-09-15T10:00:00+07:00'),
    endDate: new Date('2026-09-15T11:00:00+07:00'),
    location: { name: 'Office', address: 'Jakarta' },
    extraGuestsAllowed: true
    })

    Native scheduled-call message (the "Schedule call" card with a join reminder):

    // 'voice' (default) or 'video'
    await sock.sendScheduledCall(jid, {
    title: 'Daily standup',
    scheduledAt: new Date('2026-09-15T09:00:00+07:00'),
    callType: 'video'
    })

    // Cancel it later (needs the creation message's key)
    await sock.cancelScheduledCall(jid, creationMsg.key)

    Two modes — pass url, flow, or both (two buttons):

    import { sendMiniApp } from '@japofc/baileys'

    await sendMiniApp(sock, jid, {
    title: 'My Mini App',
    body: 'Tap the button to open the app 👇',
    // 1) webview mode: rich card + CTA opening your web app
    // (in-app webview when supported, else the browser)
    url: 'https://myapp.example.com',
    params: { ref: 'wa-bot' }, // → appended as ?ref=wa-bot
    buttonText: '🚀 Open App',
    // 2) Flows mode: TRUE native in-chat mini app (forms/screens inside
    // WhatsApp, no browser). Needs a published Flow ID from Flows Manager.
    flow: { id: '123456789', cta: '📝 Isi Form', screen: 'WELCOME' },
    thumbnail: 'https://myapp.example.com/icon.png' // url or Buffer
    })
    // or as a socket method: await sock.sendMiniApp(jid, { ... })

    High-level one-liners for modern message types — validated, with friendly aliases:

    await sock.sendLocation(jid, { lat: -6.2, lng: 106.8, name: 'Jakarta' });
    await sock.sendContact(jid, { name: 'John', vcard: 'BEGIN:VCARD\n...' });
    await sock.sendGroupInvite(jid, { code: 'AbC123', jid: groupJid, subject: 'Community' });
    await sock.sendPaymentRequest(jid, { currency: 'IDR', amount: 50000, note: 'coffee ☕' });
    await sock.sendInvoice(jid, { note: 'INV-001' });
    await sock.sendPollOption(jid, pollKey, ['New option']);
    await sock.sendEventInvite(jid, { eventTitle: 'Party', startTime: new Date(...) });
    await sock.sendNewsletterInvite(jid, { newsletterJid, newsletterName: 'News' });

    // Poll upgrades (already wired end-to-end):
    await sock.sendPoll(jid, {
    name: 'Jam berapa?', values: ['Pagi', 'Sore'],
    endDate: new Date('2026-09-16T00:00:00Z'), // auto-close
    hideVoter: true, // anonymous poll
    canAddOption: true, // voters may add options
    });

    Music messages are experimental — the proto is correct but interop with official clients is unverified (see Honestly not implemented):

    await sock.sendMusic(jid, { songUri, artworkUri, embeddedMusic: { songId, title, author } });
    

    Escape hatch: every content key also works inline via sendMessage, and { raw: true, <AnyMessageField>: {...} } passes any proto field through untouched — full WAProto coverage with zero wrapper lag:

    await sock.sendMessage(jid, { raw: true, musicMessage: { songUri } });
    

    TypeScript consumers get a typed AnyMessageContent (import type { AnyMessageContent } from '@japofc/baileys').

    Channel management (live queries):

    await sock.newsletterDelete(channelJid)
    await sock.newsletterAdminCount(channelJid)
    await sock.newsletterJoinInvite('invite-code')
    await sock.newsletterChangeOwner(channelJid, newOwnerJid) // experimental
    await sock.communityGetInviteCode(communityJid)
    // ...plus existing follow/unfollow/mute/react/fetch + full community CRUD

    Group history sharing for new members (forwards recent messages to their DM):

    import { getGroupHistoryFromStore } from '@japofc/baileys'
    const messages = getGroupHistoryFromStore(store, groupJid, 10)
    await sock.shareGroupHistory({ groupJid, members: newMemberJid, messages, greeting: true })

    Voice-note transcription (pluggable provider — cloud Whisper or your own):

    import { openAIWhisperProvider } from '@japofc/baileys'
    const provider = openAIWhisperProvider({ apiKey: process.env.OPENAI_API_KEY })
    const { text } = await sock.transcribeMessage(voiceNoteMsg, { provider })

    Mini-app deep links (HMAC-signed params your web app can verify):

    import { createMiniAppLink, parseMiniAppParams, buildFlowDataExchange } from '@japofc/baileys'
    const link = createMiniAppLink('https://app.example.com/', { uid: '123' }, { secret: 's3cr3t' })
    parseMiniAppParams(link, { secret: 's3cr3t' }) // → { uid: '123' } (throws if tampered)
    buildFlowDataExchange('navigate', { screen: 'HOME' }) // Flows data_exchange payload

    A2UI widgets now include Slider, Switch, List, ProgressBar, Avatar, Badge, Spacer, and Tabs alongside the existing Text/Image/Video/Button/Card/Modal set (all ref-validated at build()).

    Drop-in hardening for auth state, secrets, and abuse — all in lib/Utils/auth-secure.js, covered by tests/security.test.js (incl. fuzzing).

    import {
    useEncryptedFileAuthState, secureLogger, withPairingGuard,
    createQRGuard, backupAuthState, writeAuthIntegrity, repairAuthState, secureLogout
    } from '@japofc/baileys'

    // 1. AES-256-GCM encrypted auth state (reads legacy plaintext, migrates on save)
    const { state, saveCreds } = await useEncryptedFileAuthState('./auth', { password: process.env.AUTH_PW })
    const sock = makeWASocket({
    auth: state,
    logger: secureLogger(pino({ level: 'info' })), // 7. redacts qr/tokens/keys in every log line
    })

    // 2. pairing-code rate limit: 5/hour per number, 30s between attempts
    withPairingGuard(sock, { maxPerHour: 5, minIntervalMs: 30_000 })

    // 3. QR fist-guard: handle the first QR, swallow re-emits for 60s
    const qrGuard = createQRGuard({ ttlMs: 60_000 })
    sock.ev.on('connection.update', ({ qr }) => { qr = qrGuard.handle(qr); if (qr) show(qr) })

    // 6/8. encrypted backup + integrity snapshot
    await backupAuthState('./auth', './auth.jabackup', { password: process.env.BACKUP_PW })
    await writeAuthIntegrity('./auth', { secret: process.env.INT_PW })

    // 9. repair corrupt stores (quarantines + restores creds from backup)
    await repairAuthState('./auth', { password: process.env.AUTH_PW, backupFile: './auth.jabackup', backupPassword: process.env.BACKUP_PW })

    // 4. logout + overwrite/unlink every auth file + scrub in-memory creds
    await secureLogout(sock, { authFolder: './auth' })

    Single-file variant: useEncryptedSingleFileAuthState(file, { password }).

    WhatsApp 2026 features, wired into JAP-Baileys. Everything here was audited against the latest official clients — only gaps were added; what already worked is just documented.

    await sock.checkUsername('japstore');            // availability
    await sock.setUsername('japstore'); // claim it
    await sock.reserveUsername('japstore'); // reserve it (2026 reservation flow)
    await sock.findUserByUsername('japstore'); // USync lookup → JID
    const u = await sock.fetchContactUsernames(['62812@s.whatsapp.net']);

    Polls are editable for ~15 minutes after creation (server-side rule):

    const sent = await sock.sendPoll(jid, { name: 'Jam berapa?', values: ['Pagi', 'Sore'] });
    await sock.editPoll(jid, sent.key, { name: 'Jam berapa?', values: ['Pagi', 'Siang', 'Sore'] });
    await sock.sendMentionAll(groupJid, 'Announcement: meeting at 9!');
    

    In groups with 32+ members @all is admin-only and the rule is enforced server-side — non-admin calls are silently dropped by WhatsApp.

    Post a status and notify specific users or whole groups — they get the "mentioned you in their status" bubble linking to your status:

    // text status, mention two users
    await sock.sendStatusMention({ text: 'Big announcement! 🎉' }, [
    '62812xxx@s.whatsapp.net',
    '62813xxx@s.whatsapp.net',
    ])

    // image status, mention an entire group (every member is notified)
    await sock.sendStatusMention({ image: buffer, caption: 'New drop 🔥' }, [groupJid])

    Works with any status content (text, image, video, audio). Pace the notification fan-out with { delayMs } in options (default 1500 ms per jid).

    await sock.sendMessage(jid, {
    event: {
    title: 'Meeting', description: 'Q3', startDate: new Date('2026-09-15T09:00:00+07:00'),
    reminder: true, reminderOffsetSec: 1800, // remind 30 min before start
    }
    });

    Fixes LID-only payloads (e.g. call offers where caller_pn is missing):

    sock.ev.on('messages.upsert', async ({ messages }) => {
    for (const m of messages) console.log(await sock.resolveSenderPn(m)); // '62812…' | null
    });

    sendVoiceNote() content composes with viewOnce:

    await sock.sendMessage(jid, { ...(await buildVoiceNoteContent('./a.ogg')), viewOnce: true });
    

    Already exposed — no new code, documented here:

    await sock.updateMemberLabel({ groupJid, lid, label: 'Admin' });
    
    • Voice message transcripts — generated on-device by official clients only; there is no transcript API on the wire.
    • Group message history sharing (native) — WhatsApp began rolling out the official "Group Message History" feature (share the last 25–100 messages with a newly added member, E2EE, admin-controlled) in Feb 2026. Its wire format still hasn't been captured by any public library (re-verified 28 Sep 2026 by downloading and API-diffing upstream Baileys 7.0.0-rc14 plus the 8 largest living forks — none carry it). Until it is, this repo ships an honest workaround — shareGroupHistory() forwards recent messages into the new member's DM — which is NOT the native flow (no in-group history bubble, no "history shared" notice). If you need native behaviour, the only path today is an official client.
    • Music messages (interoperability) — the MusicMessage proto struct round-trips fine and sendMusic() emits it, but real interop needs provider catalog IDs (Spotify/Apple) plus an artwork upload flow that official clients negotiate privately; no public capture exists (re-verified Sep 2026). Messages built with made-up IDs render as a plain preview or nothing at all on official clients — treat sendMusic() as experimental and test against a real device before shipping. What v2.4.1 does fix: unknown embeddedMusic fields used to be silently dropped by the proto encoder (a typo'd key just vanished from the wire) — they now throw a clear 400 listing the supported field names. For the modern music-on-status surface, see withMusicAttribution() below.
    • mediaKeyDomain — now IMPLEMENTED (kept here for history). Backported from rc14 onto all 5 media types (Audio/Document/Image/Sticker/VideoMessage, enum UNSET/E2EE_CHAT/STATUS/CAPI/BOT) with a send passthrough: sendMessage(jid, { image: buf, mediaKeyDomain: 1 }). Unset by default (recommended — the server labels it). Skipped only on MMSThumbnailMetadata, where upstream's field number 8 collides with the newer messageHistoryMetadata in our proto.

    One-liners for daily bot work (also usable standalone — see examples/):

    // 📊 Poll (chats/groups) + Quiz (channels only)
    await sock.sendPoll(jid, { name: 'What to eat?', options: ['Rice', 'Noodles'] })
    await sock.sendQuiz(channelJid, { name: 'Quiz', options: ['A', 'B'], correctAnswer: 'A' })

    // 🎙️ Voice note — auto-converts mp3/wav/… to Opus PTT (needs fluent-ffmpeg)
    await sock.sendVoiceNote(jid, './hello.mp3') // path | Buffer | { url }

    // 📞 Tag everyone (visible @list vs hidden)
    await sock.tagAll(groupJid, 'Meeting at 10!')
    await sock.hideTag(groupJid, 'Announcement 📢')

    // 🔍 LID → phone number (best-effort, null when unknown)
    await sock.getPhoneNumber('12345@lid') // → '62812…'
    await sock.getLidForPhone('62812…') // reverse lookup

    // 🧍 Humanized send — typing… + natural delay + per-chat queue
    await sock.sendHumanized(jid, { text: 'Hello!' })

    // 🛡️ Send Guard — global anti-ban pacing baked into sendMessage()
    const sock = makeWASocket({
    sendRateLimit: {
    messagesPerMinute: 20, // global token-bucket ceiling (0 = off)
    perChatDelayMs: 1500, // minimum gap between sends to the same chat
    jitterRatio: 0.2 // ±20% randomness so timing never looks robotic
    }
    })
    await sock.sendMessage(jid, { text: 'auto-paced 🛡️' }) // paced
    await sock.sendMessage(jid, { text: 'now!' }, { skipRateLimit: true }) // bypass

    // ✅ Delivery tracking — await the server ack of an outgoing message
    const msg = await sock.sendMessage(jid, { text: 'important!' })
    await sock.waitForMessageAck(msg.key.id) // resolves on ack, rejects on
    // server error / 60s timeout

    // ✅ One-call variant — send + await the ack, race-free (waiter registered
    // BEFORE the send goes out, so a fast ack can never slip through)
    const { message, ack } = await sock.sendMessageAcked(jid, { text: 'critical!' }, { ackTimeoutMs: 30_000 })

    // 📣 Broadcast to many jids — paced, per-jid outcomes, never dies mid-run
    const report = await sock.sendBroadcast(jids, { text: 'promo!' }, { delayMs: 1500 })
    // { sent: [...], failed: [{ jid, error }], total }

    // 🔗 Group invite links
    extractGroupInviteCode('https://chat.whatsapp.com/AbCdEf…') // 'AbCdEf…'
    await sock.joinGroupViaLink('https://chat.whatsapp.com/AbCdEf…')

    // 🏷️ Parse @mentions out of text → ready for sendMessage
    const mentions = parseMentions(text) // ['62812…@s.whatsapp.net', …]
    await sock.sendMessage(jid, { text, mentions })

    // 🤖 Meta AI chat (experimental — needs Meta AI on the account/region)
    const { text } = await sock.askMetaAI('Explain black holes!')

    Command router for prefix bots (!menu, .sticker) with middleware + auto-help:

    import { createRouter } from '@japofc/baileys'
    const router = createRouter({ prefix: '!' })
    router.command('ping', async (ctx) => ctx.reply('pong! 🏓'), { desc: 'Check bot' })
    router.attach(sock) // → detach()

    Status / channel schedulers (in-memory):

    import { StatusScheduler, ChannelScheduler, StatusHelper } from '@japofc/baileys'
    new StatusScheduler(sock).schedule(StatusHelper.text('Pagi! ☀️'), new Date('2026-09-11T06:00:00+07:00'))
    new ChannelScheduler(sock).schedule(channelJid, { text: 'Update' }, Date.now() + 3600_000)

    Music attribution on statuses (StatusAttribution.Type.MUSIC — the official Dec-2025 "add music to your status" surface, wire-verified against WAProto):

    import { StatusHelper, withMusicAttribution } from '@japofc/baileys'

    const status = withMusicAttribution(
    StatusHelper.text('vibes 🎵'),
    { title: 'Song Name', authorName: 'Artist', songId: '<catalog-id>' }
    )
    await StatusHelper.send(sock, status, jidList)

    Official clients resolve the song via Meta's licensed catalog, so songId must be a real catalog id for the music chip to render there. The attribution struct itself is wire-correct either way (round-trip covered by tests).

    Chat export & statistics (offline — official "Export chat" format, JSON, CSV):

    import { exportChatAsText, exportChatAsCSV, chatStatistics } from '@japofc/baileys'

    // messages: WAMessage[] from your store / anti-delete cache / messages.upsert
    console.log(exportChatAsText(messages))
    // 14/09/2026, 10.32 - J.AP: halo!
    // 14/09/2026, 10.33 - Rina: <Media omitted>

    fs.writeFileSync('chat.csv', exportChatAsCSV(messages))

    const stats = chatStatistics(messages)
    // { total, bySender, byKind, byHour[24], byWeekday[7], topWords, ... }

    Also available with zero code: npx @japofc/baileys export dump.json --format text|json|csv.

    Offline bot testing (createMockSocket) — test your bot logic in CI with zero WhatsApp account, zero network, zero ban risk:

    import { createMockSocket, createRouter } from '@japofc/baileys'
    import assert from 'assert'

    const mock = createMockSocket()
    myBotSetup(mock.sock) // your real bot code, unchanged

    await mock.receiveText('628xx@s.whatsapp.net', '!ping')
    const reply = await mock.waitForReply()
    assert.equal(reply.content.text, 'pong! 🏓')

    Same ev surface and sendMessage() signature as the live socket; outgoing messages are REAL proto.WebMessageInfo objects built through the same generateWAMessage() pipeline. Supports group injection, quoted replies, connection lifecycle simulation, read-receipt/presence capture, and reset() between tests. Honest scope: it does not emulate WhatsApp servers — rate limits, sessions, and encryption are out of scope by design.

    npm test runs the offline suite (tests/, 1226 tests, no network needed).


    Fresh helpers added this release. All are exported from the package root and fully typed.

    getAggregateVotesInPollMessage is stateless and double-counts a voter who changes their mind (WhatsApp re-sends the voter's full selection on every change). createPollManager keeps that state — each voter counts once, latest choice wins:

    import { createPollManager } from '@japofc/baileys'

    const polls = createPollManager()
    const sent = await sock.sendMessage(jid, { poll: { name: 'Lunch?', values: ['Pizza', 'Sushi'], selectableCount: 1 } })
    polls.register(sent) // remembers options + key

    // feed it decrypted votes (option names, sha256 hex, or the raw hash Buffers WA sends):
    polls.applyVote(sent.key.id, voterJid, ['Pizza'])

    polls.tally(sent.key.id) // [{ name:'Pizza', count, voters:[] }, ...]
    polls.winner(sent.key.id) // { winners:['Pizza'], count } (tie-aware)
    console.log(polls.render(sent.key.id)) // live bar chart

    Detects bursts of undecryptable messages (a dead Signal session / Bad-MAC drift) per contact and alerts you — purely observational, never throws into your event loop:

    import { createSessionHealthMonitor } from '@japofc/baileys'

    const health = createSessionHealthMonitor({ badMacThreshold: 3, windowMs: 60_000 })
    health.bind(sock)
    health.onUnhealthy(({ jid, failures }) => console.warn(`${jid} session looks broken (${failures} decrypt fails)`))
    health.onRecover(({ jid }) => console.log(`${jid} recovered`))

    Ships with canonicalizeJid() and makeJidCanonicalizer(sock) to key a contact by one identity across the PN/LID split.

    Stops double-replies when WhatsApp re-delivers a message (reconnect, history sync, placeholder resend, retries):

    import { createMessageDedupe } from '@japofc/baileys'

    const dedupe = createMessageDedupe({ maxSize: 5000 }) // optional ttlMs
    sock.ev.on('messages.upsert', ({ messages }) => {
    for (const m of messages) {
    if (dedupe.seen(m.key)) continue // already handled → skip
    // ...handle exactly once
    }
    })

    The mapping helpers are now first-class socket methods (no more reaching into signalRepository):

    await sock.getLIDForPN('62812…@s.whatsapp.net') // → '…@lid' | null
    await sock.getPNForLID('123…@lid') // → '…@s.whatsapp.net' | null
    await sock.getLIDsForPNs([...]) // batch
    await sock.getPNsForLIDs([...]) // batch
    await sock.storeLIDPNMappings([{ lid, pn }]) // seed the cache

    Ten new pure, typed helpers exported from the root:

    import { partition, zip, formatCountdown, wordCount, ellipsisMiddle,
    jidType, randomString, isNumeric, firstEmoji, maskJid } from '@japofc/baileys'

    partition([1, 2, 3, 4], n => n % 2 === 0) // [[2,4],[1,3]]
    zip([1, 2], ['a', 'b']) // [[1,'a'],[2,'b']]
    formatCountdown(90_061_000) // '1d 01:01:01'
    ellipsisMiddle('abcdefgh', 5) // 'ab…gh'
    jidType('123@g.us') // 'group'
    maskJid('628123456789@s.whatsapp.net') // '628******89@s.whatsapp.net'

    Experimental audio-call support via a bundled WASM call stack + WebRTC relay. Requires the optional @roamhq/wrtc peer dependency.

    import { VoipClient } from '@japofc/baileys'

    const voip = new VoipClient({ resourcesPath: './voip-resources' })
    await voip.connectWithSocket(sock)

    const call = await voip.call('628123456789')

    call.on('ringing', () => console.log('Ringing...'))
    call.on('connected', () => console.log('Call connected'))
    call.on('ended', (reason) => console.log('Call ended:', reason))

    ActiveCall (returned by .call()) and CallState are also exported directly if you need finer-grained control over call state.

    Answering inbound calls — offers are tracked, so the bot can pick up (1:1) or join (group):

    import { attachVoip } from '@japofc/baileys'
    const voip = await attachVoip(sock) // also stored as sock.voip

    voip.on('incoming-call', async ({ callId, from, isGroupCall, busy }) => {
    if (busy) return
    if (isGroupCall) await voip.joinGroupCall(callId, { audioSource: './greeting.mp3' })
    else await voip.answerCall(callId, { audioSource: './greeting.mp3' })
    })

    // ...or fully automatic:
    await attachVoip(sock, { autoAnswer: true }) // pick up 1:1 calls
    await attachVoip(sock, { autoJoinGroup: true }) // join group calls
    await attachVoip(sock, { autoReject: true, autoRejectText: 'Bot cannot take calls 🙏' })

    // still-pending offers (answer before `offerTtlMs`, default 45s):
    voip.getPendingCalls() // → [{ callId, from, isGroupCall, ... }]

    Group / multi-party calls:

    const gcall = await voip.startGroupCall(groupJid, ['62812…', '62813…'], { chatName: 'Meeting' })
    await voip.inviteToGroupCall('62814…')
    await voip.removeGroupParticipant('62813@s.whatsapp.net')
    await voip.rejoinGroupCall() // recovery after a drop

    voip.on('group-call-started', console.log)
    voip.on('group-call-joined', console.log)

    Calls also support reactions, hand-raise, recording, and call links:

    const call = await voip.call('628123456789', { audioSource: './greeting.mp3' })
    call.react('👍')
    call.setHandRaised(true)
    const stopRecording = call.recordToFile('./call.wav') // remote peer → .wav
    await voip.previewCallLink('call-link-token')

    // 🔀 switch what the call plays WITHOUT hanging up (IVR-style flows)
    call.setAudioSource('./menu.mp3') // file/URL
    call.setAudioSource({ data: buffer, ext: 'mp3' }) // in-memory audio
    call.setAudioSource('lavfi:sine=frequency=440') // generated tone
    call.setAudioSource('silence') // stop playing, stay on the call

    Reconnect & recovery — a watchdog monitors the relay transport during every call (watchdogIntervalMs/watchdogMaxSilent); on a dead relay it emits call-degraded and automatically re-sends the crypto rekey + offer. Manual controls:

    voip.on('call-degraded', ({ callId }) => console.log('relay dead, recovering', callId))
    voip.on('call-recovery', (r) => console.log('recovery result', r))
    await voip.recoverCall({}) // manual: { rekey: true, offer: true }
    voip.getStats() // { busy, callId, call, relay }
    sock.ev.on('call', (calls) => { /* standard offer/reject path still works */ })

    Refreshing the WASM stack — if calls break after a WA Web update, re-fetch the official VoIP build directly from WhatsApp Web's bootloader endpoint:

    npm run voip:fetch-wasm
    

    No browser is needed in the default path. If WhatsApp changes the public bootloader response, the script can still fall back to a logged-in Chrome session with CALL_WASM_FETCH_MODE=browser and --remote-debugging-port=9222.


    WAUSync (USyncQuery / USyncUser + protocols) lets you check things like WhatsApp registration, device lists, status, and username info for a JID before you message it — the same mechanism behind sock.onWhatsApp().

    import { USyncQuery, USyncUser, USyncContactProtocol } from '@japofc/baileys'

    const query = new USyncQuery()
    .withContext('interactive')
    .withMode('query')
    .withUser(new USyncUser().withPhone('628123456789'))

    query.protocols.push(new USyncContactProtocol())

    const result = await sock.executeUSyncQuery(query)

    Available protocols: USyncContactProtocol, USyncDeviceProtocol, USyncStatusProtocol, USyncUsernameProtocol, USyncDisappearingModeProtocol, UsyncBotProfileProtocol, UsyncLIDProtocol.


    High-level wrappers around WhatsApp's username feature (the @username handle you can set instead of exposing your phone number), sitting on top of USyncUsernameProtocol.

    ⚠️ Query-ID rotation. The GraphQL query IDs behind these calls are captured from live WA Web sessions and WhatsApp rotates them from time to time. When that happens calls fail with GraphQL server error: Bad Request (or unexpected response structure). You don't have to wait for a package update — hot-patch the rotated ID at runtime:

    const sock = makeWASocket({
    usernameQueryIds: { CHECK: '<fresh-id>', SET: '<fresh-id>' } // override any of:
    // CHECK, CHECK_MULTI, SET, GET, GET_RECOMMENDATIONS, PIN_SET
    })

    Fresh IDs can be captured from a live WA Web session (DevTools → Network → WS frames → look for xmlns="w:mex" queries).

    // Check availability + get suggestions if taken
    const check = await sock.checkUsername('J.AP')
    // { available: true, username: 'J.AP' }
    // or: { available: false, suggestions: [...], rejectionReasons: [...] }

    // Claim a username
    await sock.setUsername('J.AP', { source: sock.USERNAME_SOURCE.USER_INPUT })

    // Lock it behind a PIN so it can't be changed without one
    await sock.setUsernamePin('123456')

    // Read your own username / drop it
    const mine = await sock.getMyUsername()
    await sock.deleteUsername()

    // Resolve a username to a JID (USync-based, like onWhatsApp() but by username)
    const user = await sock.findUserByUsername('someone')
    // { jid: '628...@s.whatsapp.net', contact: false }

    // Batch-resolve usernames for a list of known contacts
    const usernames = await sock.fetchContactUsernames(jid1, jid2, jid3)

    // Get WA's own suggestions (e.g. for onboarding flows)
    const recs = await sock.getUsernameRecommendations()

    Requires a Community/Contact-tier account in good standing — accounts under WA's usual restrictions for new/unverified numbers may see INVALID or empty suggestions regardless of the username's actual availability.


    An optional, higher-level layer on top of the raw socket: middleware routing, a !command dispatcher, a message queue that survives disconnects, exponential-backoff auto-reconnect, per-JID session storage, group activity stats, and media (sticker/voice-note) conversion helpers — so a new bot project doesn't have to hand-roll session/context management every time.

    Needs the better-sqlite3 peer dependency (session + stats storage) and fluent-ffmpeg (sticker/voice-note conversion, already listed above) — both fail with an install hint rather than crashing if you use a Framework feature that needs them without installing them first.

    import { Bot } from '@japofc/baileys'

    const bot = new Bot({
    socketConfig: { printQRInTerminal: true },
    dbPath: './bot.db', // sessions + stats, defaults to 'baileys_store.db'
    enableStats: true // group message/sticker leaderboards + ghost detection
    })

    bot.command('!ping', async (ctx) => {
    await ctx.reply({ text: 'pong' })
    })

    bot.command('!sticker', async (ctx) => {
    if (!ctx.quoted?.imageMessage) return ctx.reply({ text: 'Reply to an image with !sticker' })
    // ctx.replySticker() handles the WebP conversion for you
    await ctx.replySticker(imageBuffer, { packname: 'J.AP Pack', author: 'you' })
    })

    bot.onText(async (ctx) => {
    // ctx.session() / ctx.setSession() / ctx.updateSession() / ctx.clearSession()
    // persist small per-chat state (e.g. multi-step flows) to SQLite automatically
    const state = ctx.session()
    if (state?.awaitingReply) {
    ctx.updateSession((s) => ({ ...s, awaitingReply: false }))
    }
    })

    await bot.start()

    Context, SessionManager, StatsManager, MediaManager, and SQLiteStore are also exported individually if you only need one piece rather than the full Bot class.

    These files ship as plain .js for now — hand-written .d.ts declarations for the Framework module haven't been added yet, unlike the rest of this fork's fully-typed surface.


    📖 Show the full utility-module reference — 140+ helpers across lib/Utils

    A sample of the utilities exported from lib/Utils beyond the message builders above:

    Module What it does
    anti-delete Detect and recover messages the sender deleted for everyone
    anti-edit Capture what a message said BEFORE it was edited — before/after text, full revision history, chained edits
    trackers Reaction / receipt / presence trackers — who reacted what, who read your group message, who's online/typing
    serialize serializeMessage — flat bot-friendly message object with .reply(), .react(), .download(), quoted unwrap
    call-guard Track incoming calls, auto-reject with an optional text, per-caller counters, allowlist
    group-events Welcome/goodbye/promote/demote callbacks + per-group event log from group updates
    view-once Detect, unwrap and capture view-once messages before they disappear
    anti-link Detect (and auto-delete) group-invite/any links in groups — allowlists for chats & domains
    auto-read Auto blue-tick incoming messages — group/DM/status filters, allow/deny lists, pause/resume
    afk AFK manager — mark users away, catch @mentions & replies while away, auto welcome-back
    pairing-tools Pairing code lifecycle — validate/normalize custom codes, expiry countdown, pairWithCode one-call flow
    flood-guard Per-user burst detection — N messages in a window fires one alert per burst
    word-filter Keyword/regex moderation over full extracted text (captions too), auto-delete, runtime word list
    warn-manager Strike system — warns per user per chat, thresholds, pardon, persistence
    gatekeeper Ban users/chats from the bot; wrap any handler so banned traffic never reaches it
    level-system XP & levels per user, level-up events, global + per-chat leaderboards, persistence
    sticker-exif Read/write sticker pack-name/author EXIF on WebP in pure JS — no native deps, Termux-friendly
    economy Balances, transfers with fees, daily rewards with streak bonuses, leaderboard, persistence
    group-scheduler Open/close groups on a daily schedule ("night mode"), weekday filters
    verifier Captcha-gate new group members — auto challenge on join, timeout/attempt kick hooks
    command-stats Command analytics — top commands/users, hourly histogram, router middleware, persistence
    anti-tagall Catch mass-mention & invisible hidetag spam from members — threshold, exemptions, auto-delete
    shop Shop & inventory on top of the economy — stock, consumables, sell-back, gifting
    group-backup Snapshot group settings + members to JSON, diff against live, restore settings
    menfess Anonymous two-way DM relay sessions ("menfess" bot) — aliases, stop words, TTL
    notes Named snippets per chat (#save / #get) — search, rename, limits, persistence
    birthday Birthday book — today/upcoming lists, auto-congratulate once per year
    guess-game "Tebak-tebakan" engine — one round per chat, first correct wins, rewards, timeout reveal
    i18n Tiny translation layer — dictionaries, per-chat language, {var} interpolation
    fancy-text Unicode restyling for menus — 𝗯𝗼𝗹𝗱, 𝚖𝚘𝚗𝚘, ⓒⓘⓡⓒⓛⓔⓓ, fullwidth, ꜱᴍᴀʟʟᴄᴀᴘꜱ (12 styles)
    join-requests Auto approve/reject group join requests — allow/deny lists, manual routing, pending sweep
    session-tools Session doctor — analyze/repair auth folders, portable session-string export/import, cross-backend migration
    shutdown Graceful shutdown manager — creds flushed first, ordered hooks, signal handling, run-once
    conversation-flow Multi-step wizards per user — prompts, validation, cancel words, timeouts
    webhook-bridge POST socket events to any HTTP endpoint — HMAC signatures, retries with backoff
    wa-links Build/parse wa.me, group-invite & channel URLs; extract URLs from text
    media-probe Pure-JS image format + dimensions (PNG/JPEG/GIF/WebP/BMP) — no image library
    media-guard Block media types per chat ("no stickers here") — per-chat rules, auto-delete
    health-monitor Process health — memory, event-loop lag, custom probes, threshold alerts
    text-extras Read-more collapse, progress bars, human durations/sizes, chunking, markdown escape, fuzzy distance
    reminders "!remind 10m …" — parseDuration, timers, restart-safe persistence (overdue fire late)
    quota Daily usage limits per user with tiers — midnight reset, bonuses, exhausted events
    tiers Premium/VIP memberships with expiry — extend/stack, lifetime, expiry sweeper
    todo Shared task lists per chat — assignees, ☐/☑ render, mentions, persistence
    url-watcher Poll any URL, alert on content change — extract views, SHA-256 diffing
    bug-shield Detect crash/"bug" messages — mention bombs, zalgo, invisible floods, RTL spoofing; sanitizeText
    crash-guard Survive uncaught exceptions/rejections — owner alerts, counters, safeStringify
    connection-watchdog Catch silent half-open sockets — stale alerts fire once, re-armed by activity
    secure-logger Credential-redacting pino logger + redactSensitive() for any object
    call-log Call history + per-caller stats from the call event — outcomes, durations, persistence
    always-online Keep the green dot lit — presence refresher with live switching and failure counters
    status-watcher Receive-side stories — per-contact filters, media type, download hook
    button-extras quickButtons, sendConfirm (yes/no), sendMenuButtons one-liners
    random-tools Dice notation, coins, weighted picks, deterministic rate/"jodoh" meters
    jid-extras phone↔jid conversions, device-insensitive compare, pretty phone printing
    time-tools Relative times ('5m ago'), clocks, next-occurrence, overnight windows, greetings
    group-tools Pure groupMetadata helpers — admins, owner, stats, participant diffs, info card
    msg-tools messageTypeOf, quoted info, timestamps, one-line message previews
    kv-store Tiny JSON key-value DB — namespaces, counters, debounced atomic saves
    warmup Account warmup — ramp daily send volume on fresh numbers (20→50→…→unlimited)
    disconnect-classifier Close errors → category + recommended action (reconnect / re-pair / stop)
    group-op-guard Stay under WhatsApp's group-action ceilings (~3 adds & 2 creates per 10 min)
    giveaway Keyword-entry raffles — fair draws, multi-winner, deadlines, status cards
    attendance Daily roll-call ("absen") — check-in times, missing list, numbered render
    auction Timed bidding ("lelang") — min increments, anti-snipe extensions, outbid alerts
    text-poll Vote-by-number polls that work in every client — tallies, bars, tie handling
    tictactoe XO duels — challenge/accept, emoji boards, win/draw/forfeit
    rps Rock-paper-scissors ("suit") — hidden picks, batu/gunting/kertas aliases, bets
    word-games scrambleWord, math-problem generator, "sambung kata" word-chain engine
    rental Per-chat bot rentals ("sewa") — trials, expiry warnings, unrented-group filter
    message-counter Daily per-user activity — top chatters, 🥇 digests, day history
    command-lock Disable commands per chat/globally + maintenance mode with owner bypass
    status-tools Status auto-styling — random font/colors for text statuses, media/audio normalization (wired into status sends)
    newsletter-tools Paced batch channel ops — follow/unfollow/mute many with per-jid verdicts
    emoji-tools Detect/count/extract/strip emoji, themed randomEmoji
    array-tools chunk, unique-by, groupBy, sortBy, distinct sample, range
    validate-tools isUrl/isEmail, id/en parseBool, clamp, ensureArray, pickFields
    timing-tools debounce, throttle, lap stopwatch, measureTime
    task-queue Bounded-concurrency async jobs with retries — mass DM safe
    mask-tools maskPhone/maskEmail, boundary-aware censorText
    math-eval SAFE !calc parser (no eval) + terbilang (Indonesian spelling) + Roman numerals
    chat-settings Per-chat feature toggles with defaults — the !settings backbone, ✅/❌ cards
    voucher Generate & redeem codes (JAP-X7K2-9QMD) — max uses, expiry, once-per-user
    quiz Multi-question quiz sessions — running scores, skips, rankings
    socket-preflight validateSocketConfig — catch broken configs before cryptic 405s (auto-wired into makeWASocket)
    group-cache createGroupMetadataCache — TTL-LRU for cachedGroupMetadata with event-driven invalidation
    reputation +rep/-rep with per-giver cooldowns, leaderboards, rep cards
    invite-tracker Who invited whom — active-vs-total counts (join/leave loops don't pay), top-inviter boards
    level-rewards attachLevelRewards — auto payouts (balance/tier/custom) on level thresholds, once each
    marriage Propose/accept/divorce registry — strictly monogamous, anniversaries, couples list
    ai-groups ⚠️ EXPERIMENTAL: add/remove Meta AI in groups, createAiGroup — server-gated by Meta's per-account rollout (no flag = server rejects; nothing to verify manually)
    auto-reply Simple keyword/pattern-based auto-responder engine
    message-search Search cached/stored messages, peeling off ephemeral/view-once wrappers first
    message-retry-manager Handles WhatsApp's retry-receipt protocol for undecryptable messages
    scheduling Schedule messages/actions for later delivery
    business Business-profile & catalog helpers
    chat-control Pin, mute, archive, and mark-read/unread helpers
    chat-history-helpers Work with synced chat history payloads
    link-preview Generate link preview metadata for outgoing messages
    // anti-delete + anti-edit share one MessageStore
    import { MessageStore, createMessageStoreHandler, createAntiDeleteUpsertHandler, createAntiEditUpsertHandler } from '@japofc/baileys'

    const store = new MessageStore()
    sock.ev.on('messages.upsert', createMessageStoreHandler(store)) // register FIRST
    sock.ev.on('messages.upsert', createAntiDeleteUpsertHandler(store, (info) => {
    console.log('deleted:', info.originalMessage) // recovered content
    }))
    sock.ev.on('messages.upsert', createAntiEditUpsertHandler(store, (info) => {
    console.log(`edit #${info.editCount}: "${info.beforeText}" -> "${info.afterText}"`)
    info.history // every previous revision, oldest first
    }))
    // event trackers — reaction / read-receipt / presence
    import { createReactionTracker, createReceiptTracker, createPresenceTracker } from '@japofc/baileys'

    const reactions = createReactionTracker()
    reactions.bind(sock) // messages.reaction
    reactions.getSummary(msg.key) // { '👍': ['628…@s.whatsapp.net'], … }
    reactions.onReaction(({ user, emoji, removed }) => { /* live updates */ })

    const receipts = createReceiptTracker()
    receipts.bind(sock) // message-receipt.update + messages.update
    receipts.getReceipts(msg.key) // { delivered: [...], read: [...], played: [...] }
    receipts.isReadBy(msg.key, jid) // has THIS user read it?
    receipts.onRead(({ key, user }) => { /* fires once per reader */ })

    const presence = createPresenceTracker()
    presence.bind(sock) // presence.update
    await sock.presenceSubscribe(jid) // WA only streams presence for subscribed jids
    presence.isOnline(jid); presence.isTyping(jid); presence.get(jid)?.lastSeen
    presence.onChange(({ user, presence }) => { /* online/offline/typing transitions */ })
    // bot toolkit — serializer, call guard, group events, view-once, anti-link
    import {
    serializeMessage, createCallGuard, createGroupEventsTracker,
    createViewOnceCapture, createAntiLinkGuard
    } from '@japofc/baileys'

    sock.ev.on('messages.upsert', async ({ messages }) => {
    const m = serializeMessage(sock, messages[0])
    if (!m || m.fromMe) return
    if (m.body === 'ping') await m.reply('pong') // quotes the original
    if (m.isMedia) { const buf = await m.download() } // media as Buffer
    if (m.quoted) console.log('replying to:', m.quoted.body)
    })

    const calls = createCallGuard({ autoReject: true, rejectMessage: 'Bots cannot pick up calls.' })
    calls.bind(sock) // 'call' event
    calls.onRejected(call => console.log('rejected', call.from, call.isVideo ? '(video)' : ''))

    const groups = createGroupEventsTracker()
    groups.bind(sock) // group-participants.update + groups.update
    groups.onJoin(({ id, participants }) =>
    sock.sendMessage(id, { text: `Welcome ${participants.join(', ')}! 👋` }))
    groups.onLeave(({ participants }) => console.log('left:', participants))

    const vault = createViewOnceCapture()
    vault.bind(sock) // messages.upsert
    vault.onViewOnce(({ msg, unwrapped }) =>
    console.log('view-once', unwrapped.mediaType, 'from', msg.key.remoteJid))

    const antilink = createAntiLinkGuard({ autoDelete: true }) // invite links in groups
    antilink.bind(sock)
    antilink.onDetected(({ chat, sender }) =>
    sock.sendMessage(chat, { text: `@${sender.split('@')[0]} no group links here!`, mentions: [sender] }))
    // auto-read, AFK & tag-all
    import { createAutoRead, createAfkManager, sendMentionAll, sendHideTag } from '@japofc/baileys'

    const reader = createAutoRead({ denylist: ['boss@s.whatsapp.net'] })
    reader.bind(sock) // blue-ticks everything else as it arrives
    reader.pause(); reader.resume()

    const afk = createAfkManager()
    afk.bind(sock)
    afk.setAfk(sender, 'lunch break 🍜') // e.g. from an !afk command
    afk.onAfkMention(({ chat, afkUser, reason, msg }) =>
    sock.sendMessage(chat, { text: `@${afkUser.split('@')[0]} is AFK: ${reason}`, mentions: [afkUser] }, { quoted: msg }))
    afk.onReturn(({ user, missed }) => console.log(user, 'is back,', missed.length, 'pings while away'))

    await sendMentionAll(sock, groupJid, 'Meeting in 5 minutes!') // visible @everyone
    await sendHideTag(sock, groupJid, 'Silent announcement') // pings all, clean text

    // serializeMessage upgrades: m.isViewOnce, m.viewOnce, m.expiration, m.forward(jid), m.delete()
    // pairing, but comfortable — one call from socket to paired
    import { pairWithCode, getPairingCodeInfo } from '@japofc/baileys'

    const result = await pairWithCode(sock, '628123456789', {
    customCode: 'abcd-efgh', // optional — any format, normalized for you
    onCode: (code, formatted) => console.log('Enter on your phone:', formatted)
    })
    if (result.restartRequired) { /* recreate the socket — standard after pairing */ }

    const info = getPairingCodeInfo(state.creds)
    console.log(info.formatted, '— expires in', Math.round(info.remainingMs / 1000), 's')
    // requestPairingCode itself now also accepts "abcd-efgh" / "ABCD EFGH" custom codes
    // community & moderation pack
    import {
    createFloodGuard, createWordFilter, createWarnManager,
    createGatekeeper, createLevelSystem
    } from '@japofc/baileys'

    const flood = createFloodGuard({ maxMessages: 8, windowMs: 10_000 })
    flood.bind(sock)
    flood.onFlood(({ chat, user }) => warns.warn(user, { chat, reason: 'flooding' }))

    const filter = createWordFilter({ words: ['judol'], patterns: [/j\s*u\s*d\s*o\s*l/i], autoDelete: true })
    filter.bind(sock)
    filter.onMatch(({ chat, sender }) => warns.warn(sender, { chat, reason: 'banned word' }))

    const warns = createWarnManager({ threshold: 3 })
    warns.onThreshold(async ({ user, chat }) => {
    await sock.groupParticipantsUpdate(chat, [user], 'remove') // three strikes, out
    warns.reset(user, chat)
    })

    const gate = createGatekeeper()
    gate.banUser('pest@s.whatsapp.net', 'spam')
    sock.ev.on('messages.upsert', gate.filter(async ({ messages }) => { /* clean traffic only */ }))

    const levels = createLevelSystem()
    levels.bind(sock)
    levels.onLevelUp(({ user, chat, level }) =>
    sock.sendMessage(chat, { text: `🎉 @${user.split('@')[0]} reached level ${level}!`, mentions: [user] }))
    levels.getLeaderboard(10, chat) // top 10 in this group
    // sticker branding, economy, night mode & join captcha
    import {
    setStickerExif, readStickerExif, createEconomy,
    createGroupScheduler, createVerifier
    } from '@japofc/baileys'

    // pure JS — no node-webpmux, works on static AND animated webp
    const branded = setStickerExif(webpBuffer, { packName: 'My Pack', author: 'me', emojis: ['🔥'] })
    await sock.sendMessage(jid, { sticker: branded })
    readStickerExif(branded) // { 'sticker-pack-name': 'My Pack', … }

    const eco = createEconomy({ dailyAmount: [100, 200], streakBonus: 25, transferFee: 0.05 })
    eco.claimDaily(user) // { claimed, amount, streak } or { remainingMs }
    eco.transfer(userA, userB, 100)

    const nightMode = createGroupScheduler()
    nightMode.add({ group, action: 'close', at: '22:00' }) // announcement-only
    nightMode.add({ group, action: 'open', at: '06:00' }) // everyone can chat
    nightMode.start(sock)

    const verifier = createVerifier({ timeoutMs: 120_000 })
    verifier.bind(sock) // auto math-captcha for every new member
    verifier.onChallenge(({ chat, user, question }) =>
    sock.sendMessage(chat, { text: `👋 @${user.split('@')[0]} verify: ${question}`, mentions: [user] }))
    verifier.onFailed(({ chat, user }) => sock.groupParticipantsUpdate(chat, [user], 'remove'))

    // router upgrade — guards & categorized menu:
    router.command('kick', handler, { adminOnly: true, category: 'Admin' })
    router.command('shutdown', handler, { ownerOnly: true }) // owners: [...] in createRouter
    router.command('daily', handler, { cooldownMs: 60_000, category: 'Economy' })
    // analytics, anti-tagall, shop, group backup & menfess
    import {
    createCommandStats, createAntiTagAllGuard, createShop,
    backupGroup, diffGroupBackup, restoreGroupSettings, createMenfessRelay
    } from '@japofc/baileys'

    const stats = createCommandStats()
    router.use(stats.middleware()) // counts every executed command
    stats.getTopCommands(5); stats.getBusiestHours()

    const antiTag = createAntiTagAllGuard({ threshold: 5, autoDelete: true })
    antiTag.bind(sock)
    antiTag.onDetected(({ sender, hidden }) => console.log(sender, hidden ? 'hidetag!' : 'tag-all'))

    const shop = createShop(eco) // plugs into createEconomy()
    shop.addItem({ id: 'potion', name: 'Potion', price: 250, consumable: true })
    shop.buy(user, 'potion', 2); shop.useItem(user, 'potion')
    eco.bet(user, 100, { winChance: 0.5, multiplier: 2 }) // economy upgrade

    const backup = await backupGroup(sock, groupJid) // settings + members, JSON-safe
    const diff = await diffGroupBackup(sock, backup) // joined/left/promoted/changed
    await restoreGroupSettings(sock, backup) // subject, desc, locks

    const menfess = createMenfessRelay()
    menfess.bind(sock)
    await menfess.start(sock, sender, targetJid, 'first anonymous message')
    // both sides now chat through the bot as Anon-1 / Anon-2 until "stop"

    // more upgrades: verifier { challenge: 'emoji' }, level ranks (Newbie→Legend)
    // notes, birthdays, games, i18n, fancy menus & join requests
    import {
    createNotes, createBirthdayManager, createGuessGame,
    createI18n, styleText, createJoinRequestManager
    } from '@japofc/baileys'

    const notes = createNotes()
    notes.set(chat, 'rules', 'No spam. Be kind.') // !save rules …
    notes.get(chat, 'rules')?.content // !get rules

    const bdays = createBirthdayManager()
    bdays.set(user, { day: 17, month: 8, year: 2000, chat })
    bdays.onBirthday(({ user, age, chat }) =>
    sock.sendMessage(chat, { text: `🎂 HBD @${user.split('@')[0]} (${age})!`, mentions: [user] }))
    bdays.start()

    const game = createGuessGame({ timeoutMs: 60_000 })
    game.bind(sock)
    game.start(chat, { answer: 'Jakarta', hint: 'capital city', reward: 500 })
    game.onCorrect(({ user, reward }) => eco.add(user, reward, 'quiz win'))

    const i18n = createI18n({ defaultLang: 'en' })
    i18n.addLanguage('id', { greet: 'Halo {name}!' })
    i18n.setChatLang(chat, 'id') // !lang id
    i18n.tFor(chat, 'greet', { name: 'Budi' }) // 'Halo Budi!'

    styleText('Bot Menu', 'bold') // 𝗕𝗼𝘁 𝗠𝗲𝗻𝘂

    const joins = createJoinRequestManager({ denylist: [spammer] })
    joins.bind(sock) // live join-request events
    joins.onRequest(({ user, approve, reject }) => approve())
    await joins.sweep(sock, groupJid) // process the pending list

    // CLI upgrade: npx @japofc/baileys sticker in.webp out.webp --pack "My Pack" --author me
    // session & system tools
    import {
    analyzeAuthState, repairAuthFolder,
    exportAuthToString, importAuthFromString, migrateFolderToAuthState,
    backupAuthStateRotating, createShutdownManager
    } from '@japofc/baileys'

    const report = await analyzeAuthState('./auth') // session doctor
    report.registered; report.counts; report.corrupted // + issues list
    await repairAuthFolder('./auth') // quarantine corrupted files

    // ship the whole login as ONE string (SESSION_ID pattern) — keep it secret!
    const sessionString = await exportAuthToString('./auth')
    await importAuthFromString(sessionString, './auth') // on the new device

    // move a folder session into ANY adapter (SQLite/Redis/Mongo/single-file):
    const { state, saveCreds } = await useSQLiteAuthState('auth.db')
    await migrateFolderToAuthState('./auth', state, saveCreds)

    // timestamped encrypted backups that prune themselves:
    await backupAuthStateRotating('./auth', './backups', { password, keep: 5 })

    const shutdown = createShutdownManager({ sock, saveCreds })
    shutdown.register('close db', () => db.close())
    shutdown.attach() // SIGINT/SIGTERM → clean exit
    # CLI: session management without writing code
    npx @japofc/baileys session analyze ./auth
    npx @japofc/baileys session repair ./auth
    npx @japofc/baileys session export ./auth --out session.txt
    npx @japofc/baileys session import session.txt ./auth-new
    // wizards, webhooks, links, media tools & health
    import {
    createConversationFlow, createWebhookBridge, buildWaMeLink, parseWaLink,
    getImageDimensions, createMediaGuard, createHealthMonitor
    } from '@japofc/baileys'

    const flows = createConversationFlow()
    flows.define('order', [
    { id: 'item', prompt: 'What would you like?' },
    { id: 'qty', prompt: 'How many?', validate: t => /^\d+$/.test(t) || 'Numbers only!' }
    ])
    flows.bind(sock)
    await flows.start(sock, chat, sender, 'order') // e.g. from a !order command
    flows.onComplete(({ answers }) => console.log(answers.qty, 'x', answers.item))

    const bridge = createWebhookBridge('https://my.server/hook', { secret, retries: 2 })
    bridge.bind(sock) // messages → your backend/n8n

    buildWaMeLink('+62 812-3456-7890', 'Hello!') // https://wa.me/62812…?text=Hello%21
    parseWaLink('https://chat.whatsapp.com/AbC…') // { type: 'group-invite', code }

    getImageDimensions(buffer) // { format: 'png', width, height }

    const media = createMediaGuard({ blocked: ['sticker'], autoDelete: true })
    media.bind(sock) // "no stickers in this group"

    const health = createHealthMonitor({ thresholds: { heapUsedMb: 400, eventLoopLagMs: 200 } })
    health.onAlert(({ metric, value }) => sock.sendMessage(owner, { text: `⚠️ ${metric}: ${value}` }))
    health.start()
    // security & stability hardening
    import {
    createBugShield, sanitizeText, installCrashGuard,
    createConnectionWatchdog, createSecureLogger,
    exportAuthToString, checkAuthPermissions, hardenAuthFolder, autoReconnect
    } from '@japofc/baileys'

    const shield = createBugShield({ autoDelete: true }) // anti bug-message
    shield.bind(sock)
    shield.onDetected(({ sender, reasons }) => gate.banUser(sender, reasons.join(',')))
    sanitizeText(dirtyText) // strips RTLO/zalgo/invisible flood

    installCrashGuard({ // bot never dies silently
    onError: ({ type, error }) =>
    sock.sendMessage(owner, { text: `💥 ${type}: ${error?.message}` }).catch(() => {})
    })

    const watchdog = createConnectionWatchdog({ staleMs: 5 * 60_000 })
    watchdog.bind(sock)
    watchdog.onStale(() => sock.end(new Error('stale connection'))) // reconnect takes over
    watchdog.start()

    const logger = createSecureLogger() // creds NEVER hit the logs
    const sock2 = makeWASocket({ auth: state, logger })

    await exportAuthToString('./auth', { password }) // AES-encrypted JAPSESS2 export
    await hardenAuthFolder('./auth') // chmod 700/600 everything
    await checkAuthPermissions('./auth') // audit for leaks

    autoReconnect(factory, {
    onGiveUp: ({ reason, attempts }) => notifyOwner(reason) // new: give-up hook + getStatus()
    })
    // JAP AI cards from markdown, call tools, presence & button shortcuts
    import { AIRich, createCallLog, createCallGuard, createAlwaysOnline,
    createStatusWatcher, quickButtons, sendConfirm } from '@japofc/baileys'

    // whole AI card from one markdown string (headings/code/tables auto-detected)
    await AIRich.fromMarkdown('# Report\n\n```js\nconst x = 1\n```\n\n| A | B |\n|---|---|\n| 1 | 2 |', sock).send(jid)
    new AIRich(sock).addChecklist([{ text: 'done item', done: true }, 'open item'])
    .addKeyValue({ Name: 'JAP', Version: '2.4.5' })
    .addProgressBar('Download', 70, 100)

    const callLog = createCallLog() // who called, outcome, duration
    callLog.bind(sock)
    createCallGuard({ // quiet hours + hard blocks
    autoReject: true,
    schedule: { from: '22:00', to: '06:00' }, // reject only at night
    denylist: [spammer] // …except these: always
    }).bind(sock)

    createAlwaysOnline(sock).start() // green dot stays lit
    const statuses = createStatusWatcher() // save contacts' stories
    statuses.bind(sock)
    statuses.onStatus(({ download }) => download())

    await sendConfirm(sock, jid, 'Delete all data?') // confirm_yes/confirm_no
    await sendButtons(sock, jid, { text: 'Pick', buttons: quickButtons(['A', 'B']) })

    // the 50+ round — grab-bag of daily drivers:
    import { rollDice, matchScore, phoneToJid, prettyPhone, formatRelative,
    getGroupAdmins, diffParticipants, summarizeMessage, createKVStore } from '@japofc/baileys'

    rollDice('2d6+3') // { rolls: [4, 2], total: 9 }
    matchScore('budi', 'ani') // 0-100, deterministic "jodoh meter"
    phoneToJid('+62 812-3456-7890') // 6281234567890@s.whatsapp.net
    prettyPhone('6281234567890') // +62 812-3456-7890
    formatRelative(msg.timestamp) // '5m ago'
    getGroupAdmins(await sock.groupMetadata(jid))
    summarizeMessage(msg) // '📷 image: caption…' for logs

    const db = await createKVStore('./botdata.json') // tiny persistent DB
    db.namespace('settings').set(jid, { welcome: true })

    // anti-ban pack — fresh numbers, sane group ops, smart reconnects
    import { createAccountWarmup, classifyDisconnect, explainDisconnect,
    createGroupOpGuard, randomGaussian, createPresenceCycler } from '@japofc/baileys'

    const warmup = createAccountWarmup({ startedAt: firstLoginTs })
    if (warmup.trySend().allowed) await sock.sendMessage(jid, content)
    // day 1 → 20 msgs, then 50, 100, 200, 400, 800, unlimited

    sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
    if (connection !== 'close') return
    const verdict = classifyDisconnect(lastDisconnect)
    verdict.shouldReconnect ? restart() : console.log(explainDisconnect(lastDisconnect))
    // 🔑 [auth 401] Logged out from the phone — the session is gone, pair again.
    })

    const ops = createGroupOpGuard()
    const safe = ops.wrap(sock) // guarded automatically
    await safe.groupParticipantsUpdate(jid, users, 'add') // throws past ~3 adds/10min

    await sleep(randomGaussian(2000, 600, { clamp: [500, 5000] })) // human-like pauses
    createPresenceCycler(sock, { chats: [ownerJid] }).start() // opt-in activity

    // community events — giveaways, roll-calls, auctions, polls
    import { createGiveaway, createAttendance, createAuction, createTextPoll } from '@japofc/baileys'

    const giveaway = createGiveaway({ keyword: 'join' })
    giveaway.bind(sock)
    giveaway.start(chat, { prize: 'Voucher 50k', durationMs: 3600_000, winners: 2 })
    giveaway.onEnd(({ winners }) => announce(winners)) // fair, injectable RNG

    const absen = createAttendance()
    absen.bind(sock)
    absen.open(chat, { title: 'Absen Pagi 🌞' }) // members type "absen"
    await sock.sendMessage(chat, { text: absen.render(chat), mentions: absen.getMentions(chat) })

    const auction = createAuction()
    auction.start(chat, { item: 'Akun ML', startBid: 50_000, minIncrement: 5_000, antiSnipeMs: 30_000 })
    auction.bid(chat, user, 60_000) // → accepted / too-low / already-leading
    auction.onEnd(({ winner, amount }) => sold(winner, amount))

    const polls = createTextPoll()
    polls.bind(sock) // votes = plain "1".."9" replies
    polls.start(chat, { question: 'Mabar jam?', options: ['19:00', '20:00', '21:00'] })
    polls.onEnd(({ results }) => sock.sendMessage(chat, { text: polls.formatResults(results) }))

    // games pack — duels & word games
    import { createTicTacToe, createRPS, scrambleWord, generateMathProblem,
    createWordChain } from '@japofc/baileys'

    const ttt = createTicTacToe()
    ttt.challenge(chat, challenger, opponent); ttt.accept(chat, opponent)
    ttt.play(chat, user, 5) // squares 1-9 → next/win/draw
    await sock.sendMessage(chat, { text: ttt.render(chat) }) // ❌⭕3️⃣ emoji board

    const suit = createRPS()
    suit.challenge(chat, a, b, { bet: 5000 }); suit.accept(chat, b)
    suit.pick(chat, a, 'batu') // picks stay hidden
    suit.onResult(({ winner, loser, bet }) => eco.transfer(loser, winner, bet))

    game.start(chat, { answer: scrambleWord('bandung') }) // acak kata
    game.start(chat, generateMathProblem('hard')) // 17 × 8 - 24 = ?
    const chain = createWordChain() // sambung kata
    chain.start(chat, { firstWord: 'makan' })
    await chain.play(chat, user, 'nasi') // ✅ n… — scores by word length

    eco.rob(thief, victim) // upgrade: wallet heists — bank money stays safe

    // bot-business ops — rentals, activity, locks
    import { createRentalManager, createMessageCounter, createCommandLock } from '@japofc/baileys'

    const rental = createRentalManager()
    rental.add(groupJid, { days: 30 }); rental.startTrial(newGroup, { days: 3 })
    sock.ev.on('messages.upsert', rental.filter(handler)) // unrented groups ignored
    rental.onExpiring(({ chat }) => remind(chat)) // renewal reminder (24h before)
    rental.onExpire(({ chat }) => sock.groupLeave(chat))
    rental.startSweeper()

    const counter = createMessageCounter()
    counter.bind(sock)
    counter.renderDigest(chat) // 🥇 @user — 42 pesan (daily top chatters)

    const locks = createCommandLock({ owners: [ownerJid] })
    locks.lock(chat, 'slot'); locks.lockGlobal('rob')
    router.use(locks.middleware()) // blocked before handlers run
    locks.setMaintenance(true, { message: '🛠️ maintenance' }) // owners still pass

    // MEGA UPGRADE SWEEP (36 upgrades across the whole toolkit):
    // kv TTL + getOrSet · notes pinning 📌 · todo priorities 🔴🟡 · recurring
    // reminders · warns.renderList · eco.getEconomyStats + eco.work() jobs ·
    // shop.renderCatalog · tiers.extendAll (downtime compensation) ·
    // quota.renderStatus · level PRESTIGE ⭐ · guess.revealHint('j_k__t_') ·
    // ttt win/loss records + leaderboard · RPS best-of-3 series · giveaway
    // canJoin requirements · attendance streaks · auction buy-now price ·
    // poll renderLive bars · menfess.listActiveSessions · afk.renderAfkList ·
    // shield.getReasonStats · gate.banMany (raid cleanup) · warmup.skip ·
    // opGuard.waitAndAssert · watchdog autoRestart · health sparklines ▁▅▃█ ·
    // router hidden commands + ctx.quotedText · i18n.addLanguages ·
    // stats.renderTop · m.timestampMs · tag-all chunking for huge groups ·
    // isForwarded/getForwardInfo · rental.renderList
    //
    // HUNT/UPDATE ROUND (fork ecosystem fully swept — 0 gaps left):
    // WA version → live 2.3000.1048680055 · flow backWords ('kembali' rewinds) ·
    // url-watcher ETag/304 conditional polling · health probe timeouts ·
    // crash-guard alert throttling · webhook body caps · wa.me/message/CODE
    // business links · formatParticipantChanges(➕➖⬆️⬇️) · boxText('MENU') ╔═╗ ·
    // reminders.renderList

    // conversation-flow upgrade: choice steps
    flows.define('order', [{ id: 'size', prompt: 'Size:', choices: ['S', 'M', 'L'] }])
    // renders numbered options; answers accepted by text OR number
    // CLI: npx @japofc/baileys wa → package + baked WA Web version

    // +18 upgrades: titleCase/slugify/generateId, shop.updateItem, notes.exportText,
    // warns.getTop, stats.getTopChats, i18n.formatNumber/formatDate, levels.getRankPosition,
    // quota.getAllUsage, tiers.renderStatus, birthday.renderUpcoming, guess.startNumberGame,
    // flood.getTopFlooders, gate.listBans, health.setThreshold, watchdog.getReport

    // 10 more upgrades: temp bans gate.banUser(jid, r, { expiresInMs }),
    // flood autoMuteMs + isMuted, eco.applyInterest(0.01) bank interest,
    // shop item maxPerUser, warns.decay(30d), levels.setMultiplier(2) XP events,
    // quota.setLimit live, tiers.getExpiring(3d) renewal crons,
    // reminders.snooze(id, 10m), todos.setDue + getOverdue (⏰ in render)
    import { extractLIDPNPairs, GROUP_PARTICIPANTS_CHUNK_SIZE } from '@japofc/baileys'

    // Every LID↔PN pair a metadata fetch already carries — normalized + deduplicated.
    // The socket now stores these automatically on groupMetadata() /
    // groupFetchAllParticipating() / communityMetadata(), so getLIDForPN() and
    // getPNForLID() resolve group members without extra USync round-trips.
    const meta = await sock.groupMetadata(jid)
    extractLIDPNPairs(meta) // [{ lid: '999@lid', pn: '628…@s.whatsapp.net' }, …]
    extractLIDPNPairs(cachedMeta) // pure + null-safe — works on cached metadata too

    GROUP_PARTICIPANTS_CHUNK_SIZE // 25 — participant arrays are chunked at this size

    Also fixed in v2.4.7: group metadata no longer returns NaN for subjectTime / creation / descTime when WhatsApp omits them (they're undefined now, so they don't persist as null), and community metadata finally reports addressingMode and honours the server's size attribute — both previously diverged from the group parser on identical payloads.

    import { resolveNotificationActors, extractNotificationLIDPNPairs,
    parseNotificationParticipants } from '@japofc/baileys'

    // Who did it, and to whom — resolved as a UNIT. Before v2.4.7 the two halves fell
    // back independently, so a join request could report the requester's LID next to
    // the admin's phone number.
    const actors = resolveNotificationActors(notificationNode, actionChild)
    // { actingLid, actingPn, actingUsername, affectedLid, affectedPn, affectedIsActor }

    parseNotificationParticipants(actionChild) // messageStubParameters shape
    extractNotificationLIDPNPairs(node, actionChild) // [{ lid, pn }] — consistent pairs only

    // The socket now stores those pairs automatically on every group notification, and a
    // stale pair can be dropped after a change-number event:
    await sock.signalRepository.lidMapping.removeMapping('628…@s.whatsapp.net')
    import { parseMediaConnNode, isMediaConnExpired, MEDIA_CONN_DEFAULT_TTL,
    dedupeJidsByUser, dedupeDeviceList } from '@japofc/baileys'

    // The <media_conn> handshake, parsed safely. Before v2.4.7 a missing/non-numeric
    // `ttl` became NaN, and `now - fetchDate > NaN * 1000` is false forever — so the
    // upload connection was never refreshed again and kept using a stale auth token.
    const media = parseMediaConnNode(mediaConnNode)
    // { hosts, auth, ttl, fetchDate } — ttl is never NaN; falls back to 300s
    MEDIA_CONN_DEFAULT_TTL // 300

    isMediaConnExpired(media) // false while fresh
    isMediaConnExpired({ ttl: NaN }) // true — anything unusable counts as expired

    // Order-preserving dedup, as used by the USync device fan-out. Messaging your own
    // number used to enumerate (and encrypt to) every one of your devices twice.
    dedupeJidsByUser([{ jid: a }, { jid: a }, { jid: b }]) // → [a, b]
    dedupeDeviceList(devices) // keyed on user + device index; `u@s` === `u:0@s`
    import { extractUSyncErrors, hasUSyncQueryError, USyncQuery, USyncUser } from '@japofc/baileys'

    const result = await sock.executeUSyncQuery(
    new USyncQuery().withContactProtocol().withUsers(
    new USyncUser().withPhone('+628111'),
    new USyncUser().withPhone('+628222')
    )
    )

    // Before v2.4.7 a refused query ("rate overlimit") parsed into an empty list that looked
    // exactly like "nobody matched". The server's errors are now on the result.
    result.errors // [{ code: 479, text: 'rate overlimit' }]
    hasUSyncQueryError(result.errors) // true -> the whole query failed, don't trust `list`
    // false -> only individual users failed (entries carry `jid`)

    extractUSyncErrors(rawIqNode) // same extraction, standalone and null-safe

    // onWhatsApp() now also accepts @lid arguments for real (they used to be dropped
    // silently), always resolves to an array, and tells you which LID an entry came from:
    await sock.onWhatsApp('628111@s.whatsapp.net', '99887766@lid')
    // [{ jid: '628111@s.whatsapp.net', exists: true, lid: undefined }, …]
    import { makeOrderedDictionary } from '@japofc/baileys'
    import { upsertParticipants, removeParticipants, setParticipantsAdmin } from '@japofc/baileys'

    // Restoring a snapshot used to replace the array but not the id index, so every
    // message came back invisible to get() and re-upserting one appended a duplicate.
    const dict = makeOrderedDictionary(m => m.key.id)
    dict.fromJSON(JSON.parse(snapshot))
    dict.get('AAA') // the restored message, not undefined
    dict.update(newVersion) // true when it really updated (was always false)
    dict.has('AAA') // new in v2.4.7
    dict.size() // new in v2.4.7

    // Participant-list mutations: order-preserving, non-mutating, and matching a person
    // by `id` OR `phoneNumber` (the two halves of a LID<->PN pair are the same member).
    upsertParticipants(list, [{ id: '2@lid' }]) // merges, never duplicates
    setParticipantsAdmin(list, [{ id: '1@lid' }], null) // demote -> admin: null (not false)
    removeParticipants(list, [{ phoneNumber: '628…@s.whatsapp.net' }])
    import { ObjectRepository } from '@japofc/baileys'

    // toJSON() emits an array, but the constructor only understood a { id: entity } map, so
    // feeding a snapshot back in produced entries keyed '0', '1', … and lost every label.
    const labels = new ObjectRepository({ L1: { id: 'L1', name: 'Work' } })
    const restored = ObjectRepository.fromJSON(JSON.parse(JSON.stringify(labels)))
    restored.findById('L1') // { id: 'L1', name: 'Work' } — not undefined
    restored.hasId('L1') // new in v2.4.7
    restored.load([{ id: 'L2', name: 'Family' }])
    restored.clear()
    import { classifyMessage, isMissedCallMessage, MISSED_CALL_STUB_TYPES } from '@japofc/baileys'

    // Before v2.4.7 a missed call could never be a "real" message — the content check was
    // ANDed over the stub-type allowances too — so it never moved its chat up the list.
    classifyMessage(missedCallStub, meId)
    // { isReal: true, isStub: true, isMissedCall: true, isAboutMe: false,
    // incrementsUnread: false, contentType: undefined }

    classifyMessage(textMessage, meId).contentType // 'conversation'
    isMissedCallMessage(msg) // true for the 4 missed-call stubs
    MISSED_CALL_STUB_TYPES // the table itself
    import { parseStubParticipant, stubParticipantIdentities, stubParticipantsInclude } from '@japofc/baileys'

    // Stub parameters are ragged: a JSON pair for some stub types, a plain jid for others.
    parseStubParticipant('{"lid":"1@lid","pn":"2@s.whatsapp.net"}') // { lid, pn }
    parseStubParticipant('628111@s.whatsapp.net') // { phoneNumber }
    stubParticipantIdentities(p) // every jid this person can be addressed by

    // "Is one of these people me?" — checks lid AND pn, so a LID-addressed group matches.
    stubParticipantsInclude(msg.messageStubParameters, sock.user.id, sock.user.lid)

    Invite-link join requests now emit group.join-request as well (only the admin-add variant used to), so createJoinRequestManager() sees them.

    // the 20+ round: text tools, reminders, quotas, tiers, todos & more
    import {
    readMore, progressBar, formatDuration, chunkText, similarity,
    parseDuration, createReminderManager, createQuotaManager,
    createTierManager, createTodoList, createUrlWatcher, getZodiac
    } from '@japofc/baileys'

    readMore('Promo!', 'long details…') // collapses behind "Read more"
    progressBar(70, 100) // ███████░░░ 70%
    formatDuration(93_784_000) // 1d 2h 3m
    chunkText(longText, 4000) // split for WA limits

    const reminders = createReminderManager()
    reminders.add({ chat, user, text: 'angkat gorengan', inMs: parseDuration('10m') })
    reminders.onDue(({ chat, user, text }) =>
    sock.sendMessage(chat, { text: `⏰ @${user.split('@')[0]} ${text}`, mentions: [user] }))

    const tiers = createTierManager()
    tiers.setTier(user, 'premium', { days: 30 })
    const quota = createQuotaManager({ defaultLimit: 20, limits: { premium: 200 } })
    if (!quota.consume(sender, tiers.getTier(sender)?.name).allowed) return ctx.reply('Jatah habis!')

    const todos = createTodoList()
    todos.add(chat, 'bayar wifi', { assignee: member })
    await sock.sendMessage(chat, { text: todos.render(chat), mentions: todos.mentions(chat) })

    const watcher = createUrlWatcher('https://api.example.com/status.json', { intervalMs: 60_000 })
    watcher.onChange(({ body }) => sock.sendMessage(owner, { text: `🔔 changed: ${body.slice(0, 300)}` }))
    watcher.start()

    // upgrades: eco.deposit/withdraw + eco.getRank, levels.renderRankCard,
    // router { onUnknownCommand } + router.remove, i18n.tn plurals,
    // styleText 'negativeSquared'/'boldFraktur', getZodiac(17, 8) → 'Leo',
    // guess-game near-miss hints, formatHealthSnapshot for owner DMs

    | stickerpack | Build and send sticker packs (including animated/Lottie) | | templates | Legacy WhatsApp Business template message helpers | | vcard | Build vCard (contact card) payloads | | status | Post and manage WhatsApp Status updates | | event-buffer | Buffers/coalesces high-volume socket events for heavier bots | | doctor | checkEnvironment() / printEnvironmentReport() — one-call environment diagnostics |

    Every module above ships a matching .d.ts, so your editor will show full hover-docs regardless of which ones you import.

    ⬆ back to top

    Requirement Version
    Node.js 20+
    Module system ESM
    WhatsApp Latest Multi Device

    Full .d.ts: every shipped .js file has a matching TypeScript declaration (guarded by tests/types-parity.test.js), and the whole package verifies at zero errors under tsc --strict. import { ... } from '@japofc/baileys' resolves with no @types/ package needed (beyond the standard @types/node every Node TS project has): makeWASocket incl. the username methods and USERNAME_* constants, all Types/* definitions, WAProto, stores, Utils/*, the message builders (Button, Poll, Carousel, AIRich, A2UI, … with Bloks node types), the Bot Framework (Bot, Context, …), and the VoIP client (VoipClient, ActiveCall, …). Complex wire payloads are typed as loose records where WhatsApp publishes no schema.


    Connection keeps dropping / reconnect loop

    Check the lastDisconnect.error field on connection.update. If the status code is 401 (loggedOut), the session really is invalid and needs a fresh QR scan — do not auto-reconnect in that state. For other codes (428, 440, etc.), reconnecting with backoff is usually enough.

    QR not showing / not scanning

    Upstream Baileys removed printQRInTerminal, but in this package the option works again — the QR is drawn automatically by the built-in renderer (vendored qrcodegen, zero extra dependencies). You can also render manually from the qr event with renderQRToTerminal(qr) / qrToSVG(qr) / qrToPNG(qr) / qrToMatrix(qr). If the QR shows but linking fails, the WA Web version (version in makeWASocket) is usually stale; fetch the latest via fetchBestWaVersion() (chain: WA's sw.js → baileys forks → fallback). The Bot framework already does this automatically on every start()/reconnect (disable with versionCheck: false). QR looks "inverted" on a light terminal theme? Use renderQRToTerminal(qr, { inverted: true }).

    "Bad MAC" errors / messages fail to decrypt

    This usually happens when the auth state folder is corrupted or the session is used by more than one process at the same time. Make sure only one instance writes to a given auth state folder, and consider pruneStaleAuthFiles() to clean up stale sender keys periodically.

    Memory keeps growing on long-running bots

    With makeInMemoryStore(), the chat/message/contact caches grow without bound. For long-running bots, consider switching to one of the makePersistentStore() backends (SQLite/Redis/etc.) and use event-buffer to absorb event bursts under high traffic.

    Voice call fails to connect

    This feature is still experimental and needs the @roamhq/wrtc peer dependency — make sure it's installed and that its native bindings support your platform. Check the call.on('ended', reason => ...) event for details on why it failed.

    Media features failing / "ffmpeg not found"

    Run printEnvironmentReport() (see doctor) — it shows exactly what's missing. For ffmpeg: on servers npm i ffmpeg-static, on Termux pkg install ffmpeg — both auto-detected with zero config.

    Startup banner is in the way / want it off

    The banner is part of this package's identity and shows once per process. It's designed to stay out of your way: on an interactive terminal you get the full banner; in CI/pm2/piped logs it collapses to a single plain-text line (no ANSI codes, so JSON/structured log pipelines are never corrupted). NO_COLOR=1 removes the styling but not the banner.

    Since v2.4.3 the banner doubles as a startup health card — gradient wordmark plus a framed info panel showing the package version, the baked WA Web client version, node/platform/pid, and a rotating daily tagline:

           ██╗   █████╗   ██████╗
           ██║  ██╔══██╗  ██╔══██╗
           ██║  ███████║  ██████╔╝
      ██   ██║  ██╔══██║  ██╔═══╝
      ╚█████╔╝  ██║  ██║  ██║
       ╚════╝   ╚═╝  ╚═╝  ╚═╝
    
      ╭────────────────────────────────────────────────────────────────╮
      │ @japofc/baileys v2.4.5   ● typed · extended · battle-tested    │
      │ WA Web 2.3000.1048680055   node 20.20.2 · linux/x64 · pid 1472 │
      │ ⚡ anti-ban toolkit   🛡️ bug-shield   📚 github.com/JAPofc/baileys │
      ╰────────────────────────────────────────────────────────────────╯
       "from Termux to production."  — Made with 🍃 by J.AP
    

    Contributions are welcome — especially bug fixes, documentation, and enhancements to MessageBuilder / AIRich.

    1. Fork this repo and branch off main (feat/feature-name or fix/bug-name)
    2. Keep changes ESM-only and add/update the matching .d.ts declarations
    3. Test your change against at least one auth state path + one store backend before opening a PR
    4. Open a Pull Request with a short description: what changed and why

    For bug reports, include your Node.js version, reproduction steps, and the relevant lastDisconnect.error log snippet.


    Built on the shoulders of the open-source WhatsApp community.

    Full details and license terms per component: see NOTICE.md.


    J.AP avatar

    J.AP


    This project is an independent fork. Use responsibly and follow WhatsApp Terms of Service.


    The full, current release history lives in CHANGELOG.md. Older per-version summaries are archived below.

    📜 Show archived patch notes (v2.4.3 → V4)

    Summary — full details in CHANGELOG.md.

    • Community events: createGiveaway (fair raffles), createAttendance (absen + streaks), createAuction (anti-snipe + buy-now), createTextPoll (vote-by-number, works in every client).
    • Games: createTicTacToe (records + leaderboard), createRPS (suit, hidden picks, best-of-N), createWordChain (sambung kata), scrambleWord, generateMathProblem.
    • Bot business: createRentalManager (sewa per chat, one-trial-ever, unrented-group filter), createMessageCounter (daily digests 🥇), createCommandLock (+maintenance mode with owner bypass).
    • Fork ecosystem fully swept (upstream rc14 + 8 living forks downloaded & API-diffed — zero gaps left): took status-tools (auto-styled text statuses, wired into status sends) and newsletter-tools (paced batch follow/unfollow/mute).
    • AI groups (experimental, server-gated): addAiBotToGroup / createAiGroup — Meta AI in groups; acceptance depends on Meta's per-account rollout, rejections surface as status codes.
    • 36-upgrade sweep across the whole toolkit (kv TTL, notes pinning, recurring reminders, economy work()/rob()/interest, level PRESTIGE ⭐, revealHint, temp bans, waitAndAssert, watchdog autoRestart, sparklines, ctx.quotedText, ETag polling, alert throttling, boxText, and more) + 9 follow-ups.
    • Startup banner v2: framed health card (package + WA Web version, node/platform/pid, daily tagline).
    • WA Web version → live 2.3000.1048680055; CLI gained wa and sticker insights.
    • Anti-ban pack: createAccountWarmup (20→50→…→unlimited daily ramps), classifyDisconnect/explainDisconnect, createGroupOpGuard (~3 adds/2 creates per 10 min + wrap(sock)), Gaussian jitter, opt-in createPresenceCycler.
    • Security & stability: createBugShield (mention bombs/zalgo/RTLO + sanitizeText), createSecureLogger + redactSensitive, encrypted JAPSESS2 session strings, hardenAuthFolder, installCrashGuard + safeStringify, createConnectionWatchdog.
    • Session & DB tools: analyzeAuthState (session doctor), repairAuthFolder, portable session export/import strings, migrateFolderToAuthState, backupAuthStateRotating, createShutdownManager, CLI session command.
    • Moderation & community: flood/word-filter/warns/gatekeeper/levels/quota/tiers/economy+shop/command-stats/anti-tagall/verifier/join-requests/group-backup/menfess.
    • Bot toolkit: serializeMessage, call guard + call log, group events, view-once toolkit, anti-link, anti-delete/edit, reaction/receipt/presence trackers, auto-read, AFK, tag-all/hidetag, pairing tools (pairWithCode), router guards, conversation flows, webhook bridge, health monitor, i18n, fancy-text, 140+ utility modules in total.
    • Pure-JS sticker EXIF — node-webpmux dependency removed; sticker branding works on Termux; CLI sticker command.

    Summary — full details in CHANGELOG.md.

    • Built-in QR, zero dependencies: vendored qrcodegen (Nayuki, MIT) + our own renderer — renderQRToTerminal (half-block ▀▄█, half the height of classic renderers), qrToSVG, qrToPNG (hand-rolled PNG encoder on top of Node's zlib), qrToMatrix, formatPairingCode. Round-trip verified against an independent decoder (jsQR) in CI.
    • printQRInTerminal works again — not a deprecation warning: the QR is drawn automatically in the terminal on every connection.update.
    • Fixed the VoIP VoipStatsTracker is not a constructor spam: the Metro shim in worker-bootstrap.js shifted module/exports by one position (5th argument null). Fixed for both FB Comet bundle export conventions (flag-66 → arg 6, flag-98 → arg 7, 124/204 modules) + an ?.exports fallback in the loader. Verified the real worker boots all the way to worker_ready with no shim errors.
    • Auto WA-version in the Bot framework: every start()/reconnect resolves a fresh version via fetchBestWaVersion() (WA's sw.js → forks → fallback), preventing 405 pairing failures caused by stale versions. Opt-out: versionCheck: false or pin socketConfig.version.
    • Genuinely full .d.ts: every shipped .js has a matching declaration file, enforced by a parity test + a tsc --strict harness in CI.
    • CI + release automation: GitHub Actions (tests on Node 20/22, type-check, pack sanity) and a provenance-attested publish workflow triggered by v* tags.

    • Fixed a package.json typo: the version field was mistakenly left at 1.0.1 instead of 2.0.1 after the V5 release — corrected to 2.0.1 so the published package version matches the intended release.
    • Expanded README documentation, especially around lib/Builders/:
      • Added the new 📁 Builders Folder Map section — a per-file breakdown of everything under lib/Builders/ (shared.js, Button.js, ButtonV2.js, ButtonV3.js, Carousel.js, Poll.js, AIRich.js, A2UI.js, JapBaileys.js, index.js), what each file exports, and what it's for — previously A2UI and JapBaileys in particular had no usage documentation at all.
      • Added a usage example for the JapBaileys unified builder hub.
    • Redesigned postinstall-banner.js: cleaner box layout with a divider separating the title block from a small info section (Node version + docs link), a 256-color palette instead of basic ANSI colors, and a plain-text fallback when stdout isn't a TTY or NO_COLOR is set (CI logs, piped output) instead of forcing a box that may render misaligned.

    • Added Username Management: checkUsername, checkUsernameMulti, setUsername, deleteUsername, getMyUsername, setUsernamePin, findUserByUsername, fetchContactUsernames, getUsernameRecommendations, layered onto the existing USyncUsernameProtocol support.
    • Added the Bot Framework (Bot, Context, SessionManager, StatsManager, MediaManager, SQLiteStore). Adapted on the way in:
      • SQLiteStore/StatsManager construction moved behind an async .create() factory so better-sqlite3 stays a lazily-loaded optional peer dep instead of a hard top-level import that would crash the Framework module for anyone without it installed.
      • MediaManager's sticker/voice-note conversion now reuses this fork's existing lazy fluent-ffmpeg loader (see Utils/MessageBuilder.js) instead of adding ffmpeg-static + a second ffmpeg dependency; sticker EXIF metadata is written by the built-in pure-JS muxer (sticker-exif) — no extra package needed.
      • Bot's default logger now falls back to this fork's own pino instance instead of a silent no-op stub.
    • No .d.ts files were written for the new Framework module yet — see the note in that section.
    • Bumped to 1.0.1.

    • Kept the existing J.AP custom MessageBuilder classes and AIRich implementation intact.
    • Added whatsapp-rust-bridge@0.5.5 as a runtime dependency. The library already dynamically imports this module for LT Hash/app-state and crypto helpers; declaring it prevents accidental missing-module fallbacks in normal installations.
    • Existing guarded fallbacks for platforms where the native bridge cannot load remain in place.
    • ESM-only package metadata is preserved; no CommonJS build is included.
    ⬆ back to top

    Made with 🍃 by JAP

    Thanks for visiting, bye 👋

    ⬆️ Back to top