Web: harden WhatsApp creds handling
parent
801e44f4eb
commit
0241f1a29c
|
|
@ -20,6 +20,64 @@ import { CONFIG_DIR, ensureDir, jidToE164 } from "../utils.js";
|
||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
|
|
||||||
export const WA_WEB_AUTH_DIR = path.join(CONFIG_DIR, "credentials");
|
export const WA_WEB_AUTH_DIR = path.join(CONFIG_DIR, "credentials");
|
||||||
|
const WA_CREDS_PATH = path.join(WA_WEB_AUTH_DIR, "creds.json");
|
||||||
|
const WA_CREDS_BACKUP_PATH = path.join(WA_WEB_AUTH_DIR, "creds.json.bak");
|
||||||
|
|
||||||
|
function readCredsJsonRaw(filePath: string): string | null {
|
||||||
|
try {
|
||||||
|
if (!fsSync.existsSync(filePath)) return null;
|
||||||
|
const stats = fsSync.statSync(filePath);
|
||||||
|
if (!stats.isFile() || stats.size <= 1) return null;
|
||||||
|
return fsSync.readFileSync(filePath, "utf-8");
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRestoreCredsFromBackup(
|
||||||
|
logger: ReturnType<typeof getChildLogger>,
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
const raw = readCredsJsonRaw(WA_CREDS_PATH);
|
||||||
|
if (raw) {
|
||||||
|
// Validate that creds.json is parseable.
|
||||||
|
JSON.parse(raw);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupRaw = readCredsJsonRaw(WA_CREDS_BACKUP_PATH);
|
||||||
|
if (!backupRaw) return;
|
||||||
|
|
||||||
|
// Ensure backup is parseable before restoring.
|
||||||
|
JSON.parse(backupRaw);
|
||||||
|
fsSync.copyFileSync(WA_CREDS_BACKUP_PATH, WA_CREDS_PATH);
|
||||||
|
logger.warn(
|
||||||
|
{ credsPath: WA_CREDS_PATH },
|
||||||
|
"restored corrupted WhatsApp creds.json from backup",
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function safeSaveCreds(
|
||||||
|
saveCreds: () => Promise<void> | void,
|
||||||
|
logger: ReturnType<typeof getChildLogger>,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Best-effort backup so we can recover after abrupt restarts.
|
||||||
|
if (fsSync.existsSync(WA_CREDS_PATH)) {
|
||||||
|
fsSync.copyFileSync(WA_CREDS_PATH, WA_CREDS_BACKUP_PATH);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore backup failures
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await Promise.resolve(saveCreds());
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn({ error: String(err) }, "failed saving WhatsApp creds");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a Baileys socket backed by the multi-file auth store we keep on disk.
|
* Create a Baileys socket backed by the multi-file auth store we keep on disk.
|
||||||
|
|
@ -34,6 +92,8 @@ export async function createWaSocket(printQr: boolean, verbose: boolean) {
|
||||||
);
|
);
|
||||||
const logger = toPinoLikeLogger(baseLogger, verbose ? "info" : "silent");
|
const logger = toPinoLikeLogger(baseLogger, verbose ? "info" : "silent");
|
||||||
await ensureDir(WA_WEB_AUTH_DIR);
|
await ensureDir(WA_WEB_AUTH_DIR);
|
||||||
|
const sessionLogger = getChildLogger({ module: "web-session" });
|
||||||
|
maybeRestoreCredsFromBackup(sessionLogger);
|
||||||
const { state, saveCreds } = await useMultiFileAuthState(WA_WEB_AUTH_DIR);
|
const { state, saveCreds } = await useMultiFileAuthState(WA_WEB_AUTH_DIR);
|
||||||
const { version } = await fetchLatestBaileysVersion();
|
const { version } = await fetchLatestBaileysVersion();
|
||||||
const sock = makeWASocket({
|
const sock = makeWASocket({
|
||||||
|
|
@ -49,9 +109,7 @@ export async function createWaSocket(printQr: boolean, verbose: boolean) {
|
||||||
markOnlineOnConnect: false,
|
markOnlineOnConnect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionLogger = getChildLogger({ module: "web-session" });
|
sock.ev.on("creds.update", () => safeSaveCreds(saveCreds, sessionLogger));
|
||||||
|
|
||||||
sock.ev.on("creds.update", saveCreds);
|
|
||||||
sock.ev.on(
|
sock.ev.on(
|
||||||
"connection.update",
|
"connection.update",
|
||||||
(update: Partial<import("@whiskeysockets/baileys").ConnectionState>) => {
|
(update: Partial<import("@whiskeysockets/baileys").ConnectionState>) => {
|
||||||
|
|
@ -139,10 +197,20 @@ export function formatError(err: unknown): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function webAuthExists() {
|
export async function webAuthExists() {
|
||||||
return fs
|
try {
|
||||||
.access(WA_WEB_AUTH_DIR)
|
await fs.access(WA_WEB_AUTH_DIR);
|
||||||
.then(() => true)
|
} catch {
|
||||||
.catch(() => false);
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const stats = await fs.stat(WA_CREDS_PATH);
|
||||||
|
if (!stats.isFile() || stats.size <= 1) return false;
|
||||||
|
const raw = await fs.readFile(WA_CREDS_PATH, "utf-8");
|
||||||
|
JSON.parse(raw);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logoutWeb(runtime: RuntimeEnv = defaultRuntime) {
|
export async function logoutWeb(runtime: RuntimeEnv = defaultRuntime) {
|
||||||
|
|
@ -160,12 +228,11 @@ export async function logoutWeb(runtime: RuntimeEnv = defaultRuntime) {
|
||||||
|
|
||||||
export function readWebSelfId() {
|
export function readWebSelfId() {
|
||||||
// Read the cached WhatsApp Web identity (jid + E.164) from disk if present.
|
// Read the cached WhatsApp Web identity (jid + E.164) from disk if present.
|
||||||
const credsPath = path.join(WA_WEB_AUTH_DIR, "creds.json");
|
|
||||||
try {
|
try {
|
||||||
if (!fsSync.existsSync(credsPath)) {
|
if (!fsSync.existsSync(WA_CREDS_PATH)) {
|
||||||
return { e164: null, jid: null } as const;
|
return { e164: null, jid: null } as const;
|
||||||
}
|
}
|
||||||
const raw = fsSync.readFileSync(credsPath, "utf-8");
|
const raw = fsSync.readFileSync(WA_CREDS_PATH, "utf-8");
|
||||||
const parsed = JSON.parse(raw) as { me?: { id?: string } } | undefined;
|
const parsed = JSON.parse(raw) as { me?: { id?: string } } | undefined;
|
||||||
const jid = parsed?.me?.id ?? null;
|
const jid = parsed?.me?.id ?? null;
|
||||||
const e164 = jid ? jidToE164(jid) : null;
|
const e164 = jid ? jidToE164(jid) : null;
|
||||||
|
|
@ -180,9 +247,8 @@ export function readWebSelfId() {
|
||||||
* Helpful for heartbeats/observability to spot stale credentials.
|
* Helpful for heartbeats/observability to spot stale credentials.
|
||||||
*/
|
*/
|
||||||
export function getWebAuthAgeMs(): number | null {
|
export function getWebAuthAgeMs(): number | null {
|
||||||
const credsPath = path.join(WA_WEB_AUTH_DIR, "creds.json");
|
|
||||||
try {
|
try {
|
||||||
const stats = fsSync.statSync(credsPath);
|
const stats = fsSync.statSync(WA_CREDS_PATH);
|
||||||
return Date.now() - stats.mtimeMs;
|
return Date.now() - stats.mtimeMs;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue