Skip to content

feat: support external browser + page in puppeteer options#201676

Closed
Adi1231234 wants to merge 33 commits intowwebjs:mainfrom
Adi1231234:feat/external-browser-page
Closed

feat: support external browser + page in puppeteer options#201676
Adi1231234 wants to merge 33 commits intowwebjs:mainfrom
Adi1231234:feat/external-browser-page

Conversation

@Adi1231234
Copy link
Contributor

Adds support for passing a pre-existing browser and page via options.puppeteer, so the Client can work with a browser you already control (e.g. Electron's built-in Chromium, or a shared browser pool).

When both browser and page are provided, the Client skips puppeteer.launch() / puppeteer.connect() and uses them directly. Existing behavior is unchanged when these options are not set.

What changed

initialize() - new if branch before the existing browserWSEndpoint check:

if (puppeteerOpts.browser && puppeteerOpts.page) {
    browser = puppeteerOpts.browser;
    page = puppeteerOpts.page;
}

destroy() / logout() - use browser.process() to decide cleanup:

  • process() !== null (we launched it) -> browser.close() as before
  • process() === null (external browser) -> browser.disconnect() only

Without this, browser.close() sends CDP Browser.close which kills the entire external browser, not just this client's session. disconnect() only closes the WebSocket, leaving the browser running.

This is the same pattern puppeteer uses internally in Symbol.dispose.

Example

const browser = await puppeteer.connect({ browserWSEndpoint: '...' });
const page = /* your page */;

const client = new Client({
    puppeteer: { browser, page }
});
await client.initialize();

Adi1231234 and others added 30 commits March 10, 2026 03:45
getcontact() and getChatModel() overwrite .id directly on live Backbone
model references from WhatsApp Web's Contact/Participant store. This
permanently corrupts the store for the lifetime of the session, causing
all subsequent lookups for the same contact to crash with:
- "Cannot read properties of undefined (reading '_serialized')"
- "Data passed to getter must include an id property"

the fix resolves LID to phone on the serialized copy only, never
touching the live store model.

fixes wwebjs#127054
- requestPairingCode() now exposes onCodeReceivedEvent if needed,
  so it works on clients initialized in QR mode
- Add cancelPairingCode() method to stop pairing and return to QR
- Update TypeScript definitions
1. Add _injectInProgress concurrency guard
2. Replace polling loops with waitForFunction
3. Deduplicate Backbone listeners via tuple array with cleanup
4. Atomic hasSynced check after listener registration
5. isMainFrame guard and storeAvailable SPA skip in framenavigated

co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- try/finally around inject() to always reset _injectInProgress
- Atomic hasSynced check inside listener registration evaluate()
- Fix framenavigated: capture isLogout before async, skip re-inject
  only when not logout AND store available
- try/catch around obj.off() in listener cleanup
- Null guard in QR ref change handler
- Reset qrRetries on LOADING_SCREEN event
- Add exposeFunctionIfAbsent in requestPairingCode()

co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
extract named onRefChange handler so it can be removed via
socket.on('change:hasSynced') once authentication succeeds,
matching PR #41's cleanup behavior.

co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add automatic recovery for ciphertext messages using WhatsApp's built-in
PLACEHOLDER_MESSAGE_RESEND (PDO type 4). Override the AB test gate and
send recovery requests after a 5s grace period.

Also adds message_ciphertext_failed event at 15s and fixes incorrect
JSDoc on onAddMessageCiphertextEvent.
businessprofile.find() checks the local cache first and only fetches
from the server when the profile is missing, while fetchBizProfile()
always makes a network call (~85-100ms per contact). This significantly
reduces latency when getContact is called repeatedly for the same
business contacts.

closes wwebjs#201656

co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…o stable

Resolved conflict with SPA reinject: kept waitForFunction flow,
manually added window.getQR assignment, exposeFunctionIfAbsent
in requestPairingCode(), and cancelPairingCode() method.
Port all diagnostic logging from the fork to the new upstream-based codebase:

- DiagCommon.js: shared filter helpers (shouldSkipMsg, shouldSkipReceipt, etc.)
- DiagHooks.js: 26 browser-side hooks for media download, signal/crypto,
  receipts, E2E identity, session lifecycle, history sync, and more
  (migrated from deleted Store.js with window.Store -> window.require() adaptation)
- Client.js: inject/auth flow tracing, CDP context monitoring, evaluate
  concurrency tracking, onDiagLog bridge, change:type diagnostics,
  silent-loss fallback (ADD_BYPASS_RECOVERED), ciphertext revoked guard,
  getContactById error logging
- Message.js: downloadMedia crypto field snapshots, error analysis,
  media type without directPath warning
- Utils.js: getMessageModel serialize try/catch, getContact timing/error
- Puppeteer.js: exposeFunctionIfAbsent logging
- Add 6 missing QPL mock methods (addAnnotation, start, end, cancel,
  success, fail) to prevent runtime crashes if WA calls them
- Restore durationMs timing in the "Store ready" diagnostic log

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- WAWebMsgDeleteCollection -> WAWebRevokeMsgAction (sendRevoke)
- WAWebUploadPreKeysJob replaces WAWebPreKeyUtils (uploadPreKeys)
- WAWebHandleHistorySyncChunk replaces WAWebHistorySyncJobUtils
- WAWebSocketBridgeApi replaces WAWebSocketConnectModel (socket close)
- WAWebCryptoDecryptMedia: handle module-is-function (no .default)
- WAWebMsgModel: handle .Msg vs .default for isPlaceholder
- All replacements include fallback to old module names
- Silence expected HOOK_FAIL logs for removed modules
- Fix L7 listener count to use _events fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
businessprofile.find() never throws for non-business contacts - it returns
a BusinessProfile model with profileOptions: null, not an error. The comment
was factually incorrect. The existing profileOptions guard handles the
non-business case correctly without any exception handling.

verified in WhatsApp Web DevTools: find() returns a valid object for regular,
business, LID, and even non-existent contacts. Only throws when called without
an id, which cannot happen here.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
keep logging/timing instrumentation while adopting the removal of the
unnecessary try/catch around BusinessProfile.find().

co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add logs for disconnect-on-connect diagnosis

add console.logs to the critical paths that can cause immediate disconnect
after connection:

- attachEventListeners: start/end markers
- onAppStateChangedEvent: log every state change, ACCEPTED_STATES check,
  and when DISCONNECTED is about to be emitted with the triggering state
- attachListeners change:state browser listener: log state + hasSynced
  when fired (distinct from inject() listener to tell them apart)
- inject cleanup: log how many previous listeners were removed on re-inject

co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: add more logs for disconnect-on-connect diagnosis

- inject: log when skipped due to _injectInProgress guard
- inject: log authStrategy.onAuthenticationNeeded() result (failed/restart)
- onQRChangedEvent: log qrRetries count and when max retries disconnect fires
- onAuthAppStateChangedEvent: add hasSynced to log + log refreshQR trigger
- onAppStateChangedEvent (DISCONNECTED path): query browser for live
  socketState/hasSynced/storeInjected at the moment of disconnect
- destroy: log with stack trace to know who called it
- onAppStateHasSyncedEvent: log before/after attachEventListeners call

co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove window.require from Node.js callback

exposefunctionifabsent callbacks run in Node.js where window is not
defined. Removed the hasSynced field from onAuthAppStateChangedEvent
log - the browser-side change:state listener already captures it.

co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove JSON.stringify from Node.js console logs

* chore: convert console.error to console.warn in diag logs

* fix: wrap refreshQR in pupPage.evaluate to run in browser context

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
whatsapp's model fires change:type with (model, newType, oldType) — not
the standard Backbone (model, newType, options). The oldType is passed
as the third argument (args[2]), not stored in previousAttributes which
does not exist in WhatsApp's custom model system.

using msg?.previousAttributes?.type always returned undefined, causing
parsed.prevType to be absent from the diag event and the
pendingreceipts cleanup path in listeners.ts to never fire.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(downloadMedia): messageSecret fallback for @lid accounts

for @lid accounts, mediaKey arrives as empty string while messageSecret
contains the actual 32-byte AES-256 key. Detect and convert to base64.

co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(downloadMedia): add messageSecretType to error log

co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
adds detailed logging on both browser and Node sides to verify
that protocolMessageKey reliably provides K1 (original message ID)
during revoke events in production.

co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix(revoke): use protocolMessageKey + verification logs
replace the O(n²) LID participant loop with WAWebLidMigrationUtils.toPn()
which resolves LID WIDs via LidPnCache in O(1). Remove the findContact
wrapper since Contact.find() accepts strings directly. Use
wawebwidfactory.createWidFromWidLike() for safe isLid() calls that handle
plain-object contact ids without WID prototype methods.

co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
same fix as fix/ciphertext-batch-pdo but with diagnostic logging:

- CIPHERTEXT_BUFFERED: each ciphertext message as it enters the buffer
  (includes pendingCount to see batch buildup, flushScheduled flag)
- CIPHERTEXT_BATCH_FLUSH: when the buffer flushes (includes count, first 5 IDs)
- CIPHERTEXT_FAIL_SKIPPED: when failTimer fires but msg already resolved
- CIPHERTEXT_DEFERRED_RESOLVED: now includes removedFromBuffer flag

co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…logs

fix: batch ciphertext PDO requests + diagnostic logs
wa's own source guards this behind isBusiness || isEnterprise
(WAWebUseBusinessProfile.react). No point querying business profiles
for regular contacts - saves a wasted IQ roundtrip (~80ms) per call.
…n-with-fixes-and-logs

refactor: simplify contact/chat model utils via native WA functions
allow passing a pre-existing `browser` and `page` via
`options.puppeteer` so the Client can work with a browser
you already control.

when both are provided, the Client skips puppeteer.launch()
and uses them directly.

also uses browser.process() in destroy/logout to decide
cleanup: close() for launched browsers, disconnect() for
connected ones.
@github-actions github-actions bot added api changes API modifications typings Type definitions utility Utility code labels Mar 20, 2026
@Adi1231234 Adi1231234 closed this Mar 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api changes API modifications typings Type definitions utility Utility code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants