'use strict';
const crypto = require('crypto');
const { WhatsAppSessionCompatibilityError } = require('./SessionErrors');
const { exposeFunctionIfAbsent } = require('../util/Puppeteer');
const {
CANONICAL_PRIVATE_MODULE_ABI,
DEFAULT_MAX_CANONICAL_BYTES,
DEFAULT_MAX_CANONICAL_RECORDS,
canonicalBrowserProjectionToStore,
canonicalStoreProjectionToBrowser,
exportCanonicalSessionProjection,
importCanonicalSessionProjection,
normalizeCanonicalProjection,
} = require('./CanonicalSessionBridge');
const BRIDGE_SCHEMA_VERSION = 2;
const DEFAULT_MAX_PROJECTION_BYTES = 64 * 1024 * 1024;
const DEFAULT_MAX_PROJECTION_RECORDS = 20000;
const CANONICAL_IMPORT_CONTROL_RPC_TIMEOUT_MS = 10000;
const CANONICAL_PQ_PAIR_VALIDATION_DOMAIN =
'underchat-wwebjs-pq-pair-validation-v1';
const CANONICAL_PQ_PAIR_VALIDATION_PROOF_VERSION = 1;
const CANONICAL_APP_STATE_RESTORE_TRANSPORT_GRACE_MS = 5000;
let canonicalPqKemPromise;
const loadCanonicalPqKem = async () => {
canonicalPqKemPromise ||= import('@noble/post-quantum/ml-kem.js').then(
({ ml_kem1024 }) => {
if (
typeof ml_kem1024?.encapsulate !== 'function' ||
typeof ml_kem1024?.decapsulate !== 'function'
) {
throw new Error('canonical_pq_kem_export_missing');
}
return ml_kem1024;
},
);
return canonicalPqKemPromise;
};
const updateCanonicalPqCommitment = (hash, value) => {
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(String(value));
const length = Buffer.allocUnsafe(4);
length.writeUInt32BE(bytes.length);
hash.update(length);
hash.update(bytes);
length.fill(0);
};
/**
* Prove every imported ML-KEM-1024 pair outside Chromium. WWeb's private KEM
* API performs its nominally async work synchronously on the renderer thread,
* so a normal 101-key Baileys inventory otherwise needs roughly four minutes
* and starves the page-task observer. Noble performs the same FIPS 203
* encapsulation/decapsulation proof in the Node realm in a few milliseconds.
* The returned digest binds that proof to the exact byte material consumed by
* the page; the page recomputes it before it is allowed to skip its fallback.
*/
const validateCanonicalPqKeyPairs = async (
normalizedProjection,
{ kem: providedKem, onProgress } = {},
) => {
const rows = normalizedProjection?.tables?.pq_pre_keys;
if (!Array.isArray(rows)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_projection_incompatible',
);
}
if (rows.length === 0) return undefined;
let kem;
try {
kem = providedKem || (await loadCanonicalPqKem());
} catch (error) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_pq_validation_backend_unavailable',
undefined,
{ cause: error },
);
}
const commitment = crypto
.createHash('sha256')
.update(CANONICAL_PQ_PAIR_VALIDATION_DOMAIN);
for (let index = 0; index < rows.length; index += 1) {
const row = rows[index];
const metadata = Buffer.from(
JSON.stringify([
row.key_id,
row.key_kind,
row.timestamp_ms,
row.sent_to_server,
]),
);
const publicKey = Buffer.from(row.public_key, 'base64');
const privateKey = Buffer.from(row.private_key, 'base64');
const signature = Buffer.from(row.signature, 'base64');
let ciphertext;
let encapsulatedSecret;
let decapsulatedSecret;
try {
updateCanonicalPqCommitment(commitment, metadata);
updateCanonicalPqCommitment(commitment, publicKey);
updateCanonicalPqCommitment(commitment, privateKey);
updateCanonicalPqCommitment(commitment, signature);
const encapsulated = kem.encapsulate(publicKey);
ciphertext = encapsulated?.cipherText;
encapsulatedSecret = encapsulated?.sharedSecret;
if (
!(ciphertext instanceof Uint8Array) ||
ciphertext.byteLength !== 1568 ||
!(encapsulatedSecret instanceof Uint8Array) ||
encapsulatedSecret.byteLength !== 32
) {
throw new Error('canonical_pq_encapsulation_invalid');
}
decapsulatedSecret = kem.decapsulate(ciphertext, privateKey);
if (
!(decapsulatedSecret instanceof Uint8Array) ||
decapsulatedSecret.byteLength !== 32 ||
!crypto.timingSafeEqual(
Buffer.from(
encapsulatedSecret.buffer,
encapsulatedSecret.byteOffset,
encapsulatedSecret.byteLength,
),
Buffer.from(
decapsulatedSecret.buffer,
decapsulatedSecret.byteOffset,
decapsulatedSecret.byteLength,
),
)
) {
throw new Error('canonical_pq_decapsulation_invalid');
}
} catch (error) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_pq_pre_key_pair_invalid',
{ key_index: index },
{ cause: error },
);
} finally {
metadata.fill(0);
publicKey.fill(0);
privateKey.fill(0);
signature.fill(0);
ciphertext?.fill?.(0);
encapsulatedSecret?.fill?.(0);
decapsulatedSecret?.fill?.(0);
}
await onProgress?.(index + 1, rows.length);
}
return Object.freeze({
version: CANONICAL_PQ_PAIR_VALIDATION_PROOF_VERSION,
algorithm: 'ML-KEM-1024',
key_count: rows.length,
commitment: commitment.digest('hex'),
});
};
const isTransientNavigationError = (error) =>
/Execution context was destroyed|Execution context is not available in detached frame|Cannot find context with specified id|Attempted to use detached Frame|Navigating frame was detached|frame was detached|net::ERR_ABORTED/iu.test(
String(error?.message || ''),
);
const safeDiagnosticCode = (value, fallback) =>
typeof value === 'string' && /^[a-z0-9_.-]{1,120}$/iu.test(value)
? value
: fallback;
const durableCanonicalImportFailureCode = (stage, cause) => {
const normalizedStage =
typeof stage === 'string' && /^[a-z0-9_]{1,48}$/u.test(stage)
? stage
: undefined;
const normalizedCause =
typeof cause === 'string' &&
/^wwebjs_canonical_[a-z0-9_]{1,80}$/u.test(cause)
? cause
: undefined;
if (!normalizedStage || !normalizedCause) {
return 'wwebjs_canonical_import_task_failed';
}
const causeSuffix = normalizedCause.slice('wwebjs_canonical_'.length);
const code = `wwebjs_ci.${normalizedStage}.${causeSuffix}`;
return code.length <= 100 ? code : 'wwebjs_canonical_import_task_failed';
};
const CANONICAL_CAPABILITY_STAGES = new Set([
'initial_runtime',
'legacy_reload',
'post_import',
'post_reload',
'pre_import',
'registration_probe',
'runtime',
'secure_import_browser_bootstrap',
]);
// The official sync state machine persists a collection row containing only
// its dirty/retry state before the server snapshot is committed. During that
// bounded interval the row intentionally has no version/LT hash. It is not an
// acceptable checkpoint, but it is a valid catch-up-in-progress observation.
const CANONICAL_APP_STATE_TRANSIENT_BLOCKERS = new Set([
'app_state_versions.row_incompatible',
]);
// A LocalAuth profile can cross a short WA Web rehydration window after the
// first authenticated export: the durable Signal identity remains readable,
// while browser-owned user preferences, routing information and app-state
// stores are temporarily unavailable. Only the legacy-volume bootstrap opts
// into retrying this exact set. The stability gate still discards the prior
// sample and requires two later complete, equivalent projections.
const CANONICAL_LEGACY_PROFILE_TRANSIENT_BLOCKERS = new Set([
'app_state_sync_keys.read_failed',
'app_state_versions.read_failed',
'app_state_mutation_macs.read_failed',
'device.lid_migration_state_missing',
'device.noise_info_read_failed',
'device.noise_private_missing',
'device.noise_public_missing',
'device.noise_recovery_token_missing',
'device.platform_missing',
'transport.routing_info_missing',
]);
const CANONICAL_APP_STATE_OVERLAY_BLOCKERS = new Set([
'app_state_sync_keys.read_failed',
'app_state_sync_keys.row_incompatible',
'app_state_sync_keys.key_epoch_incompatible',
'app_state_versions.read_failed',
'app_state_versions.row_incompatible',
'app_state_mutation_macs.read_failed',
'app_state_mutation_macs.row_incompatible',
]);
const CANONICAL_APP_STATE_TRANSIENT_STALL_MS = 15000;
const CANONICAL_APP_STATE_OFFICIAL_SYNC_NO_PROGRESS_MS = 30000;
// Private browser RPCs are expected to settle in a few seconds. Keep an
// individual stalled renderer request well inside the wider 120-second
// materialization gate so RemoteAuth can terminate the abandoned realm and
// let its fenced lifecycle retry while the outer worker watchdog still owns a
// meaningful cleanup margin.
const CANONICAL_INITIAL_APP_STATE_BROWSER_OPERATION_TIMEOUT_MS = 30000;
// A freshly paired browser can keep committing Signal/app-state material for
// tens of seconds after `hasSynced` first flips. Starting a full IndexedDB
// projection while those critical stores are still moving can leave one of
// WhatsApp Web's private table promises permanently pending. Require a short
// host-observed quiet window before each initial export; the wider bounded
// stability gate still owns the final deadline.
const CANONICAL_INITIAL_APP_STATE_JOURNAL_QUIET_MS = 5000;
// Provider-neutral snapshot-resync ABI. Keep this order stable across
// WWebJS, Baileys and WhatsMeow; it is deliberately lexical so it does not
// depend on any provider's private enum order.
const CANONICAL_APP_STATE_COLLECTIONS = Object.freeze([
'critical_block',
'critical_unblock_low',
'regular',
'regular_high',
'regular_low',
]);
const CANONICAL_APP_STATE_COLLECTION_SET = new Set(
CANONICAL_APP_STATE_COLLECTIONS,
);
// Carries the byte-bearing reconciliation plan only inside this module. The
// public comparison object remains safe to spread into debug logs because a
// symbol property is neither enumerable nor JSON serializable.
const CANONICAL_APP_STATE_RECONCILIATION_PLAN = Symbol(
'canonical-app-state-reconciliation-plan',
);
const CANONICAL_APP_STATE_SYNC_KEY_RECONCILIATION_PLAN = Symbol(
'canonical-app-state-sync-key-reconciliation-plan',
);
const opaqueCollectionId = (name) =>
crypto.createHash('sha256').update(String(name)).digest('hex').slice(0, 16);
const projectionBytes = (value) => {
if (Buffer.isBuffer(value)) return value;
if (value instanceof Uint8Array) {
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
}
return undefined;
};
const canonicalAppStateCollectionMaterializationSignatures = (projection) => {
const canonicalValue = (value) => {
const bytes = projectionBytes(value);
if (bytes) return { bytes: bytes.toString('base64') };
if (Array.isArray(value)) return value.map(canonicalValue);
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.keys(value)
.sort()
.map((key) => [key, canonicalValue(value[key])]),
);
}
return value;
};
const canonicalRows = (tableName, collectionName) => {
const rows = projection?.tables?.[tableName];
if (!Array.isArray(rows)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_projection_invalid',
);
}
return rows
.filter((row) => row?.name === collectionName)
.map((row) => JSON.stringify(canonicalValue(row)))
.sort();
};
const signatures = {};
for (const collectionName of CANONICAL_APP_STATE_COLLECTIONS) {
signatures[collectionName] = crypto
.createHash('sha256')
.update(
JSON.stringify({
versions: canonicalRows(
'whatsapp_app_state_version',
collectionName,
),
mutation_macs: canonicalRows(
'whatsapp_app_state_mutation_macs',
collectionName,
),
}),
)
.digest('hex');
}
return signatures;
};
const isStableInitialSnapshotResyncCandidate = (
comparison,
appStateSyncKeyReady,
) =>
appStateSyncKeyReady === true &&
comparison?.status === 'pending' &&
comparison.future_target_mac_count > 0 &&
comparison.pending_mac_count ===
comparison.pending_future_target_mac_count &&
comparison.missing_source_mac_count === 0 &&
comparison.extra_target_mac_count === 0 &&
comparison.pending_collection_count === 0;
const canonicalAppStateProjectionStabilitySignature = (projection) => {
const canonicalValue = (value) => {
const bytes = projectionBytes(value);
if (bytes) return { bytes: bytes.toString('base64') };
if (Array.isArray(value)) return value.map(canonicalValue);
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.keys(value)
.sort()
.map((key) => [key, canonicalValue(value[key])]),
);
}
return value;
};
const tableNames = [
'whatsapp_app_state_sync_keys',
'whatsapp_app_state_version',
'whatsapp_app_state_mutation_macs',
];
const tables = {};
for (const tableName of tableNames) {
const rows = projection?.tables?.[tableName];
if (!Array.isArray(rows)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_projection_invalid',
);
}
tables[tableName] = rows
.map((row) => JSON.stringify(canonicalValue(row)))
.sort();
}
return crypto
.createHash('sha256')
.update(JSON.stringify(tables))
.digest('hex');
};
const comparePreservedNativeAppStateBaseline = (
source,
target,
snapshotResyncCollections,
) => ({
// On an active same-provider restart the restored Chromium profile is the
// durable app-state authority. Collection versions and MACs can be ahead
// of the last canonical checkpoint in *any* collection, not only the
// collection that originally carried the resync marker. Keep the
// cryptographic sync-key lineage strict, then prove the complete native
// version/MAC view is materialized and stable below.
...compareCanonicalAppStateSyncKeys(source, target),
preserved_native_collection_count: snapshotResyncCollections.size,
});
const preserveNativeSnapshotResyncMarker = (
source,
target,
snapshotResyncCollections,
) => {
const reconciled = reconcileCanonicalAppStateSyncKeysForPersistence(
source,
target,
);
return {
...reconciled.projection,
capabilities: {
...reconciled.projection.capabilities,
app_state_snapshot_resync_required:
snapshotResyncCollections.size > 0,
app_state_snapshot_resync_collections: [
...snapshotResyncCollections,
].sort(),
},
};
};
const canonicalAppStateSyncEvidenceCollectionSet = (syncJob) =>
new Set(
[
...(Array.isArray(syncJob?.observed_collection_names)
? syncJob.observed_collection_names
: []),
...(Array.isArray(
syncJob?.projection_work_observed_collection_names,
)
? syncJob.projection_work_observed_collection_names
: []),
].filter((name) => CANONICAL_APP_STATE_COLLECTION_SET.has(name)),
);
const normalizedSnapshotResyncCollections = (
capabilities,
{ optional } = {},
) => {
const required = capabilities?.app_state_snapshot_resync_required;
const collections = capabilities?.app_state_snapshot_resync_collections;
if (optional && required === undefined && collections === undefined) {
return [];
}
if (typeof required !== 'boolean' || !Array.isArray(collections)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_snapshot_resync_capability_invalid',
);
}
const normalized = [...collections].sort();
if (
collections.some(
(name) =>
typeof name !== 'string' ||
!CANONICAL_APP_STATE_COLLECTION_SET.has(name),
) ||
new Set(collections).size !== collections.length ||
JSON.stringify(collections) !== JSON.stringify(normalized) ||
required !== collections.length > 0
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_snapshot_resync_capability_invalid',
);
}
return collections;
};
const readCanonicalVarint = (bytes, cursor) => {
let value = 0;
let multiplier = 1;
for (let count = 0; count < 5; count += 1) {
if (cursor.offset >= bytes.length) return undefined;
const byte = bytes[cursor.offset++];
value += (byte & 0x7f) * multiplier;
if ((byte & 0x80) === 0 && Number.isSafeInteger(value)) return value;
multiplier *= 128;
}
return undefined;
};
const parseCanonicalAppStateFingerprint = (value) => {
const bytes = projectionBytes(value);
if (!bytes || bytes.length === 0) return undefined;
const cursor = { offset: 0 };
const fingerprint = {
rawId: undefined,
currentIndex: undefined,
deviceIndexes: [],
};
while (cursor.offset < bytes.length) {
const tag = readCanonicalVarint(bytes, cursor);
if (tag === undefined) return undefined;
const field = tag >>> 3;
const wireType = tag & 7;
if ((field === 1 || field === 2) && wireType === 0) {
const decoded = readCanonicalVarint(bytes, cursor);
if (decoded === undefined) return undefined;
if (field === 1) fingerprint.rawId = decoded;
else fingerprint.currentIndex = decoded;
} else if (field === 3 && wireType === 0) {
const decoded = readCanonicalVarint(bytes, cursor);
if (decoded === undefined) return undefined;
fingerprint.deviceIndexes.push(decoded);
} else if (field === 3 && wireType === 2) {
const length = readCanonicalVarint(bytes, cursor);
if (length === undefined) return undefined;
const end = cursor.offset + length;
if (end > bytes.length) return undefined;
while (cursor.offset < end) {
const decoded = readCanonicalVarint(bytes, cursor);
if (decoded === undefined || cursor.offset > end)
return undefined;
fingerprint.deviceIndexes.push(decoded);
}
} else if (wireType === 0) {
if (readCanonicalVarint(bytes, cursor) === undefined)
return undefined;
} else if (wireType === 2) {
const length = readCanonicalVarint(bytes, cursor);
if (length === undefined) return undefined;
cursor.offset += length;
if (cursor.offset > bytes.length) return undefined;
} else {
return undefined;
}
}
if (
!Number.isSafeInteger(fingerprint.rawId) ||
!Number.isSafeInteger(fingerprint.currentIndex) ||
new Set(fingerprint.deviceIndexes).size !==
fingerprint.deviceIndexes.length
) {
return undefined;
}
return fingerprint;
};
const canonicalAppStateSyncKey = (row) => {
const keyId = projectionBytes(row?.key_id);
const keyData = projectionBytes(row?.key_data);
const fingerprintBytes = projectionBytes(row?.fingerprint);
const fingerprint = parseCanonicalAppStateFingerprint(fingerprintBytes);
if (
!keyId ||
keyId.length < 2 ||
!keyData ||
keyData.length !== 32 ||
!fingerprintBytes ||
!fingerprint ||
!Number.isSafeInteger(row?.timestamp) ||
row.timestamp < 0
) {
return undefined;
}
return {
row,
keyId,
keyData,
fingerprintBytes,
fingerprint,
epoch: keyId[keyId.length - 2] * 256 + keyId[keyId.length - 1],
id: keyId.toString('base64'),
};
};
const canonicalAppStateSyncKeyLineage = (rows) => {
if (rows.length === 0) {
return {
duplicateEpochCount: 0,
fingerprintRawIdMismatchCount: 0,
maxEpoch: -1,
maxTimestamp: 0,
rawId: undefined,
timestampRegressionCount: 0,
};
}
const ordered = [...rows].sort((left, right) => left.epoch - right.epoch);
const rawId = ordered[0].fingerprint.rawId;
let duplicateEpochCount = 0;
let fingerprintRawIdMismatchCount = 0;
let lastEpoch = -1;
let lastKnownTimestamp = 0;
let timestampRegressionCount = 0;
for (const row of ordered) {
fingerprintRawIdMismatchCount +=
row.fingerprint.rawId === rawId ? 0 : 1;
duplicateEpochCount += row.epoch === lastEpoch ? 1 : 0;
if (
row.row.timestamp > 0 &&
lastKnownTimestamp > 0 &&
row.row.timestamp < lastKnownTimestamp
) {
timestampRegressionCount += 1;
}
lastEpoch = row.epoch;
if (row.row.timestamp > 0) {
lastKnownTimestamp = row.row.timestamp;
}
}
return {
duplicateEpochCount,
fingerprintRawIdMismatchCount,
maxEpoch: ordered[ordered.length - 1].epoch,
maxTimestamp: lastKnownTimestamp,
rawId,
timestampRegressionCount,
};
};
const canonicalAppStateSyncKeyLineageDiagnostics = (lineage) => ({
duplicate_epoch_count: lineage.duplicateEpochCount,
fingerprint_raw_id_mismatch_count: lineage.fingerprintRawIdMismatchCount,
timestamp_regression_count: lineage.timestampRegressionCount,
});
const canonicalAppStateSyncKeyLineageInvalid = (lineage) =>
lineage.duplicateEpochCount > 0 ||
lineage.fingerprintRawIdMismatchCount > 0 ||
lineage.timestampRegressionCount > 0;
const canonicalAppStateSyncKeyTimestampDiagnostics = ({
materializedTimestampCount,
preservedTimestampCount,
sourceUnknownTimestampCount,
targetUnknownTimestampCount,
}) =>
sourceUnknownTimestampCount > 0 || targetUnknownTimestampCount > 0
? {
source_unknown_timestamp_count: sourceUnknownTimestampCount,
target_unknown_timestamp_count: targetUnknownTimestampCount,
materialized_timestamp_count: materializedTimestampCount,
preserved_timestamp_count: preservedTimestampCount,
}
: {};
const attachCanonicalAppStateSyncKeyReconciliationPlan = (comparison, plan) => {
Object.defineProperty(
comparison,
CANONICAL_APP_STATE_SYNC_KEY_RECONCILIATION_PLAN,
{ value: plan },
);
return comparison;
};
const compareCanonicalAppStateSyncKeys = (source, target) => {
const sourceRows = source?.tables?.whatsapp_app_state_sync_keys;
const targetRows = target?.tables?.whatsapp_app_state_sync_keys;
if (!Array.isArray(sourceRows) || !Array.isArray(targetRows)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_projection_invalid',
);
}
const decodedSource = sourceRows.map(canonicalAppStateSyncKey);
const decodedTarget = targetRows.map(canonicalAppStateSyncKey);
if (
decodedSource.some((row) => !row) ||
decodedTarget.some((row) => !row)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_projection_invalid',
);
}
const sourceById = new Map(decodedSource.map((row) => [row.id, row]));
const targetById = new Map(decodedTarget.map((row) => [row.id, row]));
if (
sourceById.size !== decodedSource.length ||
targetById.size !== decodedTarget.length
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_projection_invalid',
);
}
const sourceAnchor = canonicalAppStateSyncKeyLineage(decodedSource);
if (canonicalAppStateSyncKeyLineageInvalid(sourceAnchor)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_projection_invalid',
{
reason: 'app_state_sync_key_source_lineage_invalid',
source_sync_key_count: sourceRows.length,
...canonicalAppStateSyncKeyLineageDiagnostics(sourceAnchor),
},
);
}
if (sourceRows.length === 0) {
const targetLineage = canonicalAppStateSyncKeyLineage(decodedTarget);
if (canonicalAppStateSyncKeyLineageInvalid(targetLineage)) {
return {
status: 'diverged',
reason: 'app_state_sync_key_rotation_invalid',
source_sync_key_count: 0,
target_sync_key_count: targetRows.length,
rotated_sync_key_count: targetRows.length,
...canonicalAppStateSyncKeyLineageDiagnostics(targetLineage),
};
}
return attachCanonicalAppStateSyncKeyReconciliationPlan(
{
status: 'ready',
source_sync_key_count: 0,
target_sync_key_count: targetRows.length,
rotated_sync_key_count: targetRows.length,
...(decodedTarget.some((row) => row.row.timestamp === 0)
? {
rotated_unknown_timestamp_count: decodedTarget.filter(
(row) => row.row.timestamp === 0,
).length,
}
: {}),
},
[],
);
}
let missingSyncKeyCount = 0;
let timestampMismatchCount = 0;
let keyDataMismatchCount = 0;
let fingerprintMismatchCount = 0;
let matchingSyncKeyCount = 0;
let materializedTimestampCount = 0;
let preservedTimestampCount = 0;
let sourceUnknownTimestampCount = 0;
let targetUnknownTimestampCount = 0;
const timestampReconciliationPlan = [];
for (const sourceRow of decodedSource) {
const targetRow = targetById.get(sourceRow.id);
if (!targetRow) {
missingSyncKeyCount += 1;
continue;
}
// This protobuf scalar is optional. Zero means unknown, so compare
// timestamps only when both projections know the value. If only the
// durable source knows it, retain that value in the persistence plan.
const sourceTimestampKnown = sourceRow.row.timestamp > 0;
const targetTimestampKnown = targetRow.row.timestamp > 0;
const timestampMismatch =
sourceTimestampKnown &&
targetTimestampKnown &&
targetRow.row.timestamp !== sourceRow.row.timestamp;
const keyDataMismatch = !targetRow.keyData.equals(sourceRow.keyData);
const fingerprintMismatch = !targetRow.fingerprintBytes.equals(
sourceRow.fingerprintBytes,
);
sourceUnknownTimestampCount += sourceTimestampKnown ? 0 : 1;
targetUnknownTimestampCount += targetTimestampKnown ? 0 : 1;
materializedTimestampCount +=
!sourceTimestampKnown && targetTimestampKnown ? 1 : 0;
if (
sourceTimestampKnown &&
!targetTimestampKnown &&
!keyDataMismatch &&
!fingerprintMismatch
) {
preservedTimestampCount += 1;
timestampReconciliationPlan.push({
targetRow: targetRow.row,
timestamp: sourceRow.row.timestamp,
});
}
timestampMismatchCount += timestampMismatch ? 1 : 0;
keyDataMismatchCount += keyDataMismatch ? 1 : 0;
fingerprintMismatchCount += fingerprintMismatch ? 1 : 0;
matchingSyncKeyCount +=
!timestampMismatch && !keyDataMismatch && !fingerprintMismatch
? 1
: 0;
}
if (
missingSyncKeyCount > 0 ||
timestampMismatchCount > 0 ||
keyDataMismatchCount > 0 ||
fingerprintMismatchCount > 0
) {
return {
status: 'diverged',
reason: 'app_state_sync_key_mismatch',
source_sync_key_count: sourceRows.length,
target_sync_key_count: targetRows.length,
matching_sync_key_count: matchingSyncKeyCount,
missing_sync_key_count: missingSyncKeyCount,
timestamp_mismatch_count: timestampMismatchCount,
key_data_mismatch_count: keyDataMismatchCount,
fingerprint_mismatch_count: fingerprintMismatchCount,
...canonicalAppStateSyncKeyTimestampDiagnostics({
materializedTimestampCount,
preservedTimestampCount,
sourceUnknownTimestampCount,
targetUnknownTimestampCount,
}),
};
}
const targetLineage = canonicalAppStateSyncKeyLineage(decodedTarget);
if (canonicalAppStateSyncKeyLineageInvalid(targetLineage)) {
return {
status: 'diverged',
reason: 'app_state_sync_key_rotation_invalid',
source_sync_key_count: sourceRows.length,
target_sync_key_count: targetRows.length,
rotated_sync_key_count: targetRows.length - sourceRows.length,
...canonicalAppStateSyncKeyLineageDiagnostics(targetLineage),
...canonicalAppStateSyncKeyTimestampDiagnostics({
materializedTimestampCount,
preservedTimestampCount,
sourceUnknownTimestampCount,
targetUnknownTimestampCount,
}),
};
}
const rotated = decodedTarget
.filter((row) => !sourceById.has(row.id))
.sort((a, b) => a.epoch - b.epoch);
let lastRotationEpoch = sourceAnchor.maxEpoch;
let lastKnownRotationTimestamp = sourceAnchor.maxTimestamp;
for (const row of rotated) {
if (
row.fingerprint.rawId !== sourceAnchor.rawId ||
row.epoch <= lastRotationEpoch ||
(row.row.timestamp > 0 &&
lastKnownRotationTimestamp > 0 &&
row.row.timestamp < lastKnownRotationTimestamp)
) {
return {
status: 'diverged',
reason: 'app_state_sync_key_rotation_invalid',
source_sync_key_count: sourceRows.length,
target_sync_key_count: targetRows.length,
rotated_sync_key_count: rotated.length,
};
}
lastRotationEpoch = row.epoch;
if (row.row.timestamp > 0) {
lastKnownRotationTimestamp = row.row.timestamp;
}
}
return attachCanonicalAppStateSyncKeyReconciliationPlan(
{
status: 'ready',
source_sync_key_count: sourceRows.length,
target_sync_key_count: targetRows.length,
rotated_sync_key_count: rotated.length,
...(rotated.some((row) => row.row.timestamp === 0)
? {
rotated_unknown_timestamp_count: rotated.filter(
(row) => row.row.timestamp === 0,
).length,
}
: {}),
...canonicalAppStateSyncKeyTimestampDiagnostics({
materializedTimestampCount,
preservedTimestampCount,
sourceUnknownTimestampCount,
targetUnknownTimestampCount,
}),
},
timestampReconciliationPlan,
);
};
const reconcileCanonicalAppStateSyncKeysForPersistence = (source, target) => {
const comparison = compareCanonicalAppStateSyncKeys(source, target);
if (comparison.status !== 'ready') {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_diverged',
comparison,
);
}
const plan =
comparison[CANONICAL_APP_STATE_SYNC_KEY_RECONCILIATION_PLAN] || [];
if (plan.length === 0) {
return {
comparison,
preservedTimestampCount: 0,
projection: target,
};
}
const replacements = new Map(
plan.map(({ targetRow, timestamp }) => [targetRow, timestamp]),
);
const targetRows = target.tables.whatsapp_app_state_sync_keys;
let replacementCount = 0;
const reconciledRows = targetRows.map((row) => {
const timestamp = replacements.get(row);
if (timestamp === undefined) return row;
replacementCount += 1;
return { ...row, timestamp };
});
if (
replacementCount !== plan.length ||
reconciledRows.length !== targetRows.length
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_reconciliation_invalid',
);
}
const projection = {
...target,
tables: {
...target.tables,
whatsapp_app_state_sync_keys: reconciledRows,
},
};
const persistedComparison = compareCanonicalAppStateSyncKeys(
source,
projection,
);
if (
persistedComparison.status !== 'ready' ||
persistedComparison.preserved_timestamp_count > 0
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_reconciliation_invalid',
);
}
return {
comparison,
preservedTimestampCount: replacementCount,
projection,
};
};
/**
* Compare the source app-state watermark with a freshly synced WWebJS
* projection. Collection names and MAC material never leave this function;
* callers only receive aggregate counts and an opaque collection identifier.
*/
const compareCanonicalAppState = (source, target) => {
const sourceVersions = source?.tables?.whatsapp_app_state_version;
const targetVersions = target?.tables?.whatsapp_app_state_version;
const sourceMacs = source?.tables?.whatsapp_app_state_mutation_macs;
const targetMacs = target?.tables?.whatsapp_app_state_mutation_macs;
if (
!Array.isArray(sourceVersions) ||
!Array.isArray(targetVersions) ||
!Array.isArray(sourceMacs) ||
!Array.isArray(targetMacs)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_projection_invalid',
);
}
const syncKeyComparison = compareCanonicalAppStateSyncKeys(source, target);
if (syncKeyComparison.status === 'diverged') return syncKeyComparison;
const targetVersionByName = new Map(
targetVersions.map((row) => [row.name, row]),
);
let equalCollectionCount = 0;
let advancedCollectionCount = 0;
let pendingCollectionCount = 0;
let pendingMacCount = 0;
let verifiedMacCount = 0;
let futureTargetMacCount;
let pendingFutureTargetMacCount = 0;
let normalizedFutureSubstitutionCount = 0;
let missingSourceMacCount = 0;
let extraTargetMacCount = 0;
let firstPendingCollectionName;
const pendingCollections = new Map();
const pendingCollection = (name) => {
let diagnostic = pendingCollections.get(name);
if (!diagnostic) {
diagnostic = {
collection_id: opaqueCollectionId(name),
source_version: sourceVersionByName.get(name) ?? null,
target_version: targetVersionByName.get(name)?.version ?? null,
future_target_mac_count: 0,
future_target_matches_source_count: 0,
future_target_value_mismatch_count: 0,
future_target_extra_count: 0,
future_target_with_prior_effective_count: 0,
future_target_min_version_delta: null,
future_target_max_version_delta: null,
future_target_pending_count: 0,
normalized_future_substitution_count: 0,
missing_source_mac_count: 0,
extra_target_mac_count: 0,
};
pendingCollections.set(name, diagnostic);
}
return diagnostic;
};
const equalNames = new Set();
const sourceVersionByName = new Map();
for (const sourceRow of sourceVersions) {
if (
typeof sourceRow?.name !== 'string' ||
!Number.isSafeInteger(sourceRow.version)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_projection_invalid',
);
}
sourceVersionByName.set(sourceRow.name, sourceRow.version);
const targetRow = targetVersionByName.get(sourceRow.name);
if (!targetRow || targetRow.version < sourceRow.version) {
pendingCollectionCount += 1;
firstPendingCollectionName ||= sourceRow.name;
pendingCollection(sourceRow.name);
continue;
}
if (targetRow.version > sourceRow.version) {
advancedCollectionCount += 1;
continue;
}
const sourceHash = projectionBytes(sourceRow.hash);
const targetHash = projectionBytes(targetRow.hash);
if (!sourceHash || !targetHash || !sourceHash.equals(targetHash)) {
return {
status: 'diverged',
reason: 'collection_hash_mismatch',
collection_id: opaqueCollectionId(sourceRow.name),
collection_count: sourceVersions.length,
};
}
equalCollectionCount += 1;
equalNames.add(sourceRow.name);
}
const buildEffectiveMacMap = (rows, currentVersionByName) => {
const effective = new Map();
const futureRows = [];
let pendingRowCount = 0;
let firstPendingName;
for (const row of rows) {
const currentVersion = currentVersionByName.get(row?.name);
const indexMac = projectionBytes(row?.index_mac);
const valueMac = projectionBytes(row?.value_mac);
if (
!Number.isSafeInteger(row?.version) ||
row.version < 0 ||
!indexMac ||
!valueMac
) {
return { invalidName: row?.name };
}
// The product sync transaction can expose a freshly written
// action one poll before its collection watermark. It is never
// accepted as current state; keep polling until the corresponding
// version/LT hash is durable or the bounded catch-up times out.
if (currentVersion === undefined || row.version > currentVersion) {
pendingRowCount += 1;
firstPendingName ||= row.name;
futureRows.push({
key: `${row.name}\0${indexMac.toString('base64')}`,
name: row.name,
version: row.version,
valueMac,
row,
});
continue;
}
const key = `${row.name}\0${indexMac.toString('base64')}`;
const previous = effective.get(key);
if (!previous || row.version > previous.version) {
effective.set(key, {
name: row.name,
version: row.version,
valueMac,
row,
});
continue;
}
if (
row.version === previous.version &&
!valueMac.equals(previous.valueMac)
) {
return { invalidName: row.name };
}
}
return {
effective,
futureRows,
pendingRowCount,
firstPendingName,
};
};
const sourceEffectiveResult = buildEffectiveMacMap(
sourceMacs,
sourceVersionByName,
);
if (
!sourceEffectiveResult.effective ||
sourceEffectiveResult.pendingRowCount > 0
) {
return {
status: 'diverged',
reason: 'source_mac_watermark_invalid',
collection_id: opaqueCollectionId(
sourceEffectiveResult.invalidName ||
sourceEffectiveResult.firstPendingName,
),
collection_count: sourceVersions.length,
};
}
const targetCurrentVersionByName = new Map(
targetVersions.map((row) => [row.name, row.version]),
);
const targetEffectiveResult = buildEffectiveMacMap(
targetMacs,
targetCurrentVersionByName,
);
if (!targetEffectiveResult.effective) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_projection_invalid',
);
}
const sourceEffectiveMacs = sourceEffectiveResult.effective;
const targetEffectiveMacs = targetEffectiveResult.effective;
futureTargetMacCount = targetEffectiveResult.pendingRowCount;
const futureRowsByKey = new Map();
for (const futureRow of targetEffectiveResult.futureRows) {
const rowsForKey = futureRowsByKey.get(futureRow.key) || [];
rowsForKey.push(futureRow);
futureRowsByKey.set(futureRow.key, rowsForKey);
const diagnostic = pendingCollection(futureRow.name);
diagnostic.future_target_mac_count += 1;
if (targetEffectiveMacs.has(futureRow.key)) {
diagnostic.future_target_with_prior_effective_count += 1;
}
const versionDelta =
futureRow.version - Number(diagnostic.target_version);
diagnostic.future_target_min_version_delta = Math.min(
diagnostic.future_target_min_version_delta ?? versionDelta,
versionDelta,
);
diagnostic.future_target_max_version_delta = Math.max(
diagnostic.future_target_max_version_delta ?? versionDelta,
versionDelta,
);
const sourceRow = sourceEffectiveMacs.get(futureRow.key);
if (!sourceRow) {
diagnostic.future_target_extra_count += 1;
} else if (sourceRow.valueMac.equals(futureRow.valueMac)) {
diagnostic.future_target_matches_source_count += 1;
} else {
diagnostic.future_target_value_mismatch_count += 1;
}
}
const normalizedFutureRows = new Set();
const reconciliationPlan = [];
for (const [key, sourceRow] of sourceEffectiveMacs) {
if (!equalNames.has(sourceRow.name)) continue;
const targetRow = targetEffectiveMacs.get(key);
if (!targetRow) {
// The WWeb sync-actions table is keyed by the action index and can
// replace the row that represents the current LT-hash map with an
// identical pending patch row tagged with a future patch version.
// Normalize that representation only for comparison when there
// is an exact one-to-one key/value substitute under an identical
// collection version and LT hash. Rows and versions are never
// rewritten, and every unmatched/mismatched/extra future row
// remains pending.
const futureRows = futureRowsByKey.get(key) || [];
if (
futureRows.length === 1 &&
futureRows[0].name === sourceRow.name &&
futureRows[0].valueMac.equals(sourceRow.valueMac)
) {
normalizedFutureRows.add(futureRows[0]);
reconciliationPlan.push({
sourceRow: sourceRow.row,
targetRow: futureRows[0].row,
});
normalizedFutureSubstitutionCount += 1;
pendingCollection(
sourceRow.name,
).normalized_future_substitution_count += 1;
verifiedMacCount += 1;
continue;
}
pendingMacCount += 1;
missingSourceMacCount += 1;
firstPendingCollectionName ||= sourceRow.name;
pendingCollection(sourceRow.name).missing_source_mac_count += 1;
continue;
}
if (!sourceRow.valueMac.equals(targetRow.valueMac)) {
return {
status: 'diverged',
reason: 'mutation_mac_mismatch',
collection_id: opaqueCollectionId(sourceRow.name),
collection_count: sourceVersions.length,
};
}
verifiedMacCount += 1;
}
for (const futureRow of targetEffectiveResult.futureRows) {
if (normalizedFutureRows.has(futureRow)) continue;
pendingMacCount += 1;
pendingFutureTargetMacCount += 1;
firstPendingCollectionName ||= futureRow.name;
pendingCollection(futureRow.name).future_target_pending_count += 1;
}
// Baileys stores only the effective index/value map and tags every row
// with the collection watermark. WhatsMeow and WWebJS may retain the
// historical version that last changed each index. Compare the effective
// latest row at or below the collection version, while still requiring an
// exact byte-identical map whenever the version and LT hash are equal.
for (const [key, targetRow] of targetEffectiveMacs) {
if (equalNames.has(targetRow.name) && !sourceEffectiveMacs.has(key)) {
pendingMacCount += 1;
extraTargetMacCount += 1;
firstPendingCollectionName ||= targetRow.name;
pendingCollection(targetRow.name).extra_target_mac_count += 1;
}
}
const collectionDiagnostics = [...pendingCollections.values()].sort(
(a, b) => a.collection_id.localeCompare(b.collection_id),
);
const result = {
status:
pendingCollectionCount === 0 && pendingMacCount === 0
? 'ready'
: 'pending',
collection_count: sourceVersions.length,
equal_collection_count: equalCollectionCount,
advanced_collection_count: advancedCollectionCount,
pending_collection_count: pendingCollectionCount,
pending_mac_count: pendingMacCount,
future_target_mac_count: futureTargetMacCount,
pending_future_target_mac_count: pendingFutureTargetMacCount,
normalized_future_substitution_count: normalizedFutureSubstitutionCount,
missing_source_mac_count: missingSourceMacCount,
extra_target_mac_count: extraTargetMacCount,
verified_mac_count: verifiedMacCount,
source_sync_key_count: syncKeyComparison.source_sync_key_count,
target_sync_key_count: syncKeyComparison.target_sync_key_count,
rotated_sync_key_count: syncKeyComparison.rotated_sync_key_count,
...(syncKeyComparison.source_unknown_timestamp_count !== undefined
? {
source_unknown_timestamp_count:
syncKeyComparison.source_unknown_timestamp_count,
target_unknown_timestamp_count:
syncKeyComparison.target_unknown_timestamp_count,
materialized_timestamp_count:
syncKeyComparison.materialized_timestamp_count,
preserved_timestamp_count:
syncKeyComparison.preserved_timestamp_count,
}
: {}),
...(syncKeyComparison.rotated_unknown_timestamp_count !== undefined
? {
rotated_unknown_timestamp_count:
syncKeyComparison.rotated_unknown_timestamp_count,
}
: {}),
...(firstPendingCollectionName
? {
collection_id: opaqueCollectionId(firstPendingCollectionName),
}
: {}),
pending_collections: collectionDiagnostics.filter(
(entry) =>
entry.source_version !== entry.target_version ||
entry.future_target_pending_count > 0 ||
entry.missing_source_mac_count > 0 ||
entry.extra_target_mac_count > 0,
),
future_substitution_collections: collectionDiagnostics.filter(
(entry) => entry.normalized_future_substitution_count > 0,
),
};
Object.defineProperty(result, CANONICAL_APP_STATE_RECONCILIATION_PLAN, {
value: reconciliationPlan,
});
return result;
};
/**
* Keep WWebJS' browser-only future sync-action representation out of the
* provider-neutral PostgreSQL projection. A future row is replaced with the
* previously trusted canonical row only when compareCanonicalAppState proved
* a one-to-one index/value substitution under an identical version/LT hash.
*/
const reconcileCanonicalAppStateForPersistence = (source, target) => {
const comparison = compareCanonicalAppState(source, target);
if (comparison.status === 'diverged') {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_diverged',
comparison,
);
}
if (comparison.status !== 'ready') {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_checkpoint_pending',
comparison,
);
}
const syncKeyReconciliation =
reconcileCanonicalAppStateSyncKeysForPersistence(source, target);
const persistenceTarget = syncKeyReconciliation.projection;
const persistedSyncKeyDetails =
syncKeyReconciliation.preservedTimestampCount > 0
? {
persisted_sync_key_timestamp_count:
syncKeyReconciliation.preservedTimestampCount,
}
: {};
const plan = comparison[CANONICAL_APP_STATE_RECONCILIATION_PLAN] || [];
if (plan.length === 0) {
return {
comparison: { ...comparison, ...persistedSyncKeyDetails },
projection: persistenceTarget,
};
}
const replacements = new Map(
plan.map(({ sourceRow, targetRow }) => [targetRow, sourceRow]),
);
const targetRows =
persistenceTarget.tables.whatsapp_app_state_mutation_macs;
let replacementCount = 0;
const reconciledRows = targetRows.map((row) => {
const sourceRow = replacements.get(row);
if (!sourceRow) return row;
replacementCount += 1;
return { ...sourceRow };
});
if (
replacementCount !== plan.length ||
reconciledRows.length !== targetRows.length
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_reconciliation_invalid',
);
}
const projection = {
...persistenceTarget,
tables: {
...persistenceTarget.tables,
whatsapp_app_state_mutation_macs: reconciledRows,
},
};
const persistedComparison = compareCanonicalAppState(source, projection);
if (
persistedComparison.status !== 'ready' ||
persistedComparison.future_target_mac_count !== 0 ||
persistedComparison.normalized_future_substitution_count !== 0
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_reconciliation_invalid',
);
}
return {
comparison: {
...comparison,
...persistedSyncKeyDetails,
persisted_future_target_mac_count: 0,
persisted_normalized_future_substitution_count: 0,
},
projection,
};
};
/**
* A newly paired browser can finish WhatsApp Web's official full-sync job
* while its sync-actions store still contains rows ahead of the collection
* watermark. Those rows are not a portable LT-hash map and must never be
* retagged as the current collection version.
*
* For collections that do not exist in a trusted baseline, represent that
* state truthfully as "snapshot resync required": remove the collection
* watermark and every mutation MAC belonging to it, while preserving the
* app-state sync keys. Baileys/WhatsMeow will consequently start from version
* zero and request an authenticated server snapshot. Existing baseline
* collections are deliberately ineligible, so restart/handoff comparison
* remains strict.
*/
const normalizeCanonicalAppStateForSnapshotResync = (
projection,
{ eligibleCollectionNames, requiredCollectionNames } = {},
) => {
const versions = projection?.tables?.whatsapp_app_state_version;
const mutationMacs = projection?.tables?.whatsapp_app_state_mutation_macs;
const syncKeys = projection?.tables?.whatsapp_app_state_sync_keys;
if (
!Array.isArray(versions) ||
!Array.isArray(mutationMacs) ||
!Array.isArray(syncKeys) ||
(eligibleCollectionNames !== undefined &&
!(eligibleCollectionNames instanceof Set)) ||
(requiredCollectionNames !== undefined &&
!(requiredCollectionNames instanceof Set))
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_snapshot_resync_projection_invalid',
);
}
for (const name of eligibleCollectionNames || []) {
if (!CANONICAL_APP_STATE_COLLECTION_SET.has(name)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_snapshot_resync_projection_invalid',
);
}
}
const snapshotResyncNames = new Set();
for (const name of requiredCollectionNames || []) {
if (
!CANONICAL_APP_STATE_COLLECTION_SET.has(name) ||
(eligibleCollectionNames !== undefined &&
!eligibleCollectionNames.has(name))
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_snapshot_resync_projection_invalid',
);
}
snapshotResyncNames.add(name);
}
const currentVersionByName = new Map();
for (const row of versions) {
if (
typeof row?.name !== 'string' ||
!row.name ||
!CANONICAL_APP_STATE_COLLECTION_SET.has(row.name) ||
!Number.isSafeInteger(row.version) ||
row.version < 0 ||
currentVersionByName.has(row.name)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_snapshot_resync_projection_invalid',
);
}
currentVersionByName.set(row.name, row.version);
}
for (const row of mutationMacs) {
const currentVersion = currentVersionByName.get(row?.name);
if (
typeof row?.name !== 'string' ||
!row.name ||
!CANONICAL_APP_STATE_COLLECTION_SET.has(row.name) ||
!Number.isSafeInteger(row.version) ||
row.version < 0 ||
!projectionBytes(row.index_mac) ||
!projectionBytes(row.value_mac)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_snapshot_resync_projection_invalid',
);
}
if (
(currentVersion === undefined || row.version > currentVersion) &&
(eligibleCollectionNames === undefined ||
eligibleCollectionNames.has(row.name))
) {
snapshotResyncNames.add(row.name);
}
}
if (snapshotResyncNames.size === 0) {
return {
projection,
snapshot_resync_collection_count: 0,
removed_version_row_count: 0,
removed_mutation_mac_count: 0,
collection_ids: [],
snapshot_resync_collections: [],
};
}
if (syncKeys.length === 0) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_snapshot_resync_sync_key_missing',
);
}
const retainedVersions = versions.filter(
(row) => !snapshotResyncNames.has(row.name),
);
const retainedMutationMacs = mutationMacs.filter((row) => {
if (snapshotResyncNames.has(row.name)) return false;
const currentVersion = currentVersionByName.get(row.name);
// WWeb's sync-actions store may retain private queue rows above the
// collection watermark after the official sync generation finishes.
// Such rows have not been committed into the LT-hash state and are
// therefore not portable canonical material. Keep every committed
// row strict, including all non-resync collections, while excluding
// only rows that are provably newer than their own native watermark.
return currentVersion !== undefined && row.version <= currentVersion;
});
const filteredProjection = {
...projection,
tables: {
...projection.tables,
whatsapp_app_state_version: retainedVersions,
whatsapp_app_state_mutation_macs: retainedMutationMacs,
},
};
const nonImportableSignalSessions = Array.isArray(
projection?.tables?.whatsapp_signal_sessions,
)
? projection.tables.whatsapp_signal_sessions
.filter((row) => row?.session === null)
.map((row) => ({
their_id: row.their_id,
scope: row.scope,
session: null,
}))
: [];
// The canonical roundtrip validates every remaining row and recalculates
// record_count/size_bytes from byte-bearing fields. Do not hand-maintain
// accounting here: a stale size would make the PostgreSQL checkpoint
// unverifiable after a crash or provider handoff.
const normalized = canonicalBrowserProjectionToStore(
canonicalStoreProjectionToBrowser(filteredProjection, {
webVersion: filteredProjection.web_version,
}),
);
normalized.capabilities = {
...normalized.capabilities,
app_state_snapshot_resync_required: true,
app_state_snapshot_resync_collections: [...snapshotResyncNames].sort(),
};
if (nonImportableSignalSessions.length > 0) {
const normalizedSignalSessionKeys = new Set(
normalized.tables.whatsapp_signal_sessions.map(
(row) => `${row.their_id}\0${row.scope}`,
),
);
for (const row of nonImportableSignalSessions) {
const key = `${row.their_id}\0${row.scope}`;
if (normalizedSignalSessionKeys.has(key)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_snapshot_resync_projection_invalid',
);
}
normalizedSignalSessionKeys.add(key);
normalized.tables.whatsapp_signal_sessions.push(row);
}
// NULL sessions have no byte payload and therefore affect only the
// canonical row count. The store/browser roundtrip intentionally
// omits them because WWeb cannot import a native-only Signal row.
normalized.record_count += nonImportableSignalSessions.length;
}
if (projection.passthrough_tables !== undefined) {
normalized.passthrough_tables = projection.passthrough_tables;
}
const normalizedComparison = compareCanonicalAppState(
normalized,
normalized,
);
if (
normalizedComparison.status !== 'ready' ||
normalizedComparison.future_target_mac_count !== 0
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_snapshot_resync_projection_invalid',
);
}
return {
projection: normalized,
snapshot_resync_collection_count: snapshotResyncNames.size,
removed_version_row_count: versions.length - retainedVersions.length,
removed_mutation_mac_count:
mutationMacs.length - retainedMutationMacs.length,
collection_ids: [...snapshotResyncNames].map(opaqueCollectionId).sort(),
snapshot_resync_collections: [...snapshotResyncNames].sort(),
};
};
/**
* A provider-handoff manifest makes every declared snapshot-resync collection
* an instruction to the target, rather than a description of browser-local
* material. Consequently no watermark or mutation MAC for such a collection
* may cross the provider boundary, even when all browser rows are at or below
* the current watermark. Baileys and WhatsMeow both start those collections
* from version zero and request an authenticated server snapshot.
*/
const normalizeCanonicalAppStateForProviderHandoff = (projection) => {
const collectionNames = normalizedSnapshotResyncCollections(
projection?.capabilities,
{ optional: true },
);
if (collectionNames.length === 0) {
return {
projection,
snapshot_resync_collection_count: 0,
removed_version_row_count: 0,
removed_mutation_mac_count: 0,
collection_ids: [],
snapshot_resync_collections: [],
};
}
const requiredCollectionNames = new Set(collectionNames);
return normalizeCanonicalAppStateForSnapshotResync(projection, {
eligibleCollectionNames: requiredCollectionNames,
requiredCollectionNames,
});
};
/**
* Convert a two-sample-attested native WWeb app-state view into portable
* canonical material. The native sync-actions store legitimately retains
* queue entries above the committed collection watermark after the socket is
* connected. Those entries are browser-private work, but the collection
* version/LT hash itself is authoritative and must not be discarded.
*
* This path is restricted to authenticated native authority. Its callers
* prove that every canonical collection is materialized and either attest two
* complete same-provider exports or bind the projection to an exact official
* cross-provider sync generation plus a strict portable-source comparison.
* We can then keep each committed watermark, discard only MAC rows above it,
* and consume an older snapshot-resync marker. Initial-pairing paths remain
* fail-closed.
*/
const normalizeCanonicalAuthoritativeAppStateForPortablePersistence = (
projection,
) => {
const versions = projection?.tables?.whatsapp_app_state_version;
const mutationMacs = projection?.tables?.whatsapp_app_state_mutation_macs;
const syncKeys = projection?.tables?.whatsapp_app_state_sync_keys;
if (
!Array.isArray(versions) ||
!Array.isArray(mutationMacs) ||
!Array.isArray(syncKeys) ||
syncKeys.length === 0
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_projection_invalid',
);
}
const currentVersionByName = new Map();
for (const row of versions) {
if (
typeof row?.name !== 'string' ||
!CANONICAL_APP_STATE_COLLECTION_SET.has(row.name) ||
!Number.isSafeInteger(row.version) ||
row.version < 0 ||
!projectionBytes(row.hash) ||
currentVersionByName.has(row.name)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_projection_invalid',
);
}
currentVersionByName.set(row.name, row.version);
}
const missingCollectionNames = CANONICAL_APP_STATE_COLLECTIONS.filter(
(name) => !currentVersionByName.has(name),
);
if (missingCollectionNames.length > 0) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_checkpoint_pending',
{
reason: 'native_profile_materialization_incomplete',
materialized_collection_count:
CANONICAL_APP_STATE_COLLECTIONS.length -
missingCollectionNames.length,
missing_materialized_collection_count:
missingCollectionNames.length,
required_materialized_collection_count:
CANONICAL_APP_STATE_COLLECTIONS.length,
},
);
}
const retainedMutationMacs = mutationMacs.filter((row) => {
const currentVersion = currentVersionByName.get(row?.name);
if (
currentVersion === undefined ||
!Number.isSafeInteger(row.version) ||
row.version < 0 ||
!projectionBytes(row.index_mac) ||
!projectionBytes(row.value_mac)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_projection_invalid',
);
}
return row.version <= currentVersion;
});
const nonImportableSignalSessions = Array.isArray(
projection?.tables?.whatsapp_signal_sessions,
)
? projection.tables.whatsapp_signal_sessions
.filter((row) => row?.session === null)
.map((row) => ({
their_id: row.their_id,
scope: row.scope,
session: null,
}))
: [];
const filteredProjection = {
...projection,
capabilities: {
...projection.capabilities,
app_state_snapshot_resync_required: false,
app_state_snapshot_resync_collections: [],
},
tables: {
...projection.tables,
whatsapp_app_state_mutation_macs: retainedMutationMacs,
},
};
// This projection has already crossed the canonical codec boundary and
// may contain provider-neutral passthrough tables or native-only NULL
// Signal rows that WWeb cannot re-import. Preserve those rows verbatim and
// adjust only the two accounting fields affected by removed 64-byte MAC
// pairs.
const removedFutureMutationMacCount =
mutationMacs.length - retainedMutationMacs.length;
const normalized = {
...filteredProjection,
tables: {
...filteredProjection.tables,
whatsapp_app_state_mutation_macs: retainedMutationMacs,
...(nonImportableSignalSessions.length > 0
? {
whatsapp_signal_sessions:
projection.tables.whatsapp_signal_sessions,
}
: {}),
},
record_count: projection.record_count - removedFutureMutationMacCount,
size_bytes: projection.size_bytes - removedFutureMutationMacCount * 64,
};
const comparison = compareCanonicalAppState(normalized, normalized);
if (
comparison.status !== 'ready' ||
comparison.future_target_mac_count !== 0
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_projection_invalid',
);
}
return {
projection: normalized,
removed_future_mutation_mac_count: removedFutureMutationMacCount,
materialized_collection_count: currentVersionByName.size,
};
};
/**
* A reused WWeb profile may retain browser-private sync-action rows from an
* older runtime even after the imported cross-provider source and every
* committed native watermark agree. During an exact official sync generation
* those future rows cannot become a prerequisite for completing that same
* generation, otherwise the page observer and host gate wait on each other.
*
* Normalize only when the raw comparison proves that every remaining blocker
* is a future target MAC. Exact one-to-one source substitutes are re-anchored
* to their trusted source row; unmatched rows above the native watermark are
* discarded as browser-private work. The resulting projection must compare
* strictly ready against the portable source. Any committed mismatch remains
* pending/diverged and is never hidden by this helper.
*/
const normalizeCanonicalOfficialSyncFutureMacs = (source, target) => {
const rawComparison = compareCanonicalAppState(source, target);
if (!isStableInitialSnapshotResyncCandidate(rawComparison, true)) {
return {
comparison: rawComparison,
projection: target,
removed_future_mutation_mac_count: 0,
};
}
const syncKeyReconciliation =
reconcileCanonicalAppStateSyncKeysForPersistence(source, target);
const persistenceTarget = syncKeyReconciliation.projection;
const currentVersionByName = new Map(
persistenceTarget.tables.whatsapp_app_state_version.map((row) => [
row.name,
row.version,
]),
);
const plan = rawComparison[CANONICAL_APP_STATE_RECONCILIATION_PLAN] || [];
const replacements = new Map(
plan.map(({ sourceRow, targetRow }) => [targetRow, sourceRow]),
);
let reconciledFutureMutationMacCount = 0;
let removedFutureMutationMacCount = 0;
const normalizedMutationMacs = [];
for (const row of persistenceTarget.tables
.whatsapp_app_state_mutation_macs) {
const currentVersion = currentVersionByName.get(row?.name);
if (
currentVersion === undefined ||
!Number.isSafeInteger(row?.version) ||
row.version < 0
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_projection_invalid',
);
}
if (row.version <= currentVersion) {
normalizedMutationMacs.push(row);
continue;
}
const sourceRow = replacements.get(row);
if (sourceRow) {
reconciledFutureMutationMacCount += 1;
normalizedMutationMacs.push({ ...sourceRow });
continue;
}
removedFutureMutationMacCount += 1;
}
if (reconciledFutureMutationMacCount !== plan.length) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_reconciliation_invalid',
);
}
const projection = {
...persistenceTarget,
tables: {
...persistenceTarget.tables,
whatsapp_app_state_mutation_macs: normalizedMutationMacs,
},
record_count:
persistenceTarget.record_count - removedFutureMutationMacCount,
size_bytes:
persistenceTarget.size_bytes - removedFutureMutationMacCount * 64,
};
const comparison = compareCanonicalAppState(source, projection);
if (comparison.status !== 'ready') {
return {
comparison: rawComparison,
projection: target,
removed_future_mutation_mac_count: 0,
};
}
return {
comparison: {
...comparison,
reconciled_future_mutation_mac_count:
reconciledFutureMutationMacCount,
removed_future_mutation_mac_count: removedFutureMutationMacCount,
},
projection,
reconciled_future_mutation_mac_count: reconciledFutureMutationMacCount,
removed_future_mutation_mac_count: removedFutureMutationMacCount,
};
};
/**
* Keep the PostgreSQL projection portable when Chromium's native sync-actions
* store retains mutation rows beyond a collection watermark. Those rows are
* valid browser-local recovery material, but they are not a complete LT-hash
* collection and therefore cannot be replayed as a canonical source.
*
* Existing snapshot-resync markers remain authoritative and newly detected
* future-row collections are added to them. The affected collection rows are
* removed without retagging any MAC; the authenticated target must request a
* fresh server snapshot for exactly those collections.
*/
const normalizeCanonicalAppStateForPortablePersistence = (projection) => {
const existingCollections = normalizedSnapshotResyncCollections(
projection?.capabilities,
{ optional: true },
);
const normalized = normalizeCanonicalAppStateForSnapshotResync(projection);
if (normalized.snapshot_resync_collection_count === 0) return normalized;
const snapshotResyncCollections = [
...new Set([
...existingCollections,
...normalized.snapshot_resync_collections,
]),
].sort();
return {
...normalized,
projection: {
...normalized.projection,
capabilities: {
...normalized.projection.capabilities,
app_state_snapshot_resync_required: true,
app_state_snapshot_resync_collections:
snapshotResyncCollections,
},
},
};
};
const canonicalScopesForJournalEvent = (event) => {
if (!event?.critical) return { tables: [], device: false, full: false };
if (event.area === 'local_storage') {
return { tables: [], device: true, full: false };
}
if (event.area !== 'indexeddb') {
return { tables: [], device: false, full: true };
}
const database = String(event.database || '').toLowerCase();
const store = String(event.store || '').toLowerCase();
if (database === 'signal-storage') {
const tableByStore = {
'identity-store': 'whatsapp_identity_keys',
'prekey-store': 'whatsapp_pre_keys',
'kyber-prekey-store': 'whatsapp_pq_pre_keys',
'kyber-last-resort-key-store': 'whatsapp_pq_pre_keys',
'session-store': 'whatsapp_signal_sessions',
'senderkey-store': 'whatsapp_sender_keys',
};
const table = tableByStore[store];
if (table) {
const tables =
table === 'whatsapp_pq_pre_keys'
? ['whatsapp_pq_pre_keys', 'whatsapp_pq_pre_key_state']
: [table];
return {
tables,
device: ['identity-store', 'prekey-store'].includes(store),
full: false,
};
}
if (store === 'signal-meta-store') {
return {
// PQ allocator/migration metadata and key rows form one
// invariant. Persist both from the same page snapshot so a
// crash cannot publish half of a migration/upload/reset.
tables: ['whatsapp_pq_pre_keys', 'whatsapp_pq_pre_key_state'],
device: true,
full: false,
};
}
if (['basekey-store', 'signed-prekey-store'].includes(store)) {
return { tables: [], device: true, full: false };
}
return { tables: [], device: false, full: true };
}
if (database === 'model-storage') {
const tableByStore = {
'sync-keys': 'whatsapp_app_state_sync_keys',
'collection-version': 'whatsapp_app_state_version',
'sync-actions': 'whatsapp_app_state_mutation_macs',
};
if (tableByStore[store]) {
return {
tables: [tableByStore[store]],
device: false,
full: false,
};
}
if (
[
'encrypted-mutations',
'pending-mutations',
'missing-keys',
].includes(store)
) {
return {
tables: [
'whatsapp_app_state_sync_keys',
'whatsapp_app_state_version',
'whatsapp_app_state_mutation_macs',
],
device: false,
full: false,
};
}
}
return { tables: [], device: false, full: true };
};
/* This function is serialized by Puppeteer and executes in the page realm. */
async function rearmCanonicalSocketOffline(options) {
const fail = (code, socketState) => ({
ok: false,
code,
socket_state: typeof socketState === 'string' ? socketState : 'unknown',
});
const resolveModule = (names) => {
for (const name of Array.isArray(names) ? names : [names]) {
try {
const value = window.require?.(name);
const resolved = value?.default || value;
if (resolved) return resolved;
} catch {
// Try the next verified alias. Current WhatsApp Web builds use
// the WAWeb-prefixed form, while older builds exposed the same
// ABI without that prefix.
}
}
return undefined;
};
const socket = resolveModule('WAWebSocketModel')?.Socket;
const multiDevice = resolveModule('WAWebUserPrefsMultiDevice');
const userPrefs = resolveModule('WAWebUserPrefsGeneral');
const launchSocket = resolveModule('WAWebLaunchSocket');
const comms = resolveModule('WAComms');
const backendModule = resolveModule('WAWebBackendEventBus');
const backendEvent = backendModule?.BackendEvent;
const backendEventBus = backendModule?.BackendEventBus;
const readState = () =>
String(socket?.state ?? socket?.get?.('state') ?? 'unknown');
const initialState = readState();
if (navigator.onLine !== false) {
return fail(
'wwebjs_canonical_reconnect_network_not_quiesced',
initialState,
);
}
if (
!socket ||
typeof socket.reconnect !== 'function' ||
socket.reconnect.length !== 0 ||
typeof socket.on !== 'function' ||
typeof socket.off !== 'function' ||
typeof comms?.isCommsInitialized !== 'function' ||
comms.isCommsInitialized.length !== 0 ||
typeof comms?.closeSocketAndResume !== 'function' ||
comms.closeSocketAndResume.length !== 0 ||
(options.allowRuntimeInitialization === true &&
(typeof launchSocket?.launchSocket !== 'function' ||
launchSocket.launchSocket.length !== 1 ||
typeof userPrefs?.getLogoutReason !== 'function' ||
userPrefs.getLogoutReason.length !== 0 ||
typeof userPrefs?.setLogoutReason !== 'function' ||
userPrefs.setLogoutReason.length !== 1 ||
typeof comms?.stopComms !== 'function' ||
comms.stopComms.length !== 0)) ||
backendEvent?.RECONNECT_SOCKET !== 'reconnect_socket' ||
typeof backendEventBus?.triggerReconnectSocket !== 'function' ||
backendEventBus.triggerReconnectSocket.length !== 0 ||
typeof backendEventBus?.getListenersCount !== 'function' ||
backendEventBus.getListenersCount.length !== 1
) {
return fail(
'wwebjs_canonical_reconnect_abi_incompatible',
initialState,
);
}
const allowRuntimeInitialization =
options.allowRuntimeInitialization === true;
let reconnectListenerCount;
let commsInitialized;
try {
reconnectListenerCount = backendEventBus.getListenersCount(
backendEvent.RECONNECT_SOCKET,
);
commsInitialized = comms.isCommsInitialized();
} catch {
if (!allowRuntimeInitialization) {
return fail(
'wwebjs_canonical_reconnect_runtime_unavailable',
initialState,
);
}
}
if (
!allowRuntimeInitialization &&
(!Number.isSafeInteger(reconnectListenerCount) ||
reconnectListenerCount < 1 ||
commsInitialized !== true)
) {
return fail(
'wwebjs_canonical_reconnect_runtime_unavailable',
initialState,
);
}
if (multiDevice?.isRegistered?.() !== true) {
return fail('wwebjs_canonical_reconnect_not_registered', initialState);
}
// A browser-start WebSocket gate can leave the official loop in OPENING
// without ever sending a handshake. The repair path restarts the official
// high-level launch; the ordinary path uses the official reconnect hook.
const allowedInitialStates =
options.allowEstablished === true
? ['OPENING', 'TIMEOUT', 'UNLAUNCHED', 'UNPAIRED', 'UNPAIRED_IDLE']
: ['OPENING', 'UNPAIRED', 'UNPAIRED_IDLE'];
if (!allowedInitialStates.includes(initialState)) {
return fail('wwebjs_canonical_reconnect_state_invalid', initialState);
}
if (window.__WWebSessionBridge?.armCanonicalPairingWatchdog?.() !== true) {
return fail(
'wwebjs_canonical_reconnect_watchdog_unavailable',
initialState,
);
}
let staleLogoutMarkerCleared = false;
if (allowRuntimeInitialization) {
try {
// An empty Chromium boot happens before the canonical registered
// projection is hydrated. WhatsApp Web can persist the boot-time
// logout marker even though the database-owned companion remains
// valid. The trusted, fenced handoff path clears only that marker
// while still offline; identity and registration data remain
// protected and are validated again before network release.
const logoutReason = await userPrefs.getLogoutReason();
if (logoutReason) {
await userPrefs.setLogoutReason(null);
if (await userPrefs.getLogoutReason()) {
return fail(
'wwebjs_canonical_logout_marker_clear_failed',
readState(),
);
}
staleLogoutMarkerCleared = true;
}
} catch {
return fail(
'wwebjs_canonical_logout_marker_clear_failed',
readState(),
);
}
}
return new Promise((resolve) => {
let settled = false;
let timeout;
let launchAccepted = false;
let dispatchProofTimeout;
const settle = (result) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
clearTimeout(dispatchProofTimeout);
socket.off('change:state', onState);
resolve(result);
};
const onState = (_model, nextState) => {
const normalized = String(nextState ?? readState());
if (!launchAccepted) return;
if (normalized === 'OPENING' || normalized === 'CONNECTED') {
if (
allowRuntimeInitialization &&
!['dispatched', 'completed'].includes(
window.__WWebCanonicalRegisteredLaunchStateV1,
)
) {
return;
}
settle({
ok: true,
previous_socket_state: initialState,
socket_state: normalized,
reconnect_triggered:
!allowRuntimeInitialization &&
commsInitialized === true,
runtime_initialized: allowRuntimeInitialization,
...(allowRuntimeInitialization
? {
runtime_launch_dispatched: true,
runtime_launch_state:
window.__WWebCanonicalRegisteredLaunchStateV1,
stale_logout_marker_cleared:
staleLogoutMarkerCleared,
}
: {}),
});
return;
}
if (
[
'PAIRING',
'PROXYBLOCK',
'SMB_TOS_BLOCK',
'TIMEOUT',
'TOS_BLOCK',
].includes(normalized)
) {
settle(
fail(
'wwebjs_canonical_reconnect_transition_invalid',
normalized,
),
);
}
};
socket.on('change:state', onState);
timeout = setTimeout(
() =>
settle(
fail(
'wwebjs_canonical_reconnect_transition_timeout',
readState(),
),
),
options.timeoutMs,
);
try {
if (allowRuntimeInitialization) {
// The page booted through WhatsApp Web's registration branch
// before canonical credentials were hydrated. Re-enter the
// one official high-level launch flow so every registered
// backend/storage/sync hook is initialized. This private API
// has no internal idempotency guard, so reentry fails closed.
if (window.__WWebCanonicalRegisteredLaunchStateV1) {
settle(
fail(
'wwebjs_canonical_registered_launch_reentry',
readState(),
),
);
return;
}
window.__WWebCanonicalRegisteredLaunchStateV1 = 'starting';
if (commsInitialized === true) comms.stopComms();
const launched = launchSocket.launchSocket(null);
if (!launched || typeof launched.then !== 'function') {
window.__WWebCanonicalRegisteredLaunchStateV1 = 'failed';
settle(
fail(
'wwebjs_canonical_runtime_initialization_return_invalid',
readState(),
),
);
return;
}
launchAccepted = true;
window.__WWebCanonicalRegisteredLaunchStateV1 = 'dispatched';
window.__WWebCanonicalRegisteredLaunchPromiseV1 =
Promise.resolve(launched).then(
() => {
if (
window.__WWebCanonicalRegisteredLaunchStateV1 ===
'dispatched'
) {
window.__WWebCanonicalRegisteredLaunchStateV1 =
'completed';
}
onState(socket, readState());
},
() => {
window.__WWebCanonicalRegisteredLaunchStateV1 =
'failed';
settle(
fail(
'wwebjs_canonical_runtime_initialization_failed',
readState(),
),
);
},
);
setTimeout(() => onState(socket, readState()), 0);
// The official registered launch may deliberately await
// network-dependent backend work before moving Socket out of
// UNLAUNCHED. Prove that the call survived an asynchronous
// turn, then let the externally fenced network gate resume it.
// This is only an offline dispatch proof; ONLINE still
// requires CONNECTED + hasSynced + a live comms socket.
dispatchProofTimeout = setTimeout(() => {
const launchState =
window.__WWebCanonicalRegisteredLaunchStateV1;
const currentState = readState();
if (
!['dispatched', 'completed'].includes(launchState) ||
!['UNLAUNCHED', 'OPENING', 'CONNECTED'].includes(
currentState,
)
) {
settle(
fail(
launchState === 'failed'
? 'wwebjs_canonical_runtime_initialization_failed'
: 'wwebjs_canonical_runtime_dispatch_invalid',
currentState,
),
);
return;
}
settle({
ok: true,
previous_socket_state: initialState,
socket_state: currentState,
reconnect_triggered: false,
runtime_initialized: currentState !== 'UNLAUNCHED',
runtime_launch_dispatched: true,
runtime_launch_state: launchState,
stale_logout_marker_cleared: staleLogoutMarkerCleared,
});
}, 250);
return;
}
launchAccepted = true;
const returned = socket.reconnect();
if (returned !== undefined) {
settle(
fail(
'wwebjs_canonical_reconnect_return_invalid',
readState(),
),
);
return;
}
onState(socket, readState());
} catch {
if (allowRuntimeInitialization) {
window.__WWebCanonicalRegisteredLaunchStateV1 = 'failed';
}
settle(
fail(
commsInitialized === true
? 'wwebjs_canonical_reconnect_invocation_failed'
: 'wwebjs_canonical_runtime_initialization_failed',
readState(),
),
);
}
});
}
/* This function is serialized by Puppeteer and executes in the page realm. */
function resumeCanonicalRegisteredSocketOnline(options) {
const fail = (code, socketState) => ({
ok: false,
code,
socket_state: typeof socketState === 'string' ? socketState : 'unknown',
});
const resolveModule = (name) => {
try {
const value = window.require?.(name);
return value?.default || value;
} catch {
return undefined;
}
};
const socket = resolveModule('WAWebSocketModel')?.Socket;
const multiDevice = resolveModule('WAWebUserPrefsMultiDevice');
const comms = resolveModule('WAComms');
const backendModule = resolveModule('WAWebBackendEventBus');
const backendEvent = backendModule?.BackendEvent;
const backendEventBus = backendModule?.BackendEventBus;
const conn = resolveModule('WAWebConnModel')?.Conn;
const readState = () =>
String(socket?.state ?? socket?.get?.('state') ?? 'unknown');
const initialState = readState();
const bridgeState = window.__WWebSessionBridge?.state?.();
let reconnectListenerCount;
let commsInitialized;
try {
reconnectListenerCount = backendEventBus?.getListenersCount?.(
backendEvent?.RECONNECT_SOCKET,
);
commsInitialized = comms?.isCommsInitialized?.();
} catch {
return fail(
'wwebjs_canonical_online_reconnect_runtime_unavailable',
initialState,
);
}
if (
navigator.onLine !== true ||
window.performance?.timeOrigin !== options.documentEpoch ||
multiDevice?.isRegistered?.() !== true ||
window.__WWebCanonicalRegisteredLaunchStateV1 !== 'completed' ||
bridgeState?.canonicalCredentialGuardActive !== true ||
bridgeState?.canonicalCredentialProtectionInstalled !== true ||
bridgeState?.canonicalDestructiveCredentialPhase !== 'live' ||
bridgeState?.canonicalDestructiveCredentialAcknowledged !== true ||
bridgeState?.canonicalDestructiveCredentialViolationDetected === true ||
bridgeState?.canonicalPairingWatchdogActive !== true ||
bridgeState?.canonicalPairingDetected === true ||
Boolean(conn?.ref)
) {
return fail(
'wwebjs_canonical_online_reconnect_proof_invalid',
initialState,
);
}
if (
!socket ||
typeof socket.reconnect !== 'function' ||
socket.reconnect.length !== 0 ||
typeof socket.on !== 'function' ||
typeof socket.off !== 'function' ||
typeof comms?.isCommsInitialized !== 'function' ||
comms.isCommsInitialized.length !== 0 ||
backendEvent?.RECONNECT_SOCKET !== 'reconnect_socket' ||
typeof backendEventBus?.getListenersCount !== 'function' ||
backendEventBus.getListenersCount.length !== 1 ||
!Number.isSafeInteger(reconnectListenerCount) ||
reconnectListenerCount < 1 ||
commsInitialized !== true
) {
return fail(
'wwebjs_canonical_online_reconnect_abi_incompatible',
initialState,
);
}
if (initialState !== 'UNLAUNCHED') {
return fail(
'wwebjs_canonical_online_reconnect_state_invalid',
initialState,
);
}
if (window.__WWebCanonicalOnlineReconnectStateV1) {
return fail('wwebjs_canonical_online_reconnect_reentry', initialState);
}
window.__WWebCanonicalOnlineReconnectStateV1 = 'starting';
return new Promise((resolve) => {
let settled = false;
let timeout;
let launchAccepted = false;
const settle = (result) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
socket.off('change:state', onState);
resolve(result);
};
const onState = (_model, nextState) => {
const normalized = String(nextState ?? readState());
if (!launchAccepted) return;
const liveBridgeState = window.__WWebSessionBridge?.state?.();
if (
liveBridgeState?.canonicalPairingDetected === true ||
Boolean(conn?.ref) ||
multiDevice?.isRegistered?.() !== true
) {
window.__WWebCanonicalOnlineReconnectStateV1 = 'failed';
settle(
fail(
'wwebjs_canonical_online_reconnect_pairing_detected',
normalized,
),
);
return;
}
if (normalized === 'OPENING' || normalized === 'CONNECTED') {
window.__WWebCanonicalOnlineReconnectStateV1 = 'triggered';
settle({
ok: true,
previous_socket_state: initialState,
socket_state: normalized,
reconnect_triggered: true,
reconnect_listener_count: reconnectListenerCount,
});
return;
}
if (
[
'PAIRING',
'PROXYBLOCK',
'SMB_TOS_BLOCK',
'TOS_BLOCK',
'UNPAIRED',
'UNPAIRED_IDLE',
].includes(normalized)
) {
window.__WWebCanonicalOnlineReconnectStateV1 = 'failed';
settle(
fail(
'wwebjs_canonical_online_reconnect_transition_invalid',
normalized,
),
);
}
};
socket.on('change:state', onState);
timeout = setTimeout(() => {
window.__WWebCanonicalOnlineReconnectStateV1 = 'failed';
settle(
fail(
'wwebjs_canonical_online_reconnect_transition_timeout',
readState(),
),
);
}, options.timeoutMs);
try {
launchAccepted = true;
const returned = socket.reconnect();
if (returned !== undefined) {
window.__WWebCanonicalOnlineReconnectStateV1 = 'failed';
settle(
fail(
'wwebjs_canonical_online_reconnect_return_invalid',
readState(),
),
);
return;
}
window.__WWebCanonicalOnlineReconnectStateV1 = 'dispatched';
onState(socket, readState());
} catch {
window.__WWebCanonicalOnlineReconnectStateV1 = 'failed';
settle(
fail(
'wwebjs_canonical_online_reconnect_invocation_failed',
readState(),
),
);
}
});
}
/* This function is serialized by Puppeteer and executes in the page realm. */
async function launchCanonicalRegisteredSocketOnline(options) {
const fail = (code, socketState) => ({
ok: false,
code,
socket_state: typeof socketState === 'string' ? socketState : 'unknown',
});
const resolveModule = (name) => {
try {
const value = window.require?.(name);
return value?.default || value;
} catch {
return undefined;
}
};
const socket = resolveModule('WAWebSocketModel')?.Socket;
const multiDevice = resolveModule('WAWebUserPrefsMultiDevice');
const launchSocket = resolveModule('WAWebLaunchSocket');
const comms = resolveModule('WAComms');
const conn = resolveModule('WAWebConnModel')?.Conn;
const readState = () =>
String(socket?.state ?? socket?.get?.('state') ?? 'unknown');
const initialState = readState();
const bridgeState = window.__WWebSessionBridge?.state?.();
let commsInitialized;
try {
commsInitialized = comms?.isCommsInitialized?.();
} catch {
return fail(
'wwebjs_canonical_online_launch_runtime_unavailable',
initialState,
);
}
if (
navigator.onLine !== true ||
window.performance?.timeOrigin !== options.documentEpoch ||
multiDevice?.isRegistered?.() !== true ||
bridgeState?.canonicalCredentialGuardActive !== true ||
bridgeState?.canonicalCredentialProtectionInstalled !== true ||
bridgeState?.canonicalDestructiveCredentialPhase !== 'live' ||
bridgeState?.canonicalDestructiveCredentialAcknowledged !== true ||
bridgeState?.canonicalDestructiveCredentialViolationDetected === true ||
bridgeState?.canonicalPairingWatchdogActive !== true ||
bridgeState?.canonicalPairingDetected === true ||
Boolean(conn?.ref)
) {
return fail(
'wwebjs_canonical_online_launch_proof_invalid',
initialState,
);
}
if (
!socket ||
typeof launchSocket?.launchSocket !== 'function' ||
launchSocket.launchSocket.length !== 1 ||
typeof comms?.isCommsInitialized !== 'function' ||
comms.isCommsInitialized.length !== 0 ||
commsInitialized !== false ||
!['OPENING', 'UNLAUNCHED'].includes(initialState)
) {
return fail(
'wwebjs_canonical_online_launch_abi_incompatible',
initialState,
);
}
if (window.__WWebCanonicalRegisteredLaunchStateV1) {
return fail('wwebjs_canonical_registered_launch_reentry', initialState);
}
window.__WWebCanonicalRegisteredLaunchStateV1 = 'starting';
let launched;
try {
launched = launchSocket.launchSocket(null);
} catch {
window.__WWebCanonicalRegisteredLaunchStateV1 = 'failed';
return fail(
'wwebjs_canonical_online_launch_invocation_failed',
readState(),
);
}
if (!launched || typeof launched.then !== 'function') {
window.__WWebCanonicalRegisteredLaunchStateV1 = 'failed';
return fail(
'wwebjs_canonical_online_launch_return_invalid',
readState(),
);
}
window.__WWebCanonicalRegisteredLaunchStateV1 = 'dispatched';
Promise.resolve(launched).then(
() => {
if (
window.__WWebCanonicalRegisteredLaunchStateV1 === 'dispatched'
) {
window.__WWebCanonicalRegisteredLaunchStateV1 = 'completed';
}
},
() => {
window.__WWebCanonicalRegisteredLaunchStateV1 = 'failed';
},
);
const startedAt = Date.now();
while (Date.now() - startedAt < options.timeoutMs) {
const liveBridgeState = window.__WWebSessionBridge?.state?.();
const socketState = readState();
let initialized = false;
try {
initialized = comms.isCommsInitialized() === true;
} catch {
// The bounded loop below remains fail-closed.
}
if (
liveBridgeState?.canonicalPairingDetected === true ||
Boolean(conn?.ref) ||
multiDevice?.isRegistered?.() !== true
) {
window.__WWebCanonicalRegisteredLaunchStateV1 = 'failed';
return fail(
'wwebjs_canonical_online_launch_pairing_detected',
socketState,
);
}
if (window.__WWebCanonicalRegisteredLaunchStateV1 === 'failed') {
return fail('wwebjs_canonical_online_launch_failed', socketState);
}
if (
initialized === true &&
['OPENING', 'CONNECTED'].includes(socketState)
) {
return {
ok: true,
previous_socket_state: initialState,
socket_state: socketState,
launch_triggered: true,
runtime_launch_state:
window.__WWebCanonicalRegisteredLaunchStateV1,
};
}
if (
[
'PAIRING',
'PROXYBLOCK',
'SMB_TOS_BLOCK',
'TOS_BLOCK',
'UNPAIRED',
'UNPAIRED_IDLE',
].includes(socketState)
) {
window.__WWebCanonicalRegisteredLaunchStateV1 = 'failed';
return fail(
'wwebjs_canonical_online_launch_transition_invalid',
socketState,
);
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
window.__WWebCanonicalRegisteredLaunchStateV1 = 'failed';
return fail(
'wwebjs_canonical_online_launch_transition_timeout',
readState(),
);
}
/* This function is serialized by Puppeteer and executes in the page realm. */
async function redriveCanonicalRealmRecoveryTransport(options) {
const fail = (code, socketState) => ({
ok: false,
code,
socket_state: typeof socketState === 'string' ? socketState : 'unknown',
});
const resolveModule = (name) => {
try {
const value = window.require?.(name);
return value?.default || value;
} catch {
return undefined;
}
};
const socket = resolveModule('WAWebSocketModel')?.Socket;
const multiDevice = resolveModule('WAWebUserPrefsMultiDevice');
const launchSocket = resolveModule('WAWebLaunchSocket');
const comms = resolveModule('WAComms');
const conn = resolveModule('WAWebConnModel')?.Conn;
const bridge = window.__WWebSessionBridge;
const readState = () =>
String(socket?.state ?? socket?.get?.('state') ?? 'unknown');
const initialState = readState();
const readCommsInitialized = () => {
try {
return comms?.isCommsInitialized?.() === true;
} catch {
return undefined;
}
};
const readSocketConnected = () => {
try {
return comms?.isSocketConnected?.() === true;
} catch {
return undefined;
}
};
const bridgeState = bridge?.state?.();
const documentEpoch = window.performance?.timeOrigin;
const pairingReferenceActive =
Boolean(conn?.ref) &&
(multiDevice?.isRegistered?.() !== true ||
['UNPAIRED', 'UNPAIRED_IDLE'].includes(initialState));
if (
!Number.isSafeInteger(options?.timeoutMs) ||
options.timeoutMs < 1000 ||
options.timeoutMs > 30000 ||
!Number.isFinite(options?.previousDocumentEpoch) ||
!Number.isFinite(documentEpoch) ||
documentEpoch === options.previousDocumentEpoch ||
navigator.onLine !== true ||
multiDevice?.isRegistered?.() !== true ||
bridgeState?.canonicalCredentialGuardActive !== true ||
bridgeState?.canonicalCredentialProtectionInstalled !== true ||
bridgeState?.canonicalDestructiveCredentialPhase !== 'suppressing' ||
bridgeState?.canonicalDestructiveCredentialAcknowledged === true ||
bridgeState?.canonicalDestructiveCredentialViolationDetected === true ||
bridgeState?.canonicalNavigationStorageGuardActive !== true ||
bridgeState?.canonicalPairingDetected === true ||
pairingReferenceActive
) {
return fail(
'wwebjs_canonical_recovery_transport_redrive_proof_invalid',
initialState,
);
}
if (
!socket ||
typeof launchSocket?.launchSocket !== 'function' ||
launchSocket.launchSocket.length !== 1 ||
typeof comms?.isCommsInitialized !== 'function' ||
comms.isCommsInitialized.length !== 0 ||
typeof comms?.isSocketConnected !== 'function' ||
comms.isSocketConnected.length !== 0 ||
typeof comms?.stopComms !== 'function' ||
comms.stopComms.length !== 0 ||
!['CONNECTED', 'OPENING', 'TIMEOUT', 'UNLAUNCHED'].includes(
initialState,
) ||
window.__WWebCanonicalRegisteredLaunchStateV1 != null
) {
return fail(
'wwebjs_canonical_recovery_transport_redrive_abi_incompatible',
initialState,
);
}
const initiallyInitialized = readCommsInitialized();
const initiallyConnected = readSocketConnected();
if (
initiallyInitialized === undefined ||
initiallyConnected === undefined
) {
return fail(
'wwebjs_canonical_recovery_transport_redrive_runtime_unavailable',
initialState,
);
}
if (
initialState === 'CONNECTED' &&
initiallyInitialized === true &&
initiallyConnected === true
) {
return {
ok: true,
previous_socket_state: initialState,
socket_state: initialState,
launch_triggered: false,
transport_already_connected: true,
};
}
if (bridge?.armCanonicalPairingWatchdog?.() !== true) {
return fail(
'wwebjs_canonical_recovery_transport_redrive_watchdog_unavailable',
initialState,
);
}
window.__WWebCanonicalRegisteredLaunchStateV1 = 'starting';
try {
if (initiallyInitialized === true) {
const stopped = comms.stopComms();
if (stopped !== undefined) {
window.__WWebCanonicalRegisteredLaunchStateV1 = 'failed';
return fail(
'wwebjs_canonical_recovery_transport_redrive_stop_invalid',
readState(),
);
}
}
const launched = launchSocket.launchSocket(null);
if (!launched || typeof launched.then !== 'function') {
window.__WWebCanonicalRegisteredLaunchStateV1 = 'failed';
return fail(
'wwebjs_canonical_recovery_transport_redrive_return_invalid',
readState(),
);
}
window.__WWebCanonicalRegisteredLaunchStateV1 = 'dispatched';
Promise.resolve(launched).then(
() => {
if (
window.__WWebCanonicalRegisteredLaunchStateV1 ===
'dispatched'
) {
window.__WWebCanonicalRegisteredLaunchStateV1 = 'completed';
}
},
() => {
window.__WWebCanonicalRegisteredLaunchStateV1 = 'failed';
},
);
} catch {
window.__WWebCanonicalRegisteredLaunchStateV1 = 'failed';
return fail(
'wwebjs_canonical_recovery_transport_redrive_invocation_failed',
readState(),
);
}
const startedAt = Date.now();
while (Date.now() - startedAt < options.timeoutMs) {
const liveBridgeState = bridge?.state?.();
const socketState = readState();
const commsInitialized = readCommsInitialized();
const socketConnected = readSocketConnected();
const livePairingReferenceActive =
Boolean(conn?.ref) &&
(multiDevice?.isRegistered?.() !== true ||
['UNPAIRED', 'UNPAIRED_IDLE'].includes(socketState));
if (
liveBridgeState?.canonicalPairingDetected === true ||
livePairingReferenceActive ||
multiDevice?.isRegistered?.() !== true
) {
window.__WWebCanonicalRegisteredLaunchStateV1 = 'failed';
return fail(
'wwebjs_canonical_recovery_transport_redrive_pairing_detected',
socketState,
);
}
if (window.__WWebCanonicalRegisteredLaunchStateV1 === 'failed') {
return fail(
'wwebjs_canonical_recovery_transport_redrive_failed',
socketState,
);
}
if (
commsInitialized === true &&
['OPENING', 'CONNECTED'].includes(socketState)
) {
return {
ok: true,
previous_socket_state: initialState,
socket_state: socketState,
launch_triggered: true,
socket_connected: socketConnected === true,
runtime_launch_state:
window.__WWebCanonicalRegisteredLaunchStateV1,
};
}
if (
[
'PAIRING',
'PROXYBLOCK',
'SMB_TOS_BLOCK',
'TOS_BLOCK',
'UNPAIRED',
'UNPAIRED_IDLE',
].includes(socketState)
) {
window.__WWebCanonicalRegisteredLaunchStateV1 = 'failed';
return fail(
'wwebjs_canonical_recovery_transport_redrive_transition_invalid',
socketState,
);
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
window.__WWebCanonicalRegisteredLaunchStateV1 = 'failed';
return fail(
'wwebjs_canonical_recovery_transport_redrive_timeout',
readState(),
);
}
/* This function is serialized by Puppeteer and executes in the page realm. */
function verifyCanonicalReloadedSocketOffline(options) {
const fail = (code, socketState) => ({
ok: false,
code,
socket_state: typeof socketState === 'string' ? socketState : 'unknown',
});
const resolveModule = (name) => {
try {
const value = window.require?.(name);
return value?.default || value;
} catch {
return undefined;
}
};
const socket = resolveModule('WAWebSocketModel')?.Socket;
const multiDevice = resolveModule('WAWebUserPrefsMultiDevice');
const comms = resolveModule('WAComms');
const backendModule = resolveModule('WAWebBackendEventBus');
const backendEvent = backendModule?.BackendEvent;
const backendEventBus = backendModule?.BackendEventBus;
const conn = resolveModule('WAWebConnModel')?.Conn;
const socketState = String(
socket?.state ?? socket?.get?.('state') ?? 'unknown',
);
const documentEpoch = window.performance?.timeOrigin;
const bridgeState = window.__WWebSessionBridge?.state?.();
const registeredLaunchState = window.__WWebCanonicalRegisteredLaunchStateV1;
const registeredLaunchPending = ['dispatched', 'completed'].includes(
registeredLaunchState,
);
if (
navigator.onLine !== false ||
!Number.isFinite(documentEpoch) ||
documentEpoch !== options.documentEpoch
) {
return fail('wwebjs_canonical_reload_proof_invalid', socketState);
}
let listenerCount;
let commsInitialized;
try {
listenerCount = backendEventBus?.getListenersCount?.(
backendEvent?.RECONNECT_SOCKET,
);
commsInitialized = comms?.isCommsInitialized?.();
} catch {
return fail(
'wwebjs_canonical_reloaded_runtime_unavailable',
socketState,
);
}
const deferredRuntimeLaunch =
options.allowDeferredRuntimeLaunch === true &&
!registeredLaunchState &&
commsInitialized === false &&
['OPENING', 'UNLAUNCHED'].includes(socketState);
if (
multiDevice?.isRegistered?.() !== true ||
(commsInitialized !== true &&
!registeredLaunchPending &&
!deferredRuntimeLaunch) ||
((!Number.isSafeInteger(listenerCount) || listenerCount < 1) &&
!registeredLaunchPending) ||
registeredLaunchState === 'failed' ||
bridgeState?.canonicalCredentialGuardActive !== true ||
bridgeState?.canonicalCredentialProtectionInstalled !== true ||
bridgeState?.canonicalDestructiveCredentialPhase !== 'sealed' ||
bridgeState?.canonicalDestructiveCredentialAcknowledged !== true ||
bridgeState?.canonicalDestructiveCredentialViolationDetected === true ||
bridgeState?.canonicalPairingWatchdogActive !== true ||
bridgeState?.canonicalPairingDetected === true ||
Boolean(conn?.ref)
) {
return fail(
'wwebjs_canonical_reloaded_runtime_unavailable',
socketState,
);
}
const allowedStates =
options.allowEstablished === true
? ['CONNECTED', 'OPENING', 'TIMEOUT', 'UNLAUNCHED']
: registeredLaunchPending
? ['OPENING', 'UNLAUNCHED']
: ['OPENING'];
if (!allowedStates.includes(socketState)) {
return fail(
'wwebjs_canonical_reloaded_socket_state_invalid',
socketState,
);
}
return {
ok: true,
previous_socket_state: socketState,
socket_state: socketState,
reconnect_triggered: false,
bootstrapped_by_reload: true,
bootstrapped_by_registered_launch: registeredLaunchPending,
deferred_runtime_launch: deferredRuntimeLaunch,
runtime_launch_state: registeredLaunchPending
? registeredLaunchState
: undefined,
reload_proof_valid: true,
document_epoch: documentEpoch,
};
}
/* This function is serialized by Puppeteer and executes before page scripts. */
function installCanonicalCredentialNavigationGuard(ttlMs) {
const deadlineKey = '__WWebCanonicalRehydrationGuardUntilV1';
const navigationGuardKey = '__WWebCanonicalNavigationStorageGuardV1';
const destructivePhaseKey = '__WWebCanonicalDestructiveCredentialPhaseV1';
const maxTtlMs = 300000;
if (!Number.isSafeInteger(ttlMs) || ttlMs < 1000 || ttlMs > maxTtlMs) {
return false;
}
const expiresAt = Date.now() + ttlMs;
const deadlineValid = () => {
const deadline = Number(window[deadlineKey]);
return (
Number.isSafeInteger(deadline) &&
deadline > Date.now() &&
deadline <= Date.now() + maxTtlMs
);
};
Object.defineProperty(window, deadlineKey, {
configurable: true,
enumerable: false,
writable: true,
value: expiresAt,
});
Object.defineProperty(window, destructivePhaseKey, {
configurable: true,
enumerable: false,
writable: true,
value: 'suppressing',
});
const existing = window[navigationGuardKey];
if (existing?.version === 1 && typeof existing.renew === 'function') {
return existing.renew(expiresAt) === true;
}
let storage;
try {
storage = window.localStorage;
} catch {
// about:blank can have an opaque origin. This script is registered
// with evaluateOnNewDocument and runs again on the WhatsApp origin.
return true;
}
if (!storage) return false;
const storagePrototype = Object.getPrototypeOf(storage);
const originalGetItem = storagePrototype?.getItem;
const originalSetItem = storagePrototype?.setItem;
const originalRemoveItem = storagePrototype?.removeItem;
const originalClear = storagePrototype?.clear;
if (
typeof originalGetItem !== 'function' ||
typeof originalSetItem !== 'function' ||
typeof originalRemoveItem !== 'function' ||
typeof originalClear !== 'function'
) {
return false;
}
let suppressionCount = 0;
let violationDetected = false;
const canonicalCredentialStorageKeys = new Set([
'WANoiseInfo',
'WANoiseInfoIv',
'WAMeInfo',
'WADeviceId',
'last-wid-md',
'WebEncKeySalt',
'WAWebEncKeySalt',
]);
const isGuardedLocalStorage = (target) => {
if (
!deadlineValid() ||
!['suppressing', 'sealed'].includes(window[destructivePhaseKey])
) {
return false;
}
try {
return target === window.localStorage;
} catch {
return false;
}
};
const noteGuardedMutation = (category = 'credential_storage_clear') => {
if (window[destructivePhaseKey] !== 'sealed') return;
violationDetected = true;
void Promise.resolve(
window.__wwebjsCanonicalCredentialViolationNotify?.({
category,
}),
).catch(() => {});
};
const guardedRemoveItem = function (key) {
if (isGuardedLocalStorage(this)) {
suppressionCount += 1;
// Chromium/WhatsApp may discard unrelated UI and cache records
// while an MV3 request gate is being released. Keep every
// removal suppressed until the candidate goes live, but only
// treat a known registration anchor as a destructive credential
// attempt. Full clear() and the official credential-cleanup APIs
// remain independently protected below and by the bridge module
// wrappers, so unknown data cannot be lost through this window.
if (canonicalCredentialStorageKeys.has(String(key))) {
noteGuardedMutation('credential_storage_anchor_removed');
}
return undefined;
}
return originalRemoveItem.call(this, key);
};
const guardedClear = function () {
if (!isGuardedLocalStorage(this)) {
return originalClear.apply(this, arguments);
}
// clearAllLocalState also owns provider-specific bootstrap records
// that are not part of this short anchor list. A clear during the
// gated destructive phase must therefore be an integral no-op; trying
// to reconstruct selected keys after clearing is already lossy.
suppressionCount += 1;
noteGuardedMutation();
return undefined;
};
Object.defineProperty(guardedRemoveItem, '__wwebjsCanonicalGuarded', {
value: true,
});
Object.defineProperty(guardedClear, '__wwebjsCanonicalGuarded', {
value: true,
});
storagePrototype.removeItem = guardedRemoveItem;
storagePrototype.clear = guardedClear;
if (
storagePrototype.removeItem !== guardedRemoveItem ||
storagePrototype.clear !== guardedClear
) {
if (storagePrototype.removeItem === guardedRemoveItem) {
storagePrototype.removeItem = originalRemoveItem;
}
if (storagePrototype.clear === guardedClear) {
storagePrototype.clear = originalClear;
}
return false;
}
const guard = {
version: 1,
renew(nextExpiry) {
if (
!Number.isSafeInteger(nextExpiry) ||
nextExpiry <= Date.now() ||
nextExpiry > Date.now() + maxTtlMs
) {
return false;
}
window[deadlineKey] = nextExpiry;
return deadlineValid();
},
state() {
return {
active: deadlineValid(),
destructiveProtectionActive:
deadlineValid() &&
['suppressing', 'sealed'].includes(
window[destructivePhaseKey],
),
destructivePhase: window[destructivePhaseKey] || 'inactive',
violationDetected,
suppressionCount,
};
},
acknowledgeDestructiveProtection() {
if (
!deadlineValid() ||
window[destructivePhaseKey] !== 'suppressing'
) {
return false;
}
window[destructivePhaseKey] = 'sealed';
suppressionCount = 0;
return true;
},
releaseSealedProtection() {
if (
!deadlineValid() ||
window[destructivePhaseKey] !== 'sealed' ||
violationDetected
) {
return false;
}
window[destructivePhaseKey] = 'live';
return true;
},
disarm() {
if (storagePrototype.removeItem === guardedRemoveItem) {
storagePrototype.removeItem = originalRemoveItem;
}
if (storagePrototype.clear === guardedClear) {
storagePrototype.clear = originalClear;
}
try {
delete window[navigationGuardKey];
} catch {
// An inaccessible marker is already unusable.
}
try {
delete window[destructivePhaseKey];
} catch {
// A missing phase marker is already inactive.
}
},
};
Object.defineProperty(window, navigationGuardKey, {
configurable: true,
enumerable: false,
writable: false,
value: guard,
});
return guard.state().active === true;
}
/* This function is serialized by Puppeteer and executes in the page realm. */
function inspectCanonicalTransportHealth(expectedDocumentEpoch) {
const resolveModule = (name) => {
try {
const value = window.require?.(name);
return value?.default || value;
} catch {
return undefined;
}
};
const socket = resolveModule('WAWebSocketModel')?.Socket;
const comms = resolveModule('WAComms');
const backendModule = resolveModule('WAWebBackendEventBus');
const backendEventBus = backendModule?.BackendEventBus;
const backendEvent = backendModule?.BackendEvent;
const conn = resolveModule('WAWebConnModel')?.Conn;
const multiDevice = resolveModule('WAWebUserPrefsMultiDevice');
const bridgeState = window.__WWebSessionBridge?.state?.();
const allowedSocketStates = new Set([
'CONFLICT',
'CONNECTED',
'DEPRECATED_VERSION',
'OPENING',
'PAIRING',
'PROXYBLOCK',
'SMB_TOS_BLOCK',
'TIMEOUT',
'TOS_BLOCK',
'UNLAUNCHED',
'UNPAIRED',
'UNPAIRED_IDLE',
]);
const allowedStreams = new Set([
'CONNECTED',
'DISCONNECTED',
'OPENING',
'PAIRING',
'RESUMING',
'SYNCING',
'UNPAIRED',
]);
const normalize = (value, allowed) => {
const normalized = String(value ?? 'unknown');
return allowed.has(normalized) ? normalized : 'unknown';
};
let commsInitialized = false;
let socketConnected = false;
let inboundAge;
let reconnectListenerCount = 0;
try {
commsInitialized = comms?.isCommsInitialized?.() === true;
socketConnected = comms?.isSocketConnected?.() === true;
inboundAge = comms?.getMsSinceLastInboundRx?.();
reconnectListenerCount = backendEventBus?.getListenersCount?.(
backendEvent?.RECONNECT_SOCKET,
);
} catch {
// Safe false/unknown defaults are diagnostic-only.
}
const inboundRxBucket =
!Number.isFinite(inboundAge) || inboundAge < 0
? 'unavailable'
: inboundAge < 5000
? 'lt_5s'
: inboundAge < 30000
? 'lt_30s'
: inboundAge < 120000
? 'lt_2m'
: 'gte_2m';
const socketState = normalize(
socket?.state ?? socket?.get?.('state'),
allowedSocketStates,
);
const pairingRefPresent = Boolean(conn?.ref);
const pairingRefActive =
pairingRefPresent &&
(multiDevice?.isRegistered?.() !== true ||
['UNPAIRED', 'UNPAIRED_IDLE'].includes(socketState));
return {
document_epoch_valid:
Number.isFinite(expectedDocumentEpoch) &&
window.performance?.timeOrigin === expectedDocumentEpoch,
online: navigator.onLine === true,
socket_state: socketState,
socket_stream: normalize(
socket?.stream ?? socket?.get?.('stream'),
allowedStreams,
),
has_synced: socket?.hasSynced === true,
comms_initialized: commsInitialized,
socket_connected: socketConnected,
inbound_rx_bucket: inboundRxBucket,
backend_socket_state: normalize(
backendEventBus?.socketState,
allowedSocketStates,
),
reconnect_listener_count:
Number.isSafeInteger(reconnectListenerCount) &&
reconnectListenerCount >= 0 &&
reconnectListenerCount <= 100
? reconnectListenerCount
: -1,
runtime_bootstrap_failed:
window.__WWebCanonicalRegisteredLaunchStateV1 === 'failed',
guard_active: bridgeState?.canonicalCredentialGuardActive === true,
credential_protection_installed:
bridgeState?.canonicalCredentialProtectionInstalled === true,
destructive_credential_phase:
bridgeState?.canonicalDestructiveCredentialPhase || 'unknown',
destructive_credential_acknowledged:
bridgeState?.canonicalDestructiveCredentialAcknowledged === true,
destructive_credential_violation_detected:
bridgeState?.canonicalDestructiveCredentialViolationDetected ===
true,
pairing_watchdog_active:
bridgeState?.canonicalPairingWatchdogActive === true,
pairing_detected: bridgeState?.canonicalPairingDetected === true,
// Conn.ref can remain populated after a registered companion has
// consumed it. Preserve the raw signal for diagnostics, but only an
// unregistered/unpaired reference is an active pairing condition.
pairing_ref_present: pairingRefPresent,
pairing_ref_active: pairingRefActive,
};
}
const CANONICAL_TRANSPORT_NATIVE_STALL_MS = 15000;
function isCanonicalTransportNativeStall(health) {
const transportProjectionStalled =
health?.socket_stream === 'DISCONNECTED' ||
(health?.socket_stream === 'CONNECTED' && health?.has_synced === true);
return Boolean(
health?.document_epoch_valid === true &&
health.online === true &&
health.socket_state === 'CONNECTED' &&
health.backend_socket_state === 'CONNECTED' &&
transportProjectionStalled &&
typeof health.has_synced === 'boolean' &&
health.comms_initialized === false &&
health.socket_connected === false &&
health.inbound_rx_bucket === 'unavailable' &&
Number.isSafeInteger(health.reconnect_listener_count) &&
health.reconnect_listener_count > 0 &&
health.runtime_bootstrap_failed === false &&
health.guard_active === true &&
health.credential_protection_installed === true &&
health.destructive_credential_phase === 'live' &&
health.destructive_credential_acknowledged === true &&
health.destructive_credential_violation_detected === false &&
health.pairing_watchdog_active === true &&
health.pairing_detected === false &&
health.pairing_ref_active === false,
);
}
/* This function is serialized by Puppeteer and executes in the page realm. */
function installBrowserSessionBridge(options) {
if (window.__WWebSessionBridge?.schemaVersion === options.schemaVersion) {
window.__WWebSessionBridge.ensureAdvCapture?.();
window.__WWebSessionBridge.ensureConnectionDiagnostics?.();
window.__WWebSessionBridge.ensurePostQuantumUploadLifecycleGate?.();
window.__WWebSessionBridge.startPostQuantumUploadLifecycleGate?.();
return;
}
const events = [];
let dirty = false;
let critical = false;
let dropped = 0;
const record = (event) => {
dirty = true;
critical ||= event.critical === true;
if (events.length < options.maxJournalEvents) {
events.push({ ...event, timestamp: Date.now() });
} else {
dropped += 1;
}
if (event.critical && window.__wwebjsSessionJournalNotify) {
void window.__wwebjsSessionJournalNotify().catch(() => {});
}
};
const wrapMethod = (target, method, wrapper) => {
if (!target?.[method] || target[method].__wwebjsSessionWrapped) return;
const original = target[method];
const wrapped = wrapper(original);
Object.defineProperty(wrapped, '__wwebjsSessionWrapped', {
value: true,
});
target[method] = wrapped;
};
const criticalName = (name) =>
/(app.?state|crypto|identity|key|pre.?key|sender|session|signal)/i.test(
String(name || ''),
);
// Canonical hydration replaces private stores while the page and network
// remain quiesced. WhatsApp Web can clear the registered tuple when lazy
// transport records are not present yet. The short-lived guard keeps that
// local cleanup from racing verified rehydration and is disarmed only after
// the official socket reconnect has validated the imported identity.
const canonicalCredentialGuardKey =
'__wwebjs_canonical_rehydration_guard_v1';
const canonicalCredentialGuardGlobalKey =
'__WWebCanonicalRehydrationGuardUntilV1';
const canonicalDestructiveCredentialPhaseGlobalKey =
'__WWebCanonicalDestructiveCredentialPhaseV1';
const canonicalCredentialGuardMaxTtlMs = 300000;
const canonicalSocketLogoutBootstrapGraceMs = 5000;
let canonicalCredentialSuppressionCount = 0;
let canonicalSocketLogoutBootstrapGraceUntil = 0;
let canonicalSocketLogoutBootstrapSuppressionCount = 0;
let canonicalDestructiveCredentialPhase = 'inactive';
let canonicalDestructiveCredentialAcknowledged = false;
let canonicalDestructiveCredentialViolationDetected = false;
const syncCanonicalDestructiveCredentialPhase = () => {
try {
const globalPhase =
window[canonicalDestructiveCredentialPhaseGlobalKey];
if (
['suppressing', 'sealed', 'live'].includes(globalPhase) &&
!(
globalPhase === 'suppressing' &&
canonicalDestructiveCredentialAcknowledged === true
)
) {
canonicalDestructiveCredentialPhase = globalPhase;
}
const navigationState =
window.__WWebCanonicalNavigationStorageGuardV1?.state?.();
if (navigationState?.violationDetected === true) {
canonicalDestructiveCredentialViolationDetected = true;
}
} catch {
// The verified page-local phase remains authoritative.
}
return canonicalDestructiveCredentialPhase;
};
const canonicalCredentialGuardActive = () => {
const now = Date.now();
const validExpiry = (value) =>
Number.isSafeInteger(value) &&
value > now &&
value <= now + canonicalCredentialGuardMaxTtlMs;
let storageExpiry;
let storagePresent = false;
try {
const storageValue = sessionStorage.getItem(
canonicalCredentialGuardKey,
);
storagePresent = storageValue !== null;
storageExpiry = storageValue === null ? 0 : Number(storageValue);
} catch {
// about:blank can have an opaque origin that denies sessionStorage.
// The non-enumerable window marker below remains authoritative for
// this document and the pre-navigation script seeds every new one.
}
let globalExpiry;
let globalPresent = false;
try {
globalPresent = Object.prototype.hasOwnProperty.call(
window,
canonicalCredentialGuardGlobalKey,
);
globalExpiry = Number(window[canonicalCredentialGuardGlobalKey]);
} catch {
// An inaccessible global is treated as absent.
}
const storageValid = validExpiry(storageExpiry);
const globalValid = validExpiry(globalExpiry);
if (storagePresent && !storageValid) {
try {
sessionStorage.removeItem(canonicalCredentialGuardKey);
} catch {
// Inaccessible storage is already unusable as a guard source.
}
}
if (globalPresent && !globalValid) {
try {
delete window[canonicalCredentialGuardGlobalKey];
} catch {
// A non-configurable invalid marker cannot prove the guard.
}
}
return storageValid || globalValid;
};
const armCanonicalCredentialGuard = (ttlMs) => {
if (
!Number.isSafeInteger(ttlMs) ||
ttlMs < 1000 ||
ttlMs > canonicalCredentialGuardMaxTtlMs
) {
return false;
}
const expiresAt = Date.now() + ttlMs;
try {
Object.defineProperty(window, canonicalCredentialGuardGlobalKey, {
configurable: true,
enumerable: false,
writable: true,
value: expiresAt,
});
} catch {
// sessionStorage can still establish the current-document guard.
}
canonicalDestructiveCredentialPhase = 'suppressing';
canonicalDestructiveCredentialAcknowledged = false;
canonicalDestructiveCredentialViolationDetected = false;
canonicalSocketLogoutBootstrapGraceUntil = 0;
canonicalSocketLogoutBootstrapSuppressionCount = 0;
try {
Object.defineProperty(
window,
canonicalDestructiveCredentialPhaseGlobalKey,
{
configurable: true,
enumerable: false,
writable: true,
value: 'suppressing',
},
);
} catch {
return false;
}
try {
sessionStorage.setItem(
canonicalCredentialGuardKey,
String(expiresAt),
);
} catch {
// Expected for about:blank with an opaque origin. The window
// marker is sufficient and is independently verified below.
}
startCanonicalCredentialProtection();
return canonicalCredentialGuardActive();
};
const disarmCanonicalCredentialGuard = () => {
canonicalDestructiveCredentialPhase = 'inactive';
canonicalDestructiveCredentialAcknowledged = false;
canonicalSocketLogoutBootstrapGraceUntil = 0;
canonicalSocketLogoutBootstrapSuppressionCount = 0;
try {
window.__WWebCanonicalNavigationStorageGuardV1?.disarm?.();
} catch {
// The page-level marker is still removed below.
}
try {
sessionStorage.removeItem(canonicalCredentialGuardKey);
} catch {
// A missing or unavailable sessionStorage is already disarmed.
}
try {
delete window[canonicalCredentialGuardGlobalKey];
} catch {
// A missing global marker already means disarmed.
}
try {
delete window[canonicalDestructiveCredentialPhaseGlobalKey];
} catch {
// A missing phase marker already means inactive.
}
};
let canonicalPairingWatchdogActive = false;
let canonicalPairingDetected = false;
let canonicalPairingWatchdogSocket;
let canonicalPairingWatchdogListener;
let canonicalPairingWatchdogConn;
let canonicalPairingWatchdogRefListener;
const isCanonicalPairingRefActive = (value) => {
if (!value) return false;
try {
const socket = window.require?.('WAWebSocketModel')?.Socket;
const multiDevice = window.require?.('WAWebUserPrefsMultiDevice');
const socketState = String(
socket?.state ?? socket?.get?.('state') ?? 'unknown',
);
// Some WA Web builds retain the last successfully consumed
// Conn.ref after the registered companion is already connected.
// Registration plus the socket state are authoritative; the
// retained value alone must not latch a false pairing failure.
return (
multiDevice?.isRegistered?.() !== true ||
['UNPAIRED', 'UNPAIRED_IDLE'].includes(socketState)
);
} catch {
// A ref without enough runtime evidence to prove a registered
// transport remains fail-closed.
return true;
}
};
const disarmCanonicalPairingWatchdog = () => {
canonicalPairingWatchdogSocket?.off?.(
'change:state',
canonicalPairingWatchdogListener,
);
canonicalPairingWatchdogSocket = undefined;
canonicalPairingWatchdogListener = undefined;
canonicalPairingWatchdogConn?.off?.(
'change:ref',
canonicalPairingWatchdogRefListener,
);
canonicalPairingWatchdogConn = undefined;
canonicalPairingWatchdogRefListener = undefined;
canonicalPairingWatchdogActive = false;
};
const armCanonicalPairingWatchdog = () => {
disarmCanonicalPairingWatchdog();
let socket;
try {
socket = window.require?.('WAWebSocketModel')?.Socket;
} catch {
return false;
}
if (
typeof socket?.on !== 'function' ||
typeof socket?.off !== 'function'
) {
return false;
}
canonicalPairingDetected = false;
canonicalPairingWatchdogActive = true;
canonicalPairingWatchdogSocket = socket;
canonicalPairingWatchdogListener = (_model, value) => {
if (
canonicalPairingWatchdogActive &&
['UNPAIRED', 'UNPAIRED_IDLE'].includes(String(value))
) {
canonicalPairingDetected = true;
}
};
socket.on('change:state', canonicalPairingWatchdogListener);
try {
const conn = window.require?.('WAWebConnModel')?.Conn;
if (isCanonicalPairingRefActive(conn?.ref)) {
canonicalPairingDetected = true;
}
if (
typeof conn?.on === 'function' &&
typeof conn?.off === 'function'
) {
canonicalPairingWatchdogConn = conn;
canonicalPairingWatchdogRefListener = (_model, value) => {
if (
canonicalPairingWatchdogActive &&
isCanonicalPairingRefActive(value)
) {
canonicalPairingDetected = true;
}
};
conn.on('change:ref', canonicalPairingWatchdogRefListener);
}
} catch {
// Socket-state monitoring remains authoritative when Conn is not
// exposed by a compatible build.
}
return true;
};
let advCaptureTimer;
let advCaptureTarget;
let advCaptureOriginal;
let advCaptureWrapped;
const normalizeAdvSecret = (value) => {
if (typeof value !== 'string') return undefined;
const normalized = value.trim().replace(/-/g, '+').replace(/_/g, '/');
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(normalized)) return undefined;
const padded = normalized.padEnd(
normalized.length + ((4 - (normalized.length % 4)) % 4),
'=',
);
try {
return atob(padded).length === 32 ? padded : undefined;
} catch {
return undefined;
}
};
const notifyAdvSecret = (value) => {
const normalized = normalizeAdvSecret(value);
if (!normalized || !window.__wwebjsAdvSecretCapture) return;
void window.__wwebjsAdvSecretCapture(normalized).catch(() => {});
};
const ensureAdvCapture = () => {
if (advCaptureWrapped) return true;
try {
const multiDevice = window.require?.('WAWebUserPrefsMultiDevice');
const original = multiDevice?.setADVSecretKey;
if (typeof original !== 'function') return false;
const wrapped = function (...args) {
notifyAdvSecret(args[0]);
return original.apply(this, args);
};
Object.defineProperty(wrapped, '__wwebjsAdvCaptureWrapped', {
value: true,
});
multiDevice.setADVSecretKey = wrapped;
if (multiDevice.setADVSecretKey !== wrapped) return false;
advCaptureTarget = multiDevice;
advCaptureOriginal = original;
advCaptureWrapped = wrapped;
void Promise.resolve(multiDevice.getADVSecretKey?.())
.then(notifyAdvSecret)
.catch(() => {});
return true;
} catch {
return false;
}
};
const startAdvCapture = () => {
if (ensureAdvCapture() || advCaptureTimer) return;
advCaptureTimer = setInterval(() => {
if (ensureAdvCapture()) {
clearInterval(advCaptureTimer);
advCaptureTimer = undefined;
}
}, 250);
};
const stopAdvCapture = () => {
clearInterval(advCaptureTimer);
advCaptureTimer = undefined;
if (
advCaptureTarget &&
advCaptureTarget.setADVSecretKey === advCaptureWrapped
) {
advCaptureTarget.setADVSecretKey = advCaptureOriginal;
}
advCaptureTarget = undefined;
advCaptureOriginal = undefined;
advCaptureWrapped = undefined;
};
let connectionDiagnosticsTimer;
let connectionDiagnosticsInstalled = false;
let canonicalSocketLogoutObserver;
const connectionDiagnosticRestores = [];
const connectionDiagnosticListeners = [];
const notifyConnectionDiagnostic = (kind, category) => {
if (!options.enableConnectionDiagnostics) return;
if (!window.__wwebjsSessionConnectionDiagnosticNotify) return;
void window
.__wwebjsSessionConnectionDiagnosticNotify({ kind, category })
.catch(() => {});
};
let canonicalCredentialProtectionTimer;
const canonicalCredentialProtectionTargets = new Map();
const resolveModule = (names) => {
for (const name of Array.isArray(names) ? names : [names]) {
try {
const value = window.require?.(name);
const resolved = value?.default || value;
if (resolved) return resolved;
} catch {
// Try the next verified module alias.
}
}
return undefined;
};
let postQuantumUploadLifecycleGateTimer;
let postQuantumUploadLifecycleGate;
const postQuantumUploadLifecycleSpecs = [
{
key: 'job',
module: 'WAWebUploadPQPrekeysJob',
method: 'generateAndUploadPQPreKeys',
kind: 'job',
isPq: () => true,
},
{
key: 'set_rpc',
module: 'WASmaxPreKeysSetRPC',
method: 'sendSetRPC',
kind: 'sink',
isPq: (args) =>
Boolean(
args[0]?.identityKeyOrPQKeyBundleMixinGroupArgs
?.pQKeyBundle,
),
},
{
key: 'add_rpc',
module: 'WASmaxPreKeysAddRPC',
method: 'sendAddRPC',
kind: 'sink',
isPq: (args) => Boolean(args[0]?.pQPreKeyListMixinArgs),
},
];
const ensurePostQuantumUploadLifecycleGate = () => {
if (postQuantumUploadLifecycleGate) {
return [...postQuantumUploadLifecycleGate.entries.values()].every(
(entry) => entry.target?.[entry.method] === entry.wrapped,
);
}
const resolved = [];
for (const spec of postQuantumUploadLifecycleSpecs) {
const target = resolveModule(spec.module);
const original = target?.[spec.method];
if (typeof original !== 'function') return false;
resolved.push({ ...spec, target, original });
}
const gate = {
phase: 'open',
active: new Set(),
entries: new Map(),
started: 0,
settled: 0,
jobStarted: 0,
blocked: 0,
recoveryPermit: false,
recoverySinkStarted: 0,
recoverySinkSettled: 0,
recoverySinkFailed: 0,
recoveryAttempt: undefined,
};
const restoreInstalledEntries = () => {
for (const installed of gate.entries.values()) {
try {
if (
installed.target?.[installed.method] ===
installed.wrapped
) {
installed.target[installed.method] = installed.original;
}
} catch {
// The failed installation remains unavailable and the ABI
// preflight will keep the session closed.
}
}
};
const blockedSinkResult = () => {
const resultOrError = resolveModule('WAResultOrError');
const fallback = { success: false, error: 'request-error' };
if (typeof resultOrError?.makeError !== 'function') {
return Promise.resolve(fallback);
}
try {
return Promise.resolve(
resultOrError.makeError('request-error'),
).catch(() => fallback);
} catch {
return Promise.resolve(fallback);
}
};
const track = (result, kind) => {
const tracked = Promise.resolve(result);
gate.active.add(tracked);
if (kind === 'sink') gate.started += 1;
else gate.jobStarted += 1;
const settle = () => {
if (gate.active.delete(tracked) && kind === 'sink') {
gate.settled += 1;
}
};
tracked.then(settle, settle);
return result;
};
for (const resolvedSpec of resolved) {
const entry = { ...resolvedSpec, wrapped: undefined };
const wrapped = function (...args) {
if (!entry.isPq(args)) {
return entry.original.apply(this, args);
}
if (gate.phase !== 'open') {
if (entry.kind === 'sink' && gate.recoveryPermit === true) {
// One exclusive sink call is admitted for source
// recovery. Normal jobs remain blocked at the job
// wrapper, and the permit is consumed synchronously.
const attempt = gate.recoveryAttempt;
gate.recoveryPermit = false;
gate.recoverySinkStarted += 1;
attempt.captureCount += 1;
gate.phase = 'recovery-settling';
let result;
try {
result = entry.original.apply(this, args);
} catch (error) {
gate.started += 1;
gate.settled += 1;
gate.recoverySinkSettled += 1;
gate.recoverySinkFailed += 1;
attempt.settleCount += 1;
attempt.failed = true;
gate.phase = 'recovery-failed';
throw error;
}
const tracked = track(result, 'sink');
Promise.resolve(tracked).then(
() => {
gate.recoverySinkSettled += 1;
attempt.settleCount += 1;
},
() => {
gate.recoverySinkSettled += 1;
gate.recoverySinkFailed += 1;
attempt.settleCount += 1;
attempt.failed = true;
gate.phase = 'recovery-failed';
},
);
return tracked;
}
if (
entry.kind === 'sink' &&
gate.recoveryAttempt &&
gate.recoveryAttempt.validated !== true
) {
gate.recoveryAttempt.blockedSinkCount += 1;
gate.recoveryAttempt.failed = true;
gate.phase = 'recovery-failed';
} else if (
entry.kind === 'sink' &&
gate.recoveryAttempt?.validated === true
) {
// Keep the fence fail-closed throughout the caller's
// durable-state validation window. A captured old job
// that reaches a sink after the admitted upload makes
// the completed recovery attempt invalid.
gate.recoveryAttempt.blockedSinkCount += 1;
gate.recoveryAttempt.failed = true;
gate.phase = 'recovery-failed';
}
gate.blocked += 1;
return entry.kind === 'sink'
? blockedSinkResult()
: Promise.resolve(undefined);
}
return track(entry.original.apply(this, args), entry.kind);
};
Object.defineProperty(wrapped, 'length', {
configurable: true,
value: entry.original.length,
});
Object.defineProperty(wrapped, '__wwebjsPqUploadLifecycleWrapped', {
value: true,
});
entry.wrapped = wrapped;
try {
entry.target[entry.method] = wrapped;
} catch {
restoreInstalledEntries();
return false;
}
if (entry.target[entry.method] !== wrapped) {
restoreInstalledEntries();
return false;
}
gate.entries.set(entry.key, entry);
}
postQuantumUploadLifecycleGate = gate;
return true;
};
const startPostQuantumUploadLifecycleGate = () => {
if (
ensurePostQuantumUploadLifecycleGate() ||
postQuantumUploadLifecycleGateTimer
) {
return;
}
// Install as soon as the private module is registered. This runs from
// evaluateOnNewDocument and therefore observes uploads long before a
// provider handoff can be requested.
postQuantumUploadLifecycleGateTimer = setInterval(() => {
if (ensurePostQuantumUploadLifecycleGate()) {
clearInterval(postQuantumUploadLifecycleGateTimer);
postQuantumUploadLifecycleGateTimer = undefined;
}
}, 10);
};
const beginPostQuantumRollbackUploadFence = async (timeoutMs) => {
if (!ensurePostQuantumUploadLifecycleGate()) {
throw new Error('wwebjs_pq_upload_lifecycle_gate_unavailable');
}
const gate = postQuantumUploadLifecycleGate;
if (
!ensurePostQuantumUploadLifecycleGate() ||
!['open', 'rollback'].includes(gate.phase)
) {
throw new Error('wwebjs_pq_upload_lifecycle_gate_tampered');
}
gate.phase = 'rollback';
const deadline = Date.now() + timeoutMs;
while (gate.active.size > 0) {
const remainingMs = deadline - Date.now();
if (remainingMs <= 0) {
throw new Error('wwebjs_pq_upload_drain_timeout');
}
let timer;
const timeout = new Promise((_, reject) => {
timer = setTimeout(
() => reject(new Error('wwebjs_pq_upload_drain_timeout')),
remainingMs,
);
});
try {
await Promise.race([
Promise.allSettled([...gate.active]),
timeout,
]);
} finally {
clearTimeout(timer);
}
}
return {
phase: gate.phase,
active: gate.active.size,
started: gate.started,
settled: gate.settled,
blocked: gate.blocked,
};
};
const recoverPostQuantumThroughUploadLifecycleGate = async () => {
if (!ensurePostQuantumUploadLifecycleGate()) {
throw new Error('wwebjs_pq_upload_lifecycle_gate_unavailable');
}
const gate = postQuantumUploadLifecycleGate;
if (gate.phase !== 'rollback' || gate.active.size !== 0) {
throw new Error('wwebjs_pq_upload_lifecycle_gate_tampered');
}
const job = gate.entries.get('job');
if (!job) {
throw new Error('wwebjs_pq_upload_lifecycle_gate_unavailable');
}
const attempt = {
captureCount: 0,
settleCount: 0,
blockedSinkCount: 0,
failed: false,
validated: false,
};
const previousRecoverySinkStarted = gate.recoverySinkStarted;
const previousRecoverySinkSettled = gate.recoverySinkSettled;
const previousRecoverySinkFailed = gate.recoverySinkFailed;
gate.recoveryAttempt = attempt;
gate.phase = 'recovery-admitting';
// This private job has no invocation token that survives its async
// path to the RPC sink. Do not treat the first caller as authoritative:
// the one-shot admission is accepted only if this job also completes
// with exactly one settled sink and no other sink reaches the fence.
// A captured old job can therefore deny recovery, never validate it.
gate.recoveryPermit = true;
let jobFailed = false;
try {
await job.original.call(job.target);
} catch {
jobFailed = true;
} finally {
gate.recoveryPermit = false;
}
if (
jobFailed ||
gate.active.size !== 0 ||
gate.phase !== 'recovery-settling' ||
attempt.failed ||
attempt.captureCount !== 1 ||
attempt.settleCount !== 1 ||
attempt.blockedSinkCount !== 0 ||
gate.recoverySinkStarted !== previousRecoverySinkStarted + 1 ||
gate.recoverySinkSettled !== previousRecoverySinkSettled + 1 ||
gate.recoverySinkFailed !== previousRecoverySinkFailed
) {
attempt.failed = true;
gate.phase = 'recovery-failed';
throw new Error('wwebjs_pq_recovery_upload_sink_unproven');
}
attempt.validated = true;
gate.phase = 'rollback';
return postQuantumUploadLifecycleGateState();
};
const resumePostQuantumUploadLifecycleGate = (
allowWithoutUpload = false,
) => {
if (!ensurePostQuantumUploadLifecycleGate()) {
throw new Error('wwebjs_pq_upload_lifecycle_gate_unavailable');
}
const gate = postQuantumUploadLifecycleGate;
const attempt = gate.recoveryAttempt;
const rolloutDisabledRecovery =
allowWithoutUpload === true &&
gate.phase === 'rollback' &&
gate.active.size === 0 &&
attempt === undefined;
if (
!rolloutDisabledRecovery &&
(gate.phase !== 'rollback' ||
gate.active.size !== 0 ||
attempt?.validated !== true ||
attempt.failed ||
attempt.captureCount !== 1 ||
attempt.settleCount !== 1 ||
attempt.blockedSinkCount !== 0)
) {
throw new Error('wwebjs_pq_upload_lifecycle_gate_tampered');
}
gate.phase = 'open';
gate.recoveryAttempt = undefined;
return true;
};
const postQuantumUploadLifecycleGateState = () => {
const gate = postQuantumUploadLifecycleGate;
return {
installed:
Boolean(gate) &&
[...gate.entries.values()].every(
(entry) => entry.target?.[entry.method] === entry.wrapped,
),
phase: gate?.phase || 'unavailable',
active: gate?.active.size || 0,
started: gate?.started || 0,
settled: gate?.settled || 0,
job_started: gate?.jobStarted || 0,
blocked: gate?.blocked || 0,
recovery_capture_count: gate?.recoveryAttempt?.captureCount || 0,
recovery_settle_count: gate?.recoveryAttempt?.settleCount || 0,
recovery_blocked_sink_count:
gate?.recoveryAttempt?.blockedSinkCount || 0,
recovery_failed: Boolean(
gate?.phase === 'recovery-failed' ||
gate?.recoveryAttempt?.failed === true,
),
sink_count: gate?.entries
? [...gate.entries.values()].filter(
(entry) => entry.kind === 'sink',
).length
: 0,
};
};
const stopPostQuantumUploadLifecycleGate = () => {
clearInterval(postQuantumUploadLifecycleGateTimer);
postQuantumUploadLifecycleGateTimer = undefined;
const gate = postQuantumUploadLifecycleGate;
// A rollback fence is intentionally sticky until the document is
// destroyed. Restoring the original while Chromium is still winding
// down would recreate the post-ACK re-upload race.
if (gate?.phase === 'open') {
for (const entry of gate.entries.values()) {
if (entry.target?.[entry.method] === entry.wrapped) {
entry.target[entry.method] = entry.original;
}
}
}
postQuantumUploadLifecycleGate = undefined;
};
const classifyCanonicalLogoutReason = (value) => {
let logoutReasonConstants;
try {
logoutReasonConstants = window.require?.(
'WAWebLogoutReasonConstants',
);
} catch {
logoutReasonConstants = undefined;
}
let candidate = value;
let enumName;
try {
const reasonEnum = logoutReasonConstants?.LogoutReason;
if (
reasonEnum?.isValid?.(candidate) === true &&
typeof reasonEnum?.getName?.(candidate) === 'string'
) {
enumName = reasonEnum.getName(candidate);
}
} catch {
enumName = undefined;
}
if (enumName) {
if (enumName === 'UserInitiated') return 'user_initiated';
if (/Storage|Salt|Cache/iu.test(enumName)) {
return 'local_storage_failure';
}
if (/Sync|History/iu.test(enumName)) return 'sync_failure';
if (/LidMigration|LidBlocklist/iu.test(enumName)) {
return 'lid_migration_failure';
}
return (
{
UnknownCompanion: 'unknown_companion',
PrimaryIdentityKeyChange: 'primary_identity_key_change',
InvalidAdvStatus: 'invalid_adv_status',
ClientVersionOutdated: 'client_version_outdated',
MDOptOut: 'md_opt_out',
AccountLocked: 'account_locked',
}[enumName] || 'other_known'
);
}
if (candidate && typeof candidate === 'object') {
for (const field of ['reason', 'logoutReason', 'type']) {
try {
if (typeof candidate[field] === 'string') {
candidate = candidate[field];
break;
}
} catch {
// Treat inaccessible reason objects as unknown.
}
}
}
if (typeof candidate !== 'string') return 'unknown';
const known = new Set(
Object.values(logoutReasonConstants?.LogoutReason || {}).filter(
(entry) => typeof entry === 'string',
),
);
if (!known.has(candidate)) return 'unknown';
if (
[
'unknown_companion',
'primary_identity_key_change',
'invalid_adv_status',
'client_version_outdated',
'md_opt_out',
'account_locked',
].includes(candidate)
) {
return candidate;
}
if (/^lid_/u.test(candidate)) return 'lid_migration_failure';
if (/sync|history/iu.test(candidate)) return 'sync_failure';
if (/storage|salt/iu.test(candidate)) {
return 'local_storage_failure';
}
if (candidate === 'user_initiated') return 'user_initiated';
return 'other_known';
};
const canonicalCredentialProtectionSpecs = [
{
key: 'socket.logout',
module: 'WAWebSocketModel',
target: (value) => value?.Socket,
method: 'logout',
arity: 1,
category: 'socket_logout',
suppressValue: () => undefined,
genuineLogout: true,
},
{
key: 'socket.clearCredentials',
module: 'WAWebSocketModel',
target: (value) => value?.Socket,
method: 'clearCredentials',
arity: 0,
category: 'socket_clear_credentials',
suppressValue: () => Promise.resolve(false),
promiseResult: true,
},
{
key: 'socket.clearCredentialsAndStoredData',
module: 'WAWebSocketModel',
target: (value) => value?.Socket,
method: 'clearCredentialsAndStoredData',
arity: 2,
category: 'socket_clear_credentials_and_stored_data',
suppressValue: () => Promise.resolve(),
promiseResult: true,
},
{
key: 'companion.startLogout',
module: 'WAWebCompanionRegUtils',
target: (value) => value,
method: 'startLogout',
arity: 0,
category: 'companion_start_logout',
suppressValue: () => Promise.resolve(),
promiseResult: true,
genuineLogout: true,
},
{
key: 'companion.logoutAfterValidationFail',
module: 'WAWebCompanionRegUtils',
target: (value) => value,
method: 'logoutAfterValidationFail',
arity: 0,
category: 'companion_logout_after_validation_fail',
suppressValue: () => undefined,
genuineLogout: true,
},
{
key: 'logoutJob.socketLogout',
module: 'WAWebSocketLogoutJob',
target: (value) => value,
method: 'socketLogout',
arity: 1,
category: 'socket_logout_job',
suppressValue: () => Promise.resolve(),
promiseResult: true,
genuineLogout: true,
},
{
key: 'signalStorage.destroy',
module: 'WAWebSignalStorage',
target: (value) => value,
method: 'destroy',
arity: 0,
category: 'signal_storage_destroy',
suppressValue: () => Promise.resolve(),
promiseResult: true,
},
{
key: 'cryptoKeys.deleteEncKeysAndCache',
module: ['CryptoEncKeyHelper', 'WAWebCryptoEncKeyHelper'],
target: (value) => value,
method: 'deleteEncKeysAndCache',
arity: 0,
category: 'crypto_keys_delete',
suppressValue: () => Promise.resolve(),
promiseResult: true,
},
{
key: 'cryptoSalt.deleteSalt',
module: [
'CryptoEncryptionSaltHelper',
'WAWebCryptoEncryptionSaltHelper',
],
target: (value) => value,
method: 'deleteSalt',
arity: 0,
category: 'crypto_salt_delete',
suppressValue: () => Promise.resolve(),
promiseResult: true,
},
{
key: 'localState.clearAllLocalState',
module: 'WAWebUserPrefsClearLocalState',
target: (value) => value,
method: 'clearAllLocalState',
arity: 0,
category: 'clear_all_local_state',
suppressValue: () => undefined,
},
];
const notifyCanonicalCredentialViolation = (category) => {
canonicalDestructiveCredentialViolationDetected = true;
notifyConnectionDiagnostic('credential_clear', `${category}_detected`);
if (!window.__wwebjsCanonicalCredentialViolationNotify) {
return Promise.resolve(false);
}
return Promise.resolve(
window.__wwebjsCanonicalCredentialViolationNotify({ category }),
).catch(() => false);
};
const observeCanonicalCredentialOperation = (
category,
{
suppressValue,
promiseResult = false,
genuineLogout = false,
reasonCategory,
} = {},
) => {
const guardActive = canonicalCredentialGuardActive();
const phase = syncCanonicalDestructiveCredentialPhase();
if (guardActive && phase === 'suppressing') {
canonicalCredentialSuppressionCount += 1;
notifyConnectionDiagnostic(
'credential_clear',
`${category}_suppressed`,
);
return {
suppress: true,
returnValue: suppressValue?.(),
};
}
if (guardActive && phase === 'sealed') {
canonicalCredentialSuppressionCount += 1;
return {
suppress: true,
returnValue: suppressValue?.(),
notification: notifyCanonicalCredentialViolation(category),
};
}
// The registered socket may enqueue one stale, reasonless logout job
// while the imported transport moves from OPENING to CONNECTED. It is
// a bootstrap race, not an authenticated server logout: allowing it
// destroys the freshly imported tuple and forces a full process retry.
// Suppress exactly one such job in a narrow post-release window. Known
// logout reasons, every other credential operation, a second job, and
// anything outside the window keep the normal fail-closed behavior.
if (
guardActive &&
phase === 'live' &&
category === 'socket_logout_job' &&
reasonCategory === 'unknown' &&
canonicalSocketLogoutBootstrapGraceUntil > Date.now() &&
canonicalSocketLogoutBootstrapSuppressionCount === 0
) {
canonicalCredentialSuppressionCount += 1;
canonicalSocketLogoutBootstrapSuppressionCount = 1;
notifyConnectionDiagnostic(
'credential_clear',
`${category}_suppressed`,
);
return {
suppress: true,
returnValue: suppressValue?.(),
};
}
if (
guardActive &&
canonicalDestructiveCredentialAcknowledged === true &&
phase === 'live'
) {
const notification = notifyCanonicalCredentialViolation(category);
if (genuineLogout) disarmCanonicalCredentialGuard();
return { notification, promiseResult };
}
return {};
};
const installCanonicalCredentialProtectionSpec = (spec) => {
const moduleValue = resolveModule(spec.module);
const target = spec.target(moduleValue);
const original = target?.[spec.method];
const existing = canonicalCredentialProtectionTargets.get(spec.key);
if (
existing &&
existing.target === target &&
original === existing.wrapped
) {
return true;
}
if (typeof original !== 'function' || original.length !== spec.arity) {
return false;
}
const wrapped = function (...args) {
let reasonCategory;
if (
spec.key === 'socket.logout' ||
spec.key === 'logoutJob.socketLogout'
) {
reasonCategory = classifyCanonicalLogoutReason(args[0]);
canonicalSocketLogoutObserver?.(args, reasonCategory);
}
const observation = observeCanonicalCredentialOperation(
spec.category,
{ ...spec, reasonCategory },
);
if (observation.suppress === true) {
return observation.returnValue;
}
if (observation.notification && observation.promiseResult) {
return observation.notification.then(() =>
original.apply(this, args),
);
}
const result = original.apply(this, args);
void observation.notification;
return result;
};
Object.defineProperty(wrapped, 'length', {
configurable: true,
value: original.length,
});
Object.defineProperty(
wrapped,
'__wwebjsCanonicalCredentialProtectionWrapped',
{ value: true },
);
target[spec.method] = wrapped;
if (target[spec.method] !== wrapped) return false;
canonicalCredentialProtectionTargets.set(spec.key, {
target,
method: spec.method,
original,
wrapped,
});
return true;
};
const ensureCanonicalCredentialProtection = () => {
let installed = 0;
for (const spec of canonicalCredentialProtectionSpecs) {
if (installCanonicalCredentialProtectionSpec(spec)) installed += 1;
}
return installed === canonicalCredentialProtectionSpecs.length;
};
const startCanonicalCredentialProtection = () => {
if (
ensureCanonicalCredentialProtection() ||
canonicalCredentialProtectionTimer
) {
return;
}
canonicalCredentialProtectionTimer = setInterval(() => {
if (ensureCanonicalCredentialProtection()) {
clearInterval(canonicalCredentialProtectionTimer);
canonicalCredentialProtectionTimer = undefined;
}
}, 10);
};
const acknowledgeCanonicalCredentialProtection = () => {
syncCanonicalDestructiveCredentialPhase();
if (
!canonicalCredentialGuardActive() ||
canonicalDestructiveCredentialPhase !== 'suppressing' ||
canonicalDestructiveCredentialViolationDetected ||
!ensureCanonicalCredentialProtection()
) {
return false;
}
const navigationGuard = window.__WWebCanonicalNavigationStorageGuardV1;
if (navigationGuard?.acknowledgeDestructiveProtection?.() !== true) {
return false;
}
try {
window[canonicalDestructiveCredentialPhaseGlobalKey] = 'sealed';
} catch {
return false;
}
canonicalDestructiveCredentialPhase = 'sealed';
canonicalDestructiveCredentialAcknowledged = true;
canonicalCredentialSuppressionCount = 0;
const navigationState = navigationGuard.state?.();
return (
navigationState?.destructiveProtectionActive === true &&
navigationState?.destructivePhase === 'sealed' &&
canonicalDestructiveCredentialPhase === 'sealed'
);
};
const releaseCanonicalCredentialProtectionToLive = () => {
syncCanonicalDestructiveCredentialPhase();
if (
!canonicalCredentialGuardActive() ||
canonicalDestructiveCredentialPhase !== 'sealed' ||
canonicalDestructiveCredentialAcknowledged !== true ||
canonicalDestructiveCredentialViolationDetected ||
!ensureCanonicalCredentialProtection()
) {
return false;
}
const navigationGuard = window.__WWebCanonicalNavigationStorageGuardV1;
if (navigationGuard?.releaseSealedProtection?.() !== true) {
return false;
}
window[canonicalDestructiveCredentialPhaseGlobalKey] = 'live';
canonicalDestructiveCredentialPhase = 'live';
canonicalSocketLogoutBootstrapGraceUntil =
Date.now() + canonicalSocketLogoutBootstrapGraceMs;
canonicalSocketLogoutBootstrapSuppressionCount = 0;
return true;
};
const stopCanonicalCredentialProtection = () => {
clearInterval(canonicalCredentialProtectionTimer);
canonicalCredentialProtectionTimer = undefined;
for (const entry of canonicalCredentialProtectionTargets.values()) {
if (entry.target?.[entry.method] === entry.wrapped) {
entry.target[entry.method] = entry.original;
}
}
canonicalCredentialProtectionTargets.clear();
};
const wrapConnectionMethod = (target, method, observe) => {
const original = target?.[method];
if (
typeof original !== 'function' ||
original.__wwebjsConnectionDiagnosticWrapped
) {
return false;
}
const wrapped = function (...args) {
const observation = observe(args);
if (observation?.suppress === true) {
return observation.returnValue;
}
return original.apply(this, args);
};
Object.defineProperty(wrapped, 'length', {
configurable: true,
value: original.length,
});
Object.defineProperty(wrapped, '__wwebjsConnectionDiagnosticWrapped', {
value: true,
});
target[method] = wrapped;
if (target[method] !== wrapped) return false;
connectionDiagnosticRestores.push(() => {
if (target[method] === wrapped) target[method] = original;
});
return true;
};
const observeCredentialClear = (category) => {
const observation = observeCanonicalCredentialOperation(category, {
suppressValue: () => undefined,
});
if (!observation.suppress && !observation.notification) {
notifyConnectionDiagnostic('credential_clear', category);
}
return observation;
};
const listenConnectionModel = (model, event, listener) => {
if (typeof model?.on !== 'function') return;
model.on(event, listener);
connectionDiagnosticListeners.push(() => model.off?.(event, listener));
};
const ensureConnectionDiagnostics = () => {
if (!options.enableConnectionDiagnostics) return true;
if (connectionDiagnosticsInstalled) return true;
try {
ensureCanonicalCredentialProtection();
const multiDevice = window.require?.('WAWebUserPrefsMultiDevice');
const me = window.require?.('WAWebUserPrefsMeUser');
const signalStore = window.require?.(
'WAWebSignalStoreApi',
)?.waSignalStore;
const signalConst = window.require?.('WAWebSignalConst');
const socket = window.require?.('WAWebSocketModel')?.Socket;
const stream = window.require?.('WAWebStreamModel')?.Stream;
const logoutReason = window.require?.('WAWebLogoutReason');
const comms = window.require?.('WAComms');
const backendModule = window.require?.('WAWebBackendEventBus');
const backendEventBus = backendModule?.BackendEventBus;
if (!multiDevice || !me || !signalStore || !socket || !stream) {
return false;
}
const classifyLogoutCode = (value) => {
const normalized = String(value ?? '');
return (
{
0: 'client_fatal',
1: 'sync_fail',
2: 'initial_history_sync_timeout',
3: 'account_locked',
}[normalized] || (normalized === '' ? 'none' : 'unknown')
);
};
let lastLogoutCodeCategory;
let lastCustomLogoutMessageCategory;
const classifyCloseCode = (args) => {
let code;
for (const value of args) {
if (Number.isSafeInteger(value)) {
code = value;
break;
}
try {
if (Number.isSafeInteger(value?.code)) {
code = value.code;
break;
}
} catch {
// Private close objects are never serialized.
}
}
return (
{
1000: 'normal',
1001: 'going_away',
1002: 'protocol_error',
1006: 'abnormal',
1011: 'internal_error',
1012: 'service_restart',
1013: 'try_again_later',
1015: 'tls_failure',
}[code] ||
(Number.isSafeInteger(code) && code >= 1000 && code <= 4999
? 'other_valid'
: 'unavailable')
);
};
for (const [method, category] of [
['closeSocket', 'close_socket'],
['closeSocketAndPause', 'close_socket_and_pause'],
[
'closeSocketAndPreventRetry',
'close_socket_and_prevent_retry',
],
['closeSocketAndResume', 'close_socket_and_resume'],
['forceAbortSocketConnection', 'force_abort_socket'],
['forceResetSocketLoop', 'force_reset_socket_loop'],
['maybeResetSocketLoop', 'maybe_reset_socket_loop'],
['openSocketLoop', 'open_socket_loop'],
]) {
wrapConnectionMethod(comms, method, (args) => {
notifyConnectionDiagnostic('comms_action', category);
if (method === 'closeSocket') {
notifyConnectionDiagnostic(
'socket_close_code',
classifyCloseCode(args),
);
}
});
}
for (const [method, category] of [
['triggerReconnectSocket', 'reconnect_socket'],
[
'triggerSocketStreamDisconnected',
'socket_stream_disconnected',
],
['triggerOpenSocketStream', 'open_socket_stream'],
['triggerSetSocketState', 'set_socket_state'],
[
'triggerStorageInitializationError',
'storage_initialization_error',
],
]) {
wrapConnectionMethod(backendEventBus, method, () =>
notifyConnectionDiagnostic('backend_event', category),
);
}
const notifyLogoutSnapshot = () => {
let code;
let customMessage;
try {
code = logoutReason?.getPrevLogoutReasonCode?.();
} catch {
code = undefined;
}
try {
customMessage =
logoutReason?.getPrevCustomLogoutMessage?.();
} catch {
customMessage = undefined;
}
const codeCategory = classifyLogoutCode(code);
const customMessageCategory =
typeof customMessage === 'string' &&
customMessage.length > 0
? 'present'
: 'absent';
if (codeCategory !== lastLogoutCodeCategory) {
lastLogoutCodeCategory = codeCategory;
notifyConnectionDiagnostic('logout_code', codeCategory);
}
if (customMessageCategory !== lastCustomLogoutMessageCategory) {
lastCustomLogoutMessageCategory = customMessageCategory;
notifyConnectionDiagnostic(
'custom_logout_message',
customMessageCategory,
);
}
};
wrapConnectionMethod(multiDevice, 'clearRegistrationInfo', () => {
const observation = observeCredentialClear(
'clear_registration_info',
);
notifyLogoutSnapshot();
setTimeout(notifyLogoutSnapshot, 0);
return observation;
});
canonicalSocketLogoutObserver = (args, reasonCategory) => {
notifyConnectionDiagnostic('logout_reason', reasonCategory);
};
wrapConnectionMethod(
logoutReason,
'setPrevLogoutReasonCode',
(args) =>
notifyConnectionDiagnostic(
'logout_code',
classifyLogoutCode(args[0]),
),
);
wrapConnectionMethod(
logoutReason,
'setPrevCustomLogoutMessage',
(args) =>
notifyConnectionDiagnostic(
'custom_logout_message',
typeof args[0] === 'string' && args[0].length > 0
? 'present'
: 'absent',
),
);
wrapConnectionMethod(me, 'setMe', (args) => {
if (args[0] == null) {
return observeCredentialClear('me_pn_cleared');
}
});
wrapConnectionMethod(me, 'setMeLid', (args) => {
if (args[0] == null) {
return observeCredentialClear('me_lid_cleared');
}
});
wrapConnectionMethod(signalStore, 'putMeta', (args) => {
const rows = Array.isArray(args[0]) ? args[0] : [];
const clearsAdvIdentity = (row) =>
row?.key === signalConst?.META_KEYS?.ADV_SIGNED_IDENTITY &&
row?.value == null;
if (!rows.some(clearsAdvIdentity)) return;
const observation = observeCredentialClear(
'adv_identity_cleared',
);
if (observation.suppress) {
const retainedRows = rows.filter(
(row) => !clearsAdvIdentity(row),
);
if (retainedRows.length === 0) return observation;
args[0] = retainedRows;
}
});
const socketStates = new Set([
'CONFLICT',
'CONNECTED',
'DEPRECATED_VERSION',
'OPENING',
'PAIRING',
'PROXYBLOCK',
'SMB_TOS_BLOCK',
'TIMEOUT',
'TOS_BLOCK',
'UNLAUNCHED',
'UNPAIRED',
'UNPAIRED_IDLE',
]);
const streamCategories = new Set([
'CONNECTED',
'DISCONNECTED',
'OPENING',
'PAIRING',
'RESUMING',
'SYNCING',
'UNPAIRED',
]);
listenConnectionModel(socket, 'change:state', (_model, value) => {
const state = String(value);
notifyConnectionDiagnostic(
'socket_state',
socketStates.has(state) ? state.toLowerCase() : 'unknown',
);
});
listenConnectionModel(socket, 'change:stream', (_model, value) => {
const category = String(value);
notifyConnectionDiagnostic(
'socket_stream',
streamCategories.has(category)
? category.toLowerCase()
: 'other',
);
});
listenConnectionModel(stream, 'change:available', (_model, value) =>
notifyConnectionDiagnostic(
'stream_availability',
value === true ? 'available' : 'unavailable',
),
);
connectionDiagnosticsInstalled = true;
return true;
} catch {
return false;
}
};
const startConnectionDiagnostics = () => {
if (ensureConnectionDiagnostics() || connectionDiagnosticsTimer) return;
connectionDiagnosticsTimer = setInterval(() => {
if (ensureConnectionDiagnostics()) {
clearInterval(connectionDiagnosticsTimer);
connectionDiagnosticsTimer = undefined;
}
}, 250);
};
const stopConnectionDiagnostics = () => {
clearInterval(connectionDiagnosticsTimer);
connectionDiagnosticsTimer = undefined;
for (const restore of connectionDiagnosticListeners.splice(0)) {
restore();
}
for (const restore of connectionDiagnosticRestores.splice(0)) {
restore();
}
canonicalSocketLogoutObserver = undefined;
connectionDiagnosticsInstalled = false;
};
if (typeof Storage !== 'undefined') {
for (const method of ['setItem', 'removeItem', 'clear']) {
wrapMethod(Storage.prototype, method, (original) => {
return function (...args) {
const key = args[0];
const result = original.apply(this, args);
record({
area: 'local_storage',
operation: method,
key_length:
typeof key === 'string' ? key.length : undefined,
critical: criticalName(key),
});
return result;
};
});
}
}
if (typeof IDBObjectStore !== 'undefined') {
const pendingTransactions = new WeakMap();
const observeTransaction = (transaction, event) => {
if (!transaction) return;
let pending = pendingTransactions.get(transaction);
if (!pending) {
pending = new Map();
pendingTransactions.set(transaction, pending);
transaction.addEventListener(
'complete',
() => {
for (const committed of pending.values()) {
record(committed);
}
pendingTransactions.delete(transaction);
},
{ once: true },
);
const discard = () => pendingTransactions.delete(transaction);
transaction.addEventListener('abort', discard, { once: true });
transaction.addEventListener('error', discard, { once: true });
}
const key = `${event.database}\0${event.store}`;
const previous = pending.get(key);
pending.set(key, {
...event,
operation: 'transaction_commit',
operation_count: (previous?.operation_count || 0) + 1,
critical: previous?.critical || event.critical,
});
};
for (const method of ['add', 'put', 'delete', 'clear']) {
wrapMethod(IDBObjectStore.prototype, method, (original) => {
return function (...args) {
const database = this.transaction?.db?.name || '';
const store = this.name || '';
const result = original.apply(this, args);
observeTransaction(this.transaction, {
area: 'indexeddb',
database,
store,
critical: criticalName(`${database}:${store}`),
});
return result;
};
});
}
}
window.__WWebSessionBridge = {
schemaVersion: options.schemaVersion,
expectedVersion: options.expectedVersion,
drain() {
const drained = events.splice(0, events.length);
const result = { dirty, critical, dropped, events: drained };
dirty = false;
critical = false;
dropped = 0;
return result;
},
markClean() {
dirty = false;
critical = false;
},
state() {
let navigationGuardState;
try {
navigationGuardState =
window.__WWebCanonicalNavigationStorageGuardV1?.state?.();
} catch {
navigationGuardState = undefined;
}
return {
dirty,
critical,
queued: events.length,
dropped,
schemaVersion: options.schemaVersion,
advCaptureInstalled: Boolean(advCaptureWrapped),
connectionDiagnosticsInstalled,
canonicalCredentialProtectionInstalled:
ensureCanonicalCredentialProtection(),
canonicalDestructiveCredentialPhase:
syncCanonicalDestructiveCredentialPhase(),
canonicalDestructiveCredentialAcknowledged,
canonicalDestructiveCredentialViolationDetected,
canonicalCredentialGuardActive:
canonicalCredentialGuardActive(),
canonicalCredentialSuppressionCount,
canonicalSocketLogoutBootstrapGraceActive:
canonicalSocketLogoutBootstrapGraceUntil > Date.now(),
canonicalSocketLogoutBootstrapSuppressionCount,
postQuantumUploadLifecycleGate:
postQuantumUploadLifecycleGateState(),
canonicalNavigationStorageGuardActive:
navigationGuardState?.active === true,
canonicalNavigationStorageSuppressionCount:
Number.isSafeInteger(navigationGuardState?.suppressionCount)
? navigationGuardState.suppressionCount
: 0,
canonicalPairingWatchdogActive,
canonicalPairingDetected,
};
},
ensureAdvCapture,
ensureConnectionDiagnostics,
ensurePostQuantumUploadLifecycleGate,
startPostQuantumUploadLifecycleGate,
beginPostQuantumRollbackUploadFence,
recoverPostQuantumThroughUploadLifecycleGate,
resumePostQuantumUploadLifecycleGate,
postQuantumUploadLifecycleGateState,
ensureCanonicalCredentialProtection,
acknowledgeCanonicalCredentialProtection,
releaseCanonicalCredentialProtectionToLive,
armCanonicalCredentialGuard,
disarmCanonicalCredentialGuard,
armCanonicalPairingWatchdog,
disarmCanonicalPairingWatchdog,
teardown() {
disarmCanonicalCredentialGuard();
disarmCanonicalPairingWatchdog();
stopAdvCapture();
stopConnectionDiagnostics();
stopPostQuantumUploadLifecycleGate();
stopCanonicalCredentialProtection();
},
};
startPostQuantumUploadLifecycleGate();
startCanonicalCredentialProtection();
startAdvCapture();
startConnectionDiagnostics();
}
/* This function is serialized by Puppeteer and executes in the page realm. */
async function exportBrowserProjection(options) {
const bridge = window.__WWebSessionBridge;
if (!bridge || bridge.schemaVersion !== options.schemaVersion) {
throw new Error('wwebjs_browser_bridge_unavailable');
}
const version = window.Debug?.VERSION;
if (version !== options.expectedVersion) {
throw new Error('wwebjs_browser_bridge_version_mismatch');
}
let totalBytes = 0;
let totalRecords = 0;
let lossyRecords = 0;
const records = [];
const indexeddbStores = [];
const accountBytes = (value) => {
totalBytes += new TextEncoder().encode(JSON.stringify(value)).length;
totalRecords += 1;
if (
totalBytes > options.maxBytes ||
totalRecords > options.maxRecords
) {
throw new Error('wwebjs_browser_projection_limit_exceeded');
}
};
const encode = async (value, seen = new WeakSet(), depth = 0) => {
if (typeof value === 'bigint') {
return { __wwebjs_type: 'bigint', value: value.toString() };
}
if (typeof value === 'undefined') {
return { __wwebjs_type: 'undefined' };
}
if (typeof value === 'function' || typeof value === 'symbol') {
lossyRecords += 1;
return { __wwebjs_type: 'unsupported' };
}
if (value == null || typeof value !== 'object') return value;
if (depth > 20 || seen.has(value)) {
lossyRecords += 1;
return { __wwebjs_type: 'unsupported' };
}
seen.add(value);
if (value instanceof ArrayBuffer) {
const bytes = new Uint8Array(value);
let binary = '';
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
binary += String.fromCharCode(
...bytes.subarray(offset, offset + 0x8000),
);
}
return {
__wwebjs_type: 'array_buffer',
data: btoa(binary),
};
}
if (ArrayBuffer.isView(value)) {
return encode(
value.buffer.slice(
value.byteOffset,
value.byteOffset + value.byteLength,
),
seen,
depth + 1,
);
}
if (value instanceof Blob) {
return {
__wwebjs_type: 'blob',
mime_type: value.type,
data: (await encode(await value.arrayBuffer())).data,
};
}
if (value instanceof Date) {
return { __wwebjs_type: 'date', value: value.toISOString() };
}
if (value instanceof RegExp) {
return {
__wwebjs_type: 'regexp',
source: value.source,
flags: value.flags,
};
}
if (value instanceof Map) {
return {
__wwebjs_type: 'map',
entries: await Promise.all(
[...value.entries()].map(async ([key, entry]) => [
await encode(key, seen, depth + 1),
await encode(entry, seen, depth + 1),
]),
),
};
}
if (value instanceof Set) {
return {
__wwebjs_type: 'set',
values: await Promise.all(
[...value].map((entry) => encode(entry, seen, depth + 1)),
),
};
}
if (typeof CryptoKey !== 'undefined' && value instanceof CryptoKey) {
lossyRecords += 1;
return { __wwebjs_type: 'unsupported' };
}
if (Array.isArray(value)) {
return Promise.all(
value.map((entry) => encode(entry, seen, depth + 1)),
);
}
const encoded = {};
for (const [key, entry] of Object.entries(value)) {
encoded[key] = await encode(entry, seen, depth + 1);
}
return encoded;
};
for (let index = 0; index < localStorage.length; index += 1) {
const key = localStorage.key(index);
const record = {
namespace: 'local_storage',
record_key: key,
value: localStorage.getItem(key),
};
accountBytes(record);
records.push(record);
}
if (typeof indexedDB.databases === 'function') {
const databases = await indexedDB.databases();
for (const metadata of databases) {
if (!metadata.name) continue;
const database = await new Promise((resolve, reject) => {
const request = indexedDB.open(metadata.name);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
try {
for (const storeName of database.objectStoreNames) {
const transaction = database.transaction(
storeName,
'readonly',
);
const store = transaction.objectStore(storeName);
indexeddbStores.push({
database_name: metadata.name,
store_name: storeName,
key_path: store.keyPath,
});
const entries = await new Promise((resolve, reject) => {
const collected = [];
const request = store.openCursor();
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const cursor = request.result;
if (!cursor) {
resolve(collected);
return;
}
collected.push({
key: cursor.key,
value: cursor.value,
});
cursor.continue();
};
});
for (const entry of entries) {
const record = {
namespace: `indexeddb:${encodeURIComponent(metadata.name)}:${encodeURIComponent(storeName)}`,
record_key: JSON.stringify(await encode(entry.key)),
value: await encode(entry.value),
key_path: store.keyPath,
};
accountBytes(record);
records.push(record);
}
}
} finally {
database.close();
}
}
}
return {
schema_version: options.schemaVersion,
web_version: version,
records,
indexeddb_stores: indexeddbStores,
complete: lossyRecords === 0,
lossy_records: lossyRecords,
size_bytes: totalBytes,
};
}
/* This function is serialized by Puppeteer and executes in the page realm. */
async function importBrowserProjection(projection, options) {
if (
!projection ||
projection.schema_version !== options.schemaVersion ||
(options.enforceExpectedVersion &&
projection.web_version !== options.expectedVersion) ||
projection.complete !== true ||
!Array.isArray(projection.records) ||
!Array.isArray(projection.indexeddb_stores)
) {
throw new Error('wwebjs_browser_projection_incompatible');
}
const decode = (value) => {
if (!value || typeof value !== 'object') return value;
if (value.__wwebjs_type === 'array_buffer') {
const binary = atob(value.data);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) {
bytes[index] = binary.charCodeAt(index);
}
return bytes.buffer;
}
if (value.__wwebjs_type === 'blob') {
return new Blob(
[
decode({
__wwebjs_type: 'array_buffer',
data: value.data,
}),
],
{ type: value.mime_type },
);
}
if (value.__wwebjs_type === 'date') return new Date(value.value);
if (value.__wwebjs_type === 'bigint') return BigInt(value.value);
if (value.__wwebjs_type === 'undefined') return undefined;
if (value.__wwebjs_type === 'regexp') {
return new RegExp(value.source, value.flags);
}
if (value.__wwebjs_type === 'map') {
return new Map(
value.entries.map(([key, entry]) => [
decode(key),
decode(entry),
]),
);
}
if (value.__wwebjs_type === 'set') {
return new Set(value.values.map(decode));
}
if (value.__wwebjs_type === 'unsupported') {
throw new Error('wwebjs_browser_projection_lossy');
}
if (Array.isArray(value)) return value.map(decode);
return Object.fromEntries(
Object.entries(value).map(([key, entry]) => [key, decode(entry)]),
);
};
const indexedRecords = projection.records.filter((record) =>
record.namespace.startsWith('indexeddb:'),
);
const grouped = new Map();
for (const record of indexedRecords) {
const [, encodedDatabaseName, encodedStoreName] =
record.namespace.split(':');
const databaseName = decodeURIComponent(encodedDatabaseName);
const storeName = decodeURIComponent(encodedStoreName);
const key = JSON.stringify([databaseName, storeName]);
const group = grouped.get(key) || {
databaseName,
storeName,
records: [],
};
group.records.push(record);
grouped.set(key, group);
}
const existingDatabases = new Set(
typeof indexedDB.databases === 'function'
? (await indexedDB.databases()).map((entry) => entry.name)
: [],
);
const existingStores = new Set();
const openedDatabases = [];
for (const databaseName of existingDatabases) {
if (!databaseName) continue;
const database = await new Promise((resolve, reject) => {
const request = indexedDB.open(databaseName);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
openedDatabases.push({ databaseName, database });
for (const storeName of database.objectStoreNames) {
existingStores.add(JSON.stringify([databaseName, storeName]));
}
}
try {
for (const metadata of projection.indexeddb_stores) {
if (
!metadata?.database_name ||
!metadata?.store_name ||
!existingStores.has(
JSON.stringify([
metadata.database_name,
metadata.store_name,
]),
)
) {
throw new Error('wwebjs_browser_projection_store_missing');
}
}
for (const group of grouped.values()) {
if (
!existingStores.has(
JSON.stringify([group.databaseName, group.storeName]),
)
) {
throw new Error('wwebjs_browser_projection_store_missing');
}
}
// Destructive hydration only starts after the complete projection has
// been checked against the fixed-version browser schema.
localStorage.clear();
for (const record of projection.records) {
if (record.namespace === 'local_storage') {
localStorage.setItem(record.record_key, record.value);
}
}
for (const { database } of openedDatabases) {
for (const storeName of database.objectStoreNames) {
const transaction = database.transaction(
storeName,
'readwrite',
);
transaction.objectStore(storeName).clear();
await new Promise((resolve, reject) => {
transaction.oncomplete = resolve;
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(transaction.error);
});
}
}
} finally {
for (const { database } of openedDatabases) {
database.close();
}
}
for (const group of grouped.values()) {
const database = await new Promise((resolve, reject) => {
const request = indexedDB.open(group.databaseName);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
try {
if (!database.objectStoreNames.contains(group.storeName)) {
throw new Error('wwebjs_browser_projection_store_missing');
}
const transaction = database.transaction(
group.storeName,
'readwrite',
);
const store = transaction.objectStore(group.storeName);
for (const record of group.records) {
const value = decode(record.value);
const key = decode(JSON.parse(record.record_key));
if (store.keyPath == null) store.put(value, key);
else store.put(value);
}
await new Promise((resolve, reject) => {
transaction.oncomplete = resolve;
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(transaction.error);
});
} finally {
database.close();
}
}
window.__WWebSessionBridge?.markClean();
return { imported_records: projection.records.length };
}
/* This function is serialized by Puppeteer and executes in the page realm. */
function observeCanonicalAppStateRestoreBarrier(options) {
const expectedCollectionNames = new Set([
'critical_block',
'critical_unblock_low',
'regular',
'regular_high',
'regular_low',
]);
const asCollectionNames = (value) => {
let entries;
if (value instanceof Map) {
entries = Array.from(value.keys());
} else if (value instanceof Set || Array.isArray(value)) {
entries = Array.from(value);
} else if (
value != null &&
typeof value[Symbol.iterator] === 'function'
) {
entries = Array.from(value);
} else {
throw new Error('collection_state_shape_unknown');
}
const names = entries.map((entry) => {
if (typeof entry === 'string') return entry;
if (typeof entry?.collectionName === 'string') {
return entry.collectionName;
}
if (typeof entry?.collection === 'string') return entry.collection;
if (typeof entry?.name === 'string') return entry.name;
throw new Error('collection_state_entry_unknown');
});
if (names.some((name) => !expectedCollectionNames.has(name))) {
throw new Error('collection_state_name_unknown');
}
return [...new Set(names)].sort();
};
let syncd;
let stateMachine;
let socket;
try {
syncd = window.require?.('WAWebSyncd');
stateMachine = window.require?.('WAWebSyncdCollectionsStateMachine');
socket = window.require?.('WAWebSocketModel')?.Socket;
} catch {
return { abi_compatible: false };
}
const requiredFunctions = [
syncd?.getInFlightCollections,
syncd?.getPendingCollections,
syncd?.markCollectionsForSync,
syncd?.syncBlockedCollections,
stateMachine?.getCollectionsInStateDirty,
stateMachine?.getCollectionsInStateRetry,
stateMachine?.getCollectionsInStateFatal,
stateMachine?.getCollectionsInStateBlocked,
];
if (
!socket ||
requiredFunctions.some((operation) => typeof operation !== 'function')
) {
return { abi_compatible: false };
}
let pending;
let inFlight;
let dirty;
let retry;
let fatal;
let blocked;
try {
pending = asCollectionNames(syncd.getPendingCollections());
inFlight = asCollectionNames(syncd.getInFlightCollections());
dirty = asCollectionNames(stateMachine.getCollectionsInStateDirty());
retry = asCollectionNames(stateMachine.getCollectionsInStateRetry());
fatal = asCollectionNames(stateMachine.getCollectionsInStateFatal());
blocked = asCollectionNames(
stateMachine.getCollectionsInStateBlocked(),
);
} catch {
return { abi_compatible: false };
}
const socketState = String(socket.state || '');
const socketStream = String(socket.stream || '');
const transportConnected =
globalThis.navigator?.onLine === true &&
socketState === 'CONNECTED' &&
socket.hasSynced === true &&
socketStream === 'CONNECTED';
const now = Date.now();
const stallMs = Math.max(1000, Number(options?.stallMs) || 5000);
const nativeStallMs = Math.max(
15000,
Number(options?.nativeStallMs) || 15000,
);
const requiredStableSamples = Math.max(
2,
Number(options?.requiredStableSamples) || 2,
);
let state = window.__WWebCanonicalAppStateRestoreBarrierV1;
if (!state || typeof state !== 'object') {
state = {
lastProgressSignature: null,
lastProgressAt: now,
stableSampleCount: 0,
blockedRedriveAttempted: false,
dirtyRedriveAttempted: false,
retryRedriveAttempted: false,
redriveFailure: null,
redrivePending: false,
blockedRedriveResolved: false,
blockedRedriveCollectionNames: [],
blockedQuiescentSince: null,
retryRedriveResolved: false,
retryRedriveCollectionNames: [],
retryQuiescentSince: null,
};
window.__WWebCanonicalAppStateRestoreBarrierV1 = state;
}
const signature = JSON.stringify({
pending,
inFlight,
dirty,
retry,
fatal,
blocked,
});
if (state.lastProgressSignature !== signature) {
state.lastProgressSignature = signature;
state.lastProgressAt = now;
state.stableSampleCount = 0;
state.blockedQuiescentSince = null;
state.retryQuiescentSince = null;
}
if (!transportConnected) state.stableSampleCount = 0;
let status = 'busy';
let redriveAction;
const blockedRedriveCollectionNames = Array.isArray(
state.blockedRedriveCollectionNames,
)
? state.blockedRedriveCollectionNames
: [];
const blockedQuiescentCandidate =
blocked.length > 0 &&
state.blockedRedriveResolved === true &&
JSON.stringify(blocked) ===
JSON.stringify(blockedRedriveCollectionNames);
const retryRedriveCollectionNames = Array.isArray(
state.retryRedriveCollectionNames,
)
? state.retryRedriveCollectionNames
: [];
const retryQuiescentCandidate =
retry.length > 0 &&
state.retryRedriveResolved === true &&
JSON.stringify(retry) === JSON.stringify(retryRedriveCollectionNames);
const activeQueuesEmpty =
pending.length === 0 &&
inFlight.length === 0 &&
dirty.length === 0 &&
(retry.length === 0 || retryQuiescentCandidate);
const nativeInFlightStalled =
transportConnected &&
state.redrivePending !== true &&
inFlight.length > 0 &&
pending.length === 0 &&
retry.length === 0 &&
fatal.length === 0 &&
blocked.length === 0 &&
inFlight.every((name) => dirty.includes(name)) &&
now - (state.lastProgressAt || now) >= nativeStallMs;
if (state.redriveFailure) {
status = 'redrive_failed';
} else if (fatal.length > 0) {
status = 'fatal';
} else if (nativeInFlightStalled) {
// WAWebSyncd can retain an orphaned request in its realm-local
// in-flight set while the same collections remain dirty. No official
// redrive API can detach that request. Surface the exact condition to
// the host so it can use its one guarded fresh-realm recovery instead
// of burning the generic restore timeout and restarting Chromium.
status = 'native_stalled';
} else if (
transportConnected &&
state.redrivePending !== true &&
activeQueuesEmpty &&
(blocked.length === 0 || blockedQuiescentCandidate)
) {
if (
blockedQuiescentCandidate &&
!Number.isSafeInteger(state.blockedQuiescentSince)
) {
state.blockedQuiescentSince = now;
}
if (
retryQuiescentCandidate &&
!Number.isSafeInteger(state.retryQuiescentSince)
) {
state.retryQuiescentSince = now;
}
state.stableSampleCount = (state.stableSampleCount || 0) + 1;
const blockedStableForMs = blockedQuiescentCandidate
? now - state.blockedQuiescentSince
: 0;
const retryStableForMs = retryQuiescentCandidate
? now - state.retryQuiescentSince
: 0;
if (
state.stableSampleCount >= requiredStableSamples &&
(!blockedQuiescentCandidate || blockedStableForMs >= 1000) &&
(!retryQuiescentCandidate || retryStableForMs >= 1000)
) {
status = 'idle';
}
} else if (transportConnected) {
state.stableSampleCount = 0;
state.blockedQuiescentSince = null;
state.retryQuiescentSince = null;
const stalledForMs = now - (state.lastProgressAt || now);
let redrive;
let redriveCollectionNames = [];
if (
stalledForMs >= stallMs &&
blocked.length > 0 &&
state.blockedRedriveAttempted !== true
) {
state.blockedRedriveAttempted = true;
state.lastProgressAt = now;
redriveAction = 'blocked';
redriveCollectionNames = [...blocked];
} else if (
stalledForMs >= stallMs &&
retry.length > 0 &&
pending.length === 0 &&
inFlight.length === 0 &&
state.retryRedriveAttempted !== true
) {
// A reload during activation recovery can leave a collection in
// WAWebSyncd's retry state without a pending timer in the new
// realm. Re-enqueue that exact collection through the official
// scheduler before declaring the restore window unavailable.
state.retryRedriveAttempted = true;
state.lastProgressAt = now;
redriveAction = 'retry';
redriveCollectionNames = [...retry];
} else if (
stalledForMs >= stallMs &&
dirty.length > 0 &&
pending.length === 0 &&
inFlight.length === 0 &&
state.dirtyRedriveAttempted !== true
) {
state.dirtyRedriveAttempted = true;
state.lastProgressAt = now;
redriveAction = 'dirty';
redriveCollectionNames = [...dirty];
}
if (redriveAction) {
state.redrivePending = true;
state.stableSampleCount = 0;
const settleRedrive = (failed) => {
if (window.__WWebCanonicalAppStateRestoreBarrierV1 !== state) {
return;
}
state.redrivePending = false;
state.stableSampleCount = 0;
state.lastProgressAt = Date.now();
state.lastProgressSignature = null;
state.blockedQuiescentSince = null;
state.retryQuiescentSince = null;
if (failed) {
state.redriveFailure = redriveAction;
return;
}
if (redriveAction === 'blocked') {
state.blockedRedriveResolved = true;
state.blockedRedriveCollectionNames =
redriveCollectionNames;
} else if (redriveAction === 'retry') {
state.retryRedriveResolved = true;
state.retryRedriveCollectionNames = redriveCollectionNames;
}
};
try {
redrive =
redriveAction === 'blocked'
? syncd.syncBlockedCollections()
: syncd.markCollectionsForSync(redriveCollectionNames);
} catch {
settleRedrive(true);
}
if (state.redriveFailure == null) {
if (redrive && typeof redrive.then === 'function') {
Promise.resolve(redrive).then(
() => settleRedrive(false),
() => settleRedrive(true),
);
} else {
settleRedrive(false);
}
}
}
}
return {
abi_compatible: true,
status: transportConnected ? status : 'disconnected',
transport_connected: transportConnected,
socket_state: socketState || 'unknown',
socket_stream: socketStream || 'unknown',
socket_synced: socket.hasSynced === true,
pending_count: pending.length,
pending_collection_names: pending,
in_flight_count: inFlight.length,
in_flight_collection_names: inFlight,
dirty_count: dirty.length,
dirty_collection_names: dirty,
retry_count: retry.length,
fatal_count: fatal.length,
blocked_count: blocked.length,
blocked_collection_names: blocked,
blocked_quiescent:
status === 'idle' && blockedQuiescentCandidate === true,
retry_quiescent: status === 'idle' && retryQuiescentCandidate === true,
passive_blocked_collections:
status === 'idle' && blockedQuiescentCandidate === true
? blocked
: [],
passive_retry_collections:
status === 'idle' && retryQuiescentCandidate === true ? retry : [],
stable_sample_count: state.stableSampleCount || 0,
redrive_pending: state.redrivePending === true,
redrive_action: redriveAction,
redrive_failure: state.redriveFailure || undefined,
native_stalled_for_ms: nativeInFlightStalled
? now - (state.lastProgressAt || now)
: 0,
native_stall_threshold_ms: nativeStallMs,
};
}
/* This function is serialized by Puppeteer and executes in the page realm. */
async function restoreCanonicalAppStateSyncKeys(appStateProjection, options) {
const fail = (code) => {
throw new Error(code);
};
const requireModule = (name) => {
try {
const value = window.require?.(name);
if (value) return value.default || value;
} catch {
// Converted to a stable compatibility error below.
}
fail('wwebjs_canonical_app_state_restore_abi_incompatible');
};
const fromBase64 = (value) => {
if (
typeof value !== 'string' ||
value.length % 4 === 1 ||
!/^[A-Za-z0-9+/]*={0,2}$/.test(value)
) {
fail('wwebjs_canonical_app_state_restore_projection_invalid');
}
let binary;
try {
binary = atob(value);
} catch {
fail('wwebjs_canonical_app_state_restore_projection_invalid');
}
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) {
bytes[index] = binary.charCodeAt(index);
}
return bytes;
};
const bytesEqual = (left, right) => {
const a =
left instanceof ArrayBuffer
? new Uint8Array(left)
: ArrayBuffer.isView(left)
? new Uint8Array(
left.buffer,
left.byteOffset,
left.byteLength,
)
: undefined;
const b =
right instanceof ArrayBuffer
? new Uint8Array(right)
: ArrayBuffer.isView(right)
? new Uint8Array(
right.buffer,
right.byteOffset,
right.byteLength,
)
: undefined;
if (!a || !b || a.byteLength !== b.byteLength) return false;
let mismatch = 0;
for (let index = 0; index < a.byteLength; index += 1) {
mismatch |= a[index] ^ b[index];
}
return mismatch === 0;
};
const parseFingerprint = (bytes) => {
let offset = 0;
const readVarint = () => {
let value = 0;
let multiplier = 1;
for (let count = 0; count < 5; count += 1) {
if (offset >= bytes.length) {
fail(
'wwebjs_canonical_app_state_restore_projection_invalid',
);
}
const byte = bytes[offset++];
value += (byte & 0x7f) * multiplier;
if ((byte & 0x80) === 0) return value;
multiplier *= 128;
}
fail('wwebjs_canonical_app_state_restore_projection_invalid');
};
const fingerprint = {
rawId: 0,
currentIndex: 0,
deviceIndexes: [],
};
let rawIdSeen = false;
let currentIndexSeen = false;
while (offset < bytes.length) {
const tag = readVarint();
const field = tag >>> 3;
const wireType = tag & 7;
if ((field === 1 || field === 2) && wireType === 0) {
const value = readVarint();
if (field === 1) {
fingerprint.rawId = value;
rawIdSeen = true;
} else {
fingerprint.currentIndex = value;
currentIndexSeen = true;
}
} else if (field === 3 && wireType === 0) {
fingerprint.deviceIndexes.push(readVarint());
} else if (field === 3 && wireType === 2) {
const length = readVarint();
const end = offset + length;
if (end > bytes.length) {
fail(
'wwebjs_canonical_app_state_restore_projection_invalid',
);
}
while (offset < end) {
fingerprint.deviceIndexes.push(readVarint());
}
if (offset !== end) {
fail(
'wwebjs_canonical_app_state_restore_projection_invalid',
);
}
} else if (wireType === 0) {
readVarint();
} else if (wireType === 2) {
const length = readVarint();
offset += length;
if (offset > bytes.length) {
fail(
'wwebjs_canonical_app_state_restore_projection_invalid',
);
}
} else {
fail('wwebjs_canonical_app_state_restore_projection_invalid');
}
}
if (!rawIdSeen || !currentIndexSeen) {
fail('wwebjs_canonical_app_state_restore_projection_invalid');
}
return fingerprint;
};
const fingerprintEqual = (left, right) =>
Number(left?.rawId) === Number(right?.rawId) &&
Number(left?.currentIndex) === Number(right?.currentIndex) &&
Array.isArray(left?.deviceIndexes) &&
Array.isArray(right?.deviceIndexes) &&
left.deviceIndexes.length === right.deviceIndexes.length &&
left.deviceIndexes.every(
(value, index) =>
Number(value) === Number(right.deviceIndexes[index]),
);
let deadline = 0;
const withDeadline = (operation, code) =>
new Promise((resolve, reject) => {
const remainingMs = deadline - Date.now();
if (remainingMs <= 0) {
reject(new Error(code));
return;
}
const timer = setTimeout(
() => reject(new Error(code)),
remainingMs,
);
let pending;
try {
pending = operation();
} catch (error) {
clearTimeout(timer);
reject(error);
return;
}
Promise.resolve(pending).then(
(value) => {
clearTimeout(timer);
resolve(value);
},
(error) => {
clearTimeout(timer);
reject(error);
},
);
});
const withControlledStage = async (operation, code) => {
try {
return await withDeadline(operation, code);
} catch (error) {
if (/^wwebjs_canonical_/u.test(String(error?.message || ''))) {
throw error;
}
const sanitizeDiagnostic = (value) =>
String(value || 'unknown')
.replace(/[^a-z0-9_.: -]/giu, '_')
.slice(0, 160);
window.__WWebCanonicalAppStateRestoreDiagnosticV1 = {
...(window.__WWebCanonicalAppStateRestoreDiagnosticV1 || {}),
stage: code,
errorName: sanitizeDiagnostic(error?.name),
errorMessage: sanitizeDiagnostic(error?.message),
recordedAt: Date.now(),
};
fail(code);
}
};
let restoreJobState;
let previousSyncJob;
let destructiveMutationStarted = false;
try {
const syncJobTtlMs = options?.syncJobTtlMs ?? 120000;
if (
!Number.isSafeInteger(options?.timeoutMs) ||
options.timeoutMs < 1000 ||
options.timeoutMs > 600000 ||
!Number.isSafeInteger(syncJobTtlMs) ||
syncJobTtlMs < 1000 ||
syncJobTtlMs > 600000
) {
fail('wwebjs_canonical_app_state_restore_timeout_invalid');
}
if (window.__WWebCanonicalAppStateRestorePoisonV1?.poisoned === true) {
fail('wwebjs_canonical_app_state_restore_restart_required');
}
deadline = Date.now() + options.timeoutMs;
const projection = Array.isArray(appStateProjection)
? {
app_state_sync_keys: appStateProjection,
app_state_versions: [],
app_state_mutation_macs: [],
}
: appStateProjection;
if (
!projection ||
!Array.isArray(projection.app_state_sync_keys) ||
!Array.isArray(projection.app_state_versions) ||
!Array.isArray(projection.app_state_mutation_macs) ||
projection.app_state_sync_keys.length === 0
) {
fail('wwebjs_canonical_app_state_sync_keys_missing');
}
const rows = projection.app_state_sync_keys;
const dbEncryption = requireModule('WAWebDbEncryptionKey');
const syncKeysSchema = requireModule('WAWebSchemaSyncKeys');
const collectionSchema = requireModule('WAWebSchemaCollectionVersion');
const syncActionsSchema = requireModule('WAWebSchemaSyncActions');
const pendingMutationsSchema = requireModule(
'WAWebSchemaPendingMutations',
);
const missingKeysSchema = requireModule('WAWebSchemaMissingKeys');
const syncdDb = requireModule('WAWebSyncdDb');
const syncd = requireModule('WAWebSyncd');
const syncdStateMachine = requireModule(
'WAWebSyncdCollectionsStateMachine',
);
const syncdConst = requireModule('WAWebSyncdConst');
const waitForFinalKey =
dbEncryption?.DbEncKeyStore?.waitForFinalDbMsgEncKey;
const syncKeysTable = syncKeysSchema?.getSyncKeysTable?.();
const collectionVersionTable =
collectionSchema?.getCollectionVersionTable?.();
const syncActionsTable = syncActionsSchema?.getSyncActionsTable?.();
const pendingMutationsTable =
pendingMutationsSchema?.getPendingMutationsTable?.();
const missingKeysTable = missingKeysSchema?.getMissingKeysTable?.();
const restoredTables = [
syncKeysTable,
collectionVersionTable,
syncActionsTable,
pendingMutationsTable,
missingKeysTable,
];
if (
typeof waitForFinalKey !== 'function' ||
!syncKeysTable ||
typeof syncKeysTable.createOrReplace !== 'function' ||
restoredTables.some(
(table) => !table || typeof table.clear !== 'function',
) ||
typeof collectionVersionTable.all !== 'function' ||
typeof syncActionsTable.all !== 'function' ||
typeof syncdDb?.getAllSyncKeys !== 'function' ||
typeof syncd?.markCollectionsForSync !== 'function' ||
typeof syncd?.getInFlightCollections !== 'function' ||
typeof syncd?.getPendingCollections !== 'function' ||
typeof syncdStateMachine?.clean !== 'function' ||
typeof syncdStateMachine?.getCollectionsInStateDirty !==
'function' ||
typeof syncdStateMachine?.getCollectionsInStateRetry !==
'function' ||
typeof syncdStateMachine?.getCollectionsInStateFatal !==
'function' ||
typeof syncdStateMachine?.getCollectionsInStateBlocked !==
'function'
) {
fail('wwebjs_canonical_app_state_restore_abi_incompatible');
}
const syncApiMethods = Object.keys(syncd)
.filter(
(name) =>
/^[a-z0-9_$]{1,80}$/iu.test(name) &&
/(sync|collection|patch|mutation|action|upload|flush|commit)/iu.test(
name,
) &&
typeof syncd[name] === 'function',
)
.sort()
.slice(0, 32);
const collectionNames = [
syncdConst?.CollectionName?.CriticalBlock,
syncdConst?.CollectionName?.CriticalUnblockLow,
syncdConst?.CollectionName?.Regular,
syncdConst?.CollectionName?.RegularHigh,
syncdConst?.CollectionName?.RegularLow,
];
const expectedCollectionNames = [
'critical_block',
'critical_unblock_low',
'regular',
'regular_high',
'regular_low',
];
if (
collectionNames.some(
(name) => typeof name !== 'string' || name.length === 0,
) ||
new Set(collectionNames).size !== collectionNames.length ||
JSON.stringify(collectionNames) !==
JSON.stringify(expectedCollectionNames)
) {
fail('wwebjs_canonical_app_state_restore_abi_incompatible');
}
if (
options?.requireOffline !== undefined &&
typeof options.requireOffline !== 'boolean'
) {
fail('wwebjs_canonical_app_state_restore_timeout_invalid');
}
if (
options?.requireOffline === true &&
globalThis.navigator?.onLine !== false
) {
fail('wwebjs_canonical_app_state_restore_requires_quiescence');
}
const decodedRows = rows.map((row) => {
const keyId = fromBase64(row?.key_id);
const keyData = fromBase64(row?.key_data);
const fingerprintBytes = fromBase64(row?.fingerprint);
if (
keyId.byteLength < 2 ||
keyData.byteLength !== 32 ||
!Number.isSafeInteger(row?.timestamp) ||
row.timestamp < 0
) {
fail('wwebjs_canonical_app_state_restore_projection_invalid');
}
return {
keyId,
keyData,
timestamp: row.timestamp,
fingerprint: parseFingerprint(fingerprintBytes),
keyEpoch:
keyId[keyId.byteLength - 2] * 256 +
keyId[keyId.byteLength - 1],
};
});
const decodedVersions = projection.app_state_versions.map((row) => {
const ltHash = fromBase64(row?.hash);
if (
typeof row?.name !== 'string' ||
!expectedCollectionNames.includes(row.name) ||
!Number.isSafeInteger(row?.version) ||
row.version < 0 ||
ltHash.byteLength !== 128
) {
fail('wwebjs_canonical_app_state_restore_projection_invalid');
}
return {
collection: row.name,
version: row.version,
ltHash: ltHash.buffer,
};
});
const decodedMutationMacs = projection.app_state_mutation_macs.map(
(row) => {
const indexMac = fromBase64(row?.index_mac);
const valueMac = fromBase64(row?.value_mac);
if (
typeof row?.name !== 'string' ||
!expectedCollectionNames.includes(row.name) ||
!Number.isSafeInteger(row?.version) ||
row.version < 0 ||
indexMac.byteLength !== 32 ||
valueMac.byteLength !== 32
) {
fail(
'wwebjs_canonical_app_state_restore_projection_invalid',
);
}
return {
collection: row.name,
version: row.version,
indexMac: indexMac.buffer,
valueMac: valueMac.buffer,
};
},
);
if (
new Set(decodedRows.map((row) => Array.from(row.keyId).join(',')))
.size !== decodedRows.length ||
new Set(decodedVersions.map((row) => row.collection)).size !==
decodedVersions.length ||
new Set(
decodedMutationMacs.map(
(row) =>
`${row.collection}\0${row.version}\0${Array.from(
new Uint8Array(row.indexMac),
).join(',')}`,
),
).size !== decodedMutationMacs.length
) {
fail('wwebjs_canonical_app_state_restore_projection_invalid');
}
// The online restore barrier attests any passive blocked collections
// after a resolved redrive. Re-prove the exact same offline window
// twice before resetting native state. No destructive mutation occurs
// until both samples and sync-job admission have succeeded.
const collectionStateNames = (value) => {
let entries;
if (value instanceof Map) {
entries = Array.from(value.keys());
} else if (value instanceof Set || Array.isArray(value)) {
entries = Array.from(value);
} else if (
value != null &&
typeof value[Symbol.iterator] === 'function'
) {
entries = Array.from(value);
} else {
fail('wwebjs_canonical_app_state_restore_abi_incompatible');
}
const names = entries.map((entry) => {
if (typeof entry === 'string') return entry;
if (typeof entry?.collectionName === 'string') {
return entry.collectionName;
}
if (typeof entry?.collection === 'string') {
return entry.collection;
}
if (typeof entry?.name === 'string') return entry.name;
fail('wwebjs_canonical_app_state_restore_abi_incompatible');
});
if (names.some((name) => !expectedCollectionNames.includes(name))) {
fail('wwebjs_canonical_app_state_restore_abi_incompatible');
}
return [...new Set(names)].sort();
};
const passiveBlockedCollections =
options?.passiveBlockedCollections ?? [];
const passiveRetryCollections = options?.passiveRetryCollections ?? [];
if (
!Array.isArray(passiveBlockedCollections) ||
passiveBlockedCollections.some(
(name) =>
typeof name !== 'string' ||
!expectedCollectionNames.includes(name),
) ||
new Set(passiveBlockedCollections).size !==
passiveBlockedCollections.length ||
!Array.isArray(passiveRetryCollections) ||
passiveRetryCollections.some(
(name) =>
typeof name !== 'string' ||
!expectedCollectionNames.includes(name),
) ||
new Set(passiveRetryCollections).size !==
passiveRetryCollections.length ||
passiveRetryCollections.some((name) =>
passiveBlockedCollections.includes(name),
)
) {
fail('wwebjs_canonical_app_state_restore_abi_incompatible');
}
const expectedPassiveBlockedCollections = [
...passiveBlockedCollections,
].sort();
const expectedPassiveRetryCollections = [
...passiveRetryCollections,
].sort();
const readRestoreWindow = () => ({
pending: collectionStateNames(syncd.getPendingCollections()),
inFlight: collectionStateNames(syncd.getInFlightCollections()),
dirty: collectionStateNames(
syncdStateMachine.getCollectionsInStateDirty(),
),
retry: collectionStateNames(
syncdStateMachine.getCollectionsInStateRetry(),
),
fatal: collectionStateNames(
syncdStateMachine.getCollectionsInStateFatal(),
),
blocked: collectionStateNames(
syncdStateMachine.getCollectionsInStateBlocked(),
),
});
const restoreWindowMatchesAttestation = (state) =>
state.pending.length === 0 &&
state.inFlight.length === 0 &&
state.dirty.length === 0 &&
JSON.stringify(state.retry) ===
JSON.stringify(expectedPassiveRetryCollections) &&
state.fatal.length === 0 &&
JSON.stringify(state.blocked) ===
JSON.stringify(expectedPassiveBlockedCollections);
previousSyncJob = window.__WWebCanonicalAppStateSyncJobV1;
if (
previousSyncJob?.status === 'restoring' ||
previousSyncJob?.status === 'prepared' ||
previousSyncJob?.status === 'running'
) {
fail('wwebjs_canonical_app_state_sync_job_in_progress');
}
if (previousSyncJob?.status === 'failed') {
fail('wwebjs_canonical_app_state_sync_job_failed');
}
if (previousSyncJob?.status === 'timed_out') {
fail('wwebjs_canonical_app_state_sync_job_restart_required');
}
const generation =
Number.isSafeInteger(previousSyncJob?.generation) &&
previousSyncJob.generation >= 0
? previousSyncJob.generation + 1
: 1;
restoreJobState = {
generation,
status: 'restoring',
preparedAt: null,
startedAt: null,
expiresAt: null,
completedAt: null,
cancelledAt: null,
cancel: null,
timeoutHandle: null,
promiseSettled: false,
quiescentSampleCount: 0,
lastProgressSignature: null,
lastProgressAt: Date.now(),
blockedRedriveAttempted: false,
dirtyRedriveAttempted: false,
retryRedriveAttempted: false,
redrivePending: false,
blockedRedriveResolved: false,
blockedRedriveCollectionNames: [],
blockedQuiescentSince: null,
workObserved: false,
observedCollectionNames: [],
projectionWorkObservedCollectionNames: [],
authoritativeProjectionReadyObserved: false,
passiveBlockedCollections: expectedPassiveBlockedCollections,
passiveRetryCollections: expectedPassiveRetryCollections,
};
window.__WWebCanonicalAppStateSyncJobV1 = restoreJobState;
await withDeadline(
() => waitForFinalKey.call(dbEncryption.DbEncKeyStore),
'wwebjs_canonical_app_state_db_key_timeout',
);
const firstRestoreWindow = readRestoreWindow();
if (!restoreWindowMatchesAttestation(firstRestoreWindow)) {
fail('wwebjs_canonical_app_state_restore_window_changed');
}
await withDeadline(
() => new Promise((resolve) => setTimeout(resolve, 50)),
'wwebjs_canonical_app_state_restore_window_changed',
);
const secondRestoreWindow = readRestoreWindow();
if (!restoreWindowMatchesAttestation(secondRestoreWindow)) {
fail('wwebjs_canonical_app_state_restore_window_changed');
}
destructiveMutationStarted = true;
syncdStateMachine.clean();
const postCleanWindow = readRestoreWindow();
if (Object.values(postCleanWindow).some((names) => names.length > 0)) {
fail('wwebjs_canonical_app_state_restore_abi_incompatible');
}
await withDeadline(
() => new Promise((resolve) => setTimeout(resolve, 50)),
'wwebjs_canonical_app_state_restore_window_changed',
);
const secondPostCleanWindow = readRestoreWindow();
if (
Object.values(secondPostCleanWindow).some(
(names) => names.length > 0,
)
) {
fail('wwebjs_canonical_app_state_restore_abi_incompatible');
}
await withControlledStage(
() => Promise.all(restoredTables.map((table) => table.clear())),
'wwebjs_canonical_app_state_material_clear_failed',
);
for (const row of decodedRows) {
await withDeadline(
() =>
syncKeysTable.createOrReplace({
...row,
keyData: row.keyData.buffer,
}),
'wwebjs_canonical_app_state_sync_key_write_timeout',
);
}
const readPersistedMaterial = () =>
withControlledStage(
() =>
Promise.all([
syncdDb.getAllSyncKeys(),
collectionVersionTable.all(),
syncActionsTable.all(),
]),
'wwebjs_canonical_app_state_material_readback_failed',
);
const persistedMaterialMatches = ([
persisted,
persistedVersions,
persistedMutationMacs,
]) =>
exactSyncKeyMaterialMatches(persisted) &&
derivedAppStateMaterialEmpty(
persistedVersions,
persistedMutationMacs,
);
const exactSyncKeyMaterialMatches = (persisted) =>
Array.isArray(persisted) &&
persisted.length === decodedRows.length &&
decodedRows.every((expected) => {
const actual = persisted.find((candidate) =>
bytesEqual(candidate?.keyId, expected.keyId),
);
return (
actual &&
bytesEqual(actual.keyData, expected.keyData) &&
Number(actual.timestamp) === expected.timestamp &&
fingerprintEqual(actual.fingerprint, expected.fingerprint)
);
});
const derivedAppStateMaterialEmpty = (
persistedVersions,
persistedMutationMacs,
) =>
Array.isArray(persistedVersions) &&
persistedVersions.length === 0 &&
Array.isArray(persistedMutationMacs) &&
persistedMutationMacs.length === 0;
const restoreWindowIsEmpty = (state) =>
Object.values(state).every((names) => names.length === 0);
// WAWebSyncdDb can expose createOrReplace() writes through a stale
// IndexedDB view after the authenticated scheduler has just been
// quiesced. This was observed live after all seven cross-provider sync
// keys had already been imported: eight 100 ms samples still saw the
// old view, while a clean retry found the exact same material. Keep
// this wait local to the offline post-write boundary. Every sample
// still requires byte-for-byte key data, timestamp, fingerprint and
// empty derived tables; a mismatch that survives the bounded window
// remains poisoned and fail-closed.
const maximumVerificationWindowMs = 5000;
const verificationStartedAt = Date.now();
const verificationDeadline = Math.min(
deadline - 25,
verificationStartedAt + maximumVerificationWindowMs,
);
let verificationReadAttempts = 0;
let derivedMaterialCleanupAttempted = false;
let derivedMaterialCleanupStableSamples = 0;
let derivedMaterialObservedVersionCount = 0;
let derivedMaterialObservedMutationMacCount = 0;
let persistedMaterial;
do {
verificationReadAttempts += 1;
persistedMaterial = await readPersistedMaterial();
restoreJobState.verificationReadAttempts = verificationReadAttempts;
if (persistedMaterialMatches(persistedMaterial)) {
if (!derivedMaterialCleanupAttempted) break;
derivedMaterialCleanupStableSamples += 1;
restoreJobState.derivedMaterialCleanupStableSamples =
derivedMaterialCleanupStableSamples;
// A late native IndexedDB transaction can commit immediately
// after the targeted clear. Require two empty readbacks with
// a real scheduling boundary before accepting the repaired
// offline state.
if (derivedMaterialCleanupStableSamples >= 2) break;
} else {
derivedMaterialCleanupStableSamples = 0;
const [persisted, persistedVersions, persistedMutationMacs] =
Array.isArray(persistedMaterial) ? persistedMaterial : [];
if (
!derivedMaterialCleanupAttempted &&
exactSyncKeyMaterialMatches(persisted) &&
Array.isArray(persistedVersions) &&
Array.isArray(persistedMutationMacs) &&
!derivedAppStateMaterialEmpty(
persistedVersions,
persistedMutationMacs,
)
) {
// The authenticated scheduler can finish one already
// queued derived-table transaction after the online idle
// barrier and network fence. Never rewrite the proven
// source keys in this case. Re-prove the state machine is
// still empty twice, clear only rebuildable derived
// tables once, and keep the exact verification bounded.
const firstDerivedCleanupWindow = readRestoreWindow();
if (!restoreWindowIsEmpty(firstDerivedCleanupWindow)) {
fail(
'wwebjs_canonical_app_state_restore_window_changed',
);
}
await withDeadline(
() => new Promise((resolve) => setTimeout(resolve, 50)),
'wwebjs_canonical_app_state_restore_window_changed',
);
const secondDerivedCleanupWindow = readRestoreWindow();
if (!restoreWindowIsEmpty(secondDerivedCleanupWindow)) {
fail(
'wwebjs_canonical_app_state_restore_window_changed',
);
}
derivedMaterialObservedVersionCount =
persistedVersions.length;
derivedMaterialObservedMutationMacCount =
persistedMutationMacs.length;
await withControlledStage(
() =>
Promise.all([
collectionVersionTable.clear(),
syncActionsTable.clear(),
pendingMutationsTable.clear(),
missingKeysTable.clear(),
]),
'wwebjs_canonical_app_state_material_clear_failed',
);
derivedMaterialCleanupAttempted = true;
restoreJobState.derivedMaterialCleanupAttempted = true;
restoreJobState.derivedMaterialObservedVersionCount =
derivedMaterialObservedVersionCount;
restoreJobState.derivedMaterialObservedMutationMacCount =
derivedMaterialObservedMutationMacCount;
}
}
const remainingVerificationMs = verificationDeadline - Date.now();
if (remainingVerificationMs > 0) {
await new Promise((resolve) =>
setTimeout(resolve, Math.min(100, remainingVerificationMs)),
);
}
} while (Date.now() < verificationDeadline);
const verificationWindowMs = Date.now() - verificationStartedAt;
restoreJobState.verificationWindowMs = verificationWindowMs;
if (!persistedMaterialMatches(persistedMaterial)) {
const [persisted, persistedVersions, persistedMutationMacs] =
Array.isArray(persistedMaterial) ? persistedMaterial : [];
const persistedRows = Array.isArray(persisted) ? persisted : [];
const matchedKeyIds = decodedRows.filter((expected) =>
persistedRows.some((candidate) =>
bytesEqual(candidate?.keyId, expected.keyId),
),
).length;
const exactRows = decodedRows.filter((expected) =>
persistedRows.some(
(candidate) =>
bytesEqual(candidate?.keyId, expected.keyId) &&
bytesEqual(candidate?.keyData, expected.keyData) &&
Number(candidate?.timestamp) === expected.timestamp &&
fingerprintEqual(
candidate?.fingerprint,
expected.fingerprint,
),
),
).length;
window.__WWebCanonicalAppStateRestoreDiagnosticV1 = {
stage: 'sync_key_exact_verification',
expectedSyncKeyCount: decodedRows.length,
observedSyncKeyCount: persistedRows.length,
matchedKeyIdCount: matchedKeyIds,
exactSyncKeyCount: exactRows,
observedVersionCount: Array.isArray(persistedVersions)
? persistedVersions.length
: -1,
observedMutationMacCount: Array.isArray(persistedMutationMacs)
? persistedMutationMacs.length
: -1,
derivedMaterialCleanupAttempted,
derivedMaterialCleanupStableSamples,
derivedMaterialObservedVersionCount,
derivedMaterialObservedMutationMacCount,
verificationReadAttempts,
verificationWindowMs,
recordedAt: Date.now(),
};
fail('wwebjs_canonical_app_state_sync_key_verification_failed');
}
// sync-actions is not a MAC-only table. Its current private ABI uses
// a mandatory `index` primary key and requires the original semantic
// action payload. The canonical projection intentionally stores only
// the protocol MAC tuple, so fabricating an IndexedDB row here would
// corrupt the app-state journal. Keep derived versions/actions empty
// and let the authenticated product API rebuild them from the source
// sync keys. The strict post-sync comparison still uses the decoded
// source versions and MACs as the acceptance baseline.
restoreJobState.status = 'prepared';
restoreJobState.preparedAt = Date.now();
restoreJobState.lastProgressAt = restoreJobState.preparedAt;
return {
installed_sync_key_count: decodedRows.length,
verified_sync_key_count: decodedRows.length,
source_app_state_version_count: decodedVersions.length,
source_app_state_mutation_mac_count: decodedMutationMacs.length,
installed_app_state_version_count: 0,
verified_app_state_version_count: 0,
installed_app_state_mutation_mac_count: 0,
verified_app_state_mutation_mac_count: 0,
app_state_materialization_mode: 'sync_keys_then_official_full_sync',
full_sync_triggered: false,
full_sync_reused: false,
full_sync_status: 'prepared',
sync_job_generation: generation,
sync_api_methods: syncApiMethods,
state_machine_reset: true,
sync_key_verification_read_attempts: verificationReadAttempts,
sync_key_verification_window_ms: verificationWindowMs,
...(derivedMaterialCleanupAttempted
? {
derived_material_cleanup_attempted: true,
derived_material_cleanup_stable_samples:
derivedMaterialCleanupStableSamples,
derived_material_observed_version_count:
derivedMaterialObservedVersionCount,
derived_material_observed_mutation_mac_count:
derivedMaterialObservedMutationMacCount,
}
: {}),
};
} catch (error) {
const code = String(error?.message || '');
if (
restoreJobState &&
window.__WWebCanonicalAppStateSyncJobV1 === restoreJobState
) {
if (destructiveMutationStarted) {
restoreJobState.status = 'failed';
restoreJobState.completedAt = Date.now();
restoreJobState.failureReason = code || 'restore_failed';
} else if (previousSyncJob === undefined) {
delete window.__WWebCanonicalAppStateSyncJobV1;
} else {
window.__WWebCanonicalAppStateSyncJobV1 = previousSyncJob;
}
}
if (
destructiveMutationStarted ||
code === 'wwebjs_canonical_app_state_sync_key_write_timeout' ||
code ===
'wwebjs_canonical_app_state_sync_key_verification_timeout' ||
code ===
'wwebjs_canonical_app_state_sync_key_verification_failed' ||
code === 'wwebjs_canonical_app_state_material_clear_failed' ||
code === 'wwebjs_canonical_app_state_material_readback_failed'
) {
window.__WWebCanonicalAppStateRestorePoisonV1 = {
poisoned: true,
failedAt: Date.now(),
code,
};
}
if (/^wwebjs_canonical_/u.test(String(error?.message || ''))) {
throw error;
}
fail('wwebjs_canonical_app_state_restore_failed');
}
}
/* This function is serialized by Puppeteer and executes in the page realm. */
async function triggerPreparedCanonicalAppStateSync(options) {
const fail = (code) => {
throw new Error(code);
};
const requireModule = (name) => {
try {
const value = window.require?.(name);
if (value) return value.default || value;
} catch {
// Converted to a stable compatibility error below.
}
fail('wwebjs_canonical_app_state_sync_abi_incompatible');
};
const generation = options?.generation;
const syncJobTtlMs = options?.syncJobTtlMs ?? 120000;
if (
!Number.isSafeInteger(generation) ||
generation <= 0 ||
!Number.isSafeInteger(syncJobTtlMs) ||
syncJobTtlMs < 1000 ||
syncJobTtlMs > 600000
) {
fail('wwebjs_canonical_app_state_sync_generation_invalid');
}
if (globalThis.navigator?.onLine !== true) {
fail('wwebjs_canonical_app_state_sync_requires_network');
}
const jobState = window.__WWebCanonicalAppStateSyncJobV1;
if (jobState?.status !== 'prepared' || jobState.generation !== generation) {
fail('wwebjs_canonical_app_state_sync_generation_mismatch');
}
const syncd = requireModule('WAWebSyncd');
const syncdConst = requireModule('WAWebSyncdConst');
if (typeof syncd?.markCollectionsForSync !== 'function') {
fail('wwebjs_canonical_app_state_sync_abi_incompatible');
}
const collectionNames = [
syncdConst?.CollectionName?.CriticalBlock,
syncdConst?.CollectionName?.CriticalUnblockLow,
syncdConst?.CollectionName?.Regular,
syncdConst?.CollectionName?.RegularHigh,
syncdConst?.CollectionName?.RegularLow,
];
const expectedCollectionNames = [
'critical_block',
'critical_unblock_low',
'regular',
'regular_high',
'regular_low',
];
if (
JSON.stringify(collectionNames) !==
JSON.stringify(expectedCollectionNames)
) {
fail('wwebjs_canonical_app_state_sync_abi_incompatible');
}
const finalizeJob = (status, reason) => {
if (
window.__WWebCanonicalAppStateSyncJobV1 !== jobState ||
jobState.status !== 'running'
) {
return;
}
if (jobState.timeoutHandle != null) {
clearTimeout(jobState.timeoutHandle);
jobState.timeoutHandle = null;
}
jobState.status = status;
jobState.completedAt = Date.now();
jobState.failureReason = reason;
jobState.cancel = null;
};
jobState.status = 'running';
jobState.startedAt = Date.now();
jobState.expiresAt = jobState.startedAt + syncJobTtlMs;
jobState.lastProgressAt = jobState.startedAt;
jobState.lastProgressSignature = null;
jobState.redrivePending = false;
jobState.blockedRedriveResolved = false;
jobState.blockedRedriveCollectionNames = [];
jobState.blockedQuiescentSince = null;
jobState.timeoutHandle = setTimeout(() => {
const cancel = jobState.cancel;
if (typeof cancel === 'function') {
try {
cancel();
} catch {
// The generation fence still detaches the timed-out job.
}
}
finalizeJob('timed_out', 'deadline');
}, syncJobTtlMs);
let job;
try {
// The official scheduler owns pagination and follow-up ticks. Starting
// it only after the network fence is released avoids treating an
// expected offline rejection as a permanent sync failure.
job = syncd.markCollectionsForSync(collectionNames);
const queuedCollectionNames = (value) => {
const entries =
value instanceof Map
? Array.from(value.keys())
: value instanceof Set || Array.isArray(value)
? Array.from(value)
: value != null &&
typeof value[Symbol.iterator] === 'function'
? Array.from(value)
: [];
return entries.filter(
(name) =>
typeof name === 'string' && collectionNames.includes(name),
);
};
const observedCollectionNames = [
...new Set([
...queuedCollectionNames(syncd.getPendingCollections?.()),
...queuedCollectionNames(syncd.getInFlightCollections?.()),
]),
].sort();
if (observedCollectionNames.length > 0) {
jobState.workObserved = true;
jobState.observedCollectionNames = observedCollectionNames;
}
} catch {
finalizeJob('failed', 'trigger_failed');
fail('wwebjs_canonical_app_state_sync_trigger_failed');
}
if (typeof job?.cancel === 'function') {
jobState.cancel = () => job.cancel();
} else if (typeof job?.abort === 'function') {
jobState.cancel = () => job.abort();
}
Promise.resolve(job).then(
() => {
if (
window.__WWebCanonicalAppStateSyncJobV1 === jobState &&
jobState.status === 'running'
) {
jobState.promiseSettled = true;
jobState.promiseSettledAt = Date.now();
}
},
() => finalizeJob('failed', 'scheduler_rejected'),
);
return {
full_sync_triggered: true,
full_sync_status: 'running',
sync_job_generation: generation,
collection_count: collectionNames.length,
};
}
/* This function is serialized by Puppeteer and executes in the page realm. */
async function observeCanonicalAppStateSyncJob(options) {
const job = window.__WWebCanonicalAppStateSyncJobV1;
const expectedCollectionNames = new Set([
'critical_block',
'critical_unblock_low',
'regular',
'regular_high',
'regular_low',
]);
const safeStoredCollectionNames = (value) =>
Array.isArray(value)
? [...new Set(value)]
.filter(
(name) =>
typeof name === 'string' &&
expectedCollectionNames.has(name),
)
.sort()
: [];
const knownStatuses = [
'restoring',
'prepared',
'running',
'completed',
'failed',
'native_stalled',
'timed_out',
];
const result = {
present: knownStatuses.includes(job?.status),
status: knownStatuses.includes(job?.status) ? job.status : 'unknown',
generation:
Number.isSafeInteger(job?.generation) && job.generation > 0
? job.generation
: null,
observed_collection_names: safeStoredCollectionNames(
job?.observedCollectionNames,
),
projection_work_observed_collection_names: safeStoredCollectionNames(
job?.projectionWorkObservedCollectionNames,
),
requested_collection_names: safeStoredCollectionNames(
job?.requestedCollectionNames,
),
};
if (!result.present || result.status !== 'running') return result;
let syncd;
let stateMachine;
try {
syncd = window.require?.('WAWebSyncd');
stateMachine = window.require?.('WAWebSyncdCollectionsStateMachine');
} catch {
return { ...result, abi_compatible: false };
}
const requiredFunctions = [
syncd?.getInFlightCollections,
syncd?.getPendingCollections,
syncd?.markCollectionsForSync,
syncd?.syncBlockedCollections,
stateMachine?.getCollectionsInStateDirty,
stateMachine?.getCollectionsInStateRetry,
stateMachine?.getCollectionsInStateFatal,
stateMachine?.getCollectionsInStateBlocked,
];
if (
requiredFunctions.some((operation) => typeof operation !== 'function')
) {
return { ...result, abi_compatible: false };
}
const asCollectionNames = (value) => {
let entries;
if (value instanceof Map) {
entries = Array.from(value.keys());
} else if (value instanceof Set || Array.isArray(value)) {
entries = Array.from(value);
} else if (
value != null &&
typeof value[Symbol.iterator] === 'function'
) {
entries = Array.from(value);
} else {
throw new Error('collection_state_shape_unknown');
}
const names = entries.map((entry) => {
if (typeof entry === 'string') return entry;
if (typeof entry?.collectionName === 'string') {
return entry.collectionName;
}
if (typeof entry?.collection === 'string') {
return entry.collection;
}
if (typeof entry?.name === 'string') return entry.name;
throw new Error('collection_state_entry_unknown');
});
if (names.some((name) => !expectedCollectionNames.has(name))) {
throw new Error('collection_state_name_unknown');
}
return [...new Set(names)].sort();
};
let pending;
let inFlight;
let dirty;
let retry;
let fatal;
let blocked;
try {
pending = asCollectionNames(syncd.getPendingCollections());
inFlight = asCollectionNames(syncd.getInFlightCollections());
dirty = asCollectionNames(stateMachine.getCollectionsInStateDirty());
retry = asCollectionNames(stateMachine.getCollectionsInStateRetry());
fatal = asCollectionNames(stateMachine.getCollectionsInStateFatal());
blocked = asCollectionNames(
stateMachine.getCollectionsInStateBlocked(),
);
} catch {
return { ...result, abi_compatible: false };
}
const signature = JSON.stringify({
pending,
inFlight,
dirty,
retry,
fatal,
blocked,
});
const now = Date.now();
const stallMs = Math.max(1000, Number(options?.stallMs) || 5000);
const nativeStallMs = Math.max(
15000,
Number(options?.nativeStallMs) || 15000,
);
if (options?.authoritativeProjectionReady === true) {
job.authoritativeProjectionReadyObserved = true;
}
const mergeObservedCollectionNames = (property, names) => {
job[property] = [
...new Set([...safeStoredCollectionNames(job[property]), ...names]),
].sort();
};
const projectionEvidenceNames = options?.projectionWorkObservedCollections;
if (projectionEvidenceNames !== undefined) {
if (
!Array.isArray(projectionEvidenceNames) ||
projectionEvidenceNames.some(
(name) =>
typeof name !== 'string' ||
!expectedCollectionNames.has(name),
)
) {
return { ...result, abi_compatible: false };
}
mergeObservedCollectionNames(
'projectionWorkObservedCollectionNames',
projectionEvidenceNames,
);
}
if (
pending.length > 0 ||
inFlight.length > 0 ||
dirty.length > 0 ||
retry.length > 0 ||
blocked.length > 0
) {
job.workObserved = true;
mergeObservedCollectionNames('observedCollectionNames', [
...pending,
...inFlight,
...dirty,
...retry,
...blocked,
]);
}
if (job.lastProgressSignature !== signature) {
job.lastProgressSignature = signature;
job.lastProgressAt = now;
job.quiescentSampleCount = 0;
job.blockedQuiescentSince = null;
}
const finalize = (status, reason) => {
if (job.timeoutHandle != null) {
clearTimeout(job.timeoutHandle);
job.timeoutHandle = null;
}
job.status = status;
job.completedAt = Date.now();
job.failureReason = reason;
job.cancel = null;
};
let redriveAction;
const blockedRedriveCollectionNames = Array.isArray(
job.blockedRedriveCollectionNames,
)
? job.blockedRedriveCollectionNames
: [];
const blockedQuiescentCandidate =
blocked.length > 0 &&
job.blockedRedriveResolved === true &&
JSON.stringify(blocked) ===
JSON.stringify(blockedRedriveCollectionNames);
const activeQueuesEmpty =
pending.length === 0 &&
inFlight.length === 0 &&
dirty.length === 0 &&
retry.length === 0;
const nativeInFlightStalled =
inFlight.length > 0 &&
pending.length === 0 &&
retry.length === 0 &&
fatal.length === 0 &&
blocked.length === 0 &&
inFlight.every((name) => dirty.includes(name)) &&
now - (job.lastProgressAt || now) >= nativeStallMs;
const authoritativeProjectionReady =
job.authoritativeProjectionReadyObserved === true &&
job.workObserved === true;
if (fatal.length > 0) {
finalize('failed', 'fatal_collection');
} else if (authoritativeProjectionReady) {
// The host only raises this acknowledgement after two complete,
// mutually equivalent canonical exports satisfy the source
// version/hash/MAC baseline. WAWebSyncd can leave its returned
// promise and in-flight/retry registries unresolved even though all
// server material has already landed. The retry state is not trusted
// by itself at this boundary: the host has already proved the source
// baseline and rejected fatal/divergent projections. Cancel that stale
// scheduler handle and reset only its collection state instead of
// reloading the authenticated document and waiting another transport
// deadline.
if (typeof stateMachine.clean !== 'function') {
finalize('failed', 'authoritative_clean_unavailable');
}
const cancel = job.cancel;
if (typeof cancel === 'function') {
try {
cancel();
} catch {
finalize('failed', 'authoritative_cancel_failed');
}
}
if (job.status === 'running') {
try {
stateMachine.clean();
finalize('completed', 'authoritative_projection_stable');
} catch {
finalize('failed', 'authoritative_clean_failed');
}
}
} else if (nativeInFlightStalled) {
// WAWebSyncd keeps the request in its realm-local in-flight set. Once
// every in-flight collection is still dirty and no queue/state has
// changed for the bounded threshold, neither the public redrive nor
// syncBlockedCollections can detach that request. Mark this condition
// distinctly so the host can perform its one allowed fresh-realm
// recovery instead of waiting for the generic job deadline.
finalize('native_stalled', 'in_flight_dirty_no_progress');
} else if (
job.redrivePending !== true &&
activeQueuesEmpty &&
(blocked.length === 0 || blockedQuiescentCandidate)
) {
if (
blockedQuiescentCandidate &&
!Number.isSafeInteger(job.blockedQuiescentSince)
) {
job.blockedQuiescentSince = now;
}
const authoritativeIdleProjectionReady =
job.authoritativeProjectionReadyObserved === true &&
job.promiseSettled === true;
const completionEvidence =
job.workObserved === true ||
safeStoredCollectionNames(job.projectionWorkObservedCollectionNames)
.length > 0 ||
authoritativeIdleProjectionReady;
if (completionEvidence) {
job.quiescentSampleCount = (job.quiescentSampleCount || 0) + 1;
const blockedStableForMs = blockedQuiescentCandidate
? now - job.blockedQuiescentSince
: 0;
if (
job.quiescentSampleCount >= 2 &&
(!blockedQuiescentCandidate || blockedStableForMs >= 1000)
) {
finalize('completed');
}
} else {
job.quiescentSampleCount = 0;
}
} else {
job.quiescentSampleCount = 0;
job.blockedQuiescentSince = null;
const stalledForMs = now - (job.lastProgressAt || now);
let redrive;
let redriveCollectionNames = [];
if (
stalledForMs >= stallMs &&
blocked.length > 0 &&
job.blockedRedriveAttempted !== true
) {
job.blockedRedriveAttempted = true;
job.lastProgressAt = now;
redriveAction = 'blocked';
redriveCollectionNames = [...blocked];
} else if (
stalledForMs >= stallMs &&
retry.length > 0 &&
pending.length === 0 &&
inFlight.length === 0 &&
job.retryRedriveAttempted !== true
) {
job.retryRedriveAttempted = true;
job.lastProgressAt = now;
redriveAction = 'retry';
redriveCollectionNames = [...retry];
} else if (
stalledForMs >= stallMs &&
dirty.length > 0 &&
pending.length === 0 &&
inFlight.length === 0 &&
job.dirtyRedriveAttempted !== true
) {
job.dirtyRedriveAttempted = true;
job.lastProgressAt = now;
redriveAction = 'dirty';
redriveCollectionNames = [...dirty];
}
if (redriveAction) {
job.redrivePending = true;
const settleRedrive = (failed) => {
if (
window.__WWebCanonicalAppStateSyncJobV1 === job &&
job.status === 'running'
) {
job.redrivePending = false;
job.quiescentSampleCount = 0;
job.lastProgressAt = Date.now();
job.lastProgressSignature = null;
job.blockedQuiescentSince = null;
if (failed) {
finalize('failed', 'redrive_failed');
} else if (redriveAction === 'blocked') {
job.blockedRedriveResolved = true;
job.blockedRedriveCollectionNames =
redriveCollectionNames;
}
}
};
try {
redrive =
redriveAction === 'blocked'
? syncd.syncBlockedCollections()
: syncd.markCollectionsForSync(redriveCollectionNames);
} catch {
settleRedrive(true);
}
if (job.status === 'running' && job.redrivePending === true) {
if (redrive && typeof redrive.then === 'function') {
Promise.resolve(redrive).then(
() => settleRedrive(false),
() => settleRedrive(true),
);
} else {
settleRedrive(false);
}
}
}
}
return {
...result,
status: knownStatuses.includes(job.status) ? job.status : 'unknown',
abi_compatible: true,
pending_count: pending.length,
in_flight_count: inFlight.length,
dirty_count: dirty.length,
retry_count: retry.length,
fatal_count: fatal.length,
blocked_count: blocked.length,
blocked_collection_names: blocked,
blocked_quiescent:
job.status === 'completed' && blockedQuiescentCandidate === true,
passive_blocked_collections:
job.status === 'completed' && blockedQuiescentCandidate === true
? blocked
: [],
redrive_pending: job.redrivePending === true,
redrive_action: redriveAction,
quiescent_sample_count: job.quiescentSampleCount || 0,
work_observed: job.workObserved === true,
observed_collection_names: safeStoredCollectionNames(
job.observedCollectionNames,
),
projection_work_observed_collection_names: safeStoredCollectionNames(
job.projectionWorkObservedCollectionNames,
),
authoritative_projection_ready_observed:
job.authoritativeProjectionReadyObserved === true,
promise_settled: job.promiseSettled === true,
native_stalled_for_ms: nativeInFlightStalled
? now - (job.lastProgressAt || now)
: 0,
native_stall_threshold_ms: nativeStallMs,
};
}
/* This function is serialized by Puppeteer and executes in the page realm. */
async function triggerCanonicalInitialAppStateFullSync(options) {
const fail = (code) => {
throw new Error(code);
};
const requireModule = (name) => {
try {
const value = window.require?.(name);
if (value) return value.default || value;
} catch {
// Converted to a stable compatibility error below.
}
fail('wwebjs_canonical_initial_app_state_sync_abi_incompatible');
};
let deadline = 0;
const withDeadline = (operation, code) =>
new Promise((resolve, reject) => {
const remainingMs = deadline - Date.now();
if (remainingMs <= 0) {
reject(new Error(code));
return;
}
const timer = setTimeout(
() => reject(new Error(code)),
remainingMs,
);
let pending;
try {
pending = operation();
} catch (error) {
clearTimeout(timer);
reject(error);
return;
}
Promise.resolve(pending).then(
(value) => {
clearTimeout(timer);
resolve(value);
},
(error) => {
clearTimeout(timer);
reject(error);
},
);
});
try {
const syncJobTtlMs = options?.syncJobTtlMs ?? 120000;
const forceFreshGeneration = options?.forceFreshGeneration === true;
if (
!Number.isSafeInteger(options?.timeoutMs) ||
options.timeoutMs < 1000 ||
options.timeoutMs > 600000 ||
!Number.isSafeInteger(syncJobTtlMs) ||
syncJobTtlMs < 1000 ||
syncJobTtlMs > 600000
) {
fail('wwebjs_canonical_initial_app_state_sync_timeout_invalid');
}
deadline = Date.now() + options.timeoutMs;
const dbEncryption = requireModule('WAWebDbEncryptionKey');
const syncdDb = requireModule('WAWebSyncdDb');
const syncd = requireModule('WAWebSyncd');
const syncdStateMachine = requireModule(
'WAWebSyncdCollectionsStateMachine',
);
const syncdConst = requireModule('WAWebSyncdConst');
const waitForFinalKey =
dbEncryption?.DbEncKeyStore?.waitForFinalDbMsgEncKey;
if (
typeof waitForFinalKey !== 'function' ||
typeof syncdDb?.getAllSyncKeys !== 'function' ||
typeof syncd?.markCollectionsForSync !== 'function' ||
typeof syncd?.getInFlightCollections !== 'function' ||
typeof syncd?.getPendingCollections !== 'function' ||
typeof syncd?.syncBlockedCollections !== 'function' ||
typeof syncdStateMachine?.getCollectionsInStateDirty !==
'function' ||
typeof syncdStateMachine?.getCollectionsInStateRetry !==
'function' ||
typeof syncdStateMachine?.getCollectionsInStateFatal !==
'function' ||
typeof syncdStateMachine?.getCollectionsInStateBlocked !==
'function'
) {
fail('wwebjs_canonical_initial_app_state_sync_abi_incompatible');
}
const collectionNames = [
syncdConst?.CollectionName?.CriticalBlock,
syncdConst?.CollectionName?.CriticalUnblockLow,
syncdConst?.CollectionName?.Regular,
syncdConst?.CollectionName?.RegularHigh,
syncdConst?.CollectionName?.RegularLow,
];
const expectedCollectionNames = [
'critical_block',
'critical_unblock_low',
'regular',
'regular_high',
'regular_low',
];
if (
collectionNames.some(
(name) => typeof name !== 'string' || name.length === 0,
) ||
new Set(collectionNames).size !== collectionNames.length ||
JSON.stringify(collectionNames) !==
JSON.stringify(expectedCollectionNames)
) {
fail('wwebjs_canonical_initial_app_state_sync_abi_incompatible');
}
await withDeadline(
() => waitForFinalKey.call(dbEncryption.DbEncKeyStore),
'wwebjs_canonical_initial_app_state_db_key_timeout',
);
const persistedSyncKeys = await withDeadline(
() => syncdDb.getAllSyncKeys(),
'wwebjs_canonical_initial_app_state_sync_key_timeout',
);
if (
!Array.isArray(persistedSyncKeys) ||
persistedSyncKeys.length === 0
) {
fail('wwebjs_canonical_initial_app_state_sync_keys_missing');
}
const cancelRunningJob = (state, status = 'timed_out') => {
if (!state || state.status !== 'running') return false;
if (state.timeoutHandle != null) {
clearTimeout(state.timeoutHandle);
state.timeoutHandle = null;
}
state.status = status;
state.completedAt = Date.now();
state.cancelledAt = state.completedAt;
const cancel = state.cancel;
state.cancel = null;
if (typeof cancel === 'function') {
try {
cancel();
} catch {
// The generation fence below still detaches this attempt.
}
}
return true;
};
const armJobTimeout = (state) => {
const remainingMs = state.expiresAt - Date.now();
if (remainingMs <= 0) {
cancelRunningJob(state);
return;
}
if (state.timeoutHandle != null) return;
state.timeoutHandle = setTimeout(() => {
if (
window.__WWebCanonicalAppStateSyncJobV1 !== state ||
state.status !== 'running'
) {
return;
}
cancelRunningJob(state);
}, remainingMs);
};
const existingJob = window.__WWebCanonicalAppStateSyncJobV1;
if (existingJob?.status === 'failed') {
fail('wwebjs_canonical_app_state_sync_job_failed');
}
if (existingJob?.status === 'running') {
if (!Number.isSafeInteger(existingJob.expiresAt)) {
const startedAt = Number.isSafeInteger(existingJob.startedAt)
? existingJob.startedAt
: Date.now() - syncJobTtlMs;
existingJob.expiresAt = startedAt + syncJobTtlMs;
}
if (existingJob.expiresAt > Date.now()) {
armJobTimeout(existingJob);
return {
current_sync_key_count: persistedSyncKeys.length,
full_sync_triggered: false,
full_sync_reused: true,
full_sync_status: 'running',
sync_job_generation: existingJob.generation,
};
}
cancelRunningJob(existingJob);
fail('wwebjs_canonical_app_state_sync_job_restart_required');
} else if (existingJob?.status === 'timed_out') {
fail('wwebjs_canonical_app_state_sync_job_restart_required');
} else if (
existingJob?.status === 'completed' &&
!forceFreshGeneration
) {
return {
current_sync_key_count: persistedSyncKeys.length,
full_sync_triggered: false,
full_sync_reused: true,
full_sync_status: 'completed',
sync_job_generation: existingJob.generation,
};
}
const generation =
Number.isSafeInteger(existingJob?.generation) &&
existingJob.generation >= 0
? existingJob.generation + 1
: 1;
const jobState = {
generation,
status: 'running',
startedAt: Date.now(),
expiresAt: Date.now() + syncJobTtlMs,
completedAt: null,
cancelledAt: null,
cancel: null,
timeoutHandle: null,
promiseSettled: false,
quiescentSampleCount: 0,
lastProgressSignature: null,
lastProgressAt: Date.now(),
blockedRedriveAttempted: false,
dirtyRedriveAttempted: false,
redrivePending: false,
blockedRedriveResolved: false,
blockedRedriveCollectionNames: [],
blockedQuiescentSince: null,
workObserved: false,
observedCollectionNames: [],
projectionWorkObservedCollectionNames: [],
requestedCollectionNames: [...collectionNames],
authoritativeProjectionReadyObserved: false,
};
window.__WWebCanonicalAppStateSyncJobV1 = jobState;
let job;
try {
job = syncd.markCollectionsForSync(collectionNames);
const queuedCollectionNames = (value) => {
const entries =
value instanceof Map
? Array.from(value.keys())
: value instanceof Set || Array.isArray(value)
? Array.from(value)
: value != null &&
typeof value[Symbol.iterator] === 'function'
? Array.from(value)
: [];
return entries.filter(
(name) =>
typeof name === 'string' &&
collectionNames.includes(name),
);
};
const observedCollectionNames = [
...new Set([
...queuedCollectionNames(syncd.getPendingCollections()),
...queuedCollectionNames(syncd.getInFlightCollections()),
]),
].sort();
if (observedCollectionNames.length > 0) {
jobState.workObserved = true;
jobState.observedCollectionNames = observedCollectionNames;
}
} catch {
jobState.status = 'failed';
jobState.completedAt = Date.now();
fail('wwebjs_canonical_initial_app_state_sync_trigger_failed');
}
if (typeof job?.cancel === 'function') {
jobState.cancel = () => job.cancel();
} else if (typeof job?.abort === 'function') {
jobState.cancel = () => job.abort();
}
armJobTimeout(jobState);
const finalizeJob = (status) => {
if (
window.__WWebCanonicalAppStateSyncJobV1 !== jobState ||
jobState.status !== 'running'
) {
return;
}
if (jobState.timeoutHandle != null) {
clearTimeout(jobState.timeoutHandle);
jobState.timeoutHandle = null;
}
jobState.status = status;
jobState.completedAt = Date.now();
jobState.cancel = null;
};
Promise.resolve(job).then(
() => {
if (
window.__WWebCanonicalAppStateSyncJobV1 === jobState &&
jobState.status === 'running'
) {
jobState.promiseSettled = true;
jobState.promiseSettledAt = Date.now();
}
},
() => finalizeJob('failed'),
);
return {
current_sync_key_count: persistedSyncKeys.length,
full_sync_triggered: true,
full_sync_reused: false,
full_sync_status: 'running',
sync_job_generation: generation,
};
} catch (error) {
if (/^wwebjs_canonical_/u.test(String(error?.message || ''))) {
throw error;
}
fail('wwebjs_canonical_initial_app_state_sync_trigger_failed');
}
}
function inspectConnectedProfileSnapshot(expectedDocumentEpoch) {
const resolveModule = (name) => {
try {
const value = window.require?.(name);
return value?.default || value;
} catch {
return undefined;
}
};
const socket = resolveModule('WAWebSocketModel')?.Socket;
const comms = resolveModule('WAComms');
const multiDevice = resolveModule('WAWebUserPrefsMultiDevice');
const conn = resolveModule('WAWebConnModel')?.Conn;
const documentEpoch = window.performance?.timeOrigin;
const socketState = String(
socket?.state ?? socket?.get?.('state') ?? 'unknown',
);
const registered = multiDevice?.isRegistered?.() === true;
return {
document_epoch: documentEpoch,
document_epoch_valid:
expectedDocumentEpoch === undefined ||
documentEpoch === expectedDocumentEpoch,
online: navigator.onLine === true,
registered,
socket_state: socketState,
comms_initialized: comms?.isCommsInitialized?.() === true,
socket_connected: comms?.isSocketConnected?.() === true,
pairing_reference_active:
Boolean(conn?.ref) &&
(!registered ||
['UNPAIRED', 'UNPAIRED_IDLE'].includes(socketState)),
};
}
// Puppeteer keeps its network emulation state on the Page/CDP target, not on
// a BrowserSessionBridge instance. RemoteAuth intentionally reuses the same
// Page after a server-side logout, so a replacement bridge must inherit the
// offline fence left by close() and explicitly release it before navigating.
// A WeakSet keeps that ownership process-local without retaining closed pages.
const bridgeQuiescedPages = new WeakSet();
class BrowserSessionBridge {
constructor({
page,
expectedVersion,
enforceExpectedVersion = false,
logger,
onCriticalMutation,
onAdvSecretCaptured,
onCanonicalProviderState,
onCanonicalCredentialViolation,
enableConnectionDiagnostics = true,
allowNoiseMetadataBootstrap = false,
flushIntervalMs = 2000,
maxJournalEvents = 1000,
maxProjectionBytes = DEFAULT_MAX_PROJECTION_BYTES,
maxProjectionRecords = DEFAULT_MAX_PROJECTION_RECORDS,
maxCanonicalBytes = DEFAULT_MAX_CANONICAL_BYTES,
maxCanonicalRecords = DEFAULT_MAX_CANONICAL_RECORDS,
}) {
this.page = page;
this.expectedVersion = expectedVersion;
this.enforceExpectedVersion = enforceExpectedVersion;
this.logger = logger;
this.onCriticalMutation = onCriticalMutation;
this.onAdvSecretCaptured = onAdvSecretCaptured;
this.onCanonicalProviderState = onCanonicalProviderState;
this.onCanonicalCredentialViolation = onCanonicalCredentialViolation;
this.enableConnectionDiagnostics = enableConnectionDiagnostics;
this.connectionDiagnosticCounts = new Map();
this.allowNoiseMetadataBootstrap = allowNoiseMetadataBootstrap;
this.flushIntervalMs = flushIntervalMs;
this.maxJournalEvents = maxJournalEvents;
this.maxProjectionBytes = maxProjectionBytes;
this.maxProjectionRecords = maxProjectionRecords;
this.maxCanonicalBytes = maxCanonicalBytes;
this.maxCanonicalRecords = maxCanonicalRecords;
this.flushTail = Promise.resolve();
this.canonicalDirtyGeneration = 0;
this.canonicalDirtyTables = new Map();
this.canonicalDeviceDirtyGeneration = 0;
this.canonicalFullReplaceGeneration = 0;
this.lastCanonicalJournalMutationAt = 0;
this.canonicalRealtimeGate = undefined;
this.bridgeInstallOptions = undefined;
this.canonicalReloadProof = undefined;
this.canonicalAppStateRealmRecoveryCount = 0;
this.canonicalPreservedNativeAppState = false;
this.canonicalCredentialViolationDetected = false;
this.canonicalProfileSnapshotFreeze = undefined;
this.canonicalOnlineRegisteredLaunchRequired = false;
this.networkQuiesced = bridgeQuiescedPages.has(page);
}
log(event, fields, options) {
try {
this.logger?.log(event, fields, options);
} catch {
// Observability is best-effort and must never change session state.
}
}
async install() {
await exposeFunctionIfAbsent(
this.page,
'__wwebjsSessionJournalNotify',
async () => {
if (this.criticalNotifyPending) return;
this.criticalNotifyPending = true;
try {
await this.flush();
this.onCriticalMutation?.();
} catch (error) {
this.log(
'browser_bridge.flush_failed',
{ error },
{ force: true },
);
throw error;
} finally {
this.criticalNotifyPending = false;
}
},
);
await exposeFunctionIfAbsent(
this.page,
'__wwebjsAdvSecretCapture',
async (encodedSecret) => {
let secret;
try {
if (
typeof encodedSecret !== 'string' ||
!/^[A-Za-z0-9+/]+={0,2}$/.test(encodedSecret)
) {
return false;
}
secret = Buffer.from(encodedSecret, 'base64');
if (secret.length !== 32) return false;
if (
this.capturedAdvSecret?.length === secret.length &&
this.capturedAdvSecret.equals(secret)
) {
this.log(
'browser_bridge.adv_secret_capture_unchanged',
{ byte_length: 32 },
);
return true;
}
this.capturedAdvSecret?.fill(0);
this.capturedAdvSecret = Buffer.from(secret);
const callbackSecret = Buffer.from(secret);
try {
await this.onAdvSecretCaptured?.(callbackSecret);
} finally {
callbackSecret.fill(0);
}
this.log('browser_bridge.adv_secret_captured', {
byte_length: 32,
});
this.canonicalDirtyGeneration += 1;
this.canonicalDeviceDirtyGeneration =
this.canonicalDirtyGeneration;
this.onCriticalMutation?.();
return true;
} finally {
secret?.fill(0);
}
},
);
await exposeFunctionIfAbsent(
this.page,
'__wwebjsSessionConnectionDiagnosticNotify',
async (event) => {
const allowed = {
credential_clear: new Set([
'clear_registration_info',
'clear_registration_info_suppressed',
'clear_registration_info_detected',
'me_pn_cleared',
'me_pn_cleared_suppressed',
'me_pn_cleared_detected',
'me_lid_cleared',
'me_lid_cleared_suppressed',
'me_lid_cleared_detected',
'adv_identity_cleared',
'adv_identity_cleared_suppressed',
'adv_identity_cleared_detected',
'socket_logout_suppressed',
'socket_logout_detected',
'socket_clear_credentials_suppressed',
'socket_clear_credentials_detected',
'socket_clear_credentials_and_stored_data_suppressed',
'socket_clear_credentials_and_stored_data_detected',
'companion_start_logout_suppressed',
'companion_start_logout_detected',
'companion_logout_after_validation_fail_suppressed',
'companion_logout_after_validation_fail_detected',
'socket_logout_job_suppressed',
'socket_logout_job_detected',
'signal_storage_destroy_suppressed',
'signal_storage_destroy_detected',
'crypto_keys_delete_suppressed',
'crypto_keys_delete_detected',
'crypto_salt_delete_suppressed',
'crypto_salt_delete_detected',
'clear_all_local_state_suppressed',
'clear_all_local_state_detected',
]),
socket_state: new Set([
'conflict',
'connected',
'deprecated_version',
'opening',
'pairing',
'proxyblock',
'smb_tos_block',
'timeout',
'tos_block',
'unlaunched',
'unpaired',
'unpaired_idle',
'unknown',
]),
socket_stream: new Set([
'connected',
'disconnected',
'opening',
'other',
'pairing',
'resuming',
'syncing',
'unpaired',
]),
stream_availability: new Set(['available', 'unavailable']),
logout_reason: new Set([
'unknown_companion',
'primary_identity_key_change',
'invalid_adv_status',
'client_version_outdated',
'md_opt_out',
'account_locked',
'lid_migration_failure',
'sync_failure',
'local_storage_failure',
'user_initiated',
'other_known',
'unknown',
]),
logout_code: new Set([
'client_fatal',
'sync_fail',
'initial_history_sync_timeout',
'account_locked',
'none',
'unknown',
]),
custom_logout_message: new Set(['present', 'absent']),
comms_action: new Set([
'close_socket',
'close_socket_and_pause',
'close_socket_and_prevent_retry',
'close_socket_and_resume',
'force_abort_socket',
'force_reset_socket_loop',
'maybe_reset_socket_loop',
'open_socket_loop',
]),
socket_close_code: new Set([
'normal',
'going_away',
'protocol_error',
'abnormal',
'internal_error',
'service_restart',
'try_again_later',
'tls_failure',
'other_valid',
'unavailable',
]),
backend_event: new Set([
'reconnect_socket',
'socket_stream_disconnected',
'open_socket_stream',
'set_socket_state',
'storage_initialization_error',
]),
};
if (!allowed[event?.kind]?.has(event?.category)) return false;
const key = `${event.kind}:${event.category}`;
const count =
(this.connectionDiagnosticCounts.get(key) || 0) + 1;
this.connectionDiagnosticCounts.set(key, count);
this.log(
'browser_bridge.connection_diagnostic',
{
diagnostic_kind: event.kind,
reason_category: event.category,
count,
},
{
force: [
'credential_clear',
'logout_reason',
'logout_code',
'custom_logout_message',
'comms_action',
'socket_close_code',
'backend_event',
].includes(event.kind),
},
);
return true;
},
);
await exposeFunctionIfAbsent(
this.page,
'__wwebjsCanonicalCredentialViolationNotify',
async (event) => {
const allowed = new Set([
'socket_logout',
'socket_clear_credentials',
'socket_clear_credentials_and_stored_data',
'companion_start_logout',
'companion_logout_after_validation_fail',
'socket_logout_job',
'signal_storage_destroy',
'crypto_keys_delete',
'crypto_salt_delete',
'clear_all_local_state',
'credential_storage_anchor_removed',
'credential_storage_clear',
'clear_registration_info',
'me_pn_cleared',
'me_lid_cleared',
'adv_identity_cleared',
]);
const category = allowed.has(event?.category)
? event.category
: 'unknown';
this.canonicalCredentialViolationDetected = true;
this.networkQuiesced = true;
await this.page.setOfflineMode(true).catch(() => {});
try {
await this.onCanonicalCredentialViolation?.({ category });
} catch {
// The local offline cut and sticky violation are the
// authoritative fail-closed path; callbacks are optional.
}
this.log(
'browser_bridge.canonical_credential_violation',
{ reason_category: category, offline: true },
{ force: true },
);
return true;
},
);
const options = {
schemaVersion: BRIDGE_SCHEMA_VERSION,
expectedVersion: this.expectedVersion,
maxJournalEvents: this.maxJournalEvents,
enableConnectionDiagnostics: this.enableConnectionDiagnostics,
};
this.bridgeInstallOptions = options;
await this.page.evaluateOnNewDocument(
installBrowserSessionBridge,
options,
);
await this.page.evaluate(installBrowserSessionBridge, options);
this.interval = setInterval(() => {
void this.flush().catch((error) => {
this.log(
'browser_bridge.flush_failed',
{ error },
{ force: true },
);
});
}, this.flushIntervalMs);
this.log('browser_bridge.installed', {
schema_version: BRIDGE_SCHEMA_VERSION,
});
}
async assertCompatible({
waitForModulesMs = 0,
pollIntervalMs = 250,
capabilityStage = 'runtime',
} = {}) {
if (
!Number.isSafeInteger(waitForModulesMs) ||
waitForModulesMs < 0 ||
waitForModulesMs > 60000 ||
!Number.isSafeInteger(pollIntervalMs) ||
pollIntervalMs < 50 ||
pollIntervalMs > 5000 ||
!CANONICAL_CAPABILITY_STAGES.has(capabilityStage)
) {
throw new TypeError('Invalid browser compatibility wait options.');
}
const startedAt = Date.now();
const deadline = Date.now() + waitForModulesMs;
let attemptCount = 0;
let initialIncomplete;
let result;
do {
try {
attemptCount += 1;
result = await this.page.evaluate(
({ expectedVersion, schemaVersion, requiredModules }) => {
const resolveModule = (name) => {
const aliases = {
CryptoEncKeyHelper: [
'CryptoEncKeyHelper',
'WAWebCryptoEncKeyHelper',
],
CryptoEncryptionSaltHelper: [
'CryptoEncryptionSaltHelper',
'WAWebCryptoEncryptionSaltHelper',
],
};
for (const candidate of aliases[name] || [name]) {
try {
const value = window.require?.(candidate);
const resolved = value?.default || value;
if (resolved) return resolved;
} catch {
// Try the next verified alias.
}
}
return undefined;
};
const modules = new Map(
requiredModules.map((name) => [
name,
resolveModule(name),
]),
);
const missingModules = [...modules].filter(
([, value]) => !value,
);
const shapeFailures = [];
let pqStorageMode = 'unknown';
if (missingModules.length === 0) {
const expectFunctions = (scope, value, names) => {
if (
!value ||
names.some(
(name) =>
typeof value[name] !== 'function',
)
) {
shapeFailures.push(scope);
}
};
const signalStorage =
modules.get('WAWebSignalStorage');
const signalStore = modules.get(
'WAWebSignalStoreApi',
)?.waSignalStore;
expectFunctions('signal_store', signalStore, [
'getRegistrationInfo',
'getADVSignedIdentity',
'setRegistrationInfo',
'putSignedPreKeys',
'putADVSignedIdentity',
'putPreKeys',
'bulkPutIdentity',
'bulkPutSession',
'bulkPutSenderKey',
'putMeta',
]);
if (
!(
signalStore.idToSignedPreKey instanceof Map
) ||
typeof signalStore.idToSignedPreKey.clear !==
'function'
) {
shapeFailures.push(
'signal_signed_pre_key_cache',
);
}
expectFunctions('signal_storage', signalStorage, [
'initialize',
'destroy',
'getBaseKeyTable',
'getIdentityTable',
'getMetaTable',
'getPreKeyTable',
'getSessionTable',
'getSignedPreKeyTable',
'getSenderKeyTable',
'getKyberPreKeyTable',
'getKyberLastResortKeyTable',
]);
for (const getterName of [
'getBaseKeyTable',
'getIdentityTable',
'getMetaTable',
'getPreKeyTable',
'getSessionTable',
'getSignedPreKeyTable',
'getSenderKeyTable',
]) {
try {
const table = signalStorage[getterName]();
if (
!table ||
typeof table.clear !== 'function' ||
typeof table.all !== 'function'
) {
shapeFailures.push('signal_table');
}
} catch {
shapeFailures.push('signal_table');
}
}
const pqTables = [];
let missingPqTables = 0;
for (const getterName of [
'getKyberPreKeyTable',
'getKyberLastResortKeyTable',
]) {
try {
const table = signalStorage[getterName]();
if (
!table ||
typeof table.clear !== 'function' ||
typeof table.all !== 'function'
) {
shapeFailures.push('pq_table');
} else {
pqTables.push(table);
}
} catch (error) {
if (error?.name === 'NoSuchTableError') {
missingPqTables += 1;
} else {
shapeFailures.push('pq_table');
}
}
}
const kyberStore = modules.get(
'WAWebKyberPreKeyStore',
);
const pqGating = modules.get('WAWebPQGatingUtils');
expectFunctions('pq_store', kyberStore, [
'isPQMigrated',
'setPQMigrated',
'loadKyberKeysForDigest',
'getUnsentKyberPreKeys',
'loadLatestKyberLastResortKey',
]);
expectFunctions('pq_gating', pqGating, [
'isPqKeysUploadEnabled',
'isPq1on1MessageEnabled',
]);
if (
missingPqTables === 0 &&
pqTables.length === 2
) {
pqStorageMode = 'legacy_tables';
} else if (missingPqTables === 2) {
let inactive = true;
try {
inactive =
!pqGating.isPqKeysUploadEnabled() &&
!pqGating.isPq1on1MessageEnabled();
} catch {
shapeFailures.push('pq_rollout_state');
}
pqStorageMode = inactive
? 'rollout_without_tables'
: 'managed_store';
} else {
shapeFailures.push('pq_table_symmetry');
}
const tableChecks = [
[
'WAWebSchemaSyncKeys',
'getSyncKeysTable',
true,
],
[
'WAWebSchemaCollectionVersion',
'getCollectionVersionTable',
false,
],
[
'WAWebSchemaSyncActions',
'getSyncActionsTable',
false,
],
[
'WAWebSchemaPendingMutations',
'getPendingMutationsTable',
false,
],
[
'WAWebSchemaMissingKeys',
'getMissingKeysTable',
false,
],
];
for (const [
moduleName,
getterName,
writable,
] of tableChecks) {
try {
const schema = modules.get(moduleName);
const table = schema?.[getterName]?.();
if (
!table ||
typeof table.clear !== 'function' ||
(writable &&
typeof table.createOrReplace !==
'function')
) {
shapeFailures.push('app_state_table');
}
} catch {
shapeFailures.push('app_state_table');
}
}
expectFunctions(
'syncd_db',
modules.get('WAWebSyncdDb'),
['getAllSyncKeys', 'createSyncKey'],
);
expectFunctions(
'app_state_sync_job',
modules.get('WAWebSyncd'),
['markCollectionsForSync'],
);
const syncdCollectionNames =
modules.get('WAWebSyncdConst')?.CollectionName;
const requiredSyncdCollections = [
syncdCollectionNames?.CriticalBlock,
syncdCollectionNames?.CriticalUnblockLow,
syncdCollectionNames?.Regular,
syncdCollectionNames?.RegularLow,
syncdCollectionNames?.RegularHigh,
];
if (
requiredSyncdCollections.some(
(name) =>
typeof name !== 'string' ||
name.length === 0,
) ||
new Set(requiredSyncdCollections).size !==
requiredSyncdCollections.length
) {
shapeFailures.push(
'app_state_sync_collections',
);
}
expectFunctions(
'db_message_key',
modules.get('WAWebDbEncryptionKey')
?.DbEncKeyStore,
['waitForFinalDbMsgEncKey'],
);
expectFunctions(
'pq_upload_job',
modules.get('WAWebUploadPQPrekeysJob'),
['generateAndUploadPQPreKeys'],
);
expectFunctions(
'pq_upload_set_rpc',
modules.get('WASmaxPreKeysSetRPC'),
['sendSetRPC'],
);
expectFunctions(
'pq_upload_add_rpc',
modules.get('WASmaxPreKeysAddRPC'),
['sendAddRPC'],
);
expectFunctions(
'result_or_error',
modules.get('WAResultOrError'),
['makeError'],
);
const pqUploadProtocol = modules.get(
'WAPQUploadPreKeysProtocol',
);
expectFunctions(
'pq_upload_protocol',
pqUploadProtocol,
[
'uploadPQPreKeysProtocol',
'addPQPreKeysProtocol',
],
);
try {
const setSource =
Function.prototype.toString.call(
pqUploadProtocol.uploadPQPreKeysProtocol,
);
const addSource =
Function.prototype.toString.call(
pqUploadProtocol.addPQPreKeysProtocol,
);
if (
!/WASmaxPreKeysSetRPC[\s\S]*sendSetRPC/u.test(
setSource,
) ||
!/WASmaxPreKeysAddRPC[\s\S]*sendAddRPC/u.test(
addSource,
)
) {
shapeFailures.push(
'pq_upload_final_sink_binding',
);
}
} catch {
shapeFailures.push(
'pq_upload_final_sink_binding',
);
}
expectFunctions(
'multi_device',
modules.get('WAWebUserPrefsMultiDevice'),
[
'getADVSecretKey',
'setADVSecretKey',
'getRoutingInfo',
'setRoutingInfo',
'isRegistered',
],
);
// Device-specific getters are intentionally not a
// baseline ABI requirement. Older supported builds
// can still be exported through the user-WID
// fallback, while a cross-provider handoff fails
// closed later if no companion device WID exists.
expectFunctions(
'me_preferences',
modules.get('WAWebUserPrefsMeUser'),
['setMe', 'setMeLid'],
);
expectFunctions(
'noise_preferences',
modules.get('WAWebUserPrefsInfoStore')
?.waNoiseInfo,
['get'],
);
const socket =
modules.get('WAWebSocketModel')?.Socket;
const comms = modules.get('WAComms');
const launchSocket =
modules.get('WAWebLaunchSocket');
const backendModule = modules.get(
'WAWebBackendEventBus',
);
const backendEvent = backendModule?.BackendEvent;
const backendEventBus =
backendModule?.BackendEventBus;
expectFunctions('socket_reconnect', socket, [
'reconnect',
'on',
'off',
'logout',
'clearCredentials',
'clearCredentialsAndStoredData',
]);
expectFunctions(
'companion_logout',
modules.get('WAWebCompanionRegUtils'),
['startLogout', 'logoutAfterValidationFail'],
);
expectFunctions(
'socket_logout_job',
modules.get('WAWebSocketLogoutJob'),
['socketLogout'],
);
expectFunctions(
'crypto_key_cleanup',
modules.get('CryptoEncKeyHelper'),
['deleteEncKeysAndCache'],
);
expectFunctions(
'crypto_salt_cleanup',
modules.get('CryptoEncryptionSaltHelper'),
['deleteSalt'],
);
expectFunctions(
'local_state_cleanup',
modules.get('WAWebUserPrefsClearLocalState'),
['clearAllLocalState'],
);
expectFunctions('comms_reconnect', comms, [
'isCommsInitialized',
'closeSocketAndResume',
'stopComms',
]);
expectFunctions(
'registered_socket_launch',
launchSocket,
['launchSocket'],
);
expectFunctions(
'backend_reconnect',
backendEventBus,
['triggerReconnectSocket', 'getListenersCount'],
);
if (
socket?.reconnect?.length !== 0 ||
socket?.logout?.length !== 1 ||
socket?.clearCredentials?.length !== 0 ||
socket?.clearCredentialsAndStoredData
?.length !== 2 ||
modules.get('WAWebCompanionRegUtils')
?.startLogout?.length !== 0 ||
modules.get('WAWebCompanionRegUtils')
?.logoutAfterValidationFail?.length !== 0 ||
modules.get('WAWebSocketLogoutJob')
?.socketLogout?.length !== 1 ||
signalStorage?.destroy?.length !== 0 ||
modules.get('CryptoEncKeyHelper')
?.deleteEncKeysAndCache?.length !== 0 ||
modules.get('CryptoEncryptionSaltHelper')
?.deleteSalt?.length !== 0 ||
modules.get('WAWebUserPrefsClearLocalState')
?.clearAllLocalState?.length !== 0 ||
comms?.isCommsInitialized?.length !== 0 ||
comms?.closeSocketAndResume?.length !== 0 ||
comms?.stopComms?.length !== 0 ||
launchSocket?.launchSocket?.length !== 1 ||
backendEvent?.RECONNECT_SOCKET !==
'reconnect_socket' ||
backendEventBus?.triggerReconnectSocket
?.length !== 0 ||
backendEventBus?.getListenersCount?.length !== 1
) {
shapeFailures.push('canonical_reconnect');
}
}
const credentialProtectionInstalled =
window.__WWebSessionBridge?.ensureCanonicalCredentialProtection?.() ===
true;
const pqUploadLifecycleGateInstalled =
window.__WWebSessionBridge?.ensurePostQuantumUploadLifecycleGate?.() ===
true;
if (
missingModules.length === 0 &&
!credentialProtectionInstalled
) {
shapeFailures.push(
'canonical_credential_protection',
);
}
if (
missingModules.length === 0 &&
!pqUploadLifecycleGateInstalled
) {
shapeFailures.push('pq_upload_lifecycle_gate');
}
const moduleAbiCompatible =
missingModules.length === 0 &&
shapeFailures.length === 0;
return {
actualVersion: window.Debug?.VERSION,
schemaVersion:
window.__WWebSessionBridge?.schemaVersion,
indexedDbEnumeration:
typeof indexedDB.databases === 'function',
moduleAbiCompatible,
moduleAbi: moduleAbiCompatible
? 'wwebjs-private-modules-v1'
: undefined,
missingModuleCount: missingModules.length,
shapeFailureCount: shapeFailures.length,
missingModules: missingModules
.map(([name]) => name)
.sort(),
shapeFailures: [...new Set(shapeFailures)].sort(),
credentialProtectionInstalled,
pqUploadLifecycleGateInstalled,
pqStorageMode,
expectedVersion,
expectedSchemaVersion: schemaVersion,
};
},
{
expectedVersion: this.expectedVersion,
schemaVersion: BRIDGE_SCHEMA_VERSION,
requiredModules: [
'WAWebSignalStoreApi',
'WAWebSignalStorage',
'WAWebKyberPreKeyStore',
'WAWebPQGatingUtils',
'WAWebUploadPQPrekeysJob',
'WASmaxPreKeysSetRPC',
'WASmaxPreKeysAddRPC',
'WAResultOrError',
'WAPQUploadPreKeysProtocol',
'WAWebSignalConst',
'WAWebSignalCommonUtils',
'WAWebSignalConvertApi',
'WASignalSessions',
'WASignalGroupSession',
'WASignalLocalStorageProtocol.pb',
'encodeProtobuf',
'decodeProtobuf',
'WAWebCryptoCurve25519',
'WASignalSignatures',
'WAWebUserPrefsInfoStore',
'WAWebUserPrefsMultiDevice',
'WAWebSyncdDb',
'WAWebSchemaSyncKeys',
'WAWebSchemaCollectionVersion',
'WAWebSchemaSyncActions',
'WAWebSchemaPendingMutations',
'WAWebSchemaMissingKeys',
'WAWebDbEncryptionKey',
'WAWebSyncd',
'WAWebSyncdConst',
'WAWebWidFactory',
'WAWebUserPrefsMeUser',
'WAWebUserPrefsGeneral',
'WAWebMobilePlatforms',
'WAWebLid1X1MigrationGating',
'WAWebUserPrefsTypes',
'WAWebSocketModel',
'WAWebCompanionRegUtils',
'WAWebSocketLogoutJob',
'CryptoEncKeyHelper',
'CryptoEncryptionSaltHelper',
'WAWebUserPrefsClearLocalState',
'WAWebBackendEventBus',
'WAWebLaunchSocket',
'WAComms',
],
},
);
} catch (error) {
if (
!isTransientNavigationError(error) ||
Date.now() >= deadline
) {
throw error;
}
await new Promise((resolve) =>
setTimeout(resolve, pollIntervalMs),
);
continue;
}
if (
result.schemaVersion === BRIDGE_SCHEMA_VERSION &&
result.indexedDbEnumeration &&
result.moduleAbiCompatible
) {
break;
}
initialIncomplete ||= result;
if (Date.now() >= deadline) break;
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
} while (Date.now() < deadline);
if (
this.enforceExpectedVersion &&
result.actualVersion !== this.expectedVersion
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_web_version_incompatible',
{
expected: this.expectedVersion,
actual: result.actualVersion,
},
);
}
if (
result.schemaVersion !== BRIDGE_SCHEMA_VERSION ||
!result.indexedDbEnumeration ||
!result.moduleAbiCompatible
) {
this.log(
'browser_bridge.capability_missing',
{
capability_stage: capabilityStage,
attempt_count: attemptCount,
duration_ms: Date.now() - startedAt,
schema_version_match:
result.schemaVersion === BRIDGE_SCHEMA_VERSION,
indexeddb_enumeration: result.indexedDbEnumeration === true,
missing_module_count: result.missingModuleCount,
shape_failure_count: result.shapeFailureCount,
missing_modules: result.missingModules,
shape_failures: result.shapeFailures,
pq_storage_mode: safeDiagnosticCode(
result.pqStorageMode,
'unknown',
),
},
{ force: true },
);
throw new WhatsAppSessionCompatibilityError(
'wwebjs_browser_bridge_capability_missing',
result,
);
}
if (initialIncomplete) {
this.log(
'browser_bridge.capability_recovered',
{
capability_stage: capabilityStage,
attempt_count: attemptCount,
duration_ms: Date.now() - startedAt,
initial_missing_module_count:
initialIncomplete.missingModuleCount,
initial_shape_failure_count:
initialIncomplete.shapeFailureCount,
initial_missing_modules: initialIncomplete.missingModules,
initial_shape_failures: initialIncomplete.shapeFailures,
pq_storage_mode: safeDiagnosticCode(
result.pqStorageMode,
'unknown',
),
},
{ force: true },
);
}
if (
typeof result.actualVersion === 'string' &&
result.actualVersion.length > 0
) {
this.expectedVersion = result.actualVersion;
}
return result;
}
async inspectCanonicalReloadContinuityState({
requireAcknowledged = false,
} = {}) {
if (typeof requireAcknowledged !== 'boolean') {
throw new TypeError('Invalid canonical continuity proof mode.');
}
if (!this.networkQuiesced || !this.canonicalReloadProof) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_reload_continuity_order_invalid',
);
}
this.assertNoCanonicalCredentialViolation();
const state = await this.page.evaluate((expectedDocumentEpoch) => {
const multiDevice = window.require?.('WAWebUserPrefsMultiDevice');
const socket = window.require?.('WAWebSocketModel')?.Socket;
const conn = window.require?.('WAWebConnModel')?.Conn;
const bridgeState = window.__WWebSessionBridge?.state?.();
const runtimeLaunchState =
window.__WWebCanonicalRegisteredLaunchStateV1;
const rawSocketState = String(
socket?.state ?? socket?.get?.('state') ?? 'unknown',
);
const knownSocketStates = new Set([
'CONNECTED',
'OPENING',
'TIMEOUT',
'UNLAUNCHED',
]);
return {
document_epoch_valid:
window.performance?.timeOrigin === expectedDocumentEpoch,
offline: navigator.onLine === false,
is_registered: multiDevice?.isRegistered?.() === true,
canonical_credential_guard_active:
bridgeState?.canonicalCredentialGuardActive === true,
credential_protection_installed:
bridgeState?.canonicalCredentialProtectionInstalled ===
true,
destructive_credential_phase:
bridgeState?.canonicalDestructiveCredentialPhase ||
'unknown',
destructive_credential_acknowledged:
bridgeState?.canonicalDestructiveCredentialAcknowledged ===
true,
destructive_credential_violation_detected:
bridgeState?.canonicalDestructiveCredentialViolationDetected ===
true,
navigation_storage_guard_active:
bridgeState?.canonicalNavigationStorageGuardActive === true,
canonical_pairing_watchdog_active:
bridgeState?.canonicalPairingWatchdogActive === true,
canonical_pairing_detected:
bridgeState?.canonicalPairingDetected === true,
canonical_pairing_ref_present: Boolean(conn?.ref),
canonical_runtime_launch_state: [
'starting',
'dispatched',
'completed',
'failed',
].includes(runtimeLaunchState)
? runtimeLaunchState
: 'none',
has_synced: socket?.hasSynced === true,
socket_state: knownSocketStates.has(rawSocketState)
? rawSocketState
: 'unknown',
};
}, this.canonicalReloadProof.documentEpoch);
const expectedPhase = requireAcknowledged ? 'sealed' : 'suppressing';
if (
state?.document_epoch_valid !== true ||
state.offline !== true ||
state.is_registered !== true ||
state.canonical_credential_guard_active !== true ||
state.credential_protection_installed !== true ||
state.destructive_credential_phase !== expectedPhase ||
state.destructive_credential_acknowledged !== requireAcknowledged ||
state.destructive_credential_violation_detected === true ||
state.navigation_storage_guard_active !== true ||
state.canonical_pairing_watchdog_active !== true ||
state.canonical_pairing_detected === true ||
state.canonical_pairing_ref_present === true ||
state.canonical_runtime_launch_state === 'failed' ||
!['OPENING', 'UNLAUNCHED'].includes(state.socket_state)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_reload_continuity_invalid',
{ socket_state: state?.socket_state || 'unknown' },
);
}
return state;
}
async inspectCanonicalRegistrationState({ capabilityWaitMs = 15000 } = {}) {
if (
!Number.isSafeInteger(capabilityWaitMs) ||
capabilityWaitMs < 0 ||
capabilityWaitMs > 60000
) {
throw new TypeError('Invalid canonical capability wait.');
}
await this.assertCompatible({
waitForModulesMs: capabilityWaitMs,
capabilityStage: 'registration_probe',
});
return this.page.evaluate(async () => {
const requireModule = (name) => {
const value = window.require?.(name);
return value?.default || value;
};
const byteLength = (value) => {
if (value instanceof ArrayBuffer) return value.byteLength;
if (ArrayBuffer.isView(value)) return value.byteLength;
return undefined;
};
const toBytes = (value) => {
if (value instanceof ArrayBuffer) {
return new Uint8Array(value);
}
if (ArrayBuffer.isView(value)) {
return new Uint8Array(
value.buffer,
value.byteOffset,
value.byteLength,
);
}
return undefined;
};
const hexDigest = async (value) => {
if (!value) return null;
const digest = new Uint8Array(
await crypto.subtle.digest('SHA-256', value),
);
return [...digest]
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
};
const widString = (wid) => {
if (typeof wid === 'string') return wid;
if (!wid || typeof wid !== 'object') return undefined;
if (typeof wid._serialized === 'string') {
return wid._serialized;
}
try {
const serialized = wid.toString?.();
return typeof serialized === 'string' &&
serialized !== '[object Object]'
? serialized
: undefined;
} catch {
return undefined;
}
};
const canonicalWid = (wid) => {
const serialized = widString(wid)?.trim().toLowerCase();
if (!serialized) return undefined;
const separator = serialized.lastIndexOf('@');
if (separator <= 0 || separator === serialized.length - 1) {
return undefined;
}
const local = serialized.slice(0, separator);
const rawServer = serialized.slice(separator + 1);
const server =
rawServer === 'c.us' ? 's.whatsapp.net' : rawServer;
return `${local}@${server}`;
};
const hashWid = async (wid) => {
const canonical = canonicalWid(wid);
return canonical
? hexDigest(new TextEncoder().encode(canonical))
: null;
};
const companionFingerprint = async (
registrationInfo,
signedIdentity,
) => {
const fields = [
registrationInfo?.identityKeyPair?.pubKey,
signedIdentity?.details,
signedIdentity?.accountSignatureKey,
signedIdentity?.accountSignature,
signedIdentity?.deviceSignature,
].map(toBytes);
if (fields.some((field) => !field || field.byteLength === 0)) {
return null;
}
const domain = new TextEncoder().encode(
'underchat-whatsapp-device-fingerprint-v2',
);
const total = fields.reduce(
(size, field) => size + 4 + field.byteLength,
domain.byteLength,
);
const payload = new Uint8Array(total);
payload.set(domain);
const view = new DataView(payload.buffer);
let offset = domain.byteLength;
for (const field of fields) {
view.setUint32(offset, field.byteLength, false);
offset += 4;
payload.set(field, offset);
offset += field.byteLength;
}
try {
return await hexDigest(payload);
} finally {
payload.fill(0);
}
};
const signedIdentityDigest = async (signedIdentity) => {
const fields = [
signedIdentity?.details,
signedIdentity?.accountSignatureKey,
signedIdentity?.accountSignature,
signedIdentity?.deviceSignature,
].map(toBytes);
if (fields.some((field) => !field || field.byteLength === 0)) {
return null;
}
const domain = new TextEncoder().encode(
'underchat-wwebjs-signed-identity-tuple-v1',
);
const total = fields.reduce(
(size, field) => size + 4 + field.byteLength,
domain.byteLength,
);
const payload = new Uint8Array(total);
payload.set(domain);
const view = new DataView(payload.buffer);
let offset = domain.byteLength;
for (const field of fields) {
view.setUint32(offset, field.byteLength, false);
offset += 4;
payload.set(field, offset);
offset += field.byteLength;
}
try {
return await hexDigest(payload);
} finally {
payload.fill(0);
}
};
const deviceInfo = (wid) => {
if (!wid) return { present: false, nonzero: false };
try {
const deviceId = wid.getDeviceId?.();
const normalizedDeviceId =
typeof deviceId === 'string' &&
/^[0-9]+$/u.test(deviceId)
? Number(deviceId)
: deviceId;
if (
Number.isSafeInteger(normalizedDeviceId) &&
normalizedDeviceId >= 0
) {
return {
present: true,
nonzero: normalizedDeviceId > 0,
};
}
} catch {
// Fall through to the stable serialized-WID shapes.
}
if (Number.isSafeInteger(wid.device) && wid.device >= 0) {
return {
present: true,
nonzero: wid.device > 0,
};
}
const serialized =
typeof wid === 'string'
? wid
: wid._serialized || wid.toString?.();
const match = /:([0-9]+)@/u.exec(String(serialized || ''));
return {
present: Boolean(match),
nonzero: Boolean(match && Number(match[1]) > 0),
};
};
const safeRead = async (reader) => {
try {
return await reader();
} catch {
return undefined;
}
};
const safeReadSync = (reader) => {
try {
return reader();
} catch {
return undefined;
}
};
const hasLocalStorageRecord = (key) =>
safeReadSync(() => window.localStorage?.getItem?.(key)) != null;
const curve = requireModule('WAWebCryptoCurve25519');
const signalSignatures = requireModule('WASignalSignatures');
const validateCurveKeyPair = async (keyPair) => {
const publicKey = toBytes(keyPair?.pubKey);
const privateKey = toBytes(keyPair?.privKey);
if (
publicKey?.byteLength !== 32 ||
privateKey?.byteLength !== 32 ||
typeof curve?.keyPair !== 'function'
) {
return false;
}
const privateCopy = new Uint8Array(privateKey);
let derivedPrivate;
try {
const derived = await curve.keyPair(
privateCopy.buffer.slice(0),
);
const derivedPublic = toBytes(derived?.pubKey);
derivedPrivate = toBytes(derived?.privKey);
if (derivedPublic?.byteLength !== 32) return false;
let difference = 0;
for (let index = 0; index < 32; index += 1) {
difference |= publicKey[index] ^ derivedPublic[index];
}
return difference === 0;
} catch {
return false;
} finally {
privateCopy.fill(0);
derivedPrivate?.fill(0);
}
};
const validateSignedPreKey = async (
registrationInfo,
signedPreKey,
) => {
const identityPublic = toBytes(
registrationInfo?.identityKeyPair?.pubKey,
);
const signedPublic = toBytes(signedPreKey?.keyPair?.pubKey);
const signature = toBytes(signedPreKey?.signature);
if (
identityPublic?.byteLength !== 32 ||
signedPublic?.byteLength !== 32 ||
signature?.byteLength !== 64 ||
typeof signalSignatures?.verifyMsgSignalVariant !==
'function'
) {
return false;
}
const serializedIdentity = new Uint8Array(33);
serializedIdentity[0] = 5;
serializedIdentity.set(identityPublic, 1);
const serializedSigned = new Uint8Array(33);
serializedSigned[0] = 5;
serializedSigned.set(signedPublic, 1);
try {
return (
(await signalSignatures.verifyMsgSignalVariant(
serializedIdentity,
serializedSigned,
signature,
)) === true
);
} catch {
return false;
}
};
const multiDevice = requireModule('WAWebUserPrefsMultiDevice');
const me = requireModule('WAWebUserPrefsMeUser');
const infoStore = requireModule('WAWebUserPrefsInfoStore');
const signalStore = requireModule(
'WAWebSignalStoreApi',
)?.waSignalStore;
const signalStorage = requireModule('WAWebSignalStorage');
const signalConst = requireModule('WAWebSignalConst');
const socket = requireModule('WAWebSocketModel')?.Socket;
const conn = requireModule('WAWebConnModel')?.Conn;
const [
noiseInfo,
registrationInfo,
signedIdentity,
signedPreKey,
routingInfo,
mms4Connection,
] = await Promise.all([
safeRead(() => infoStore?.waNoiseInfo?.get?.()),
safeRead(() => signalStore?.getRegistrationInfo?.()),
safeRead(() => signalStore?.getADVSignedIdentity?.()),
safeRead(() => signalStore?.getSignedPreKey?.()),
safeRead(() => multiDevice?.getRoutingInfo?.()),
safeRead(() => multiDevice?.getMms4Conn?.()),
]);
let metaRows;
let baseKeyRows;
try {
await signalStorage?.initialize?.();
[metaRows, baseKeyRows] = await Promise.all([
signalStorage?.getMetaTable?.()?.all?.() || [],
signalStorage?.getBaseKeyTable?.()?.all?.() || [],
]);
} catch {
metaRows = [];
baseKeyRows = [];
}
const devicePn = safeReadSync(() => me?.getMaybeMeDevicePn?.());
const deviceLid = safeReadSync(() => me?.getMaybeMeDeviceLid?.());
const devicePnInfo = deviceInfo(devicePn);
const deviceLidInfo = deviceInfo(deviceLid);
const staticKeyPair = noiseInfo?.staticKeyPair;
const identityKeyPair = registrationInfo?.identityKeyPair;
const [
devicePnHash,
deviceLidHash,
fingerprint,
noisePublicHash,
identityPublicHash,
signedIdentityHash,
] = await Promise.all([
hashWid(devicePn),
hashWid(deviceLid),
companionFingerprint(registrationInfo, signedIdentity),
hexDigest(toBytes(staticKeyPair?.pubKey)),
hexDigest(toBytes(identityKeyPair?.pubKey)),
signedIdentityDigest(signedIdentity),
]);
// The private curve worker is not guaranteed to be reentrant.
// Validate the three persisted pairs sequentially before network
// resume so a transient worker race cannot create a false pass.
const signedPreKeySignatureValid = await validateSignedPreKey(
registrationInfo,
signedPreKey,
);
const noiseKeyPairMathValid =
await validateCurveKeyPair(staticKeyPair);
const identityKeyPairMathValid =
await validateCurveKeyPair(identityKeyPair);
const signedPreKeyPairMathValid = await validateCurveKeyPair(
signedPreKey?.keyPair,
);
const metaKeys = new Set(metaRows.map((row) => row?.key));
const mms4ShapeValid =
mms4Connection &&
typeof mms4Connection === 'object' &&
Array.isArray(mms4Connection.hosts) &&
mms4Connection.hosts.length > 0 &&
mms4Connection.auth != null;
const routingInfoPresent =
(typeof routingInfo === 'string' && routingInfo.length > 0) ||
byteLength(routingInfo) > 0 ||
(routingInfo != null &&
typeof routingInfo === 'object' &&
((typeof routingInfo.domain === 'string' &&
routingInfo.domain.length > 0) ||
Number.isSafeInteger(routingInfo.domain) ||
(typeof routingInfo.edgeRouting === 'string' &&
routingInfo.edgeRouting.length > 0) ||
byteLength(routingInfo.edgeRouting) > 0));
const routingInfoHash = await hexDigest(
routingInfo?.edgeRouting ?? routingInfo,
);
const rawSocketState =
socket?.state ?? socket?.get?.('state') ?? 'unknown';
const knownSocketStates = new Set([
'CONFLICT',
'CONNECTED',
'DEPRECATED_VERSION',
'OPENING',
'PAIRING',
'PROXYBLOCK',
'SMB_TOS_BLOCK',
'TIMEOUT',
'TOS_BLOCK',
'UNLAUNCHED',
'UNPAIRED',
'UNPAIRED_IDLE',
]);
const normalizedSocketState = String(rawSocketState);
return {
is_registered: multiDevice?.isRegistered?.() === true,
has_noise_info: Boolean(noiseInfo),
noise_shape_valid:
byteLength(staticKeyPair?.pubKey) === 32 &&
byteLength(staticKeyPair?.privKey) === 32 &&
byteLength(noiseInfo?.recoveryToken) === 24,
noise_public_hash: noisePublicHash,
noise_keypair_math_valid: noiseKeyPairMathValid,
has_me_device_pn: Boolean(devicePn),
me_device_pn_hash: devicePnHash,
me_device_pn_device_id_present: devicePnInfo.present,
me_device_pn_device_id_nonzero: devicePnInfo.nonzero,
has_me_device_lid: Boolean(deviceLid),
me_device_lid_hash: deviceLidHash,
me_device_lid_device_id_present:
!deviceLid || deviceLidInfo.present,
me_device_lid_device_id_nonzero:
!deviceLid || deviceLidInfo.nonzero,
has_registration_info: Boolean(registrationInfo),
registration_id: Number.isSafeInteger(
registrationInfo?.registrationId,
)
? registrationInfo.registrationId
: null,
identity_public_hash: identityPublicHash,
identity_keypair_math_valid: identityKeyPairMathValid,
registration_shape_valid:
Number.isSafeInteger(registrationInfo?.registrationId) &&
byteLength(identityKeyPair?.pubKey) === 32 &&
byteLength(identityKeyPair?.privKey) === 32,
has_signed_identity: Boolean(signedIdentity),
signed_identity_shape_valid:
byteLength(signedIdentity?.details) > 0 &&
byteLength(signedIdentity?.accountSignature) === 64 &&
byteLength(signedIdentity?.accountSignatureKey) === 32 &&
byteLength(signedIdentity?.deviceSignature) === 64,
signed_identity_hash: signedIdentityHash,
has_signed_pre_key: Boolean(signedPreKey),
signed_pre_key_id: Number.isSafeInteger(signedPreKey?.keyId)
? signedPreKey.keyId
: null,
signed_pre_key_shape_valid:
Number.isSafeInteger(signedPreKey?.keyId) &&
byteLength(signedPreKey?.keyPair?.pubKey) === 32 &&
byteLength(signedPreKey?.keyPair?.privKey) === 32 &&
byteLength(signedPreKey?.signature) === 64,
signed_pre_key_keypair_math_valid: signedPreKeyPairMathValid,
signed_pre_key_signature_valid: signedPreKeySignatureValid,
companion_fingerprint: fingerprint,
bootstrap_session_record_present:
hasLocalStorageRecord('Session'),
bootstrap_md_session_id_present:
hasLocalStorageRecord('mdSessionId'),
bootstrap_mms4_record_present:
hasLocalStorageRecord('WAMms4Conn'),
bootstrap_mms4_shape_valid: mms4ShapeValid === true,
bootstrap_routing_info_present: routingInfoPresent,
routing_info_hash: routingInfoHash,
bootstrap_noise_record_present:
hasLocalStorageRecord('WANoiseInfo'),
bootstrap_noise_iv_present:
hasLocalStorageRecord('WANoiseInfoIv'),
bootstrap_encryption_salt_present:
hasLocalStorageRecord('WAWebEncKeySalt') ||
hasLocalStorageRecord('WebEncKeySalt'),
bootstrap_signal_meta_row_count: metaRows.length,
bootstrap_signal_base_key_row_count: baseKeyRows.length,
bootstrap_registration_meta_present:
metaKeys.has(signalConst?.META_KEYS?.REG_ID) &&
metaKeys.has(signalConst?.META_KEYS?.STATIC_PRIVKEY) &&
metaKeys.has(signalConst?.META_KEYS?.STATIC_PUBKEY),
bootstrap_adv_identity_meta_present: metaKeys.has(
signalConst?.META_KEYS?.ADV_SIGNED_IDENTITY,
),
canonical_credential_guard_active:
window.__WWebSessionBridge?.state?.()
?.canonicalCredentialGuardActive === true,
canonical_credential_protection_installed:
window.__WWebSessionBridge?.state?.()
?.canonicalCredentialProtectionInstalled === true,
canonical_destructive_credential_phase:
window.__WWebSessionBridge?.state?.()
?.canonicalDestructiveCredentialPhase || 'unknown',
canonical_destructive_credential_acknowledged:
window.__WWebSessionBridge?.state?.()
?.canonicalDestructiveCredentialAcknowledged === true,
canonical_destructive_credential_violation_detected:
window.__WWebSessionBridge?.state?.()
?.canonicalDestructiveCredentialViolationDetected ===
true,
canonical_pairing_watchdog_active:
window.__WWebSessionBridge?.state?.()
?.canonicalPairingWatchdogActive === true,
canonical_pairing_detected:
window.__WWebSessionBridge?.state?.()
?.canonicalPairingDetected === true,
canonical_pairing_ref_present: Boolean(conn?.ref),
canonical_runtime_launch_state: [
'starting',
'dispatched',
'completed',
'failed',
].includes(window.__WWebCanonicalRegisteredLaunchStateV1)
? window.__WWebCanonicalRegisteredLaunchStateV1
: 'none',
has_synced: socket?.hasSynced === true,
socket_state: knownSocketStates.has(normalizedSocketState)
? normalizedSocketState
: 'unknown',
};
});
}
flush() {
const operation = this.flushTail.then(async () => {
const journal = await this.page.evaluate(() =>
window.__WWebSessionBridge?.drain(),
);
if (journal?.dirty) {
this.dirty = true;
if (journal.critical) {
this.lastCanonicalJournalMutationAt = Date.now();
this.captureCanonicalDirtyJournal(journal);
}
this.log('browser_bridge.journal_flushed', {
event_count: journal.events.length,
dropped_count: journal.dropped,
critical: journal.critical,
});
}
return journal;
});
this.flushTail = operation.catch(() => {});
return operation;
}
captureCanonicalDirtyJournal(journal) {
const generation = ++this.canonicalDirtyGeneration;
if (journal.dropped > 0) {
this.canonicalFullReplaceGeneration = generation;
}
for (const event of journal.events || []) {
if (!event?.critical) continue;
const scope = canonicalScopesForJournalEvent(event);
if (scope.full) this.canonicalFullReplaceGeneration = generation;
if (scope.device) this.canonicalDeviceDirtyGeneration = generation;
for (const table of scope.tables) {
this.canonicalDirtyTables.set(table, generation);
}
}
if (
this.canonicalFullReplaceGeneration === 0 &&
this.canonicalDeviceDirtyGeneration === 0 &&
this.canonicalDirtyTables.size === 0
) {
this.canonicalFullReplaceGeneration = generation;
}
}
getCanonicalDirtyPlan() {
const generation = this.canonicalDirtyGeneration;
if (generation === 0) return undefined;
return {
generation,
fullReplace:
this.canonicalFullReplaceGeneration > 0 &&
this.canonicalFullReplaceGeneration <= generation,
device:
this.canonicalDeviceDirtyGeneration > 0 &&
this.canonicalDeviceDirtyGeneration <= generation,
tables: [...this.canonicalDirtyTables.entries()]
.filter(([, changedAt]) => changedAt <= generation)
.map(([table]) => table)
.sort(),
};
}
acknowledgeCanonicalDirtyPlan(plan) {
const generation = Number(plan?.generation || 0);
if (!Number.isSafeInteger(generation) || generation <= 0) return;
if (
this.canonicalFullReplaceGeneration > 0 &&
this.canonicalFullReplaceGeneration <= generation
) {
this.canonicalFullReplaceGeneration = 0;
}
if (
this.canonicalDeviceDirtyGeneration > 0 &&
this.canonicalDeviceDirtyGeneration <= generation
) {
this.canonicalDeviceDirtyGeneration = 0;
}
for (const [table, changedAt] of this.canonicalDirtyTables) {
if (changedAt <= generation)
this.canonicalDirtyTables.delete(table);
}
}
async exportProjection() {
await this.assertCompatible();
const projection = await this.page.evaluate(exportBrowserProjection, {
schemaVersion: BRIDGE_SCHEMA_VERSION,
expectedVersion: this.expectedVersion,
maxBytes: this.maxProjectionBytes,
maxRecords: this.maxProjectionRecords,
});
this.log('browser_bridge.projection_exported', {
record_count: projection.records.length,
size_bytes: projection.size_bytes,
complete: projection.complete,
lossy_records: projection.lossy_records,
});
return projection;
}
async exportCanonicalProjection({
pqServerRollback,
allowIncompleteAppStateOverlay = false,
} = {}) {
const browserProjection = await this.page.evaluate(
exportCanonicalSessionProjection,
{
moduleAbi: CANONICAL_PRIVATE_MODULE_ABI,
maxBytes: this.maxCanonicalBytes,
maxRecords: this.maxCanonicalRecords,
capturedAdvSecret: this.capturedAdvSecret?.toString('base64'),
pqServerRollback,
},
);
const rawBlockers = Array.isArray(browserProjection?.blockers)
? [...new Set(browserProjection.blockers)].filter(
(entry) => typeof entry === 'string',
)
: [];
const appStateOverlayRequired =
allowIncompleteAppStateOverlay === true &&
browserProjection?.complete === false &&
rawBlockers.length > 0 &&
rawBlockers.every((entry) =>
CANONICAL_APP_STATE_OVERLAY_BLOCKERS.has(entry),
);
let normalized;
try {
normalized = normalizeCanonicalProjection(browserProjection, {
requireComplete: !appStateOverlayRequired,
maxBytes: this.maxCanonicalBytes,
maxRecords: this.maxCanonicalRecords,
});
} catch (error) {
const blockers = Array.isArray(error?.details?.blockers)
? error.details.blockers.filter(
(entry) =>
typeof entry === 'string' &&
/^[a-z0-9_.-]{1,100}$/u.test(entry),
)
: [];
const appStateVersionDiagnostics =
browserProjection?.diagnostics?.app_state_versions;
this.log(
'browser_bridge.canonical_projection_rejected',
{
error_code: String(
error?.code || 'canonical_rejected',
).slice(0, 100),
blockers: [...new Set(blockers)].sort(),
...(appStateVersionDiagnostics &&
Number.isSafeInteger(
appStateVersionDiagnostics.incompatible_count,
)
? {
app_state_version_diagnostics:
appStateVersionDiagnostics,
}
: {}),
},
{ force: true },
);
throw error;
}
const projection = canonicalBrowserProjectionToStore(normalized, {
requireComplete: !appStateOverlayRequired,
});
if (appStateOverlayRequired) {
this.log(
'browser_bridge.canonical_projection_app_state_overlay_required',
{
blockers: [...rawBlockers].sort(),
},
{ force: true },
);
}
if (projection.provider_state) {
await this.onCanonicalProviderState?.(projection.provider_state);
}
this.log('browser_bridge.canonical_projection_exported', {
record_count: projection.record_count,
size_bytes: projection.size_bytes,
complete: projection.complete,
adv_secret_available: projection.capabilities.adv_secret_available,
signal_session_count:
projection.tables.whatsapp_signal_sessions.length,
sender_key_count: projection.tables.whatsapp_sender_keys.length,
app_state_sync_key_count:
projection.tables.whatsapp_app_state_sync_keys.length,
});
return projection;
}
async importProjection(projection) {
await this.assertCompatible();
let projectionBytes;
try {
projectionBytes = Buffer.byteLength(JSON.stringify(projection));
} catch {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_browser_projection_invalid',
);
}
if (projection.complete !== true) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_browser_projection_lossy',
);
}
if (
!Array.isArray(projection.records) ||
projection.records.length > this.maxProjectionRecords ||
projectionBytes > this.maxProjectionBytes
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_browser_projection_limit_exceeded',
);
}
const result = await this.page.evaluate(
importBrowserProjection,
projection,
{
schemaVersion: BRIDGE_SCHEMA_VERSION,
expectedVersion: this.expectedVersion,
enforceExpectedVersion: this.enforceExpectedVersion,
},
);
this.log('browser_bridge.projection_imported', result);
return result;
}
async evaluateCanonicalProjectionImportTask(
projection,
options,
{ timeoutMs, timeoutStage, controlOperation },
) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1 ||
timeoutMs > 300000
) {
throw new TypeError('Invalid canonical import control timeout.');
}
let timeout;
let evaluation;
try {
evaluation = Promise.resolve(
this.page.evaluate(
importCanonicalSessionProjection,
projection,
options,
),
);
} catch (error) {
evaluation = Promise.reject(error);
}
// A renderer blocked in a private crypto module may reject this
// abandoned protocol request only after Chromium is terminated. Keep
// that late rejection handled while the Node-side deadline lets
// RemoteAuth enter its fail-closed termination path.
evaluation.catch(() => {});
try {
return await Promise.race([
evaluation,
new Promise((_, reject) => {
timeout = setTimeout(
() =>
reject(
new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_import_task_timeout',
{
stage: safeDiagnosticCode(
timeoutStage,
'unknown_stage',
),
control_operation: safeDiagnosticCode(
controlOperation,
'unknown_operation',
),
},
),
),
timeoutMs,
);
timeout.unref?.();
}),
]);
} finally {
if (timeout) clearTimeout(timeout);
}
}
async cancelCanonicalProjectionImportTask(
taskId,
{ timeoutMs = 1000 } = {},
) {
if (typeof taskId !== 'string' || taskId.length === 0) return false;
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1 ||
timeoutMs > 5000
) {
throw new TypeError('Invalid canonical import cancel timeout.');
}
let timeout;
let evaluation;
try {
evaluation = Promise.resolve(
this.page.evaluate(
importCanonicalSessionProjection,
undefined,
{
backgroundTaskMode: 'cancel',
backgroundTaskId: taskId,
},
),
);
} catch (error) {
evaluation = Promise.reject(error);
}
evaluation.catch(() => {});
try {
return (
(await Promise.race([
evaluation,
new Promise((resolve) => {
timeout = setTimeout(() => resolve(false), timeoutMs);
timeout.unref?.();
}),
])) === true
);
} catch {
// The caller is already failing closed. A task can only continue
// in the page that is about to be fenced and terminated by
// RemoteAuth, so cancellation is best effort here.
return false;
} finally {
if (timeout) clearTimeout(timeout);
}
}
async importCanonicalProjectionInBackground(
normalizedProjection,
importOptions,
{ timeoutMs },
) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 300000
) {
throw new TypeError('Invalid canonical import task timeout.');
}
const taskId = crypto.randomUUID();
const startedAt = Date.now();
const deadlineAtMs = startedAt + timeoutMs;
const projectionSummary = {
record_count: normalizedProjection.record_count,
signal_session_count:
normalizedProjection.tables.signal_sessions.length,
sender_key_count: normalizedProjection.tables.sender_keys.length,
pq_pre_key_count: Array.isArray(
normalizedProjection.tables.pq_pre_keys,
)
? normalizedProjection.tables.pq_pre_keys.length
: 0,
app_state_sync_key_count:
normalizedProjection.tables.app_state_sync_keys.length,
timeout_ms: timeoutMs,
};
let lastStage;
let lastProgressLoggedAt = 0;
let lastSnapshot;
const logProgress = (snapshot, force = false) => {
const now = Date.now();
if (
!force &&
snapshot.stage === lastStage &&
now - lastProgressLoggedAt < 15000
) {
return;
}
lastStage = snapshot.stage;
lastProgressLoggedAt = now;
this.log(
'browser_bridge.canonical_projection_import_progress',
{
stage: safeDiagnosticCode(snapshot.stage, 'unknown_stage'),
duration_ms: Math.max(0, snapshot.duration_ms || 0),
...(Number.isSafeInteger(snapshot.progress_completed) &&
Number.isSafeInteger(snapshot.progress_total)
? {
progress_completed: snapshot.progress_completed,
progress_total: snapshot.progress_total,
}
: {}),
...projectionSummary,
},
{ force: true },
);
};
const consumeCompletedTask = async (snapshot) => {
const result = await this.evaluateCanonicalProjectionImportTask(
undefined,
{
backgroundTaskMode: 'consume',
backgroundTaskId: taskId,
},
{
// Completion is already fenced by the page-local crypto
// deadline. Give only the short control RPC its own fixed
// grace instead of rejecting a valid result because the
// last observation arrived at the deadline boundary.
timeoutMs: CANONICAL_IMPORT_CONTROL_RPC_TIMEOUT_MS,
timeoutStage: snapshot.stage,
controlOperation: 'consume',
},
);
this.log(
'browser_bridge.canonical_projection_import_completed',
{
stage: safeDiagnosticCode(snapshot.stage, 'completed'),
duration_ms: Math.max(0, Date.now() - startedAt),
...projectionSummary,
},
{ force: true },
);
return result;
};
const assertTaskSnapshot = (snapshot) => {
if (
snapshot?.task_id !== taskId ||
typeof snapshot?.status !== 'string' ||
typeof snapshot?.stage !== 'string'
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_import_task_observation_failed',
);
}
if (
snapshot.status !== 'running' &&
snapshot.status !== 'completed'
) {
const stage = safeDiagnosticCode(
snapshot.stage,
'unknown_stage',
);
const cause = safeDiagnosticCode(
snapshot.error_code,
'runtime_failure',
);
throw new WhatsAppSessionCompatibilityError(
durableCanonicalImportFailureCode(stage, cause),
{
stage,
cause,
duration_ms: Math.max(0, snapshot.duration_ms || 0),
},
);
}
};
try {
const started = await this.evaluateCanonicalProjectionImportTask(
normalizedProjection,
{
...importOptions,
backgroundTaskMode: 'start',
backgroundTaskId: taskId,
backgroundTaskDeadlineAtMs: deadlineAtMs,
},
{
timeoutMs: Math.min(
CANONICAL_IMPORT_CONTROL_RPC_TIMEOUT_MS,
Math.max(1, deadlineAtMs - Date.now()),
),
timeoutStage: 'projection_validation',
controlOperation: 'start',
},
);
if (
started?.status !== 'running' ||
started?.task_id !== taskId ||
typeof started?.stage !== 'string'
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_import_task_start_failed',
);
}
lastSnapshot = started;
logProgress(started, true);
while (Date.now() < deadlineAtMs) {
lastSnapshot = await this.evaluateCanonicalProjectionImportTask(
undefined,
{
backgroundTaskMode: 'observe',
backgroundTaskId: taskId,
},
{
// The private ML-KEM implementation can occupy the
// renderer main thread for longer than the normal
// control-RPC budget. An observe call made during that
// work is queued behind the import even though the
// page-scoped task is still inside its valid deadline.
// Let that queued observation use the remaining task
// budget; the original page-local deadline still
// fences mutations, while start/consume/cancel retain
// their short independent control deadlines.
timeoutMs: Math.max(1, deadlineAtMs - Date.now()),
timeoutStage: lastSnapshot?.stage || 'task_observation',
controlOperation: 'observe',
},
);
assertTaskSnapshot(lastSnapshot);
logProgress(lastSnapshot);
if (lastSnapshot.status === 'completed') {
return await consumeCompletedTask(lastSnapshot);
}
await new Promise((resolve) =>
setTimeout(
resolve,
Math.max(0, Math.min(500, deadlineAtMs - Date.now())),
),
);
}
// The task enforces the original crypto deadline internally. One
// final bounded observation collects a result/failure that settled
// at the boundary without ever allowing a still-running task to
// extend its mutation budget.
lastSnapshot = await this.evaluateCanonicalProjectionImportTask(
undefined,
{
backgroundTaskMode: 'observe',
backgroundTaskId: taskId,
},
{
timeoutMs: CANONICAL_IMPORT_CONTROL_RPC_TIMEOUT_MS,
timeoutStage: lastSnapshot?.stage || 'task_observation',
controlOperation: 'observe',
},
);
assertTaskSnapshot(lastSnapshot);
logProgress(lastSnapshot, true);
if (lastSnapshot.status === 'completed') {
return await consumeCompletedTask(lastSnapshot);
}
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_import_task_timeout',
{
stage: safeDiagnosticCode(
lastSnapshot?.stage,
'unknown_stage',
),
duration_ms: Date.now() - startedAt,
},
);
} catch (error) {
await this.cancelCanonicalProjectionImportTask(taskId);
const details = error?.details;
this.log(
'browser_bridge.canonical_projection_import_failed',
{
stage: safeDiagnosticCode(
details?.stage || lastSnapshot?.stage,
'unknown_stage',
),
...(details?.control_operation
? {
control_operation: safeDiagnosticCode(
details.control_operation,
'unknown_operation',
),
}
: {}),
error_code: safeDiagnosticCode(
error?.code || error?.message,
'wwebjs_canonical_import_task_failed',
),
duration_ms: Date.now() - startedAt,
...projectionSummary,
},
{ force: true },
);
if (error instanceof WhatsAppSessionCompatibilityError) {
throw error;
}
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_import_task_failed',
{
stage: safeDiagnosticCode(
lastSnapshot?.stage,
'unknown_stage',
),
cause: safeDiagnosticCode(
error?.code || error?.message,
'runtime_failure',
),
duration_ms: Date.now() - startedAt,
},
);
}
}
async importCanonicalProjection(projection, options = {}) {
const capabilityWaitMs = options.capabilityWaitMs ?? 15000;
const backgroundTaskTimeoutMs =
options.backgroundTaskTimeoutMs ?? 120000;
const signalComponentNames = [
'identity_keys',
'pre_keys',
'pq_pre_keys',
'signal_sessions',
'sender_keys',
];
let preserveExistingSignalComponents;
if (options.preserveExistingSignalComponents !== undefined) {
const candidate = options.preserveExistingSignalComponents;
const keys =
candidate && typeof candidate === 'object'
? Object.keys(candidate)
: [];
if (
!candidate ||
Array.isArray(candidate) ||
Object.getPrototypeOf(candidate) !== Object.prototype ||
keys.length !== signalComponentNames.length ||
signalComponentNames.some(
(name) =>
!Object.hasOwn(candidate, name) ||
typeof candidate[name] !== 'boolean',
) ||
keys.some((name) => !signalComponentNames.includes(name)) ||
options.preserveExistingAppState !== true ||
options.preserveExistingSignalTables === true
) {
throw new TypeError(
'Invalid canonical Signal component preservation plan.',
);
}
preserveExistingSignalComponents = Object.fromEntries(
signalComponentNames.map((name) => [name, candidate[name]]),
);
}
if (
!Number.isSafeInteger(capabilityWaitMs) ||
capabilityWaitMs < 1000 ||
capabilityWaitMs > 60000 ||
(options.backgroundTask === true &&
(!Number.isSafeInteger(backgroundTaskTimeoutMs) ||
backgroundTaskTimeoutMs < 1000 ||
backgroundTaskTimeoutMs > 300000))
) {
throw new TypeError('Invalid canonical capability wait.');
}
if (
this.networkQuiesced &&
this.canonicalCredentialGuardArmed &&
this.canonicalCredentialGuardScriptIdentifier
) {
await this.stabilizeCanonicalImportRuntime({
timeoutMs: capabilityWaitMs,
});
} else {
await this.assertCompatible({
waitForModulesMs: capabilityWaitMs,
capabilityStage: 'pre_import',
});
}
const browserProjection = canonicalStoreProjectionToBrowser(
projection,
{ webVersion: this.expectedVersion },
);
const normalized = normalizeCanonicalProjection(browserProjection, {
maxBytes: this.maxCanonicalBytes,
maxRecords: this.maxCanonicalRecords,
});
let pqPairValidationProof;
if (
options.backgroundTask === true &&
normalized.tables.pq_pre_keys.length > 0
) {
const startedAt = Date.now();
this.log(
'browser_bridge.canonical_pq_pair_validation_started',
{
backend: 'noble_ml_kem_1024',
progress_completed: 0,
progress_total: normalized.tables.pq_pre_keys.length,
},
{ force: true },
);
try {
pqPairValidationProof =
await validateCanonicalPqKeyPairs(normalized);
this.log(
'browser_bridge.canonical_pq_pair_validation_completed',
{
backend: 'noble_ml_kem_1024',
duration_ms: Date.now() - startedAt,
progress_completed:
normalized.tables.pq_pre_keys.length,
progress_total: normalized.tables.pq_pre_keys.length,
},
{ force: true },
);
} catch (error) {
this.log(
'browser_bridge.canonical_pq_pair_validation_failed',
{
backend: 'noble_ml_kem_1024',
duration_ms: Date.now() - startedAt,
error_code: safeDiagnosticCode(
error?.code || error?.message,
'wwebjs_canonical_pq_pair_validation_failed',
),
progress_total: normalized.tables.pq_pre_keys.length,
},
{ force: true },
);
throw error;
}
}
const previousAdvSecret = this.capturedAdvSecret
? Buffer.from(this.capturedAdvSecret)
: undefined;
const incomingAdvSecret = projection?.device?.adv_secret_available
? Buffer.from(projection.device.adv_key || [])
: undefined;
if (incomingAdvSecret && incomingAdvSecret.length !== 32) {
incomingAdvSecret.fill(0);
previousAdvSecret?.fill(0);
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_adv_key_invalid',
);
}
if (incomingAdvSecret) {
this.capturedAdvSecret?.fill(0);
this.capturedAdvSecret = Buffer.from(incomingAdvSecret);
}
let result;
try {
const importOptions = {
moduleAbi: CANONICAL_PRIVATE_MODULE_ABI,
allowNoiseMetadataBootstrap:
options.allowNoiseMetadataBootstrap ??
this.allowNoiseMetadataBootstrap,
noiseBootstrap: options.noiseBootstrap,
requireRoutingInfo: options.requireRoutingInfo === true,
preserveExistingAppState:
options.preserveExistingAppState === true,
preserveExistingSignalTables:
options.preserveExistingSignalTables === true,
...(preserveExistingSignalComponents
? { preserveExistingSignalComponents }
: {}),
...(pqPairValidationProof ? { pqPairValidationProof } : {}),
};
result =
options.backgroundTask === true
? await this.importCanonicalProjectionInBackground(
normalized,
importOptions,
{ timeoutMs: backgroundTaskTimeoutMs },
)
: await this.page.evaluate(
importCanonicalSessionProjection,
normalized,
importOptions,
);
} catch (error) {
if (incomingAdvSecret) {
this.capturedAdvSecret?.fill(0);
this.capturedAdvSecret = previousAdvSecret
? Buffer.from(previousAdvSecret)
: undefined;
}
throw error;
} finally {
incomingAdvSecret?.fill(0);
previousAdvSecret?.fill(0);
}
await this.assertCompatible({
waitForModulesMs: capabilityWaitMs,
capabilityStage: 'post_import',
});
this.canonicalPreservedNativeAppState = Boolean(
options.preserveExistingAppState === true &&
result.app_state_import_mode === 'preserved_native_profile' &&
result.pending_app_state_sync_keys === 0,
);
if (result.generated_provider_state) {
await this.onCanonicalProviderState?.({
kind: result.generated_provider_state.kind,
codec_version: result.generated_provider_state.codec_version,
device_fingerprint: Buffer.from(
result.generated_provider_state.device_fingerprint,
'hex',
),
noise_public_key: Buffer.from(
result.generated_provider_state.noise_public_key,
'base64',
),
recovery_token: Buffer.from(
result.generated_provider_state.recovery_token,
'base64',
),
certificate_chain:
result.generated_provider_state.certificate_chain === null
? null
: Buffer.from(
result.generated_provider_state.certificate_chain,
'base64',
),
});
}
this.log(
'browser_bridge.canonical_projection_imported',
{
imported_records: result.imported_records,
imported_signal_sessions: result.imported_signal_sessions,
imported_sender_keys: result.imported_sender_keys,
imported_app_state_sync_keys:
result.imported_app_state_sync_keys,
pending_app_state_sync_keys: result.pending_app_state_sync_keys,
adv_secret_available: result.adv_secret_available,
app_state_import_mode: result.app_state_import_mode,
preserve_existing_app_state:
options.preserveExistingAppState === true,
preserve_existing_signal_tables:
options.preserveExistingSignalTables === true,
preserved_signal_components: result.preserved_signal_components,
replaced_signal_components: result.replaced_signal_components,
noise_metadata_mode: result.noise_metadata_mode,
signal_storage_initialization_mode:
result.signal_storage_initialization_mode,
signal_table_import_mode: result.signal_table_import_mode,
pq_key_mode: result.pq_key_mode,
pq_storage_mode: result.pq_storage_mode,
imported_signal_session_scopes:
result.imported_signal_session_scopes,
discarded_signal_session_count:
result.discarded_signal_session_count,
signal_session_resync_required:
result.signal_session_resync_required,
signal_session_resync_scopes:
result.signal_session_resync_scopes,
status_session_mode: result.status_session_mode,
routing_info_imported: result.routing_info_imported,
reconnect_required: result.reconnect_required,
reload_required: result.reload_required,
},
// This is the terminal, auditable proof of which preservation
// contract actually reached the page import. Progress events are
// forced already, but the terminal event used to be sampled out
// after several consecutive handoffs, making a successful safe
// import indistinguishable from missing evidence.
{ force: true },
);
return result;
}
async beginPostQuantumRollbackUploadFence({ timeoutMs = 30000 } = {}) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 120000
) {
throw new TypeError('wwebjs_pq_upload_drain_timeout_invalid');
}
const result = await this.page.evaluate(async (drainTimeoutMs) => {
const bridge = window.__WWebSessionBridge;
if (
bridge?.schemaVersion !== 2 ||
typeof bridge.beginPostQuantumRollbackUploadFence !== 'function'
) {
throw new Error('wwebjs_pq_upload_lifecycle_gate_unavailable');
}
return bridge.beginPostQuantumRollbackUploadFence(drainTimeoutMs);
}, timeoutMs);
if (result?.phase !== 'rollback' || result?.active !== 0) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_pq_upload_drain_unproven',
);
}
this.log('browser_bridge.pq_upload_fence_acquired', result, {
force: true,
});
return result;
}
async resumePostQuantumUploadLifecycleGate() {
const resumed = await this.page.evaluate(() => {
const bridge = window.__WWebSessionBridge;
if (
bridge?.schemaVersion !== 2 ||
typeof bridge.resumePostQuantumUploadLifecycleGate !==
'function'
) {
throw new Error('wwebjs_pq_upload_lifecycle_gate_unavailable');
}
return bridge.resumePostQuantumUploadLifecycleGate();
});
if (resumed !== true) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_pq_upload_lifecycle_gate_resume_failed',
);
}
this.log(
'browser_bridge.pq_upload_fence_released_for_source_recovery',
{},
{ force: true },
);
}
/**
* Complete a cross-provider PQ bootstrap through WhatsApp Web's own job.
* The private job intentionally swallows upload failures, so readiness is
* established only by rereading the migration bit and both key stores.
* No key material crosses the page boundary.
*/
async ensurePostQuantumReady() {
await this.assertCompatible();
const result = await this.page.evaluate(async () => {
const requireModule = (name) => {
const value = window.require?.(name);
if (!value) {
throw new Error(
'wwebjs_canonical_pq_bootstrap_abi_incompatible',
);
}
return value.default || value;
};
const store = requireModule('WAWebKyberPreKeyStore');
const gating = requireModule('WAWebPQGatingUtils');
const uploadJob = requireModule('WAWebUploadPQPrekeysJob');
if (
typeof store.isPQMigrated !== 'function' ||
typeof store.loadKyberKeysForDigest !== 'function' ||
typeof gating.isPqKeysUploadEnabled !== 'function' ||
typeof gating.isPq1on1MessageEnabled !== 'function' ||
typeof uploadJob.generateAndUploadPQPreKeys !== 'function'
) {
throw new Error(
'wwebjs_canonical_pq_bootstrap_abi_incompatible',
);
}
if (!gating.isPqKeysUploadEnabled()) {
throw new Error(
'wwebjs_canonical_pq_bootstrap_rollout_disabled',
);
}
if (!gating.isPq1on1MessageEnabled()) {
throw new Error(
'wwebjs_canonical_pq_messaging_runtime_disabled',
);
}
if (!(await store.isPQMigrated())) {
await uploadJob.generateAndUploadPQPreKeys();
}
if (!(await store.isPQMigrated())) {
throw new Error('wwebjs_canonical_pq_bootstrap_failed');
}
if (!gating.isPq1on1MessageEnabled()) {
throw new Error(
'wwebjs_canonical_pq_messaging_runtime_disabled',
);
}
let preKeys;
let lastResortKeys;
let digestState;
try {
[preKeys, lastResortKeys, digestState] = await Promise.all([
store.getUnsentKyberPreKeys(),
store.loadLatestKyberLastResortKey(),
store.loadKyberKeysForDigest(),
]);
} catch {
throw new Error('wwebjs_canonical_pq_bootstrap_failed');
}
// All generated prekeys are marked sent after a successful job,
// therefore the unsent count may be zero. The last-resort key and
// migrated flag are the durable post-upload proof available from
// the public private-module surface.
if (
!Array.isArray(preKeys) ||
!lastResortKeys ||
!Array.isArray(digestState?.kyberPreKeys) ||
digestState.kyberPreKeys.length === 0 ||
!Array.isArray(digestState?.kyberLastResortKeys) ||
digestState.kyberLastResortKeys.length === 0
) {
throw new Error('wwebjs_canonical_pq_bootstrap_failed');
}
return {
migrated: true,
pre_key_count: digestState.kyberPreKeys.length,
unsent_pre_key_count: preKeys.length,
last_resort_key_count: digestState.kyberLastResortKeys.length,
};
});
this.log('browser_bridge.pq_bootstrap_completed', result, {
force: true,
});
return result;
}
/**
* Remove this companion's server-side PQ prekey bundle before handing the
* same identity to a provider that only implements classical X3DH.
*
* The official protocol resolves successfully only after the delete RPC
* receives DeleteResponseSuccess. Local material is never cleared before
* that acknowledgement. A local-cleanup failure is surfaced separately so
* the caller can keep the handoff closed and re-bootstrap PQ on the source.
*/
async rollbackPostQuantumForClassicalHandoff({ timeoutMs = 30000 } = {}) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 120000
) {
throw new TypeError('wwebjs_pq_rollback_timeout_invalid');
}
await this.assertCompatible();
// The gate is installed from evaluateOnNewDocument and tracks the
// product's own upload job. Flip it before the delete RPC, drain any
// upload already in flight, and keep it sticky until Chromium is
// proven offline. This makes the server ACK a final-state proof.
await this.beginPostQuantumRollbackUploadFence({ timeoutMs });
// Resolve and initialize every optional private module before the RPC.
// Once the next evaluation is dispatched, any context/page failure is
// an unknown ACK and must be recovered as though the server accepted.
await this.page.evaluate(async () => {
const requireModule = (name) => {
const value = window.require?.(name);
if (!value) {
throw new Error(
'wwebjs_pq_rollback_module_abi_incompatible',
);
}
return value.default || value;
};
const rollback = requireModule('WAPQRollbackProtocol');
const signalStorage = requireModule('WAWebSignalStorage');
const kyberStore = requireModule('WAWebKyberPreKeyStore');
const pqGating = requireModule('WAWebPQGatingUtils');
if (
typeof rollback.deletePQPreKeysProtocol !== 'function' ||
typeof signalStorage.initialize !== 'function' ||
typeof signalStorage.getKyberPreKeyTable !== 'function' ||
typeof signalStorage.getKyberLastResortKeyTable !==
'function' ||
typeof kyberStore.isPQMigrated !== 'function' ||
typeof kyberStore.setPQMigrated !== 'function' ||
typeof kyberStore.loadKyberKeysForDigest !== 'function' ||
typeof pqGating.isPqKeysUploadEnabled !== 'function' ||
typeof pqGating.isPq1on1MessageEnabled !== 'function'
) {
throw new Error('wwebjs_pq_rollback_module_abi_incompatible');
}
await signalStorage.initialize();
window.__WWebPqRollbackContext = {
rollback,
signalStorage,
kyberStore,
pqGating,
};
});
let result;
let timeout;
try {
const rpc = this.page.evaluate(async () => {
const context = window.__WWebPqRollbackContext;
if (!context) {
throw new Error('pq_rollback_context_lost');
}
const uploadFenceBefore =
window.__WWebSessionBridge?.postQuantumUploadLifecycleGateState?.();
if (
uploadFenceBefore?.installed !== true ||
uploadFenceBefore.phase !== 'rollback' ||
uploadFenceBefore.active !== 0
) {
throw new Error('pq_upload_lifecycle_fence_lost');
}
const { rollback, signalStorage, kyberStore, pqGating } =
context;
let response;
try {
response = await rollback.deletePQPreKeysProtocol();
} finally {
delete window.__WWebPqRollbackContext;
}
if (response?.success !== true) {
return {
server_acknowledged: false,
server_rejected: true,
local_cleanup_complete: false,
};
}
const acknowledgedAtMs = Date.now();
const runtimeUploadEnabled = Boolean(
pqGating.isPqKeysUploadEnabled(),
);
const runtimeMessagingEnabled = Boolean(
pqGating.isPq1on1MessageEnabled(),
);
try {
// Persist the migration bit first. If a later table
// operation fails, reconnect sees migrated=false and must
// re-bootstrap instead of trusting the deleted bundle.
await kyberStore.setPQMigrated(false);
let missingTables = 0;
const tables = [];
for (const getterName of [
'getKyberPreKeyTable',
'getKyberLastResortKeyTable',
]) {
try {
const table = signalStorage[getterName]();
if (!table || typeof table.clear !== 'function') {
throw new Error('invalid_table');
}
tables.push(table);
} catch (error) {
if (error?.name !== 'NoSuchTableError') throw error;
missingTables += 1;
}
}
if (missingTables !== 0 && missingTables !== 2) {
throw new Error('inconsistent_tables');
}
await Promise.all(tables.map((table) => table.clear()));
let digest;
try {
digest = await kyberStore.loadKyberKeysForDigest();
} catch (error) {
if (
missingTables !== 2 ||
error?.name !== 'NoSuchTableError'
) {
throw error;
}
digest = {
kyberPreKeys: [],
kyberLastResortKeys: [],
};
}
if (
(await kyberStore.isPQMigrated()) !== false ||
!Array.isArray(digest?.kyberPreKeys) ||
digest.kyberPreKeys.length !== 0 ||
!Array.isArray(digest?.kyberLastResortKeys) ||
digest.kyberLastResortKeys.length !== 0
) {
throw new Error('local_state_not_empty');
}
} catch {
return {
server_acknowledged: true,
local_cleanup_complete: false,
acknowledged_at_ms: acknowledgedAtMs,
};
}
const uploadFenceAfter =
window.__WWebSessionBridge?.postQuantumUploadLifecycleGateState?.();
if (
uploadFenceAfter?.installed !== true ||
uploadFenceAfter.phase !== 'rollback' ||
uploadFenceAfter.active !== 0
) {
return {
server_acknowledged: true,
local_cleanup_complete: false,
acknowledged_at_ms: acknowledgedAtMs,
};
}
return {
protocol: 'delete_pq_prekeys_server_ack_v1',
server_acknowledged: true,
local_cleanup_complete: true,
acknowledged_at_ms: acknowledgedAtMs,
upload_lifecycle_fenced: true,
upload_lifecycle_fence_version: 1,
runtime_upload_enabled: runtimeUploadEnabled,
runtime_messaging_enabled: runtimeMessagingEnabled,
};
});
// The browser RPC cannot be cancelled safely. On timeout the
// acknowledgement is therefore unknown and the caller must
// terminate Chromium before doing anything else. Suppress a late
// rejection from the abandoned evaluate after that termination.
rpc.catch(() => {});
result = await Promise.race([
rpc,
new Promise((_, reject) => {
timeout = setTimeout(() => {
reject(new Error('wwebjs_pq_rollback_rpc_timeout'));
}, timeoutMs);
timeout.unref?.();
}),
]);
} catch (cause) {
const error = new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_server_ack_unknown',
{ cause: cause?.message },
);
error.serverAcknowledged = false;
error.acknowledgementUnknown = true;
throw error;
} finally {
if (timeout) clearTimeout(timeout);
}
if (result?.server_rejected === true) {
const error = new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_server_rejected',
);
error.serverAcknowledged = false;
error.acknowledgementUnknown = false;
throw error;
}
if (
result?.server_acknowledged !== true ||
result?.local_cleanup_complete !== true
) {
const error = new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_local_cleanup_failed',
);
error.serverAcknowledged = result?.server_acknowledged === true;
error.acknowledgementUnknown = false;
throw error;
}
this.log('browser_bridge.pq_server_rollback_completed', result, {
force: true,
});
return result;
}
/** Restore the WWeb source after an acknowledged handoff is aborted. */
async recoverPostQuantumAfterClassicalHandoffAbort() {
await this.assertCompatible();
// A fresh runtime starts with the gate open, while an in-process abort
// retains the rollback phase. Normalize both cases by fencing/draining
// first. The gate remains closed throughout cleanup, the one exclusive
// official upload, and digest validation.
await this.beginPostQuantumRollbackUploadFence();
const result = await this.page.evaluate(async () => {
const requireModule = (name) => {
const value = window.require?.(name);
if (!value) {
throw new Error(
'wwebjs_pq_rollback_recovery_abi_incompatible',
);
}
return value.default || value;
};
const bridge = window.__WWebSessionBridge;
const signalStorage = requireModule('WAWebSignalStorage');
const kyberStore = requireModule('WAWebKyberPreKeyStore');
const pqGating = requireModule('WAWebPQGatingUtils');
if (
typeof signalStorage.initialize !== 'function' ||
typeof kyberStore.setPQMigrated !== 'function' ||
typeof kyberStore.isPQMigrated !== 'function' ||
typeof kyberStore.getUnsentKyberPreKeys !== 'function' ||
typeof kyberStore.loadLatestKyberLastResortKey !== 'function' ||
typeof kyberStore.loadKyberKeysForDigest !== 'function' ||
typeof pqGating.isPqKeysUploadEnabled !== 'function' ||
typeof pqGating.isPq1on1MessageEnabled !== 'function' ||
typeof bridge?.recoverPostQuantumThroughUploadLifecycleGate !==
'function' ||
typeof bridge?.resumePostQuantumUploadLifecycleGate !==
'function'
) {
throw new Error('wwebjs_pq_rollback_recovery_abi_incompatible');
}
await signalStorage.initialize();
await kyberStore.setPQMigrated(false);
let missingTables = 0;
const tables = [];
for (const getterName of [
'getKyberPreKeyTable',
'getKyberLastResortKeyTable',
]) {
try {
const table = signalStorage[getterName]?.();
if (!table || typeof table.clear !== 'function') {
throw new Error('invalid_table');
}
tables.push(table);
} catch (error) {
if (error?.name !== 'NoSuchTableError') throw error;
missingTables += 1;
}
}
if (missingTables !== 0 && missingTables !== 2) {
throw new Error(
'wwebjs_pq_rollback_recovery_storage_inconsistent',
);
}
await Promise.all(tables.map((table) => table.clear()));
const runtimeUploadEnabled = Boolean(
pqGating.isPqKeysUploadEnabled(),
);
const runtimeMessagingEnabled = Boolean(
pqGating.isPq1on1MessageEnabled(),
);
if (!runtimeUploadEnabled && !runtimeMessagingEnabled) {
if (
bridge.resumePostQuantumUploadLifecycleGate(true) !== true
) {
throw new Error('wwebjs_pq_rollback_recovery_failed');
}
return {
migrated: false,
deferred: true,
reason: 'rollout_disabled',
pre_key_count: 0,
unsent_pre_key_count: 0,
last_resort_key_count: 0,
};
}
if (!runtimeUploadEnabled || !runtimeMessagingEnabled) {
throw new Error(
'wwebjs_pq_rollback_recovery_rollout_inconsistent',
);
}
await bridge.recoverPostQuantumThroughUploadLifecycleGate();
let preKeys;
let lastResortKey;
let digest;
try {
[preKeys, lastResortKey, digest] = await Promise.all([
kyberStore.getUnsentKyberPreKeys(),
kyberStore.loadLatestKyberLastResortKey(),
kyberStore.loadKyberKeysForDigest(),
]);
} catch {
throw new Error('wwebjs_pq_rollback_recovery_failed');
}
if (
!(await kyberStore.isPQMigrated()) ||
!Array.isArray(preKeys) ||
!lastResortKey ||
!Array.isArray(digest?.kyberPreKeys) ||
digest.kyberPreKeys.length === 0 ||
!Array.isArray(digest?.kyberLastResortKeys) ||
digest.kyberLastResortKeys.length === 0 ||
!pqGating.isPq1on1MessageEnabled()
) {
throw new Error('wwebjs_pq_rollback_recovery_failed');
}
if (bridge.resumePostQuantumUploadLifecycleGate() !== true) {
throw new Error('wwebjs_pq_rollback_recovery_failed');
}
return {
migrated: true,
pre_key_count: digest.kyberPreKeys.length,
unsent_pre_key_count: preKeys.length,
last_resort_key_count: digest.kyberLastResortKeys.length,
};
});
this.log('browser_bridge.pq_rollback_source_recovered', result, {
force: true,
});
return result;
}
/**
* Install source sync keys after WhatsApp Web has loaded its final database
* encryption key, then start the product's own full collection sync.
*/
async waitForCanonicalAppStateRestoreIdle({
timeoutMs = 60000,
pollIntervalMs = 250,
requiredStableSamples = 2,
} = {}) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 600000 ||
!Number.isSafeInteger(pollIntervalMs) ||
pollIntervalMs < 50 ||
pollIntervalMs > timeoutMs ||
!Number.isSafeInteger(requiredStableSamples) ||
requiredStableSamples < 2 ||
requiredStableSamples > 10
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_restore_barrier_options_invalid',
);
}
const startedAt = Date.now();
let lastObservation;
let lastSignature;
let lastProgressLogAt = 0;
let realmRecoveryCount = 0;
let transportDisconnectedAt;
const recoverReplacedRealm = async (error) => {
if (!isTransientNavigationError(error) || realmRecoveryCount >= 1) {
throw error;
}
const remainingMs = startedAt + timeoutMs - Date.now();
if (remainingMs < 1000) throw error;
const previousDocumentEpoch =
this.canonicalReloadProof?.documentEpoch;
await this.recoverCanonicalAppStateRestoreBarrierRealm({
previousDocumentEpoch,
timeoutMs: Math.min(60000, remainingMs),
});
realmRecoveryCount += 1;
lastObservation = undefined;
lastSignature = undefined;
lastProgressLogAt = 0;
transportDisconnectedAt = undefined;
};
// A restore-window retry is a new proof, not a continuation of the
// previous one. Reset the page-realm observer once per invocation so
// every attempt must independently produce all stable samples. The
// authenticated document can be replaced in this exact interval, so
// the same single-recovery budget also protects the initial reset.
try {
await this.page.evaluate(() => {
delete window.__WWebCanonicalAppStateRestoreBarrierV1;
});
} catch (error) {
await recoverReplacedRealm(error);
}
do {
let observation;
try {
observation = await this.page.evaluate(
observeCanonicalAppStateRestoreBarrier,
{
stallMs: Math.max(
5000,
Math.min(10000, pollIntervalMs * 3),
),
nativeStallMs: 15000,
requiredStableSamples,
},
);
} catch (error) {
await recoverReplacedRealm(error);
continue;
}
lastObservation = observation;
if (observation?.abi_compatible !== true) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_restore_barrier_abi_incompatible',
);
}
const details = {
socket_state: observation.socket_state,
socket_stream: observation.socket_stream,
socket_synced: observation.socket_synced,
pending_count: observation.pending_count,
pending_collection_names:
observation.pending_collection_names || [],
in_flight_count: observation.in_flight_count,
in_flight_collection_names:
observation.in_flight_collection_names || [],
dirty_count: observation.dirty_count,
dirty_collection_names:
observation.dirty_collection_names || [],
retry_count: observation.retry_count,
fatal_count: observation.fatal_count,
blocked_count: observation.blocked_count,
blocked_collection_names:
observation.blocked_collection_names || [],
blocked_quiescent: observation.blocked_quiescent === true,
retry_quiescent: observation.retry_quiescent === true,
passive_blocked_collections:
observation.passive_blocked_collections || [],
passive_retry_collections:
observation.passive_retry_collections || [],
stable_sample_count: observation.stable_sample_count,
redrive_pending: observation.redrive_pending === true,
native_stalled_for_ms: observation.native_stalled_for_ms || 0,
native_stall_threshold_ms:
observation.native_stall_threshold_ms || 15000,
};
if (observation.transport_connected !== true) {
const now = Date.now();
transportDisconnectedAt ??= now;
const disconnectedForMs = now - transportDisconnectedAt;
const remainingMs = startedAt + timeoutMs - now;
if (
disconnectedForMs >=
CANONICAL_APP_STATE_RESTORE_TRANSPORT_GRACE_MS ||
remainingMs <= 0
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_restore_barrier_not_connected',
{
...details,
transport_disconnected_for_ms: disconnectedForMs,
transport_grace_ms:
CANONICAL_APP_STATE_RESTORE_TRANSPORT_GRACE_MS,
},
);
}
if (
lastSignature !== 'transport_disconnected' ||
now - lastProgressLogAt >= 1000
) {
lastSignature = 'transport_disconnected';
lastProgressLogAt = now;
this.log(
'browser_bridge.app_state_restore_transport_reconciling',
{
...details,
transport_disconnected_for_ms: disconnectedForMs,
transport_grace_ms:
CANONICAL_APP_STATE_RESTORE_TRANSPORT_GRACE_MS,
},
{ force: true },
);
}
await new Promise((resolve) =>
setTimeout(
resolve,
Math.min(pollIntervalMs, Math.max(1, remainingMs)),
),
);
continue;
}
transportDisconnectedAt = undefined;
if (observation.status === 'fatal') {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_restore_barrier_fatal',
details,
);
}
if (observation.status === 'redrive_failed') {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_restore_barrier_redrive_failed',
{
...details,
redrive_failure: observation.redrive_failure,
},
);
}
if (observation.status === 'native_stalled') {
if (realmRecoveryCount >= 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_restore_native_stall_recovery_exhausted',
details,
);
}
const remainingMs = startedAt + timeoutMs - Date.now();
if (remainingMs < 1000) break;
this.log(
'browser_bridge.app_state_restore_native_stall_recovery_started',
details,
{ force: true },
);
await this.recoverCanonicalAppStateRestoreBarrierAfterNativeStall(
{
timeoutMs: Math.min(60000, remainingMs),
},
);
realmRecoveryCount += 1;
lastObservation = undefined;
lastSignature = undefined;
lastProgressLogAt = 0;
transportDisconnectedAt = undefined;
continue;
}
if (observation.redrive_action) {
this.log(
'browser_bridge.app_state_restore_barrier_redriven',
{
...details,
action: observation.redrive_action,
},
{ force: true },
);
}
const signature = JSON.stringify(details);
if (
signature !== lastSignature ||
Date.now() - lastProgressLogAt >= 5000
) {
lastSignature = signature;
lastProgressLogAt = Date.now();
this.log(
'browser_bridge.app_state_restore_barrier_progress',
details,
{ force: true },
);
}
if (observation.status === 'idle') {
const result = {
...details,
realm_recovery_count: realmRecoveryCount,
duration_ms: Date.now() - startedAt,
};
this.log(
'browser_bridge.app_state_restore_barrier_completed',
result,
{ force: true },
);
return result;
}
if (Date.now() - startedAt >= timeoutMs) break;
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
} while (Date.now() - startedAt < timeoutMs);
if (
lastObservation?.transport_connected !== true &&
transportDisconnectedAt !== undefined
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_restore_barrier_not_connected',
{
socket_state: lastObservation?.socket_state,
socket_stream: lastObservation?.socket_stream,
socket_synced: lastObservation?.socket_synced,
duration_ms: Date.now() - startedAt,
transport_disconnected_for_ms:
Date.now() - transportDisconnectedAt,
transport_grace_ms:
CANONICAL_APP_STATE_RESTORE_TRANSPORT_GRACE_MS,
realm_recovery_count: realmRecoveryCount,
},
);
}
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_restore_barrier_timeout',
{
duration_ms: Date.now() - startedAt,
pending_count: lastObservation?.pending_count,
pending_collection_names:
lastObservation?.pending_collection_names,
in_flight_count: lastObservation?.in_flight_count,
in_flight_collection_names:
lastObservation?.in_flight_collection_names,
dirty_count: lastObservation?.dirty_count,
dirty_collection_names: lastObservation?.dirty_collection_names,
retry_count: lastObservation?.retry_count,
fatal_count: lastObservation?.fatal_count,
blocked_count: lastObservation?.blocked_count,
blocked_collection_names:
lastObservation?.blocked_collection_names,
blocked_quiescent: lastObservation?.blocked_quiescent === true,
retry_quiescent: lastObservation?.retry_quiescent === true,
passive_blocked_collections:
lastObservation?.passive_blocked_collections,
passive_retry_collections:
lastObservation?.passive_retry_collections,
redrive_pending: lastObservation?.redrive_pending === true,
native_stalled_for_ms: lastObservation?.native_stalled_for_ms,
native_stall_threshold_ms:
lastObservation?.native_stall_threshold_ms,
realm_recovery_count: realmRecoveryCount,
},
);
}
/**
* WhatsApp Web can replace its document after the authenticated transport
* is released and before the app-state restore barrier settles. The
* navigation-scoped guard survives that transition, while the page-local
* acknowledgement, pairing watchdog and observer do not. Rebind the exact
* same safety contract to one replacement realm and make the caller earn a
* fresh stability window; repeated navigation remains fail-closed.
*/
async recoverCanonicalAppStateRestoreBarrierRealm({
previousDocumentEpoch,
timeoutMs = 60000,
} = {}) {
if (
!Number.isFinite(previousDocumentEpoch) ||
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 60000 ||
!this.bridgeInstallOptions ||
!this.canonicalReloadProof ||
this.canonicalReloadProof.documentEpoch !== previousDocumentEpoch ||
!this.canonicalCredentialGuardArmed ||
!this.canonicalCredentialGuardScriptIdentifier ||
this.networkQuiesced
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_restore_realm_recovery_order_invalid',
);
}
const startedAt = Date.now();
const deadlineAt = startedAt + timeoutMs;
const remainingTimeout = () => {
const remainingMs = deadlineAt - Date.now();
if (remainingMs < 1000) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_restore_realm_recovery_timeout',
);
}
return remainingMs;
};
let stage = 'runtime_stability';
try {
await this.waitForRuntimeStable({
timeoutMs: remainingTimeout(),
});
stage = 'bridge_reinstall';
await this.page.evaluate(
installBrowserSessionBridge,
this.bridgeInstallOptions,
);
stage = 'abi_preflight';
await this.assertCompatible({
waitForModulesMs: remainingTimeout(),
capabilityStage: 'runtime',
});
stage = 'transport_wait';
await this.page.waitForFunction(
() => {
const resolveModule = (name) => {
try {
const value = window.require?.(name);
return value?.default || value;
} catch {
return undefined;
}
};
const socket = resolveModule('WAWebSocketModel')?.Socket;
const comms = resolveModule('WAComms');
const multiDevice = resolveModule(
'WAWebUserPrefsMultiDevice',
);
const conn = resolveModule('WAWebConnModel')?.Conn;
const socketState = String(
socket?.state ?? socket?.get?.('state') ?? 'unknown',
);
const registered = multiDevice?.isRegistered?.() === true;
const pairingReferenceActive =
Boolean(conn?.ref) &&
(!registered ||
['UNPAIRED', 'UNPAIRED_IDLE'].includes(
socketState,
));
return (
navigator.onLine === true &&
registered &&
socketState === 'CONNECTED' &&
// MeUser is a UI projection and can lag behind an
// already authenticated transport after navigation.
// WAComms is the native, fail-closed transport proof.
comms?.isCommsInitialized?.() === true &&
comms?.isSocketConnected?.() === true &&
!pairingReferenceActive
);
},
{ timeout: remainingTimeout() },
);
stage = 'safety_resume';
await this.resumeCanonicalSafetyAfterAppStateRealmRecovery({
previousDocumentEpoch,
ttlMs: Math.min(300000, Math.max(60000, timeoutMs + 30000)),
});
stage = 'observer_reset';
await this.page.evaluate(() => {
delete window.__WWebCanonicalAppStateRestoreBarrierV1;
});
const result = {
recovered: true,
document_epoch_changed: true,
duration_ms: Date.now() - startedAt,
};
this.log(
'browser_bridge.app_state_restore_realm_recovered',
result,
{ force: true },
);
return result;
} catch (error) {
this.log(
'browser_bridge.app_state_restore_realm_recovery_failed',
{
stage,
error_code: safeDiagnosticCode(
error?.code || error?.message,
'wwebjs_canonical_app_state_restore_realm_recovery_failed',
),
},
{ force: true },
);
throw error;
}
}
async restoreCanonicalAppState(
sourceProjection,
{
timeoutMs = 30000,
syncJobTtlMs = 120000,
requireOffline = false,
passiveBlockedCollections = [],
passiveRetryCollections = [],
} = {},
) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 600000 ||
!Number.isSafeInteger(syncJobTtlMs) ||
syncJobTtlMs < 1000 ||
syncJobTtlMs > 600000 ||
typeof requireOffline !== 'boolean' ||
!Array.isArray(passiveBlockedCollections) ||
passiveBlockedCollections.some(
(name) => typeof name !== 'string',
) ||
new Set(passiveBlockedCollections).size !==
passiveBlockedCollections.length ||
!Array.isArray(passiveRetryCollections) ||
passiveRetryCollections.some((name) => typeof name !== 'string') ||
new Set(passiveRetryCollections).size !==
passiveRetryCollections.length ||
passiveRetryCollections.some((name) =>
passiveBlockedCollections.includes(name),
)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_restore_timeout_invalid',
);
}
await this.assertCompatible();
const browserProjection = canonicalStoreProjectionToBrowser(
sourceProjection,
{ webVersion: this.expectedVersion },
);
let result;
try {
result = await this.page.evaluate(
restoreCanonicalAppStateSyncKeys,
browserProjection.tables,
{
timeoutMs,
syncJobTtlMs,
requireOffline,
passiveBlockedCollections,
passiveRetryCollections,
},
);
} catch (error) {
const controlledCode = /wwebjs_canonical_[a-z0-9_]+/u.exec(
String(error?.message || error?.code || ''),
)?.[0];
if (controlledCode) {
let browserDiagnostic;
try {
browserDiagnostic = await this.page.evaluate(() => {
const value =
window.__WWebCanonicalAppStateRestoreDiagnosticV1;
if (!value || typeof value !== 'object') return null;
return {
stage:
typeof value.stage === 'string'
? value.stage
: undefined,
error_name:
typeof value.errorName === 'string'
? value.errorName
: undefined,
error_message:
typeof value.errorMessage === 'string'
? value.errorMessage
: undefined,
recorded_at: Number.isSafeInteger(value.recordedAt)
? value.recordedAt
: undefined,
expected_sync_key_count: Number.isSafeInteger(
value.expectedSyncKeyCount,
)
? value.expectedSyncKeyCount
: undefined,
observed_sync_key_count: Number.isSafeInteger(
value.observedSyncKeyCount,
)
? value.observedSyncKeyCount
: undefined,
matched_key_id_count: Number.isSafeInteger(
value.matchedKeyIdCount,
)
? value.matchedKeyIdCount
: undefined,
exact_sync_key_count: Number.isSafeInteger(
value.exactSyncKeyCount,
)
? value.exactSyncKeyCount
: undefined,
observed_version_count: Number.isSafeInteger(
value.observedVersionCount,
)
? value.observedVersionCount
: undefined,
observed_mutation_mac_count: Number.isSafeInteger(
value.observedMutationMacCount,
)
? value.observedMutationMacCount
: undefined,
derived_material_cleanup_attempted:
value.derivedMaterialCleanupAttempted === true,
derived_material_cleanup_stable_samples:
Number.isSafeInteger(
value.derivedMaterialCleanupStableSamples,
)
? value.derivedMaterialCleanupStableSamples
: undefined,
derived_material_observed_version_count:
Number.isSafeInteger(
value.derivedMaterialObservedVersionCount,
)
? value.derivedMaterialObservedVersionCount
: undefined,
derived_material_observed_mutation_mac_count:
Number.isSafeInteger(
value.derivedMaterialObservedMutationMacCount,
)
? value.derivedMaterialObservedMutationMacCount
: undefined,
verification_read_attempts: Number.isSafeInteger(
value.verificationReadAttempts,
)
? value.verificationReadAttempts
: undefined,
verification_window_ms: Number.isSafeInteger(
value.verificationWindowMs,
)
? value.verificationWindowMs
: undefined,
};
});
} catch {
browserDiagnostic = undefined;
}
this.log(
'browser_bridge.app_state_restore_failed',
{
error_code: controlledCode,
...(browserDiagnostic || {}),
},
{ force: true },
);
throw new WhatsAppSessionCompatibilityError(controlledCode, {
stage: 'app_state_restore',
});
}
throw error;
}
this.log('browser_bridge.app_state_restore_started', result, {
force: true,
});
return result;
}
async startCanonicalAppStateSync({
expectedGeneration,
syncJobTtlMs = 120000,
}) {
if (
!Number.isSafeInteger(expectedGeneration) ||
expectedGeneration <= 0 ||
!Number.isSafeInteger(syncJobTtlMs) ||
syncJobTtlMs < 1000 ||
syncJobTtlMs > 600000
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_sync_generation_invalid',
);
}
const result = await this.page.evaluate(
triggerPreparedCanonicalAppStateSync,
{
generation: expectedGeneration,
syncJobTtlMs,
},
);
this.log('browser_bridge.app_state_sync_started', result, {
force: true,
});
return result;
}
/**
* Recover WAWebSyncd states that cannot be redriven inside their current
* JavaScript realm. This includes a motionless dirty/in-flight queue and a
* persistent IndexedDB collection placeholder that never receives its
* version/LT hash. The IndexedDB sync keys survive navigation, while the
* realm-local scheduler state does not. Exactly one reload is allowed for
* a bridge lifecycle; a repeated stall is a hard compatibility failure.
*/
async recoverCanonicalAppStateSyncAfterNativeStall({
timeoutMs = 60000,
syncJobTtlMs = 120000,
restartSync = true,
recoveryKind = 'app_state_restore',
} = {}) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 120000 ||
!Number.isSafeInteger(syncJobTtlMs) ||
syncJobTtlMs < 1000 ||
syncJobTtlMs > 600000 ||
typeof restartSync !== 'boolean' ||
!['app_state_restore', 'transport'].includes(recoveryKind)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_native_sync_recovery_options_invalid',
);
}
if (
this.canonicalAppStateRealmRecoveryCount >= 1 ||
!this.bridgeInstallOptions ||
this.networkQuiesced
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_native_sync_recovery_exhausted',
);
}
// Consume the sole recovery attempt before navigation. A reload whose
// acknowledgement is ambiguous must never be retried automatically.
this.canonicalAppStateRealmRecoveryCount += 1;
const before = await this.page.evaluate(() => {
const resolveModule = (name) => {
try {
const value = window.require?.(name);
return value?.default || value;
} catch {
return undefined;
}
};
const socket = resolveModule('WAWebSocketModel')?.Socket;
const multiDevice = resolveModule('WAWebUserPrefsMultiDevice');
const me = resolveModule('WAWebUserPrefsMeUser');
const conn = resolveModule('WAWebConnModel')?.Conn;
const bridgeState = window.__WWebSessionBridge?.state?.();
const hasMe =
me?.getMaybeMeDevicePn?.() != null ||
me?.getMaybeMePnUser?.() != null ||
me?.getMaybeMeLidUser?.() != null;
return {
document_epoch: window.performance?.timeOrigin,
online: navigator.onLine === true,
registered: multiDevice?.isRegistered?.() === true,
socket_state: String(
socket?.state ?? socket?.get?.('state') ?? 'unknown',
),
has_me_device_pn: hasMe,
guard_active:
bridgeState?.canonicalCredentialGuardActive === true,
credential_protection_installed:
bridgeState?.canonicalCredentialProtectionInstalled ===
true,
destructive_credential_phase:
bridgeState?.canonicalDestructiveCredentialPhase ||
'unknown',
destructive_credential_acknowledged:
bridgeState?.canonicalDestructiveCredentialAcknowledged ===
true,
destructive_credential_violation_detected:
bridgeState?.canonicalDestructiveCredentialViolationDetected ===
true,
navigation_storage_guard_active:
bridgeState?.canonicalNavigationStorageGuardActive === true,
pairing_watchdog_active:
bridgeState?.canonicalPairingWatchdogActive === true,
pairing_detected:
bridgeState?.canonicalPairingDetected === true,
pairing_reference_retained: Boolean(conn?.ref),
};
});
if (
!Number.isFinite(before?.document_epoch) ||
before.online !== true ||
before.registered !== true ||
before.socket_state !== 'CONNECTED' ||
before.has_me_device_pn !== true ||
before.guard_active !== true ||
before.credential_protection_installed !== true ||
before.destructive_credential_phase !== 'live' ||
before.destructive_credential_acknowledged !== true ||
before.destructive_credential_violation_detected === true ||
before.navigation_storage_guard_active !== true ||
before.pairing_watchdog_active !== true ||
before.pairing_detected === true
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_native_sync_recovery_precondition_invalid',
);
}
const startedAt = Date.now();
const deadlineAt = startedAt + timeoutMs;
const remainingTimeout = () => {
const remainingMs = deadlineAt - Date.now();
if (remainingMs < 1000) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_native_sync_recovery_timeout',
);
}
return remainingMs;
};
const waitForTransportProof = (waitTimeoutMs) =>
this.page.waitForFunction(
() => {
const resolveModule = (name) => {
try {
const value = window.require?.(name);
return value?.default || value;
} catch {
return undefined;
}
};
const socket = resolveModule('WAWebSocketModel')?.Socket;
const comms = resolveModule('WAComms');
const multiDevice = resolveModule(
'WAWebUserPrefsMultiDevice',
);
return (
navigator.onLine === true &&
multiDevice?.isRegistered?.() === true &&
String(
socket?.state ??
socket?.get?.('state') ??
'unknown',
) === 'CONNECTED' &&
// Do not make native recovery wait for the eventual
// MeUser UI projection. The registered socket plus
// live WAComms transport is the authoritative proof.
comms?.isCommsInitialized?.() === true &&
comms?.isSocketConnected?.() === true
);
},
{ timeout: waitTimeoutMs },
);
let stage = 'document_reload';
try {
try {
await this.page.reload({
waitUntil: 'load',
timeout: remainingTimeout(),
});
} catch (error) {
if (!isTransientNavigationError(error)) throw error;
}
stage = 'runtime_stability';
await this.waitForRuntimeStable({
timeoutMs: remainingTimeout(),
});
stage = 'bridge_reinstall';
await this.page.evaluate(
installBrowserSessionBridge,
this.bridgeInstallOptions,
);
stage = 'abi_preflight';
await this.assertCompatible({
waitForModulesMs: remainingTimeout(),
capabilityStage: 'runtime',
});
stage = 'transport_proof';
const passiveTransportProofMs = Math.min(5000, remainingTimeout());
try {
await waitForTransportProof(passiveTransportProofMs);
} catch {
stage = 'transport_redrive';
const redrive = await this.page.evaluate(
redriveCanonicalRealmRecoveryTransport,
{
previousDocumentEpoch: before.document_epoch,
timeoutMs: Math.min(15000, remainingTimeout()),
},
);
if (
redrive?.ok !== true ||
(redrive.launch_triggered !== true &&
redrive.transport_already_connected !== true)
) {
throw new WhatsAppSessionCompatibilityError(
redrive?.code ||
'wwebjs_canonical_recovery_transport_redrive_invalid',
{ socket_state: redrive?.socket_state || 'unknown' },
);
}
this.log(
'browser_bridge.app_state_native_sync_transport_redriven',
{
previous_socket_state: redrive.previous_socket_state,
socket_state: redrive.socket_state,
launch_triggered: redrive.launch_triggered === true,
socket_connected: redrive.socket_connected === true,
runtime_launch_state: redrive.runtime_launch_state,
},
{ force: true },
);
stage = 'transport_proof';
await waitForTransportProof(remainingTimeout());
}
const proof = await this.page.evaluate(() => {
const resolveModule = (name) => {
try {
const value = window.require?.(name);
return value?.default || value;
} catch {
return undefined;
}
};
const socket = resolveModule('WAWebSocketModel')?.Socket;
const comms = resolveModule('WAComms');
const multiDevice = resolveModule('WAWebUserPrefsMultiDevice');
const me = resolveModule('WAWebUserPrefsMeUser');
const conn = resolveModule('WAWebConnModel')?.Conn;
const hasMe =
me?.getMaybeMeDevicePn?.() != null ||
me?.getMaybeMePnUser?.() != null ||
me?.getMaybeMeLidUser?.() != null;
return {
document_epoch: window.performance?.timeOrigin,
online: navigator.onLine === true,
registered: multiDevice?.isRegistered?.() === true,
socket_state: String(
socket?.state ?? socket?.get?.('state') ?? 'unknown',
),
comms_initialized: comms?.isCommsInitialized?.() === true,
socket_connected: comms?.isSocketConnected?.() === true,
has_me_device_pn: hasMe,
pairing_reference_retained: Boolean(conn?.ref),
};
});
if (
!Number.isFinite(proof?.document_epoch) ||
proof.document_epoch === before.document_epoch ||
proof.online !== true ||
proof.registered !== true ||
proof.socket_state !== 'CONNECTED' ||
proof.comms_initialized !== true ||
proof.socket_connected !== true
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_native_sync_recovery_transport_invalid',
);
}
stage = 'safety_resume';
await this.resumeCanonicalSafetyAfterAppStateRealmRecovery({
previousDocumentEpoch: before.document_epoch,
ttlMs: Math.min(300000, Math.max(60000, syncJobTtlMs + 30000)),
});
let sync;
if (restartSync) {
stage = 'sync_restart';
sync = await this.triggerInitialCanonicalAppStateFullSync({
timeoutMs: Math.min(timeoutMs, 30000),
syncJobTtlMs,
forceFreshGeneration: true,
});
if (
sync?.full_sync_status !== 'running' ||
!Number.isSafeInteger(sync?.sync_job_generation) ||
sync.sync_job_generation <= 0
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_native_sync_recovery_generation_invalid',
);
}
} else {
stage = 'restore_barrier_reset';
await this.page.evaluate(() => {
delete window.__WWebCanonicalAppStateRestoreBarrierV1;
});
}
const result = {
recovered: true,
reload_count: this.canonicalAppStateRealmRecoveryCount,
document_epoch_changed: true,
duration_ms: Date.now() - startedAt,
...(restartSync
? { sync_job_generation: sync.sync_job_generation }
: { restore_barrier_reset: true }),
};
this.log(
restartSync
? 'browser_bridge.app_state_native_sync_recovered'
: recoveryKind === 'transport'
? 'browser_bridge.canonical_transport_native_stall_recovered'
: 'browser_bridge.app_state_restore_native_stall_recovered',
result,
{ force: true },
);
return result;
} catch (error) {
this.log(
recoveryKind === 'transport'
? 'browser_bridge.canonical_transport_native_stall_recovery_failed'
: 'browser_bridge.app_state_native_sync_recovery_failed',
{
stage,
reload_count: this.canonicalAppStateRealmRecoveryCount,
error_code: safeDiagnosticCode(
error?.code || error?.message,
'wwebjs_canonical_app_state_native_sync_recovery_failed',
),
},
{ force: true },
);
throw error;
}
}
/**
* Replace one authenticated page realm when the pre-restore WAWebSyncd
* scheduler is provably stalled. The source overlay is not installed yet,
* so this recovery intentionally does not start a canonical full sync; the
* caller must re-earn a fresh idle restore-window proof first.
*/
async recoverCanonicalAppStateRestoreBarrierAfterNativeStall({
timeoutMs = 60000,
} = {}) {
return this.recoverCanonicalAppStateSyncAfterNativeStall({
timeoutMs,
syncJobTtlMs: 120000,
restartSync: false,
});
}
async recoverCanonicalConnectedTransportAfterNativeStall({
timeoutMs = 60000,
} = {}) {
return this.recoverCanonicalAppStateSyncAfterNativeStall({
timeoutMs,
syncJobTtlMs: 120000,
restartSync: false,
recoveryKind: 'transport',
});
}
/**
* A native app-state stall is recovered by replacing the JavaScript
* realm after the candidate is already online. The pre-navigation guard
* protects that bootstrap, but the page-local acknowledgement and pairing
* watchdog intentionally do not survive navigation. Bind those latches to
* the replacement document before any synchronized state is accepted.
*/
async resumeCanonicalSafetyAfterAppStateRealmRecovery({
previousDocumentEpoch,
ttlMs = 150000,
} = {}) {
if (
!Number.isFinite(previousDocumentEpoch) ||
!Number.isSafeInteger(ttlMs) ||
ttlMs < 1000 ||
ttlMs > 300000 ||
!this.canonicalReloadProof ||
this.canonicalReloadProof.documentEpoch !== previousDocumentEpoch ||
!this.canonicalCredentialGuardArmed ||
!this.canonicalCredentialGuardScriptIdentifier ||
this.networkQuiesced
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_native_sync_safety_resume_order_invalid',
);
}
this.assertNoCanonicalCredentialViolation();
const proof = await this.page.evaluate(
({ previousEpoch, leaseTtlMs }) => {
const bridge = window.__WWebSessionBridge;
const resolveModule = (name) => {
try {
const value = window.require?.(name);
return value?.default || value;
} catch {
return undefined;
}
};
const socket = resolveModule('WAWebSocketModel')?.Socket;
const comms = resolveModule('WAComms');
const multiDevice = resolveModule('WAWebUserPrefsMultiDevice');
const me = resolveModule('WAWebUserPrefsMeUser');
const conn = resolveModule('WAWebConnModel')?.Conn;
const before = bridge?.state?.();
const documentEpoch = window.performance?.timeOrigin;
const hasMe =
me?.getMaybeMeDevicePn?.() != null ||
me?.getMaybeMePnUser?.() != null ||
me?.getMaybeMeLidUser?.() != null;
const transportValid =
Number.isFinite(documentEpoch) &&
documentEpoch !== previousEpoch &&
navigator.onLine === true &&
multiDevice?.isRegistered?.() === true &&
String(
socket?.state ?? socket?.get?.('state') ?? 'unknown',
) === 'CONNECTED' &&
comms?.isCommsInitialized?.() === true &&
comms?.isSocketConnected?.() === true;
const bootstrapSafetyValid =
before?.canonicalCredentialGuardActive === true &&
before?.canonicalCredentialProtectionInstalled === true &&
before?.canonicalDestructiveCredentialPhase ===
'suppressing' &&
before?.canonicalDestructiveCredentialAcknowledged !==
true &&
before?.canonicalDestructiveCredentialViolationDetected !==
true &&
before?.canonicalNavigationStorageGuardActive === true &&
before?.canonicalPairingDetected !== true;
if (!transportValid || !bootstrapSafetyValid) {
return {
ok: false,
document_epoch: documentEpoch,
transport_valid: transportValid,
bootstrap_safety_valid: bootstrapSafetyValid,
has_me_device_pn: hasMe,
};
}
const expiresAt = Date.now() + leaseTtlMs;
let renewed;
try {
renewed =
window.__WWebCanonicalNavigationStorageGuardV1?.renew?.(
expiresAt,
) === true;
window.__WWebCanonicalRehydrationGuardUntilV1 = expiresAt;
sessionStorage.setItem(
'__wwebjs_canonical_rehydration_guard_v1',
String(expiresAt),
);
} catch {
return {
ok: false,
document_epoch: documentEpoch,
guard_renewed: false,
};
}
const acknowledged =
renewed &&
bridge?.acknowledgeCanonicalCredentialProtection?.() ===
true;
const released =
acknowledged &&
bridge?.releaseCanonicalCredentialProtectionToLive?.() ===
true;
const watchdogArmed =
released &&
bridge?.armCanonicalPairingWatchdog?.() === true;
const after = bridge?.state?.();
const ok =
renewed &&
acknowledged &&
released &&
watchdogArmed &&
after?.canonicalCredentialGuardActive === true &&
after?.canonicalCredentialProtectionInstalled === true &&
after?.canonicalDestructiveCredentialPhase === 'live' &&
after?.canonicalDestructiveCredentialAcknowledged ===
true &&
after?.canonicalDestructiveCredentialViolationDetected !==
true &&
after?.canonicalNavigationStorageGuardActive === true &&
after?.canonicalNavigationStorageSuppressionCount === 0 &&
after?.canonicalCredentialSuppressionCount === 0 &&
after?.canonicalPairingWatchdogActive === true &&
after?.canonicalPairingDetected !== true;
return {
ok,
document_epoch: documentEpoch,
guard_renewed: renewed,
credential_protection_acknowledged: acknowledged,
credential_protection_released: released,
pairing_watchdog_armed: watchdogArmed,
pairing_reference_retained: Boolean(conn?.ref),
};
},
{
previousEpoch: previousDocumentEpoch,
leaseTtlMs: ttlMs,
},
);
if (proof?.ok !== true || !Number.isFinite(proof.document_epoch)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_native_sync_safety_resume_invalid',
);
}
this.canonicalReloadProof = Object.freeze({
...this.canonicalReloadProof,
documentEpoch: proof.document_epoch,
appStateRealmRecoveryCount:
this.canonicalAppStateRealmRecoveryCount,
appStateRealmPreviousDocumentEpoch: previousDocumentEpoch,
});
this.log(
'browser_bridge.app_state_native_sync_safety_resumed',
{
document_epoch_changed: true,
credential_guard_renewed: true,
credential_protection_acknowledged: true,
credential_protection_live: true,
pairing_watchdog_armed: true,
reload_proof_rebound: true,
},
{ force: true },
);
return proof;
}
/**
* Start WhatsApp Web's own collection sync for a freshly paired browser.
* Unlike restoreCanonicalAppState, this path never rewrites sync keys:
* the browser is authoritative and we only verify that its encrypted key
* store is ready before invoking the official sync API once.
*/
async triggerInitialCanonicalAppStateFullSync({
timeoutMs = 30000,
syncJobTtlMs = 120000,
forceFreshGeneration = false,
} = {}) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 600000 ||
!Number.isSafeInteger(syncJobTtlMs) ||
syncJobTtlMs < 1000 ||
syncJobTtlMs > 600000 ||
typeof forceFreshGeneration !== 'boolean'
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_initial_app_state_sync_timeout_invalid',
);
}
await this.assertCompatible();
let result;
try {
result = await this.page.evaluate(
triggerCanonicalInitialAppStateFullSync,
{ timeoutMs, syncJobTtlMs, forceFreshGeneration },
);
} catch (error) {
const controlledCode = /wwebjs_canonical_[a-z0-9_]+/u.exec(
String(error?.message || error?.code || ''),
)?.[0];
if (controlledCode) {
throw new WhatsAppSessionCompatibilityError(controlledCode, {
stage: 'initial_app_state_sync',
});
}
throw error;
}
this.log('browser_bridge.initial_app_state_sync_started', result, {
force: true,
});
return result;
}
/**
* Materialize the two durable bootstrap acknowledgements represented by a
* validated canonical snapshot, then let WhatsApp Web's own backend event
* move Socket.hasSynced/stream to their ready state. This never assigns
* either socket property directly.
*/
async finalizeCanonicalAppStateReadiness({ timeoutMs = 30000 } = {}) {
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1000) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_readiness_timeout_invalid',
);
}
const result = await this.page.evaluate(
async ({ timeoutMs }) => {
const fail = (code) => {
throw new Error(code);
};
const requireModule = (name) => {
try {
return window.require?.(name);
} catch {
return undefined;
}
};
const appState = requireModule('WAWebUserPrefsAppStateSync');
const historySync = requireModule('WAWebUserPrefsHistorySync');
const backendEventBus = requireModule('WAWebBackendEventBus');
const socket = requireModule('WAWebSocketModel')?.Socket;
if (
typeof appState?.setAllCriticalDataSynced !== 'function' ||
typeof appState?.getAllCriticalDataSynced !== 'function' ||
typeof historySync?.setInitialHistorySyncComplete !==
'function' ||
typeof historySync?.getInitialHistorySyncComplete !==
'function' ||
typeof backendEventBus?.BackendEventBus
?.triggerCriticalSyncDone !== 'function' ||
!socket
) {
fail(
'wwebjs_canonical_app_state_readiness_abi_incompatible',
);
}
if (
navigator.onLine !== true ||
String(socket.state || '') !== 'CONNECTED'
) {
fail('wwebjs_canonical_app_state_readiness_not_connected');
}
await appState.setAllCriticalDataSynced();
await historySync.setInitialHistorySyncComplete();
if (
(await appState.getAllCriticalDataSynced()) !== true ||
(await historySync.getInitialHistorySyncComplete()) !== true
) {
fail('wwebjs_canonical_app_state_readiness_write_failed');
}
backendEventBus.BackendEventBus.triggerCriticalSyncDone();
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (
navigator.onLine === true &&
String(socket.state || '') === 'CONNECTED' &&
socket.hasSynced === true &&
String(socket.stream || '') === 'CONNECTED'
) {
return {
critical_data_synced: true,
initial_history_sync_complete: true,
socket_synced: true,
socket_stream: 'CONNECTED',
};
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
fail('wwebjs_canonical_app_state_readiness_timeout');
},
{ timeoutMs },
);
this.log('browser_bridge.app_state_readiness_finalized', result, {
force: true,
});
return result;
}
/**
* Wait until a newly paired browser exposes an internally consistent
* app-state projection. A fresh pairing has no trusted canonical baseline
* yet, so using the first browser export as both source and target would
* incorrectly classify an in-flight sync-action row (whose version is
* ahead of the collection watermark) as source divergence.
*
* When the browser has already persisted a sync key but its collection
* watermarks still trail durable mutation MACs, start one official full
* collection sync. Native pairing does not pass through the restore path,
* so without this trigger the browser can remain internally pending until
* the bounded gate expires. Two consecutive, equivalent samples are then
* required. This keeps the first durable checkpoint behind a bounded
* materialization barrier without weakening the strict source comparison
* used by restarts and handoffs.
*/
async waitForInitialCanonicalAppStateStability({
timeoutMs = 120000,
pollIntervalMs = 2000,
requiredStableSamples = 2,
allowLegacyProfileTransientProjection = false,
} = {}) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
!Number.isSafeInteger(pollIntervalMs) ||
pollIntervalMs < 100 ||
pollIntervalMs > timeoutMs ||
!Number.isSafeInteger(requiredStableSamples) ||
requiredStableSamples < 2 ||
requiredStableSamples > 10 ||
typeof allowLegacyProfileTransientProjection !== 'boolean'
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_initial_app_state_stability_options_invalid',
);
}
const emptyAppStateBaseline = {
tables: {
whatsapp_app_state_sync_keys: [],
whatsapp_app_state_version: [],
whatsapp_app_state_mutation_macs: [],
},
};
const startedAt = Date.now();
let trustedCandidate;
let stableSampleCount = 0;
let comparison = {
status: 'pending',
reason: 'initial_app_state_materializing',
stable_sample_count: 0,
required_stable_samples: requiredStableSamples,
};
const deadlineAtMs = startedAt + timeoutMs;
const runWithinGateDeadline = async (
phase,
operation,
operationTimeoutMs = CANONICAL_INITIAL_APP_STATE_BROWSER_OPERATION_TIMEOUT_MS,
) => {
const remainingMs = deadlineAtMs - Date.now();
if (remainingMs <= 0) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_initial_app_state_stability_timeout',
{
...comparison,
phase,
duration_ms: Date.now() - startedAt,
},
);
}
const effectiveTimeoutMs = Math.min(
remainingMs,
operationTimeoutMs,
);
const timeoutCode =
effectiveTimeoutMs < remainingMs
? 'wwebjs_canonical_initial_app_state_browser_operation_timeout'
: 'wwebjs_canonical_initial_app_state_stability_timeout';
let timeout;
const pending = Promise.resolve().then(operation);
// A renderer evaluation cannot be cancelled. RemoteAuth will
// terminate Chromium after this bounded gate fails closed, but a
// rejection from the abandoned protocol request can arrive only
// after that termination. Keep it handled independently.
pending.catch(() => {});
try {
return await Promise.race([
pending,
new Promise((_, reject) => {
timeout = setTimeout(
() =>
reject(
new WhatsAppSessionCompatibilityError(
timeoutCode,
{
...comparison,
phase,
operation_timeout_ms:
effectiveTimeoutMs,
gate_remaining_ms: remainingMs,
duration_ms: Date.now() - startedAt,
},
),
),
effectiveTimeoutMs,
);
timeout.unref?.();
}),
]);
} finally {
if (timeout) clearTimeout(timeout);
}
};
let lastPendingSignature;
let lastPendingLogAt = 0;
let initialFullSyncTriggered = false;
let requiredSyncGeneration;
let futureMacPendingSamples = 0;
let lastSnapshotResyncSignature;
let authoritativeProjectionReady = false;
let initialFullSyncBaselineSignatures;
let stableInitialSnapshotSignature;
let stableInitialSnapshotSamples = 0;
let canonicalProjectionOperationTimeoutCount = 0;
const projectionWorkObservedCollections = new Set();
const transientProjectionCodes = new Set([
'wwebjs_canonical_app_state_projection_invalid',
]);
const observeTransientProjection = (error, phase) => {
if (!transientProjectionCodes.has(error?.code)) return false;
trustedCandidate = undefined;
stableSampleCount = 0;
comparison = {
status: 'pending',
reason: 'initial_app_state_projection_transient',
error_code: error.code,
phase,
stable_sample_count: stableSampleCount,
required_stable_samples: requiredStableSamples,
initial_full_sync_triggered: initialFullSyncTriggered,
sync_job_generation: requiredSyncGeneration ?? null,
};
return true;
};
do {
const journalQuietForMs = this.lastCanonicalJournalMutationAt
? Date.now() - this.lastCanonicalJournalMutationAt
: CANONICAL_INITIAL_APP_STATE_JOURNAL_QUIET_MS;
if (
journalQuietForMs < CANONICAL_INITIAL_APP_STATE_JOURNAL_QUIET_MS
) {
trustedCandidate = undefined;
stableSampleCount = 0;
comparison = {
status: 'pending',
reason: 'initial_canonical_journal_busy',
journal_quiet_for_ms: Math.max(0, journalQuietForMs),
journal_quiet_required_ms:
CANONICAL_INITIAL_APP_STATE_JOURNAL_QUIET_MS,
stable_sample_count: 0,
required_stable_samples: requiredStableSamples,
};
const pendingSignature = comparison.reason;
const now = Date.now();
if (
pendingSignature !== lastPendingSignature ||
now - lastPendingLogAt >= 30000
) {
lastPendingSignature = pendingSignature;
lastPendingLogAt = now;
this.log(
'browser_bridge.initial_app_state_pending',
{
...comparison,
sync_job_status: 'not_observed',
duration_ms: now - startedAt,
},
{ force: true },
);
}
if (now - startedAt >= timeoutMs) break;
await new Promise((resolve) =>
setTimeout(resolve, pollIntervalMs),
);
continue;
}
const syncJob = await runWithinGateDeadline(
'sync_job_observation',
() =>
this.page.evaluate(observeCanonicalAppStateSyncJob, {
stallMs: Math.max(
5000,
Math.min(10000, pollIntervalMs * 3),
),
nativeStallMs: 15000,
authoritativeProjectionReady,
projectionWorkObservedCollections: [
...projectionWorkObservedCollections,
],
}),
);
if (syncJob.abi_compatible === false) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_sync_abi_incompatible',
);
}
if (syncJob.redrive_action) {
this.log(
'browser_bridge.app_state_sync_redriven',
{
action: syncJob.redrive_action,
sync_job_generation: syncJob.generation,
pending_count: syncJob.pending_count,
in_flight_count: syncJob.in_flight_count,
dirty_count: syncJob.dirty_count,
retry_count: syncJob.retry_count,
blocked_count: syncJob.blocked_count,
},
{ force: true },
);
}
if (
requiredSyncGeneration !== undefined &&
syncJob.generation !== null &&
syncJob.generation > requiredSyncGeneration
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_sync_generation_mismatch',
);
}
if (syncJob.present && syncJob.status === 'native_stalled') {
if (requiredSyncGeneration === undefined) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_native_sync_stalled',
{
native_stalled_for_ms:
syncJob.native_stalled_for_ms,
native_stall_threshold_ms:
syncJob.native_stall_threshold_ms,
},
);
}
const remainingMs = startedAt + timeoutMs - Date.now();
if (remainingMs < 1000) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_native_sync_stalled',
{
native_stalled_for_ms:
syncJob.native_stalled_for_ms,
native_stall_threshold_ms:
syncJob.native_stall_threshold_ms,
},
);
}
const recovered = await runWithinGateDeadline(
'native_sync_recovery',
() =>
this.recoverCanonicalAppStateSyncAfterNativeStall({
timeoutMs: Math.min(60000, remainingMs),
syncJobTtlMs: Math.min(600000, remainingMs),
}),
Math.min(60000, remainingMs),
);
requiredSyncGeneration = recovered.sync_job_generation;
trustedCandidate = undefined;
stableSampleCount = 0;
authoritativeProjectionReady = false;
stableInitialSnapshotSignature = undefined;
stableInitialSnapshotSamples = 0;
projectionWorkObservedCollections.clear();
comparison = {
status: 'pending',
reason: 'initial_app_state_native_sync_recovered',
stable_sample_count: 0,
required_stable_samples: requiredStableSamples,
sync_job_generation: requiredSyncGeneration,
};
continue;
}
if (syncJob.present && syncJob.status === 'timed_out') {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_sync_job_timeout',
);
}
if (syncJob.present && syncJob.status === 'failed') {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_sync_job_failed',
);
}
const requiredFullSyncCompleted =
requiredSyncGeneration !== undefined &&
syncJob.present &&
syncJob.status === 'completed' &&
syncJob.generation === requiredSyncGeneration;
const waitingForRequiredFullSync =
requiredSyncGeneration !== undefined &&
!requiredFullSyncCompleted;
let targetProjection;
if (waitingForRequiredFullSync) {
trustedCandidate = undefined;
stableSampleCount = 0;
comparison = {
status: 'pending',
reason: 'initial_app_state_full_sync_pending',
stable_sample_count: stableSampleCount,
required_stable_samples: requiredStableSamples,
initial_full_sync_triggered: true,
sync_job_generation: requiredSyncGeneration,
};
}
try {
targetProjection = await runWithinGateDeadline(
'canonical_projection_export',
() => this.exportCanonicalProjection(),
);
} catch (error) {
if (
error?.code ===
'wwebjs_canonical_initial_app_state_browser_operation_timeout' &&
error?.details?.phase === 'canonical_projection_export' &&
Date.now() + pollIntervalMs < deadlineAtMs
) {
canonicalProjectionOperationTimeoutCount += 1;
trustedCandidate = undefined;
stableSampleCount = 0;
comparison = {
status: 'pending',
reason: 'canonical_projection_export_retry',
phase: error.details.phase,
operation_timeout_ms:
error.details.operation_timeout_ms,
operation_timeout_count:
canonicalProjectionOperationTimeoutCount,
stable_sample_count: 0,
required_stable_samples: requiredStableSamples,
};
this.log(
'browser_bridge.initial_app_state_browser_operation_retry',
{
...comparison,
duration_ms: Date.now() - startedAt,
},
{ force: true },
);
await new Promise((resolve) =>
setTimeout(resolve, pollIntervalMs),
);
continue;
}
if (observeTransientProjection(error, 'export')) {
targetProjection = undefined;
if (Date.now() - startedAt >= timeoutMs) break;
await new Promise((resolve) =>
setTimeout(resolve, pollIntervalMs),
);
continue;
}
const blockers = Array.isArray(error?.details?.blockers)
? [...new Set(error.details.blockers)].filter(
(entry) => typeof entry === 'string',
)
: [];
const transientAppStateProjection =
error?.code === 'wwebjs_canonical_projection_incomplete' &&
blockers.length > 0 &&
blockers.every((entry) =>
CANONICAL_APP_STATE_TRANSIENT_BLOCKERS.has(entry),
);
const transientLegacyProfileProjection =
allowLegacyProfileTransientProjection &&
error?.code === 'wwebjs_canonical_projection_incomplete' &&
blockers.length > 0 &&
blockers.every(
(entry) =>
entry === 'module_abi.incompatible' ||
CANONICAL_LEGACY_PROFILE_TRANSIENT_BLOCKERS.has(
entry,
),
) &&
blockers.some((entry) =>
CANONICAL_LEGACY_PROFILE_TRANSIENT_BLOCKERS.has(entry),
);
if (
!transientAppStateProjection &&
!transientLegacyProfileProjection
) {
throw error;
}
trustedCandidate = undefined;
stableSampleCount = 0;
comparison = {
status: 'pending',
reason: transientLegacyProfileProjection
? 'legacy_profile_rehydrating'
: 'app_state_materializing',
transient_blocker_count: blockers.length,
stable_sample_count: stableSampleCount,
required_stable_samples: requiredStableSamples,
};
}
if (targetProjection) {
let internalComparison;
try {
internalComparison = compareCanonicalAppState(
emptyAppStateBaseline,
targetProjection,
);
} catch (error) {
if (!observeTransientProjection(error, 'comparison')) {
throw error;
}
const now = Date.now();
this.log(
'browser_bridge.initial_app_state_pending',
{
...comparison,
sync_job_status: syncJob.status,
duration_ms: now - startedAt,
},
{ force: true },
);
if (now - startedAt >= timeoutMs) break;
await new Promise((resolve) =>
setTimeout(resolve, pollIntervalMs),
);
continue;
}
const targetVersions =
targetProjection.tables.whatsapp_app_state_version;
const targetMacs =
targetProjection.tables.whatsapp_app_state_mutation_macs;
const targetSyncKeys =
targetProjection.tables.whatsapp_app_state_sync_keys;
const appStateRequiresSyncKey =
targetVersions.length > 0 || targetMacs.length > 0;
const appStateSyncKeyReady =
!appStateRequiresSyncKey ||
(Array.isArray(targetSyncKeys) &&
targetSyncKeys.length > 0);
if (
initialFullSyncTriggered &&
internalComparison.status === 'ready' &&
appStateSyncKeyReady
) {
authoritativeProjectionReady = true;
}
if (
initialFullSyncTriggered &&
initialFullSyncBaselineSignatures !== undefined
) {
const currentSignatures =
canonicalAppStateCollectionMaterializationSignatures(
targetProjection,
);
for (const collectionName of CANONICAL_APP_STATE_COLLECTIONS) {
if (
currentSignatures[collectionName] !==
initialFullSyncBaselineSignatures[collectionName]
) {
projectionWorkObservedCollections.add(
collectionName,
);
}
}
const stableSnapshotResyncCandidate =
isStableInitialSnapshotResyncCandidate(
internalComparison,
appStateSyncKeyReady,
);
if (stableSnapshotResyncCandidate) {
const currentSignature =
JSON.stringify(currentSignatures);
stableInitialSnapshotSamples =
currentSignature === stableInitialSnapshotSignature
? stableInitialSnapshotSamples + 1
: 1;
stableInitialSnapshotSignature = currentSignature;
if (
stableInitialSnapshotSamples >=
requiredStableSamples
) {
// Some WA Web builds resolve the official sync
// request without exposing its short-lived native
// queues. A complete, key-backed projection that
// remains byte-identical across the stability
// window is authoritative no-op evidence. The page
// observer still requires the official scheduler
// promise to have settled before completing the
// fenced generation.
authoritativeProjectionReady = true;
}
} else {
stableInitialSnapshotSignature = undefined;
stableInitialSnapshotSamples = 0;
}
}
const futureMacNeedsFullSync =
internalComparison.status === 'pending' &&
internalComparison.future_target_mac_count > 0 &&
appStateSyncKeyReady &&
(!syncJob.present || syncJob.status === 'completed') &&
!initialFullSyncTriggered;
futureMacPendingSamples = futureMacNeedsFullSync
? futureMacPendingSamples + 1
: 0;
const remainingGateMs = timeoutMs - (Date.now() - startedAt);
const needsOfficialInitialFullSync =
futureMacNeedsFullSync &&
futureMacPendingSamples >= 2 &&
remainingGateMs >= 1000;
if (needsOfficialInitialFullSync) {
// Mark before awaiting so an ambiguous page-side failure
// cannot start concurrent/repeated full-sync jobs. The
// restore helper verifies the same browser-owned sync key
// and invokes WhatsApp Web's official collection API.
initialFullSyncTriggered = true;
initialFullSyncBaselineSignatures =
canonicalAppStateCollectionMaterializationSignatures(
targetProjection,
);
this.log(
'browser_bridge.initial_app_state_sync_requested',
{
future_target_mac_count:
internalComparison.future_target_mac_count,
pending_mac_count:
internalComparison.pending_mac_count,
sync_key_count: targetSyncKeys.length,
persistent_sample_count: futureMacPendingSamples,
duration_ms: Date.now() - startedAt,
},
{ force: true },
);
const syncResult = await runWithinGateDeadline(
'official_full_sync_trigger',
() =>
this.triggerInitialCanonicalAppStateFullSync({
timeoutMs: Math.min(30000, remainingGateMs),
syncJobTtlMs: Math.min(600000, remainingGateMs),
forceFreshGeneration: syncJob.present,
}),
Math.min(30000, remainingGateMs),
);
if (
!Number.isSafeInteger(syncResult.sync_job_generation) ||
syncResult.sync_job_generation <= 0 ||
(syncJob.generation !== null &&
syncResult.sync_job_generation <=
syncJob.generation)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_sync_generation_invalid',
);
}
requiredSyncGeneration = syncResult.sync_job_generation;
this.log(
'browser_bridge.initial_app_state_sync_triggered',
{
current_sync_key_count:
syncResult.current_sync_key_count,
full_sync_triggered: syncResult.full_sync_triggered,
full_sync_reused: syncResult.full_sync_reused,
full_sync_status: syncResult.full_sync_status,
sync_job_generation: requiredSyncGeneration,
duration_ms: Date.now() - startedAt,
},
{ force: true },
);
}
const initialFullSyncCompleted =
!initialFullSyncTriggered ||
(syncJob.present &&
syncJob.status === 'completed' &&
syncJob.generation === requiredSyncGeneration);
let candidateProjection = targetProjection;
let snapshotResync;
let snapshotResyncEvidencePending = false;
const officialFullSyncCompleted =
initialFullSyncTriggered && initialFullSyncCompleted;
if (
officialFullSyncCompleted &&
internalComparison.status === 'pending' &&
internalComparison.future_target_mac_count > 0 &&
appStateSyncKeyReady
) {
const snapshotResyncCandidate =
normalizeCanonicalAppStateForSnapshotResync(
targetProjection,
);
const syncEvidenceCollectionSet =
canonicalAppStateSyncEvidenceCollectionSet(syncJob);
// A fresh pairing may finish the exact official full-sync
// generation before its native queue can be sampled. The
// persisted request is therefore valid evidence only in
// this completed initial-pairing gate. Restart and handoff
// catch-up intentionally keep requiring observed work.
const requestedCollectionSet = new Set(
(Array.isArray(syncJob?.requested_collection_names)
? syncJob.requested_collection_names
: []
).filter((name) =>
CANONICAL_APP_STATE_COLLECTION_SET.has(name),
),
);
const everyResyncCollectionCovered =
snapshotResyncCandidate.snapshot_resync_collections.every(
(name) =>
syncEvidenceCollectionSet.has(name) ||
requestedCollectionSet.has(name),
);
snapshotResyncEvidencePending =
snapshotResyncCandidate.snapshot_resync_collection_count >
0 && !everyResyncCollectionCovered;
if (!snapshotResyncEvidencePending) {
snapshotResync = snapshotResyncCandidate;
candidateProjection = snapshotResync.projection;
internalComparison = compareCanonicalAppState(
emptyAppStateBaseline,
candidateProjection,
);
const snapshotResyncSignature = JSON.stringify({
collection_ids: snapshotResync.collection_ids,
removed_version_row_count:
snapshotResync.removed_version_row_count,
removed_mutation_mac_count:
snapshotResync.removed_mutation_mac_count,
});
if (
snapshotResyncSignature !==
lastSnapshotResyncSignature
) {
lastSnapshotResyncSignature =
snapshotResyncSignature;
this.log(
'browser_bridge.initial_app_state_snapshot_resync',
{
snapshot_resync_collection_count:
snapshotResync.snapshot_resync_collection_count,
removed_version_row_count:
snapshotResync.removed_version_row_count,
removed_mutation_mac_count:
snapshotResync.removed_mutation_mac_count,
collection_ids:
snapshotResync.collection_ids,
sync_key_count: targetSyncKeys.length,
duration_ms: Date.now() - startedAt,
},
{ force: true },
);
}
}
}
if (
internalComparison.status !== 'ready' ||
!appStateSyncKeyReady ||
!initialFullSyncCompleted
) {
trustedCandidate = undefined;
stableSampleCount = 0;
comparison = {
...internalComparison,
status: 'pending',
reason: !appStateSyncKeyReady
? 'initial_app_state_sync_key_pending'
: !initialFullSyncCompleted
? 'initial_app_state_full_sync_pending'
: snapshotResyncEvidencePending
? 'snapshot_resync_evidence_pending'
: internalComparison.reason ||
'initial_app_state_materializing',
stable_sample_count: stableSampleCount,
required_stable_samples: requiredStableSamples,
initial_full_sync_triggered: initialFullSyncTriggered,
future_mac_pending_samples: futureMacPendingSamples,
};
} else if (!trustedCandidate) {
trustedCandidate = candidateProjection;
stableSampleCount = 1;
comparison = {
...internalComparison,
status: 'pending',
reason: 'initial_app_state_stability_window',
stable_sample_count: stableSampleCount,
required_stable_samples: requiredStableSamples,
...(snapshotResync
? {
snapshot_resync_collection_count:
snapshotResync.snapshot_resync_collection_count,
}
: {}),
};
} else {
const forwardComparison = compareCanonicalAppState(
trustedCandidate,
candidateProjection,
);
const reverseComparison = compareCanonicalAppState(
candidateProjection,
trustedCandidate,
);
const equivalent =
forwardComparison.status === 'ready' &&
reverseComparison.status === 'ready';
if (!equivalent) {
trustedCandidate = candidateProjection;
stableSampleCount = 1;
comparison = {
status: 'pending',
reason: 'initial_app_state_stability_reset',
forward_status: forwardComparison.status,
reverse_status: reverseComparison.status,
stable_sample_count: stableSampleCount,
required_stable_samples: requiredStableSamples,
};
this.log(
'browser_bridge.initial_app_state_stability_reset',
{
...comparison,
sync_job_status: syncJob.status,
duration_ms: Date.now() - startedAt,
},
{ force: true },
);
} else {
trustedCandidate = candidateProjection;
stableSampleCount += 1;
if (stableSampleCount >= requiredStableSamples) {
const result = {
...forwardComparison,
duration_ms: Date.now() - startedAt,
stable_sample_count: stableSampleCount,
required_stable_samples: requiredStableSamples,
projection: candidateProjection,
sync_job_generation:
requiredSyncGeneration ?? null,
...(snapshotResync
? {
snapshot_resync_collection_count:
snapshotResync.snapshot_resync_collection_count,
}
: {}),
};
this.log(
'browser_bridge.initial_app_state_stable',
{
...forwardComparison,
duration_ms: result.duration_ms,
stable_sample_count: stableSampleCount,
required_stable_samples:
requiredStableSamples,
},
{ force: true },
);
return result;
}
comparison = {
...forwardComparison,
status: 'pending',
reason: 'initial_app_state_stability_window',
stable_sample_count: stableSampleCount,
required_stable_samples: requiredStableSamples,
};
}
}
}
const pendingSignature = JSON.stringify(comparison);
const now = Date.now();
if (
pendingSignature !== lastPendingSignature ||
now - lastPendingLogAt >= 30000
) {
lastPendingSignature = pendingSignature;
lastPendingLogAt = now;
this.log(
'browser_bridge.initial_app_state_pending',
{
...comparison,
sync_job_status: syncJob.status,
duration_ms: now - startedAt,
},
{ force: true },
);
}
if (Date.now() - startedAt >= timeoutMs) break;
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
} while (Date.now() - startedAt < timeoutMs);
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_initial_app_state_stability_timeout',
{
...comparison,
duration_ms: Date.now() - startedAt,
},
);
}
/**
* Hold the handoff readiness boundary until the server-backed app-state is
* at least as new as the source snapshot. Equal versions require identical
* LT hashes and a byte-identical source MAC subset.
*/
async waitForCanonicalAppStateCatchUp(
sourceProjection,
{
timeoutMs = 120000,
pollIntervalMs = 2000,
deadlineAtMs,
expectedSyncGeneration,
allowPreservedNativeAppStateBaseline = false,
} = {},
) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
!Number.isSafeInteger(pollIntervalMs) ||
pollIntervalMs < 100 ||
pollIntervalMs > timeoutMs ||
(deadlineAtMs !== undefined &&
(!Number.isSafeInteger(deadlineAtMs) || deadlineAtMs <= 0)) ||
(expectedSyncGeneration !== undefined &&
(!Number.isSafeInteger(expectedSyncGeneration) ||
expectedSyncGeneration <= 0)) ||
typeof allowPreservedNativeAppStateBaseline !== 'boolean'
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_catch_up_timeout_invalid',
);
}
const startedAt = Date.now();
const effectiveDeadlineAtMs = Math.min(
startedAt + timeoutMs,
deadlineAtMs ?? Number.POSITIVE_INFINITY,
);
// This is the longest authenticated handoff stage. Extend the lease
// while it is still valid, before the official scheduler starts, so
// the terminal connection proof cannot inherit only the remainder of
// the import/reload lease.
if (this.canonicalCredentialGuardScriptIdentifier) {
await this.renewCanonicalCredentialGuard({
ttlMs: Math.min(300000, Math.max(60000, timeoutMs + 30000)),
});
}
let comparison;
let lastPendingSignature;
let lastPendingLogAt = 0;
let lastSnapshotResyncSignature;
let lastOfficialSyncFutureMacSignature;
let reconciledOfficialSyncFutureMutationMacCount = 0;
let removedOfficialSyncFutureMutationMacCount = 0;
let snapshotResyncPreserved = false;
const snapshotResyncCollections = normalizedSnapshotResyncCollections(
sourceProjection?.capabilities,
{ optional: true },
);
const snapshotResyncCollectionSet = new Set(snapshotResyncCollections);
const snapshotResyncRequired = snapshotResyncCollections.length > 0;
const preservedNativeBaselineMode =
allowPreservedNativeAppStateBaseline === true;
if (
allowPreservedNativeAppStateBaseline &&
(this.canonicalPreservedNativeAppState !== true ||
this.networkQuiesced)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_preserved_native_mode_invalid',
);
}
if (
snapshotResyncRequired &&
expectedSyncGeneration === undefined &&
!allowPreservedNativeAppStateBaseline
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_sync_generation_invalid',
);
}
const requiredStableSamples = 2;
let trustedMaterializedProjection;
let stableSampleCount = 0;
let authoritativeProjectionReady = false;
let pendingAuthoritativeProjection;
let authoritativeStableSampleCount = 0;
let transientProjectionStallStartedAt;
let officialSyncNoProgressSignature;
let officialSyncNoProgressStartedAt;
const projectionWorkObservedCollections = new Set();
while (Date.now() < effectiveDeadlineAtMs) {
const syncJob = await this.page.evaluate(
observeCanonicalAppStateSyncJob,
{
stallMs: Math.max(
5000,
Math.min(10000, pollIntervalMs * 3),
),
nativeStallMs: 15000,
authoritativeProjectionReady,
projectionWorkObservedCollections: [
...projectionWorkObservedCollections,
],
},
);
if (syncJob.abi_compatible === false) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_sync_abi_incompatible',
);
}
if (syncJob.redrive_action) {
this.log(
'browser_bridge.app_state_sync_redriven',
{
action: syncJob.redrive_action,
sync_job_generation: syncJob.generation,
pending_count: syncJob.pending_count,
in_flight_count: syncJob.in_flight_count,
dirty_count: syncJob.dirty_count,
retry_count: syncJob.retry_count,
blocked_count: syncJob.blocked_count,
},
{ force: true },
);
}
if (
expectedSyncGeneration !== undefined &&
(!syncJob.present ||
syncJob.generation !== expectedSyncGeneration)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_sync_generation_mismatch',
);
}
if (syncJob.present && syncJob.status === 'native_stalled') {
if (expectedSyncGeneration === undefined) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_native_sync_stalled',
{
native_stalled_for_ms:
syncJob.native_stalled_for_ms,
native_stall_threshold_ms:
syncJob.native_stall_threshold_ms,
},
);
}
const remainingMs = effectiveDeadlineAtMs - Date.now();
if (remainingMs < 1000) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_native_sync_stalled',
{
native_stalled_for_ms:
syncJob.native_stalled_for_ms,
native_stall_threshold_ms:
syncJob.native_stall_threshold_ms,
},
);
}
const recovered =
await this.recoverCanonicalAppStateSyncAfterNativeStall({
timeoutMs: Math.min(60000, remainingMs),
syncJobTtlMs: Math.min(600000, remainingMs),
});
expectedSyncGeneration = recovered.sync_job_generation;
trustedMaterializedProjection = undefined;
stableSampleCount = 0;
authoritativeProjectionReady = false;
pendingAuthoritativeProjection = undefined;
authoritativeStableSampleCount = 0;
projectionWorkObservedCollections.clear();
comparison = {
status: 'pending',
reason: 'app_state_native_sync_recovered',
stable_sample_count: 0,
required_stable_samples: requiredStableSamples,
sync_job_generation: expectedSyncGeneration,
};
continue;
}
if (syncJob.present && syncJob.status === 'timed_out') {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_sync_job_timeout',
);
}
if (syncJob.present && syncJob.status === 'failed') {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_sync_job_failed',
);
}
const requiredFullSyncCompleted =
expectedSyncGeneration === undefined ||
(syncJob.present &&
syncJob.status === 'completed' &&
syncJob.generation === expectedSyncGeneration);
let targetProjection;
let candidateProjection;
if (!requiredFullSyncCompleted) {
trustedMaterializedProjection = undefined;
stableSampleCount = 0;
comparison = {
status: 'pending',
reason: 'app_state_official_sync_pending',
stable_sample_count: stableSampleCount,
required_stable_samples: requiredStableSamples,
};
}
try {
targetProjection = await this.exportCanonicalProjection();
transientProjectionStallStartedAt = undefined;
} catch (error) {
const blockers = Array.isArray(error?.details?.blockers)
? [...new Set(error.details.blockers)].filter(
(entry) => typeof entry === 'string',
)
: [];
const transientAppStateProjection =
error?.code === 'wwebjs_canonical_projection_incomplete' &&
blockers.length > 0 &&
blockers.every((entry) =>
CANONICAL_APP_STATE_TRANSIENT_BLOCKERS.has(entry),
);
if (!transientAppStateProjection) throw error;
const transientProjectionObservedAt = Date.now();
transientProjectionStallStartedAt ??=
transientProjectionObservedAt;
const transientProjectionStalledForMs =
transientProjectionObservedAt -
transientProjectionStallStartedAt;
trustedMaterializedProjection = undefined;
stableSampleCount = 0;
pendingAuthoritativeProjection = undefined;
authoritativeStableSampleCount = 0;
authoritativeProjectionReady = false;
comparison = {
status: 'pending',
reason: 'app_state_materializing',
transient_blocker_count: blockers.length,
transient_stalled_for_ms: transientProjectionStalledForMs,
transient_stall_threshold_ms:
CANONICAL_APP_STATE_TRANSIENT_STALL_MS,
stable_sample_count: stableSampleCount,
required_stable_samples: requiredStableSamples,
};
if (
expectedSyncGeneration !== undefined &&
syncJob.present &&
['running', 'completed'].includes(syncJob.status) &&
transientProjectionStalledForMs >=
CANONICAL_APP_STATE_TRANSIENT_STALL_MS &&
this.canonicalAppStateRealmRecoveryCount < 1
) {
const remainingMs =
effectiveDeadlineAtMs - transientProjectionObservedAt;
if (remainingMs >= 1000) {
this.log(
'browser_bridge.app_state_projection_stall_recovery_started',
{
transient_blocker_count: blockers.length,
transient_stalled_for_ms:
transientProjectionStalledForMs,
transient_stall_threshold_ms:
CANONICAL_APP_STATE_TRANSIENT_STALL_MS,
sync_job_generation: expectedSyncGeneration,
sync_job_status: syncJob.status,
},
{ force: true },
);
const recovered =
await this.recoverCanonicalAppStateSyncAfterNativeStall(
{
timeoutMs: Math.min(60000, remainingMs),
syncJobTtlMs: Math.min(600000, remainingMs),
},
);
expectedSyncGeneration = recovered.sync_job_generation;
transientProjectionStallStartedAt = undefined;
trustedMaterializedProjection = undefined;
stableSampleCount = 0;
authoritativeProjectionReady = false;
pendingAuthoritativeProjection = undefined;
authoritativeStableSampleCount = 0;
projectionWorkObservedCollections.clear();
comparison = {
status: 'pending',
reason: 'app_state_projection_stall_recovered',
stable_sample_count: 0,
required_stable_samples: requiredStableSamples,
sync_job_generation: expectedSyncGeneration,
};
continue;
}
}
}
if (!requiredFullSyncCompleted && targetProjection) {
const materializedVersionRows =
targetProjection?.tables?.whatsapp_app_state_version;
const materializedMacRows =
targetProjection?.tables?.whatsapp_app_state_mutation_macs;
if (
expectedSyncGeneration !== undefined &&
((Array.isArray(materializedVersionRows) &&
materializedVersionRows.length > 0) ||
(Array.isArray(materializedMacRows) &&
materializedMacRows.length > 0))
) {
for (const row of [
...(materializedVersionRows || []),
...(materializedMacRows || []),
]) {
if (CANONICAL_APP_STATE_COLLECTION_SET.has(row?.name)) {
projectionWorkObservedCollections.add(row.name);
}
}
}
let evidenceProjection = targetProjection;
let preCompletionSnapshotResync;
if (
!preservedNativeBaselineMode &&
snapshotResyncRequired &&
expectedSyncGeneration !== undefined
) {
// A WWeb profile can legitimately retain private future
// rows for collections whose portable source projection
// carries the snapshot-resync marker. Those rows are not
// portable evidence and must not create a circular wait:
// the page-side sync job needs two authoritative host
// samples before it can detach a stale native scheduler,
// while the host previously waited for that job to finish
// before excluding the marked private rows. Normalize
// only the explicitly authorized collections for this
// pre-completion evidence check. The terminal branch
// below still requires the matching official generation
// to complete and its per-collection evidence before the
// marker can be preserved.
const candidate =
normalizeCanonicalAppStateForSnapshotResync(
targetProjection,
{
eligibleCollectionNames:
snapshotResyncCollectionSet,
requiredCollectionNames:
snapshotResyncCollectionSet,
},
);
if (candidate.snapshot_resync_collection_count > 0) {
preCompletionSnapshotResync = candidate;
evidenceProjection = candidate.projection;
}
}
const evidenceComparison = preservedNativeBaselineMode
? comparePreservedNativeAppStateBaseline(
sourceProjection,
targetProjection,
snapshotResyncCollectionSet,
)
: compareCanonicalAppState(
sourceProjection,
evidenceProjection,
);
let effectiveEvidenceComparison = evidenceComparison;
let officialSyncFutureMacNormalization;
if (
!preservedNativeBaselineMode &&
!snapshotResyncRequired &&
expectedSyncGeneration !== undefined &&
evidenceComparison.status === 'pending' &&
evidenceComparison.future_target_mac_count > 0
) {
officialSyncFutureMacNormalization =
normalizeCanonicalOfficialSyncFutureMacs(
sourceProjection,
evidenceProjection,
);
if (
officialSyncFutureMacNormalization.removed_future_mutation_mac_count >
0
) {
reconciledOfficialSyncFutureMutationMacCount =
officialSyncFutureMacNormalization.reconciled_future_mutation_mac_count;
removedOfficialSyncFutureMutationMacCount =
officialSyncFutureMacNormalization.removed_future_mutation_mac_count;
evidenceProjection =
officialSyncFutureMacNormalization.projection;
effectiveEvidenceComparison =
officialSyncFutureMacNormalization.comparison;
}
}
if (effectiveEvidenceComparison.status === 'ready') {
const authoritativeCandidate = preservedNativeBaselineMode
? targetProjection
: preCompletionSnapshotResync
? preCompletionSnapshotResync.projection
: reconcileCanonicalAppStateForPersistence(
sourceProjection,
evidenceProjection,
).projection;
if (!pendingAuthoritativeProjection) {
pendingAuthoritativeProjection = authoritativeCandidate;
authoritativeStableSampleCount = 1;
} else {
// A preserved native WWeb profile may contain stable
// browser-private sync-action rows above the committed
// collection watermark. Comparing that projection to
// itself with the provider-neutral comparator remains
// intentionally pending because those future rows are
// not portable. At this same-provider authenticated
// boundary, attest the *complete* native view using
// the same deterministic signature required again by
// the terminal materialization gate below. Cross-
// provider paths keep their strict bidirectional
// version/hash/MAC comparison.
const equivalent = preservedNativeBaselineMode
? canonicalAppStateProjectionStabilitySignature(
pendingAuthoritativeProjection,
) ===
canonicalAppStateProjectionStabilitySignature(
authoritativeCandidate,
)
: (() => {
const forwardComparison =
compareCanonicalAppState(
pendingAuthoritativeProjection,
authoritativeCandidate,
);
const reverseComparison =
compareCanonicalAppState(
authoritativeCandidate,
pendingAuthoritativeProjection,
);
return (
forwardComparison.status === 'ready' &&
reverseComparison.status === 'ready'
);
})();
if (equivalent) {
pendingAuthoritativeProjection =
authoritativeCandidate;
authoritativeStableSampleCount += 1;
} else {
pendingAuthoritativeProjection =
authoritativeCandidate;
authoritativeStableSampleCount = 1;
}
}
authoritativeProjectionReady =
authoritativeStableSampleCount >= requiredStableSamples;
} else {
pendingAuthoritativeProjection = undefined;
authoritativeStableSampleCount = 0;
authoritativeProjectionReady = false;
const pendingCollectionDiagnostics = Array.isArray(
effectiveEvidenceComparison.pending_collections,
)
? effectiveEvidenceComparison.pending_collections.map(
(entry) => ({
collection_id: entry.collection_id,
source_version: entry.source_version,
target_version: entry.target_version,
future_target_mac_count:
entry.future_target_mac_count,
future_target_matches_source_count:
entry.future_target_matches_source_count,
future_target_value_mismatch_count:
entry.future_target_value_mismatch_count,
future_target_extra_count:
entry.future_target_extra_count,
future_target_pending_count:
entry.future_target_pending_count,
missing_source_mac_count:
entry.missing_source_mac_count,
extra_target_mac_count:
entry.extra_target_mac_count,
}),
)
: undefined;
comparison = {
status: 'pending',
reason:
effectiveEvidenceComparison.reason ||
'app_state_official_sync_pending',
collection_count:
effectiveEvidenceComparison.collection_count,
equal_collection_count:
effectiveEvidenceComparison.equal_collection_count,
advanced_collection_count:
effectiveEvidenceComparison.advanced_collection_count,
pending_collection_count:
effectiveEvidenceComparison.pending_collection_count,
pending_mac_count:
effectiveEvidenceComparison.pending_mac_count,
future_target_mac_count:
effectiveEvidenceComparison.future_target_mac_count,
pending_future_target_mac_count:
effectiveEvidenceComparison.pending_future_target_mac_count,
normalized_future_substitution_count:
effectiveEvidenceComparison.normalized_future_substitution_count,
missing_source_mac_count:
effectiveEvidenceComparison.missing_source_mac_count,
extra_target_mac_count:
effectiveEvidenceComparison.extra_target_mac_count,
source_sync_key_count:
effectiveEvidenceComparison.source_sync_key_count,
target_sync_key_count:
effectiveEvidenceComparison.target_sync_key_count,
matching_sync_key_count:
effectiveEvidenceComparison.matching_sync_key_count,
missing_sync_key_count:
effectiveEvidenceComparison.missing_sync_key_count,
key_data_mismatch_count:
effectiveEvidenceComparison.key_data_mismatch_count,
fingerprint_mismatch_count:
effectiveEvidenceComparison.fingerprint_mismatch_count,
...(pendingCollectionDiagnostics
? {
pending_collections:
pendingCollectionDiagnostics,
}
: {}),
stable_sample_count: 0,
required_stable_samples: requiredStableSamples,
};
const sourceBehindCollections = Array.isArray(
effectiveEvidenceComparison.pending_collections,
)
? effectiveEvidenceComparison.pending_collections.filter(
(entry) =>
Number.isSafeInteger(entry?.source_version) &&
Number.isSafeInteger(entry?.target_version) &&
entry.source_version > entry.target_version,
)
: [];
const eligibleOfficialSyncNoProgress = Boolean(
expectedSyncGeneration !== undefined &&
syncJob.present &&
syncJob.status === 'running' &&
!preservedNativeBaselineMode &&
!snapshotResyncRequired &&
effectiveEvidenceComparison.collection_count ===
CANONICAL_APP_STATE_COLLECTIONS.length &&
effectiveEvidenceComparison.equal_collection_count >=
CANONICAL_APP_STATE_COLLECTIONS.length - 1 &&
effectiveEvidenceComparison.pending_collection_count ===
1 &&
effectiveEvidenceComparison.missing_source_mac_count ===
0 &&
effectiveEvidenceComparison.extra_target_mac_count ===
0 &&
(effectiveEvidenceComparison.missing_sync_key_count ??
0) === 0 &&
(effectiveEvidenceComparison.key_data_mismatch_count ??
0) === 0 &&
(effectiveEvidenceComparison.fingerprint_mismatch_count ??
0) === 0 &&
sourceBehindCollections.length === 1,
);
if (eligibleOfficialSyncNoProgress) {
const noProgressSignature = JSON.stringify({
generation: expectedSyncGeneration,
collection_count:
effectiveEvidenceComparison.collection_count,
equal_collection_count:
effectiveEvidenceComparison.equal_collection_count,
pending_collection_count:
effectiveEvidenceComparison.pending_collection_count,
pending_mac_count:
effectiveEvidenceComparison.pending_mac_count,
source_version:
sourceBehindCollections[0].source_version,
target_version:
sourceBehindCollections[0].target_version,
});
if (
noProgressSignature !==
officialSyncNoProgressSignature
) {
officialSyncNoProgressSignature =
noProgressSignature;
officialSyncNoProgressStartedAt = Date.now();
} else if (
Date.now() - officialSyncNoProgressStartedAt >=
CANONICAL_APP_STATE_OFFICIAL_SYNC_NO_PROGRESS_MS &&
this.canonicalAppStateRealmRecoveryCount < 1
) {
const remainingMs =
effectiveDeadlineAtMs - Date.now();
if (remainingMs >= 1000) {
this.log(
'browser_bridge.app_state_official_sync_no_progress_recovery_started',
{
stalled_for_ms:
Date.now() -
officialSyncNoProgressStartedAt,
stall_threshold_ms:
CANONICAL_APP_STATE_OFFICIAL_SYNC_NO_PROGRESS_MS,
equal_collection_count:
effectiveEvidenceComparison.equal_collection_count,
pending_collection_count: 1,
sync_job_generation:
expectedSyncGeneration,
},
{ force: true },
);
const recovered =
await this.recoverCanonicalAppStateSyncAfterNativeStall(
{
timeoutMs: Math.min(
60000,
remainingMs,
),
syncJobTtlMs: Math.min(
600000,
remainingMs,
),
},
);
expectedSyncGeneration =
recovered.sync_job_generation;
transientProjectionStallStartedAt = undefined;
officialSyncNoProgressSignature = undefined;
officialSyncNoProgressStartedAt = undefined;
trustedMaterializedProjection = undefined;
stableSampleCount = 0;
authoritativeProjectionReady = false;
pendingAuthoritativeProjection = undefined;
authoritativeStableSampleCount = 0;
projectionWorkObservedCollections.clear();
comparison = {
status: 'pending',
reason: 'app_state_official_sync_no_progress_recovered',
stable_sample_count: 0,
required_stable_samples:
requiredStableSamples,
sync_job_generation: expectedSyncGeneration,
};
continue;
}
}
} else {
officialSyncNoProgressSignature = undefined;
officialSyncNoProgressStartedAt = undefined;
}
}
}
if (requiredFullSyncCompleted && targetProjection) {
candidateProjection = targetProjection;
snapshotResyncPreserved = false;
comparison = preservedNativeBaselineMode
? comparePreservedNativeAppStateBaseline(
sourceProjection,
targetProjection,
snapshotResyncCollectionSet,
)
: compareCanonicalAppState(
sourceProjection,
targetProjection,
);
if (
!preservedNativeBaselineMode &&
!snapshotResyncRequired &&
expectedSyncGeneration !== undefined &&
comparison.status === 'pending' &&
comparison.future_target_mac_count > 0
) {
const normalized = normalizeCanonicalOfficialSyncFutureMacs(
sourceProjection,
targetProjection,
);
if (normalized.removed_future_mutation_mac_count > 0) {
reconciledOfficialSyncFutureMutationMacCount =
normalized.reconciled_future_mutation_mac_count;
removedOfficialSyncFutureMutationMacCount =
normalized.removed_future_mutation_mac_count;
candidateProjection = normalized.projection;
comparison = normalized.comparison;
const signature = JSON.stringify({
reconciled_future_mutation_mac_count:
normalized.reconciled_future_mutation_mac_count,
removed_future_mutation_mac_count:
normalized.removed_future_mutation_mac_count,
sync_job_generation: expectedSyncGeneration,
});
if (signature !== lastOfficialSyncFutureMacSignature) {
lastOfficialSyncFutureMacSignature = signature;
this.log(
'browser_bridge.app_state_official_sync_future_macs_normalized',
{
reconciled_future_mutation_mac_count:
normalized.reconciled_future_mutation_mac_count,
removed_future_mutation_mac_count:
normalized.removed_future_mutation_mac_count,
sync_job_generation: expectedSyncGeneration,
},
{ force: true },
);
}
}
}
let snapshotResync;
let snapshotResyncEvidencePending = false;
if (
!preservedNativeBaselineMode &&
snapshotResyncRequired &&
comparison.status === 'pending' &&
comparison.future_target_mac_count > 0
) {
const snapshotResyncCandidate =
normalizeCanonicalAppStateForSnapshotResync(
targetProjection,
{
eligibleCollectionNames:
snapshotResyncCollectionSet,
requiredCollectionNames:
snapshotResyncCollectionSet,
},
);
const syncEvidenceCollectionSet =
canonicalAppStateSyncEvidenceCollectionSet(syncJob);
const everyResyncCollectionObserved =
snapshotResyncCandidate.snapshot_resync_collections.every(
(name) => syncEvidenceCollectionSet.has(name),
);
snapshotResyncEvidencePending =
snapshotResyncCandidate.snapshot_resync_collection_count >
0 && !everyResyncCollectionObserved;
if (
snapshotResyncCandidate.snapshot_resync_collection_count >
0 &&
!snapshotResyncEvidencePending
) {
snapshotResync = snapshotResyncCandidate;
candidateProjection = snapshotResync.projection;
comparison = compareCanonicalAppState(
sourceProjection,
candidateProjection,
);
const signature = JSON.stringify({
collection_ids: snapshotResync.collection_ids,
removed_version_row_count:
snapshotResync.removed_version_row_count,
removed_mutation_mac_count:
snapshotResync.removed_mutation_mac_count,
});
if (signature !== lastSnapshotResyncSignature) {
lastSnapshotResyncSignature = signature;
this.log(
'browser_bridge.app_state_snapshot_resync_preserved',
{
snapshot_resync_collection_count:
snapshotResync.snapshot_resync_collection_count,
removed_version_row_count:
snapshotResync.removed_version_row_count,
removed_mutation_mac_count:
snapshotResync.removed_mutation_mac_count,
collection_ids:
snapshotResync.collection_ids,
sync_job_generation: expectedSyncGeneration,
},
{ force: true },
);
}
snapshotResyncPreserved = true;
}
}
if (snapshotResyncEvidencePending) {
comparison = {
...comparison,
status: 'pending',
reason: 'snapshot_resync_evidence_pending',
stable_sample_count: 0,
required_stable_samples: requiredStableSamples,
};
}
if (
comparison.status === 'ready' &&
!preservedNativeBaselineMode
) {
const reconciledCandidate =
reconcileCanonicalAppStateForPersistence(
sourceProjection,
candidateProjection,
);
candidateProjection = reconciledCandidate.projection;
comparison = reconciledCandidate.comparison;
}
if (comparison.status === 'ready') {
const materializationRequiredCollections =
preservedNativeBaselineMode
? CANONICAL_APP_STATE_COLLECTIONS
: snapshotResyncCollections;
const materializationRequired =
preservedNativeBaselineMode || snapshotResyncRequired;
const materializedCollectionNames = materializationRequired
? new Set(
targetProjection.tables.whatsapp_app_state_version.map(
(row) => row.name,
),
)
: new Set();
const missingMaterializedCollectionCount =
materializationRequired
? materializationRequiredCollections.reduce(
(count, name) =>
count +
(materializedCollectionNames.has(name)
? 0
: 1),
0,
)
: 0;
const materializedCollectionCount = materializationRequired
? materializationRequiredCollections.length -
missingMaterializedCollectionCount
: undefined;
if (missingMaterializedCollectionCount > 0) {
trustedMaterializedProjection = undefined;
stableSampleCount = 0;
comparison = {
...comparison,
status: 'pending',
reason: 'snapshot_resync_materialization_pending',
materialized_collection_count:
materializedCollectionCount,
missing_materialized_collection_count:
missingMaterializedCollectionCount,
required_materialized_collection_count:
materializationRequiredCollections.length,
stable_sample_count: stableSampleCount,
required_stable_samples: requiredStableSamples,
};
} else if (!trustedMaterializedProjection) {
trustedMaterializedProjection = candidateProjection;
stableSampleCount = 1;
comparison = {
...comparison,
status: 'pending',
reason: 'app_state_stability_window',
...(materializationRequired
? {
materialized_collection_count:
materializedCollectionCount,
}
: {}),
stable_sample_count: stableSampleCount,
required_stable_samples: requiredStableSamples,
};
} else {
const equivalent = preservedNativeBaselineMode
? canonicalAppStateProjectionStabilitySignature(
trustedMaterializedProjection,
) ===
canonicalAppStateProjectionStabilitySignature(
candidateProjection,
)
: (() => {
const forwardComparison =
compareCanonicalAppState(
trustedMaterializedProjection,
candidateProjection,
);
const reverseComparison =
compareCanonicalAppState(
candidateProjection,
trustedMaterializedProjection,
);
return (
forwardComparison.status === 'ready' &&
reverseComparison.status === 'ready'
);
})();
if (!equivalent) {
trustedMaterializedProjection = candidateProjection;
stableSampleCount = 1;
comparison = {
status: 'pending',
reason: 'app_state_stability_reset',
...(materializationRequired
? {
materialized_collection_count:
materializedCollectionCount,
}
: {}),
stable_sample_count: stableSampleCount,
required_stable_samples: requiredStableSamples,
};
} else {
trustedMaterializedProjection = candidateProjection;
stableSampleCount += 1;
if (stableSampleCount < requiredStableSamples) {
comparison = {
...comparison,
status: 'pending',
reason: 'app_state_stability_window',
...(materializationRequired
? {
materialized_collection_count:
materializedCollectionCount,
}
: {}),
stable_sample_count: stableSampleCount,
required_stable_samples:
requiredStableSamples,
};
}
}
}
} else {
trustedMaterializedProjection = undefined;
stableSampleCount = 0;
}
}
if (comparison.status === 'ready') {
const reconciled = preservedNativeBaselineMode
? expectedSyncGeneration !== undefined
? {
comparison,
projection:
normalizeCanonicalAuthoritativeAppStateForPortablePersistence(
candidateProjection,
).projection,
}
: {
comparison,
projection: preserveNativeSnapshotResyncMarker(
sourceProjection,
candidateProjection,
snapshotResyncCollectionSet,
),
}
: reconcileCanonicalAppStateForPersistence(
sourceProjection,
candidateProjection,
);
const result = {
...comparison,
duration_ms: Date.now() - startedAt,
stable_sample_count: stableSampleCount,
required_stable_samples: requiredStableSamples,
sync_job_generation: expectedSyncGeneration ?? null,
snapshot_resync_preserved:
preservedNativeBaselineMode &&
expectedSyncGeneration !== undefined
? false
: snapshotResyncPreserved,
...(removedOfficialSyncFutureMutationMacCount > 0
? {
reconciled_future_mutation_mac_count:
reconciledOfficialSyncFutureMutationMacCount,
removed_future_mutation_mac_count:
removedOfficialSyncFutureMutationMacCount,
}
: {}),
projection: reconciled.projection,
};
this.log(
'browser_bridge.app_state_catch_up_completed',
{
...comparison,
duration_ms: result.duration_ms,
},
{ force: true },
);
return result;
}
if (comparison.status === 'diverged') {
this.log(
'browser_bridge.app_state_catch_up_diverged',
{
...comparison,
sync_job_status: syncJob.status,
duration_ms: Date.now() - startedAt,
},
{ force: true },
);
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_diverged',
comparison,
);
}
const pendingSignature = JSON.stringify(comparison);
const now = Date.now();
if (
pendingSignature !== lastPendingSignature ||
now - lastPendingLogAt >= 30000
) {
lastPendingSignature = pendingSignature;
lastPendingLogAt = now;
this.log(
'browser_bridge.app_state_catch_up_pending',
{
...comparison,
sync_job_status: syncJob.status,
duration_ms: now - startedAt,
},
{ force: true },
);
}
const remainingMs = effectiveDeadlineAtMs - Date.now();
if (remainingMs <= 0) break;
await new Promise((resolve) =>
setTimeout(resolve, Math.min(pollIntervalMs, remainingMs)),
);
}
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_app_state_catch_up_timeout',
{
...comparison,
duration_ms: Date.now() - startedAt,
},
);
}
markClean() {
this.dirty = false;
return this.page.evaluate(() =>
window.__WWebSessionBridge?.markClean(),
);
}
/**
* Bind a canonical import to one stable, offline document. A fresh
* WhatsApp bootstrap may replace its main-frame context while clearing
* locally generated credentials. Reinstalling the idempotent bridge and
* renewing the navigation guard after that settles prevents a stale
* module registry from turning a recoverable bootstrap race into a
* candidate failure.
*/
async stabilizeCanonicalImportRuntime({
timeoutMs = 15000,
credentialGuardTtlMs = 300000,
} = {}) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 60000 ||
!Number.isSafeInteger(credentialGuardTtlMs) ||
credentialGuardTtlMs < 1000 ||
credentialGuardTtlMs > 300000
) {
throw new TypeError('Invalid canonical import runtime options.');
}
if (
!this.networkQuiesced ||
!this.canonicalCredentialGuardArmed ||
!this.canonicalCredentialGuardScriptIdentifier ||
!this.bridgeInstallOptions
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_import_runtime_order_invalid',
);
}
const boundRuntimeVersion = this.expectedVersion;
let stabilizationStage = 'bridge_reinstall';
try {
try {
await this.page.evaluate(
installBrowserSessionBridge,
this.bridgeInstallOptions,
);
} catch (error) {
if (!isTransientNavigationError(error)) throw error;
}
// Chromium may reset emulated network conditions while replacing
// the bootstrap document. Re-apply CDP offline mode after the
// bridge is present and prove it again in the page realm below.
stabilizationStage = 'network_requiesce';
await this.page.setOfflineMode(true);
this.networkQuiesced = true;
stabilizationStage = 'runtime_stability';
await this.waitForRuntimeStable({
timeoutMs,
runtimeMode: 'canonical_import',
});
stabilizationStage = 'stable_bridge_reinstall';
await this.page.evaluate(
installBrowserSessionBridge,
this.bridgeInstallOptions,
);
stabilizationStage = 'credential_guard_rearm';
await this.armCanonicalCredentialGuard({
ttlMs: credentialGuardTtlMs,
navigationScoped: true,
});
stabilizationStage = 'capability_wait';
const compatibility = await this.assertCompatible({
waitForModulesMs: timeoutMs,
capabilityStage: 'pre_import',
});
if (
typeof compatibility.actualVersion === 'string' &&
compatibility.actualVersion.length > 0 &&
compatibility.actualVersion !== boundRuntimeVersion
) {
this.expectedVersion = boundRuntimeVersion;
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_import_runtime_version_changed',
);
}
stabilizationStage = 'offline_proof';
const proof = await this.page.evaluate(() => ({
document_epoch_valid: Number.isFinite(
window.performance?.timeOrigin,
),
offline: navigator.onLine === false,
schema_version: window.__WWebSessionBridge?.schemaVersion,
guard_active:
window.__WWebSessionBridge?.state?.()
?.canonicalCredentialGuardActive === true,
}));
if (
proof?.document_epoch_valid !== true ||
proof.offline !== true ||
proof.schema_version !== BRIDGE_SCHEMA_VERSION ||
proof.guard_active !== true
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_import_runtime_proof_invalid',
);
}
this.log(
'browser_bridge.canonical_import_runtime_stabilized',
{
offline: true,
guard_active: true,
module_abi: compatibility.moduleAbi,
},
{ force: true },
);
return compatibility;
} catch (error) {
this.log(
'browser_bridge.canonical_import_runtime_failed',
{
stabilization_stage: stabilizationStage,
error_code: safeDiagnosticCode(
error?.code || error?.message,
'wwebjs_canonical_import_runtime_unknown_failure',
),
offline: this.networkQuiesced === true,
guard_armed: this.canonicalCredentialGuardArmed === true,
},
{ force: true },
);
if (error instanceof WhatsAppSessionCompatibilityError) {
throw error;
}
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_import_runtime_stabilization_failed',
{ stabilization_stage: stabilizationStage },
);
}
}
async waitForRuntimeStable({
timeoutMs = 30000,
pollIntervalMs = 250,
stableProbeCount = 3,
runtimeMode = 'full',
} = {}) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 120000 ||
!Number.isSafeInteger(pollIntervalMs) ||
pollIntervalMs < 50 ||
pollIntervalMs > 5000 ||
!Number.isSafeInteger(stableProbeCount) ||
stableProbeCount < 2 ||
stableProbeCount > 10 ||
!['canonical_import', 'full'].includes(runtimeMode)
) {
throw new TypeError('Invalid browser runtime wait options.');
}
const canonicalImportMode = runtimeMode === 'canonical_import';
const boundVersionValid =
typeof this.expectedVersion === 'string' &&
this.expectedVersion.length > 0 &&
this.expectedVersion !== 'latest';
const deadline = Date.now() + timeoutMs;
let stableCount = 0;
let stableDocumentEpoch;
let documentChangeCount = 0;
let transientNavigationCount = 0;
let lastProbe;
do {
try {
const probe = await this.page.evaluate(() => ({
actualVersion: window.Debug?.VERSION,
schemaVersion: window.__WWebSessionBridge?.schemaVersion,
documentEpoch: window.performance?.timeOrigin,
documentReadyState: document.readyState,
offline: navigator.onLine === false,
guardActive:
window.__WWebSessionBridge?.state?.()
?.canonicalCredentialGuardActive === true,
}));
lastProbe = probe;
const actualVersionPresent =
typeof probe?.actualVersion === 'string' &&
probe.actualVersion.length > 0;
const versionValid = canonicalImportMode
? boundVersionValid &&
(!actualVersionPresent ||
probe.actualVersion === this.expectedVersion)
: actualVersionPresent;
if (
versionValid &&
probe.schemaVersion === BRIDGE_SCHEMA_VERSION &&
Number.isFinite(probe.documentEpoch) &&
(!canonicalImportMode ||
(['interactive', 'complete'].includes(
probe.documentReadyState,
) &&
probe.offline === true &&
probe.guardActive === true))
) {
if (probe.documentEpoch === stableDocumentEpoch) {
stableCount += 1;
} else {
if (stableDocumentEpoch !== undefined) {
documentChangeCount += 1;
}
stableDocumentEpoch = probe.documentEpoch;
stableCount = 1;
}
if (stableCount >= stableProbeCount) {
return {
actualVersion: actualVersionPresent
? probe.actualVersion
: this.expectedVersion,
schemaVersion: probe.schemaVersion,
documentEpoch: probe.documentEpoch,
actualVersionObserved: actualVersionPresent,
};
}
} else {
stableCount = 0;
stableDocumentEpoch = undefined;
}
} catch (error) {
if (!isTransientNavigationError(error)) throw error;
transientNavigationCount += 1;
stableCount = 0;
stableDocumentEpoch = undefined;
}
if (Date.now() >= deadline) break;
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
} while (Date.now() < deadline);
const timeoutDetails = {
runtimeMode,
actualVersionPresent:
typeof lastProbe?.actualVersion === 'string' &&
lastProbe.actualVersion.length > 0,
boundVersionValid,
schemaVersionMatch:
lastProbe?.schemaVersion === BRIDGE_SCHEMA_VERSION,
documentReady:
lastProbe?.documentReadyState === 'interactive' ||
lastProbe?.documentReadyState === 'complete',
offline: lastProbe?.offline === true,
guardActive: lastProbe?.guardActive === true,
stableProbeCount: stableCount,
documentChangeCount,
transientNavigationCount,
};
this.log('browser_bridge.runtime_stability_timeout', timeoutDetails, {
force: true,
});
throw new WhatsAppSessionCompatibilityError(
'wwebjs_browser_runtime_stability_timeout',
timeoutDetails,
);
}
armMainFrameNavigation(timeoutMs) {
let settled = false;
let timer;
let resolveNavigation;
const onNavigation = (frame) => {
if (frame.parentFrame() !== null) return;
finish(true);
};
const finish = (navigated) => {
if (settled) return;
settled = true;
clearTimeout(timer);
this.page.off('framenavigated', onNavigation);
resolveNavigation(navigated);
};
const promise = new Promise((resolve) => {
resolveNavigation = resolve;
this.page.on('framenavigated', onNavigation);
timer = setTimeout(() => finish(false), timeoutMs);
});
return { promise, cancel: () => finish(false) };
}
async resumeNetworkAndReload({
timeoutMs = 30000,
automaticNavigationWaitMs = 3000,
} = {}) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 120000 ||
!Number.isSafeInteger(automaticNavigationWaitMs) ||
automaticNavigationWaitMs < 100 ||
automaticNavigationWaitMs > Math.min(timeoutMs, 10000)
) {
throw new TypeError('Invalid browser reload options.');
}
const navigation = this.armMainFrameNavigation(
automaticNavigationWaitMs,
);
let automaticNavigation;
try {
await this.resumeNetwork();
automaticNavigation = await navigation.promise;
} catch (error) {
navigation.cancel();
throw error;
}
let explicitReloadCount = 0;
const navigationSource = automaticNavigation ? 'automatic' : 'explicit';
if (!automaticNavigation) {
explicitReloadCount = 1;
try {
await this.page.reload({
waitUntil: 'load',
timeout: timeoutMs,
});
} catch (error) {
if (!isTransientNavigationError(error)) throw error;
}
}
const runtime = await this.waitForRuntimeStable({ timeoutMs });
this.log('browser_bridge.reload_navigation_settled', {
navigation_source: navigationSource,
explicit_reload_count: explicitReloadCount,
web_version: runtime.actualVersion,
});
return {
...runtime,
navigationSource,
explicitReloadCount,
};
}
/**
* Re-run WhatsApp Web's official launch branch after canonical credentials
* have been imported. HTTP is temporarily available so the document can
* be reconstructed, while the caller's MV3 gate continues to block every
* WebSocket at Chromium's request boundary. The page is returned offline
* and the proof is bound to the new document epoch.
*/
async reloadCanonicalImportedRuntime({
timeoutMs = 60000,
waitForModulesMs = 60000,
credentialGuardTtlMs = 300000,
allowRegistrationRepair = false,
} = {}) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 120000 ||
!Number.isSafeInteger(waitForModulesMs) ||
waitForModulesMs < 0 ||
waitForModulesMs > 60000 ||
!Number.isSafeInteger(credentialGuardTtlMs) ||
credentialGuardTtlMs < 1000 ||
credentialGuardTtlMs > 300000 ||
typeof allowRegistrationRepair !== 'boolean'
) {
throw new TypeError('Invalid canonical reload options.');
}
if (
!this.networkQuiesced ||
!this.canonicalCredentialGuardArmed ||
!this.canonicalCredentialGuardScriptIdentifier ||
!this.bridgeInstallOptions
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_reload_order_invalid',
);
}
const before = await this.page.evaluate(() => ({
document_epoch: window.performance?.timeOrigin,
offline: navigator.onLine === false,
registered:
window
.require?.('WAWebUserPrefsMultiDevice')
?.isRegistered?.() === true,
guard_active:
window.__WWebSessionBridge?.state?.()
?.canonicalCredentialGuardActive === true,
}));
if (
!Number.isFinite(before?.document_epoch) ||
before.offline !== true ||
before.registered !== true ||
before.guard_active !== true
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_reload_precondition_invalid',
);
}
this.canonicalReloadProof = undefined;
let reloadStage = 'logout_marker_clear';
try {
const logoutMarker = await this.page.evaluate(async () => {
let userPrefs;
try {
const value = window.require?.('WAWebUserPrefsGeneral');
userPrefs = value?.default || value;
} catch {
return { ok: false, cleared: false };
}
if (
typeof userPrefs?.getLogoutReason !== 'function' ||
userPrefs.getLogoutReason.length !== 0 ||
typeof userPrefs?.setLogoutReason !== 'function' ||
userPrefs.setLogoutReason.length !== 1
) {
return { ok: false, cleared: false };
}
try {
const current = await userPrefs.getLogoutReason();
if (!current) return { ok: true, cleared: false };
await userPrefs.setLogoutReason(null);
return {
ok: !(await userPrefs.getLogoutReason()),
cleared: true,
};
} catch {
return { ok: false, cleared: false };
}
});
if (logoutMarker?.ok !== true) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_logout_marker_clear_failed',
);
}
this.log(
'browser_bridge.canonical_logout_marker_cleared',
{
stale_marker_present: logoutMarker.cleared === true,
offline: true,
},
{ force: true },
);
reloadStage = 'network_release';
// CDP offline cannot reconstruct the WhatsApp document. Only HTTP
// is released here; the independently monitored MV3 rule remains
// installed and blocks all ws/wss requests.
await this.page.setOfflineMode(false);
this.networkQuiesced = false;
reloadStage = 'document_reload';
try {
await this.page.reload({
waitUntil: 'load',
timeout: timeoutMs,
});
} catch (error) {
if (!isTransientNavigationError(error)) throw error;
}
reloadStage = 'runtime_stability';
await this.waitForRuntimeStable({ timeoutMs });
reloadStage = 'bridge_reinstall';
await this.page.evaluate(
installBrowserSessionBridge,
this.bridgeInstallOptions,
);
reloadStage = 'credential_guard_rearm';
await this.armCanonicalCredentialGuard({
ttlMs: credentialGuardTtlMs,
navigationScoped: true,
});
reloadStage = 'abi_preflight';
const compatibility = await this.assertCompatible({
waitForModulesMs,
capabilityStage: 'post_reload',
});
reloadStage = 'watchdog_rearm';
const watchdogProof = await this.page.evaluate(
(expectedDocumentEpoch) => {
const bridge = window.__WWebSessionBridge;
const conn = window.require?.('WAWebConnModel')?.Conn;
const armed =
bridge?.armCanonicalPairingWatchdog?.() === true;
const bridgeState = bridge?.state?.();
return {
document_epoch_changed:
Number.isFinite(window.performance?.timeOrigin) &&
window.performance.timeOrigin !==
expectedDocumentEpoch,
armed,
guard_active:
bridgeState?.canonicalCredentialGuardActive ===
true,
navigation_storage_guard_active:
bridgeState?.canonicalNavigationStorageGuardActive ===
true,
pairing_detected:
bridgeState?.canonicalPairingDetected === true,
pairing_ref_present: Boolean(conn?.ref),
};
},
before.document_epoch,
);
// The watchdog is armed on the new document, so its epoch must be
// different from the pre-import bootstrap epoch.
if (
watchdogProof?.document_epoch_changed !== true ||
watchdogProof?.armed !== true ||
watchdogProof.guard_active !== true ||
watchdogProof.navigation_storage_guard_active !== true ||
watchdogProof.pairing_detected === true ||
watchdogProof.pairing_ref_present === true
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_reload_watchdog_invalid',
);
}
reloadStage = 'online_runtime_wait';
let onlineRuntimeReady = true;
try {
await this.page.waitForFunction(
(registrationRepairAllowed) => {
const bridgeState =
window.__WWebSessionBridge?.state?.();
const socket =
window.require?.('WAWebSocketModel')?.Socket;
const multiDevice = window.require?.(
'WAWebUserPrefsMultiDevice',
);
const comms = window.require?.('WAComms');
const conn = window.require?.('WAWebConnModel')?.Conn;
const socketState = String(
socket?.state ??
socket?.get?.('state') ??
'unknown',
);
const registered =
multiDevice?.isRegistered?.() === true;
const registrationBranchReady = registered
? ['OPENING', 'TIMEOUT', 'UNLAUNCHED'].includes(
socketState,
) &&
(comms?.isCommsInitialized?.() === true ||
registrationRepairAllowed === true)
: registrationRepairAllowed === true &&
['UNPAIRED', 'UNPAIRED_IDLE'].includes(
socketState,
);
return (
navigator.onLine === true &&
registrationBranchReady &&
bridgeState?.canonicalCredentialGuardActive ===
true &&
bridgeState?.canonicalCredentialProtectionInstalled ===
true &&
bridgeState?.canonicalDestructiveCredentialPhase ===
'suppressing' &&
bridgeState?.canonicalDestructiveCredentialViolationDetected !==
true &&
bridgeState?.canonicalNavigationStorageGuardActive ===
true &&
bridgeState?.canonicalPairingWatchdogActive ===
true &&
bridgeState?.canonicalPairingDetected !== true &&
!conn?.ref
);
},
{ timeout: Math.min(timeoutMs, 15000) },
allowRegistrationRepair,
);
} catch {
onlineRuntimeReady = false;
}
reloadStage = 'online_proof';
const onlineProof = await this.page.evaluate(async () => {
const bridge = window.__WWebSessionBridge;
const socket = window.require?.('WAWebSocketModel')?.Socket;
const multiDevice = window.require?.(
'WAWebUserPrefsMultiDevice',
);
const me = window.require?.('WAWebUserPrefsMeUser');
const infoStore = window.require?.('WAWebUserPrefsInfoStore');
const comms = window.require?.('WAComms');
const conn = window.require?.('WAWebConnModel')?.Conn;
const bridgeState = bridge?.state?.();
let noiseInfo;
try {
noiseInfo = await infoStore?.waNoiseInfo?.get?.();
} catch {
noiseInfo = undefined;
}
return {
document_epoch: window.performance?.timeOrigin,
online: navigator.onLine === true,
registered: multiDevice?.isRegistered?.() === true,
has_noise_record:
window.localStorage?.getItem?.('WANoiseInfo') != null,
has_noise_info: noiseInfo != null,
has_me_device_pn: me?.getMaybeMeDevicePn?.() != null,
comms_initialized: comms?.isCommsInitialized?.() === true,
socket_state: String(
socket?.state ?? socket?.get?.('state') ?? 'unknown',
),
guard_active:
bridgeState?.canonicalCredentialGuardActive === true,
credential_protection_installed:
bridgeState?.canonicalCredentialProtectionInstalled ===
true,
destructive_credential_phase:
bridgeState?.canonicalDestructiveCredentialPhase ||
'unknown',
destructive_credential_violation_detected:
bridgeState?.canonicalDestructiveCredentialViolationDetected ===
true,
navigation_storage_guard_active:
bridgeState?.canonicalNavigationStorageGuardActive ===
true,
navigation_storage_suppression_count: Number.isSafeInteger(
bridgeState?.canonicalNavigationStorageSuppressionCount,
)
? bridgeState.canonicalNavigationStorageSuppressionCount
: 0,
pairing_watchdog_active:
bridgeState?.canonicalPairingWatchdogActive === true,
pairing_detected:
bridgeState?.canonicalPairingDetected === true,
pairing_ref_present: Boolean(conn?.ref),
};
});
const onlineRegistrationReady = onlineProof?.registered
? ['OPENING', 'TIMEOUT', 'UNLAUNCHED'].includes(
onlineProof.socket_state,
) &&
(onlineProof.comms_initialized === true ||
allowRegistrationRepair === true)
: allowRegistrationRepair === true &&
['UNPAIRED', 'UNPAIRED_IDLE'].includes(
onlineProof?.socket_state,
);
if (
onlineRuntimeReady !== true ||
!Number.isFinite(onlineProof?.document_epoch) ||
onlineProof.document_epoch === before.document_epoch ||
onlineProof.online !== true ||
onlineRegistrationReady !== true ||
onlineProof.guard_active !== true ||
onlineProof.credential_protection_installed !== true ||
onlineProof.destructive_credential_phase !== 'suppressing' ||
onlineProof.destructive_credential_violation_detected ===
true ||
onlineProof.navigation_storage_guard_active !== true ||
onlineProof.pairing_watchdog_active !== true ||
onlineProof.pairing_detected === true ||
onlineProof.pairing_ref_present === true ||
![
'OPENING',
'TIMEOUT',
'UNLAUNCHED',
'UNPAIRED',
'UNPAIRED_IDLE',
].includes(onlineProof.socket_state)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_reload_runtime_invalid',
{
socket_state:
typeof onlineProof?.socket_state === 'string'
? onlineProof.socket_state
: 'unknown',
registered: onlineProof?.registered === true,
has_noise_record:
onlineProof?.has_noise_record === true,
has_noise_info: onlineProof?.has_noise_info === true,
has_me_device_pn:
onlineProof?.has_me_device_pn === true,
comms_initialized:
onlineProof?.comms_initialized === true,
guard_active: onlineProof?.guard_active === true,
navigation_storage_guard_active:
onlineProof?.navigation_storage_guard_active ===
true,
navigation_storage_suppression_count:
Number.isSafeInteger(
onlineProof?.navigation_storage_suppression_count,
)
? onlineProof.navigation_storage_suppression_count
: 0,
pairing_watchdog_active:
onlineProof?.pairing_watchdog_active === true,
pairing_detected:
onlineProof?.pairing_detected === true,
pairing_ref_present:
onlineProof?.pairing_ref_present === true,
},
);
}
reloadStage = 'network_requiesce';
await this.page.setOfflineMode(true);
this.networkQuiesced = true;
await this.page.waitForFunction(
() => {
const socket = window.require?.('WAWebSocketModel')?.Socket;
return (
navigator.onLine === false &&
String(
socket?.state ??
socket?.get?.('state') ??
'unknown',
) !== 'CONNECTED'
);
},
{ timeout: Math.min(timeoutMs, 15000) },
);
reloadStage = 'journal_flush';
await this.flush();
reloadStage = 'offline_proof';
const offlineProof = await this.page.evaluate(() => {
const bridge = window.__WWebSessionBridge;
const multiDevice = window.require?.(
'WAWebUserPrefsMultiDevice',
);
const socket = window.require?.('WAWebSocketModel')?.Socket;
const comms = window.require?.('WAComms');
const conn = window.require?.('WAWebConnModel')?.Conn;
const bridgeState = bridge?.state?.();
return {
document_epoch: window.performance?.timeOrigin,
offline: navigator.onLine === false,
registered: multiDevice?.isRegistered?.() === true,
comms_initialized: comms?.isCommsInitialized?.() === true,
socket_state: String(
socket?.state ?? socket?.get?.('state') ?? 'unknown',
),
guard_active:
bridgeState?.canonicalCredentialGuardActive === true,
credential_protection_installed:
bridgeState?.canonicalCredentialProtectionInstalled ===
true,
destructive_credential_phase:
bridgeState?.canonicalDestructiveCredentialPhase ||
'unknown',
destructive_credential_violation_detected:
bridgeState?.canonicalDestructiveCredentialViolationDetected ===
true,
navigation_storage_guard_active:
bridgeState?.canonicalNavigationStorageGuardActive ===
true,
pairing_watchdog_active:
bridgeState?.canonicalPairingWatchdogActive === true,
pairing_detected:
bridgeState?.canonicalPairingDetected === true,
pairing_ref_present: Boolean(conn?.ref),
};
});
if (
offlineProof?.document_epoch !== onlineProof.document_epoch ||
offlineProof.offline !== true ||
(offlineProof.registered !== true &&
allowRegistrationRepair !== true) ||
offlineProof.guard_active !== true ||
offlineProof.credential_protection_installed !== true ||
offlineProof.destructive_credential_phase !== 'suppressing' ||
offlineProof.destructive_credential_violation_detected ===
true ||
offlineProof.navigation_storage_guard_active !== true ||
offlineProof.pairing_watchdog_active !== true ||
offlineProof.pairing_detected === true ||
offlineProof.pairing_ref_present === true
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_reload_offline_proof_invalid',
);
}
this.canonicalReloadProof = Object.freeze({
previousDocumentEpoch: before.document_epoch,
documentEpoch: offlineProof.document_epoch,
explicitReloadCount: 1,
actualVersion: compatibility.actualVersion,
moduleAbi: compatibility.moduleAbi,
registrationRepairRequired: offlineProof.registered !== true,
runtimeBootstrapRequired:
offlineProof.comms_initialized !== true ||
offlineProof.socket_state !== 'OPENING',
});
this.log(
'browser_bridge.canonical_runtime_reloaded',
{
explicit_reload_count: 1,
document_epoch_changed: true,
websocket_gate_required: true,
registered: offlineProof.registered === true,
registration_repair_required:
offlineProof.registered !== true,
socket_state: onlineProof.socket_state,
offline: true,
},
{ force: true },
);
return {
...compatibility,
...this.canonicalReloadProof,
socketState: onlineProof.socket_state,
};
} catch (error) {
await this.page.setOfflineMode(true).catch(() => {});
this.networkQuiesced = true;
this.canonicalReloadProof = undefined;
const details = error?.details;
const reloadStageCode = safeDiagnosticCode(
reloadStage,
'unknown_stage',
);
this.log(
'browser_bridge.canonical_reload_failed',
{
reload_stage: reloadStage,
error_code: safeDiagnosticCode(
error?.code || error?.message,
'wwebjs_canonical_reload_unknown_failure',
),
socket_state:
typeof details?.socket_state === 'string' &&
[
'OPENING',
'TIMEOUT',
'UNLAUNCHED',
'UNPAIRED',
'UNPAIRED_IDLE',
'PAIRING',
'CONNECTED',
'unknown',
].includes(details.socket_state)
? details.socket_state
: 'unknown',
registered: details?.registered === true,
has_noise_record: details?.has_noise_record === true,
has_noise_info: details?.has_noise_info === true,
has_me_device_pn: details?.has_me_device_pn === true,
comms_initialized: details?.comms_initialized === true,
guard_active: details?.guard_active === true,
navigation_storage_guard_active:
details?.navigation_storage_guard_active === true,
navigation_storage_suppression_count: Number.isSafeInteger(
details?.navigation_storage_suppression_count,
)
? details.navigation_storage_suppression_count
: 0,
pairing_watchdog_active:
details?.pairing_watchdog_active === true,
pairing_detected: details?.pairing_detected === true,
pairing_ref_present: details?.pairing_ref_present === true,
},
{ force: true },
);
if (error instanceof WhatsAppSessionCompatibilityError) {
if (
reloadStage === 'runtime_stability' &&
error.code === 'wwebjs_browser_runtime_stability_timeout'
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_reload_runtime_stability_timeout',
{ reload_stage: reloadStageCode },
);
}
throw error;
}
throw new WhatsAppSessionCompatibilityError(
`wwebjs_canonical_reload_${reloadStageCode}_failed`,
{ reload_stage: reloadStageCode },
);
}
}
async armCanonicalCredentialGuard({
ttlMs = 300000,
navigationScoped = true,
} = {}) {
if (!Number.isSafeInteger(ttlMs) || ttlMs < 1000 || ttlMs > 300000) {
throw new TypeError('Invalid canonical credential guard TTL.');
}
if (this.canonicalCredentialGuardScriptIdentifier) {
await this.page.removeScriptToEvaluateOnNewDocument(
this.canonicalCredentialGuardScriptIdentifier,
);
this.canonicalCredentialGuardScriptIdentifier = undefined;
}
let script;
if (navigationScoped) {
script = await this.page.evaluateOnNewDocument(
installCanonicalCredentialNavigationGuard,
ttlMs,
);
if (typeof script?.identifier !== 'string' || !script.identifier) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_credential_guard_unavailable',
);
}
this.canonicalCredentialGuardScriptIdentifier = script.identifier;
}
let armed;
try {
armed = await this.page.evaluate(
(ttl) =>
window.__WWebSessionBridge?.armCanonicalCredentialGuard?.(
ttl,
),
ttlMs,
);
} catch (error) {
if (script) {
await this.page
.removeScriptToEvaluateOnNewDocument(script.identifier)
.catch(() => {});
}
this.canonicalCredentialGuardScriptIdentifier = undefined;
throw error;
}
if (armed !== true) {
if (script) {
await this.page
.removeScriptToEvaluateOnNewDocument(script.identifier)
.catch(() => {});
}
this.canonicalCredentialGuardScriptIdentifier = undefined;
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_credential_guard_unavailable',
);
}
this.canonicalCredentialGuardArmed = true;
this.log('browser_bridge.canonical_credential_guard_armed', {
ttl_ms: ttlMs,
navigation_scoped: navigationScoped,
});
}
async renewCanonicalCredentialGuard({ ttlMs = 300000 } = {}) {
if (!Number.isSafeInteger(ttlMs) || ttlMs < 1000 || ttlMs > 300000) {
throw new TypeError('Invalid canonical credential guard TTL.');
}
if (!this.canonicalCredentialGuardScriptIdentifier) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_credential_guard_unavailable',
);
}
const renewed = await this.page.evaluate((ttl) => {
const bridge = window.__WWebSessionBridge;
const before = bridge?.state?.();
const expiresAt = Date.now() + ttl;
let navigationRenewed;
try {
navigationRenewed =
window.__WWebCanonicalNavigationStorageGuardV1?.renew?.(
expiresAt,
) === true;
window.__WWebCanonicalRehydrationGuardUntilV1 = expiresAt;
sessionStorage.setItem(
'__wwebjs_canonical_rehydration_guard_v1',
String(expiresAt),
);
} catch {
return false;
}
const after = bridge?.state?.();
return (
navigationRenewed &&
before?.canonicalCredentialGuardActive === true &&
after?.canonicalCredentialGuardActive === true &&
before?.canonicalDestructiveCredentialPhase ===
after?.canonicalDestructiveCredentialPhase &&
before?.canonicalDestructiveCredentialAcknowledged ===
after?.canonicalDestructiveCredentialAcknowledged &&
before?.canonicalDestructiveCredentialViolationDetected ===
after?.canonicalDestructiveCredentialViolationDetected &&
before?.canonicalPairingWatchdogActive ===
after?.canonicalPairingWatchdogActive &&
before?.canonicalPairingDetected ===
after?.canonicalPairingDetected
);
}, ttlMs);
if (renewed !== true) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_credential_guard_renew_invalid',
);
}
this.log('browser_bridge.canonical_credential_guard_renewed', {
ttl_ms: ttlMs,
phase_preserved: true,
violation_latch_preserved: true,
pairing_watchdog_preserved: true,
});
return true;
}
assertNoCanonicalCredentialViolation() {
if (this.canonicalCredentialViolationDetected) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_credential_violation_detected',
);
}
}
async acknowledgeCanonicalCredentialProtection() {
this.assertNoCanonicalCredentialViolation();
if (!this.networkQuiesced || !this.canonicalReloadProof) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_credential_protection_order_invalid',
);
}
const state = await this.page.evaluate((expectedDocumentEpoch) => {
const bridge = window.__WWebSessionBridge;
const acknowledged =
bridge?.acknowledgeCanonicalCredentialProtection?.() === true;
const next = bridge?.state?.();
return {
acknowledged,
document_epoch_valid:
window.performance?.timeOrigin === expectedDocumentEpoch,
offline: navigator.onLine === false,
guard_active: next?.canonicalCredentialGuardActive === true,
protection_installed:
next?.canonicalCredentialProtectionInstalled === true,
destructive_phase: next?.canonicalDestructiveCredentialPhase,
destructive_acknowledged:
next?.canonicalDestructiveCredentialAcknowledged === true,
violation_detected:
next?.canonicalDestructiveCredentialViolationDetected ===
true,
suppression_count: next?.canonicalCredentialSuppressionCount,
navigation_suppression_count:
next?.canonicalNavigationStorageSuppressionCount,
};
}, this.canonicalReloadProof.documentEpoch);
if (
state?.acknowledged !== true ||
state.document_epoch_valid !== true ||
state.offline !== true ||
state.guard_active !== true ||
state.protection_installed !== true ||
state.destructive_phase !== 'sealed' ||
state.destructive_acknowledged !== true ||
state.violation_detected === true ||
state.suppression_count !== 0 ||
state.navigation_suppression_count !== 0
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_credential_protection_ack_invalid',
);
}
this.log(
'browser_bridge.canonical_credential_protection_acknowledged',
{ offline: true, document_epoch_valid: true },
{ force: true },
);
return state;
}
async releaseCanonicalCredentialProtectionToLive() {
this.assertNoCanonicalCredentialViolation();
if (!this.networkQuiesced || !this.canonicalReloadProof) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_credential_protection_order_invalid',
);
}
const state = await this.page.evaluate((expectedDocumentEpoch) => {
const bridge = window.__WWebSessionBridge;
const released =
bridge?.releaseCanonicalCredentialProtectionToLive?.() === true;
const next = bridge?.state?.();
return {
released,
document_epoch_valid:
window.performance?.timeOrigin === expectedDocumentEpoch,
offline: navigator.onLine === false,
guard_active: next?.canonicalCredentialGuardActive === true,
protection_installed:
next?.canonicalCredentialProtectionInstalled === true,
destructive_phase: next?.canonicalDestructiveCredentialPhase,
destructive_acknowledged:
next?.canonicalDestructiveCredentialAcknowledged === true,
violation_detected:
next?.canonicalDestructiveCredentialViolationDetected ===
true,
};
}, this.canonicalReloadProof.documentEpoch);
if (
state?.released !== true ||
state.document_epoch_valid !== true ||
state.offline !== true ||
state.guard_active !== true ||
state.protection_installed !== true ||
state.destructive_phase !== 'live' ||
state.destructive_acknowledged !== true ||
state.violation_detected === true
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_credential_protection_release_invalid',
);
}
this.log(
'browser_bridge.canonical_credential_protection_live',
{ offline: true, document_epoch_valid: true },
{ force: true },
);
return state;
}
async disarmCanonicalCredentialGuard({ strict = false } = {}) {
let scriptRemoved = true;
if (this.canonicalCredentialGuardScriptIdentifier) {
const identifier = this.canonicalCredentialGuardScriptIdentifier;
this.canonicalCredentialGuardScriptIdentifier = undefined;
try {
await this.page.removeScriptToEvaluateOnNewDocument(identifier);
} catch (error) {
scriptRemoved = false;
if (strict) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_credential_guard_release_failed',
{ cause: error?.message },
);
}
}
}
let disarmed = false;
try {
disarmed = await this.page.evaluate(() => {
const bridge = window.__WWebSessionBridge;
if (!bridge?.disarmCanonicalCredentialGuard) return false;
bridge.disarmCanonicalCredentialGuard();
return (
bridge.state?.().canonicalCredentialGuardActive === false
);
});
} catch (error) {
if (strict) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_credential_guard_release_failed',
{ cause: error?.message },
);
}
}
if (strict && (disarmed !== true || !scriptRemoved)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_credential_guard_release_failed',
);
}
if (this.canonicalCredentialGuardArmed) {
this.log('browser_bridge.canonical_credential_guard_disarmed');
}
this.canonicalCredentialGuardArmed = false;
}
/**
* Make the official socket loop reread the tuple restored by the
* post-reload reseal. This repairs either an unregistered launch branch or
* a registered-but-UNLAUNCHED runtime. It never enables the network and
* never reloads.
*/
async repairCanonicalRegistrationOffline({ timeoutMs = 10000 } = {}) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 30000
) {
throw new TypeError(
'Invalid canonical registration repair timeout.',
);
}
if (!this.networkQuiesced || !this.canonicalReloadProof) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_registration_repair_order_invalid',
);
}
this.assertNoCanonicalCredentialViolation();
const result = await this.page.evaluate(rearmCanonicalSocketOffline, {
timeoutMs,
allowEstablished: true,
allowRuntimeInitialization: true,
});
if (
result?.ok !== true ||
(result.reconnect_triggered !== true &&
result.runtime_initialized !== true &&
result.runtime_launch_dispatched !== true) ||
![
'OPENING',
'TIMEOUT',
'UNLAUNCHED',
'UNPAIRED',
'UNPAIRED_IDLE',
].includes(result.previous_socket_state) ||
(result.socket_state !== 'OPENING' &&
!(
result.socket_state === 'UNLAUNCHED' &&
result.runtime_launch_dispatched === true &&
['dispatched', 'completed'].includes(
result.runtime_launch_state,
)
))
) {
throw new WhatsAppSessionCompatibilityError(
result?.code || 'wwebjs_canonical_registration_repair_invalid',
{ socket_state: result?.socket_state || 'unknown' },
);
}
const proof = await this.page.evaluate((expectedDocumentEpoch) => {
const bridgeState = window.__WWebSessionBridge?.state?.();
const conn = window.require?.('WAWebConnModel')?.Conn;
const socket = window.require?.('WAWebSocketModel')?.Socket;
const multiDevice = window.require?.('WAWebUserPrefsMultiDevice');
return {
document_epoch_valid:
window.performance?.timeOrigin === expectedDocumentEpoch,
offline: navigator.onLine === false,
registered: multiDevice?.isRegistered?.() === true,
socket_state: String(
socket?.state ?? socket?.get?.('state') ?? 'unknown',
),
guard_active:
bridgeState?.canonicalCredentialGuardActive === true,
protection_installed:
bridgeState?.canonicalCredentialProtectionInstalled ===
true,
destructive_phase:
bridgeState?.canonicalDestructiveCredentialPhase,
violation_detected:
bridgeState?.canonicalDestructiveCredentialViolationDetected ===
true,
pairing_watchdog_active:
bridgeState?.canonicalPairingWatchdogActive === true,
pairing_detected:
bridgeState?.canonicalPairingDetected === true,
pairing_ref_present: Boolean(conn?.ref),
runtime_launch_state:
window.__WWebCanonicalRegisteredLaunchStateV1,
};
}, this.canonicalReloadProof.documentEpoch);
if (
proof?.document_epoch_valid !== true ||
proof.offline !== true ||
proof.registered !== true ||
(proof.socket_state !== 'OPENING' &&
!(
proof.socket_state === 'UNLAUNCHED' &&
['dispatched', 'completed'].includes(
proof.runtime_launch_state,
)
)) ||
proof.guard_active !== true ||
proof.protection_installed !== true ||
proof.destructive_phase !== 'suppressing' ||
proof.violation_detected === true ||
proof.pairing_watchdog_active !== true ||
proof.pairing_detected === true ||
proof.pairing_ref_present === true
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_registration_repair_proof_invalid',
{ socket_state: proof?.socket_state || 'unknown' },
);
}
this.log(
'browser_bridge.canonical_registration_repaired_offline',
{
previous_socket_state: result.previous_socket_state,
socket_state: result.socket_state,
reconnect_triggered: result.reconnect_triggered === true,
runtime_initialized: result.runtime_initialized === true,
runtime_launch_dispatched:
result.runtime_launch_dispatched === true,
runtime_launch_state: result.runtime_launch_state,
offline: true,
},
{ force: true },
);
return result;
}
async deferCanonicalRegisteredLaunchOffline() {
if (!this.networkQuiesced || !this.canonicalReloadProof) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_online_launch_order_invalid',
);
}
this.assertNoCanonicalCredentialViolation();
const proof = await this.page.evaluate(
async (expectedDocumentEpoch) => {
const conn = window.require?.('WAWebConnModel')?.Conn;
const socket = window.require?.('WAWebSocketModel')?.Socket;
const multiDevice = window.require?.(
'WAWebUserPrefsMultiDevice',
);
const comms = window.require?.('WAComms');
const readCommsInitialized = () => {
try {
return comms?.isCommsInitialized?.() === true;
} catch {
return undefined;
}
};
if (
typeof comms?.isCommsInitialized !== 'function' ||
comms.isCommsInitialized.length !== 0 ||
typeof comms?.stopComms !== 'function' ||
comms.stopComms.length !== 0
) {
return { abi_valid: false };
}
const commsWasInitialized = readCommsInitialized();
if (commsWasInitialized === undefined) {
return { abi_valid: false };
}
if (commsWasInitialized === true) {
try {
const stopped = comms.stopComms();
if (stopped !== undefined) {
return { abi_valid: false };
}
} catch {
return { abi_valid: false };
}
const startedAt = Date.now();
while (
readCommsInitialized() === true &&
Date.now() - startedAt < 1000
) {
await new Promise((resolve) => setTimeout(resolve, 25));
}
}
const stoppedBridgeState =
window.__WWebSessionBridge?.state?.();
return {
abi_valid: true,
document_epoch_valid:
window.performance?.timeOrigin ===
expectedDocumentEpoch,
offline: navigator.onLine === false,
registered: multiDevice?.isRegistered?.() === true,
socket_state: String(
socket?.state ?? socket?.get?.('state') ?? 'unknown',
),
comms_was_initialized: commsWasInitialized === true,
comms_initialized: readCommsInitialized() === true,
guard_active:
stoppedBridgeState?.canonicalCredentialGuardActive ===
true,
protection_installed:
stoppedBridgeState?.canonicalCredentialProtectionInstalled ===
true,
destructive_phase:
stoppedBridgeState?.canonicalDestructiveCredentialPhase,
violation_detected:
stoppedBridgeState?.canonicalDestructiveCredentialViolationDetected ===
true,
pairing_watchdog_active:
stoppedBridgeState?.canonicalPairingWatchdogActive ===
true,
pairing_detected:
stoppedBridgeState?.canonicalPairingDetected === true,
pairing_ref_present: Boolean(conn?.ref),
runtime_launch_state:
window.__WWebCanonicalRegisteredLaunchStateV1,
};
},
this.canonicalReloadProof.documentEpoch,
);
if (
proof?.abi_valid !== true ||
proof?.document_epoch_valid !== true ||
proof.offline !== true ||
proof.registered !== true ||
!['OPENING', 'UNLAUNCHED'].includes(proof.socket_state) ||
proof.comms_initialized !== false ||
proof.guard_active !== true ||
proof.protection_installed !== true ||
proof.destructive_phase !== 'suppressing' ||
proof.violation_detected === true ||
proof.pairing_watchdog_active !== true ||
proof.pairing_detected === true ||
proof.pairing_ref_present === true ||
proof.runtime_launch_state != null
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_online_launch_proof_invalid',
{ socket_state: proof?.socket_state || 'unknown' },
);
}
this.canonicalOnlineRegisteredLaunchRequired = true;
this.log(
'browser_bridge.canonical_registered_launch_deferred_online',
{
socket_state: proof.socket_state,
provisional_runtime_stopped:
proof.comms_was_initialized === true,
comms_initialized: false,
offline: true,
},
{ force: true },
);
return proof;
}
/**
* Verify the official login bootstrap created by the canonical reload.
* Reconnect alone cannot switch an already launched registration branch
* to login, so this method deliberately performs no reconnect mutation.
*/
async rearmCanonicalImportedSocket({
timeoutMs = 10000,
allowEstablished = false,
} = {}) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 30000
) {
throw new TypeError('Invalid canonical reconnect timeout.');
}
if (!this.networkQuiesced || !this.canonicalReloadProof) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_reconnect_order_invalid',
);
}
this.assertNoCanonicalCredentialViolation();
let result;
try {
result = await this.page.evaluate(
verifyCanonicalReloadedSocketOffline,
{
documentEpoch: this.canonicalReloadProof.documentEpoch,
allowEstablished: allowEstablished === true,
allowDeferredRuntimeLaunch:
this.canonicalOnlineRegisteredLaunchRequired === true,
},
);
} catch (error) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_reloaded_runtime_invocation_failed',
{ cause: error?.message },
);
}
const allowedCodes = new Set([
'wwebjs_canonical_reload_proof_invalid',
'wwebjs_canonical_reloaded_runtime_unavailable',
'wwebjs_canonical_reloaded_socket_state_invalid',
]);
if (result?.ok !== true) {
const code = allowedCodes.has(result?.code)
? result.code
: 'wwebjs_canonical_reloaded_runtime_invalid';
throw new WhatsAppSessionCompatibilityError(code, {
socket_state:
typeof result?.socket_state === 'string'
? result.socket_state
: 'unknown',
});
}
const allowedPreparedStates =
allowEstablished === true
? ['CONNECTED', 'OPENING', 'TIMEOUT', 'UNLAUNCHED']
: ['OPENING', 'UNLAUNCHED'];
if (
result.reconnect_triggered !== false ||
result.bootstrapped_by_reload !== true ||
result.reload_proof_valid !== true ||
result.document_epoch !== this.canonicalReloadProof.documentEpoch ||
!allowedPreparedStates.includes(result.socket_state) ||
(result.socket_state === 'UNLAUNCHED' &&
result.bootstrapped_by_registered_launch !== true &&
result.deferred_runtime_launch !== true)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_reload_proof_invalid',
);
}
this.canonicalReconnectPrepared = true;
this.log(
'browser_bridge.canonical_reconnect_prepared',
{
previous_socket_state: result.previous_socket_state,
socket_state: result.socket_state,
reconnect_triggered: false,
bootstrapped_by_reload: true,
bootstrapped_by_registered_launch:
result.bootstrapped_by_registered_launch === true,
deferred_runtime_launch:
result.deferred_runtime_launch === true,
runtime_launch_state: result.runtime_launch_state,
reload_proof_valid: true,
explicit_reload_count:
this.canonicalReloadProof.explicitReloadCount,
offline: true,
},
{ force: true },
);
return result;
}
async prepareCanonicalCandidateOffline({ allowEstablished = false } = {}) {
if (!this.networkQuiesced || !this.canonicalReloadProof) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_offline_candidate_order_invalid',
);
}
this.assertNoCanonicalCredentialViolation();
const prepared = await this.page.evaluate(
(options) => {
const socket = window.require?.('WAWebSocketModel')?.Socket;
const multiDevice = window.require?.(
'WAWebUserPrefsMultiDevice',
);
const conn = window.require?.('WAWebConnModel')?.Conn;
const bridge = window.__WWebSessionBridge;
const socketState = String(
socket?.state ?? socket?.get?.('state') ?? 'unknown',
);
const runtimeLaunchState =
window.__WWebCanonicalRegisteredLaunchStateV1;
const registeredLaunchPending = [
'dispatched',
'completed',
].includes(runtimeLaunchState);
const allowedSocketStates =
options.allowEstablished === true
? ['OPENING', 'TIMEOUT', 'UNLAUNCHED']
: registeredLaunchPending
? ['OPENING', 'UNLAUNCHED']
: ['OPENING'];
const currentBridgeState = bridge?.state?.();
const watchdogReady =
currentBridgeState?.canonicalPairingWatchdogActive === true
? true
: bridge?.armCanonicalPairingWatchdog?.() === true;
const bridgeState = bridge?.state?.();
if (
navigator.onLine !== false ||
window.performance?.timeOrigin !== options.documentEpoch ||
multiDevice?.isRegistered?.() !== true ||
runtimeLaunchState === 'failed' ||
Boolean(conn?.ref) ||
!allowedSocketStates.includes(socketState) ||
watchdogReady !== true ||
bridgeState?.canonicalCredentialGuardActive !== true ||
bridgeState?.canonicalCredentialProtectionInstalled !==
true ||
bridgeState?.canonicalDestructiveCredentialPhase !==
'sealed' ||
bridgeState?.canonicalDestructiveCredentialAcknowledged !==
true ||
bridgeState?.canonicalDestructiveCredentialViolationDetected ===
true ||
bridgeState?.canonicalPairingWatchdogActive !== true ||
bridgeState?.canonicalPairingDetected === true
) {
return { ok: false, socket_state: socketState };
}
return {
ok: true,
socket_state: socketState,
runtime_launch_state: registeredLaunchPending
? runtimeLaunchState
: undefined,
document_epoch: window.performance?.timeOrigin,
};
},
{
allowEstablished: allowEstablished === true,
documentEpoch: this.canonicalReloadProof.documentEpoch,
},
);
if (
prepared?.ok !== true ||
prepared.document_epoch !== this.canonicalReloadProof.documentEpoch
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_offline_candidate_invalid',
{ socket_state: prepared?.socket_state || 'unknown' },
);
}
this.log(
'browser_bridge.canonical_offline_candidate_prepared',
{
socket_state: prepared.socket_state,
runtime_launch_state: prepared.runtime_launch_state,
reload_proof_valid: true,
offline: true,
},
{ force: true },
);
return prepared;
}
async waitForCanonicalConnected({
timeoutMs = 120000,
requireSynced = true,
} = {}) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 180000
) {
throw new TypeError('Invalid canonical connected timeout.');
}
if (typeof requireSynced !== 'boolean') {
throw new TypeError('Invalid canonical synchronization gate.');
}
if (!this.canonicalReloadProof) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_reload_proof_invalid',
);
}
const startedAt = Date.now();
let lastSignature;
let lastLoggedAt = 0;
let nativeStallStartedAt;
let health;
// Cross-provider app-state materialization can legitimately run close
// to the five-minute credential-guard lease. Renew only the already
// armed deadlines before this terminal transport proof; phase,
// acknowledgement, violation and pairing latches remain unchanged and
// are verified by renewCanonicalCredentialGuard.
if (this.canonicalCredentialGuardScriptIdentifier) {
await this.renewCanonicalCredentialGuard({
ttlMs: Math.min(300000, Math.max(60000, timeoutMs + 30000)),
});
}
do {
this.assertNoCanonicalCredentialViolation();
health = await this.page.evaluate(
inspectCanonicalTransportHealth,
this.canonicalReloadProof.documentEpoch,
);
if (
health?.document_epoch_valid !== true ||
health.guard_active !== true ||
health.credential_protection_installed !== true ||
health.destructive_credential_phase !== 'live' ||
health.destructive_credential_acknowledged !== true ||
health.destructive_credential_violation_detected === true ||
health.runtime_bootstrap_failed === true ||
health.pairing_watchdog_active !== true ||
health.pairing_detected === true ||
health.pairing_ref_active === true
) {
this.log(
'browser_bridge.canonical_connected_safety_rejected',
{
document_epoch_valid:
health?.document_epoch_valid === true,
guard_active: health?.guard_active === true,
credential_protection_installed:
health?.credential_protection_installed === true,
destructive_phase_live:
health?.destructive_credential_phase === 'live',
destructive_credential_acknowledged:
health?.destructive_credential_acknowledged ===
true,
destructive_credential_violation_detected:
health?.destructive_credential_violation_detected ===
true,
runtime_bootstrap_failed:
health?.runtime_bootstrap_failed === true,
pairing_watchdog_active:
health?.pairing_watchdog_active === true,
pairing_detected: health?.pairing_detected === true,
pairing_ref_active: health?.pairing_ref_active === true,
socket_state: health?.socket_state || 'unknown',
},
{ force: true },
);
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_connected_safety_invalid',
{
socket_state: health?.socket_state || 'unknown',
},
);
}
if (
health.socket_state === 'CONNECTED' &&
health.socket_connected === true &&
(!requireSynced || health.has_synced === true)
) {
this.log(
requireSynced
? 'browser_bridge.canonical_promoted_socket_connected'
: 'browser_bridge.canonical_authenticated_transport_connected',
{ ...health, synchronization_required: requireSynced },
{ force: true },
);
return health;
}
const now = Date.now();
if (isCanonicalTransportNativeStall(health)) {
nativeStallStartedAt ??= now;
const nativeStalledForMs = now - nativeStallStartedAt;
if (nativeStalledForMs >= CANONICAL_TRANSPORT_NATIVE_STALL_MS) {
const details = {
native_stall_kind:
health.socket_stream === 'CONNECTED'
? 'connected_stream_without_comms'
: 'disconnected_stream_without_comms',
socket_state: health.socket_state,
socket_stream: health.socket_stream,
backend_socket_state: health.backend_socket_state,
has_synced: health.has_synced,
comms_initialized: health.comms_initialized,
socket_connected: health.socket_connected,
inbound_rx_bucket: health.inbound_rx_bucket,
reconnect_listener_count:
health.reconnect_listener_count,
native_stalled_for_ms: nativeStalledForMs,
native_stall_threshold_ms:
CANONICAL_TRANSPORT_NATIVE_STALL_MS,
};
if (this.canonicalAppStateRealmRecoveryCount >= 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_connected_native_stall_recovery_exhausted',
details,
);
}
const remainingMs = startedAt + timeoutMs - now;
if (remainingMs < 1000) break;
this.log(
'browser_bridge.canonical_transport_native_stall_recovery_started',
details,
{ force: true },
);
await this.recoverCanonicalConnectedTransportAfterNativeStall(
{ timeoutMs: Math.min(60000, remainingMs) },
);
nativeStallStartedAt = undefined;
lastSignature = undefined;
lastLoggedAt = 0;
continue;
}
} else {
nativeStallStartedAt = undefined;
}
const signature = JSON.stringify(health);
if (signature !== lastSignature || now - lastLoggedAt >= 30000) {
lastSignature = signature;
lastLoggedAt = now;
this.log(
'browser_bridge.canonical_transport_health',
{
...health,
duration_ms: now - startedAt,
},
{ force: true },
);
}
if (now - startedAt >= timeoutMs) break;
await new Promise((resolve) =>
setTimeout(
resolve,
Math.min(1000, timeoutMs - (now - startedAt)),
),
);
} while (Date.now() - startedAt < timeoutMs);
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_connected_timeout',
{
...health,
duration_ms: Date.now() - startedAt,
},
);
}
async releaseCanonicalImportedNetwork() {
if (!this.networkQuiesced || !this.canonicalReconnectPrepared) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_reconnect_order_invalid',
);
}
this.assertNoCanonicalCredentialViolation();
await this.releaseCanonicalCredentialProtectionToLive();
const ready = await this.page.evaluate(
(expectedDocumentEpoch, deferredRuntimeLaunchRequired) => {
const socket = window.require?.('WAWebSocketModel')?.Socket;
const multiDevice = window.require?.(
'WAWebUserPrefsMultiDevice',
);
const state = String(
socket?.state ?? socket?.get?.('state') ?? 'unknown',
);
const runtimeLaunchState =
window.__WWebCanonicalRegisteredLaunchStateV1;
return {
registered: multiDevice?.isRegistered?.() === true,
socket_state: state,
guard_active:
window.__WWebSessionBridge?.state?.()
?.canonicalCredentialGuardActive === true,
credential_protection_installed:
window.__WWebSessionBridge?.state?.()
?.canonicalCredentialProtectionInstalled === true,
destructive_credential_phase:
window.__WWebSessionBridge?.state?.()
?.canonicalDestructiveCredentialPhase || 'unknown',
destructive_credential_acknowledged:
window.__WWebSessionBridge?.state?.()
?.canonicalDestructiveCredentialAcknowledged ===
true,
destructive_credential_violation_detected:
window.__WWebSessionBridge?.state?.()
?.canonicalDestructiveCredentialViolationDetected ===
true,
pairing_watchdog_active:
window.__WWebSessionBridge?.state?.()
?.canonicalPairingWatchdogActive === true,
pairing_detected:
window.__WWebSessionBridge?.state?.()
?.canonicalPairingDetected === true,
runtime_launch_state: [
'dispatched',
'completed',
'failed',
].includes(runtimeLaunchState)
? runtimeLaunchState
: 'none',
deferred_runtime_launch_required:
deferredRuntimeLaunchRequired === true,
offline: navigator.onLine === false,
reload_proof_valid:
window.performance?.timeOrigin ===
expectedDocumentEpoch,
};
},
this.canonicalReloadProof?.documentEpoch,
this.canonicalOnlineRegisteredLaunchRequired === true,
);
if (
ready?.registered !== true ||
ready.guard_active !== true ||
ready.credential_protection_installed !== true ||
ready.destructive_credential_phase !== 'live' ||
ready.destructive_credential_acknowledged !== true ||
ready.destructive_credential_violation_detected === true ||
ready.pairing_watchdog_active !== true ||
ready.pairing_detected !== false ||
ready.runtime_launch_state === 'failed' ||
(ready.deferred_runtime_launch_required === true &&
ready.runtime_launch_state !== 'none') ||
ready.offline !== true ||
ready.reload_proof_valid !== true ||
(!['OPENING', 'CONNECTED'].includes(ready.socket_state) &&
!(
ready.socket_state === 'UNLAUNCHED' &&
(ready.deferred_runtime_launch_required === true ||
['dispatched', 'completed'].includes(
ready.runtime_launch_state,
))
))
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_reconnect_release_invalid',
{
socket_state:
typeof ready?.socket_state === 'string'
? ready.socket_state
: 'unknown',
},
);
}
await this.page.setOfflineMode(false);
let onlineReconnect;
let onlineLaunch;
try {
if (ready.deferred_runtime_launch_required === true) {
onlineLaunch = await this.page.evaluate(
launchCanonicalRegisteredSocketOnline,
{
documentEpoch: this.canonicalReloadProof.documentEpoch,
timeoutMs: 15000,
},
);
if (
onlineLaunch?.ok !== true ||
onlineLaunch.launch_triggered !== true ||
!['OPENING', 'CONNECTED'].includes(
onlineLaunch.socket_state,
)
) {
throw new WhatsAppSessionCompatibilityError(
onlineLaunch?.code ||
'wwebjs_canonical_online_launch_invalid',
{
socket_state:
onlineLaunch?.socket_state || 'unknown',
},
);
}
} else if (
ready.socket_state === 'UNLAUNCHED' &&
ready.runtime_launch_state === 'completed'
) {
onlineReconnect = await this.page.evaluate(
resumeCanonicalRegisteredSocketOnline,
{
documentEpoch: this.canonicalReloadProof.documentEpoch,
timeoutMs: 15000,
},
);
if (
onlineReconnect?.ok !== true ||
onlineReconnect.reconnect_triggered !== true ||
!['OPENING', 'CONNECTED'].includes(
onlineReconnect.socket_state,
)
) {
throw new WhatsAppSessionCompatibilityError(
onlineReconnect?.code ||
'wwebjs_canonical_online_reconnect_invalid',
{
socket_state:
onlineReconnect?.socket_state || 'unknown',
},
);
}
}
} catch (error) {
await this.page.setOfflineMode(true).catch(() => {});
throw error;
}
this.assertNoCanonicalCredentialViolation();
this.networkQuiesced = false;
this.canonicalReconnectPrepared = false;
this.canonicalOnlineRegisteredLaunchRequired = false;
this.log(
'browser_bridge.canonical_reconnect_resumed',
{
socket_state: ready.socket_state,
runtime_launch_state: ready.runtime_launch_state,
online_reconnect_triggered:
onlineReconnect?.reconnect_triggered === true,
online_reconnect_socket_state: onlineReconnect?.socket_state,
online_launch_triggered:
onlineLaunch?.launch_triggered === true,
online_launch_socket_state: onlineLaunch?.socket_state,
offline: false,
},
{ force: true },
);
return ready;
}
async disarmCanonicalPairingWatchdog() {
const disarmed = await this.page.evaluate(() => {
const bridge = window.__WWebSessionBridge;
if (!bridge?.disarmCanonicalPairingWatchdog) return false;
bridge.disarmCanonicalPairingWatchdog();
return bridge.state?.().canonicalPairingWatchdogActive === false;
});
if (disarmed !== true) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_pairing_watchdog_release_failed',
);
}
this.log('browser_bridge.canonical_pairing_watchdog_disarmed');
}
async freezeConnectedProfileSnapshot({ settleMs = 500 } = {}) {
if (
!Number.isSafeInteger(settleMs) ||
settleMs < 50 ||
settleMs > 5000
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_connected_profile_freeze_settle_invalid',
);
}
if (
this.networkQuiesced ||
this.canonicalProfileSnapshotFreeze ||
typeof this.page?.createCDPSession !== 'function'
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_connected_profile_freeze_order_invalid',
);
}
const proof = await this.page.evaluate(inspectConnectedProfileSnapshot);
if (
!Number.isFinite(proof?.document_epoch) ||
proof.online !== true ||
proof.registered !== true ||
proof.socket_state !== 'CONNECTED' ||
proof.comms_initialized !== true ||
proof.socket_connected !== true ||
proof.pairing_reference_active !== false
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_connected_profile_freeze_proof_invalid',
);
}
const session = await this.page.createCDPSession();
const freeze = {
session,
mode: 'connected',
documentEpoch: proof.document_epoch,
frozen: false,
};
this.canonicalProfileSnapshotFreeze = freeze;
try {
// This path is deliberately narrower than the canonical handoff
// freeze below. Initial pairing and legacy-volume bootstrap have
// already passed app-state and identity gates, but their live
// Chromium LevelDB keeps rotating during the first profile upload.
// Freeze only the renderer lifecycle: the network stays online and
// no credential guard or imported-socket state is modified.
await session.send('Page.setWebLifecycleState', {
state: 'frozen',
});
freeze.frozen = true;
await new Promise((resolve) => setTimeout(resolve, settleMs));
} catch (error) {
if (freeze.frozen) {
await session
.send('Page.setWebLifecycleState', { state: 'active' })
.catch(() => {});
}
await session.detach?.().catch(() => {});
this.canonicalProfileSnapshotFreeze = undefined;
throw new WhatsAppSessionCompatibilityError(
'wwebjs_connected_profile_freeze_failed',
{ cause: safeDiagnosticCode(error?.code, 'cdp_failure') },
);
}
this.log(
'browser_bridge.connected_profile_frozen',
{ online: true, settle_ms: settleMs },
{ force: true },
);
return {
frozen: true,
online: true,
document_epoch: proof.document_epoch,
};
}
async resumeConnectedProfileSnapshot({ timeoutMs = 15000 } = {}) {
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1000 ||
timeoutMs > 60000
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_connected_profile_resume_timeout_invalid',
);
}
const freeze = this.canonicalProfileSnapshotFreeze;
if (
freeze?.mode !== 'connected' ||
!freeze.session ||
freeze.frozen !== true
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_connected_profile_resume_order_invalid',
);
}
let resumeError;
try {
await freeze.session.send('Page.setWebLifecycleState', {
state: 'active',
});
} catch (error) {
resumeError = error;
} finally {
if (typeof freeze.session.detach === 'function') {
await freeze.session.detach().catch((error) => {
resumeError ||= error;
});
}
this.canonicalProfileSnapshotFreeze = undefined;
}
if (resumeError) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_connected_profile_resume_failed',
{
cause: safeDiagnosticCode(resumeError?.code, 'cdp_failure'),
},
);
}
let proof;
const deadlineAt = Date.now() + timeoutMs;
do {
try {
proof = await this.page.evaluate(
inspectConnectedProfileSnapshot,
freeze.documentEpoch,
);
} catch (error) {
if (!isTransientNavigationError(error)) throw error;
const diagnostic = {
document_epoch_valid: false,
online: false,
registered: false,
socket_state: 'unknown',
comms_initialized: false,
socket_connected: false,
pairing_reference_active: false,
network_quiesced: this.networkQuiesced === true,
navigation_superseded: true,
navigation_observed_during_evaluation: true,
};
this.log(
'browser_bridge.connected_profile_resume_proof_rejected',
diagnostic,
{ force: true },
);
throw new WhatsAppSessionCompatibilityError(
'wwebjs_connected_profile_navigation_superseded',
diagnostic,
{ cause: error },
);
}
if (
proof?.document_epoch_valid === true &&
proof.online === true &&
proof.registered === true &&
proof.socket_state === 'CONNECTED' &&
proof.comms_initialized === true &&
proof.socket_connected === true &&
proof.pairing_reference_active === false &&
!this.networkQuiesced
) {
break;
}
if (proof?.document_epoch_valid !== true) break;
await new Promise((resolve) => setTimeout(resolve, 50));
} while (Date.now() < deadlineAt);
if (
proof?.document_epoch_valid !== true ||
proof.online !== true ||
proof.registered !== true ||
proof.socket_state !== 'CONNECTED' ||
proof.comms_initialized !== true ||
proof.socket_connected !== true ||
proof.pairing_reference_active !== false ||
this.networkQuiesced
) {
const navigationSuperseded = proof?.document_epoch_valid === false;
const diagnostic = {
document_epoch_valid: proof?.document_epoch_valid === true,
online: proof?.online === true,
registered: proof?.registered === true,
socket_state: proof?.socket_state || 'unknown',
comms_initialized: proof?.comms_initialized === true,
socket_connected: proof?.socket_connected === true,
pairing_reference_active:
proof?.pairing_reference_active === true,
network_quiesced: this.networkQuiesced === true,
navigation_superseded: navigationSuperseded,
};
this.log(
'browser_bridge.connected_profile_resume_proof_rejected',
diagnostic,
{ force: true },
);
throw new WhatsAppSessionCompatibilityError(
navigationSuperseded
? 'wwebjs_connected_profile_navigation_superseded'
: 'wwebjs_connected_profile_resume_proof_invalid',
diagnostic,
);
}
this.log(
'browser_bridge.connected_profile_resumed',
{ online: true, document_epoch_valid: true },
{ force: true },
);
return { resumed: true, online: true };
}
async freezeCanonicalProfileSnapshot({ settleMs = 500 } = {}) {
if (
!Number.isSafeInteger(settleMs) ||
settleMs < 50 ||
settleMs > 5000
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_profile_freeze_settle_invalid',
);
}
if (
!this.networkQuiesced ||
this.canonicalProfileSnapshotFreeze ||
typeof this.page?.createCDPSession !== 'function'
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_profile_freeze_order_invalid',
);
}
const proof = await this.page.evaluate(() => {
const state = window.__WWebSessionBridge?.state?.();
return {
document_epoch: window.performance?.timeOrigin,
offline: navigator.onLine === false,
credential_guard_active:
state?.canonicalCredentialGuardActive === true,
credential_protection_installed:
state?.canonicalCredentialProtectionInstalled === true,
destructive_credential_violation_detected:
state?.canonicalDestructiveCredentialViolationDetected ===
true,
pairing_watchdog_active:
state?.canonicalPairingWatchdogActive === true,
pairing_detected: state?.canonicalPairingDetected === true,
};
});
if (
!Number.isFinite(proof?.document_epoch) ||
proof.offline !== true ||
proof.credential_guard_active !== true ||
proof.credential_protection_installed !== true ||
proof.destructive_credential_violation_detected !== false ||
proof.pairing_watchdog_active !== true ||
proof.pairing_detected !== false ||
(this.canonicalReloadProof?.documentEpoch !== undefined &&
proof.document_epoch !==
this.canonicalReloadProof.documentEpoch)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_profile_freeze_proof_invalid',
);
}
const session = await this.page.createCDPSession();
const freeze = {
session,
documentEpoch: proof.document_epoch,
frozen: false,
};
this.canonicalProfileSnapshotFreeze = freeze;
try {
// CDP network emulation is scoped to a session. Pin this newly
// created lifecycle session offline as well so reactivation never
// creates a one-tick network window before Puppeteer's primary
// session reasserts its existing offline override.
await session.send('Network.emulateNetworkConditions', {
offline: true,
latency: 0,
uploadThroughput: 0,
downloadThroughput: 0,
});
await session.send('Page.setWebLifecycleState', {
state: 'frozen',
});
freeze.frozen = true;
await new Promise((resolve) => setTimeout(resolve, settleMs));
} catch (error) {
if (freeze.frozen) {
await session
.send('Page.setWebLifecycleState', { state: 'active' })
.catch(() => {});
}
await session.detach?.().catch(() => {});
this.canonicalProfileSnapshotFreeze = undefined;
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_profile_freeze_failed',
{ cause: safeDiagnosticCode(error?.code, 'cdp_failure') },
);
}
this.log(
'browser_bridge.canonical_profile_frozen',
{ offline: true, settle_ms: settleMs },
{ force: true },
);
return {
frozen: true,
document_epoch: proof.document_epoch,
};
}
async resumeCanonicalProfileSnapshot() {
const freeze = this.canonicalProfileSnapshotFreeze;
if (!freeze?.session || freeze.frozen !== true) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_profile_resume_order_invalid',
);
}
let resumeError;
let proof;
const inspectResumeProof = () =>
this.page.evaluate((documentEpoch) => {
const state = window.__WWebSessionBridge?.state?.();
return {
document_epoch_valid:
window.performance?.timeOrigin === documentEpoch,
offline: navigator.onLine === false,
credential_guard_active:
state?.canonicalCredentialGuardActive === true,
credential_protection_installed:
state?.canonicalCredentialProtectionInstalled === true,
destructive_credential_violation_detected:
state?.canonicalDestructiveCredentialViolationDetected ===
true,
pairing_watchdog_active:
state?.canonicalPairingWatchdogActive === true,
pairing_detected: state?.canonicalPairingDetected === true,
};
}, freeze.documentEpoch);
try {
await freeze.session.send('Page.setWebLifecycleState', {
state: 'active',
});
// Chromium recomputes navigator.onLine when moving from frozen to
// active. Reassert Puppeteer's target-wide override while the CDP
// lifecycle session remains independently offline.
await this.page.setOfflineMode(true);
} catch (error) {
resumeError = error;
} finally {
if (typeof freeze.session.detach === 'function') {
await freeze.session.detach().catch((error) => {
resumeError ||= error;
});
}
this.canonicalProfileSnapshotFreeze = undefined;
}
// Detaching a CDP session can make Chromium recompute its network
// state even though Puppeteer's primary session was already offline.
// Reassert and prove the final post-detach state; callers may reconnect
// immediately after this method returns.
if (!resumeError) {
try {
await this.page.setOfflineMode(true);
const deadline = Date.now() + 1000;
do {
proof = await inspectResumeProof();
if (
proof?.document_epoch_valid === true &&
proof.offline === true &&
proof.credential_guard_active === true &&
proof.credential_protection_installed === true &&
proof.destructive_credential_violation_detected ===
false &&
proof.pairing_watchdog_active === true &&
proof.pairing_detected === false &&
this.networkQuiesced
) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 50));
} while (Date.now() < deadline);
} catch (error) {
resumeError = error;
}
}
if (resumeError) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_profile_resume_failed',
{
cause: safeDiagnosticCode(resumeError?.code, 'cdp_failure'),
},
);
}
if (
proof?.document_epoch_valid !== true ||
proof.offline !== true ||
proof.credential_guard_active !== true ||
proof.credential_protection_installed !== true ||
proof.destructive_credential_violation_detected !== false ||
proof.pairing_watchdog_active !== true ||
proof.pairing_detected !== false ||
!this.networkQuiesced
) {
const diagnostic = {
document_epoch_valid: proof?.document_epoch_valid === true,
offline: proof?.offline === true,
credential_guard_active:
proof?.credential_guard_active === true,
credential_protection_installed:
proof?.credential_protection_installed === true,
destructive_credential_violation_detected:
proof?.destructive_credential_violation_detected === true,
pairing_watchdog_active:
proof?.pairing_watchdog_active === true,
pairing_detected: proof?.pairing_detected === true,
network_quiesced: this.networkQuiesced === true,
};
this.log(
'browser_bridge.canonical_profile_resume_proof_rejected',
diagnostic,
{ force: true },
);
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_profile_resume_proof_invalid',
diagnostic,
);
}
this.log(
'browser_bridge.canonical_profile_resumed',
{ offline: true, document_epoch_valid: true },
{ force: true },
);
return { resumed: true, offline: true };
}
async quiesceNetwork({ timeoutMs = 15000 } = {}) {
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_network_quiescence_timeout_invalid',
);
}
const deadline = Date.now() + timeoutMs;
let proof;
let stage = 'set_offline';
let transientNavigationRetries = 0;
let offlineCommandApplied = false;
let officialSocketCloseAttempted = false;
let officialSocketCloseInvoked = false;
const transportIsQuiesced = (value) =>
value?.socket_state !== 'CONNECTED' ||
(value?.socket_stream === 'DISCONNECTED' &&
value?.comms_socket_connected === false);
try {
do {
stage = 'set_offline';
await this.page.setOfflineMode(true);
offlineCommandApplied = true;
stage = 'evaluate_proof';
try {
proof = await this.page.evaluate((allowSocketClose) => {
try {
const resolveModule = (name) => {
const value = window.require?.(name);
return value?.default || value;
};
const socket =
resolveModule('WAWebSocketModel')?.Socket;
const comms = resolveModule('WAComms');
const readSocketState = () =>
String(
socket?.state ??
socket?.get?.('state') ??
'unknown',
);
const readSocketStream = () =>
String(
socket?.stream ??
socket?.get?.('stream') ??
'unknown',
);
const readCommsSocketConnected = () => {
try {
return (
comms?.isSocketConnected?.() === true
);
} catch {
return undefined;
}
};
let socketState = readSocketState();
let socketStream = readSocketStream();
let socketCloseInvoked = false;
let socketCloseAttempted = false;
let socketCloseError;
if (
allowSocketClose === true &&
navigator.onLine === false &&
socketState === 'CONNECTED'
) {
if (
typeof comms?.closeSocketAndResume !==
'function' ||
comms.closeSocketAndResume.length !== 0
) {
socketCloseError =
'wwebjs_network_quiescence_socket_close_abi_incompatible';
} else {
socketCloseAttempted = true;
try {
const returned =
comms.closeSocketAndResume();
if (returned !== undefined) {
socketCloseError =
'wwebjs_network_quiescence_socket_close_return_invalid';
} else {
socketCloseInvoked = true;
}
} catch {
socketCloseError =
'wwebjs_network_quiescence_socket_close_failed';
}
socketState = readSocketState();
socketStream = readSocketStream();
}
}
return {
offline: navigator.onLine === false,
socket_state: socketState,
socket_stream: socketStream,
comms_socket_connected:
readCommsSocketConnected(),
socket_close_attempted: socketCloseAttempted,
socket_close_invoked: socketCloseInvoked,
socket_close_error: socketCloseError,
};
} catch {
return {
offline: false,
socket_state: 'unknown',
};
}
}, !officialSocketCloseAttempted);
if (proof?.socket_close_attempted === true) {
officialSocketCloseAttempted = true;
}
if (proof?.socket_close_error) {
// The verified close ABI can throw after WhatsApp has
// already moved the socket stream to DISCONNECTED.
// Keep the CDP network fence active and require an
// independent transport proof below. Contract/ABI
// errors still abort immediately, and a transport
// that remains live still times out fail-closed.
if (
proof.socket_close_error !==
'wwebjs_network_quiescence_socket_close_failed' ||
proof.offline !== true
) {
throw new WhatsAppSessionCompatibilityError(
proof.socket_close_error,
{
offline: proof.offline === true,
socket_state:
proof.socket_state || 'unknown',
official_socket_close_attempted:
officialSocketCloseAttempted,
official_socket_close_invoked:
officialSocketCloseInvoked,
},
);
}
this.log(
'browser_bridge.network_quiescence_socket_close_settling',
{
offline: true,
socket_state: proof.socket_state || 'unknown',
socket_stream: proof.socket_stream || 'unknown',
comms_socket_connected:
proof.comms_socket_connected === true,
},
{ force: true },
);
}
if (proof?.socket_close_invoked === true) {
officialSocketCloseInvoked = true;
this.log(
'browser_bridge.network_quiescence_socket_close_requested',
{
offline: proof.offline === true,
socket_state: proof.socket_state || 'unknown',
},
{ force: true },
);
}
} catch (error) {
// Switching a live WhatsApp document offline can race a
// main-frame/service-worker navigation. Puppeteer then
// rejects evaluate() while the CDP network fence is
// already active. Keep the page offline and retry only
// that bounded, well-known context transition; every
// other protocol error remains fail-closed.
if (!isTransientNavigationError(error)) throw error;
transientNavigationRetries += 1;
proof = undefined;
if (Date.now() >= deadline) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_network_quiescence_timeout',
{
offline: false,
socket_state: 'unknown',
transient_navigation_retries:
transientNavigationRetries,
official_socket_close_invoked:
officialSocketCloseInvoked,
},
);
}
}
if (proof?.offline === true && transportIsQuiesced(proof)) {
break;
}
// Lifecycle/session detach and extension transitions can
// recompute navigator.onLine after a prior CDP command. Keep
// the bounded barrier fail-closed by reasserting the target-
// wide override until both page and socket proofs agree.
await new Promise((resolve) => setTimeout(resolve, 250));
} while (Date.now() < deadline);
if (proof?.offline !== true || !transportIsQuiesced(proof)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_network_quiescence_timeout',
{
offline: proof?.offline === true,
socket_state: proof?.socket_state || 'unknown',
socket_stream: proof?.socket_stream || 'unknown',
comms_socket_connected:
proof?.comms_socket_connected === true,
transient_navigation_retries:
transientNavigationRetries,
official_socket_close_attempted:
officialSocketCloseAttempted,
official_socket_close_invoked:
officialSocketCloseInvoked,
},
);
}
stage = 'flush';
await this.flush();
this.networkQuiesced = true;
this.log(
'browser_bridge.network_quiesced',
{
offline: true,
transient_navigation_retries: transientNavigationRetries,
official_socket_close_attempted:
officialSocketCloseAttempted,
official_socket_close_invoked: officialSocketCloseInvoked,
},
{
force:
transientNavigationRetries > 0 ||
officialSocketCloseInvoked,
},
);
} catch (error) {
// Failure to prove a fence must never reopen the writer. Reassert
// the target-wide CDP override best-effort and let the caller
// terminate Chromium while surfacing the original failure.
await this.page.setOfflineMode(true).catch(() => {});
const details = error?.details;
const contextUnavailableAfterOffline = Boolean(
stage === 'evaluate_proof' &&
offlineCommandApplied &&
transientNavigationRetries > 0 &&
proof === undefined &&
error?.code === 'wwebjs_network_quiescence_timeout',
);
this.log(
'browser_bridge.network_quiescence_failed',
{
offline: details?.offline === true,
socket_state:
typeof details?.socket_state === 'string'
? details.socket_state
: 'unknown',
error_code: safeDiagnosticCode(
error?.code,
'wwebjs_network_quiescence_failed',
),
error_stage: safeDiagnosticCode(
stage,
'network_quiescence',
),
transient_navigation_retries: transientNavigationRetries,
offline_command_applied: offlineCommandApplied,
official_socket_close_attempted:
officialSocketCloseAttempted,
official_socket_close_invoked: officialSocketCloseInvoked,
context_unavailable_after_offline:
contextUnavailableAfterOffline,
},
{ force: true },
);
throw new WhatsAppSessionCompatibilityError(
'wwebjs_network_quiescence_failed',
{
cause: safeDiagnosticCode(error?.code, 'runtime_failure'),
stage: safeDiagnosticCode(stage, 'network_quiescence'),
offline_command_applied: offlineCommandApplied,
official_socket_close_attempted:
officialSocketCloseAttempted,
official_socket_close_invoked: officialSocketCloseInvoked,
transient_navigation_retries: transientNavigationRetries,
context_unavailable_after_offline:
contextUnavailableAfterOffline,
},
);
}
}
async resumeNetwork() {
if (!this.networkQuiesced) return;
await this.page.setOfflineMode(false);
this.networkQuiesced = false;
bridgeQuiescedPages.delete(this.page);
this.log('browser_bridge.network_resumed');
}
async close() {
clearInterval(this.interval);
// Cut network access before awaiting any queued flush. Lease-loss and
// initialization-failure teardown must not leave Chromium online when
// a journal callback is slow or wedged.
if (typeof this.page.setOfflineMode === 'function') {
await this.page.setOfflineMode(true).catch(() => {});
}
this.networkQuiesced = true;
bridgeQuiescedPages.add(this.page);
// A full app-state sync belongs to this page lifecycle. Tear down its
// watchdog and request cooperative cancellation before the page can be
// reused, while the generation fence prevents a late completion from
// publishing state after this bridge has closed.
await this.page
.evaluate(() => {
const state = window.__WWebCanonicalAppStateSyncJobV1;
let cancelled = false;
if (state) {
if (state.timeoutHandle != null) {
clearTimeout(state.timeoutHandle);
state.timeoutHandle = null;
}
const cancel = state.cancel;
state.cancel = null;
state.status = 'cancelled';
state.cancelledAt = Date.now();
state.completedAt = state.cancelledAt;
delete window.__WWebCanonicalAppStateSyncJobV1;
cancelled = true;
if (typeof cancel === 'function') {
try {
Promise.resolve(cancel()).catch(() => {});
} catch {
// Network is already fenced offline and the global
// generation reference has been detached above.
}
}
}
const importTask = window.__WWebCanonicalImportTaskV1;
if (importTask) {
// The opaque import task may be awaiting a private
// IndexedDB promise. Mark it cancelled before detaching
// the global so a late continuation cannot retain or
// publish its result into a reused page lifecycle.
importTask.cancelled = true;
importTask.status = 'cancelled';
importTask.stage = 'cancelled';
importTask.error_code =
'wwebjs_canonical_import_task_cancelled';
importTask.clearResult?.();
importTask.consumeResult = undefined;
importTask.clearResult = undefined;
importTask.result = undefined;
importTask.updated_at_ms = Date.now();
delete window.__WWebCanonicalImportTaskV1;
cancelled = true;
}
return cancelled;
})
.catch(() => {});
const freeze = this.canonicalProfileSnapshotFreeze;
this.canonicalProfileSnapshotFreeze = undefined;
if (freeze?.session) {
await freeze.session
.send('Page.setWebLifecycleState', { state: 'active' })
.catch(() => {});
if (typeof freeze.session.detach === 'function') {
await freeze.session.detach().catch(() => {});
}
}
await this.flushTail.catch(() => {});
await this.disarmCanonicalCredentialGuard();
await this.page
.evaluate(() => window.__WWebSessionBridge?.teardown?.())
.catch(() => {});
this.capturedAdvSecret?.fill(0);
this.capturedAdvSecret = undefined;
}
}
module.exports = {
BRIDGE_SCHEMA_VERSION,
BrowserSessionBridge,
compareCanonicalAppState,
compareCanonicalAppStateSyncKeys,
exportCanonicalSessionProjection,
exportBrowserProjection,
importCanonicalSessionProjection,
importBrowserProjection,
inspectCanonicalTransportHealth,
isCanonicalTransportNativeStall,
launchCanonicalRegisteredSocketOnline,
redriveCanonicalRealmRecoveryTransport,
installCanonicalCredentialNavigationGuard,
installBrowserSessionBridge,
normalizeCanonicalAppStateForSnapshotResync,
normalizeCanonicalAppStateForProviderHandoff,
normalizeCanonicalAuthoritativeAppStateForPortablePersistence,
normalizeCanonicalAppStateForPortablePersistence,
normalizedSnapshotResyncCollections,
observeCanonicalAppStateRestoreBarrier,
observeCanonicalAppStateSyncJob,
reconcileCanonicalAppStateForPersistence,
reconcileCanonicalAppStateSyncKeysForPersistence,
rearmCanonicalSocketOffline,
resumeCanonicalRegisteredSocketOnline,
restoreCanonicalAppStateSyncKeys,
triggerPreparedCanonicalAppStateSync,
triggerCanonicalInitialAppStateFullSync,
validateCanonicalPqKeyPairs,
verifyCanonicalReloadedSocketOffline,
};