Source: util/SecureSessionImport.js

'use strict';

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

const CHROMIUM_LOCK_FILES = new Set([
    'DevToolsActivePort',
    'SingletonCookie',
    'SingletonLock',
    'SingletonSocket',
]);
const WHATSAPP_WEB_PACKAGE_V1 = 'underchat-wa-web-session-v1';
const BROWSER_PROJECTION_SCHEMA_VERSION = 2;
const MAX_BROWSER_PROJECTION_RECORDS = 20000;
const MAX_BROWSER_PROJECTION_BYTES = 64 * 1024 * 1024;
const MAX_BROWSER_VALUE_DEPTH = 20;

const isRecord = (value) =>
    Boolean(value) && typeof value === 'object' && !Array.isArray(value);

const assertSupportedPackage = (sessionPackage) => {
    if (!sessionPackage || sessionPackage.source !== 'whatsapp_web') {
        throw new Error('wwebjs_import_source_unsupported');
    }

    const targetProvider = sessionPackage.target_provider || 'auto';
    if (targetProvider !== 'auto' && targetProvider !== 'wwebjs') {
        throw new Error('wwebjs_import_target_provider_mismatch');
    }
};

const assertExactObjectKeys = (value, keys, code) => {
    const actual = Object.keys(value).sort();
    const expected = [...keys].sort();
    if (
        actual.length !== expected.length ||
        actual.some((key, index) => key !== expected[index])
    ) {
        throw new Error(code);
    }
};

const normalizeBase64 = (value) => {
    if (
        typeof value !== 'string' ||
        !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
            value,
        )
    ) {
        throw new Error('wwebjs_import_profile_binary_invalid');
    }
    const bytes = Buffer.from(value, 'base64');
    if (bytes.toString('base64') !== value) {
        throw new Error('wwebjs_import_profile_binary_invalid');
    }
    return value;
};

const convertBrowserValue = (value, depth = 0) => {
    if (depth > MAX_BROWSER_VALUE_DEPTH) {
        throw new Error('wwebjs_import_profile_value_too_deep');
    }
    if (
        value === null ||
        typeof value === 'string' ||
        typeof value === 'boolean'
    ) {
        return value;
    }
    if (typeof value === 'number') {
        if (!Number.isFinite(value)) {
            throw new Error('wwebjs_import_profile_value_invalid');
        }
        return value;
    }
    if (Array.isArray(value)) {
        return value.map((item) => convertBrowserValue(item, depth + 1));
    }
    if (!isRecord(value)) {
        throw new Error('wwebjs_import_profile_value_invalid');
    }

    const hasWwebjsTag = Object.hasOwn(value, '__wwebjs_type');
    const hasUnderchatTag = Object.hasOwn(value, '__underchat_type');
    if (hasWwebjsTag && hasUnderchatTag) {
        throw new Error('wwebjs_import_profile_value_invalid');
    }
    const tagKey = hasWwebjsTag
        ? '__wwebjs_type'
        : hasUnderchatTag
          ? '__underchat_type'
          : undefined;
    const taggedType = tagKey ? value[tagKey] : undefined;
    if (taggedType !== undefined) {
        if (typeof taggedType !== 'string') {
            throw new Error('wwebjs_import_profile_value_invalid');
        }
        if (taggedType === 'undefined') {
            assertExactObjectKeys(
                value,
                [tagKey],
                'wwebjs_import_profile_value_invalid',
            );
            return { __wwebjs_type: 'undefined' };
        }
        if (taggedType === 'bytes' || taggedType === 'array_buffer') {
            assertExactObjectKeys(
                value,
                [tagKey, 'data'],
                'wwebjs_import_profile_value_invalid',
            );
            return {
                __wwebjs_type: 'array_buffer',
                data: normalizeBase64(value.data),
            };
        }
        if (taggedType === 'bigint') {
            assertExactObjectKeys(
                value,
                [tagKey, 'value'],
                'wwebjs_import_profile_value_invalid',
            );
            if (
                typeof value.value !== 'string' ||
                !/^-?(?:0|[1-9]\d*)$/.test(value.value)
            ) {
                throw new Error('wwebjs_import_profile_bigint_invalid');
            }
            return { __wwebjs_type: 'bigint', value: value.value };
        }
        if (taggedType === 'date') {
            assertExactObjectKeys(
                value,
                [tagKey, 'value'],
                'wwebjs_import_profile_value_invalid',
            );
            if (
                typeof value.value !== 'string' ||
                !Number.isFinite(Date.parse(value.value)) ||
                new Date(value.value).toISOString() !== value.value
            ) {
                throw new Error('wwebjs_import_profile_date_invalid');
            }
            return { __wwebjs_type: 'date', value: value.value };
        }
        if (taggedType === 'blob') {
            assertExactObjectKeys(
                value,
                [tagKey, 'mime_type', 'data'],
                'wwebjs_import_profile_value_invalid',
            );
            if (typeof value.mime_type !== 'string') {
                throw new Error('wwebjs_import_profile_blob_invalid');
            }
            return {
                __wwebjs_type: 'blob',
                mime_type: value.mime_type,
                data: normalizeBase64(value.data),
            };
        }
        if (taggedType === 'regexp') {
            assertExactObjectKeys(
                value,
                [tagKey, 'source', 'flags'],
                'wwebjs_import_profile_value_invalid',
            );
            if (
                typeof value.source !== 'string' ||
                typeof value.flags !== 'string'
            ) {
                throw new Error('wwebjs_import_profile_regexp_invalid');
            }
            let regexp;
            try {
                regexp = new RegExp(value.source, value.flags);
            } catch {
                throw new Error('wwebjs_import_profile_regexp_invalid');
            }
            if (regexp.flags !== value.flags) {
                throw new Error('wwebjs_import_profile_regexp_invalid');
            }
            return {
                __wwebjs_type: 'regexp',
                source: value.source,
                flags: value.flags,
            };
        }
        if (taggedType === 'map') {
            assertExactObjectKeys(
                value,
                [tagKey, 'entries'],
                'wwebjs_import_profile_value_invalid',
            );
            if (
                !Array.isArray(value.entries) ||
                value.entries.some(
                    (entry) => !Array.isArray(entry) || entry.length !== 2,
                )
            ) {
                throw new Error('wwebjs_import_profile_map_invalid');
            }
            return {
                __wwebjs_type: 'map',
                entries: value.entries.map(([key, entry]) => [
                    convertBrowserValue(key, depth + 1),
                    convertBrowserValue(entry, depth + 1),
                ]),
            };
        }
        if (taggedType === 'set') {
            assertExactObjectKeys(
                value,
                [tagKey, 'values'],
                'wwebjs_import_profile_value_invalid',
            );
            if (!Array.isArray(value.values)) {
                throw new Error('wwebjs_import_profile_set_invalid');
            }
            return {
                __wwebjs_type: 'set',
                values: value.values.map((entry) =>
                    convertBrowserValue(entry, depth + 1),
                ),
            };
        }
        // A package advertised as complete may not contain placeholders that
        // lose identity-bearing browser values.
        throw new Error('wwebjs_import_profile_value_lossy');
    }

    return Object.fromEntries(
        Object.entries(value).map(([key, item]) => [
            key,
            convertBrowserValue(item, depth + 1),
        ]),
    );
};

const normalizeKeyPath = (keyPath) => {
    if (keyPath === null || typeof keyPath === 'string') return keyPath;
    if (
        Array.isArray(keyPath) &&
        keyPath.every((entry) => typeof entry === 'string')
    ) {
        return [...keyPath];
    }
    throw new Error('wwebjs_import_profile_key_path_invalid');
};

/**
 * Converts the Chrome extension's lossless WhatsApp Web profile into the
 * BrowserSessionBridge ABI. Only packages produced by the loss-aware v1
 * serializer are accepted. Older v1 packages omitted completeness metadata
 * while silently dropping CryptoKey/cyclic values and are intentionally
 * rejected rather than treated as a recoverable browser session.
 */
const browserProjectionFromWhatsAppWebProfile = (sessionPackage) => {
    assertSupportedPackage(sessionPackage);
    if (sessionPackage.format_version !== WHATSAPP_WEB_PACKAGE_V1) {
        throw new Error('wwebjs_import_format_unsupported');
    }
    if (
        typeof sessionPackage.web_version !== 'string' ||
        !sessionPackage.web_version.trim() ||
        sessionPackage.web_version.length > 100 ||
        sessionPackage.web_version !== sessionPackage.web_version.trim()
    ) {
        throw new Error('wwebjs_import_web_version_missing');
    }
    const profile = sessionPackage.payload?.whatsapp_web_profile;
    if (!isRecord(profile)) {
        throw new Error('wwebjs_import_profile_missing');
    }
    if (profile.complete !== true || profile.lossyRecordCount !== 0) {
        throw new Error('wwebjs_import_profile_lossy');
    }
    if (profile.serializationFormat !== 'wwebjs-browser-value-v1') {
        throw new Error('wwebjs_import_profile_serialization_unsupported');
    }
    if (!isRecord(profile.localStorage) || !isRecord(profile.signalStorage)) {
        throw new Error('wwebjs_import_profile_storage_missing');
    }
    const signalStorage = profile.signalStorage;
    if (
        signalStorage.databaseName !== 'signal-storage' ||
        !Array.isArray(signalStorage.stores)
    ) {
        throw new Error('wwebjs_import_profile_signal_storage_invalid');
    }

    const records = [];
    const indexeddbStores = [];
    const addresses = new Set();
    const storeNames = new Set();
    const pushRecord = (record) => {
        const address = `${record.namespace}\u0000${record.record_key}`;
        if (addresses.has(address)) {
            throw new Error('wwebjs_import_profile_duplicate_record');
        }
        addresses.add(address);
        records.push(record);
        if (records.length > MAX_BROWSER_PROJECTION_RECORDS) {
            throw new Error('wwebjs_import_profile_limit_exceeded');
        }
    };

    for (const [key, value] of Object.entries(profile.localStorage)) {
        if (!key || typeof value !== 'string') {
            throw new Error('wwebjs_import_profile_local_storage_invalid');
        }
        pushRecord({
            namespace: 'local_storage',
            record_key: key,
            value,
        });
    }
    for (const store of signalStorage.stores) {
        if (
            !isRecord(store) ||
            typeof store.name !== 'string' ||
            !store.name ||
            storeNames.has(store.name) ||
            typeof store.autoIncrement !== 'boolean' ||
            !Array.isArray(store.records)
        ) {
            throw new Error('wwebjs_import_profile_store_invalid');
        }
        storeNames.add(store.name);
        const keyPath = normalizeKeyPath(store.keyPath);
        indexeddbStores.push({
            database_name: signalStorage.databaseName,
            store_name: store.name,
            key_path: keyPath,
        });
        const namespace = `indexeddb:${encodeURIComponent(signalStorage.databaseName)}:${encodeURIComponent(store.name)}`;
        for (const entry of store.records) {
            if (
                !isRecord(entry) ||
                !Object.hasOwn(entry, 'key') ||
                !Object.hasOwn(entry, 'value')
            ) {
                throw new Error('wwebjs_import_profile_record_invalid');
            }
            const convertedKey = convertBrowserValue(entry.key);
            const recordKey = JSON.stringify(convertedKey);
            if (recordKey === undefined) {
                throw new Error('wwebjs_import_profile_record_invalid');
            }
            pushRecord({
                namespace,
                record_key: recordKey,
                value: convertBrowserValue(entry.value),
                key_path: keyPath,
            });
        }
    }
    const sizeBytes = records.reduce(
        (total, record) => total + Buffer.byteLength(JSON.stringify(record)),
        0,
    );
    if (sizeBytes > MAX_BROWSER_PROJECTION_BYTES) {
        throw new Error('wwebjs_import_profile_limit_exceeded');
    }
    return {
        schema_version: BROWSER_PROJECTION_SCHEMA_VERSION,
        web_version: sessionPackage.web_version,
        complete: true,
        lossy_records: 0,
        size_bytes: sizeBytes,
        indexeddb_stores: indexeddbStores,
        records,
    };
};

const getLocalAuthPayload = (sessionPackage) => {
    const payload = sessionPackage.payload;
    if (!isRecord(payload)) {
        throw new Error('wwebjs_import_payload_missing');
    }

    const localAuthPayload =
        payload.wwebjs_local_auth ||
        payload.local_auth ||
        payload.wwebjsLocalAuth;

    if (!isRecord(localAuthPayload) || !isRecord(localAuthPayload.files)) {
        throw new Error('wwebjs_import_payload_unsupported');
    }

    return localAuthPayload.files;
};

const assertSafeRelativePath = (relativePath) => {
    if (
        !relativePath ||
        path.isAbsolute(relativePath) ||
        relativePath.split(/[\\/]/).includes('..') ||
        relativePath.includes('\0')
    ) {
        throw new Error(`wwebjs_import_invalid_profile_file:${relativePath}`);
    }
};

const decodeFileValue = (value) => {
    if (typeof value === 'string') {
        return Buffer.from(value, 'utf8');
    }

    if (!isRecord(value)) {
        throw new Error('wwebjs_import_invalid_file_payload');
    }

    const data = value.data ?? value.content;
    const encoding = value.encoding || 'utf8';

    if (typeof data !== 'string') {
        throw new Error('wwebjs_import_invalid_file_payload');
    }

    if (encoding !== 'utf8' && encoding !== 'base64') {
        throw new Error('wwebjs_import_invalid_file_encoding');
    }

    return Buffer.from(data, encoding);
};

const pathExists = async (targetPath) =>
    Boolean(await fs.promises.stat(targetPath).catch(() => undefined));

const sessionDirNameForClientId = (clientId) => {
    if (!clientId) {
        return 'session';
    }

    if (!/^[-_\w]+$/i.test(clientId)) {
        throw new Error('wwebjs_import_invalid_client_id');
    }

    return `session-${clientId}`;
};

const resolveLocalAuthSessionPath = ({ clientId, dataPath }) =>
    path.join(
        path.resolve(dataPath || './.wwebjs_auth/'),
        sessionDirNameForClientId(clientId),
    );

const writeProfileFiles = async (profilePath, files) => {
    await fs.promises.mkdir(profilePath, { recursive: true });

    await Promise.all(
        Object.entries(files).map(async ([relativePath, value]) => {
            assertSafeRelativePath(relativePath);

            const outputPath = path.join(profilePath, relativePath);
            await fs.promises.mkdir(path.dirname(outputPath), {
                recursive: true,
            });
            await fs.promises.writeFile(outputPath, decodeFileValue(value));
        }),
    );
};

const cleanChromiumProfileLocks = async (profilePath) => {
    const entries = await fs.promises
        .readdir(profilePath, { withFileTypes: true })
        .catch(() => []);

    await Promise.all(
        entries.map(async (entry) => {
            const entryPath = path.join(profilePath, entry.name);

            if (CHROMIUM_LOCK_FILES.has(entry.name)) {
                await fs.promises.rm(entryPath, {
                    recursive: true,
                    force: true,
                });
                return;
            }

            if (entry.isDirectory()) {
                await cleanChromiumProfileLocks(entryPath);
            }
        }),
    );
};

/**
 * Restores a normalized WhatsApp Web profile package into the directory used by LocalAuth.
 *
 * Raw browser cookies/localStorage are not enough to build a Chromium profile. The package must include
 * `payload.wwebjs_local_auth.files`, where each key is a profile-relative path and each value is either
 * a UTF-8 string or `{ data, encoding: "utf8" | "base64" }`.
 */
const importWhatsAppWebSessionToLocalAuth = async ({
    sessionPackage,
    clientId,
    dataPath,
    overwrite = true,
    backupPath,
    cleanupBackupOnSuccess = false,
}) => {
    assertSupportedPackage(sessionPackage);

    const files = getLocalAuthPayload(sessionPackage);
    const fileNames = Object.keys(files);
    if (!fileNames.length) {
        throw new Error('wwebjs_import_empty_profile');
    }

    const sessionPath = resolveLocalAuthSessionPath({ clientId, dataPath });
    const tempPath = `${sessionPath}.secure-import-${crypto.randomUUID()}`;
    const resolvedBackupPath =
        backupPath ||
        `${sessionPath}.backup-${Date.now()}-${crypto
            .randomUUID()
            .slice(0, 8)}`;
    let backupCreated = false;

    await fs.promises.mkdir(path.dirname(sessionPath), { recursive: true });

    try {
        await writeProfileFiles(tempPath, files);
        await cleanChromiumProfileLocks(tempPath);

        if (await pathExists(sessionPath)) {
            if (!overwrite) {
                throw new Error('wwebjs_import_auth_folder_exists');
            }

            await fs.promises.rm(resolvedBackupPath, {
                recursive: true,
                force: true,
            });
            await fs.promises.rename(sessionPath, resolvedBackupPath);
            backupCreated = true;
        }

        await fs.promises.rename(tempPath, sessionPath);

        if (backupCreated && cleanupBackupOnSuccess) {
            await fs.promises.rm(resolvedBackupPath, {
                recursive: true,
                force: true,
            });
        }

        return {
            sessionPath,
            backupPath:
                backupCreated && !cleanupBackupOnSuccess
                    ? resolvedBackupPath
                    : undefined,
            importedFiles: fileNames.sort(),
            formatVersion: sessionPackage.format_version,
            accountHint: sessionPackage.account_hint,
        };
    } catch (error) {
        await fs.promises
            .rm(tempPath, { recursive: true, force: true })
            .catch(() => undefined);

        if (backupCreated && !(await pathExists(sessionPath))) {
            await fs.promises
                .rename(resolvedBackupPath, sessionPath)
                .catch(() => undefined);
        }

        throw error;
    }
};

module.exports = {
    browserProjectionFromWhatsAppWebProfile,
    cleanChromiumProfileLocks,
    importWhatsAppWebSessionToLocalAuth,
    resolveLocalAuthSessionPath,
};