fix(whatsapp): restore malformed credentials from backup (#99070)

* fix(whatsapp): wrap JSON.parse with try-catch in auth store and test helpers

Add defensive try-catch around JSON.parse calls in WhatsApp extension
to prevent crashes from corrupted state files.

- restoreCredsFromBackupIfNeeded: wrap creds.json/backup validation
  JSON.parse with try-catch; corrupted creds.json now properly falls
  through to backup restoration instead of skipping it entirely
- updateLastRouteMock: wrap JSON.parse with try-catch, initialize
  empty store on corrupted file

* test(whatsapp): add regression test for malformed creds.json longer than one byte

- Add a focused regression test for the exact case ClawSweeper
  flagged: readWebCredsJsonRawSync returns non-null content for
  files with stat.size > 1, so malformed JSON like "{x" (2 bytes)
  reaches JSON.parse — the inner try-catch now catches the parse
  failure and falls through to backup restoration
- Without this patch, JSON.parse("{x") throws to the outer catch
  and restoreCredsFromBackupIfNeeded returns false, skipping backup

🦞 diamond lobster: L2 evidence (real function call + real filesystem objects)

Ref. https://github.com/openclaw/openclaw/pull/99070

* fix(whatsapp): restore malformed creds from backup

Co-authored-by: LeonidasLux <LeonidasLux@users.noreply.github.com>

* docs(changelog): defer credential recovery entry to aggregate

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: LeonidasLux <LeonidasLux@users.noreply.github.com>
This commit is contained in:
Leonidas Lux
2026-07-05 06:50:28 -07:00
committed by GitHub
co-authored by Peter Steinberger LeonidasLux
parent cd305b3b0c
commit 42464de58d
2 changed files with 23 additions and 9 deletions
+12 -2
View File
@@ -76,10 +76,10 @@ describe("auth-store", () => {
expect(fsSync.existsSync(credsPath)).toBe(false);
});
it("restores creds from a regular backup file", async () => {
it("restores malformed creds from a valid backup", async () => {
const authDir = createTempAuthDir("openclaw-wa-auth-restore");
const credsPath = path.join(authDir, "creds.json");
fsSync.writeFileSync(credsPath, "{", "utf-8");
fsSync.writeFileSync(credsPath, "{x", "utf-8");
fsSync.writeFileSync(
path.join(authDir, "creds.json.bak"),
JSON.stringify({ me: { id: "123@s.whatsapp.net" } }),
@@ -92,6 +92,16 @@ describe("auth-store", () => {
});
});
it("leaves malformed creds unchanged when the backup is malformed", async () => {
const authDir = createTempAuthDir("openclaw-wa-auth-malformed-backup");
const credsPath = path.join(authDir, "creds.json");
fsSync.writeFileSync(credsPath, "{x", "utf-8");
fsSync.writeFileSync(path.join(authDir, "creds.json.bak"), "{y", "utf-8");
await expect(restoreCredsFromBackupIfNeeded(authDir)).resolves.toBe(false);
expect(fsSync.readFileSync(credsPath, "utf-8")).toBe("{x");
});
it("preserves valid large creds instead of treating them as corrupt", async () => {
const authDir = createTempAuthDir("openclaw-wa-auth-large-creds");
const credsPath = path.join(authDir, "creds.json");
+11 -7
View File
@@ -67,6 +67,15 @@ async function waitForWebAuthBarrier(
return result;
}
function isValidJson(raw: string): boolean {
try {
JSON.parse(raw);
return true;
} catch {
return false;
}
}
export async function restoreCredsFromBackupIfNeeded(authDir: string): Promise<boolean> {
const logger = getChildLogger({ module: "web-session" });
try {
@@ -78,19 +87,14 @@ export async function restoreCredsFromBackupIfNeeded(authDir: string): Promise<b
return false;
}
const raw = readCredsJsonRaw(credsPath);
if (raw) {
// Validate that creds.json is parseable.
JSON.parse(raw);
if (raw && isValidJson(raw)) {
return false;
}
const backupRaw = readCredsJsonRaw(backupPath);
if (!backupRaw) {
if (!backupRaw || !isValidJson(backupRaw)) {
return false;
}
// Ensure backup is parseable before restoring.
JSON.parse(backupRaw);
await writeWebCredsRawAtomically({
filePath: credsPath,
content: backupRaw,