'use strict';
const crypto = require('crypto');
const {
DEFAULT_CHUNK_BYTES,
DEFAULT_MAX_PROFILE_BYTES,
buildProfileArtifact,
canonicalJson,
ensureProfileFingerprint,
profileFingerprint,
rebindProfileFingerprint,
restoreProfileArtifact,
validateProfileArtifactMetadata,
} = require('./ProfileArtifact');
const {
BRIDGE_SCHEMA_VERSION,
normalizeCanonicalAuthoritativeAppStateForPortablePersistence,
} = require('./BrowserSessionBridge');
const {
CANONICAL_FINGERPRINT_VERSION,
CANONICAL_PRIVATE_MODULE_ABI,
CANONICAL_SESSION_CODEC_KIND,
CANONICAL_SESSION_CODEC_VERSION,
CANONICAL_SESSION_SCHEMA_VERSION,
CANONICAL_SESSION_STORE_ABI,
DEFAULT_MAX_CANONICAL_BYTES,
assertCanonicalTargetProviderCompatibility,
canonicalBrowserProjectionToStore,
canonicalStoreProjectionToBrowser,
} = require('./CanonicalSessionBridge');
const { WhatsAppSessionDebugLogger } = require('./SessionDebug');
const {
WhatsAppSessionCompatibilityError,
WhatsAppSessionError,
WhatsAppSessionLeaseLostError,
} = require('./SessionErrors');
const { SupportedWebVersion } = require('../util/Constants');
const SESSION_STORE_KIND = 'whatsapp-postgres-v1';
const PROVIDER = 'wwebjs';
const PROFILE_ARTIFACT_KIND = 'wwebjs_profile';
const SESSION_SCHEMA_VERSION = CANONICAL_SESSION_SCHEMA_VERSION;
const SESSION_CODEC_VERSION = 1;
const SESSION_FORMAT = 'wwebjs-profile-manifest-v1';
const PROVIDER_RECORD_CODEC_VERSION = 1;
const PROJECTION_META_NAMESPACE = '_wwebjs_projection';
const PROJECTION_META_KEY = 'v1';
const CANONICAL_META_NAMESPACE = '_wwebjs_canonical';
const CANONICAL_META_KEY = 'v1';
const CANONICAL_PROVIDER_STATE_KEY = 'noise_bootstrap_v1';
const CANONICAL_TRANSPORT_NAMESPACE = 'whatsapp/transport';
const CANONICAL_ROUTING_INFO_KEY = 'routing_info';
const LIFECYCLE_NAMESPACE = '_wwebjs_lifecycle';
const CANONICAL_ACTIVATION_PENDING_KEY = 'pending_canonical_activation_v1';
const PQ_SERVER_ROLLBACK_MARKER_KEY = 'pq_server_rollback_v1';
const LAST_GOOD_PROFILE_ANCHOR_KEY = 'last_good_profile_anchor_v1';
const PROFILE_ANCHOR_OPERATION_ABI = 'profile-anchor-canonical-checkpoint-v1';
const PQ_SERVER_ROLLBACK_PROTOCOL = 'delete_pq_prekeys_server_ack_v1';
const APP_STATE_SNAPSHOT_RESYNC_GATE_KIND = 'app_state_snapshot_resync_gate';
const APP_STATE_SNAPSHOT_RESYNC_GATE_VERSION = 1;
const APP_STATE_SNAPSHOT_RESYNC_GATE_UUID_NAME =
'underchat:whatsapp:app-state-snapshot-resync-gate:v1';
const MAX_APP_STATE_SYNC_KEY_PROOFS = 65536;
const MAX_ROUTING_INFO_BYTES = 65535;
const DEFAULT_LEASE_TTL_MS = 90000;
const DEFAULT_LEASE_RENEW_MS = 5000;
const DEFAULT_LEASE_SAFETY_MS = 5000;
const postgresSqlState = (error) => {
let current = error;
const seen = new Set();
for (
let depth = 0;
depth < 4 &&
current &&
typeof current === 'object' &&
!seen.has(current);
depth += 1
) {
seen.add(current);
if (typeof current.code === 'string') return current.code;
current = current.cause;
}
};
const isDefinitiveLeaseRejection = (error) =>
postgresSqlState(error) === '55000';
const leaseRenewalErrorForLog = (error) => ({
name: error instanceof Error ? error.name : 'UnknownError',
sql_state: postgresSqlState(error),
});
const PROFILE_CHANGED_DURING_CHECKPOINT =
'whatsapp_artifact_profile_changed_during_checkpoint';
const CONNECTED_PROFILE_NAVIGATION_SUPERSEDED =
'wwebjs_connected_profile_navigation_superseded';
const PROFILE_TOO_LARGE = 'whatsapp_artifact_profile_too_large';
const LAST_GOOD_PROFILE_FALLBACK_MODE = 'last_good_plus_fresh_canonical_v1';
const PROFILE_ANCHOR_CHECKPOINT_MODE =
'immutable_profile_anchor_plus_fresh_canonical_v1';
const LAST_GOOD_PROFILE_ANCHOR_SOURCES = new Set([
'active_restart_ready',
'handoff',
'shutdown',
]);
const MAX_PROVIDER_PROJECTION_BYTES = 64 * 1024 * 1024;
const MAX_PROVIDER_PROJECTION_RECORDS = 20000;
const MAX_DATABASE_ARTIFACT_BYTES = 512 * 1024 * 1024;
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,
);
const canonicalProjectionChecksum = (projection) =>
crypto.createHash('sha256').update(canonicalJson(projection)).digest('hex');
const canonicalAppStateChecksum = (projection) =>
crypto
.createHash('sha256')
.update(
canonicalJson({
sync_keys:
projection?.tables?.whatsapp_app_state_sync_keys || [],
versions: projection?.tables?.whatsapp_app_state_version || [],
mutation_macs:
projection?.tables?.whatsapp_app_state_mutation_macs || [],
capabilities: normalizeCanonicalAppStateSnapshotCapabilities(
projection?.capabilities,
{ allowMissing: true },
),
}),
)
.digest('hex');
const canonicalProjectionHasFutureMutationMac = (projection) => {
const versions = projection?.tables?.whatsapp_app_state_version;
const mutationMacs = projection?.tables?.whatsapp_app_state_mutation_macs;
if (!Array.isArray(versions) || !Array.isArray(mutationMacs)) return false;
const committedVersionByName = new Map(
versions
.filter(
(row) =>
typeof row?.name === 'string' &&
Number.isSafeInteger(row?.version),
)
.map((row) => [row.name, row.version]),
);
return mutationMacs.some((row) => {
const committedVersion = committedVersionByName.get(row?.name);
return (
!Number.isSafeInteger(committedVersion) ||
(Number.isSafeInteger(row?.version) &&
row.version > committedVersion)
);
});
};
const canonicalSemanticRows = (rows) =>
(Array.isArray(rows) ? rows : [])
.map((row) => ({ row, canonical: canonicalJson(row) }))
.sort((left, right) =>
left.canonical < right.canonical
? -1
: left.canonical > right.canonical
? 1
: 0,
)
.map(({ row }) => row);
const canonicalSignalDevice = (device) => {
if (!device || typeof device !== 'object' || Array.isArray(device)) {
return device;
}
if (typeof device.jid !== 'string') return device;
const normalizedJid = device.jid.trim();
const separator = normalizedJid.lastIndexOf('@');
if (separator <= 0 || separator === normalizedJid.length - 1) {
return device;
}
const local = normalizedJid.slice(0, separator);
const server = normalizedJid.slice(separator + 1).toLowerCase();
if (server !== 'c.us' && server !== 's.whatsapp.net') return device;
return { ...device, jid: `${local}@s.whatsapp.net` };
};
// PostgreSQL tables are unordered sets. Keep the legacy order-sensitive
// checksum above for durable anchor compatibility, but compare a second,
// deterministic checksum when the same app-state is read from two revisions
// during a cross-provider handoff. This proves content equivalence without
// mistaking a different query row order for native profile drift.
const canonicalAppStateSemanticChecksum = (projection) =>
crypto
.createHash('sha256')
.update(
canonicalJson({
sync_keys: canonicalSemanticRows(
projection?.tables?.whatsapp_app_state_sync_keys,
),
versions: canonicalSemanticRows(
projection?.tables?.whatsapp_app_state_version,
),
mutation_macs: canonicalSemanticRows(
projection?.tables?.whatsapp_app_state_mutation_macs,
),
capabilities: normalizeCanonicalAppStateSnapshotCapabilities(
projection?.capabilities,
{ allowMissing: true },
),
}),
)
.digest('hex');
const canonicalSignalTablesSemanticChecksum = (projection) =>
crypto
.createHash('sha256')
.update(
canonicalJson({
device: canonicalSignalDevice(projection?.device),
identity_keys: canonicalSemanticRows(
projection?.tables?.whatsapp_identity_keys,
),
pre_keys: canonicalSemanticRows(
projection?.tables?.whatsapp_pre_keys,
),
pq_pre_keys: canonicalSemanticRows(
projection?.tables?.whatsapp_pq_pre_keys,
),
pq_pre_key_state: canonicalSemanticRows(
projection?.tables?.whatsapp_pq_pre_key_state,
),
signal_sessions: canonicalSemanticRows(
projection?.tables?.whatsapp_signal_sessions,
),
sender_keys: canonicalSemanticRows(
projection?.tables?.whatsapp_sender_keys,
),
}),
)
.digest('hex');
const canonicalSignalTableComponentChecksums = (projection) => {
const components = {
device: canonicalSignalDevice(projection?.device),
identity_keys: canonicalSemanticRows(
projection?.tables?.whatsapp_identity_keys,
),
pre_keys: canonicalSemanticRows(projection?.tables?.whatsapp_pre_keys),
pq_pre_keys: canonicalSemanticRows(
projection?.tables?.whatsapp_pq_pre_keys,
),
pq_pre_key_state: canonicalSemanticRows(
projection?.tables?.whatsapp_pq_pre_key_state,
),
signal_sessions: canonicalSemanticRows(
projection?.tables?.whatsapp_signal_sessions,
),
sender_keys: canonicalSemanticRows(
projection?.tables?.whatsapp_sender_keys,
),
};
return Object.fromEntries(
Object.entries(components).map(([name, value]) => [
name,
crypto
.createHash('sha256')
.update(canonicalJson(value))
.digest('hex'),
]),
);
};
const canonicalMetadataPayload = (projection, advSecretAvailable) =>
Buffer.from(
JSON.stringify({
schema_version: CANONICAL_SESSION_SCHEMA_VERSION,
codec_kind: CANONICAL_SESSION_CODEC_KIND,
codec_version: CANONICAL_SESSION_CODEC_VERSION,
module_abi: CANONICAL_PRIVATE_MODULE_ABI,
web_version: projection.web_version,
fingerprint_version: CANONICAL_FINGERPRINT_VERSION,
complete: true,
capabilities: {
...projection.capabilities,
adv_secret_available: advSecretAvailable,
passkey_handoff_supported: advSecretAvailable,
},
record_count: projection.record_count,
size_bytes: projection.size_bytes,
}),
);
const CANONICAL_TABLE_CONFIGS = Object.freeze([
Object.freeze({
table: 'whatsapp_identity_keys',
projectionKey: 'whatsapp_identity_keys',
columns: Object.freeze(['their_id', 'identity']),
}),
Object.freeze({
table: 'whatsapp_pre_keys',
projectionKey: 'whatsapp_pre_keys',
columns: Object.freeze(['key_id', 'key', 'uploaded']),
}),
Object.freeze({
table: 'whatsapp_pq_pre_keys',
projectionKey: 'whatsapp_pq_pre_keys',
columns: Object.freeze([
'key_id',
'key_kind',
'public_key',
'private_key',
'signature',
'timestamp_ms',
'sent_to_server',
]),
}),
Object.freeze({
table: 'whatsapp_pq_pre_key_state',
projectionKey: 'whatsapp_pq_pre_key_state',
columns: Object.freeze([
'codec_version',
'algorithm',
'next_pre_key_id',
'migrated',
'last_server_count',
'last_server_count_timestamp_ms',
]),
}),
Object.freeze({
table: 'whatsapp_signal_sessions',
projectionKey: 'whatsapp_signal_sessions',
columns: Object.freeze(['their_id', 'scope', 'session']),
}),
Object.freeze({
table: 'whatsapp_sender_keys',
projectionKey: 'whatsapp_sender_keys',
columns: Object.freeze(['chat_id', 'sender_id', 'sender_key']),
}),
Object.freeze({
table: 'whatsapp_app_state_sync_keys',
projectionKey: 'whatsapp_app_state_sync_keys',
columns: Object.freeze([
'key_id',
'key_data',
'timestamp',
'fingerprint',
]),
}),
Object.freeze({
table: 'whatsapp_app_state_version',
projectionKey: 'whatsapp_app_state_version',
columns: Object.freeze(['name', 'version', 'hash']),
}),
Object.freeze({
table: 'whatsapp_app_state_mutation_macs',
projectionKey: 'whatsapp_app_state_mutation_macs',
columns: Object.freeze(['name', 'version', 'index_mac', 'value_mac']),
}),
]);
const CANONICAL_TABLE_NAMES = new Set(
CANONICAL_TABLE_CONFIGS.map(({ table }) => table),
);
const normalizeCanonicalAppStateSnapshotCapabilities = (
capabilities,
{ allowMissing = false } = {},
) => {
const required = capabilities?.app_state_snapshot_resync_required;
const collections = capabilities?.app_state_snapshot_resync_collections;
if (allowMissing && required === undefined && collections === undefined) {
return {
app_state_snapshot_resync_required: false,
app_state_snapshot_resync_collections: [],
};
}
const sorted = Array.isArray(collections) ? [...collections].sort() : [];
if (
typeof required !== 'boolean' ||
!Array.isArray(collections) ||
collections.some(
(name) =>
typeof name !== 'string' ||
!CANONICAL_APP_STATE_COLLECTION_SET.has(name),
) ||
new Set(collections).size !== collections.length ||
JSON.stringify(collections) !== JSON.stringify(sorted) ||
required !== collections.length > 0
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_handoff_app_state_capability_invalid',
);
}
return {
app_state_snapshot_resync_required: required,
app_state_snapshot_resync_collections: [...collections],
};
};
const assertCanonicalAppStateSnapshotResyncProjectionNormalized = (
projection,
capabilities = normalizeCanonicalAppStateSnapshotCapabilities(
projection?.capabilities,
),
) => {
const versions = projection?.tables?.whatsapp_app_state_version;
const mutationMacs = projection?.tables?.whatsapp_app_state_mutation_macs;
if (!Array.isArray(versions) || !Array.isArray(mutationMacs)) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_source_inconsistent',
);
}
const collectionNames = new Set(
capabilities.app_state_snapshot_resync_collections,
);
if (
versions.some((row) => collectionNames.has(row?.name)) ||
mutationMacs.some((row) => collectionNames.has(row?.name))
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_source_inconsistent',
);
}
return capabilities;
};
const PASSTHROUGH_TABLE_CONFIGS = Object.freeze([
Object.freeze({
table: 'whatsapp_contacts',
columns: Object.freeze([
'their_jid',
'first_name',
'full_name',
'push_name',
'business_name',
'redacted_phone',
]),
key: (row) => row.their_jid,
}),
Object.freeze({
table: 'whatsapp_chat_settings',
columns: Object.freeze([
'chat_jid',
'muted_until',
'pinned',
'archived',
]),
key: (row) => row.chat_jid,
}),
Object.freeze({
table: 'whatsapp_message_secrets',
columns: Object.freeze(['chat_jid', 'sender_jid', 'message_id', 'key']),
byteColumns: Object.freeze(['key']),
key: (row) => `${row.chat_jid}\0${row.sender_jid}\0${row.message_id}`,
}),
Object.freeze({
table: 'whatsapp_privacy_tokens',
columns: Object.freeze([
'their_jid',
'token',
'timestamp',
'sender_timestamp',
]),
byteColumns: Object.freeze(['token']),
key: (row) => row.their_jid,
}),
Object.freeze({
table: 'whatsapp_nct_salt',
columns: Object.freeze(['salt']),
byteColumns: Object.freeze(['salt']),
key: () => 'singleton',
}),
Object.freeze({
table: 'whatsapp_lid_map',
columns: Object.freeze(['lid', 'pn']),
key: (row) => row.lid,
}),
Object.freeze({
table: 'whatsapp_event_buffer',
columns: Object.freeze([
'ciphertext_hash',
'plaintext',
'server_timestamp',
'insert_timestamp',
]),
byteColumns: Object.freeze(['ciphertext_hash', 'plaintext']),
nullableByteColumns: Object.freeze(['plaintext']),
key: (row) => Buffer.from(row.ciphertext_hash).toString('hex'),
}),
Object.freeze({
table: 'whatsapp_retry_buffer',
columns: Object.freeze([
'chat_jid',
'message_id',
'format',
'plaintext',
'timestamp',
]),
byteColumns: Object.freeze(['plaintext']),
key: (row) => `${row.chat_jid}\0${row.message_id}`,
}),
]);
const ALL_HANDOFF_STATE_CONFIGS = Object.freeze([
...CANONICAL_TABLE_CONFIGS,
...PASSTHROUGH_TABLE_CONFIGS,
]);
const MAX_HANDOFF_SOURCE_ROWS = 200000;
const UUID_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const BIGINT_PATTERN = /^[1-9][0-9]*$/;
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
const PROFILE_ANCHOR_CHECKPOINT_MODES = new Set([
'legacy_adoption_v1',
'full_profile_plus_fresh_canonical_v1',
PROFILE_ANCHOR_CHECKPOINT_MODE,
LAST_GOOD_PROFILE_FALLBACK_MODE,
]);
const DIAGNOSTIC_ERROR_CODE_PATTERN =
/^(?:handoff|whatsapp|wwebjs)_[a-z0-9_.-]{1,91}$/i;
const safeDiagnosticErrorCode = (value, fallback) => {
const normalized = typeof value === 'string' ? value : '';
return DIAGNOSTIC_ERROR_CODE_PATTERN.test(normalized)
? normalized
: fallback;
};
const normalizeLastGoodProfileFallback = (fallback, maxProfileBytes) => {
if (fallback === undefined) return undefined;
if (
!isObjectRecord(fallback) ||
Object.keys(fallback).sort().join(',') !==
'error_code,max_bytes,size_bytes,version' ||
fallback.version !== 1 ||
fallback.error_code !== PROFILE_TOO_LARGE ||
!Number.isSafeInteger(fallback.size_bytes) ||
!Number.isSafeInteger(fallback.max_bytes) ||
fallback.size_bytes <= fallback.max_bytes ||
fallback.max_bytes !== maxProfileBytes
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_last_good_profile_fallback_invalid',
);
}
return {
version: 1,
errorCode: fallback.error_code,
sizeBytes: fallback.size_bytes,
maxBytes: fallback.max_bytes,
};
};
const assertUuid = (value, code) => {
if (!UUID_PATTERN.test(String(value || ''))) {
throw new WhatsAppSessionError(code);
}
};
const normalizeRevisionId = (revisionId) => {
if (revisionId == null) return undefined;
if (
typeof revisionId === 'number' &&
(!Number.isSafeInteger(revisionId) || revisionId <= 0)
) {
throw new WhatsAppSessionError('whatsapp_session_revision_id_invalid');
}
const normalized = String(revisionId);
if (
!BIGINT_PATTERN.test(normalized) ||
BigInt(normalized) > 9223372036854775807n
) {
throw new WhatsAppSessionError('whatsapp_session_revision_id_invalid');
}
return normalized;
};
const sha256 = (value) =>
crypto.createHash('sha256').update(value).digest('hex');
const normalizeProfileAnchorCheckpoint = (row) => {
if (!row) return undefined;
const anchorGeneration = normalizeRevisionId(row.anchor_generation);
const canonicalGeneration = normalizeRevisionId(row.canonical_generation);
const artifactSizeBytes = Number(row.artifact_size_bytes);
const artifactChunkCount = Number(row.artifact_chunk_count);
const canonicalRecordCount = Number(row.canonical_record_count);
const canonicalSizeBytes = Number(row.canonical_size_bytes);
const baselineAppStateChecksumSha256 =
row.baseline_app_state_checksum_sha256 ?? null;
if (
!UUID_PATTERN.test(String(row.artifact_id || '')) ||
row.state !== 'active' ||
!PROFILE_ANCHOR_CHECKPOINT_MODES.has(row.checkpoint_mode) ||
!SHA256_PATTERN.test(String(row.artifact_checksum_sha256 || '')) ||
!Number.isSafeInteger(artifactSizeBytes) ||
artifactSizeBytes < 1 ||
artifactSizeBytes > MAX_DATABASE_ARTIFACT_BYTES ||
!Number.isSafeInteger(artifactChunkCount) ||
artifactChunkCount < 1 ||
!SHA256_PATTERN.test(String(row.canonical_checksum_sha256 || '')) ||
!Number.isSafeInteger(canonicalRecordCount) ||
canonicalRecordCount < 0 ||
!Number.isSafeInteger(canonicalSizeBytes) ||
canonicalSizeBytes < 1 ||
canonicalSizeBytes > MAX_PROVIDER_PROJECTION_BYTES ||
!SHA256_PATTERN.test(
String(row.current_app_state_checksum_sha256 || ''),
) ||
(baselineAppStateChecksumSha256 !== null &&
!SHA256_PATTERN.test(String(baselineAppStateChecksumSha256))) ||
typeof row.app_state_overlay_required !== 'boolean' ||
!row.artifact_persisted_at ||
!row.canonical_persisted_at
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_anchor_checkpoint_invalid',
);
}
return {
sessionId: row.session_id,
revisionId: normalizeRevisionId(row.revision_id),
anchorGeneration,
canonicalGeneration,
artifactId: row.artifact_id,
checkpointMode: row.checkpoint_mode,
artifactChecksumSha256: row.artifact_checksum_sha256,
artifactSizeBytes,
artifactChunkCount,
artifactPersistedAt: row.artifact_persisted_at,
artifactVerifiedAt: row.artifact_verified_at,
baselineAppStateChecksumSha256,
currentAppStateChecksumSha256: row.current_app_state_checksum_sha256,
appStateOverlayRequired: row.app_state_overlay_required,
canonicalChecksumSha256: row.canonical_checksum_sha256,
canonicalRecordCount,
canonicalSizeBytes,
canonicalPersistedAt: row.canonical_persisted_at,
source: row.source,
lastProfileObservedSizeBytes:
row.last_profile_observed_size_bytes == null
? undefined
: Number(row.last_profile_observed_size_bytes),
};
};
const uuidBytes = (value) =>
Buffer.from(String(value).replace(/-/g, ''), 'hex');
const appStateSnapshotResyncArtifactId = (handoffId) => {
assertUuid(
handoffId,
'whatsapp_session_app_state_snapshot_resync_handoff_id_invalid',
);
if (/^0{8}-0{4}-0{4}-0{4}-0{12}$/i.test(String(handoffId))) {
throw new WhatsAppSessionError(
'whatsapp_session_app_state_snapshot_resync_handoff_id_invalid',
);
}
const digest = crypto
.createHash('sha1')
.update(uuidBytes(handoffId))
.update(APP_STATE_SNAPSHOT_RESYNC_GATE_UUID_NAME, 'utf8')
.digest();
digest[6] = (digest[6] & 0x0f) | 0x50;
digest[8] = (digest[8] & 0x3f) | 0x80;
const hex = digest.subarray(0, 16).toString('hex');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(
12,
16,
)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
};
const safeJsonInt64 = (value, code) => {
const normalized = String(value);
if (!/^(?:0|[1-9][0-9]*)$/.test(normalized)) {
throw new WhatsAppSessionCompatibilityError(code);
}
const number = Number(normalized);
if (!Number.isSafeInteger(number) || number < 0) {
throw new WhatsAppSessionCompatibilityError(code);
}
return number;
};
const readProtoVarint = (bytes, offset, code) => {
let result = 0n;
let shift = 0n;
for (
let index = offset;
index < bytes.length && index < offset + 10;
index += 1
) {
const byte = bytes[index];
result |= BigInt(byte & 0x7f) << shift;
if ((byte & 0x80) === 0) {
return { value: result, offset: index + 1 };
}
shift += 7n;
}
throw new WhatsAppSessionCompatibilityError(code);
};
const appStateSyncKeyFingerprintRawId = (fingerprint) => {
const code = 'whatsapp_session_app_state_snapshot_resync_sync_key_invalid';
const bytes = Buffer.from(fingerprint || []);
if (bytes.length === 0) {
throw new WhatsAppSessionCompatibilityError(code);
}
let offset = 0;
let rawId;
while (offset < bytes.length) {
const tag = readProtoVarint(bytes, offset, code);
offset = tag.offset;
const fieldNumber = Number(tag.value >> 3n);
const wireType = Number(tag.value & 7n);
if (fieldNumber <= 0) {
throw new WhatsAppSessionCompatibilityError(code);
}
if (wireType === 0) {
const decoded = readProtoVarint(bytes, offset, code);
offset = decoded.offset;
if (fieldNumber === 1) {
if (decoded.value > 0xffffffffn) {
throw new WhatsAppSessionCompatibilityError(code);
}
rawId = Number(decoded.value);
}
continue;
}
if (wireType === 1) {
offset += 8;
} else if (wireType === 2) {
const length = readProtoVarint(bytes, offset, code);
offset = length.offset;
if (length.value > BigInt(bytes.length - offset)) {
throw new WhatsAppSessionCompatibilityError(code);
}
offset += Number(length.value);
} else if (wireType === 5) {
offset += 4;
} else {
throw new WhatsAppSessionCompatibilityError(code);
}
if (offset > bytes.length) {
throw new WhatsAppSessionCompatibilityError(code);
}
}
if (rawId === undefined) {
throw new WhatsAppSessionCompatibilityError(code);
}
return rawId;
};
const appStateSyncKeyRowChecksum = ({
keyId,
keyData,
timestamp,
fingerprint,
}) => {
const hasher = crypto.createHash('sha256');
for (const value of [keyId, keyData, fingerprint]) {
const length = Buffer.allocUnsafe(8);
length.writeBigUInt64BE(BigInt(value.length));
hasher.update(length).update(value);
}
const encodedTimestamp = Buffer.allocUnsafe(8);
encodedTimestamp.writeBigUInt64BE(BigInt(timestamp));
return hasher.update(encodedTimestamp).digest('hex');
};
const appStateSyncKeyProofChains = (keys, code) => {
const chains = new Map();
for (const entry of keys) {
const chain = chains.get(entry.fingerprint_raw_id) || [];
chain.push(entry);
chains.set(entry.fingerprint_raw_id, chain);
}
for (const chain of chains.values()) {
chain.sort((left, right) => left.epoch - right.epoch);
// This protobuf scalar is optional: 0 means unknown and must not
// reset the last real timestamp observed for the lineage.
let lastKnownTimestamp =
chain[0].timestamp > 0 ? chain[0].timestamp : 0;
for (let index = 1; index < chain.length; index += 1) {
const previous = chain[index - 1];
const current = chain[index];
if (current.epoch === previous.epoch) {
throw new WhatsAppSessionCompatibilityError(code);
}
if (
current.timestamp > 0 &&
lastKnownTimestamp > 0 &&
current.timestamp < lastKnownTimestamp
) {
throw new WhatsAppSessionCompatibilityError(code);
}
if (current.timestamp > 0) {
lastKnownTimestamp = current.timestamp;
}
}
}
return chains;
};
const appStateSyncKeyProofSummary = (keys, code) => {
const chains = appStateSyncKeyProofChains(keys, code);
const rawIds = [...chains.keys()].sort((left, right) => left - right);
return {
chains,
// Version 1 already exposes this scalar in its canonical ABI. For a
// multi-chain proof it is the deterministic minimum raw ID; the
// per-key raw IDs remain authoritative for chain validation.
fingerprintRawId: rawIds[0],
maxEpoch: Math.max(...keys.map((entry) => entry.epoch)),
maxTimestamp: Math.max(...keys.map((entry) => entry.timestamp)),
};
};
const captureAppStateSyncKeyAnchorProof = (rows) => {
const code = 'whatsapp_session_app_state_snapshot_resync_sync_key_invalid';
if (!Array.isArray(rows) || rows.length === 0) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_sync_key_missing',
);
}
if (rows.length > MAX_APP_STATE_SYNC_KEY_PROOFS) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_sync_key_limit_exceeded',
);
}
const seen = new Set();
const keys = rows.map((row) => {
const keyId = Buffer.from(row?.key_id || []);
const keyData = Buffer.from(row?.key_data || []);
const fingerprint = Buffer.from(row?.fingerprint || []);
const timestamp = safeJsonInt64(row?.timestamp, code);
if (keyId.length < 2 || keyData.length !== 32) {
throw new WhatsAppSessionCompatibilityError(code);
}
const rawId = appStateSyncKeyFingerprintRawId(fingerprint);
const epoch = keyId.readUInt16BE(keyId.length - 2);
const keyIdChecksum = sha256(keyId);
if (seen.has(keyIdChecksum)) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_sync_key_duplicate',
);
}
seen.add(keyIdChecksum);
return {
key_id_checksum_sha256: keyIdChecksum,
row_checksum_sha256: appStateSyncKeyRowChecksum({
keyId,
keyData,
timestamp,
fingerprint,
}),
epoch,
timestamp,
fingerprint_raw_id: rawId,
};
});
keys.sort((left, right) =>
left.key_id_checksum_sha256.localeCompare(right.key_id_checksum_sha256),
);
const summary = appStateSyncKeyProofSummary(keys, code);
return {
count: keys.length,
max_epoch: summary.maxEpoch,
max_timestamp: summary.maxTimestamp,
fingerprint_raw_id: summary.fingerprintRawId,
keys,
};
};
const normalizeAppStateSyncKeyAnchorProof = (proof) => {
const code =
'whatsapp_session_app_state_snapshot_resync_sync_key_proof_invalid';
if (
!proof ||
typeof proof !== 'object' ||
Array.isArray(proof) ||
!Array.isArray(proof.keys) ||
proof.keys.length === 0 ||
proof.keys.length > MAX_APP_STATE_SYNC_KEY_PROOFS
) {
throw new WhatsAppSessionCompatibilityError(code);
}
const declaredFingerprintRawId = safeJsonInt64(
proof.fingerprint_raw_id,
code,
);
if (declaredFingerprintRawId > 0xffffffff) {
throw new WhatsAppSessionCompatibilityError(code);
}
const seen = new Set();
let previousChecksum = '';
const keys = proof.keys.map((entry) => {
const epoch = safeJsonInt64(entry?.epoch, code);
const timestamp = safeJsonInt64(entry?.timestamp, code);
const entryRawId = safeJsonInt64(entry?.fingerprint_raw_id, code);
const keyIdChecksum = entry?.key_id_checksum_sha256;
const rowChecksum = entry?.row_checksum_sha256;
if (
epoch > 0xffff ||
entryRawId > 0xffffffff ||
typeof keyIdChecksum !== 'string' ||
!/^[0-9a-f]{64}$/.test(keyIdChecksum) ||
typeof rowChecksum !== 'string' ||
!/^[0-9a-f]{64}$/.test(rowChecksum) ||
keyIdChecksum <= previousChecksum ||
seen.has(keyIdChecksum)
) {
throw new WhatsAppSessionCompatibilityError(code);
}
seen.add(keyIdChecksum);
previousChecksum = keyIdChecksum;
return {
key_id_checksum_sha256: keyIdChecksum,
row_checksum_sha256: rowChecksum,
epoch,
timestamp,
fingerprint_raw_id: entryRawId,
};
});
const summary = appStateSyncKeyProofSummary(keys, code);
if (
safeJsonInt64(proof.count, code) !== keys.length ||
safeJsonInt64(proof.max_epoch, code) !== summary.maxEpoch ||
safeJsonInt64(proof.max_timestamp, code) !== summary.maxTimestamp ||
declaredFingerprintRawId !== summary.fingerprintRawId
) {
throw new WhatsAppSessionCompatibilityError(code);
}
return {
count: keys.length,
max_epoch: summary.maxEpoch,
max_timestamp: summary.maxTimestamp,
fingerprint_raw_id: summary.fingerprintRawId,
keys,
};
};
const validateTargetAppStateSyncKeyProof = (source, target) => {
const normalizedSource = normalizeAppStateSyncKeyAnchorProof(source);
const normalizedTarget = normalizeAppStateSyncKeyAnchorProof(target);
if (normalizedTarget.count < normalizedSource.count) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_sync_key_missing',
);
}
const sourceChains = appStateSyncKeyProofChains(
normalizedSource.keys,
'whatsapp_session_app_state_snapshot_resync_sync_key_proof_invalid',
);
const sourceById = new Map(
normalizedSource.keys.map((entry) => [
entry.key_id_checksum_sha256,
entry,
]),
);
let matched = 0;
for (const targetEntry of normalizedTarget.keys) {
const sourceEntry = sourceById.get(targetEntry.key_id_checksum_sha256);
if (sourceEntry) {
if (JSON.stringify(sourceEntry) !== JSON.stringify(targetEntry)) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_sync_key_changed',
);
}
matched += 1;
continue;
}
const sourceChain = sourceChains.get(targetEntry.fingerprint_raw_id);
const sourceAnchor = sourceChain?.[sourceChain.length - 1];
const sourceTimestampAnchor = sourceChain?.reduce(
(timestamp, entry) =>
entry.timestamp > 0 ? entry.timestamp : timestamp,
0,
);
if (
!sourceAnchor ||
targetEntry.epoch <= sourceAnchor.epoch ||
(targetEntry.timestamp > 0 &&
sourceTimestampAnchor > 0 &&
targetEntry.timestamp < sourceTimestampAnchor)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_extra_sync_key_invalid',
);
}
}
if (matched !== normalizedSource.count) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_sync_key_missing',
);
}
return normalizedTarget;
};
const canonicalAppStateSnapshotResyncManifest = (manifest) =>
JSON.stringify({
version: manifest.version,
handoff_id: manifest.handoff_id,
lifecycle_operation_id: manifest.lifecycle_operation_id,
source_provider: manifest.source_provider,
source_revision_id: manifest.source_revision_id,
target_provider: manifest.target_provider,
target_revision_id: manifest.target_revision_id,
app_state_snapshot_resync_required:
manifest.app_state_snapshot_resync_required,
app_state_snapshot_resync_collections:
manifest.app_state_snapshot_resync_collections,
source_sync_keys: {
count: manifest.source_sync_keys.count,
max_epoch: manifest.source_sync_keys.max_epoch,
max_timestamp: manifest.source_sync_keys.max_timestamp,
fingerprint_raw_id: manifest.source_sync_keys.fingerprint_raw_id,
keys: manifest.source_sync_keys.keys.map((entry) => ({
key_id_checksum_sha256: entry.key_id_checksum_sha256,
row_checksum_sha256: entry.row_checksum_sha256,
epoch: entry.epoch,
timestamp: entry.timestamp,
fingerprint_raw_id: entry.fingerprint_raw_id,
})),
},
materialized_collections: manifest.materialized_collections.map(
(entry) => ({
name: entry.name,
version: entry.version,
}),
),
});
const normalizeAppStateSnapshotResyncManifest = (manifest) => {
const code = 'whatsapp_session_app_state_snapshot_resync_artifact_invalid';
if (
!manifest ||
typeof manifest !== 'object' ||
Array.isArray(manifest) ||
manifest.version !== APP_STATE_SNAPSHOT_RESYNC_GATE_VERSION ||
!UUID_PATTERN.test(String(manifest.handoff_id || '')) ||
!UUID_PATTERN.test(String(manifest.lifecycle_operation_id || '')) ||
!['baileys', 'wwebjs', 'whatsmeow'].includes(
manifest.source_provider,
) ||
manifest.target_provider !== PROVIDER ||
!Array.isArray(manifest.materialized_collections)
) {
throw new WhatsAppSessionCompatibilityError(code);
}
const capabilities =
normalizeCanonicalAppStateSnapshotCapabilities(manifest);
const sourceRevisionId = safeJsonInt64(manifest.source_revision_id, code);
const targetRevisionId = safeJsonInt64(manifest.target_revision_id, code);
if (sourceRevisionId <= 0 || targetRevisionId <= 0) {
throw new WhatsAppSessionCompatibilityError(code);
}
const sourceSyncKeys = normalizeAppStateSyncKeyAnchorProof(
manifest.source_sync_keys,
);
const required = new Set(
capabilities.app_state_snapshot_resync_collections,
);
let previousName = '';
const materializedCollections = manifest.materialized_collections.map(
(entry) => {
const name = entry?.name;
const version = safeJsonInt64(entry?.version, code);
if (
typeof name !== 'string' ||
!required.has(name) ||
name <= previousName
) {
throw new WhatsAppSessionCompatibilityError(code);
}
previousName = name;
return { name, version };
},
);
if (
!capabilities.app_state_snapshot_resync_required &&
materializedCollections.length !== 0
) {
throw new WhatsAppSessionCompatibilityError(code);
}
return {
version: APP_STATE_SNAPSHOT_RESYNC_GATE_VERSION,
handoff_id: String(manifest.handoff_id).toLowerCase(),
lifecycle_operation_id: String(
manifest.lifecycle_operation_id,
).toLowerCase(),
source_provider: manifest.source_provider,
source_revision_id: sourceRevisionId,
target_provider: PROVIDER,
target_revision_id: targetRevisionId,
...capabilities,
source_sync_keys: sourceSyncKeys,
materialized_collections: materializedCollections,
};
};
const normalizeAppStateSnapshotResyncLineage = (scope, requirement) => {
const code =
'whatsapp_session_app_state_snapshot_resync_gate_lineage_invalid';
assertUuid(scope?.handoff_id, code);
assertUuid(scope?.lifecycle_operation_id, code);
if (
!['baileys', 'wwebjs', 'whatsmeow'].includes(scope?.source_provider) ||
scope?.target_provider !== PROVIDER
) {
throw new WhatsAppSessionCompatibilityError(code);
}
const sourceRevisionId = safeJsonInt64(scope.source_revision_id, code);
const targetRevisionId = safeJsonInt64(scope.target_revision_id, code);
if (sourceRevisionId <= 0 || targetRevisionId <= 0) {
throw new WhatsAppSessionCompatibilityError(code);
}
return {
handoff_id: String(scope.handoff_id).toLowerCase(),
lifecycle_operation_id: String(
scope.lifecycle_operation_id,
).toLowerCase(),
source_provider: scope.source_provider,
source_revision_id: sourceRevisionId,
target_provider: PROVIDER,
target_revision_id: targetRevisionId,
...normalizeCanonicalAppStateSnapshotCapabilities(requirement),
};
};
const assertAppStateSnapshotResyncManifestLineage = (manifest, lineage) => {
const normalized = normalizeAppStateSnapshotResyncManifest(manifest);
const expected = normalizeAppStateSnapshotResyncLineage(lineage, lineage);
if (
normalized.handoff_id !== expected.handoff_id ||
normalized.lifecycle_operation_id !== expected.lifecycle_operation_id ||
normalized.source_provider !== expected.source_provider ||
normalized.source_revision_id !== expected.source_revision_id ||
normalized.target_provider !== expected.target_provider ||
normalized.target_revision_id !== expected.target_revision_id ||
normalized.app_state_snapshot_resync_required !==
expected.app_state_snapshot_resync_required ||
JSON.stringify(normalized.app_state_snapshot_resync_collections) !==
JSON.stringify(expected.app_state_snapshot_resync_collections)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_gate_lineage_invalid',
);
}
return normalized;
};
const normalizeFingerprint = (fingerprint) => {
if (fingerprint == null) return undefined;
let normalized;
if (Buffer.isBuffer(fingerprint) || fingerprint instanceof Uint8Array) {
normalized = Buffer.from(fingerprint);
} else if (/^[0-9a-f]{64}$/i.test(String(fingerprint))) {
normalized = Buffer.from(String(fingerprint), 'hex');
} else {
try {
normalized = Buffer.from(String(fingerprint), 'base64');
} catch {
normalized = undefined;
}
}
if (!normalized || normalized.length !== 32) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_companion_fingerprint_invalid',
);
}
return normalized;
};
const fingerprintsEqual = (left, right) => {
if (!left || !right) return false;
const normalizedLeft = Buffer.from(left);
const normalizedRight = Buffer.from(right);
return (
normalizedLeft.length === normalizedRight.length &&
crypto.timingSafeEqual(normalizedLeft, normalizedRight)
);
};
const targetFormat = (provider) => {
switch (provider) {
case 'baileys':
return 'baileys-canonical-v17';
case 'whatsmeow':
return 'whatsmeow-sqlstore-v17';
case 'wwebjs':
return SESSION_FORMAT;
default:
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_target_provider_invalid',
);
}
};
const comparableJid = (jid) => {
const normalized = String(jid || '')
.trim()
.toLowerCase();
if (!normalized) return '';
const separator = normalized.lastIndexOf('@');
const rawUser =
separator === -1 ? normalized : normalized.slice(0, separator);
const rawServer =
separator === -1 ? 's.whatsapp.net' : normalized.slice(separator + 1);
const server = rawServer === 'c.us' ? 's.whatsapp.net' : rawServer;
return `${rawUser.split(':')[0]}@${server}`;
};
const isObjectRecord = (value) =>
value !== null && typeof value === 'object' && !Array.isArray(value);
const throwProjectionCompatibilityError = (code, details) => {
throw new WhatsAppSessionCompatibilityError(code, details);
};
const normalizeProjectionInteger = (value, code) => {
if (!Number.isSafeInteger(value) || value < 0) {
throwProjectionCompatibilityError(code);
}
return value;
};
const providerRecordAddress = (namespace, recordKey) => ({
namespace:
namespace.length <= 100 ? namespace : `_sha256:${sha256(namespace)}`,
recordKey:
recordKey.length <= 500 ? recordKey : `sha256:${sha256(recordKey)}`,
});
const indexedDbStoreKey = (databaseName, storeName) =>
JSON.stringify([databaseName, storeName]);
const parseIndexedDbNamespace = (namespace) => {
const parts = namespace.split(':');
if (parts.length !== 3 || parts[0] !== 'indexeddb') return undefined;
try {
const databaseName = decodeURIComponent(parts[1]);
const storeName = decodeURIComponent(parts[2]);
if (!databaseName || !storeName) return undefined;
return { databaseName, storeName };
} catch {
return undefined;
}
};
const normalizeIndexedDbStores = (stores, code) => {
if (
!Array.isArray(stores) ||
stores.length > MAX_PROVIDER_PROJECTION_RECORDS
) {
throwProjectionCompatibilityError(code);
}
const seen = new Set();
return stores.map((store) => {
if (
!isObjectRecord(store) ||
typeof store.database_name !== 'string' ||
!store.database_name ||
typeof store.store_name !== 'string' ||
!store.store_name
) {
throwProjectionCompatibilityError(code);
}
const keyPath = store.key_path;
if (
keyPath !== undefined &&
keyPath !== null &&
typeof keyPath !== 'string' &&
(!Array.isArray(keyPath) ||
keyPath.some((entry) => typeof entry !== 'string'))
) {
throwProjectionCompatibilityError(code);
}
const key = indexedDbStoreKey(store.database_name, store.store_name);
if (seen.has(key)) {
throwProjectionCompatibilityError(code);
}
seen.add(key);
return keyPath === undefined
? {
database_name: store.database_name,
store_name: store.store_name,
}
: {
database_name: store.database_name,
store_name: store.store_name,
key_path: Array.isArray(keyPath) ? [...keyPath] : keyPath,
};
});
};
const normalizeProjectionRecord = (record, indexedDbStores) => {
let serialized;
let parsed;
try {
serialized = JSON.stringify(record);
parsed = JSON.parse(serialized);
} catch {
throwProjectionCompatibilityError('wwebjs_projection_record_invalid');
}
if (
!isObjectRecord(parsed) ||
typeof parsed.namespace !== 'string' ||
!parsed.namespace ||
typeof parsed.record_key !== 'string'
) {
throwProjectionCompatibilityError('wwebjs_projection_record_invalid');
}
// Lifecycle records are store-owned. A browser projection must never be
// able to forge or overwrite the activation point-of-no-return marker.
if (parsed.namespace === LIFECYCLE_NAMESPACE) {
throwProjectionCompatibilityError(
'wwebjs_projection_lifecycle_namespace_forbidden',
);
}
if (parsed.namespace === 'local_storage') {
if (parsed.value !== null && typeof parsed.value !== 'string') {
throwProjectionCompatibilityError(
'wwebjs_projection_record_invalid',
);
}
} else {
const indexedDbStore = parseIndexedDbNamespace(parsed.namespace);
if (
!indexedDbStore ||
!indexedDbStores.has(
indexedDbStoreKey(
indexedDbStore.databaseName,
indexedDbStore.storeName,
),
)
) {
throwProjectionCompatibilityError(
'wwebjs_projection_record_invalid',
);
}
try {
JSON.parse(parsed.record_key);
} catch {
throwProjectionCompatibilityError(
'wwebjs_projection_record_invalid',
);
}
}
const address = providerRecordAddress(parsed.namespace, parsed.record_key);
if (
address.namespace === PROJECTION_META_NAMESPACE &&
address.recordKey === PROJECTION_META_KEY
) {
throwProjectionCompatibilityError('wwebjs_projection_record_invalid');
}
return {
address,
payload: Buffer.from(serialized, 'utf8'),
record: parsed,
};
};
const normalizeProjection = (
projection,
webVersion,
enforceWebVersion = true,
) => {
if (projection === undefined) return undefined;
if (
!isObjectRecord(projection) ||
projection.schema_version !== BRIDGE_SCHEMA_VERSION ||
typeof projection.web_version !== 'string' ||
!projection.web_version ||
(enforceWebVersion && projection.web_version !== webVersion) ||
!Array.isArray(projection.records)
) {
throwProjectionCompatibilityError('wwebjs_projection_incompatible');
}
if (
typeof projection.complete !== 'boolean' ||
projection.records.length > MAX_PROVIDER_PROJECTION_RECORDS
) {
throwProjectionCompatibilityError('wwebjs_projection_incompatible');
}
const lossyRecords =
projection.lossy_records === undefined
? projection.complete
? 0
: 1
: normalizeProjectionInteger(
projection.lossy_records,
'wwebjs_projection_incompatible',
);
const declaredSizeBytes =
projection.size_bytes === undefined
? undefined
: normalizeProjectionInteger(
projection.size_bytes,
'wwebjs_projection_incompatible',
);
if (
lossyRecords > MAX_PROVIDER_PROJECTION_RECORDS ||
(declaredSizeBytes !== undefined &&
declaredSizeBytes > MAX_PROVIDER_PROJECTION_BYTES) ||
projection.complete !== (lossyRecords === 0)
) {
throwProjectionCompatibilityError('wwebjs_projection_incompatible');
}
const stores = normalizeIndexedDbStores(
projection.indexeddb_stores,
'wwebjs_projection_incompatible',
);
const indexedDbStores = new Set(
stores.map((store) =>
indexedDbStoreKey(store.database_name, store.store_name),
),
);
let totalBytes = 0;
const addresses = new Set();
const normalizedRecords = projection.records.map((record) => {
const normalized = normalizeProjectionRecord(record, indexedDbStores);
totalBytes += normalized.payload.length;
if (totalBytes > MAX_PROVIDER_PROJECTION_BYTES) {
throwProjectionCompatibilityError(
'wwebjs_projection_limit_exceeded',
);
}
const key = `${normalized.address.namespace}\u0000${normalized.address.recordKey}`;
if (addresses.has(key)) {
throwProjectionCompatibilityError(
'wwebjs_projection_record_invalid',
);
}
addresses.add(key);
return normalized;
});
const sizeBytes = declaredSizeBytes ?? totalBytes;
if (sizeBytes !== totalBytes) {
throwProjectionCompatibilityError('wwebjs_projection_incompatible');
}
const metadata = Buffer.from(
JSON.stringify({
schema_version: BRIDGE_SCHEMA_VERSION,
web_version: projection.web_version,
complete: projection.complete,
lossy_records: lossyRecords,
size_bytes: sizeBytes,
indexeddb_stores: stores,
}),
'utf8',
);
if (totalBytes + metadata.length > MAX_PROVIDER_PROJECTION_BYTES) {
throwProjectionCompatibilityError('wwebjs_projection_limit_exceeded');
}
return {
projection: {
schema_version: BRIDGE_SCHEMA_VERSION,
web_version: projection.web_version,
complete: projection.complete,
lossy_records: lossyRecords,
size_bytes: sizeBytes,
indexeddb_stores: stores,
records: normalizedRecords.map((record) => record.record),
},
records: [
...normalizedRecords.map(({ address, payload }) => ({
namespace: address.namespace,
recordKey: address.recordKey,
payload,
})),
{
namespace: PROJECTION_META_NAMESPACE,
recordKey: PROJECTION_META_KEY,
payload: metadata,
},
],
};
};
const parseProjectionPayload = (payload, code) => {
if (!Buffer.isBuffer(payload) && !(payload instanceof Uint8Array)) {
throwProjectionCompatibilityError(code);
}
const bytes = Buffer.from(payload);
try {
return {
bytes,
value: JSON.parse(bytes.toString('utf8')),
};
} catch {
throwProjectionCompatibilityError(code);
}
};
const normalizeProjectionMetadata = (
metadata,
webVersion,
enforceWebVersion = true,
) => {
if (!isObjectRecord(metadata)) {
throwProjectionCompatibilityError('wwebjs_projection_metadata_invalid');
}
if (
metadata.schema_version !== BRIDGE_SCHEMA_VERSION ||
typeof metadata.web_version !== 'string' ||
!metadata.web_version ||
(enforceWebVersion && metadata.web_version !== webVersion)
) {
throwProjectionCompatibilityError('wwebjs_projection_incompatible', {
expected_schema_version: BRIDGE_SCHEMA_VERSION,
actual_schema_version: metadata.schema_version,
expected_web_version: webVersion,
actual_web_version: metadata.web_version,
});
}
if (typeof metadata.complete !== 'boolean') {
throwProjectionCompatibilityError('wwebjs_projection_metadata_invalid');
}
const lossyRecords = normalizeProjectionInteger(
metadata.lossy_records,
'wwebjs_projection_metadata_invalid',
);
const sizeBytes = normalizeProjectionInteger(
metadata.size_bytes,
'wwebjs_projection_metadata_invalid',
);
if (
lossyRecords > MAX_PROVIDER_PROJECTION_RECORDS ||
sizeBytes > MAX_PROVIDER_PROJECTION_BYTES ||
metadata.complete !== (lossyRecords === 0)
) {
throwProjectionCompatibilityError('wwebjs_projection_metadata_invalid');
}
return {
web_version: metadata.web_version,
complete: metadata.complete,
lossy_records: lossyRecords,
size_bytes: sizeBytes,
indexeddb_stores: normalizeIndexedDbStores(
metadata.indexeddb_stores,
'wwebjs_projection_metadata_invalid',
),
};
};
const normalizeImportedIdentity = (identity) => {
if (
!isObjectRecord(identity) ||
typeof identity.jid !== 'string' ||
!identity.jid.trim() ||
identity.device_fingerprint == null
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_import_identity_missing',
);
}
return {
jid: identity.jid.trim(),
device_fingerprint: normalizeFingerprint(identity.device_fingerprint),
};
};
const normalizePassthroughTables = (tables) => {
if (tables === undefined) return undefined;
if (!isObjectRecord(tables)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_passthrough_invalid',
);
}
const knownTables = new Set(
PASSTHROUGH_TABLE_CONFIGS.map(({ table }) => table),
);
if (Object.keys(tables).some((table) => !knownTables.has(table))) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_passthrough_invalid',
);
}
let rowCount = 0;
let byteCount = 0;
const normalized = {};
for (const config of PASSTHROUGH_TABLE_CONFIGS) {
const rows = tables[config.table] || [];
if (!Array.isArray(rows)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_passthrough_invalid',
);
}
const keys = new Set();
normalized[config.table] = rows.map((row) => {
if (!isObjectRecord(row)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_passthrough_invalid',
);
}
const output = {};
for (const column of config.columns) {
const value = row[column];
if (config.byteColumns?.includes(column)) {
if (
value == null &&
config.nullableByteColumns?.includes(column)
) {
output[column] = null;
continue;
}
if (
!Buffer.isBuffer(value) &&
!(value instanceof Uint8Array)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_passthrough_invalid',
);
}
output[column] = Buffer.from(value);
byteCount += output[column].length;
continue;
}
if (
value !== null &&
!['string', 'number', 'bigint', 'boolean'].includes(
typeof value,
)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_passthrough_invalid',
);
}
output[column] = value;
if (typeof value === 'string') {
byteCount += Buffer.byteLength(value);
}
}
if (
config.table === 'whatsapp_event_buffer' &&
output.ciphertext_hash.length !== 32
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_passthrough_invalid',
);
}
const key = String(config.key(output));
if (keys.has(key)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_passthrough_duplicate',
);
}
keys.add(key);
rowCount += 1;
if (
rowCount > MAX_HANDOFF_SOURCE_ROWS ||
byteCount > DEFAULT_MAX_CANONICAL_BYTES
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_passthrough_limit_exceeded',
);
}
return output;
});
if (config.table === 'whatsapp_lid_map') {
const phoneNumbers = new Set();
for (const row of normalized[config.table]) {
if (phoneNumbers.has(row.pn)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_passthrough_duplicate',
);
}
phoneNumbers.add(row.pn);
}
}
}
return { tables: normalized, rowCount, byteCount };
};
const normalizeStoreCanonicalProjection = (projection, webVersion) => {
const passthrough = normalizePassthroughTables(
projection?.passthrough_tables,
);
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 normalized = canonicalBrowserProjectionToStore(
canonicalStoreProjectionToBrowser(projection, { webVersion }),
);
normalized.capabilities = {
...normalized.capabilities,
...normalizeCanonicalAppStateSnapshotCapabilities(
normalized.capabilities,
{ allowMissing: true },
),
};
if (nonImportableSignalSessions.length > 0) {
normalized.tables.whatsapp_signal_sessions.push(
...nonImportableSignalSessions,
);
normalized.record_count += nonImportableSignalSessions.length;
}
if (passthrough !== undefined) {
if (
normalized.record_count + passthrough.rowCount >
MAX_HANDOFF_SOURCE_ROWS ||
normalized.size_bytes + passthrough.byteCount >
DEFAULT_MAX_CANONICAL_BYTES
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_projection_limit_exceeded',
);
}
normalized.passthrough_tables = passthrough.tables;
}
return normalized;
};
const normalizeCanonicalDirtyPlan = (plan) => {
if (plan === undefined) {
return {
fullReplace: true,
device: true,
tables: CANONICAL_TABLE_CONFIGS.map(({ table }) => table),
};
}
if (
!isObjectRecord(plan) ||
typeof plan.fullReplace !== 'boolean' ||
typeof plan.device !== 'boolean' ||
!Array.isArray(plan.tables) ||
plan.tables.some((table) => !CANONICAL_TABLE_NAMES.has(table))
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_dirty_plan_invalid',
);
}
const fullReplace = plan.fullReplace;
const tables = fullReplace
? CANONICAL_TABLE_CONFIGS.map(({ table }) => table)
: [...new Set(plan.tables)];
if (
tables.includes('whatsapp_pq_pre_keys') ||
tables.includes('whatsapp_pq_pre_key_state')
) {
// Key rows and allocator/migration/server-count state have cross-table
// invariants. Never allow a caller's selective journal plan to commit
// one side without the other.
for (const pqTable of [
'whatsapp_pq_pre_keys',
'whatsapp_pq_pre_key_state',
]) {
if (!tables.includes(pqTable)) tables.push(pqTable);
}
}
const appStateTables = [
'whatsapp_app_state_sync_keys',
'whatsapp_app_state_version',
'whatsapp_app_state_mutation_macs',
];
if (appStateTables.some((table) => tables.includes(table))) {
// Sync-key rotation, collection watermarks and mutation MACs form one
// recoverable snapshot. A selective journal flush must not expose a
// key from one generation with collection state from another.
for (const appStateTable of appStateTables) {
if (!tables.includes(appStateTable)) tables.push(appStateTable);
}
}
return {
fullReplace,
device: fullReplace || plan.device,
tables,
};
};
const encodeCanonicalProviderState = (state) => {
if (!state) return undefined;
const fingerprint = Buffer.from(state.device_fingerprint);
const noisePublicKey = Buffer.from(state.noise_public_key);
const recoveryToken = Buffer.from(state.recovery_token);
const certificateChain = state.certificate_chain
? Buffer.from(state.certificate_chain)
: Buffer.alloc(0);
if (
state.kind !== 'wwebjs-noise-bootstrap-v1' ||
state.codec_version !== 1 ||
fingerprint.length !== 32 ||
noisePublicKey.length !== 32 ||
recoveryToken.length !== 24 ||
certificateChain.length > MAX_PROVIDER_PROJECTION_BYTES
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_provider_state_invalid',
);
}
const payload = Buffer.allocUnsafe(93 + certificateChain.length);
payload.writeUInt8(1, 0);
fingerprint.copy(payload, 1);
noisePublicKey.copy(payload, 33);
recoveryToken.copy(payload, 65);
payload.writeUInt32BE(certificateChain.length, 89);
certificateChain.copy(payload, 93);
return payload;
};
const decodeCanonicalProviderState = (payload) => {
const bytes = Buffer.from(payload || []);
if (bytes.length < 93 || bytes.readUInt8(0) !== 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_provider_state_invalid',
);
}
const certificateLength = bytes.readUInt32BE(89);
if (bytes.length !== 93 + certificateLength) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_provider_state_invalid',
);
}
return {
kind: 'wwebjs-noise-bootstrap-v1',
codec_version: 1,
device_fingerprint: Buffer.from(bytes.subarray(1, 33)),
noise_public_key: Buffer.from(bytes.subarray(33, 65)),
recovery_token: Buffer.from(bytes.subarray(65, 89)),
certificate_chain:
certificateLength === 0 ? null : Buffer.from(bytes.subarray(93)),
};
};
const normalizeCanonicalRoutingInfo = (value) => {
const bytes = Buffer.from(value || []);
if (bytes.length < 1 || bytes.length > MAX_ROUTING_INFO_BYTES) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_routing_info_invalid',
);
}
return bytes;
};
const canonicalSignalCapabilitiesFromHandoffArtifact = (manifest, scope) => {
const scopes = manifest?.canonical_signal_session_scopes;
const resyncScopes = manifest?.canonical_signal_session_resync_scopes;
const sourceProvider = scope?.source_provider;
const carriesBaileysScopes =
sourceProvider === 'baileys' &&
Array.isArray(scopes) &&
JSON.stringify(scopes) ===
JSON.stringify(['default', 'status', 'pq']) &&
manifest?.canonical_signal_session_resync_required === false &&
Array.isArray(resyncScopes) &&
resyncScopes.length === 0;
const carriesBaileysClassicalRollback =
sourceProvider === 'baileys' &&
Array.isArray(scopes) &&
JSON.stringify(scopes) === JSON.stringify(['default']) &&
manifest?.canonical_signal_session_resync_required === true &&
Array.isArray(resyncScopes) &&
JSON.stringify(resyncScopes) === JSON.stringify(['status', 'pq']) &&
manifest?.pq_server_rollback_acknowledged === true &&
manifest?.pq_server_rollback_protocol === PQ_SERVER_ROLLBACK_PROTOCOL &&
Number.isSafeInteger(manifest?.pq_server_rollback_acknowledged_at_ms) &&
manifest.pq_server_rollback_acknowledged_at_ms > 0 &&
String(manifest?.pq_server_rollback_source_revision_id || '') ===
String(scope?.source_revision_id || '') &&
manifest?.pq_upload_lifecycle_fenced === true &&
manifest?.pq_upload_lifecycle_fence_version === 1;
const carriesWhatsMeowScopes =
sourceProvider === 'whatsmeow' &&
Array.isArray(scopes) &&
JSON.stringify(scopes) === JSON.stringify(['default']) &&
manifest?.canonical_signal_session_resync_required === true &&
Array.isArray(resyncScopes) &&
JSON.stringify(resyncScopes) === JSON.stringify(['status', 'pq']);
if (
!isObjectRecord(manifest) ||
manifest.handoff_id !== scope?.handoff_id ||
manifest.lifecycle_operation_id !== scope?.lifecycle_operation_id ||
(!carriesBaileysScopes &&
!carriesBaileysClassicalRollback &&
!carriesWhatsMeowScopes)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_handoff_signal_capability_invalid',
);
}
const appStateCapabilities =
normalizeCanonicalAppStateSnapshotCapabilities(manifest);
const capabilities = {
canonical_signal_session_scopes: [...scopes],
canonical_signal_session_resync_required:
manifest.canonical_signal_session_resync_required,
canonical_signal_session_resync_scopes: [...resyncScopes],
...appStateCapabilities,
};
Object.defineProperty(
capabilities,
'baileysClassicalPostQuantumRollbackAcknowledged',
{
configurable: false,
enumerable: false,
value: carriesBaileysClassicalRollback,
writable: false,
},
);
return capabilities;
};
const canonicalProjectionFromSnapshot = (snapshot, webVersion) => {
const canonicalRecords = snapshot.providerRecords.filter(
(row) => row.namespace === CANONICAL_META_NAMESPACE,
);
const transportRecords = snapshot.providerRecords.filter(
(row) => row.namespace === CANONICAL_TRANSPORT_NAMESPACE,
);
if (
canonicalRecords.some(
(row) =>
Number(row.codec_version) !== 1 ||
![CANONICAL_META_KEY, CANONICAL_PROVIDER_STATE_KEY].includes(
row.record_key,
),
) ||
transportRecords.some(
(row) =>
Number(row.codec_version) !== 1 ||
row.record_key !== CANONICAL_ROUTING_INFO_KEY,
)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_provider_record_incompatible',
);
}
const metadataRows = canonicalRecords.filter(
(row) => row.record_key === CANONICAL_META_KEY,
);
if (metadataRows.length > 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_metadata_invalid',
);
}
let metadata;
if (metadataRows.length === 1) {
try {
metadata = JSON.parse(
Buffer.from(metadataRows[0].payload).toString('utf8'),
);
} catch {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_metadata_invalid',
);
}
if (
Number(metadata.schema_version) !==
CANONICAL_SESSION_SCHEMA_VERSION ||
metadata.codec_kind !== CANONICAL_SESSION_CODEC_KIND ||
Number(metadata.codec_version) !==
CANONICAL_SESSION_CODEC_VERSION ||
metadata.module_abi !== CANONICAL_PRIVATE_MODULE_ABI ||
metadata.fingerprint_version !== CANONICAL_FINGERPRINT_VERSION
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_metadata_incompatible',
);
}
}
const providerStateRows = canonicalRecords.filter(
(row) => row.record_key === CANONICAL_PROVIDER_STATE_KEY,
);
if (providerStateRows.length > 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_provider_state_invalid',
);
}
const providerState = providerStateRows[0]
? decodeCanonicalProviderState(providerStateRows[0].payload)
: null;
if (transportRecords.length > 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_routing_info_invalid',
);
}
const transportState = transportRecords[0]
? {
kind: 'whatsapp-transport-v1',
codec_version: 1,
routing_info: normalizeCanonicalRoutingInfo(
transportRecords[0].payload,
),
}
: null;
const device = snapshot.device;
const projection = {
schema_version: CANONICAL_SESSION_SCHEMA_VERSION,
codec_kind: CANONICAL_SESSION_CODEC_KIND,
codec_version: CANONICAL_SESSION_CODEC_VERSION,
module_abi: CANONICAL_PRIVATE_MODULE_ABI,
web_version: metadata?.web_version || webVersion,
fingerprint_version: CANONICAL_FINGERPRINT_VERSION,
complete: true,
blockers: [],
capabilities: {
...(metadata?.capabilities || {
adv_secret_available: device.adv_secret_available,
passkey_handoff_supported: device.adv_secret_available,
signal_scopes: ['default', 'status', 'pq'],
signal_domain_types: [0, 1, 128, 129],
app_state_import_mode: 'snapshot_resync',
canonical_signal_session_scopes: ['default', 'status', 'pq'],
canonical_signal_session_resync_required: false,
canonical_signal_session_resync_scopes: [],
}),
...(snapshot.canonicalCapabilities || {}),
lid_migrated:
Boolean(device.lid) &&
(Number(device.lid_migration_ts || 0) > 0 ||
(snapshot.passthroughTables?.whatsapp_lid_map?.length ||
0) > 0),
},
device: {
jid: device.jid,
lid: device.lid,
facebook_uuid: device.facebook_uuid,
registration_id: Number(device.registration_id),
noise_key: device.noise_key,
identity_key: device.identity_key,
signed_pre_key: device.signed_pre_key,
signed_pre_key_id: Number(device.signed_pre_key_id),
signed_pre_key_sig: device.signed_pre_key_sig,
adv_secret_available: device.adv_secret_available,
adv_key: device.adv_key,
adv_details: device.adv_details,
adv_account_sig: device.adv_account_sig,
adv_account_sig_key: device.adv_account_sig_key,
adv_device_sig: device.adv_device_sig,
platform: device.platform,
business_name: device.business_name,
push_name: device.push_name,
lid_migration_ts: Number(device.lid_migration_ts || 0),
next_pre_key_id: Number(device.next_pre_key_id),
device_fingerprint: device.device_fingerprint,
fingerprint_version: device.fingerprint_version,
},
provider_state: providerState,
transport_state: transportState,
tables: {
whatsapp_identity_keys: snapshot.identityKeys,
whatsapp_pre_keys: snapshot.preKeys.map((row) => ({
...row,
key_id: Number(row.key_id),
})),
whatsapp_pq_pre_keys: snapshot.pqPreKeys.map((row) => ({
...row,
key_id: Number(row.key_id),
timestamp_ms: Number(row.timestamp_ms),
})),
whatsapp_pq_pre_key_state: snapshot.pqPreKeyState.map((row) => ({
...row,
codec_version: Number(row.codec_version),
next_pre_key_id: Number(row.next_pre_key_id),
last_server_count:
row.last_server_count === null
? null
: Number(row.last_server_count),
last_server_count_timestamp_ms:
row.last_server_count_timestamp_ms === null
? null
: Number(row.last_server_count_timestamp_ms),
})),
whatsapp_signal_sessions: snapshot.signalSessions,
whatsapp_sender_keys: snapshot.senderKeys,
whatsapp_app_state_sync_keys: snapshot.syncKeys.map((row) => ({
...row,
timestamp: Number(row.timestamp),
})),
whatsapp_app_state_version: snapshot.versions.map((row) => ({
...row,
version: Number(row.version),
})),
whatsapp_app_state_mutation_macs: snapshot.mutationMacs.map(
(row) => ({
...row,
version: Number(row.version),
}),
),
},
};
if (snapshot.passthroughTables !== undefined) {
projection.passthrough_tables = snapshot.passthroughTables;
}
// Once the metadata row exists, its web_version is part of the durable
// canonical bytes and therefore of the typed anchor checksum. A worker
// starts with the bundled fallback version and binds the actual runtime
// only after Chromium opens; overriding the persisted version here would
// make a healthy checkpoint unverifiable during that pre-browser restart
// window. Legacy snapshots without metadata already received webVersion
// above, so preserving projection.web_version is safe for both paths.
return normalizeStoreCanonicalProjection(projection);
};
class BlobBatchUploader {
constructor(store, batchSize = 16) {
this.store = store;
this.batchSize = batchSize;
this.pending = new Map();
}
async add(blob) {
this.pending.set(blob.sha256, blob);
if (this.pending.size >= this.batchSize) await this.flush();
}
async flush() {
if (this.pending.size === 0) return;
const blobs = [...this.pending.values()];
this.pending = new Map();
await this.store.insertArtifactBlobs(blobs);
}
}
class PostgresSessionStore {
constructor({
pool,
connectionString,
sessionId,
revisionId,
ownerId,
generation,
epoch,
runtimeCapability,
revisionSource = 'pairing',
storageMigrationId,
webVersion = SupportedWebVersion,
webVersionIntegrityPinned = false,
leaseTtlMs = DEFAULT_LEASE_TTL_MS,
leaseRenewIntervalMs = DEFAULT_LEASE_RENEW_MS,
leaseSafetyMarginMs = DEFAULT_LEASE_SAFETY_MS,
chunkBytes = DEFAULT_CHUNK_BYTES,
maxProfileBytes = DEFAULT_MAX_PROFILE_BYTES,
debugEnabled,
logger,
} = {}) {
assertUuid(sessionId, 'whatsapp_session_id_invalid');
assertUuid(epoch, 'whatsapp_session_epoch_invalid');
assertUuid(ownerId, 'whatsapp_session_owner_id_invalid');
if (!Number.isSafeInteger(generation) || generation <= 0) {
throw new WhatsAppSessionError(
'whatsapp_session_generation_invalid',
);
}
if (
typeof webVersion !== 'string' ||
webVersion.length === 0 ||
webVersion.length > 100
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_web_version_unsupported',
{ actual: webVersion },
);
}
if (
!Number.isSafeInteger(leaseTtlMs) ||
!Number.isSafeInteger(leaseRenewIntervalMs) ||
!Number.isSafeInteger(leaseSafetyMarginMs) ||
leaseTtlMs <= leaseRenewIntervalMs + leaseSafetyMarginMs
) {
throw new WhatsAppSessionError(
'whatsapp_session_lease_timing_invalid',
);
}
if (
!Number.isSafeInteger(chunkBytes) ||
chunkBytes <= 0 ||
chunkBytes > DEFAULT_CHUNK_BYTES ||
!Number.isSafeInteger(maxProfileBytes) ||
maxProfileBytes <= 0 ||
maxProfileBytes > MAX_DATABASE_ARTIFACT_BYTES ||
chunkBytes > maxProfileBytes
) {
throw new WhatsAppSessionError(
'whatsapp_session_artifact_limits_invalid',
);
}
if (!pool && !connectionString) {
throw new WhatsAppSessionError(
'whatsapp_session_postgres_pool_required',
);
}
if (!pool) {
const { Pool } = require('pg');
this.pool = new Pool({ connectionString });
this.ownsPool = true;
} else {
this.pool = pool;
this.ownsPool = false;
}
Object.defineProperty(this, 'pool', {
value: this.pool,
enumerable: false,
configurable: false,
writable: false,
});
this.kind = SESSION_STORE_KIND;
this.provider = PROVIDER;
this.sessionId = sessionId;
this.revisionId = normalizeRevisionId(revisionId);
this.ownerId = ownerId;
this.generation = generation;
this.epoch = epoch;
if (typeof runtimeCapability !== 'string') {
throw new WhatsAppSessionError(
'whatsapp_session_runtime_capability_required',
);
}
if (runtimeCapability.length < 32 || runtimeCapability.length > 512) {
throw new WhatsAppSessionError(
'whatsapp_session_runtime_capability_invalid',
);
}
Object.defineProperty(this, 'runtimeCapability', {
value: runtimeCapability,
enumerable: false,
configurable: false,
writable: false,
});
if (!['pairing', 'legacy_volume_migration'].includes(revisionSource)) {
throw new WhatsAppSessionError(
'whatsapp_session_revision_source_invalid',
);
}
if (storageMigrationId !== undefined) {
assertUuid(
storageMigrationId,
'whatsapp_session_storage_migration_id_invalid',
);
}
if (
(revisionSource === 'legacy_volume_migration') !==
Boolean(storageMigrationId)
) {
throw new WhatsAppSessionError(
'whatsapp_session_storage_migration_scope_invalid',
);
}
this.requestedRevisionSource = revisionSource;
this.storageMigrationId = storageMigrationId;
this.webVersion = webVersion;
this.webVersionIntegrityPinned = webVersionIntegrityPinned === true;
this.leaseTtlMs = leaseTtlMs;
this.leaseRenewIntervalMs = leaseRenewIntervalMs;
this.leaseSafetyMarginMs = leaseSafetyMarginMs;
this.chunkBytes = chunkBytes;
this.maxProfileBytes = maxProfileBytes;
this.logger =
logger ||
new WhatsAppSessionDebugLogger({
enabled: debugEnabled,
sessionId,
provider: PROVIDER,
revisionId: this.revisionId,
generation,
epoch,
});
this.closed = false;
this.opened = false;
this.leaseReleasePending = false;
this.renewSuccessCount = 0;
this.renewFailureCount = 0;
this.leaseDeadlineTimer = undefined;
this.appStateSnapshotResyncGateArtifactId = undefined;
this.reusableHandoffProfileAuthority = undefined;
this.secureImportRollbackContext = undefined;
}
log(event, fields, options) {
try {
this.logger?.log?.(event, fields, options);
} catch {
// Diagnostics must never change lease or persistence semantics.
}
}
updateLoggerContext(fields) {
try {
Object.assign(this.logger, fields);
} catch {
// An injected logger is diagnostic-only, including its context.
}
}
bindRuntimeWebVersion(webVersion, moduleAbi) {
if (
typeof webVersion !== 'string' ||
!webVersion ||
webVersion.length > 100 ||
moduleAbi !== CANONICAL_PRIVATE_MODULE_ABI
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_runtime_module_abi_incompatible',
);
}
if (this.webVersionIntegrityPinned && webVersion !== this.webVersion) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_web_version_incompatible',
{ expected: this.webVersion, actual: webVersion },
);
}
this.webVersion = webVersion;
this.log('runtime.web_version_bound', {
web_version: webVersion,
module_abi: moduleAbi,
integrity_pinned: this.webVersionIntegrityPinned,
});
return webVersion;
}
async open({ onLeaseLost } = {}) {
if (this.leaseReleasePending) {
throw new WhatsAppSessionError(
'whatsapp_session_lease_release_pending',
);
}
if (this.closed) {
throw new WhatsAppSessionError('whatsapp_session_store_closed');
}
this.onLeaseLost = onLeaseLost;
if (this.opened && this.fencingToken) return this.describe();
await this.acquireLease();
try {
await this.resolveRevision();
this.opened = true;
this.startLeaseRenewal();
await this.hydrateAuthorizedHandoff();
} catch (error) {
this.log(
'session.open_failed',
{
stage: this.revisionId
? 'revision_context_or_hydration'
: 'revision_open',
error,
},
{ force: true },
);
this.opened = false;
clearInterval(this.leaseInterval);
clearInterval(this.leaseSummaryInterval);
await this.releaseLease().catch(() => {});
throw error;
}
this.updateLoggerContext({ revisionId: this.revisionId });
this.log('session.opened', this.describe(), { force: true });
return this.describe();
}
describe() {
return {
session_id: this.sessionId,
revision_id: this.revisionId,
revision_status: this.revisionStatus,
revision_source: this.revisionSource,
handoff_id: this.handoffId,
lifecycle_operation_id: this.lifecycleOperationId,
source_revision_id: this.sourceRevisionId,
source_provider: this.sourceProvider,
provider: PROVIDER,
generation: this.generation,
epoch: this.epoch,
fencing_token: this.fencingToken,
web_version: this.webVersion,
};
}
async acquireLease() {
this.leaseLost = false;
this.leaseReleasePending = false;
const result = await this.pool.query(
`SELECT fencing_token::text AS fencing_token,
remaining_ms::text AS remaining_ms
FROM acquire_whatsapp_session_lease(
$1, $2, 'wwebjs', $3, $4, $5, $6
)`,
[
this.sessionId,
this.ownerId,
this.generation,
this.epoch,
this.leaseTtlMs,
this.runtimeCapability,
],
);
if (result.rowCount !== 1) {
throw new WhatsAppSessionError('whatsapp_session_lease_contended');
}
this.fencingToken = result.rows[0].fencing_token;
this.updateLoggerContext({ fencingToken: this.fencingToken });
this.updateLocalLeaseDeadline(result.rows[0].remaining_ms);
this.log(
'lease.acquired',
{ fencing_token: this.fencingToken },
{ force: true },
);
}
updateLocalLeaseDeadline(remainingMs) {
const remaining = Number(remainingMs);
this.localLeaseDeadline =
Date.now() +
(Number.isFinite(remaining) ? remaining : this.leaseTtlMs) -
this.leaseSafetyMarginMs;
clearTimeout(this.leaseDeadlineTimer);
const delay = Math.max(0, this.localLeaseDeadline - Date.now());
const expireLease = () => {
if (this.leaseLost || this.leaseReleasePending) return;
const remainingDelay = this.localLeaseDeadline - Date.now();
if (remainingDelay > 0) {
// Node timers may fire slightly before their requested wall
// clock deadline. Re-arm for the remainder instead of losing
// the only local fencing alarm.
this.leaseDeadlineTimer = setTimeout(
expireLease,
remainingDelay,
);
this.leaseDeadlineTimer.unref?.();
return;
}
this.handleLeaseLoss(
new WhatsAppSessionLeaseLostError(this.describe()),
).catch((error) => {
this.log(
'lease.deadline_handler_failed',
{ error },
{ force: true },
);
});
};
this.leaseDeadlineTimer = setTimeout(expireLease, delay);
this.leaseDeadlineTimer.unref?.();
}
startLeaseRenewal() {
this.leaseInterval = setInterval(() => {
if (this.renewInFlight) return;
const renewal = this.renewLease()
.catch(async (error) => {
await this.handleLeaseRenewalFailure(error);
})
.finally(() => {
if (this.renewInFlight === renewal) {
this.renewInFlight = undefined;
}
});
this.renewInFlight = renewal;
}, this.leaseRenewIntervalMs);
this.leaseSummaryInterval = setInterval(() => {
this.log('lease.renew_summary', {
success_count: this.renewSuccessCount,
failure_count: this.renewFailureCount,
});
this.renewSuccessCount = 0;
this.renewFailureCount = 0;
}, 60000);
}
async renewLease() {
this.assertLocalLease();
const result = await this.pool.query({
text: `SELECT fencing_token::text AS fencing_token,
remaining_ms::text AS remaining_ms
FROM renew_whatsapp_session_lease(
$1, $2, 'wwebjs', $3::bigint, $4, $5, $6, $7
)`,
values: [
this.sessionId,
this.ownerId,
this.fencingToken,
this.generation,
this.epoch,
this.leaseTtlMs,
this.runtimeCapability,
],
query_timeout: Math.min(this.leaseRenewIntervalMs, 5000),
});
if (this.leaseLost || this.leaseReleasePending) {
throw new WhatsAppSessionLeaseLostError(this.describe());
}
if (result.rowCount !== 1) {
throw new WhatsAppSessionLeaseLostError(this.describe());
}
this.renewSuccessCount += 1;
this.updateLocalLeaseDeadline(result.rows[0].remaining_ms);
}
async handleLeaseRenewalFailure(error) {
this.renewFailureCount += 1;
if (
error instanceof WhatsAppSessionLeaseLostError ||
isDefinitiveLeaseRejection(error) ||
Date.now() >= this.localLeaseDeadline
) {
await this.handleLeaseLoss(error).catch((callbackError) => {
this.log(
'lease.loss_handler_failed',
{ error: callbackError },
{ force: true },
);
});
return;
}
this.log(
'lease.renew_failed',
{ error: leaseRenewalErrorForLog(error) },
{ force: true },
);
}
assertLocalLease() {
if (
this.leaseLost ||
this.leaseReleasePending ||
!this.fencingToken ||
Date.now() >= this.localLeaseDeadline
) {
throw new WhatsAppSessionLeaseLostError(this.describe());
}
}
async handleLeaseLoss(error) {
if (this.leaseLost) return;
this.leaseLost = true;
this.opened = false;
clearTimeout(this.leaseDeadlineTimer);
clearInterval(this.leaseInterval);
clearInterval(this.leaseSummaryInterval);
const leaseError =
error instanceof WhatsAppSessionLeaseLostError
? error
: new WhatsAppSessionLeaseLostError(this.describe(), {
cause: error,
});
this.log('lease.lost', { error: leaseError }, { force: true });
await this.onLeaseLost?.(leaseError);
}
async validateLease(client, revisionId = this.revisionId) {
return this.validateLeaseScope(client, revisionId, false);
}
async validateMutationLease(client, revisionId = this.revisionId) {
return this.validateLeaseScope(client, revisionId, true);
}
async validateLeaseScope(client, revisionId, mutation) {
this.assertLocalLease();
const sql = mutation
? `SELECT begin_whatsapp_session_mutation(
$1, $2::bigint, $3, $4::bigint, $5, $6, $7, $8
) AS allowed`
: `SELECT begin_whatsapp_session_operation(
$1, $2::bigint, $3, $4::bigint, $5, $6, $7, $8
) AS allowed`;
const result = await client.query(sql, [
this.sessionId,
revisionId ?? null,
this.ownerId,
this.fencingToken,
this.generation,
this.epoch,
this.runtimeCapability,
PROFILE_ANCHOR_OPERATION_ABI,
]);
if (result.rows[0]?.allowed !== true) {
throw new WhatsAppSessionLeaseLostError(this.describe());
}
}
async withLeaseTransaction(operation, revisionId = this.revisionId) {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
await this.validateLease(client, revisionId);
const result = await operation(client);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK').catch(() => {});
if (error instanceof WhatsAppSessionLeaseLostError) {
await this.handleLeaseLoss(error);
}
throw error;
} finally {
client.release();
}
}
async withLeaseMutation(operation, revisionId = this.revisionId) {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
await this.validateMutationLease(client, revisionId);
const result = await operation(client);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK').catch(() => {});
if (error instanceof WhatsAppSessionLeaseLostError) {
await this.handleLeaseLoss(error);
}
throw error;
} finally {
client.release();
}
}
async withLifecycleTransaction(operation) {
const client = await this.pool.connect();
try {
// Lifecycle functions validate the lease/fence themselves after
// locking worker first. Keep this transaction free from
// begin_whatsapp_session_{operation,mutation}; otherwise a client
// would lock session before worker and invert the control-plane
// lock order.
this.assertLocalLease();
await client.query('BEGIN');
const result = await operation(client);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK').catch(() => {});
if (error instanceof WhatsAppSessionLeaseLostError) {
await this.handleLeaseLoss(error);
}
throw error;
} finally {
client.release();
}
}
async resolveRevision() {
if (!this.revisionId) {
const previousContext = {
revisionId: this.revisionId,
revisionStatus: this.revisionStatus,
revisionSource: this.revisionSource,
sourceRevisionId: this.sourceRevisionId,
sourceProvider: this.sourceProvider,
handoffId: this.handoffId,
lifecycleOperationId: this.lifecycleOperationId,
};
const client = await this.pool.connect();
try {
await client.query('BEGIN');
if (this.storageMigrationId) {
await client.query(
`SELECT set_config(
'app.whatsapp_storage_migration_id', $1, true
)`,
[this.storageMigrationId],
);
}
const opened = await client.query(
`SELECT revision_id::text AS revision_id, status, handoff_id
FROM open_whatsapp_session_revision(
$1, $2, 'wwebjs', $3::bigint, $4, $5, $6,
$7, $8, $9, $10
)`,
[
this.sessionId,
this.ownerId,
this.fencingToken,
this.generation,
this.epoch,
this.runtimeCapability,
this.requestedRevisionSource,
SESSION_SCHEMA_VERSION,
SESSION_CODEC_VERSION,
SESSION_FORMAT,
],
);
if (opened.rowCount !== 1) {
throw new WhatsAppSessionError(
'whatsapp_session_revision_missing',
);
}
this.revisionId = opened.rows[0].revision_id;
this.handoffId = opened.rows[0].handoff_id || undefined;
await this.loadRevisionContext(client);
await client.query('COMMIT');
return;
} catch (error) {
await client.query('ROLLBACK').catch(() => {});
// The opened revision and its context are transactional. Never
// retain an identifier whose INSERT may just have rolled back;
// otherwise the same Store retries forever through the
// explicit-revision branch with a phantom revision.
this.revisionId = previousContext.revisionId;
this.revisionStatus = previousContext.revisionStatus;
this.revisionSource = previousContext.revisionSource;
this.sourceRevisionId = previousContext.sourceRevisionId;
this.sourceProvider = previousContext.sourceProvider;
this.handoffId = previousContext.handoffId;
this.lifecycleOperationId =
previousContext.lifecycleOperationId;
this.updateLoggerContext({
revisionId: previousContext.revisionId,
});
throw error;
} finally {
client.release();
}
}
await this.withLeaseTransaction(
(client) => this.loadRevisionContext(client),
this.revisionId,
);
}
async loadRevisionContext(client) {
const revisionResult = await client.query(
`SELECT revision.status,
revision.source,
revision.schema_version,
revision.codec_version,
revision.format,
COALESCE(
handoff.source_revision_id,
CASE
WHEN session.active_revision_id = revision.revision_id
THEN session.previous_revision_id
END
)::text AS source_revision_id,
COALESCE(handoff.source_provider, previous.provider)
AS source_provider,
previous.status AS source_revision_status,
previous.source AS source_revision_source,
handoff.handoff_id,
handoff.lifecycle_operation_id
FROM whatsapp_session_revision revision
JOIN whatsapp_session session
ON session.session_id = revision.session_id
LEFT JOIN whatsapp_session_handoff handoff
ON handoff.session_id = revision.session_id
AND handoff.target_revision_id = revision.revision_id
LEFT JOIN whatsapp_session_revision previous
ON previous.session_id = session.session_id
AND previous.revision_id = session.previous_revision_id
WHERE revision.session_id = $1
AND revision.revision_id = $2::bigint
AND revision.provider = 'wwebjs'
ORDER BY handoff.created_at DESC NULLS LAST
LIMIT 1`,
[this.sessionId, this.revisionId],
);
if (revisionResult.rowCount !== 1) {
throw new WhatsAppSessionError('whatsapp_session_revision_missing');
}
const revision = revisionResult.rows[0];
this.assertRevisionCompatibility(revision);
if (!['active', 'staging', 'validating'].includes(revision.status)) {
throw new WhatsAppSessionError(
'whatsapp_session_revision_not_openable',
);
}
this.revisionSource = revision.source;
this.revisionStatus = revision.status;
this.sourceRevisionId = revision.source_revision_id || undefined;
this.sourceProvider = revision.source_provider || undefined;
this.handoffId = revision.handoff_id || undefined;
this.lifecycleOperationId =
revision.lifecycle_operation_id ||
(revision.source === 'secure_import'
? revision.handoff_id
: undefined);
this.secureImportRollbackContext =
revision.source === 'secure_import' &&
revision.source_provider === PROVIDER &&
revision.source_revision_id &&
['staging', 'validating', 'active'].includes(
revision.source_revision_status,
) &&
typeof revision.source_revision_source === 'string'
? {
revisionId: revision.source_revision_id,
status: revision.source_revision_status,
source: revision.source_revision_source,
}
: undefined;
}
assertRevisionCompatibility(revision) {
if (
Number(revision.schema_version) !== SESSION_SCHEMA_VERSION ||
Number(revision.codec_version) !== SESSION_CODEC_VERSION ||
revision.format !== SESSION_FORMAT
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_revision_codec_unsupported',
{
schema_version: revision.schema_version,
codec_version: revision.codec_version,
format: revision.format,
},
);
}
}
async createCandidateRevision(
provider = PROVIDER,
source = 'secure_import',
{ requireEmptyPairing = false } = {},
) {
if (provider !== PROVIDER || source !== 'secure_import') {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_cross_provider_handoff_requires_manager',
);
}
if (
!this.revisionId ||
!['staging', 'validating', 'active'].includes(this.revisionStatus)
) {
throw new WhatsAppSessionError(
'whatsapp_session_active_revision_required',
);
}
const expectedActiveRevisionId = this.revisionId;
return this.withLeaseMutation(async (client) => {
if (requireEmptyPairing) {
const proof = await client.query(
this.emptyPairingRevisionProofSql({
allowFailedHandoffs: true,
statuses: ['staging', 'validating'],
}),
[this.sessionId, expectedActiveRevisionId],
);
if (
proof.rowCount !== 1 ||
proof.rows[0]?.pairing_allowed !== true
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_secure_import_requires_empty_pairing',
);
}
}
const result = await client.query(
`SELECT revision_id::text AS revision_id,
handoff_id,
source_revision_id::text AS source_revision_id
FROM create_whatsapp_session_candidate(
$1, $2::bigint, $3, $4, $5::bigint, $6, $7,
$8, $9, $10, $11, $12
)`,
[
this.sessionId,
expectedActiveRevisionId,
this.ownerId,
provider,
this.fencingToken,
this.generation,
this.epoch,
this.runtimeCapability,
source,
SESSION_SCHEMA_VERSION,
SESSION_CODEC_VERSION,
targetFormat(provider),
],
);
if (result.rowCount !== 1) {
throw new WhatsAppSessionError(
'whatsapp_session_candidate_creation_failed',
);
}
return result.rows[0];
}, expectedActiveRevisionId);
}
async loadActiveProfileAnchorInTransaction(
client,
revisionId = this.revisionId,
) {
// withLeaseMutation() has already entered the capability-fenced
// begin_whatsapp_session_mutation() function. That function takes
// FOR UPDATE locks on the owning session and revision before this
// read, so every anchor writer for the revision is serialized here.
// The SECURITY DEFINER checkpoint function takes the row lock again
// and enforces the expected artifact/generation CAS at commit time.
// Do not use SELECT ... FOR UPDATE from the runtime role: PostgreSQL
// requires direct UPDATE privilege for that syntax, while the runtime
// is deliberately granted SELECT-only access to the anchor table.
const result = await client.query(
`SELECT anchor.session_id,
anchor.revision_id::text AS revision_id,
anchor.anchor_generation::text AS anchor_generation,
anchor.artifact_id,
anchor.state,
anchor.checkpoint_mode,
anchor.artifact_checksum_sha256,
anchor.artifact_size_bytes::text AS artifact_size_bytes,
anchor.artifact_chunk_count,
anchor.artifact_persisted_at,
anchor.artifact_verified_at,
anchor.baseline_app_state_checksum_sha256,
anchor.current_app_state_checksum_sha256,
anchor.app_state_overlay_required,
anchor.canonical_generation::text AS canonical_generation,
anchor.canonical_checksum_sha256,
anchor.canonical_record_count,
anchor.canonical_size_bytes::text AS canonical_size_bytes,
anchor.canonical_persisted_at,
anchor.source,
anchor.last_profile_observed_size_bytes::text
AS last_profile_observed_size_bytes,
artifact.manifest,
artifact.checksum_sha256,
artifact.size_bytes::text AS size_bytes,
artifact.chunk_count,
artifact.status,
artifact.created_at,
artifact.persisted_at,
revision.source AS revision_source
FROM whatsapp_wwebjs_profile_anchor AS anchor
JOIN whatsapp_artifact AS artifact
ON artifact.session_id = anchor.session_id
AND artifact.revision_id = anchor.revision_id
AND artifact.artifact_id = anchor.artifact_id
JOIN whatsapp_session_revision AS revision
ON revision.session_id = anchor.session_id
AND revision.revision_id = anchor.revision_id
WHERE anchor.session_id = $1
AND anchor.revision_id = $2::bigint
AND anchor.state = 'active'`,
[this.sessionId, revisionId],
);
if (!Array.isArray(result.rows) || result.rows.length === 0) {
return undefined;
}
if (result.rows.length !== 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_anchor_checkpoint_ambiguous',
);
}
const row = result.rows[0];
const checkpoint = normalizeProfileAnchorCheckpoint(row);
if (
checkpoint.sessionId !== this.sessionId ||
checkpoint.revisionId !== String(revisionId) ||
row.status !== 'ready' ||
row.checksum_sha256 !== checkpoint.artifactChecksumSha256 ||
Number(row.size_bytes) !== checkpoint.artifactSizeBytes ||
Number(row.chunk_count) !== checkpoint.artifactChunkCount ||
String(row.persisted_at) !== String(checkpoint.artifactPersistedAt)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_anchor_artifact_mismatch',
);
}
return {
...checkpoint,
artifact: {
artifact_id: checkpoint.artifactId,
revision_id: checkpoint.revisionId,
manifest: row.manifest,
checksum_sha256: row.checksum_sha256,
size_bytes: row.size_bytes,
chunk_count: row.chunk_count,
status: row.status,
created_at: row.created_at,
persisted_at: row.persisted_at,
source: row.revision_source,
},
};
}
sameProfileAnchorCheckpoint(left, right) {
if (!left || !right) return left === right;
return (
left.revisionId === right.revisionId &&
left.anchorGeneration === right.anchorGeneration &&
left.canonicalGeneration === right.canonicalGeneration &&
left.artifactId === right.artifactId &&
left.artifactChecksumSha256 === right.artifactChecksumSha256 &&
left.canonicalChecksumSha256 === right.canonicalChecksumSha256 &&
left.currentAppStateChecksumSha256 ===
right.currentAppStateChecksumSha256 &&
left.appStateOverlayRequired === right.appStateOverlayRequired
);
}
async loadActiveProfileAnchor(revisionId = this.revisionId) {
return this.withLeaseTransaction((client) =>
this.loadActiveProfileAnchorInTransaction(client, revisionId),
);
}
async canReusePromotedProfileForActivationCheckpoint() {
if (
this.revisionStatus !== 'active' ||
!this.handoffId ||
!this.lifecycleOperationId
) {
return false;
}
const result = await this.withLeaseTransaction((client) =>
client.query(
`/* wwebjs_post_activation_profile_checkpoint_reuse */
SELECT anchor.artifact_id::text AS artifact_id,
handoff.pre_activation_artifact_id::text
AS pre_activation_artifact_id,
anchor.anchor_generation::text AS anchor_generation,
anchor.canonical_generation::text
AS canonical_generation
FROM whatsapp_session AS session
JOIN whatsapp_session_handoff AS handoff
ON handoff.session_id = session.session_id
AND handoff.handoff_id = $3::uuid
AND handoff.lifecycle_operation_id = $4::uuid
AND handoff.target_revision_id = $2::bigint
JOIN whatsapp_session_revision AS revision
ON revision.session_id = session.session_id
AND revision.revision_id = handoff.target_revision_id
JOIN whatsapp_wwebjs_profile_anchor AS anchor
ON anchor.session_id = revision.session_id
AND anchor.revision_id = revision.revision_id
AND anchor.state = 'active'
JOIN whatsapp_artifact AS artifact
ON artifact.session_id = anchor.session_id
AND artifact.revision_id = anchor.revision_id
AND artifact.artifact_id = anchor.artifact_id
WHERE session.session_id = $1
AND session.provider = 'wwebjs'
AND session.state = 'preparing'
AND session.active_revision_id = revision.revision_id
AND session.previous_revision_id =
handoff.source_revision_id
AND session.last_persisted_at >=
handoff.point_of_no_return_at
AND handoff.target_provider = 'wwebjs'
AND handoff.state = 'activating'
AND handoff.point_of_no_return_at IS NOT NULL
AND handoff.pre_activation_artifact_id IS NOT NULL
AND handoff.completed_at IS NULL
AND revision.provider = 'wwebjs'
AND revision.status = 'active'
AND revision.checksum_sha256 =
artifact.checksum_sha256
AND revision.size_bytes = artifact.size_bytes
AND revision.persisted_at >=
handoff.point_of_no_return_at
AND artifact.provider = 'wwebjs'
AND artifact.kind = $5
AND artifact.status = 'ready'
AND artifact.persisted_at >=
handoff.point_of_no_return_at
AND artifact.artifact_id <>
handoff.pre_activation_artifact_id
AND anchor.app_state_overlay_required = false
AND anchor.baseline_app_state_checksum_sha256 =
anchor.current_app_state_checksum_sha256`,
[
this.sessionId,
this.revisionId,
this.handoffId,
this.lifecycleOperationId,
PROFILE_ARTIFACT_KIND,
],
),
);
const reusable = result.rowCount === 1;
this.log(
'handoff.activation_profile_checkpoint_reuse_evaluated',
{
reusable,
profile_artifact_id: result.rows[0]?.artifact_id,
pre_activation_artifact_id:
result.rows[0]?.pre_activation_artifact_id,
anchor_generation: result.rows[0]?.anchor_generation,
canonical_generation: result.rows[0]?.canonical_generation,
},
{ force: true },
);
return reusable;
}
async loadReusableHandoffProfileAnchor(canonicalProjection) {
if (!canonicalProjection?.device?.device_fingerprint) return undefined;
const appStateChecksumSha256 =
canonicalAppStateChecksum(canonicalProjection);
return this.withLeaseTransaction(async (client) => {
const result = await client.query(
`/* wwebjs_reusable_handoff_profile_anchor */
SELECT reusable.session_id,
reusable.revision_id::text AS revision_id,
reusable.anchor_generation::text
AS anchor_generation,
reusable.artifact_id,
reusable.state,
reusable.checkpoint_mode,
reusable.artifact_checksum_sha256,
reusable.artifact_size_bytes::text
AS artifact_size_bytes,
reusable.artifact_chunk_count,
reusable.artifact_persisted_at,
reusable.artifact_verified_at,
reusable.baseline_app_state_checksum_sha256,
reusable.current_app_state_checksum_sha256,
reusable.app_state_overlay_required,
reusable.canonical_generation::text
AS canonical_generation,
reusable.canonical_checksum_sha256,
reusable.canonical_record_count,
reusable.canonical_size_bytes::text
AS canonical_size_bytes,
reusable.canonical_persisted_at,
reusable.source,
reusable.last_profile_observed_size_bytes::text
AS last_profile_observed_size_bytes,
reusable.manifest,
reusable.checksum_sha256,
reusable.size_bytes::text AS size_bytes,
reusable.chunk_count,
reusable.status,
reusable.created_at,
reusable.persisted_at,
reusable.revision_source
FROM resolve_whatsapp_wwebjs_reusable_handoff_profile_v1(
$1, $2::bigint, $3, $4
) AS reusable`,
[
this.sessionId,
this.revisionId,
appStateChecksumSha256,
this.webVersionIntegrityPinned ? this.webVersion : null,
],
);
const row = result.rows?.[0];
if (!row) return undefined;
const checkpoint = normalizeProfileAnchorCheckpoint(row);
if (
checkpoint.sessionId !== this.sessionId ||
checkpoint.revisionId === String(this.revisionId) ||
checkpoint.currentAppStateChecksumSha256 !==
appStateChecksumSha256 ||
checkpoint.baselineAppStateChecksumSha256 !==
appStateChecksumSha256 ||
checkpoint.appStateOverlayRequired !== false ||
row.status !== 'ready' ||
row.checksum_sha256 !== checkpoint.artifactChecksumSha256 ||
Number(row.size_bytes) !== checkpoint.artifactSizeBytes ||
Number(row.chunk_count) !== checkpoint.artifactChunkCount ||
String(row.persisted_at) !==
String(checkpoint.artifactPersistedAt)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_reusable_handoff_profile_anchor_invalid',
);
}
return {
...checkpoint,
targetAppStateChecksumSha256: appStateChecksumSha256,
targetAppStateSemanticChecksumSha256:
canonicalAppStateSemanticChecksum(canonicalProjection),
artifact: {
artifact_id: checkpoint.artifactId,
revision_id: checkpoint.revisionId,
manifest: row.manifest,
checksum_sha256: row.checksum_sha256,
size_bytes: row.size_bytes,
chunk_count: row.chunk_count,
status: row.status,
created_at: row.created_at,
persisted_at: row.persisted_at,
source: row.revision_source,
},
};
});
}
canPreserveReusableHandoffProfileAppState(canonicalProjection) {
const authority = this.reusableHandoffProfileAuthority;
const legacyOrderSensitiveChecksumMatched = Boolean(
authority &&
authority.targetAppStateChecksumSha256 ===
canonicalAppStateChecksum(canonicalProjection),
);
const semanticChecksumMatched = Boolean(
authority &&
authority.targetAppStateSemanticChecksumSha256 ===
canonicalAppStateSemanticChecksum(canonicalProjection),
);
this.log(
'handoff.reusable_profile_app_state_equivalence_evaluated',
{
authority_present: Boolean(authority),
legacy_order_sensitive_checksum_matched:
legacyOrderSensitiveChecksumMatched,
semantic_checksum_matched: semanticChecksumMatched,
preserved: semanticChecksumMatched,
},
{ force: true },
);
return semanticChecksumMatched;
}
getReusableHandoffProfileSignalTableEquivalence(
canonicalProjection,
restoredNativeProjection,
) {
const authority = this.reusableHandoffProfileAuthority;
const appStateAuthorityMatched = Boolean(
authority &&
authority.targetAppStateSemanticChecksumSha256 ===
canonicalAppStateSemanticChecksum(canonicalProjection),
);
const targetComponentChecksums =
canonicalSignalTableComponentChecksums(canonicalProjection);
const nativeComponentChecksums = restoredNativeProjection
? canonicalSignalTableComponentChecksums(restoredNativeProjection)
: null;
const componentMatches = Object.fromEntries(
Object.entries(targetComponentChecksums).map(([name, checksum]) => [
name,
Boolean(
nativeComponentChecksums &&
nativeComponentChecksums[name] === checksum,
),
]),
);
const mismatchedComponents = Object.entries(componentMatches)
.filter(([, matched]) => !matched)
.map(([name]) => name);
const semanticChecksumMatched = Boolean(
appStateAuthorityMatched &&
restoredNativeProjection &&
canonicalSignalTablesSemanticChecksum(restoredNativeProjection) ===
canonicalSignalTablesSemanticChecksum(canonicalProjection),
);
this.log(
'handoff.reusable_profile_signal_table_equivalence_evaluated',
{
authority_present: Boolean(authority),
native_projection_present: Boolean(restoredNativeProjection),
app_state_authority_matched: appStateAuthorityMatched,
semantic_checksum_matched: semanticChecksumMatched,
component_matches: componentMatches,
mismatched_components: mismatchedComponents,
preserved: semanticChecksumMatched,
},
{ force: true },
);
return Object.freeze({
appStateAuthorityMatched,
semanticChecksumMatched,
componentMatches: Object.freeze({ ...componentMatches }),
mismatchedComponents: Object.freeze([...mismatchedComponents]),
preserved: semanticChecksumMatched,
});
}
canPreserveReusableHandoffProfileSignalTables(
canonicalProjection,
restoredNativeProjection,
) {
return this.getReusableHandoffProfileSignalTableEquivalence(
canonicalProjection,
restoredNativeProjection,
).semanticChecksumMatched;
}
async loadSoleReadyProfileArtifactInTransaction(
client,
revisionId = this.revisionId,
) {
const result = await client.query(
`/* wwebjs_profile_anchor_ready_artifact */
SELECT artifact_id,
revision_id::text AS revision_id,
manifest,
checksum_sha256,
size_bytes::text AS size_bytes,
chunk_count,
status,
created_at,
persisted_at
FROM whatsapp_artifact
WHERE session_id = $1
AND revision_id = $2::bigint
AND provider = 'wwebjs'
AND kind = $3
AND status = 'ready'
ORDER BY created_at DESC
LIMIT 2`,
[this.sessionId, revisionId, PROFILE_ARTIFACT_KIND],
);
if (result.rows.length > 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_artifact_ambiguous',
);
}
const artifact = result.rows[0];
if (!artifact) return undefined;
const candidate = this.normalizeProfileArtifactAnchor(
artifact,
revisionId,
);
await this.assertLastGoodProfileAnchor(client, candidate);
return artifact;
}
async getArtifact(selector = 'active') {
const revisionColumn =
selector === 'previous'
? 'previous_revision_id'
: 'active_revision_id';
const revisionId = await this.withLeaseTransaction(async (client) => {
const result = await client.query(
`SELECT ${revisionColumn}::text AS revision_id
FROM whatsapp_session
WHERE session_id = $1`,
[this.sessionId],
);
return result.rows[0]?.revision_id;
});
if (!revisionId) return undefined;
return this.withLeaseTransaction(async (client) => {
if (selector === 'active') {
const anchor = await this.loadActiveProfileAnchorInTransaction(
client,
revisionId,
);
if (anchor) return anchor.artifact;
}
const result = await client.query(
`SELECT artifact.artifact_id,
artifact.revision_id::text AS revision_id,
artifact.manifest,
artifact.checksum_sha256,
artifact.size_bytes::text AS size_bytes,
artifact.chunk_count,
artifact.status,
artifact.created_at,
artifact.persisted_at,
revision.source
FROM whatsapp_session_revision revision
JOIN whatsapp_artifact artifact
ON artifact.session_id = revision.session_id
AND artifact.revision_id = revision.revision_id
AND artifact.provider = 'wwebjs'
AND artifact.kind = $3
AND artifact.status = 'ready'
WHERE revision.session_id = $1
AND revision.revision_id = $2::bigint
ORDER BY artifact.created_at DESC
LIMIT 2`,
[this.sessionId, revisionId, PROFILE_ARTIFACT_KIND],
);
if (result.rowCount > 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_artifact_ambiguous',
);
}
return result.rows[0];
}, revisionId);
}
async getCurrentRevisionArtifact() {
if (!this.revisionId) return undefined;
return this.withLeaseTransaction(async (client) => {
const anchor = await this.loadActiveProfileAnchorInTransaction(
client,
this.revisionId,
);
if (anchor) return anchor.artifact;
const result = await client.query(
`SELECT artifact.artifact_id,
artifact.revision_id::text AS revision_id,
artifact.manifest,
artifact.checksum_sha256,
artifact.size_bytes::text AS size_bytes,
artifact.chunk_count,
artifact.status,
artifact.created_at,
artifact.persisted_at,
revision.source
FROM whatsapp_artifact artifact
JOIN whatsapp_session_revision revision
ON revision.session_id = artifact.session_id
AND revision.revision_id = artifact.revision_id
WHERE artifact.session_id = $1
AND artifact.revision_id = $2::bigint
AND artifact.provider = 'wwebjs'
AND artifact.kind = $3
AND artifact.status = 'ready'
ORDER BY artifact.created_at DESC
LIMIT 2`,
[this.sessionId, this.revisionId, PROFILE_ARTIFACT_KIND],
);
if (result.rowCount > 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_artifact_ambiguous',
);
}
return result.rows[0];
});
}
async insertArtifactBlobs(blobs) {
if (!Array.isArray(blobs) || blobs.length < 1 || blobs.length > 16) {
throw new WhatsAppSessionError(
'whatsapp_artifact_blob_batch_invalid',
);
}
const checksums = [];
const payloads = [];
const sizes = [];
for (const blob of blobs) {
const payload = Buffer.from(blob?.payload || []);
if (
typeof blob?.sha256 !== 'string' ||
!/^[0-9a-f]{64}$/.test(blob.sha256) ||
payload.length < 1 ||
payload.length > this.chunkBytes ||
crypto.createHash('sha256').update(payload).digest('hex') !==
blob.sha256
) {
throw new WhatsAppSessionError(
'whatsapp_artifact_blob_payload_invalid',
);
}
checksums.push(blob.sha256);
payloads.push(payload);
sizes.push(payload.length);
}
return this.withLeaseMutation(async (client) => {
const stored = await client.query(
`SELECT put_whatsapp_artifact_blobs(
$1, $2::text[], $3::bytea[], $4::integer[]
) AS stored`,
[this.sessionId, checksums, payloads, sizes],
);
if (stored.rows[0]?.stored !== true) {
throw new WhatsAppSessionError(
'whatsapp_artifact_blob_batch_rejected',
);
}
});
}
async getArtifactBlob(checksum) {
const blobs = await this.getArtifactBlobs([checksum]);
return blobs.get(checksum);
}
async getArtifactBlobs(checksums) {
return this.withLeaseTransaction(async (client) => {
const reusable = this.reusableHandoffProfileAuthority;
const result = reusable
? await client.query(
`/* wwebjs_reusable_handoff_profile_blobs */
SELECT sha256, payload
FROM read_whatsapp_wwebjs_reusable_handoff_profile_blobs_v1(
$1, $2::bigint, $3::bigint, $4, $5, $6, $7::text[]
)`,
[
this.sessionId,
this.revisionId,
reusable.revisionId,
reusable.artifactId,
reusable.targetAppStateChecksumSha256,
this.webVersionIntegrityPinned
? this.webVersion
: null,
checksums,
],
)
: await client.query(
`SELECT sha256, payload
FROM whatsapp_artifact_blob
WHERE session_id = $1
AND sha256 = ANY($2::text[])`,
[this.sessionId, checksums],
);
if (result.rowCount !== new Set(checksums).size) {
throw new WhatsAppSessionError(
'whatsapp_artifact_blob_missing',
);
}
return new Map(result.rows.map((row) => [row.sha256, row.payload]));
});
}
async checkpointProfile({
profilePath,
projection,
canonicalProjection,
source = 'checkpoint',
promote = true,
validateSnapshot,
}) {
this.assertLocalLease();
const startedAt = Date.now();
const normalizedCanonicalProjection = canonicalProjection
? normalizeStoreCanonicalProjection(
canonicalProjection,
this.webVersion,
)
: undefined;
const wasActive = this.revisionStatus === 'active';
const currentArtifact = await this.getCurrentRevisionArtifact();
const targetRevisionId = this.revisionId;
const uploader = new BlobBatchUploader(this);
const artifactId = crypto.randomUUID();
try {
const artifact = await buildProfileArtifact({
profilePath,
chunkBytes: this.chunkBytes,
previousManifest: currentArtifact?.manifest,
uploadBlob: (blob) => uploader.add(blob),
webVersion: this.webVersion,
logger: this.logger,
maxProfileBytes: this.maxProfileBytes,
expectedProfileFingerprint: profileFingerprint({
sessionId: this.sessionId,
revisionId: targetRevisionId,
}),
});
await uploader.flush();
// The profile is streamed outside PostgreSQL by design. Give the
// provider a final read-only consistency gate after the artifact
// inventory is frozen and before any revision metadata is
// replaced. A retryable failure here only leaves orphan chunks,
// which the catch path removes without touching the active
// artifact or candidate revision.
await validateSnapshot?.();
const normalizedProjection = normalizeProjection(
projection,
this.webVersion,
);
// Profile artifacts are the native WWeb restore source. Generic
// full-IDB records are retained only by an explicit export call;
// every ordinary checkpoint retires any older legacy records.
const records = normalizedProjection?.records || [];
await this.persistRevision({
artifactId,
revisionId: targetRevisionId,
artifact,
records,
canonicalProjection: normalizedCanonicalProjection,
promote,
});
if (promote) {
this.revisionStatus = 'active';
if (!wasActive) this.revisionSource = source;
this.updateLoggerContext({ revisionId: targetRevisionId });
} else if (this.revisionStatus !== 'active') {
this.revisionStatus = 'validating';
}
this.log('checkpoint.completed', {
revision_id: targetRevisionId,
artifact_id: artifactId,
size_bytes: artifact.sizeBytes,
uploaded_bytes: artifact.uploadedBytes,
reused_bytes: artifact.reusedBytes,
record_count: records?.length,
canonical_record_count:
normalizedCanonicalProjection?.record_count,
promoted: promote,
duration_ms: Date.now() - startedAt,
});
return {
revisionId: targetRevisionId,
artifactId,
...artifact,
};
} catch (error) {
await uploader.flush().catch(() => {});
await this.withLeaseMutation((client) =>
this.deleteOrphanedBlobs(client),
).catch(() => {});
const errorCode = String(error?.code || error?.message || '');
const retryableProfileMutation =
errorCode === PROFILE_CHANGED_DURING_CHECKPOINT;
const navigationSuperseded =
String(error?.code || '') ===
CONNECTED_PROFILE_NAVIGATION_SUPERSEDED;
const nonDestructiveCheckpointRace =
retryableProfileMutation || navigationSuperseded;
// Chromium may commit a profile file while the artifact is being
// streamed or replace the connected document before the
// post-stream proof completes. No revision or artifact metadata
// has been committed at this point, so deleting a fresh
// pairing/handoff revision would turn either harmless snapshot
// race into credential loss. Keep the candidate intact, remove
// only unreferenced chunks, and let the caller serialize a
// complete, consistency-checked retry against the successor.
if (
!nonDestructiveCheckpointRace &&
this.revisionStatus !== 'active'
) {
await this.failRevision(targetRevisionId, error).catch(
() => {},
);
}
this.log(
retryableProfileMutation
? 'checkpoint.retryable_profile_mutation'
: navigationSuperseded
? 'checkpoint.navigation_superseded'
: 'checkpoint.failed',
{
revision_id: targetRevisionId,
error,
retryable: nonDestructiveCheckpointRace,
duration_ms: Date.now() - startedAt,
},
{ force: true },
);
throw error;
}
}
async persistRevision({
artifactId,
revisionId,
artifact,
records,
canonicalProjection,
promote,
}) {
const directlyPromotableBootstrap = Boolean(
!this.sourceRevisionId &&
['pairing', 'legacy_volume_migration'].includes(
this.revisionSource,
),
);
const requiresLifecyclePromotion =
promote &&
this.revisionStatus !== 'active' &&
!directlyPromotableBootstrap;
if (requiresLifecyclePromotion) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_candidate_activation_requires_explicit_commit',
);
}
await this.withLeaseMutation(async (client) => {
const revision = await client.query(
`UPDATE whatsapp_session_revision
SET status = CASE
WHEN status = 'active' THEN 'active'
ELSE 'validating'
END,
checksum_sha256 = $3,
size_bytes = $4,
persisted_at = now()
WHERE session_id = $1
AND revision_id = $2::bigint
AND provider = 'wwebjs'
AND status IN ('staging', 'validating', 'active')
RETURNING revision_id, status`,
[
this.sessionId,
revisionId,
artifact.checksumSha256,
artifact.sizeBytes,
],
);
if (revision.rowCount !== 1) {
throw new WhatsAppSessionError(
'whatsapp_session_candidate_revision_invalid',
);
}
const expectedAnchor =
await this.loadActiveProfileAnchorInTransaction(
client,
revisionId,
);
if (expectedAnchor && !canonicalProjection) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_anchor_canonical_projection_required',
);
}
await client.query(
`INSERT INTO whatsapp_artifact (
session_id, artifact_id, revision_id, provider, kind,
manifest, checksum_sha256, size_bytes, chunk_count,
status, created_at, persisted_at
) VALUES (
$1, $2, $3::bigint, 'wwebjs', $4, $5::jsonb, $6, $7,
$8, 'staging', now(), now()
)`,
[
this.sessionId,
artifactId,
revisionId,
PROFILE_ARTIFACT_KIND,
JSON.stringify(artifact.manifest),
artifact.checksumSha256,
artifact.sizeBytes,
artifact.chunkCount,
],
);
const checksums = artifact.manifest.files.flatMap((file) =>
file.chunks.map((chunk) => chunk.sha256),
);
if (checksums.length > 0) {
await client.query(
`INSERT INTO whatsapp_artifact_chunk (
session_id, artifact_id, chunk_index, sha256
)
SELECT $1, $2, entry.ordinality - 1, entry.sha256
FROM unnest($3::text[]) WITH ORDINALITY
AS entry(sha256, ordinality)`,
[this.sessionId, artifactId, checksums],
);
}
if (records) {
await this.replaceProviderRecords(client, revisionId, records);
}
let persistedCanonicalProjection;
if (canonicalProjection) {
const canonical = await this.replaceCanonicalProjection(
client,
revisionId,
canonicalProjection,
);
persistedCanonicalProjection = canonical.projection;
}
await client.query(
`UPDATE whatsapp_artifact
SET status = 'retired'
WHERE session_id = $1
AND revision_id = $2::bigint
AND provider = 'wwebjs'
AND kind = $3
AND artifact_id <> $4
AND status = 'ready'`,
[this.sessionId, revisionId, PROFILE_ARTIFACT_KIND, artifactId],
);
await client.query(
`UPDATE whatsapp_artifact
SET status = 'ready', persisted_at = now()
WHERE session_id = $1 AND artifact_id = $2`,
[this.sessionId, artifactId],
);
if (promote && !requiresLifecyclePromotion) {
await this.promoteInTransaction(client, revisionId);
} else if (this.revisionStatus === 'active') {
await client.query(
`UPDATE whatsapp_session
SET last_persisted_at = now(), updated_at = now()
WHERE session_id = $1
AND active_revision_id = $2::bigint`,
[this.sessionId, revisionId],
);
}
const revisionIsActive =
revision.rows[0]?.status === 'active' ||
(promote && !requiresLifecyclePromotion);
if (persistedCanonicalProjection && revisionIsActive) {
await this.commitProfileAnchorCheckpointInTransaction(client, {
revisionId,
artifactId,
expectedAnchor,
checkpointMode: 'full_profile_plus_fresh_canonical_v1',
canonicalProjection: persistedCanonicalProjection,
source: 'full_checkpoint',
lastProfileObservedSizeBytes: artifact.sizeBytes,
});
}
// Retired artifacts referenced by a previous anchor generation
// remain available for the bounded rollback/GC grace period.
// The anchor GC owns their later deletion; deleting them here
// would make the descriptor's FK cascade during the same commit.
await this.deleteOrphanedBlobs(client);
}, revisionId);
// Candidate activation is deliberately excluded here. It must pass
// commitActivation() while Chromium is offline and finalize only
// after the authenticated online checkpoint.
}
async replaceProviderRecords(client, revisionId, records) {
if (
records.some((record) => record.namespace === LIFECYCLE_NAMESPACE)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_projection_lifecycle_namespace_forbidden',
);
}
await client.query(
`DELETE FROM whatsapp_provider_record
WHERE session_id = $1
AND revision_id = $2::bigint
AND namespace <> ALL($3::text[])`,
[
this.sessionId,
revisionId,
[CANONICAL_META_NAMESPACE, LIFECYCLE_NAMESPACE],
],
);
for (let start = 0; start < records.length; start += 100) {
const batch = records.slice(start, start + 100);
const parameters = [];
const values = batch.map((record, index) => {
const offset = index * 6;
parameters.push(
this.sessionId,
revisionId,
record.namespace,
record.recordKey,
PROVIDER_RECORD_CODEC_VERSION,
record.payload,
);
return `($${offset + 1}, $${offset + 2}::bigint, $${offset + 3}, $${offset + 4}, $${offset + 5}, $${offset + 6}, now())`;
});
await client.query(
`INSERT INTO whatsapp_provider_record (
session_id, revision_id, namespace, record_key,
codec_version, payload, updated_at
) VALUES ${values.join(', ')}
ON CONFLICT (
session_id, revision_id, namespace, record_key
) DO UPDATE
SET codec_version = EXCLUDED.codec_version,
payload = EXCLUDED.payload,
updated_at = now()`,
parameters,
);
}
}
async insertCanonicalRows(client, revisionId, table, columns, rows) {
for (let start = 0; start < rows.length; start += 1000) {
const batch = rows.slice(start, start + 1000);
const parameters = [];
const values = batch.map((row, index) => {
const rowValues = columns.map((column) => row[column]);
const offset = index * (columns.length + 2);
parameters.push(this.sessionId, revisionId, ...rowValues);
return `(${Array.from(
{ length: columns.length + 2 },
(_, parameterIndex) => `$${offset + parameterIndex + 1}`,
).join(', ')})`;
});
await client.query(
`INSERT INTO ${table} (
session_id, revision_id, ${columns.join(', ')}
) VALUES ${values.join(', ')}`,
parameters,
);
}
}
async upsertCanonicalPqStatePreservingServerCount(
client,
revisionId,
state,
) {
await client.query(
`INSERT INTO whatsapp_pq_pre_key_state (
session_id, revision_id, codec_version, algorithm,
next_pre_key_id, migrated, last_server_count,
last_server_count_timestamp_ms
) VALUES (
$1, $2::bigint, $3, $4, $5, $6, $7, $8
)
ON CONFLICT (session_id, revision_id) DO UPDATE SET
codec_version = EXCLUDED.codec_version,
algorithm = EXCLUDED.algorithm,
next_pre_key_id = EXCLUDED.next_pre_key_id,
migrated = EXCLUDED.migrated`,
[
this.sessionId,
revisionId,
state.codec_version,
state.algorithm,
state.next_pre_key_id,
state.migrated,
state.last_server_count,
state.last_server_count_timestamp_ms,
],
);
}
async loadCanonicalIdentityContext(
client,
revisionId,
{ lock = false } = {},
) {
const result = await client.query(
`SELECT session.active_revision_id::text AS active_revision_id,
session.state AS session_state,
session.active_device_fingerprint,
device.session_id IS NOT NULL AS device_present,
device.jid,
device.adv_secret_available,
device.device_fingerprint,
device.fingerprint_version
FROM whatsapp_session session
LEFT JOIN whatsapp_device device
ON device.session_id = session.session_id
AND device.revision_id = $2::bigint
WHERE session.session_id = $1
${lock ? 'FOR UPDATE OF session' : ''}`,
[this.sessionId, revisionId],
);
if (result.rowCount !== 1 || !result.rows[0]) {
throw new WhatsAppSessionError('whatsapp_session_header_missing');
}
return result.rows[0];
}
assertCanonicalIdentityContext(context, device) {
const projectedJid = comparableJid(device.jid);
const projectedFingerprint = normalizeFingerprint(
device.device_fingerprint,
);
if (
!projectedJid ||
!projectedFingerprint ||
device.fingerprint_version !== CANONICAL_FINGERPRINT_VERSION
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_device_identity_invalid',
);
}
const storedFingerprint = normalizeFingerprint(
context.device_fingerprint,
);
const activeFingerprint = normalizeFingerprint(
context.active_device_fingerprint,
);
if (
context.device_fingerprint &&
context.fingerprint_version !== CANONICAL_FINGERPRINT_VERSION
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_companion_fingerprint_version_mismatch',
);
}
if (context.jid && comparableJid(context.jid) !== projectedJid) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_identity_mismatch',
);
}
for (const expectedFingerprint of [
storedFingerprint,
activeFingerprint,
]) {
if (
expectedFingerprint &&
!fingerprintsEqual(expectedFingerprint, projectedFingerprint)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_companion_fingerprint_mismatch',
);
}
}
const trulyEmptyPairing =
this.revisionSource === 'pairing' &&
!this.sourceRevisionId &&
(!context.active_revision_id ||
String(context.active_revision_id) ===
String(this.revisionId)) &&
!context.active_device_fingerprint;
const legacyVolumeMigrationBootstrap =
this.revisionSource === 'legacy_volume_migration' &&
!this.sourceRevisionId &&
Boolean(this.storageMigrationId) &&
['staging', 'validating'].includes(this.revisionStatus) &&
context.session_state === 'preparing' &&
(!context.active_revision_id ||
String(context.active_revision_id) ===
String(this.revisionId)) &&
!context.active_device_fingerprint;
// A plugin import starts from a database-proven empty pairing source,
// so the candidate has no active fingerprint to authorize its first
// device row. Keep this admission bound to the exact same-provider
// handoff and its reversible pairing rollback context.
const secureImportBrowserBootstrap =
this.revisionSource === 'secure_import' &&
this.sourceProvider === PROVIDER &&
Boolean(this.sourceRevisionId) &&
Boolean(this.handoffId) &&
this.lifecycleOperationId === this.handoffId &&
['staging', 'validating'].includes(this.revisionStatus) &&
context.session_state === 'handoff' &&
String(context.active_revision_id) ===
String(this.sourceRevisionId) &&
!context.active_device_fingerprint &&
String(this.secureImportRollbackContext?.revisionId) ===
String(this.sourceRevisionId) &&
['staging', 'validating'].includes(
this.secureImportRollbackContext?.status,
) &&
this.secureImportRollbackContext?.source === 'pairing';
const materializingAuthorizedCandidate =
this.isHandoffRevision() && Boolean(activeFingerprint);
if (
!context.device_present &&
!trulyEmptyPairing &&
!legacyVolumeMigrationBootstrap &&
!secureImportBrowserBootstrap &&
!materializingAuthorizedCandidate
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_canonical_device_missing',
);
}
if (
this.revisionStatus === 'active' &&
(!context.device_present || !context.jid || !storedFingerprint)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_active_device_missing',
);
}
return {
projectedFingerprint,
projectedJid,
storedFingerprint,
activeFingerprint,
};
}
async preflightReadyIdentity({
jid,
companionFingerprint,
canonicalProjection,
} = {}) {
if (!jid || !canonicalProjection) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_ready_identity_missing',
);
}
const normalizedProjection = normalizeStoreCanonicalProjection(
canonicalProjection,
this.webVersion,
);
const browserJid = comparableJid(jid);
const projectedJid = comparableJid(normalizedProjection.device.jid);
if (!browserJid || browserJid !== projectedJid) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_projection_jid_mismatch',
);
}
const trustedFingerprint = normalizeFingerprint(companionFingerprint);
const projectedFingerprint = normalizeFingerprint(
normalizedProjection.device.device_fingerprint,
);
if (
trustedFingerprint &&
!fingerprintsEqual(trustedFingerprint, projectedFingerprint)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_projection_fingerprint_mismatch',
);
}
const identityContext = await this.withLeaseTransaction(
async (client) => {
const context = await this.loadCanonicalIdentityContext(
client,
this.revisionId,
);
return {
context,
identity: this.assertCanonicalIdentityContext(
context,
normalizedProjection.device,
),
};
},
);
return {
canonicalProjection: normalizedProjection,
browserJid,
projectedJid,
projectedFingerprint,
trustedFingerprint,
databaseJidPresent: Boolean(identityContext.context.jid),
fingerprintVerified: Boolean(trustedFingerprint),
};
}
async replaceCanonicalProjection(
client,
revisionId,
projection,
dirtyPlan,
) {
const requestedPersistencePlan = normalizeCanonicalDirtyPlan(dirtyPlan);
// The typed authority binds a checksum for the complete canonical
// projection, not merely the rows named by the browser journal. An
// active anchor therefore always replaces the complete deterministic
// canonical unit before advancing its generation. Selective writes
// remain valid for staging revisions, which do not publish an anchor.
const persistencePlan =
this.revisionStatus === 'active'
? normalizeCanonicalDirtyPlan()
: requestedPersistencePlan;
const device = projection.device;
const identityContext = await this.loadCanonicalIdentityContext(
client,
revisionId,
{ lock: true },
);
this.assertCanonicalIdentityContext(identityContext, device);
let persistedAdvSecretAvailable = persistencePlan.device
? device.adv_secret_available
: identityContext.adv_secret_available === true;
if (persistencePlan.device) {
const deviceWrite = await client.query(
`INSERT INTO whatsapp_device (
session_id, revision_id, jid, lid, facebook_uuid,
registration_id, noise_key, identity_key, signed_pre_key,
signed_pre_key_id, signed_pre_key_sig, adv_key,
adv_secret_available, adv_details, adv_account_sig,
adv_account_sig_key, adv_device_sig, platform,
business_name, push_name, lid_migration_ts,
next_pre_key_id, device_fingerprint, fingerprint_version
) VALUES (
$1, $2::bigint, $3, $4, $5::uuid, $6::bigint, $7, $8, $9,
$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,
$21::bigint, $22, $23, $24
)
ON CONFLICT (session_id, revision_id) DO UPDATE SET
jid = EXCLUDED.jid,
lid = EXCLUDED.lid,
facebook_uuid = EXCLUDED.facebook_uuid,
registration_id = EXCLUDED.registration_id,
noise_key = EXCLUDED.noise_key,
identity_key = EXCLUDED.identity_key,
signed_pre_key = EXCLUDED.signed_pre_key,
signed_pre_key_id = EXCLUDED.signed_pre_key_id,
signed_pre_key_sig = EXCLUDED.signed_pre_key_sig,
adv_key = COALESCE(
EXCLUDED.adv_key,
whatsapp_device.adv_key
),
adv_secret_available = (
whatsapp_device.adv_secret_available
OR EXCLUDED.adv_secret_available
),
adv_details = EXCLUDED.adv_details,
adv_account_sig = EXCLUDED.adv_account_sig,
adv_account_sig_key = EXCLUDED.adv_account_sig_key,
adv_device_sig = EXCLUDED.adv_device_sig,
platform = EXCLUDED.platform,
business_name = EXCLUDED.business_name,
push_name = EXCLUDED.push_name,
lid_migration_ts = EXCLUDED.lid_migration_ts,
next_pre_key_id = EXCLUDED.next_pre_key_id,
device_fingerprint = EXCLUDED.device_fingerprint,
fingerprint_version = EXCLUDED.fingerprint_version
WHERE (
whatsapp_device.jid IS NULL
OR (
lower(split_part(
split_part(whatsapp_device.jid, '@', 1), ':', 1
)) = lower(split_part(
split_part(EXCLUDED.jid, '@', 1), ':', 1
))
AND CASE lower(split_part(
whatsapp_device.jid, '@', 2
))
WHEN 'c.us' THEN 's.whatsapp.net'
ELSE lower(split_part(
whatsapp_device.jid, '@', 2
))
END
= CASE lower(split_part(EXCLUDED.jid, '@', 2))
WHEN 'c.us' THEN 's.whatsapp.net'
ELSE lower(split_part(EXCLUDED.jid, '@', 2))
END
)
)
AND (
whatsapp_device.device_fingerprint IS NULL
OR whatsapp_device.device_fingerprint
= EXCLUDED.device_fingerprint
)
AND (
whatsapp_device.fingerprint_version IS NULL
OR whatsapp_device.fingerprint_version
= EXCLUDED.fingerprint_version
)
RETURNING session_id, adv_secret_available`,
[
this.sessionId,
revisionId,
device.jid,
device.lid,
device.facebook_uuid,
device.registration_id,
device.noise_key,
device.identity_key,
device.signed_pre_key,
device.signed_pre_key_id,
device.signed_pre_key_sig,
device.adv_key,
device.adv_secret_available,
device.adv_details,
device.adv_account_sig,
device.adv_account_sig_key,
device.adv_device_sig,
device.platform,
device.business_name,
device.push_name,
device.lid_migration_ts,
device.next_pre_key_id,
device.device_fingerprint,
device.fingerprint_version,
],
);
if (deviceWrite.rowCount !== 1) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_canonical_identity_changed',
);
}
persistedAdvSecretAvailable =
deviceWrite.rows[0].adv_secret_available === true;
}
const selectedTables = new Set(persistencePlan.tables);
const tables = projection.tables;
let pqServerCountMode = projection.capabilities.pq_server_count_mode;
if (
selectedTables.has('whatsapp_pq_pre_key_state') &&
pqServerCountMode === 'unknown' &&
tables.whatsapp_pq_pre_key_state.length === 1 &&
tables.whatsapp_pq_pre_key_state[0].migrated === false &&
tables.whatsapp_pq_pre_keys.length === 0
) {
const previousPqState = await client.query(
`SELECT state.migrated,
EXISTS (
SELECT 1
FROM whatsapp_pq_pre_keys AS key
WHERE key.session_id = state.session_id
AND key.revision_id = state.revision_id
) AS had_keys
FROM whatsapp_pq_pre_key_state AS state
WHERE state.session_id = $1
AND state.revision_id = $2::bigint`,
[this.sessionId, revisionId],
);
if (
previousPqState.rows[0]?.migrated === true ||
previousPqState.rows[0]?.had_keys === true
) {
// A legacy WWeb clear removes all material and resets migrated,
// but exposes no server-count API. Treat that transition as an
// explicit new PQ epoch, not as an unknown observation.
pqServerCountMode = 'reset';
}
}
for (const { table } of [...CANONICAL_TABLE_CONFIGS].reverse()) {
if (!selectedTables.has(table)) continue;
if (
table === 'whatsapp_pq_pre_key_state' &&
pqServerCountMode === 'unknown' &&
tables.whatsapp_pq_pre_key_state.length === 1
) {
continue;
}
const replaceNonImportableSignalSessions =
table === 'whatsapp_signal_sessions' &&
projection.tables.whatsapp_signal_sessions.some(
(row) => row.session === null,
);
await client.query(
`DELETE FROM ${table}
WHERE session_id = $1 AND revision_id = $2::bigint
${
table === 'whatsapp_signal_sessions' &&
!replaceNonImportableSignalSessions
? 'AND session IS NOT NULL'
: ''
}`,
[this.sessionId, revisionId],
);
}
for (const {
table,
projectionKey,
columns,
} of CANONICAL_TABLE_CONFIGS) {
if (!selectedTables.has(table)) continue;
if (
table === 'whatsapp_pq_pre_key_state' &&
pqServerCountMode === 'unknown' &&
tables.whatsapp_pq_pre_key_state.length === 1
) {
await this.upsertCanonicalPqStatePreservingServerCount(
client,
revisionId,
tables.whatsapp_pq_pre_key_state[0],
);
continue;
}
await this.insertCanonicalRows(
client,
revisionId,
table,
columns,
tables[projectionKey],
);
}
if (projection.passthrough_tables !== undefined) {
for (const { table } of [...PASSTHROUGH_TABLE_CONFIGS].reverse()) {
await client.query(
`DELETE FROM ${table}
WHERE session_id = $1 AND revision_id = $2::bigint`,
[this.sessionId, revisionId],
);
}
for (const { table, columns } of PASSTHROUGH_TABLE_CONFIGS) {
await this.insertCanonicalRows(
client,
revisionId,
table,
columns,
projection.passthrough_tables[table],
);
}
}
const metadata = canonicalMetadataPayload(
projection,
persistedAdvSecretAvailable,
);
await client.query(
`DELETE FROM whatsapp_provider_record
WHERE session_id = $1
AND revision_id = $2::bigint
AND namespace = $3`,
[this.sessionId, revisionId, CANONICAL_META_NAMESPACE],
);
const providerStatePayload = encodeCanonicalProviderState(
projection.provider_state,
);
const routingInfoPayload = projection.transport_state
? normalizeCanonicalRoutingInfo(
projection.transport_state.routing_info,
)
: undefined;
const records = [
{
key: CANONICAL_META_KEY,
payload: metadata,
},
...(providerStatePayload
? [
{
key: CANONICAL_PROVIDER_STATE_KEY,
payload: providerStatePayload,
},
]
: []),
];
for (const record of records) {
await client.query(
`INSERT INTO whatsapp_provider_record (
session_id, revision_id, namespace, record_key,
codec_version, payload, updated_at
) VALUES ($1, $2::bigint, $3, $4, 1, $5, now())`,
[
this.sessionId,
revisionId,
CANONICAL_META_NAMESPACE,
record.key,
record.payload,
],
);
}
await client.query(
`DELETE FROM whatsapp_provider_record
WHERE session_id = $1
AND revision_id = $2::bigint
AND namespace = $3`,
[this.sessionId, revisionId, CANONICAL_TRANSPORT_NAMESPACE],
);
if (routingInfoPayload) {
await client.query(
`INSERT INTO whatsapp_provider_record (
session_id, revision_id, namespace, record_key,
codec_version, payload, updated_at
) VALUES ($1, $2::bigint, $3, $4, 1, $5, now())`,
[
this.sessionId,
revisionId,
CANONICAL_TRANSPORT_NAMESPACE,
CANONICAL_ROUTING_INFO_KEY,
routingInfoPayload,
],
);
}
// Monotonic device/PQ writes and preserved non-importable rows can make
// the database representation differ from the browser proposal. Read
// it back inside this same mutation transaction, then make the metadata
// and typed authority describe those exact durable bytes.
const persistedProjection = await this.loadCanonicalProjection({
revisionId,
client,
});
if (!persistedProjection?.device?.jid) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_projection_readback_missing',
);
}
const metadataReadback = await client.query(
`UPDATE whatsapp_provider_record
SET payload = $5, updated_at = now()
WHERE session_id = $1
AND revision_id = $2::bigint
AND namespace = $3
AND record_key = $4
AND codec_version = 1`,
[
this.sessionId,
revisionId,
CANONICAL_META_NAMESPACE,
CANONICAL_META_KEY,
canonicalMetadataPayload(
persistedProjection,
persistedAdvSecretAvailable,
),
],
);
if (metadataReadback.rowCount !== 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_projection_readback_metadata_changed',
);
}
return {
advSecretAvailable: persistedAdvSecretAvailable,
projection: persistedProjection,
};
}
normalizeProfileArtifactAnchor(
artifact,
revisionId = this.revisionId,
fallback,
) {
const artifactSizeBytes = Number(artifact.size_bytes);
const artifactChunkCount = Number(artifact.chunk_count);
if (
artifact.revision_id !== String(revisionId) ||
artifact.status !== 'ready' ||
!Number.isSafeInteger(artifactSizeBytes) ||
artifactSizeBytes < 1 ||
!Number.isSafeInteger(artifactChunkCount) ||
artifactChunkCount < 1
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_last_good_profile_invalid',
);
}
let metadata;
try {
metadata = validateProfileArtifactMetadata({
manifest: artifact.manifest,
expectedChecksum: artifact.checksum_sha256,
expectedWebVersion: this.webVersionIntegrityPinned
? this.webVersion
: undefined,
maxProfileBytes: this.maxProfileBytes,
expectedProfileFingerprint: profileFingerprint({
sessionId: this.sessionId,
revisionId,
}),
});
} catch (error) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_last_good_profile_invalid',
{
cause: safeDiagnosticErrorCode(
error?.code,
'whatsapp_artifact_manifest_invalid',
),
},
{ cause: error },
);
}
if (
!metadata.profileFingerprint ||
metadata.sizeBytes !== artifactSizeBytes ||
metadata.chunkCount !== artifactChunkCount
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_last_good_profile_invalid',
);
}
return {
artifact,
fallback,
metadata,
revisionId: String(revisionId),
chunkChecksums: artifact.manifest.files.flatMap((file) =>
file.chunks.map((chunk) => chunk.sha256),
),
chunkSizes: artifact.manifest.files.flatMap((file) =>
file.chunks.map((chunk) => chunk.size_bytes),
),
};
}
async loadLastGoodProfileAnchor(fallback, revisionId = this.revisionId) {
const artifact = await this.getCurrentRevisionArtifact();
if (!artifact) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_last_good_profile_missing',
);
}
return this.normalizeProfileArtifactAnchor(
artifact,
revisionId,
fallback,
);
}
async commitProfileAnchorCheckpointInTransaction(
client,
{
revisionId = this.revisionId,
artifactId,
expectedAnchor,
checkpointMode,
canonicalProjection,
source,
lastProfileObservedSizeBytes,
},
) {
if (
!UUID_PATTERN.test(String(artifactId || '')) ||
!PROFILE_ANCHOR_CHECKPOINT_MODES.has(checkpointMode) ||
typeof source !== 'string' ||
!/^[a-z][a-z0-9_]{0,63}$/u.test(source) ||
(lastProfileObservedSizeBytes !== undefined &&
(!Number.isSafeInteger(lastProfileObservedSizeBytes) ||
lastProfileObservedSizeBytes < 0))
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_anchor_checkpoint_invalid',
);
}
const canonicalChecksumSha256 =
canonicalProjectionChecksum(canonicalProjection);
const currentAppStateChecksumSha256 =
canonicalAppStateChecksum(canonicalProjection);
const committed = await client.query(
`SELECT *
FROM commit_wwebjs_profile_anchor_checkpoint_v2(
$1, $2::bigint, $3::uuid, $4::uuid, $5::bigint,
$6::bigint, $7, $8, $9::integer, $10::bigint,
$11, $12, $13::bigint
)`,
[
this.sessionId,
revisionId,
artifactId,
expectedAnchor?.artifactId ?? null,
expectedAnchor?.anchorGeneration ?? '0',
expectedAnchor?.canonicalGeneration ?? '0',
checkpointMode,
canonicalChecksumSha256,
canonicalProjection.record_count,
canonicalProjection.size_bytes,
currentAppStateChecksumSha256,
source,
lastProfileObservedSizeBytes ?? null,
],
);
if (committed.rowCount !== 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_anchor_checkpoint_rejected',
);
}
const checkpoint = normalizeProfileAnchorCheckpoint(committed.rows[0]);
if (
checkpoint.sessionId !== this.sessionId ||
checkpoint.revisionId !== String(revisionId) ||
checkpoint.artifactId !== artifactId ||
checkpoint.canonicalChecksumSha256 !== canonicalChecksumSha256 ||
checkpoint.currentAppStateChecksumSha256 !==
currentAppStateChecksumSha256 ||
checkpoint.canonicalRecordCount !==
canonicalProjection.record_count ||
checkpoint.canonicalSizeBytes !== canonicalProjection.size_bytes
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_anchor_checkpoint_proof_invalid',
);
}
await client.query(
`DELETE FROM whatsapp_provider_record
WHERE session_id = $1
AND revision_id = $2::bigint
AND namespace = $3
AND record_key = $4`,
[
this.sessionId,
revisionId,
LIFECYCLE_NAMESPACE,
LAST_GOOD_PROFILE_ANCHOR_KEY,
],
);
return checkpoint;
}
async assertLastGoodProfileAnchor(client, anchor) {
const result = await client.query(
`/* wwebjs_last_good_profile_anchor_guard */
SELECT artifact.chunk_count = $8::integer
AND artifact.chunk_count > 0
AND cardinality($9::text[]) = artifact.chunk_count
AND cardinality($10::integer[]) = artifact.chunk_count
AND chunk_summary.chunk_count =
artifact.chunk_count::bigint
AND chunk_summary.blob_count =
artifact.chunk_count::bigint
AND chunk_summary.byte_count = artifact.size_bytes
AND chunk_summary.min_index = 0
AND chunk_summary.max_index =
artifact.chunk_count - 1
AND NOT EXISTS (
SELECT 1
FROM unnest(
$9::text[], $10::integer[]
) WITH ORDINALITY
AS expected(
sha256, size_bytes, ordinality
)
LEFT JOIN whatsapp_artifact_chunk chunk
ON chunk.session_id = artifact.session_id
AND chunk.artifact_id = artifact.artifact_id
AND chunk.chunk_index =
expected.ordinality - 1
LEFT JOIN whatsapp_artifact_blob blob
ON blob.session_id = chunk.session_id
AND blob.sha256 = chunk.sha256
WHERE chunk.sha256 IS DISTINCT FROM
expected.sha256
OR blob.size_bytes IS DISTINCT FROM
expected.size_bytes
) AS chunks_complete
FROM whatsapp_artifact artifact
JOIN whatsapp_session_revision revision
ON revision.session_id = artifact.session_id
AND revision.revision_id = artifact.revision_id
AND revision.provider = 'wwebjs'
AND revision.status = 'active'
AND revision.checksum_sha256 = artifact.checksum_sha256
AND revision.size_bytes = artifact.size_bytes
JOIN whatsapp_session session
ON session.session_id = revision.session_id
AND session.provider = 'wwebjs'
AND (
session.state IN ('ready', 'handoff')
OR (
session.state = 'preparing'
AND $12::uuid IS NOT NULL
AND $13::uuid IS NOT NULL
AND $14 IN ('baileys', 'whatsmeow')
AND $15::bigint IS NOT NULL
AND EXISTS (
SELECT 1
FROM whatsapp_session_handoff target_handoff
WHERE target_handoff.session_id =
session.session_id
AND target_handoff.handoff_id = $12::uuid
AND target_handoff.lifecycle_operation_id =
$13::uuid
AND target_handoff.source_provider = $14
AND target_handoff.source_revision_id =
$15::bigint
AND target_handoff.target_provider = 'wwebjs'
AND target_handoff.target_revision_id =
revision.revision_id
AND target_handoff.state = 'activating'
AND target_handoff.point_of_no_return_at
IS NOT NULL
AND target_handoff.pre_activation_artifact_id
IS NOT NULL
AND target_handoff.completed_at IS NULL
)
)
)
AND session.active_revision_id = revision.revision_id
CROSS JOIN LATERAL (
SELECT count(chunk.chunk_index)::bigint AS chunk_count,
count(blob.sha256)::bigint AS blob_count,
COALESCE(sum(blob.size_bytes), 0)::bigint
AS byte_count,
min(chunk.chunk_index) AS min_index,
max(chunk.chunk_index) AS max_index
FROM whatsapp_artifact_chunk chunk
LEFT JOIN whatsapp_artifact_blob blob
ON blob.session_id = chunk.session_id
AND blob.sha256 = chunk.sha256
WHERE chunk.session_id = artifact.session_id
AND chunk.artifact_id = artifact.artifact_id
) chunk_summary
WHERE artifact.session_id = $1
AND artifact.revision_id = $2::bigint
AND artifact.artifact_id = $3::uuid
AND artifact.provider = 'wwebjs'
AND artifact.kind = $6
AND artifact.status = 'ready'
AND artifact.persisted_at IS NOT NULL
AND artifact.manifest = $11::jsonb
AND artifact.checksum_sha256 = $4
AND artifact.size_bytes = $5::bigint
AND artifact.size_bytes <= $7::bigint`,
[
this.sessionId,
anchor.revisionId,
anchor.artifact.artifact_id,
anchor.artifact.checksum_sha256,
anchor.metadata.sizeBytes,
PROFILE_ARTIFACT_KIND,
this.maxProfileBytes,
anchor.metadata.chunkCount,
anchor.chunkChecksums,
anchor.chunkSizes,
JSON.stringify(anchor.artifact.manifest),
this.handoffId ?? null,
this.lifecycleOperationId ?? null,
this.sourceProvider ?? null,
this.sourceRevisionId ?? null,
],
);
if (result.rowCount !== 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_last_good_profile_changed',
);
}
if (result.rows[0]?.chunks_complete !== true) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_last_good_profile_chunks_invalid',
);
}
}
async persistCanonicalWithLastGoodProfileAnchor(
client,
{
anchor,
canonicalProjection,
source,
checkpointMode = LAST_GOOD_PROFILE_FALLBACK_MODE,
},
) {
if (
!LAST_GOOD_PROFILE_ANCHOR_SOURCES.has(source) ||
![
LAST_GOOD_PROFILE_FALLBACK_MODE,
PROFILE_ANCHOR_CHECKPOINT_MODE,
].includes(checkpointMode) ||
(checkpointMode === LAST_GOOD_PROFILE_FALLBACK_MODE &&
!anchor.fallback)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_last_good_profile_fallback_invalid',
);
}
const expectedAnchor = await this.loadActiveProfileAnchorInTransaction(
client,
anchor.revisionId,
);
if (
expectedAnchor &&
expectedAnchor.artifactId !== anchor.artifact.artifact_id
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_last_good_profile_changed',
);
}
const persisted = await this.replaceCanonicalProjection(
client,
anchor.revisionId,
canonicalProjection,
);
const checkpoint =
await this.commitProfileAnchorCheckpointInTransaction(client, {
revisionId: anchor.revisionId,
artifactId: anchor.artifact.artifact_id,
expectedAnchor,
checkpointMode,
canonicalProjection: persisted.projection,
source,
lastProfileObservedSizeBytes: anchor.fallback?.sizeBytes,
});
return {
...persisted,
checkpoint,
canonicalChecksumSha256: checkpoint.canonicalChecksumSha256,
};
}
async checkpointCanonicalWithLastGoodProfile({
canonicalProjection,
lastGoodProfileFallback,
source,
} = {}) {
if (
this.revisionStatus !== 'active' ||
!LAST_GOOD_PROFILE_ANCHOR_SOURCES.has(source)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_last_good_profile_fallback_invalid',
);
}
const fallback = normalizeLastGoodProfileFallback(
lastGoodProfileFallback,
this.maxProfileBytes,
);
if (!fallback) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_last_good_profile_fallback_invalid',
);
}
const normalized = normalizeStoreCanonicalProjection(
canonicalProjection,
this.webVersion,
);
const anchor = await this.loadLastGoodProfileAnchor(fallback);
const persisted = await this.withLeaseMutation(async (client) => {
await this.assertLastGoodProfileAnchor(client, anchor);
const result = await this.persistCanonicalWithLastGoodProfileAnchor(
client,
{ anchor, canonicalProjection: normalized, source },
);
await client.query(
`UPDATE whatsapp_session
SET last_persisted_at = now(), updated_at = now()
WHERE session_id = $1
AND active_revision_id = $2::bigint`,
[this.sessionId, anchor.revisionId],
);
return result;
}, anchor.revisionId);
this.log(
'canonical_projection.last_good_profile_anchor_persisted',
{
source,
revision_id: anchor.revisionId,
profile_artifact_id: anchor.artifact.artifact_id,
rejected_profile_size_bytes: fallback.sizeBytes,
profile_max_bytes: fallback.maxBytes,
canonical_record_count: normalized.record_count,
canonical_size_bytes: normalized.size_bytes,
canonical_checksum_sha256: persisted.canonicalChecksumSha256,
adv_secret_available: persisted.advSecretAvailable,
},
{ force: true },
);
return {
revisionId: anchor.revisionId,
artifactId: anchor.artifact.artifact_id,
checksumSha256: anchor.artifact.checksum_sha256,
sizeBytes: anchor.metadata.sizeBytes,
chunkCount: anchor.metadata.chunkCount,
canonicalChecksumSha256: persisted.canonicalChecksumSha256,
anchorGeneration: persisted.checkpoint.anchorGeneration,
canonicalGeneration: persisted.checkpoint.canonicalGeneration,
appStateOverlayRequired:
persisted.checkpoint.appStateOverlayRequired,
profileCheckpointMode: LAST_GOOD_PROFILE_FALLBACK_MODE,
};
}
async saveCanonicalProjection({
projection,
source = 'checkpoint',
dirtyPlan,
} = {}) {
const normalized = normalizeStoreCanonicalProjection(
projection,
this.webVersion,
);
const persistencePlan = normalizeCanonicalDirtyPlan(
this.revisionStatus === 'active' ? undefined : dirtyPlan,
);
if (this.revisionStatus !== 'active') {
const canonical = await this.withLeaseMutation((client) =>
this.replaceCanonicalProjection(
client,
this.revisionId,
normalized,
persistencePlan,
),
);
this.log('canonical_projection.persisted', {
source,
record_count: normalized.record_count,
size_bytes: normalized.size_bytes,
adv_secret_available: canonical.advSecretAvailable,
full_replace: persistencePlan.fullReplace,
device_replaced: persistencePlan.device,
table_count: persistencePlan.tables.length,
profile_anchor: false,
});
return {
revisionId: this.revisionId,
recordCount: normalized.record_count,
sizeBytes: normalized.size_bytes,
};
}
const persisted = await this.withLeaseMutation(async (client) => {
const expectedAnchor =
await this.loadActiveProfileAnchorInTransaction(
client,
this.revisionId,
);
const artifact = expectedAnchor?.artifactId
? expectedAnchor.artifact
: await this.loadSoleReadyProfileArtifactInTransaction(
client,
this.revisionId,
);
if (!artifact?.artifact_id) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_anchor_artifact_missing',
);
}
const canonical = await this.replaceCanonicalProjection(
client,
this.revisionId,
normalized,
persistencePlan,
);
const checkpoint =
await this.commitProfileAnchorCheckpointInTransaction(client, {
artifactId: artifact.artifact_id,
expectedAnchor,
checkpointMode: expectedAnchor
? PROFILE_ANCHOR_CHECKPOINT_MODE
: 'legacy_adoption_v1',
canonicalProjection: canonical.projection,
source,
});
return { ...canonical, checkpoint };
});
this.log('canonical_projection.persisted', {
source,
record_count: persisted.projection.record_count,
size_bytes: persisted.projection.size_bytes,
adv_secret_available: persisted.advSecretAvailable,
full_replace: persistencePlan.fullReplace,
device_replaced: persistencePlan.device,
table_count: persistencePlan.tables.length,
profile_artifact_id: persisted.checkpoint.artifactId,
anchor_generation: persisted.checkpoint.anchorGeneration,
canonical_generation: persisted.checkpoint.canonicalGeneration,
app_state_overlay_required:
persisted.checkpoint.appStateOverlayRequired,
});
return {
revisionId: this.revisionId,
recordCount: persisted.projection.record_count,
sizeBytes: persisted.projection.size_bytes,
artifactId: persisted.checkpoint.artifactId,
checksumSha256: persisted.checkpoint.artifactChecksumSha256,
anchorGeneration: persisted.checkpoint.anchorGeneration,
canonicalGeneration: persisted.checkpoint.canonicalGeneration,
canonicalChecksumSha256:
persisted.checkpoint.canonicalChecksumSha256,
appStateOverlayRequired:
persisted.checkpoint.appStateOverlayRequired,
};
}
async saveCanonicalProviderState(providerState) {
const payload = encodeCanonicalProviderState(providerState);
if (!payload) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_provider_state_invalid',
);
}
await this.withLeaseMutation(async (client) => {
const device = await client.query(
`SELECT device_fingerprint
FROM whatsapp_device
WHERE session_id = $1 AND revision_id = $2::bigint`,
[this.sessionId, this.revisionId],
);
if (
device.rowCount !== 1 ||
!device.rows[0].device_fingerprint ||
!fingerprintsEqual(
device.rows[0].device_fingerprint,
providerState.device_fingerprint,
)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_provider_state_fingerprint_mismatch',
);
}
await client.query(
`INSERT INTO whatsapp_provider_record (
session_id, revision_id, namespace, record_key,
codec_version, payload, updated_at
) VALUES ($1, $2::bigint, $3, $4, 1, $5, now())
ON CONFLICT (
session_id, revision_id, namespace, record_key
) DO UPDATE SET
codec_version = EXCLUDED.codec_version,
payload = EXCLUDED.payload,
updated_at = now()`,
[
this.sessionId,
this.revisionId,
CANONICAL_META_NAMESPACE,
CANONICAL_PROVIDER_STATE_KEY,
payload,
],
);
});
this.log('canonical_provider_state.persisted', {
codec_version: 1,
certificate_chain_available: Boolean(
providerState.certificate_chain,
),
});
}
async markCanonicalActivationPending({
appStateHydrationRequired = false,
pqBootstrapRequired = false,
readyCheckpointArtifactId = null,
readyCheckpointChecksumSha256 = null,
} = {}) {
const hasReadyCheckpointArtifact = readyCheckpointArtifactId !== null;
const hasReadyCheckpointChecksum =
readyCheckpointChecksumSha256 !== null;
if (
hasReadyCheckpointArtifact !== hasReadyCheckpointChecksum ||
(hasReadyCheckpointArtifact &&
!UUID_PATTERN.test(String(readyCheckpointArtifactId))) ||
(hasReadyCheckpointChecksum &&
!/^[0-9a-f]{64}$/.test(String(readyCheckpointChecksumSha256)))
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_activation_checkpoint_proof_invalid',
);
}
const payload = Buffer.from(
JSON.stringify({
version: 1,
app_state_hydration_required:
appStateHydrationRequired === true,
pq_bootstrap_required: pqBootstrapRequired === true,
ready_checkpoint_artifact_id: readyCheckpointArtifactId,
ready_checkpoint_checksum_sha256: readyCheckpointChecksumSha256,
}),
'utf8',
);
await this.withLeaseMutation((client) =>
client.query(
`INSERT INTO whatsapp_provider_record (
session_id, revision_id, namespace, record_key,
codec_version, payload, updated_at
) VALUES ($1, $2::bigint, $3, $4, 1, $5, now())
ON CONFLICT (
session_id, revision_id, namespace, record_key
) DO UPDATE SET
codec_version = EXCLUDED.codec_version,
payload = EXCLUDED.payload,
updated_at = now()`,
[
this.sessionId,
this.revisionId,
LIFECYCLE_NAMESPACE,
CANONICAL_ACTIVATION_PENDING_KEY,
payload,
],
),
);
this.log(
'canonical_activation.pending',
{
app_state_hydration_required:
appStateHydrationRequired === true,
pq_bootstrap_required: pqBootstrapRequired === true,
ready_checkpoint_present: hasReadyCheckpointArtifact,
},
{
force: true,
},
);
}
async loadPendingCanonicalActivation() {
const result = await this.withLeaseTransaction((client) =>
client.query(
`SELECT marker.payload,
EXISTS (
SELECT 1
FROM whatsapp_session session
JOIN whatsapp_session_revision revision
ON revision.session_id = session.session_id
AND revision.revision_id =
session.active_revision_id
JOIN whatsapp_session_handoff handoff
ON handoff.session_id = session.session_id
AND handoff.target_revision_id =
revision.revision_id
WHERE session.session_id = marker.session_id
AND session.provider = 'wwebjs'
AND session.state = 'preparing'
AND session.previous_revision_id =
handoff.source_revision_id
AND revision.revision_id = marker.revision_id
AND revision.provider = 'wwebjs'
AND revision.status = 'active'
AND revision.checksum_sha256 IS NOT NULL
AND handoff.target_provider = 'wwebjs'
AND handoff.state = 'activating'
AND handoff.point_of_no_return_at IS NOT NULL
AND handoff.pre_activation_artifact_id
IS NOT NULL
AND EXISTS (
SELECT 1
FROM whatsapp_artifact artifact
WHERE artifact.session_id =
revision.session_id
AND artifact.revision_id =
revision.revision_id
AND artifact.provider = 'wwebjs'
AND artifact.kind = $5
AND artifact.status = 'ready'
AND artifact.checksum_sha256 =
revision.checksum_sha256
)
) AS lineage_valid
FROM whatsapp_provider_record marker
WHERE marker.session_id = $1
AND marker.revision_id = $2::bigint
AND marker.namespace = $3
AND marker.record_key = $4
AND marker.codec_version = 1
LIMIT 1`,
[
this.sessionId,
this.revisionId,
LIFECYCLE_NAMESPACE,
CANONICAL_ACTIVATION_PENDING_KEY,
PROFILE_ARTIFACT_KIND,
],
),
);
if (result.rowCount !== 1) return undefined;
if (result.rows[0]?.lineage_valid !== true) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_activation_lineage_invalid',
);
}
let metadata;
try {
metadata = JSON.parse(
Buffer.from(result.rows[0].payload).toString('utf8'),
);
} catch {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_activation_marker_invalid',
);
}
if (
metadata?.version !== 1 ||
typeof metadata.app_state_hydration_required !== 'boolean' ||
typeof metadata.pq_bootstrap_required !== 'boolean' ||
!(
metadata.ready_checkpoint_artifact_id === null ||
UUID_PATTERN.test(String(metadata.ready_checkpoint_artifact_id))
) ||
!(
metadata.ready_checkpoint_checksum_sha256 === null ||
/^[0-9a-f]{64}$/.test(
String(metadata.ready_checkpoint_checksum_sha256),
)
) ||
(metadata.ready_checkpoint_artifact_id === null) !==
(metadata.ready_checkpoint_checksum_sha256 === null)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_activation_marker_invalid',
);
}
const projection = await this.loadCanonicalProjection();
return {
projection,
appStateHydrationRequired: metadata.app_state_hydration_required,
pqBootstrapRequired: metadata.pq_bootstrap_required,
readyCheckpointArtifactId: metadata.ready_checkpoint_artifact_id,
readyCheckpointChecksumSha256:
metadata.ready_checkpoint_checksum_sha256,
noiseMetadataBootstrapRequired: Boolean(
this.revisionStatus === 'active' &&
this.revisionSource === 'handoff' &&
['baileys', 'whatsmeow'].includes(this.sourceProvider) &&
this.handoffId &&
this.lifecycleOperationId &&
this.sourceRevisionId &&
projection &&
projection.provider_state == null,
),
};
}
parsePostQuantumServerRollbackMarker(payload) {
let marker;
try {
marker = JSON.parse(Buffer.from(payload).toString('utf8'));
} catch {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_marker_invalid',
);
}
const baseValid =
marker?.version === 1 &&
['intent', 'acknowledged'].includes(marker.state) &&
UUID_PATTERN.test(String(marker.handoff_id || '')) &&
UUID_PATTERN.test(String(marker.lifecycle_operation_id || '')) &&
BIGINT_PATTERN.test(String(marker.source_revision_id || '')) &&
marker.target_provider === 'whatsmeow' &&
UUID_PATTERN.test(String(marker.owner_id || '')) &&
BIGINT_PATTERN.test(String(marker.fencing_token || '')) &&
Number.isSafeInteger(marker.generation) &&
marker.generation > 0 &&
UUID_PATTERN.test(String(marker.epoch || '')) &&
Number.isSafeInteger(marker.created_at_ms) &&
marker.created_at_ms > 0;
const acknowledgementValid =
marker.state === 'intent' ||
(marker.protocol === PQ_SERVER_ROLLBACK_PROTOCOL &&
marker.server_acknowledged === true &&
marker.local_cleanup_complete === true &&
marker.response_validated === true &&
Number.isSafeInteger(marker.acknowledged_at_ms) &&
marker.acknowledged_at_ms > 0 &&
marker.upload_lifecycle_fenced === true &&
marker.upload_lifecycle_fence_version === 1 &&
typeof marker.runtime_upload_enabled === 'boolean' &&
typeof marker.runtime_messaging_enabled === 'boolean');
if (!baseValid || !acknowledgementValid) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_marker_invalid',
);
}
return marker;
}
assertPostQuantumServerRollbackProof(marker, authorized, capabilities) {
const matches =
marker?.state === 'acknowledged' &&
marker.handoff_id === authorized?.handoffId &&
marker.lifecycle_operation_id ===
authorized?.lifecycleOperationId &&
marker.source_revision_id ===
String(authorized?.sourceRevisionId || '') &&
marker.owner_id === this.ownerId &&
marker.fencing_token === String(this.fencingToken) &&
marker.generation === this.generation &&
marker.epoch === this.epoch &&
capabilities?.pq_server_rollback_acknowledged === true &&
capabilities.pq_server_rollback_protocol === marker.protocol &&
capabilities.pq_server_rollback_handoff_id === marker.handoff_id &&
capabilities.pq_server_rollback_lifecycle_operation_id ===
marker.lifecycle_operation_id &&
String(capabilities.pq_server_rollback_source_revision_id || '') ===
marker.source_revision_id &&
capabilities.pq_server_rollback_owner_id === marker.owner_id &&
String(capabilities.pq_server_rollback_fencing_token || '') ===
marker.fencing_token &&
capabilities.pq_server_rollback_generation === marker.generation &&
capabilities.pq_server_rollback_epoch === marker.epoch &&
capabilities.pq_server_rollback_acknowledged_at_ms ===
marker.acknowledged_at_ms &&
capabilities.pq_server_rollback_response_validated === true &&
capabilities.pq_upload_lifecycle_fenced === true &&
capabilities.pq_upload_lifecycle_fence_version === 1 &&
capabilities.pq_runtime_upload_enabled ===
marker.runtime_upload_enabled &&
capabilities.pq_runtime_messaging_enabled ===
marker.runtime_messaging_enabled;
if (!matches) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_proof_lineage_invalid',
);
}
return marker;
}
async beginPostQuantumServerRollbackIntent(authorized) {
if (
authorized?.targetProvider !== 'whatsmeow' ||
!UUID_PATTERN.test(String(authorized.handoffId || '')) ||
!UUID_PATTERN.test(String(authorized.lifecycleOperationId || '')) ||
!BIGINT_PATTERN.test(String(authorized.sourceRevisionId || '')) ||
String(authorized.sourceRevisionId) !== String(this.revisionId)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_intent_lineage_invalid',
);
}
const marker = {
version: 1,
state: 'intent',
handoff_id: authorized.handoffId,
lifecycle_operation_id: authorized.lifecycleOperationId,
source_revision_id: String(authorized.sourceRevisionId),
target_provider: 'whatsmeow',
owner_id: this.ownerId,
fencing_token: String(this.fencingToken),
generation: this.generation,
epoch: this.epoch,
created_at_ms: Date.now(),
};
const payload = Buffer.from(JSON.stringify(marker), 'utf8');
await this.withLeaseMutation(async (client) => {
const authorization = await client.query(
`SELECT EXISTS (
SELECT 1
FROM whatsapp_session session
JOIN whatsapp_session_revision revision
ON revision.session_id = session.session_id
AND revision.revision_id = session.active_revision_id
JOIN whatsapp_session_handoff handoff
ON handoff.session_id = session.session_id
AND handoff.source_revision_id = revision.revision_id
WHERE session.session_id = $1
AND session.provider = 'wwebjs'
AND session.state = 'handoff'
AND session.active_revision_id = $2::bigint
AND revision.provider = 'wwebjs'
AND revision.status = 'active'
AND handoff.handoff_id = $3::uuid
AND handoff.lifecycle_operation_id = $4::uuid
AND handoff.source_provider = 'wwebjs'
AND handoff.target_provider = 'whatsmeow'
AND handoff.state IN (
'requested', 'draining', 'transforming'
)
) AS authorized`,
[
this.sessionId,
this.revisionId,
authorized.handoffId,
authorized.lifecycleOperationId,
],
);
if (authorization.rows[0]?.authorized !== true) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_manager_handoff_missing',
);
}
const persisted = await client.query(
`INSERT INTO whatsapp_provider_record (
session_id, revision_id, namespace, record_key,
codec_version, payload, updated_at
) VALUES ($1, $2::bigint, $3, $4, 1, $5, now())
ON CONFLICT (
session_id, revision_id, namespace, record_key
) DO UPDATE SET updated_at =
whatsapp_provider_record.updated_at
WHERE convert_from(
whatsapp_provider_record.payload, 'UTF8'
)::jsonb->>'handoff_id' = $6
AND convert_from(
whatsapp_provider_record.payload, 'UTF8'
)::jsonb->>'lifecycle_operation_id' = $7
AND convert_from(
whatsapp_provider_record.payload, 'UTF8'
)::jsonb->>'source_revision_id' = $8
RETURNING 1 AS persisted`,
[
this.sessionId,
this.revisionId,
LIFECYCLE_NAMESPACE,
PQ_SERVER_ROLLBACK_MARKER_KEY,
payload,
authorized.handoffId,
authorized.lifecycleOperationId,
String(authorized.sourceRevisionId),
],
);
if (persisted.rowCount !== 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_intent_conflict',
);
}
});
this.log(
'handoff.pq_server_rollback_intent_persisted',
{ handoff_id: authorized.handoffId },
{ force: true },
);
return marker;
}
async acknowledgePostQuantumServerRollback(marker, proof) {
if (
proof?.protocol !== PQ_SERVER_ROLLBACK_PROTOCOL ||
proof.server_acknowledged !== true ||
proof.local_cleanup_complete !== true ||
!Number.isSafeInteger(proof.acknowledged_at_ms) ||
proof.acknowledged_at_ms <= 0 ||
proof.upload_lifecycle_fenced !== true ||
proof.upload_lifecycle_fence_version !== 1 ||
typeof proof.runtime_upload_enabled !== 'boolean' ||
typeof proof.runtime_messaging_enabled !== 'boolean'
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_ack_invalid',
);
}
let acknowledged;
await this.withLeaseMutation(async (client) => {
const result = await client.query(
`SELECT payload
FROM whatsapp_provider_record
WHERE session_id = $1 AND revision_id = $2::bigint
AND namespace = $3 AND record_key = $4
AND codec_version = 1
FOR UPDATE`,
[
this.sessionId,
this.revisionId,
LIFECYCLE_NAMESPACE,
PQ_SERVER_ROLLBACK_MARKER_KEY,
],
);
if (result.rowCount !== 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_intent_missing',
);
}
const current = this.parsePostQuantumServerRollbackMarker(
result.rows[0].payload,
);
if (
current.handoff_id !== marker.handoff_id ||
current.lifecycle_operation_id !==
marker.lifecycle_operation_id ||
current.source_revision_id !== marker.source_revision_id ||
current.owner_id !== this.ownerId ||
current.fencing_token !== String(this.fencingToken) ||
current.generation !== this.generation ||
current.epoch !== this.epoch
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_intent_fence_stale',
);
}
acknowledged = {
...current,
state: 'acknowledged',
protocol: proof.protocol,
server_acknowledged: true,
local_cleanup_complete: true,
acknowledged_at_ms: proof.acknowledged_at_ms,
response_validated: true,
upload_lifecycle_fenced: true,
upload_lifecycle_fence_version: 1,
runtime_upload_enabled: proof.runtime_upload_enabled,
runtime_messaging_enabled: proof.runtime_messaging_enabled,
};
const updated = await client.query(
`UPDATE whatsapp_provider_record
SET payload = $5, updated_at = now()
WHERE session_id = $1 AND revision_id = $2::bigint
AND namespace = $3 AND record_key = $4
AND payload = $6
RETURNING 1 AS updated`,
[
this.sessionId,
this.revisionId,
LIFECYCLE_NAMESPACE,
PQ_SERVER_ROLLBACK_MARKER_KEY,
Buffer.from(JSON.stringify(acknowledged), 'utf8'),
Buffer.from(result.rows[0].payload),
],
);
if (updated.rowCount !== 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_ack_fence_stale',
);
}
});
this.log(
'handoff.pq_server_rollback_ack_persisted',
{ handoff_id: acknowledged.handoff_id },
{ force: true },
);
return acknowledged;
}
async loadPendingPostQuantumServerRollback() {
const result = await this.withLeaseTransaction((client) =>
client.query(
`SELECT marker.payload,
EXISTS (
SELECT 1
FROM whatsapp_session session
JOIN whatsapp_session_revision revision
ON revision.session_id = session.session_id
AND revision.revision_id =
session.active_revision_id
WHERE session.session_id = marker.session_id
AND session.provider = 'wwebjs'
AND session.active_revision_id =
marker.revision_id
AND revision.provider = 'wwebjs'
AND revision.status = 'active'
) AS lineage_valid
FROM whatsapp_provider_record marker
WHERE marker.session_id = $1
AND marker.revision_id = $2::bigint
AND marker.namespace = $3
AND marker.record_key = $4
AND marker.codec_version = 1
LIMIT 1`,
[
this.sessionId,
this.revisionId,
LIFECYCLE_NAMESPACE,
PQ_SERVER_ROLLBACK_MARKER_KEY,
],
),
);
if (result.rowCount !== 1) return undefined;
if (result.rows[0]?.lineage_valid !== true) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_marker_lineage_invalid',
);
}
return this.parsePostQuantumServerRollbackMarker(
result.rows[0].payload,
);
}
async completePostQuantumServerRollbackRecovery(marker, recovery = {}) {
const rolloutDisabled =
recovery?.deferred === true &&
recovery?.reason === 'rollout_disabled' &&
recovery?.migrated === false &&
recovery?.pre_key_count === 0 &&
recovery?.unsent_pre_key_count === 0 &&
recovery?.last_resort_key_count === 0;
const result = await this.withLeaseMutation((client) =>
client.query(
`DELETE FROM whatsapp_provider_record
WHERE session_id = $1 AND revision_id = $2::bigint
AND namespace = $3 AND record_key = $4
AND codec_version = 1
AND convert_from(payload, 'UTF8')::jsonb->>'handoff_id' =
$5
AND convert_from(payload, 'UTF8')::jsonb
->>'lifecycle_operation_id' = $7
AND convert_from(payload, 'UTF8')::jsonb
->>'source_revision_id' = $6
AND (
(
$8::boolean = false
AND EXISTS (
SELECT 1
FROM whatsapp_pq_pre_key_state state
WHERE state.session_id = $1
AND state.revision_id = $2::bigint
AND state.migrated = true
)
AND (
SELECT count(*)
FROM whatsapp_pq_pre_keys key
WHERE key.session_id = $1
AND key.revision_id = $2::bigint
) > 0
AND (
SELECT count(*)
FROM whatsapp_pq_pre_keys key
WHERE key.session_id = $1
AND key.revision_id = $2::bigint
AND key.key_kind = 'last_resort'
AND key.sent_to_server = true
) = 1
AND NOT EXISTS (
SELECT 1
FROM whatsapp_pq_pre_keys key
WHERE key.session_id = $1
AND key.revision_id = $2::bigint
AND key.key_kind = 'one_time'
AND key.sent_to_server = false
)
)
OR (
$8::boolean = true
AND convert_from(payload, 'UTF8')::jsonb
->>'runtime_upload_enabled' = 'false'
AND convert_from(payload, 'UTF8')::jsonb
->>'runtime_messaging_enabled' = 'false'
AND NOT EXISTS (
SELECT 1
FROM whatsapp_pq_pre_key_state state
WHERE state.session_id = $1
AND state.revision_id = $2::bigint
AND state.migrated = true
)
AND NOT EXISTS (
SELECT 1
FROM whatsapp_pq_pre_keys key
WHERE key.session_id = $1
AND key.revision_id = $2::bigint
)
)
)
RETURNING 1 AS deleted`,
[
this.sessionId,
this.revisionId,
LIFECYCLE_NAMESPACE,
PQ_SERVER_ROLLBACK_MARKER_KEY,
marker.handoff_id,
String(marker.source_revision_id),
marker.lifecycle_operation_id,
rolloutDisabled,
],
),
);
if (result.rowCount !== 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_recovery_not_durable',
);
}
this.log(
'handoff.pq_source_recovery_completed',
{ handoff_id: marker.handoff_id },
{ force: true },
);
}
async assertCanonicalActivationComplete() {
const result = await this.withLeaseTransaction((client) =>
client.query(
`SELECT NOT EXISTS (
SELECT 1
FROM whatsapp_provider_record
WHERE session_id = $1
AND revision_id = $2::bigint
AND namespace = $3
AND record_key = $4
) AS complete`,
[
this.sessionId,
this.revisionId,
LIFECYCLE_NAMESPACE,
CANONICAL_ACTIVATION_PENDING_KEY,
],
),
);
if (result.rows[0]?.complete !== true) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_activation_pending',
);
}
return true;
}
async promoteInTransaction(client, revisionId) {
if (this.revisionStatus === 'active') {
const checkpoint = await client.query(
`UPDATE whatsapp_session
SET last_persisted_at = now(), updated_at = now()
WHERE session_id = $1
AND active_revision_id = $2::bigint
AND provider = 'wwebjs'
RETURNING active_revision_id`,
[this.sessionId, revisionId],
);
if (checkpoint.rowCount !== 1) {
throw new WhatsAppSessionError(
'whatsapp_session_checkpoint_header_changed',
);
}
return;
}
if (
this.revisionSource === 'legacy_volume_migration' &&
!this.sourceRevisionId
) {
if (!this.storageMigrationId) {
throw new WhatsAppSessionError(
'whatsapp_session_storage_migration_id_missing',
);
}
const promoted = await client.query(
`SELECT promote_legacy_volume_migration_revision(
$1, $2, $3::bigint, $4, $5::bigint, $6, $7, $8
) AS promoted`,
[
this.storageMigrationId,
this.sessionId,
revisionId,
this.ownerId,
this.fencingToken,
this.generation,
this.epoch,
this.runtimeCapability,
],
);
if (promoted.rows[0]?.promoted !== true) {
throw new WhatsAppSessionError(
'whatsapp_session_storage_migration_promotion_failed',
);
}
return;
}
if (this.revisionSource === 'pairing' && !this.sourceRevisionId) {
const finalized = await client.query(
`SELECT finalize_whatsapp_session_pairing(
$1, $2::bigint, $3, $4::bigint, $5, $6, $7
) AS finalized`,
[
this.sessionId,
revisionId,
this.ownerId,
this.fencingToken,
this.generation,
this.epoch,
this.runtimeCapability,
],
);
if (finalized.rows[0]?.finalized !== true) {
throw new WhatsAppSessionError(
'whatsapp_session_pairing_finalization_failed',
);
}
return;
}
if (!this.sourceRevisionId) {
throw new WhatsAppSessionError(
'whatsapp_session_candidate_source_missing',
);
}
const promoted = await client.query(
`SELECT promote_whatsapp_session_revision(
$1, $2::bigint, $3::bigint, $4, $5::bigint, $6, $7, $8, $9
) AS promoted`,
[
this.sessionId,
this.sourceRevisionId,
revisionId,
this.ownerId,
this.fencingToken,
this.generation,
this.epoch,
this.runtimeCapability,
this.validatedJid || null,
],
);
if (promoted.rows[0]?.promoted !== true) {
throw new WhatsAppSessionError('whatsapp_session_promotion_failed');
}
}
async deleteOrphanedBlobs(client) {
const pruned = await client.query(
`SELECT prune_whatsapp_orphan_artifact_blobs($1) AS pruned`,
[this.sessionId],
);
if (pruned.rows[0]?.pruned !== true) {
throw new WhatsAppSessionError(
'whatsapp_artifact_blob_prune_rejected',
);
}
}
async restoreProfile({ profilePath, selector = 'active' }) {
if (selector === 'previous') {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_previous_profile_restore_requires_explicit_pre_activation_rollback',
);
}
const retainCandidate = ['staging', 'validating'].includes(
this.revisionStatus,
);
const initializeEmptyProfile = () =>
ensureProfileFingerprint({
profilePath,
expectedFingerprint: profileFingerprint({
sessionId: this.sessionId,
revisionId: this.revisionId,
}),
allowCreate: true,
logger: this.logger,
reason: 'empty_revision_profile',
});
let artifact;
let profileAuthority;
if (selector === 'active' && retainCandidate) {
artifact = await this.getCurrentRevisionArtifact();
if (!artifact) {
const canonicalProjection = this.isHandoffRevision()
? await this.loadCanonicalProjection()
: undefined;
const reusableAnchor = canonicalProjection
? await this.loadReusableHandoffProfileAnchor(
canonicalProjection,
)
: undefined;
if (reusableAnchor) {
artifact = reusableAnchor.artifact;
this.reusableHandoffProfileAuthority = reusableAnchor;
} else {
await initializeEmptyProfile();
return false;
}
}
}
if (
selector === 'active' &&
!retainCandidate &&
!this.reusableHandoffProfileAuthority
) {
profileAuthority = await this.loadActiveProfileAnchor();
artifact = profileAuthority?.artifact;
if (
this.revisionSource === 'handoff' &&
this.sourceProvider &&
this.sourceProvider !== PROVIDER &&
(!profileAuthority ||
profileAuthority.appStateOverlayRequired === true)
) {
const canonicalProjection =
await this.loadCanonicalProjection();
const reusableAnchor =
await this.loadReusableHandoffProfileAnchor(
canonicalProjection,
);
if (reusableAnchor) {
artifact = reusableAnchor.artifact;
this.reusableHandoffProfileAuthority = reusableAnchor;
this.log(
'handoff.reusable_profile_preferred_for_activation_resume',
{
target_profile_anchor_present:
Boolean(profileAuthority),
target_profile_app_state_overlay_required:
profileAuthority?.appStateOverlayRequired ===
true,
source_revision_id: reusableAnchor.revisionId,
source_anchor_generation:
reusableAnchor.anchorGeneration,
source_canonical_generation:
reusableAnchor.canonicalGeneration,
app_state_checksum_matched: true,
},
{ force: true },
);
}
}
}
artifact ||= await this.getArtifact(selector);
if (!artifact) {
if (selector === 'active') {
if (this.revisionStatus !== 'active') {
await initializeEmptyProfile();
}
return false;
}
throw new WhatsAppSessionError(
'whatsapp_session_previous_artifact_missing',
);
}
const expectedProfileFingerprint = profileFingerprint({
sessionId: this.sessionId,
revisionId: artifact.revision_id,
});
await restoreProfileArtifact({
profilePath,
manifest: artifact.manifest,
expectedChecksum: artifact.checksum_sha256,
expectedWebVersion: this.webVersionIntegrityPinned
? this.webVersion
: undefined,
maxProfileBytes: this.maxProfileBytes,
getBlob: (checksum) => this.getArtifactBlob(checksum),
getBlobs: (checksums) => this.getArtifactBlobs(checksums),
logger: this.logger,
expectedProfileFingerprint,
});
if (this.reusableHandoffProfileAuthority) {
const rebound = await rebindProfileFingerprint({
profilePath,
expectedCurrentFingerprint: expectedProfileFingerprint,
nextFingerprint: profileFingerprint({
sessionId: this.sessionId,
revisionId: this.revisionId,
}),
logger: this.logger,
reason: 'reusable_handoff_profile',
});
if (!rebound) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_reusable_handoff_profile_rebind_failed',
);
}
this.log(
'handoff.reusable_profile_restored',
{
source_revision_id:
this.reusableHandoffProfileAuthority.revisionId,
source_anchor_generation:
this.reusableHandoffProfileAuthority.anchorGeneration,
source_canonical_generation:
this.reusableHandoffProfileAuthority
.canonicalGeneration,
app_state_checksum_matched: true,
},
{ force: true },
);
}
if (selector === 'active' && !retainCandidate) {
// A post-PONR cross-provider retry can deliberately restore an
// older, checksum-matched WWeb profile when the target-local
// anchor still requires a canonical app-state overlay. In that
// case the target anchor remains the immutable recovery proof;
// compare it against its own revision instead of comparing it to
// the reusable source anchor that was restored into Chromium.
const currentAuthority = await this.loadActiveProfileAnchor(
this.reusableHandoffProfileAuthority
? this.revisionId
: artifact.revision_id,
);
if (
profileAuthority &&
!this.sameProfileAnchorCheckpoint(
profileAuthority,
currentAuthority,
)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_anchor_changed_during_restore',
);
}
if (!profileAuthority && currentAuthority) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_anchor_changed_during_restore',
);
}
this.restoredProfileAuthority = profileAuthority || {
legacy: true,
revisionId: String(artifact.revision_id),
artifactId: artifact.artifact_id,
artifactChecksumSha256: artifact.checksum_sha256,
artifactSizeBytes: Number(artifact.size_bytes),
artifactChunkCount: Number(artifact.chunk_count),
artifactPersistedAt: artifact.persisted_at,
appStateOverlayRequired: true,
};
}
if (!retainCandidate && !this.reusableHandoffProfileAuthority) {
this.revisionId = artifact.revision_id;
this.revisionSource = artifact.source;
this.revisionStatus = selector === 'active' ? 'active' : 'retired';
}
this.updateLoggerContext({ revisionId: this.revisionId });
return true;
}
async loadCanonicalRestartAuthority() {
if (
this.revisionStatus !== 'active' ||
!this.restoredProfileAuthority
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_anchor_restore_authority_missing',
);
}
const authorityBefore = await this.loadActiveProfileAnchor();
const projection = await this.loadCanonicalProjection();
const authorityAfter = await this.loadActiveProfileAnchor();
if (!projection?.device?.jid) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_active_canonical_projection_missing',
);
}
if (
!this.sameProfileAnchorCheckpoint(
authorityBefore,
authorityAfter,
) ||
(authorityAfter &&
!this.sameProfileAnchorCheckpoint(
authorityAfter,
this.restoredProfileAuthority,
)) ||
(!authorityAfter && !this.restoredProfileAuthority.legacy)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_anchor_changed_during_restore',
);
}
const projectionChecksumSha256 =
canonicalProjectionChecksum(projection);
const appStateChecksumSha256 = canonicalAppStateChecksum(projection);
if (
authorityAfter &&
(projectionChecksumSha256 !==
authorityAfter.canonicalChecksumSha256 ||
Number(projection.record_count) !==
authorityAfter.canonicalRecordCount ||
Number(projection.size_bytes) !==
authorityAfter.canonicalSizeBytes ||
appStateChecksumSha256 !==
authorityAfter.currentAppStateChecksumSha256)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_profile_anchor_canonical_mismatch',
);
}
return {
projection,
appStateOverlayRequired:
authorityAfter?.appStateOverlayRequired !== false,
canonicalGeneration: authorityAfter?.canonicalGeneration,
profileArtifactId: this.restoredProfileAuthority.artifactId,
legacy: !authorityAfter,
};
}
async loadProjection(revisionId = this.revisionId) {
return this.withLeaseTransaction(async (client) => {
const result = await client.query(
`SELECT namespace, record_key, codec_version, payload
FROM whatsapp_provider_record
WHERE session_id = $1
AND revision_id = $2::bigint
AND namespace <> ALL($3::text[])
ORDER BY namespace, record_key`,
[
this.sessionId,
revisionId,
[
CANONICAL_META_NAMESPACE,
CANONICAL_TRANSPORT_NAMESPACE,
LIFECYCLE_NAMESPACE,
],
],
);
if (!Array.isArray(result.rows)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_projection_metadata_invalid',
);
}
if (result.rows.length === 0) return undefined;
if (!result.rows.every(isObjectRecord)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_projection_metadata_invalid',
);
}
if (
result.rows.some(
(row) =>
Number(row.codec_version) !==
PROVIDER_RECORD_CODEC_VERSION,
)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_provider_record_codec_unsupported',
);
}
if (result.rows.length - 1 > MAX_PROVIDER_PROJECTION_RECORDS) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_projection_limit_exceeded',
);
}
const metadataRows = result.rows.filter(
(row) =>
row.namespace === PROJECTION_META_NAMESPACE &&
row.record_key === PROJECTION_META_KEY,
);
if (metadataRows.length === 0) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_projection_metadata_missing',
);
}
if (metadataRows.length !== 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_projection_metadata_invalid',
);
}
const { bytes: metadataBytes, value: rawMetadata } =
parseProjectionPayload(
metadataRows[0].payload,
'wwebjs_projection_metadata_invalid',
);
const metadata = normalizeProjectionMetadata(
rawMetadata,
this.webVersion,
this.webVersionIntegrityPinned,
);
const indexedDbStores = new Set(
metadata.indexeddb_stores.map((store) =>
indexedDbStoreKey(store.database_name, store.store_name),
),
);
let totalBytes = metadataBytes.length;
if (totalBytes > MAX_PROVIDER_PROJECTION_BYTES) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_projection_limit_exceeded',
);
}
const addresses = new Set();
const records = result.rows
.filter(
(row) =>
!(
row.namespace === PROJECTION_META_NAMESPACE &&
row.record_key === PROJECTION_META_KEY
),
)
.map((row) => {
const { bytes, value } = parseProjectionPayload(
row.payload,
'wwebjs_projection_record_invalid',
);
totalBytes += bytes.length;
if (totalBytes > MAX_PROVIDER_PROJECTION_BYTES) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_projection_limit_exceeded',
);
}
const normalized = normalizeProjectionRecord(
value,
indexedDbStores,
);
if (
row.namespace !== normalized.address.namespace ||
row.record_key !== normalized.address.recordKey
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_projection_record_invalid',
);
}
const key = `${row.namespace}\u0000${row.record_key}`;
if (addresses.has(key)) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_projection_record_invalid',
);
}
addresses.add(key);
return normalized.record;
});
if (metadata.size_bytes !== totalBytes - metadataBytes.length) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_projection_metadata_invalid',
);
}
return {
schema_version: BRIDGE_SCHEMA_VERSION,
web_version: metadata.web_version,
complete: metadata.complete,
lossy_records: metadata.lossy_records,
size_bytes: metadata.size_bytes,
indexeddb_stores: metadata.indexeddb_stores,
records,
};
}, revisionId);
}
async preflightCanonicalSnapshot(
client,
revisionId,
providerNamespaces = [],
) {
const aggregates = ALL_HANDOFF_STATE_CONFIGS.map(
({ table }) =>
`SELECT count(*)::bigint AS row_count,
COALESCE(sum(pg_column_size(state_row)), 0)::bigint
AS byte_count
FROM ${table} AS state_row
WHERE session_id = $1 AND revision_id = $2::bigint`,
);
if (providerNamespaces.length > 0) {
aggregates.push(
`SELECT count(*)::bigint AS row_count,
COALESCE(sum(pg_column_size(state_row)), 0)::bigint
AS byte_count
FROM whatsapp_provider_record AS state_row
WHERE session_id = $1
AND revision_id = $2::bigint
AND namespace = ANY($3::text[])`,
);
}
const result = await client.query(
`/* wwebjs_canonical_snapshot_preflight */
SELECT COALESCE(sum(row_count), 0)::text AS row_count,
COALESCE(sum(byte_count), 0)::text AS byte_count
FROM (${aggregates.join(' UNION ALL ')}) AS totals`,
providerNamespaces.length > 0
? [this.sessionId, revisionId, providerNamespaces]
: [this.sessionId, revisionId],
);
const rowCount = BigInt(result.rows[0]?.row_count || 0);
const byteCount = BigInt(result.rows[0]?.byte_count || 0);
if (
rowCount > BigInt(MAX_HANDOFF_SOURCE_ROWS) ||
byteCount > BigInt(DEFAULT_MAX_CANONICAL_BYTES)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_snapshot_limit_exceeded',
{
row_count: rowCount.toString(),
byte_count: byteCount.toString(),
},
);
}
return { rowCount, byteCount };
}
async readAuthorizedHandoffSource() {
const client = await this.pool.connect();
try {
await client.query(
'BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY',
);
const scopeResult = await client.query(
`SELECT source_provider,
source_revision_id::text AS source_revision_id,
target_provider,
target_revision_id::text AS target_revision_id,
handoff_id
FROM begin_whatsapp_handoff_source_read(
$1, $2::bigint, $3, $4::bigint,
$5, $6, $7, 'wwebjs'
)`,
[
this.sessionId,
this.revisionId,
this.ownerId,
this.fencingToken,
this.generation,
this.epoch,
this.runtimeCapability,
],
);
const scope = scopeResult.rows[0];
const lifecycleResult = scope
? await client.query(
`SELECT lifecycle_operation_id::text
FROM whatsapp_session_handoff
WHERE session_id = $1
AND handoff_id = $2::uuid
AND source_revision_id = $3::bigint
AND target_revision_id = $4::bigint
AND source_provider = $5
AND target_provider = 'wwebjs'
LIMIT 2`,
[
this.sessionId,
scope.handoff_id,
scope.source_revision_id,
scope.target_revision_id,
scope.source_provider,
],
)
: { rowCount: 0, rows: [] };
if (
scope &&
lifecycleResult.rowCount === 1 &&
lifecycleResult.rows[0]
) {
scope.lifecycle_operation_id =
lifecycleResult.rows[0].lifecycle_operation_id;
}
if (
scopeResult.rowCount !== 1 ||
lifecycleResult.rowCount !== 1 ||
!lifecycleResult.rows[0] ||
!scope ||
!['baileys', 'wwebjs', 'whatsmeow'].includes(
scope.source_provider,
) ||
scope.target_provider !== PROVIDER ||
String(scope.target_revision_id) !== String(this.revisionId) ||
(this.sourceRevisionId &&
String(scope.source_revision_id) !==
String(this.sourceRevisionId)) ||
(this.sourceProvider &&
scope.source_provider !== this.sourceProvider) ||
(this.handoffId && scope.handoff_id !== this.handoffId) ||
!UUID_PATTERN.test(
String(scope.lifecycle_operation_id || ''),
) ||
(this.lifecycleOperationId &&
scope.lifecycle_operation_id !== this.lifecycleOperationId)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_handoff_source_scope_invalid',
);
}
await this.preflightCanonicalSnapshot(
client,
scope.source_revision_id,
scope.source_provider === PROVIDER
? [CANONICAL_META_NAMESPACE, CANONICAL_TRANSPORT_NAMESPACE]
: [CANONICAL_TRANSPORT_NAMESPACE],
);
const parameters = [this.sessionId, scope.source_revision_id];
const deviceResult = await client.query(
`SELECT jid, lid, facebook_uuid, registration_id,
noise_key, identity_key, signed_pre_key,
signed_pre_key_id, signed_pre_key_sig, adv_key,
adv_secret_available, adv_details,
adv_account_sig, adv_account_sig_key,
adv_device_sig, platform, business_name, push_name,
lid_migration_ts, next_pre_key_id,
device_fingerprint, fingerprint_version
FROM whatsapp_device
WHERE session_id = $1 AND revision_id = $2::bigint`,
parameters,
);
if (deviceResult.rowCount !== 1 || !deviceResult.rows[0]) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_handoff_source_device_missing',
);
}
const snapshot = {
device: deviceResult.rows[0],
identityKeys: [],
preKeys: [],
pqPreKeys: [],
pqPreKeyState: [],
signalSessions: [],
senderKeys: [],
syncKeys: [],
versions: [],
mutationMacs: [],
passthroughTables: {},
providerRecords: [],
};
const snapshotKeyByTable = {
whatsapp_identity_keys: 'identityKeys',
whatsapp_pre_keys: 'preKeys',
whatsapp_pq_pre_keys: 'pqPreKeys',
whatsapp_pq_pre_key_state: 'pqPreKeyState',
whatsapp_signal_sessions: 'signalSessions',
whatsapp_sender_keys: 'senderKeys',
whatsapp_app_state_sync_keys: 'syncKeys',
whatsapp_app_state_version: 'versions',
whatsapp_app_state_mutation_macs: 'mutationMacs',
};
let remainingRows = MAX_HANDOFF_SOURCE_ROWS;
for (const { table, columns } of CANONICAL_TABLE_CONFIGS) {
const result = await client.query(
`SELECT ${columns.join(', ')}
FROM ${table}
WHERE session_id = $1 AND revision_id = $2::bigint
LIMIT $3`,
[...parameters, remainingRows + 1],
);
if (result.rows.length > remainingRows) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_projection_record_limit_exceeded',
);
}
remainingRows -= result.rows.length;
snapshot[snapshotKeyByTable[table]] = result.rows;
}
for (const { table, columns } of PASSTHROUGH_TABLE_CONFIGS) {
const result = await client.query(
`SELECT ${columns.join(', ')}
FROM ${table}
WHERE session_id = $1 AND revision_id = $2::bigint
LIMIT $3`,
[...parameters, remainingRows + 1],
);
if (result.rows.length > remainingRows) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_projection_record_limit_exceeded',
);
}
remainingRows -= result.rows.length;
snapshot.passthroughTables[table] = result.rows;
}
const providerNamespaces =
scope.source_provider === PROVIDER
? [CANONICAL_META_NAMESPACE, CANONICAL_TRANSPORT_NAMESPACE]
: [CANONICAL_TRANSPORT_NAMESPACE];
const providerRecords = await client.query(
`SELECT namespace, record_key, codec_version, payload
FROM whatsapp_provider_record
WHERE session_id = $1
AND revision_id = $2::bigint
AND namespace = ANY($3::text[])
ORDER BY namespace, record_key
LIMIT 4`,
[...parameters, providerNamespaces],
);
snapshot.providerRecords = providerRecords.rows;
if (scope.source_provider !== PROVIDER) {
const capabilityArtifact = await client.query(
`SELECT manifest
FROM whatsapp_artifact
WHERE session_id = $1
AND artifact_id = $2::uuid
AND revision_id = $3::bigint
AND provider = $4
AND kind = 'provider_handoff_checkpoint'
AND status = 'ready'
LIMIT 2`,
[
this.sessionId,
scope.handoff_id,
scope.source_revision_id,
scope.source_provider,
],
);
if (capabilityArtifact.rowCount !== 1) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_handoff_signal_capability_missing',
);
}
snapshot.canonicalCapabilities =
canonicalSignalCapabilitiesFromHandoffArtifact(
capabilityArtifact.rows[0].manifest,
scope,
);
if (
snapshot.canonicalCapabilities
.baileysClassicalPostQuantumRollbackAcknowledged ===
true
) {
const rollbackState = snapshot.pqPreKeyState[0];
if (
snapshot.pqPreKeys.length !== 0 ||
snapshot.pqPreKeyState.length !== 1 ||
Number(rollbackState?.codec_version) !== 1 ||
rollbackState?.algorithm !== 'ML-KEM-1024' ||
rollbackState?.migrated !== false ||
rollbackState?.last_server_count !== null ||
rollbackState?.last_server_count_timestamp_ms !== null
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_handoff_pq_rollback_state_invalid',
);
}
// The source keeps this allocator row so an aborted handoff
// can regenerate and upload a fresh bundle. The WWebJS
// candidate must instead materialize the proven classical
// target state, whose current runtime has no Kyber tables.
snapshot.pqPreKeyState = [];
}
const importableSignalScopes = new Set(
snapshot.canonicalCapabilities
.canonical_signal_session_scopes,
);
const resyncSignalScopes = new Set(
snapshot.canonicalCapabilities
.canonical_signal_session_resync_scopes,
);
snapshot.signalSessions = snapshot.signalSessions.filter(
(row) => {
if (importableSignalScopes.has(row.scope)) {
if (row.session === null) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_handoff_signal_capability_invalid',
);
}
return true;
}
if (resyncSignalScopes.has(row.scope)) return false;
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_handoff_signal_capability_invalid',
);
},
);
}
const projection = canonicalProjectionFromSnapshot(
snapshot,
this.webVersion,
);
const appStateRequirement =
normalizeCanonicalAppStateSnapshotCapabilities(
projection.capabilities,
);
if (appStateRequirement.app_state_snapshot_resync_required) {
const resyncCollections = new Set(
appStateRequirement.app_state_snapshot_resync_collections,
);
if (
projection.tables.whatsapp_app_state_version.some((row) =>
resyncCollections.has(row.name),
) ||
projection.tables.whatsapp_app_state_mutation_macs.some(
(row) => resyncCollections.has(row.name),
)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_source_inconsistent',
);
}
}
if (
scope.source_provider !== PROVIDER &&
projection.transport_state == null
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_routing_info_unavailable',
{ source_provider: scope.source_provider },
);
}
await client.query('COMMIT');
return {
scope,
projection,
};
} catch (error) {
await client.query('ROLLBACK').catch(() => {});
throw error;
} finally {
client.release();
}
}
async hydrateAuthorizedHandoff() {
if (!this.isHandoffRevision()) return false;
if (this.revisionSource === 'secure_import') {
// A browser-profile import intentionally starts from an empty
// pairing revision, so there is no canonical source projection
// to hydrate. Resume from the target-local provider projection;
// once RemoteAuth derives a canonical projection, subsequent
// restarts take the ordinary canonical path below.
const canonicalProjection = await this.loadCanonicalProjection();
if (canonicalProjection) {
if (!this.webVersionIntegrityPinned) {
this.webVersion = canonicalProjection.web_version;
}
this.pendingCanonicalProjection = canonicalProjection;
await this.ensureAppStateSnapshotResyncGate(
this.appStateSnapshotResyncScope(
canonicalProjection.capabilities,
),
canonicalProjection,
);
return false;
}
const providerProjection = await this.loadProjection();
if (!providerProjection) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_secure_import_provider_projection_missing',
);
}
if (!this.webVersionIntegrityPinned) {
this.webVersion = providerProjection.web_version;
}
this.pendingProjection = providerProjection;
return false;
}
const targetReady = await this.withLeaseTransaction(async (client) => {
const result = await client.query(
`SELECT registration_id
FROM whatsapp_device
WHERE session_id = $1 AND revision_id = $2::bigint`,
[this.sessionId, this.revisionId],
);
return (
result.rowCount === 1 && result.rows[0]?.registration_id != null
);
});
const startedAt = Date.now();
const source = await this.readAuthorizedHandoffSource();
this.sourceProvider = source.scope.source_provider;
this.sourceRevisionId = source.scope.source_revision_id;
this.handoffId = source.scope.handoff_id;
this.lifecycleOperationId = source.scope.lifecycle_operation_id;
if (targetReady) {
this.pendingCanonicalProjection =
this.pendingCanonicalProjection ||
(await this.loadCanonicalProjection());
} else {
await this.saveCanonicalProjection({
projection: source.projection,
source: 'handoff_source_hydration',
});
this.pendingCanonicalProjection = source.projection;
}
const gate = await this.ensureAppStateSnapshotResyncGate(
source.scope,
source.projection,
);
this.log('handoff.source_projection_hydrated', {
source_provider: this.sourceProvider,
source_revision_id: this.sourceRevisionId,
target_revision_id: this.revisionId,
record_count: source.projection.record_count,
size_bytes: source.projection.size_bytes,
target_projection_preexisting: targetReady,
app_state_snapshot_resync_gate_artifact_id: gate.artifactId,
app_state_snapshot_resync_gate_ready: gate.ready,
duration_ms: Date.now() - startedAt,
});
return !targetReady;
}
appStateSnapshotResyncScope(requirement) {
const code =
'whatsapp_session_app_state_snapshot_resync_gate_lineage_invalid';
if (
!this.handoffId ||
!this.lifecycleOperationId ||
!this.sourceProvider ||
!this.sourceRevisionId ||
!this.revisionId
) {
throw new WhatsAppSessionCompatibilityError(code);
}
return normalizeAppStateSnapshotResyncLineage(
{
handoff_id: this.handoffId,
lifecycle_operation_id: this.lifecycleOperationId,
source_provider: this.sourceProvider,
source_revision_id: this.sourceRevisionId,
target_provider: PROVIDER,
target_revision_id: this.revisionId,
},
requirement,
);
}
async readAppStateSnapshotResyncArtifact(
client,
expectedScope,
{ forUpdate = false, required = true } = {},
) {
const artifactId = appStateSnapshotResyncArtifactId(
expectedScope.handoff_id,
);
const result = await client.query(
`SELECT artifact_id::text,
revision_id::text AS revision_id,
provider, kind, status, manifest,
checksum_sha256, size_bytes::text AS size_bytes,
chunk_count, persisted_at
FROM whatsapp_artifact
WHERE session_id = $1
AND artifact_id = $2::uuid
LIMIT 2${forUpdate ? ' FOR UPDATE' : ''}`,
[this.sessionId, artifactId],
);
if (result.rowCount === 0 && !required) return undefined;
if (result.rowCount !== 1 || !result.rows[0]) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_gate_missing',
);
}
const row = result.rows[0];
const parsed = normalizeAppStateSnapshotResyncManifest(row.manifest);
const lineage = {
...expectedScope,
app_state_snapshot_resync_required:
parsed.app_state_snapshot_resync_required,
app_state_snapshot_resync_collections:
parsed.app_state_snapshot_resync_collections,
};
const manifest = assertAppStateSnapshotResyncManifestLineage(
parsed,
lineage,
);
const payload = canonicalAppStateSnapshotResyncManifest(manifest);
const checksum = sha256(Buffer.from(payload, 'utf8'));
const sizeBytes = safeJsonInt64(
row.size_bytes,
'whatsapp_session_app_state_snapshot_resync_artifact_invalid',
);
if (
row.artifact_id !== artifactId ||
String(row.revision_id) !== String(this.revisionId) ||
row.provider !== PROVIDER ||
row.kind !== APP_STATE_SNAPSHOT_RESYNC_GATE_KIND ||
!['staging', 'ready'].includes(row.status) ||
Number(row.chunk_count) !== 0 ||
row.checksum_sha256 !== checksum ||
sizeBytes !== Buffer.byteLength(payload) ||
!row.persisted_at
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_artifact_invalid',
);
}
this.appStateSnapshotResyncGateArtifactId = artifactId;
return { artifactId, manifest, status: row.status };
}
async readAppStateSnapshotResyncState(client, manifest) {
const syncKeys = await client.query(
`SELECT key_id, key_data, timestamp, fingerprint
FROM whatsapp_app_state_sync_keys
WHERE session_id = $1 AND revision_id = $2::bigint
ORDER BY key_id
LIMIT $3`,
[
this.sessionId,
this.revisionId,
MAX_APP_STATE_SYNC_KEY_PROOFS + 1,
],
);
if (syncKeys.rows.length > MAX_APP_STATE_SYNC_KEY_PROOFS) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_sync_key_limit_exceeded',
);
}
const targetSyncKeys = captureAppStateSyncKeyAnchorProof(syncKeys.rows);
validateTargetAppStateSyncKeyProof(
manifest.source_sync_keys,
targetSyncKeys,
);
let materializedCollections = [];
if (manifest.app_state_snapshot_resync_collections.length > 0) {
const versions = await client.query(
`SELECT name, version::text AS version
FROM whatsapp_app_state_version
WHERE session_id = $1
AND revision_id = $2::bigint
AND name = ANY($3::text[])
ORDER BY name`,
[
this.sessionId,
this.revisionId,
manifest.app_state_snapshot_resync_collections,
],
);
let previousName = '';
materializedCollections = versions.rows.map((row) => {
if (
!manifest.app_state_snapshot_resync_collections.includes(
row.name,
) ||
row.name <= previousName
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_checkpoint_invalid',
);
}
previousName = row.name;
return {
name: row.name,
version: safeJsonInt64(
row.version,
'whatsapp_session_app_state_snapshot_resync_version_invalid',
),
};
});
}
const futureMac = await client.query(
`SELECT EXISTS (
SELECT 1
FROM whatsapp_app_state_mutation_macs AS mac
LEFT JOIN whatsapp_app_state_version AS version
ON version.session_id = mac.session_id
AND version.revision_id = mac.revision_id
AND version.name = mac.name
WHERE mac.session_id = $1
AND mac.revision_id = $2::bigint
AND (
version.name IS NULL OR mac.version > version.version
)
) AS future_mac`,
[this.sessionId, this.revisionId],
);
if (
futureMac.rowCount !== 1 ||
futureMac.rows[0]?.future_mac !== false
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_future_mutation_mac',
);
}
const ready =
(!manifest.app_state_snapshot_resync_required &&
materializedCollections.length === 0) ||
(manifest.app_state_snapshot_resync_required &&
materializedCollections.length ===
manifest.app_state_snapshot_resync_collections.length &&
manifest.app_state_snapshot_resync_collections.every(
(name, index) =>
materializedCollections[index]?.name === name,
));
return { ready, materializedCollections, targetSyncKeys };
}
async persistAppStateSnapshotResyncArtifact(
client,
artifactId,
manifest,
ready,
) {
const normalized = normalizeAppStateSnapshotResyncManifest(manifest);
const payload = canonicalAppStateSnapshotResyncManifest(normalized);
const status = ready ? 'ready' : 'staging';
const result = await client.query(
`INSERT INTO whatsapp_artifact (
session_id, artifact_id, revision_id, provider, kind,
status, manifest, checksum_sha256, size_bytes,
chunk_count, persisted_at
) VALUES (
$1, $2::uuid, $3::bigint, 'wwebjs', $4,
$5, $6::jsonb, $7, $8, 0, clock_timestamp()
)
ON CONFLICT (session_id, artifact_id) DO UPDATE SET
status = EXCLUDED.status,
manifest = EXCLUDED.manifest,
checksum_sha256 = EXCLUDED.checksum_sha256,
size_bytes = EXCLUDED.size_bytes,
chunk_count = 0,
persisted_at = clock_timestamp()
WHERE whatsapp_artifact.revision_id = EXCLUDED.revision_id
AND whatsapp_artifact.provider = 'wwebjs'
AND whatsapp_artifact.kind = $4
AND whatsapp_artifact.status IN ('staging', 'ready')
AND (
whatsapp_artifact.status = 'staging'
OR EXCLUDED.status = 'ready'
)
RETURNING artifact_id::text`,
[
this.sessionId,
artifactId,
this.revisionId,
APP_STATE_SNAPSHOT_RESYNC_GATE_KIND,
status,
payload,
sha256(Buffer.from(payload, 'utf8')),
Buffer.byteLength(payload),
],
);
if (
result.rowCount !== 1 ||
result.rows[0]?.artifact_id !== artifactId
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_artifact_scope_changed',
);
}
}
async ensureAppStateSnapshotResyncGate(scope, sourceProjection) {
const requirement = normalizeCanonicalAppStateSnapshotCapabilities(
sourceProjection?.capabilities,
);
const lineage = normalizeAppStateSnapshotResyncLineage(
scope,
requirement,
);
const sourceSyncKeys = captureAppStateSyncKeyAnchorProof(
sourceProjection?.tables?.whatsapp_app_state_sync_keys,
);
const artifactId = appStateSnapshotResyncArtifactId(lineage.handoff_id);
const gate = await this.withLeaseMutation(async (client) => {
const stored = await this.readAppStateSnapshotResyncArtifact(
client,
lineage,
{ forUpdate: true, required: false },
);
let manifest = stored?.manifest || {
version: APP_STATE_SNAPSHOT_RESYNC_GATE_VERSION,
...lineage,
source_sync_keys: sourceSyncKeys,
materialized_collections: [],
};
if (
JSON.stringify(manifest.source_sync_keys) !==
JSON.stringify(sourceSyncKeys)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_source_sync_key_proof_changed',
);
}
const state = await this.readAppStateSnapshotResyncState(
client,
manifest,
);
manifest = {
...manifest,
materialized_collections: state.materializedCollections,
};
if (stored?.status === 'ready' && !state.ready) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_ready_artifact_diverged',
);
}
await this.persistAppStateSnapshotResyncArtifact(
client,
artifactId,
manifest,
state.ready,
);
return { artifactId, ready: state.ready };
});
this.appStateSnapshotResyncGateArtifactId = artifactId;
this.log(
'handoff.app_state_snapshot_resync_gate.persisted',
{
artifact_id: artifactId,
ready: gate.ready,
collection_count:
requirement.app_state_snapshot_resync_collections.length,
source_sync_key_count: sourceSyncKeys.count,
},
{ force: true },
);
return gate;
}
async checkpointAppStateSnapshotResyncGate() {
if (!this.handoffId || !this.sourceRevisionId) return undefined;
const gate = await this.withLeaseMutation(async (client) => {
const scope = this.appStateSnapshotResyncScope({
app_state_snapshot_resync_required: false,
app_state_snapshot_resync_collections: [],
});
const stored = await this.readAppStateSnapshotResyncArtifact(
client,
scope,
{ forUpdate: true },
);
const lineage = this.appStateSnapshotResyncScope(stored.manifest);
assertAppStateSnapshotResyncManifestLineage(
stored.manifest,
lineage,
);
const state = await this.readAppStateSnapshotResyncState(
client,
stored.manifest,
);
if (stored.status === 'ready' && !state.ready) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_ready_artifact_diverged',
);
}
const manifest = {
...stored.manifest,
materialized_collections: state.materializedCollections,
};
await this.persistAppStateSnapshotResyncArtifact(
client,
stored.artifactId,
manifest,
state.ready,
);
return { artifactId: stored.artifactId, ready: state.ready };
});
this.log(
'handoff.app_state_snapshot_resync_gate.checkpointed',
{
artifact_id: gate.artifactId,
ready: gate.ready,
},
{ force: true },
);
return gate;
}
async assertAppStateSnapshotResyncGateInitializedInTransaction(
client,
{ forUpdate = false } = {},
) {
if (!this.handoffId || !this.sourceRevisionId) return;
const scope = this.appStateSnapshotResyncScope({
app_state_snapshot_resync_required: false,
app_state_snapshot_resync_collections: [],
});
const stored = await this.readAppStateSnapshotResyncArtifact(
client,
scope,
{ forUpdate },
);
assertAppStateSnapshotResyncManifestLineage(
stored.manifest,
this.appStateSnapshotResyncScope(stored.manifest),
);
return stored;
}
async assertAppStateSnapshotResyncGateInitialized() {
await this.withLeaseTransaction((client) =>
this.assertAppStateSnapshotResyncGateInitializedInTransaction(
client,
),
);
}
async assertAppStateSnapshotResyncGateReadyInTransaction(
client,
{ forUpdate = false } = {},
) {
if (!this.handoffId || !this.sourceRevisionId) return;
const stored =
await this.assertAppStateSnapshotResyncGateInitializedInTransaction(
client,
{ forUpdate },
);
const state = await this.readAppStateSnapshotResyncState(
client,
stored.manifest,
);
if (stored.status !== 'ready' || !state.ready) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_pending',
);
}
return stored;
}
async assertAppStateSnapshotResyncGateReady() {
await this.withLeaseTransaction((client) =>
this.assertAppStateSnapshotResyncGateReadyInTransaction(client),
);
}
requiresNoiseMetadataBootstrap() {
return Boolean(
this.isHandoffRevision() &&
this.revisionSource === 'handoff' &&
['staging', 'validating'].includes(this.revisionStatus) &&
['baileys', 'whatsmeow'].includes(this.sourceProvider) &&
this.pendingCanonicalProjection &&
this.pendingCanonicalProjection.provider_state == null,
);
}
async loadCanonicalProjection(options = {}) {
const revisionId =
typeof options === 'string' || typeof options === 'number'
? normalizeRevisionId(options)
: normalizeRevisionId(options.revisionId) || this.revisionId;
const existingClient =
typeof options === 'object' && options !== null
? options.client
: undefined;
const loadSnapshot = async (client) => {
const parameters = [this.sessionId, revisionId];
const deviceResult = await client.query(
`SELECT jid, lid, facebook_uuid, registration_id,
noise_key, identity_key, signed_pre_key,
signed_pre_key_id, signed_pre_key_sig, adv_key,
adv_secret_available, adv_details,
adv_account_sig, adv_account_sig_key,
adv_device_sig, platform, business_name, push_name,
lid_migration_ts, next_pre_key_id,
device_fingerprint, fingerprint_version
FROM whatsapp_device
WHERE session_id = $1 AND revision_id = $2::bigint`,
parameters,
);
if (deviceResult.rowCount === 0 || !deviceResult.rows[0]) {
return undefined;
}
if (deviceResult.rowCount !== 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_device_ambiguous',
);
}
if (deviceResult.rows[0].registration_id == null) {
return undefined;
}
await this.preflightCanonicalSnapshot(client, revisionId, [
CANONICAL_META_NAMESPACE,
CANONICAL_TRANSPORT_NAMESPACE,
]);
const identityResult = await client.query(
`SELECT their_id, identity FROM whatsapp_identity_keys
WHERE session_id = $1 AND revision_id = $2::bigint
ORDER BY their_id`,
parameters,
);
const preKeyResult = await client.query(
`SELECT key_id, key, uploaded FROM whatsapp_pre_keys
WHERE session_id = $1 AND revision_id = $2::bigint
ORDER BY key_id`,
parameters,
);
const pqPreKeyResult = await client.query(
`SELECT key_id, key_kind, public_key, private_key, signature,
timestamp_ms, sent_to_server
FROM whatsapp_pq_pre_keys
WHERE session_id = $1 AND revision_id = $2::bigint
ORDER BY key_kind, key_id`,
parameters,
);
const pqPreKeyStateResult = await client.query(
`SELECT codec_version, algorithm, next_pre_key_id, migrated,
last_server_count, last_server_count_timestamp_ms
FROM whatsapp_pq_pre_key_state
WHERE session_id = $1 AND revision_id = $2::bigint`,
parameters,
);
if (pqPreKeyStateResult.rowCount > 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_pq_pre_key_state_ambiguous',
);
}
const sessionResult = await client.query(
`SELECT their_id, scope, session
FROM whatsapp_signal_sessions
WHERE session_id = $1 AND revision_id = $2::bigint
ORDER BY their_id, scope`,
parameters,
);
const senderKeyResult = await client.query(
`SELECT chat_id, sender_id, sender_key
FROM whatsapp_sender_keys
WHERE session_id = $1 AND revision_id = $2::bigint
ORDER BY chat_id, sender_id`,
parameters,
);
const syncKeyResult = await client.query(
`SELECT key_id, key_data, timestamp, fingerprint
FROM whatsapp_app_state_sync_keys
WHERE session_id = $1 AND revision_id = $2::bigint
ORDER BY timestamp DESC, key_id`,
parameters,
);
const versionResult = await client.query(
`SELECT name, version, hash
FROM whatsapp_app_state_version
WHERE session_id = $1 AND revision_id = $2::bigint
ORDER BY name`,
parameters,
);
const macResult = await client.query(
`SELECT name, version, index_mac, value_mac
FROM whatsapp_app_state_mutation_macs
WHERE session_id = $1 AND revision_id = $2::bigint
ORDER BY name, index_mac, version DESC`,
parameters,
);
const providerRecordResult = await client.query(
`SELECT namespace, record_key, codec_version, payload
FROM whatsapp_provider_record
WHERE session_id = $1
AND revision_id = $2::bigint
AND namespace = ANY($3::text[])
ORDER BY namespace, record_key`,
[
...parameters,
[CANONICAL_META_NAMESPACE, CANONICAL_TRANSPORT_NAMESPACE],
],
);
const passthroughTables = {};
for (const { table, columns } of PASSTHROUGH_TABLE_CONFIGS) {
const result = await client.query(
`SELECT ${columns.join(', ')}
FROM ${table}
WHERE session_id = $1 AND revision_id = $2::bigint`,
parameters,
);
passthroughTables[table] = result.rows;
}
return {
device: deviceResult.rows[0],
identityKeys: identityResult.rows,
preKeys: preKeyResult.rows,
pqPreKeys: pqPreKeyResult.rows,
pqPreKeyState: pqPreKeyStateResult.rows,
signalSessions: sessionResult.rows,
senderKeys: senderKeyResult.rows,
syncKeys: syncKeyResult.rows,
versions: versionResult.rows,
mutationMacs: macResult.rows,
providerRecords: providerRecordResult.rows,
passthroughTables,
};
};
const snapshot = existingClient
? await loadSnapshot(existingClient)
: await this.withLeaseTransaction(loadSnapshot, revisionId);
if (!snapshot) return undefined;
return canonicalProjectionFromSnapshot(snapshot, this.webVersion);
}
async consumePendingCanonicalProjection() {
if (this.pendingCanonicalProjection) {
const projection = this.pendingCanonicalProjection;
this.pendingCanonicalProjection = undefined;
return projection;
}
if (!this.isHandoffRevision()) return undefined;
return this.loadCanonicalProjection();
}
async checkpointCanonicalProjection({ bridge, projection, source } = {}) {
const exported =
projection || (await bridge?.exportCanonicalProjection?.());
if (!exported) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_projection_missing',
);
}
await this.saveCanonicalProjection({ projection: exported, source });
return exported;
}
async persistExternalBrowserBootstrapCanonicalProjection(projection) {
if (
!this.isHandoffRevision() ||
this.revisionSource !== 'secure_import' ||
!this.handoffId ||
!this.sourceRevisionId
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_secure_import_revision_invalid',
);
}
let portableProjection = projection;
if (canonicalProjectionHasFutureMutationMac(projection)) {
const portable =
normalizeCanonicalAuthoritativeAppStateForPortablePersistence(
projection,
);
portableProjection = portable.projection;
this.log(
'secure_import.browser_canonical_app_state_normalized',
{
removed_future_mutation_mac_count:
portable.removed_future_mutation_mac_count,
materialized_collection_count:
portable.materialized_collection_count,
},
{ force: true },
);
}
const persisted = await this.saveCanonicalProjection({
projection: portableProjection,
source: 'secure_import_browser_bootstrap',
});
const normalized = await this.loadCanonicalProjection();
if (!normalized) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_secure_import_canonical_projection_missing',
);
}
await this.ensureAppStateSnapshotResyncGate(
this.appStateSnapshotResyncScope(normalized.capabilities),
normalized,
);
return { projection: normalized, ...persisted };
}
async exportProjection() {
const artifact = await this.getCurrentRevisionArtifact();
const identity = await this.loadIdentity();
const canonicalProjection = await this.loadCanonicalProjection();
return {
format_version: 'whatsapp-session-v1',
source_provider: PROVIDER,
session_id: this.sessionId,
revision_id: this.revisionId,
web_version: this.webVersion,
identity: identity
? {
jid: identity.jid,
device_fingerprint: Buffer.from(
identity.device_fingerprint,
).toString('hex'),
}
: undefined,
provider_projection: await this.loadProjection(),
canonicalProjection,
artifact: artifact
? {
artifact_id: artifact.artifact_id,
manifest: artifact.manifest,
checksum_sha256: artifact.checksum_sha256,
size_bytes: Number(artifact.size_bytes),
}
: undefined,
};
}
async loadIdentity(revisionId = this.revisionId) {
return this.withLeaseTransaction(async (client) => {
const result = await client.query(
`SELECT jid, device_fingerprint
FROM whatsapp_device
WHERE session_id = $1
AND revision_id = $2::bigint`,
[this.sessionId, revisionId],
);
const identity = result.rows[0];
if (!identity?.jid || !identity?.device_fingerprint) {
return undefined;
}
return identity;
}, revisionId);
}
validateImportProjectionEnvelope(projection) {
try {
if (
!isObjectRecord(projection) ||
projection.format_version !== 'whatsapp-session-v1'
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_import_format_unsupported',
);
}
if (
!['wwebjs', 'baileys', 'whatsmeow'].includes(
projection.source_provider,
)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_import_source_provider_unsupported',
{ actual: projection.source_provider },
);
}
if (projection.session_id !== this.sessionId) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_import_session_mismatch',
);
}
const isActive = this.revisionStatus === 'active';
if (isActive && projection.source_provider !== PROVIDER) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_cross_provider_handoff_requires_manager',
);
}
if (!isActive) {
if (!this.isHandoffRevision()) {
throw new WhatsAppSessionError(
'whatsapp_session_import_revision_invalid',
);
}
if (this.sourceProvider !== projection.source_provider) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_import_source_provider_mismatch',
{
expected: this.sourceProvider,
actual: projection.source_provider,
},
);
}
}
const expectedSourceRevisionId = isActive
? this.revisionId
: this.sourceRevisionId;
let importedRevisionId;
try {
importedRevisionId = normalizeRevisionId(
projection.revision_id,
);
} catch {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_import_source_revision_mismatch',
);
}
if (
!expectedSourceRevisionId ||
!importedRevisionId ||
importedRevisionId !== String(expectedSourceRevisionId)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_import_source_revision_mismatch',
);
}
const crossProvider = projection.source_provider !== PROVIDER;
if (
!crossProvider &&
(typeof projection.web_version !== 'string' ||
!projection.web_version ||
(this.webVersionIntegrityPinned &&
projection.web_version !== this.webVersion))
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_import_web_version_mismatch',
{
expected: this.webVersion,
actual: projection.web_version,
},
);
}
const canonicalProjection = projection.canonicalProjection
? normalizeStoreCanonicalProjection(
projection.canonicalProjection,
this.webVersion,
)
: undefined;
if (crossProvider && !canonicalProjection) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_import_canonical_projection_missing',
);
}
const identity = canonicalProjection
? {
jid: canonicalProjection.device.jid,
device_fingerprint:
canonicalProjection.device.device_fingerprint,
}
: projection.identity === undefined
? undefined
: normalizeImportedIdentity(projection.identity);
const normalizedProjection =
crossProvider || projection.provider_projection === undefined
? undefined
: normalizeProjection(
projection.provider_projection,
this.webVersion,
this.webVersionIntegrityPinned,
);
return {
identity,
providerProjection: normalizedProjection?.projection,
records: normalizedProjection?.records,
canonicalProjection,
};
} catch (error) {
if (
error instanceof WhatsAppSessionCompatibilityError ||
error instanceof WhatsAppSessionError
) {
throw error;
}
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_import_envelope_invalid',
);
}
}
async importProjection({ projection, profilePath, promote = false }) {
const imported = this.validateImportProjectionEnvelope(projection);
const originalRevisionId = this.revisionId;
const originalRevisionStatus = this.revisionStatus;
const originalRevisionSource = this.revisionSource;
let candidateCreated = false;
let candidateRevisionId;
let profileRebound = false;
try {
if (this.revisionStatus === 'active') {
if (profilePath) {
await ensureProfileFingerprint({
profilePath,
expectedFingerprint: profileFingerprint({
sessionId: this.sessionId,
revisionId: originalRevisionId,
}),
allowCreate: false,
logger: this.logger,
reason: 'secure_import_source_validation',
});
}
const candidate = await this.createCandidateRevision(
PROVIDER,
'secure_import',
);
candidateCreated = true;
candidateRevisionId = candidate.revision_id;
this.revisionId = candidateRevisionId;
this.sourceRevisionId = candidate.source_revision_id;
this.handoffId = candidate.handoff_id;
this.sourceProvider = PROVIDER;
this.revisionStatus = 'staging';
this.revisionSource = 'secure_import';
this.secureImportRollbackContext = {
revisionId: originalRevisionId,
status: originalRevisionStatus,
source: originalRevisionSource,
};
this.updateLoggerContext({ revisionId: this.revisionId });
if (profilePath) {
profileRebound = await rebindProfileFingerprint({
profilePath,
expectedCurrentFingerprint: profileFingerprint({
sessionId: this.sessionId,
revisionId: originalRevisionId,
}),
nextFingerprint: profileFingerprint({
sessionId: this.sessionId,
revisionId: candidateRevisionId,
}),
logger: this.logger,
reason: 'secure_import_candidate',
});
}
} else if (!this.isHandoffRevision()) {
throw new WhatsAppSessionError(
'whatsapp_session_import_revision_invalid',
);
}
const revisionId = this.revisionId;
this.pendingProjection = imported.providerProjection;
this.pendingCanonicalProjection = imported.canonicalProjection;
if (imported.canonicalProjection) {
await this.saveCanonicalProjection({
projection: imported.canonicalProjection,
source: 'secure_import',
});
}
if (imported.identity) {
await this.stageImportedIdentity(imported.identity);
}
if (imported.records) {
await this.withLeaseMutation(
(client) =>
this.replaceProviderRecords(
client,
revisionId,
imported.records,
),
revisionId,
);
}
if (profilePath) {
await this.checkpointProfile({
profilePath,
projection: imported.providerProjection,
source: this.revisionSource,
promote,
});
}
this.log('handoff.projection_staged', {
source_provider: projection.source_provider,
candidate_revision_id: revisionId,
});
return revisionId;
} catch (error) {
if (!candidateCreated) throw error;
let rollbackError;
if (
this.revisionId !== originalRevisionId ||
this.revisionStatus !== 'active'
) {
try {
await this.failRevision(candidateRevisionId, error);
} catch (caughtError) {
rollbackError = caughtError;
}
}
if (!rollbackError && profileRebound) {
try {
await rebindProfileFingerprint({
profilePath,
expectedCurrentFingerprint: profileFingerprint({
sessionId: this.sessionId,
revisionId: candidateRevisionId,
}),
nextFingerprint: profileFingerprint({
sessionId: this.sessionId,
revisionId: originalRevisionId,
}),
logger: this.logger,
reason: 'secure_import_rollback',
});
} catch (caughtError) {
rollbackError = caughtError;
}
}
this.pendingProjection = undefined;
this.pendingCanonicalProjection = undefined;
if (rollbackError) {
throw new WhatsAppSessionError(
'whatsapp_session_import_rollback_failed',
undefined,
{ cause: new AggregateError([error, rollbackError]) },
);
}
throw error;
}
}
/**
* Stages an extension-produced BrowserSessionBridge projection from the
* only safe bootstrap state: a completely empty pairing revision.
*/
async stageExternalBrowserProjection({ projection, profilePath }) {
this.assertLocalLease();
if (typeof profilePath !== 'string' || !profilePath) {
throw new TypeError('profilePath is required');
}
if (!(await this.isExternalBrowserImportAllowed())) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_secure_import_requires_empty_pairing',
);
}
const originalWebVersion = this.webVersion;
const normalized = normalizeProjection(
projection,
this.webVersion,
this.webVersionIntegrityPinned,
);
if (!this.webVersionIntegrityPinned) {
// This is the exact version observed by the authenticated source
// tab. Adopt it for artifact/projection persistence now; the
// guarded runtime still has to prove the same version and module
// ABI before imported credentials can reach a WebSocket.
this.webVersion = normalized.projection.web_version;
}
const originalRevisionId = this.revisionId;
const originalRevisionStatus = this.revisionStatus;
const originalRevisionSource = this.revisionSource;
const sourceFingerprint = profileFingerprint({
sessionId: this.sessionId,
revisionId: originalRevisionId,
});
let candidateRevisionId;
let profileRebound = false;
try {
await ensureProfileFingerprint({
profilePath,
expectedFingerprint: sourceFingerprint,
allowCreate: true,
logger: this.logger,
reason: 'secure_import_empty_pairing_profile',
});
const candidate = await this.createCandidateRevision(
PROVIDER,
'secure_import',
{ requireEmptyPairing: true },
);
candidateRevisionId = candidate.revision_id;
this.revisionId = candidateRevisionId;
this.sourceRevisionId = candidate.source_revision_id;
this.handoffId = candidate.handoff_id;
// Same-provider secure imports do not originate in the lifecycle
// manager. The handoff UUID is a stable, collision-free lineage
// token for the mandatory app-state resync gate.
this.lifecycleOperationId = candidate.handoff_id;
this.sourceProvider = PROVIDER;
this.revisionStatus = 'staging';
this.revisionSource = 'secure_import';
this.secureImportRollbackContext = {
revisionId: originalRevisionId,
status: originalRevisionStatus,
source: originalRevisionSource,
};
this.updateLoggerContext({ revisionId: this.revisionId });
profileRebound = await rebindProfileFingerprint({
profilePath,
expectedCurrentFingerprint: sourceFingerprint,
nextFingerprint: profileFingerprint({
sessionId: this.sessionId,
revisionId: candidateRevisionId,
}),
logger: this.logger,
reason: 'secure_import_external_browser_candidate',
});
if (!profileRebound) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_secure_import_profile_rebind_failed',
);
}
this.pendingProjection = normalized.projection;
await this.checkpointProfile({
profilePath,
projection: normalized.projection,
source: 'secure_import',
promote: false,
});
this.log('secure_import.browser_projection_staged', {
candidate_revision_id: candidateRevisionId,
record_count: normalized.projection.records.length,
size_bytes: normalized.projection.size_bytes,
});
return candidateRevisionId;
} catch (error) {
let rollbackError;
if (
candidateRevisionId &&
this.revisionId === candidateRevisionId &&
this.revisionStatus !== 'active'
) {
try {
await this.failRevision(candidateRevisionId, error);
} catch (caughtError) {
rollbackError = caughtError;
}
}
if (!rollbackError && profileRebound) {
try {
await rebindProfileFingerprint({
profilePath,
expectedCurrentFingerprint: profileFingerprint({
sessionId: this.sessionId,
revisionId: candidateRevisionId,
}),
nextFingerprint: sourceFingerprint,
logger: this.logger,
reason: 'secure_import_external_browser_rollback',
});
} catch (caughtError) {
rollbackError = caughtError;
}
}
this.pendingProjection = undefined;
this.pendingCanonicalProjection = undefined;
this.webVersion = originalWebVersion;
if (rollbackError) {
throw new WhatsAppSessionError(
'whatsapp_session_import_rollback_failed',
undefined,
{ cause: new AggregateError([error, rollbackError]) },
);
}
throw error;
}
}
async stageImportedIdentity(identity) {
if (!identity?.jid || !identity.device_fingerprint) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_import_identity_missing',
);
}
const fingerprint = normalizeFingerprint(identity.device_fingerprint);
await this.withLeaseMutation(async (client) => {
const current = await client.query(
`SELECT jid, device_fingerprint
FROM whatsapp_device
WHERE session_id = $1
AND revision_id = $2::bigint`,
[this.sessionId, this.revisionId],
);
const row = current.rows[0];
if (
row?.jid &&
comparableJid(row.jid) !== comparableJid(identity.jid)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_import_identity_mismatch',
);
}
if (
row?.device_fingerprint &&
!fingerprintsEqual(row.device_fingerprint, fingerprint)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_import_fingerprint_mismatch',
);
}
const updated = await client.query(
`UPDATE whatsapp_device
SET jid = $3,
device_fingerprint = $4,
fingerprint_version = $5
WHERE session_id = $1 AND revision_id = $2::bigint
RETURNING session_id`,
[
this.sessionId,
this.revisionId,
identity.jid,
fingerprint,
CANONICAL_FINGERPRINT_VERSION,
],
);
if (updated.rowCount !== 1) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_import_canonical_device_missing',
);
}
});
}
async consumePendingProjection() {
if (this.pendingProjection) {
const projection = this.pendingProjection;
this.pendingProjection = undefined;
return projection;
}
if (!this.isHandoffRevision()) return undefined;
return this.loadProjection();
}
isHandoffRevision() {
return (
['staging', 'validating'].includes(this.revisionStatus) &&
['handoff', 'secure_import'].includes(this.revisionSource)
);
}
/**
* Pairing is the sole PostgreSQL state allowed to surface a QR. A revision
* header that says `pairing` is not sufficient: every identity, artifact,
* provider record, and scoped canonical table must still be empty.
*/
async isPairingAllowed() {
return this.isEmptyPairingRevisionAllowed({
allowFailedHandoffs: false,
statuses: ['staging'],
});
}
/**
* A failed external import leaves an auditable terminal handoff behind.
* Retrying that import must not relax the QR gate, so use a separate
* predicate that admits only failed historical handoffs while proving the
* source revision is still an entirely empty pairing draft.
*/
async isExternalBrowserImportAllowed() {
return this.isEmptyPairingRevisionAllowed({
allowFailedHandoffs: true,
statuses: ['staging', 'validating'],
});
}
emptyPairingRevisionProofSql({ allowFailedHandoffs, statuses }) {
const stateEmptyChecks = ALL_HANDOFF_STATE_CONFIGS.map(
({ table }) =>
`NOT EXISTS (
SELECT 1 FROM ${table} state_row
WHERE state_row.session_id = $1
AND state_row.revision_id = $2::bigint
)`,
);
const revisionStatusPredicate = statuses
.map((status) => `'${status}'`)
.join(', ');
const handoffTerminalPredicate = allowFailedHandoffs
? "AND handoff.state <> 'failed'"
: '';
return `SELECT (
revision.status IN (${revisionStatusPredicate})
AND revision.source = 'pairing'
AND NOT EXISTS (
SELECT 1
FROM whatsapp_session_handoff handoff
WHERE handoff.session_id = revision.session_id
AND (
handoff.source_revision_id = revision.revision_id
OR handoff.target_revision_id = revision.revision_id
)
${handoffTerminalPredicate}
)
AND NOT EXISTS (
SELECT 1 FROM whatsapp_device device
WHERE device.session_id = revision.session_id
AND device.revision_id = revision.revision_id
)
AND NOT EXISTS (
SELECT 1 FROM whatsapp_artifact artifact
WHERE artifact.session_id = revision.session_id
AND artifact.revision_id = revision.revision_id
)
AND NOT EXISTS (
SELECT 1 FROM whatsapp_provider_record provider_record
WHERE provider_record.session_id = revision.session_id
AND provider_record.revision_id = revision.revision_id
)
AND ${stateEmptyChecks.join('\n AND ')}
) AS pairing_allowed
FROM whatsapp_session_revision revision
WHERE revision.session_id = $1
AND revision.revision_id = $2::bigint
AND revision.provider = 'wwebjs'`;
}
async isEmptyPairingRevisionAllowed({ allowFailedHandoffs, statuses }) {
if (
!statuses.includes(this.revisionStatus) ||
this.revisionSource !== 'pairing' ||
this.sourceRevisionId ||
this.handoffId
) {
return false;
}
const result = await this.withLeaseTransaction(async (client) =>
client.query(
this.emptyPairingRevisionProofSql({
allowFailedHandoffs,
statuses,
}),
[this.sessionId, this.revisionId],
),
);
return (
result.rowCount === 1 && result.rows[0]?.pairing_allowed === true
);
}
async validateReadyIdentity({ jid, companionFingerprint } = {}) {
if (!jid) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_ready_identity_missing',
);
}
const providedFingerprint = normalizeFingerprint(companionFingerprint);
return this.withLeaseMutation(async (client) => {
const expected = await client.query(
`SELECT device.jid,
device.device_fingerprint,
device.session_id IS NOT NULL AS device_present,
session.active_device_fingerprint
FROM whatsapp_session session
LEFT JOIN whatsapp_device device
ON device.session_id = session.session_id
AND device.revision_id = $2::bigint
WHERE session.session_id = $1`,
[this.sessionId, this.revisionId],
);
const row = expected.rows[0];
if (!row) {
throw new WhatsAppSessionError(
'whatsapp_session_header_missing',
);
}
const expectedFingerprint =
row.device_fingerprint ||
(this.revisionStatus === 'active'
? row.active_device_fingerprint
: undefined);
if (
this.revisionStatus === 'active' &&
(!row.device_present || !row.jid || !row.device_fingerprint)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_active_device_missing',
);
}
if (
this.isHandoffRevision() &&
(!row.device_present || !row.jid || !expectedFingerprint)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_handoff_device_missing',
);
}
if (row.jid && comparableJid(row.jid) !== comparableJid(jid)) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_identity_mismatch',
);
}
if (
providedFingerprint &&
expectedFingerprint &&
!fingerprintsEqual(providedFingerprint, expectedFingerprint)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_companion_fingerprint_mismatch',
);
}
const selectedFingerprint =
providedFingerprint || expectedFingerprint;
if (!selectedFingerprint) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_companion_fingerprint_missing',
);
}
const persistedJid = row.jid || comparableJid(jid);
if (!persistedJid) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_ready_identity_missing',
);
}
const updated = await client.query(
`UPDATE whatsapp_device
SET jid = $3,
device_fingerprint = COALESCE(
$4,
whatsapp_device.device_fingerprint
),
fingerprint_version = $5
WHERE session_id = $1 AND revision_id = $2::bigint
RETURNING session_id`,
[
this.sessionId,
this.revisionId,
persistedJid,
selectedFingerprint,
CANONICAL_FINGERPRINT_VERSION,
],
);
if (updated.rowCount !== 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_ready_canonical_device_missing',
);
}
this.validatedJid = persistedJid;
this.log('session.identity_validated', {
jid_hash: sha256(persistedJid).slice(0, 16),
database_jid_present: Boolean(row.jid),
fingerprint_verified: Boolean(
providedFingerprint && expectedFingerprint,
),
fingerprint_registered: Boolean(
providedFingerprint && !expectedFingerprint,
),
fingerprint_present: true,
});
return {
jidMatched: Boolean(row.jid),
fingerprintVerified: Boolean(
providedFingerprint && expectedFingerprint,
),
fingerprintRegistered: Boolean(
providedFingerprint && !expectedFingerprint,
),
};
});
}
async assertAuthorizedHandoff(targetProvider, expected = {}) {
targetFormat(targetProvider);
await this.assertCanonicalActivationComplete();
if (targetProvider === PROVIDER) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_handoff_target_must_differ',
);
}
if (
!this.revisionId ||
!['staging', 'validating', 'active'].includes(this.revisionStatus)
) {
throw new WhatsAppSessionError(
'whatsapp_session_source_revision_required',
);
}
// The lease transaction is FORCE-RLS scoped to the source revision.
// The manager row proves target lineage, but its row-lock policy is
// target-scoped. Read it without a row lock and lock only the visible
// session and source revision.
const result = await this.withLeaseTransaction(async (client) => {
return client.query(
`SELECT handoff.handoff_id,
handoff.source_revision_id::text
AS source_revision_id,
handoff.target_revision_id::text
AS target_revision_id,
handoff.state,
handoff.lifecycle_operation_id::text
AS lifecycle_operation_id
FROM whatsapp_session_handoff handoff
JOIN whatsapp_session session
ON session.session_id = handoff.session_id
JOIN whatsapp_session_revision source
ON source.session_id = handoff.session_id
AND source.revision_id = handoff.source_revision_id
WHERE handoff.session_id = $1
AND handoff.source_provider = 'wwebjs'
AND handoff.target_provider = $2
AND handoff.source_revision_id = $3::bigint
AND handoff.target_revision_id IS NOT NULL
AND handoff.target_revision_id <>
handoff.source_revision_id
AND handoff.lifecycle_operation_id IS NOT NULL
AND ($4::uuid IS NULL OR handoff.handoff_id = $4::uuid)
AND ($5::uuid IS NULL OR
handoff.lifecycle_operation_id = $5::uuid)
AND handoff.state IN (
'requested', 'draining', 'transforming'
)
AND session.provider = 'wwebjs'
AND session.state = 'handoff'
AND session.active_revision_id = $3::bigint
AND source.provider = 'wwebjs'
AND source.status IN ('staging', 'validating', 'active')
ORDER BY handoff.created_at DESC
LIMIT 1
FOR SHARE OF session, source`,
[
this.sessionId,
targetProvider,
this.revisionId,
expected.handoffId || null,
expected.lifecycleOperationId || null,
],
);
}, this.revisionId);
if (result.rowCount !== 1) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_manager_handoff_missing',
);
}
return {
handoffId: result.rows[0].handoff_id,
sourceRevisionId: result.rows[0].source_revision_id,
targetRevisionId: result.rows[0].target_revision_id,
lifecycleOperationId: result.rows[0].lifecycle_operation_id,
state: result.rows[0].state,
targetProvider,
};
}
async prepareHandoff(
targetProvider,
{
profilePath,
projection,
handoffId,
lifecycleOperationId,
sourceRevisionId,
canonicalProjection: fallbackCanonicalProjection,
lastGoodProfileFallback,
} = {},
) {
const authorized = await this.assertAuthorizedHandoff(targetProvider, {
handoffId,
lifecycleOperationId,
});
if (
sourceRevisionId &&
String(authorized.sourceRevisionId) !== String(sourceRevisionId)
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_handoff_source_revision_mismatch',
);
}
const preparingSource = ['staging', 'validating'].includes(
this.revisionStatus,
);
const fallback = normalizeLastGoodProfileFallback(
lastGoodProfileFallback,
this.maxProfileBytes,
);
if (
fallback &&
(preparingSource || profilePath || !fallbackCanonicalProjection)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_last_good_profile_fallback_invalid',
);
}
if (profilePath && !preparingSource && !fallbackCanonicalProjection) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_profile_canonical_projection_required',
);
}
let stableProfileCheckpoint;
let stableProfileCheckpointDurationMs;
if (profilePath && !preparingSource) {
const checkpointStartedAt = Date.now();
stableProfileCheckpoint = await this.checkpointProfile({
profilePath,
projection,
canonicalProjection: fallbackCanonicalProjection,
source: 'handoff',
promote: true,
});
stableProfileCheckpointDurationMs =
Date.now() - checkpointStartedAt;
}
const useProfileAnchor = Boolean(
!preparingSource && !profilePath && fallbackCanonicalProjection,
);
const exportedProjection = await this.exportProjection();
let canonicalProjection = exportedProjection.canonicalProjection;
let fallbackAnchor;
if (fallback || useProfileAnchor) {
canonicalProjection = normalizeStoreCanonicalProjection(
fallbackCanonicalProjection,
this.webVersion,
);
fallbackAnchor = await this.loadLastGoodProfileAnchor(
fallback,
authorized.sourceRevisionId,
);
exportedProjection.canonicalProjection = canonicalProjection;
exportedProjection.artifact = {
artifact_id: fallbackAnchor.artifact.artifact_id,
manifest: fallbackAnchor.artifact.manifest,
checksum_sha256: fallbackAnchor.artifact.checksum_sha256,
size_bytes: fallbackAnchor.metadata.sizeBytes,
};
}
const hasProviderProjection = Boolean(
exportedProjection.provider_projection,
);
const checkpointRecords = hasProviderProjection
? exportedProjection.provider_projection.records
: [];
const indexedDBStores =
exportedProjection.provider_projection?.indexeddb_stores || [];
let checkpointArtifact = exportedProjection.artifact;
let checkpointRecordCount;
let pqServerRollback;
let pqServerRollbackMarker;
if (preparingSource) {
if (
exportedProjection.identity ||
checkpointArtifact ||
!Array.isArray(checkpointRecords) ||
checkpointRecords.length !== 0 ||
!Array.isArray(indexedDBStores) ||
indexedDBStores.length !== 0
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_preparing_handoff_source_not_empty',
);
}
checkpointArtifact = {
manifest: { empty: true },
checksum_sha256: sha256(Buffer.alloc(0)),
size_bytes: 0,
};
if (!hasProviderProjection) {
exportedProjection.provider_projection = {
schema_version: BRIDGE_SCHEMA_VERSION,
web_version: this.webVersion,
complete: false,
indexeddb_stores: [],
records: [],
};
}
exportedProjection.artifact = checkpointArtifact;
checkpointRecordCount = 0;
} else {
if (!canonicalProjection?.device) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_canonical_handoff_checkpoint_missing',
);
}
// Defense in depth for callers that bypass RemoteAuth. The source
// browser performs the same check before it is closed so a
// rejection normally leaves the WWeb session online.
assertCanonicalTargetProviderCompatibility(
canonicalProjection,
targetProvider,
);
if (targetProvider === 'whatsmeow' && !preparingSource) {
pqServerRollback = canonicalProjection.capabilities;
pqServerRollbackMarker =
await this.loadPendingPostQuantumServerRollback();
this.assertPostQuantumServerRollbackProof(
pqServerRollbackMarker,
authorized,
pqServerRollback,
);
}
checkpointRecordCount = canonicalProjection.record_count + 1;
}
if (
!checkpointArtifact ||
(!preparingSource &&
!UUID_PATTERN.test(String(checkpointArtifact.artifact_id))) ||
!/^[0-9a-f]{64}$/.test(checkpointArtifact.checksum_sha256) ||
!Number.isSafeInteger(checkpointArtifact.size_bytes) ||
checkpointArtifact.size_bytes < 0 ||
!Array.isArray(checkpointRecords) ||
!Number.isSafeInteger(checkpointRecordCount) ||
checkpointRecordCount < 0
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_checkpoint_proof_invalid',
);
}
const appStateSnapshotCapabilities = preparingSource
? {
app_state_snapshot_resync_required: false,
app_state_snapshot_resync_collections: [],
}
: normalizeCanonicalAppStateSnapshotCapabilities(
canonicalProjection.capabilities,
);
if (!preparingSource) {
assertCanonicalAppStateSnapshotResyncProjectionNormalized(
canonicalProjection,
appStateSnapshotCapabilities,
);
}
const profileCheckpointMode = stableProfileCheckpoint
? 'full_profile_plus_fresh_canonical_v1'
: fallback
? LAST_GOOD_PROFILE_FALLBACK_MODE
: useProfileAnchor
? PROFILE_ANCHOR_CHECKPOINT_MODE
: undefined;
const profileCheckpointArtifact =
fallbackAnchor?.artifact ||
(stableProfileCheckpoint
? {
artifact_id: checkpointArtifact.artifact_id,
persisted_at: null,
}
: undefined);
const profileCheckpointMetadata = profileCheckpointMode
? JSON.stringify({
profile_checkpoint_mode: profileCheckpointMode,
profile_artifact_id: profileCheckpointArtifact.artifact_id,
profile_artifact_persisted_at:
profileCheckpointArtifact.persisted_at ?? null,
...(stableProfileCheckpoint
? {
profile_checkpoint_duration_ms:
stableProfileCheckpointDurationMs,
profile_uploaded_bytes:
stableProfileCheckpoint.uploadedBytes,
profile_reused_bytes:
stableProfileCheckpoint.reusedBytes,
}
: {}),
...(fallback
? {
rejected_profile_size_bytes: fallback.sizeBytes,
profile_max_bytes: fallback.maxBytes,
}
: {}),
canonical_record_count: canonicalProjection.record_count,
canonical_size_bytes: canonicalProjection.size_bytes,
})
: undefined;
const checkpointManifestSuffix = profileCheckpointMetadata
? ') || $23::jsonb'
: ')';
await this.withLeaseMutation(async (client) => {
if (fallbackAnchor) {
await this.assertLastGoodProfileAnchor(client, fallbackAnchor);
const authorizationGuard = await client.query(
`/* wwebjs_handoff_last_good_profile_authorization_guard */
SELECT EXISTS (
SELECT 1
FROM whatsapp_session_handoff handoff
WHERE handoff.session_id = $1
AND handoff.handoff_id = $2::uuid
AND handoff.lifecycle_operation_id =
$3::uuid
AND handoff.source_provider = 'wwebjs'
AND handoff.target_provider = $4
AND handoff.source_revision_id = $5::bigint
AND handoff.target_revision_id IS NOT NULL
AND handoff.target_revision_id <>
handoff.source_revision_id
AND handoff.target_revision_id = $6::bigint
AND handoff.state IN (
'requested', 'draining', 'transforming'
)
) AS authorization_matches`,
[
this.sessionId,
authorized.handoffId,
authorized.lifecycleOperationId,
targetProvider,
authorized.sourceRevisionId,
authorized.targetRevisionId,
],
);
if (
authorizationGuard.rowCount !== 1 ||
authorizationGuard.rows[0]?.authorization_matches !== true
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_manager_handoff_missing',
);
}
// The browser is already quiesced and terminated. Persist the
// final provider-neutral state in the same lease transaction
// that publishes its handoff proof, while the verified native
// profile remains the rollback base for an active restart.
await this.persistCanonicalWithLastGoodProfileAnchor(client, {
anchor: fallbackAnchor,
canonicalProjection,
source: 'handoff',
checkpointMode: profileCheckpointMode,
});
}
if (!preparingSource) {
const canonicalRowAggregates = CANONICAL_TABLE_CONFIGS.map(
({ table }) =>
`SELECT count(*)::bigint AS row_count
FROM ${table}
WHERE session_id = $1
AND revision_id = $2::bigint`,
);
const checkpointGuard = await client.query(
`/* wwebjs_handoff_checkpoint_projection_guard */
SELECT NOT EXISTS (
SELECT 1
FROM whatsapp_app_state_version
WHERE session_id = $1
AND revision_id = $2::bigint
AND name = ANY($3::text[])
) AND NOT EXISTS (
SELECT 1
FROM whatsapp_app_state_mutation_macs
WHERE session_id = $1
AND revision_id = $2::bigint
AND name = ANY($3::text[])
) AS app_state_normalized,
(
SELECT COALESCE(sum(row_count), 0)::bigint
FROM (${canonicalRowAggregates.join(
' UNION ALL ',
)}) AS canonical_counts
) = $4::bigint AS canonical_record_count_matches,
EXISTS (
SELECT 1
FROM whatsapp_artifact
WHERE session_id = $1
AND revision_id = $2::bigint
AND artifact_id = $5::uuid
AND provider = 'wwebjs'
AND kind = $8
AND status = 'ready'
AND checksum_sha256 = $6
AND size_bytes = $7::bigint
) AS profile_artifact_matches`,
[
this.sessionId,
authorized.sourceRevisionId,
appStateSnapshotCapabilities.app_state_snapshot_resync_collections,
canonicalProjection.record_count,
checkpointArtifact.artifact_id,
checkpointArtifact.checksum_sha256,
checkpointArtifact.size_bytes,
PROFILE_ARTIFACT_KIND,
],
);
const guard = checkpointGuard.rows[0];
if (
checkpointGuard.rowCount !== 1 ||
guard?.app_state_normalized !== true
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_app_state_snapshot_resync_source_inconsistent',
);
}
if (guard.canonical_record_count_matches !== true) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_checkpoint_record_count_changed',
);
}
if (guard.profile_artifact_matches !== true) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_checkpoint_artifact_changed',
);
}
}
if (targetProvider === 'whatsmeow' && !preparingSource) {
const persistedMarker = await client.query(
`SELECT payload
FROM whatsapp_provider_record
WHERE session_id = $1
AND revision_id = $2::bigint
AND namespace = $3
AND record_key = $4
AND codec_version = 1
FOR SHARE`,
[
this.sessionId,
authorized.sourceRevisionId,
LIFECYCLE_NAMESPACE,
PQ_SERVER_ROLLBACK_MARKER_KEY,
],
);
if (persistedMarker.rowCount !== 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_marker_missing',
);
}
const currentMarker = this.parsePostQuantumServerRollbackMarker(
persistedMarker.rows[0].payload,
);
this.assertPostQuantumServerRollbackProof(
currentMarker,
authorized,
pqServerRollback,
);
if (
JSON.stringify(currentMarker) !==
JSON.stringify(pqServerRollbackMarker)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_pq_rollback_marker_changed',
);
}
}
const proof = await client.query(
`INSERT INTO whatsapp_artifact (
session_id, artifact_id, revision_id, provider, kind,
status, manifest, checksum_sha256, size_bytes,
chunk_count, created_at, persisted_at
) VALUES (
$1, $2::uuid, $3::bigint, 'wwebjs',
'provider_handoff_checkpoint', 'ready',
jsonb_build_object(
'handoff_id', $2::text,
'lifecycle_operation_id', $4::text,
'record_count', $7::bigint,
'canonical_signal_session_scopes',
$12::jsonb,
'canonical_signal_session_resync_required',
$13::boolean,
'canonical_signal_session_resync_scopes',
$14::jsonb,
'pq_server_rollback_acknowledged', $8::boolean,
'pq_server_rollback_protocol', $9::text,
'pq_server_rollback_acknowledged_at_ms', $10::bigint,
'pq_server_rollback_source_revision_id', $11::text,
'pq_server_rollback_lifecycle_operation_id',
$15::text,
'pq_server_rollback_owner_id', $16::text,
'pq_server_rollback_fencing_token', $17::text,
'pq_server_rollback_generation', $18::integer,
'pq_server_rollback_epoch', $19::text,
'pq_server_rollback_response_validated', $20::boolean,
'app_state_snapshot_resync_required', $21::boolean,
'app_state_snapshot_resync_collections', $22::jsonb
${checkpointManifestSuffix},
$5, $6::bigint, 0, now(), now()
)
ON CONFLICT (session_id, artifact_id) DO UPDATE SET
revision_id = EXCLUDED.revision_id,
provider = EXCLUDED.provider,
kind = EXCLUDED.kind,
status = EXCLUDED.status,
manifest = EXCLUDED.manifest,
checksum_sha256 = EXCLUDED.checksum_sha256,
size_bytes = EXCLUDED.size_bytes,
chunk_count = 0,
persisted_at = now()
WHERE whatsapp_artifact.revision_id = EXCLUDED.revision_id
AND whatsapp_artifact.provider = 'wwebjs'
AND whatsapp_artifact.kind =
'provider_handoff_checkpoint'`,
[
this.sessionId,
authorized.handoffId,
authorized.sourceRevisionId,
authorized.lifecycleOperationId,
checkpointArtifact.checksum_sha256,
checkpointArtifact.size_bytes,
checkpointRecordCount,
pqServerRollback?.pq_server_rollback_acknowledged ?? null,
pqServerRollback?.pq_server_rollback_protocol ?? null,
pqServerRollback?.pq_server_rollback_acknowledged_at_ms ??
null,
pqServerRollback?.pq_server_rollback_source_revision_id ??
null,
JSON.stringify(
canonicalProjection?.capabilities
?.canonical_signal_session_scopes || [
'default',
'status',
'pq',
],
),
canonicalProjection?.capabilities
?.canonical_signal_session_resync_required === true,
JSON.stringify(
canonicalProjection?.capabilities
?.canonical_signal_session_resync_scopes || [],
),
pqServerRollback?.pq_server_rollback_lifecycle_operation_id ??
null,
pqServerRollback?.pq_server_rollback_owner_id ?? null,
pqServerRollback?.pq_server_rollback_fencing_token ?? null,
pqServerRollback?.pq_server_rollback_generation ?? null,
pqServerRollback?.pq_server_rollback_epoch ?? null,
pqServerRollback?.pq_server_rollback_response_validated ??
null,
appStateSnapshotCapabilities.app_state_snapshot_resync_required,
JSON.stringify(
appStateSnapshotCapabilities.app_state_snapshot_resync_collections,
),
...(profileCheckpointMetadata
? [profileCheckpointMetadata]
: []),
],
);
if (proof.rowCount !== 1) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_handoff_checkpoint_proof_rejected',
);
}
}, this.revisionId);
this.log('handoff.prepared', {
handoff_id: authorized.handoffId,
target_provider: targetProvider,
target_revision_id: authorized.targetRevisionId,
...(profileCheckpointMode
? {
profile_checkpoint_mode: profileCheckpointMode,
profile_artifact_id:
profileCheckpointArtifact.artifact_id,
...(stableProfileCheckpoint
? {
profile_checkpoint_duration_ms:
stableProfileCheckpointDurationMs,
profile_size_bytes:
stableProfileCheckpoint.sizeBytes,
profile_uploaded_bytes:
stableProfileCheckpoint.uploadedBytes,
profile_reused_bytes:
stableProfileCheckpoint.reusedBytes,
}
: {}),
...(fallback
? {
rejected_profile_size_bytes: fallback.sizeBytes,
profile_max_bytes: fallback.maxBytes,
}
: {}),
}
: {}),
});
return {
handoffId: authorized.handoffId,
lifecycleOperationId: authorized.lifecycleOperationId,
sourceRevisionId: authorized.sourceRevisionId,
targetRevisionId: authorized.targetRevisionId,
checkpointRecordCount,
canonicalRecordCount: canonicalProjection?.record_count || 0,
...this.describe(),
projection: exportedProjection,
};
}
async promote(revisionId = this.revisionId) {
const normalized = normalizeRevisionId(revisionId);
if (normalized !== this.revisionId) {
throw new WhatsAppSessionError(
'whatsapp_session_promotion_revision_mismatch',
);
}
if (
this.isHandoffRevision() ||
this.sourceRevisionId ||
this.handoffId ||
['handoff', 'secure_import'].includes(this.revisionSource) ||
(this.sourceProvider && this.sourceProvider !== PROVIDER)
) {
throw new WhatsAppSessionCompatibilityError(
'wwebjs_candidate_activation_commit_required',
);
}
if (
normalized === this.revisionId &&
this.revisionStatus === 'active'
) {
return this.describe();
}
await this.withLifecycleTransaction((client) =>
this.promoteInTransaction(client, normalized),
);
this.revisionId = normalized;
this.revisionStatus = 'active';
this.updateLoggerContext({ revisionId: normalized });
this.log('handoff.promoted', {
promoted_revision_id: normalized,
});
return this.describe();
}
async commitActivation(revisionId = this.revisionId) {
const normalized = normalizeRevisionId(revisionId);
if (normalized !== this.revisionId) {
throw new WhatsAppSessionError(
'whatsapp_session_activation_revision_mismatch',
);
}
if (!this.sourceRevisionId) {
throw new WhatsAppSessionError(
'whatsapp_session_candidate_source_missing',
);
}
await this.withLifecycleTransaction(async (client) => {
const result = await client.query(
`SELECT commit_whatsapp_session_activation(
$1, $2::bigint, $3::bigint, $4, $5::bigint,
$6, $7, $8, $9
) AS committed`,
[
this.sessionId,
this.sourceRevisionId,
normalized,
this.ownerId,
this.fencingToken,
this.generation,
this.epoch,
this.runtimeCapability,
this.validatedJid || null,
],
);
if (result.rows[0]?.committed !== true) {
throw new WhatsAppSessionError(
'whatsapp_session_activation_commit_failed',
);
}
// commit_whatsapp_session_activation is the authoritative atomic
// transition. It already locks and verifies the source/target
// revisions, session header, handoff lineage, activation marker,
// retained profile artifact and every affected row count before
// returning true. Re-querying the same transition here coupled
// this package to a duplicate row-shape contract and could reject
// a successful server-side CAS. Keep the app-state proof in this
// transaction, but do not second-guess the stored procedure.
// The lifecycle function locks worker -> lease -> session first.
// Re-read and lock the durable gate only after that fixed lock
// order, but before COMMIT, so a changed/missing proof rolls the
// activation CAS back atomically instead of creating a TOCTOU gap.
await this.assertAppStateSnapshotResyncGateInitializedInTransaction(
client,
{ forUpdate: true },
);
});
this.revisionStatus = 'active';
this.updateLoggerContext({ revisionId: normalized });
this.log(
'handoff.activation_committed',
{ activated_revision_id: normalized },
{ force: true },
);
return this.describe();
}
async finalizeActivation(revisionId = this.revisionId) {
const normalized = normalizeRevisionId(revisionId);
if (
normalized !== this.revisionId ||
this.revisionStatus !== 'active'
) {
throw new WhatsAppSessionError(
'whatsapp_session_activation_finalize_revision_mismatch',
);
}
await this.withLifecycleTransaction(async (client) => {
const result = await client.query(
`SELECT finalize_whatsapp_session_activation(
$1, $2::bigint, $3, $4::bigint, $5, $6, $7
) AS finalized`,
[
this.sessionId,
normalized,
this.ownerId,
this.fencingToken,
this.generation,
this.epoch,
this.runtimeCapability,
],
);
if (result.rows[0]?.finalized !== true) {
throw new WhatsAppSessionError(
'whatsapp_session_activation_finalize_failed',
);
}
// Keep finalization and the last database-state proof in one
// transaction. If a ready artifact or its canonical rows have
// diverged, throwing here rolls the finalization CAS back.
await this.assertAppStateSnapshotResyncGateReadyInTransaction(
client,
{ forUpdate: true },
);
});
this.log(
'handoff.activation_finalized',
{ activated_revision_id: normalized },
{ force: true },
);
return this.describe();
}
async rollback(errorCode = 'handoff_validation_failed') {
if (
!['staging', 'validating'].includes(this.revisionStatus) ||
!this.isHandoffRevision()
) {
throw new WhatsAppSessionCompatibilityError(
'whatsapp_session_rollback_after_activation_forbidden',
);
}
const failedRevisionId = this.revisionId;
const previousRevisionId = this.sourceRevisionId;
const previousProvider = this.sourceProvider;
const secureImportRollback = this.revisionSource === 'secure_import';
const secureImportRollbackContext =
secureImportRollback &&
this.secureImportRollbackContext?.revisionId === previousRevisionId
? this.secureImportRollbackContext
: undefined;
if (!failedRevisionId || !previousRevisionId) {
throw new WhatsAppSessionError(
'whatsapp_session_rollback_revision_missing',
);
}
const diagnosticErrorCode = safeDiagnosticErrorCode(
errorCode,
'handoff_validation_failed',
);
await this.withLifecycleTransaction(async (client) => {
const rolledBack = await client.query(
`SELECT rollback_whatsapp_session_revision(
$1, $2::bigint, $3::bigint, $4, $5::bigint, $6, $7, $8,
$9
) AS rolled_back`,
[
this.sessionId,
failedRevisionId,
previousRevisionId,
this.ownerId,
this.fencingToken,
this.generation,
this.epoch,
this.runtimeCapability,
diagnosticErrorCode,
],
);
if (rolledBack.rows[0]?.rolled_back !== true) {
throw new WhatsAppSessionError(
'whatsapp_session_rollback_failed',
);
}
});
this.revisionId = previousRevisionId;
this.revisionStatus = secureImportRollbackContext?.status || 'active';
this.revisionSource = secureImportRollbackContext?.source || 'rollback';
this.sourceRevisionId = undefined;
this.sourceProvider = undefined;
this.handoffId = undefined;
this.lifecycleOperationId = undefined;
this.secureImportRollbackContext = undefined;
this.updateLoggerContext({ revisionId: previousRevisionId });
this.log(
'handoff.rolled_back',
{
failed_revision_id: failedRevisionId,
restored_revision_id: previousRevisionId,
restored_provider: previousProvider,
error_code: diagnosticErrorCode,
},
{ force: true },
);
return {
revisionId: previousRevisionId,
provider: previousProvider,
};
}
async failRevision(revisionId, error) {
if (!revisionId) return;
if (this.sourceRevisionId) {
await this.rollback(
error?.code || error?.message || 'checkpoint_failed',
);
return;
}
await this.withLeaseMutation(async (client) => {
const cleared = await client.query(
`SELECT clear_whatsapp_session(
$1, $2, $3::bigint, $4, $5, $6
) AS cleared`,
[
this.sessionId,
this.ownerId,
this.fencingToken,
this.generation,
this.epoch,
this.runtimeCapability,
],
);
if (cleared.rows[0]?.cleared !== true) {
throw new WhatsAppSessionError('whatsapp_session_clear_failed');
}
}, revisionId);
this.revisionId = undefined;
this.revisionStatus = undefined;
this.log('session.failed_pairing_cleared', {
error_code: safeDiagnosticErrorCode(
error?.code,
'wwebjs_checkpoint_failed',
),
});
}
async delete() {
await this.withLeaseMutation(async (client) => {
const cleared = await client.query(
`SELECT clear_whatsapp_session(
$1, $2, $3::bigint, $4, $5, $6
) AS cleared`,
[
this.sessionId,
this.ownerId,
this.fencingToken,
this.generation,
this.epoch,
this.runtimeCapability,
],
);
if (cleared.rows[0]?.cleared !== true) {
throw new WhatsAppSessionError('whatsapp_session_clear_failed');
}
});
this.revisionId = undefined;
this.revisionStatus = undefined;
}
async releaseLease() {
if (!this.fencingToken) return false;
clearInterval(this.leaseInterval);
clearInterval(this.leaseSummaryInterval);
clearTimeout(this.leaseDeadlineTimer);
this.leaseReleasePending = true;
await this.renewInFlight?.catch(() => {});
const fencingToken = this.fencingToken;
const result = await this.pool.query(
`SELECT release_whatsapp_session_lease(
$1, $2, 'wwebjs', $3::bigint, $4, $5, $6
) AS released`,
[
this.sessionId,
this.ownerId,
fencingToken,
this.generation,
this.epoch,
this.runtimeCapability,
],
);
const released = result.rows[0]?.released === true;
this.log('lease.released', {
fencing_token: fencingToken,
released,
});
if (released) {
this.fencingToken = undefined;
this.updateLoggerContext({ fencingToken: undefined });
this.opened = false;
this.leaseReleasePending = false;
}
return released;
}
async close({ requireLeaseRelease = false } = {}) {
if (this.closed) return !this.fencingToken;
if (requireLeaseRelease && !this.fencingToken) {
throw new WhatsAppSessionError(
'whatsapp_session_handoff_lease_not_held',
);
}
let released = !this.fencingToken;
if (this.fencingToken) {
try {
released = await this.releaseLease();
} catch (error) {
if (requireLeaseRelease) throw error;
}
}
if (requireLeaseRelease && !released) {
throw new WhatsAppSessionError(
'whatsapp_session_handoff_lease_release_rejected',
);
}
if (released) {
this.closed = true;
if (this.ownsPool) await this.pool.end();
this.log('session.closed');
}
return released;
}
}
module.exports = {
APP_STATE_SNAPSHOT_RESYNC_GATE_KIND,
DEFAULT_LEASE_RENEW_MS,
DEFAULT_LEASE_SAFETY_MS,
DEFAULT_LEASE_TTL_MS,
PostgresSessionStore,
CANONICAL_SESSION_STORE_ABI,
SESSION_STORE_KIND,
appStateSnapshotResyncArtifactId,
canonicalAppStateSnapshotResyncManifest,
};