'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 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 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 = {
cleanChromiumProfileLocks,
importWhatsAppWebSessionToLocalAuth,
resolveLocalAuthSessionPath,
};