mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-21 18:08:05 -06:00
fix(whatsapp): clear and migrate every Baileys credential class (#118610)
* fix(whatsapp): clear and migrate every auth credential class * test(whatsapp): await nullable legacy migration detector
This commit is contained in:
committed by
GitHub
parent
0dbf9b6fae
commit
fe6fa891ea
@@ -1,4 +1,7 @@
|
||||
// Whatsapp tests cover setup entry plugin behavior.
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import * as legacySessionSurfaceApi from "./legacy-session-surface-api.js";
|
||||
import * as legacyStateMigrationsApi from "./legacy-state-migrations-api.js";
|
||||
@@ -68,6 +71,57 @@ describe("whatsapp setup entry", () => {
|
||||
expect(legacySessionSurface.isLegacyGroupSessionKey).toBeTypeOf("function");
|
||||
});
|
||||
|
||||
it("plans migration for every Baileys auth category while preserving other shared-root files", async () => {
|
||||
const oauthDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-wa-legacy-migration-"));
|
||||
const authFiles = [
|
||||
"creds.json",
|
||||
"creds.json.bak",
|
||||
"pre-key-1.json",
|
||||
"session-contact.json",
|
||||
"sender-key-group.json",
|
||||
"sender-key-memory-group.json",
|
||||
"app-state-sync-key-contact.json",
|
||||
"app-state-sync-version-contact.json",
|
||||
"lid-mapping-15551234567.json",
|
||||
"device-list-15551234567.json",
|
||||
"tctoken-15551234567.json",
|
||||
"identity-key-15551234567.json",
|
||||
];
|
||||
|
||||
try {
|
||||
for (const file of [...authFiles, "oauth.json", "google-oauth.json", "notes.txt"]) {
|
||||
fs.writeFileSync(path.join(oauthDir, file), "{}", "utf-8");
|
||||
}
|
||||
fs.mkdirSync(path.join(oauthDir, "nested"));
|
||||
fs.writeFileSync(path.join(oauthDir, "nested", "session-keep.json"), "{}", "utf-8");
|
||||
fs.symlinkSync(path.join(oauthDir, "notes.txt"), path.join(oauthDir, "session-linked.json"));
|
||||
|
||||
const detectLegacyStateMigrations =
|
||||
setupEntry.loadLegacyStateMigrationDetector?.(setupEntryLoadOptions);
|
||||
if (!detectLegacyStateMigrations) {
|
||||
throw new Error("expected WhatsApp legacy state migration detector");
|
||||
}
|
||||
const migrations =
|
||||
(await detectLegacyStateMigrations({
|
||||
cfg: {},
|
||||
env: {},
|
||||
oauthDir,
|
||||
stateDir: oauthDir,
|
||||
})) ?? [];
|
||||
|
||||
expect(migrations.map((migration) => path.basename(migration.sourcePath)).toSorted()).toEqual(
|
||||
authFiles.toSorted(),
|
||||
);
|
||||
for (const migration of migrations) {
|
||||
expect(migration.targetPath).toBe(
|
||||
path.join(oauthDir, "whatsapp", "default", path.basename(migration.sourcePath)),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(oauthDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("loads the delegated setup wizard without importing runtime dependencies", async () => {
|
||||
const { whatsappSetupWizard } = await import("./src/setup-surface.js");
|
||||
|
||||
|
||||
@@ -351,6 +351,56 @@ describe("auth-store", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("clears every Baileys auth category from the shared legacy root without touching other files", async () => {
|
||||
const authDir = createTempAuthDir("openclaw-wa-auth-legacy-categories");
|
||||
const previousOAuthDir = hoisted.oauthDir;
|
||||
const authFiles = [
|
||||
"creds.json",
|
||||
"creds.json.bak",
|
||||
"pre-key-1.json",
|
||||
"session-contact.json",
|
||||
"sender-key-group.json",
|
||||
"sender-key-memory-group.json",
|
||||
"app-state-sync-key-contact.json",
|
||||
"app-state-sync-version-contact.json",
|
||||
"lid-mapping-15551234567.json",
|
||||
"device-list-15551234567.json",
|
||||
"tctoken-15551234567.json",
|
||||
"identity-key-15551234567.json",
|
||||
];
|
||||
const unrelatedFiles = ["oauth.json", "google-oauth.json", "notes.txt"];
|
||||
const nestedAuthFile = path.join(authDir, "nested", "session-keep.json");
|
||||
hoisted.oauthDir = authDir;
|
||||
|
||||
try {
|
||||
for (const file of [...authFiles, ...unrelatedFiles]) {
|
||||
fsSync.writeFileSync(path.join(authDir, file), "{}", "utf-8");
|
||||
}
|
||||
fsSync.mkdirSync(path.dirname(nestedAuthFile));
|
||||
fsSync.writeFileSync(nestedAuthFile, "keep", "utf-8");
|
||||
fsSync.symlinkSync(
|
||||
path.join(authDir, "notes.txt"),
|
||||
path.join(authDir, "session-linked.json"),
|
||||
);
|
||||
|
||||
await expect(logoutWeb({ authDir, isLegacyAuthDir: true })).resolves.toBe(true);
|
||||
|
||||
for (const file of authFiles) {
|
||||
expect(fsSync.existsSync(path.join(authDir, file)), file).toBe(false);
|
||||
}
|
||||
for (const file of unrelatedFiles) {
|
||||
expect(fsSync.existsSync(path.join(authDir, file)), file).toBe(true);
|
||||
}
|
||||
expect(fsSync.readFileSync(nestedAuthFile, "utf-8")).toBe("keep");
|
||||
expect(fsSync.lstatSync(path.join(authDir, "session-linked.json")).isSymbolicLink()).toBe(
|
||||
true,
|
||||
);
|
||||
} finally {
|
||||
hoisted.oauthDir = previousOAuthDir;
|
||||
fsSync.rmSync(authDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("clears auth state even when directory enumeration fails", async () => {
|
||||
await withOwnedOAuthAuthDir("openclaw-wa-auth-readdir", async (authDir) => {
|
||||
fsSync.writeFileSync(path.join(authDir, "creds.json"), "{}", "utf-8");
|
||||
|
||||
@@ -10,6 +10,7 @@ import { resolveOAuthDir } from "./auth-store.runtime.js";
|
||||
import {
|
||||
assertWebCredsPathRegularFileOrMissing,
|
||||
hasWebCredsSync,
|
||||
isWhatsAppBaileysAuthFileName,
|
||||
readWebCredsJsonRaw,
|
||||
readWebCredsJsonRawSync,
|
||||
resolveWebCredsBackupPath,
|
||||
@@ -225,19 +226,6 @@ export async function readWebAuthSnapshotBestEffort(authDir: string = resolveDef
|
||||
} as const;
|
||||
}
|
||||
|
||||
function isBaileysAuthFileName(name: string): boolean {
|
||||
if (name === "oauth.json") {
|
||||
return false;
|
||||
}
|
||||
if (name === "creds.json" || name === "creds.json.bak") {
|
||||
return true;
|
||||
}
|
||||
if (!name.endsWith(".json")) {
|
||||
return false;
|
||||
}
|
||||
return /^(app-state-sync|session|sender-key|pre-key)-/.test(name);
|
||||
}
|
||||
|
||||
async function clearBaileysAuthFiles(
|
||||
authDir: string,
|
||||
beforeCredentialPersistence?: () => Promise<void>,
|
||||
@@ -248,7 +236,7 @@ async function clearBaileysAuthFiles(
|
||||
}
|
||||
const entries = await fs.readdir(authDir, { withFileTypes: true });
|
||||
const credentialFiles = entries.filter(
|
||||
(entry) => entry.isFile() && isBaileysAuthFileName(entry.name),
|
||||
(entry) => entry.isFile() && isWhatsAppBaileysAuthFileName(entry.name),
|
||||
);
|
||||
if (credentialFiles.length === 0) {
|
||||
return;
|
||||
@@ -273,7 +261,7 @@ async function shouldClearOnLogout(authDir: string, isLegacyAuthDir: boolean): P
|
||||
if (!entry.isFile()) {
|
||||
return false;
|
||||
}
|
||||
return isBaileysAuthFileName(entry.name);
|
||||
return isWhatsAppBaileysAuthFileName(entry.name);
|
||||
});
|
||||
}
|
||||
const credsStats = await fs.lstat(resolveWebCredsPath(authDir)).catch(() => null);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Whatsapp plugin module implements creds files behavior.
|
||||
import path from "node:path";
|
||||
import type { SignalDataTypeMap } from "baileys";
|
||||
import {
|
||||
assertNoSymlinkParents,
|
||||
assertNoSymlinkParentsSync,
|
||||
@@ -9,6 +10,31 @@ import {
|
||||
statRegularFileSync,
|
||||
} from "openclaw/plugin-sdk/security-runtime";
|
||||
|
||||
// The legacy OAuth root is shared; keep its exact WhatsApp namespaces aligned
|
||||
// with Baileys without importing the provider into setup discovery.
|
||||
const BAILEYS_SIGNAL_AUTH_CATEGORIES = {
|
||||
"app-state-sync-key": true,
|
||||
"app-state-sync-version": true,
|
||||
"device-list": true,
|
||||
"identity-key": true,
|
||||
"lid-mapping": true,
|
||||
"pre-key": true,
|
||||
"sender-key": true,
|
||||
"sender-key-memory": true,
|
||||
session: true,
|
||||
tctoken: true,
|
||||
} satisfies Record<keyof SignalDataTypeMap, true>;
|
||||
|
||||
export function isWhatsAppBaileysAuthFileName(name: string): boolean {
|
||||
if (name === "creds.json" || name === "creds.json.bak") {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
name.endsWith(".json") &&
|
||||
Object.keys(BAILEYS_SIGNAL_AUTH_CATEGORIES).some((category) => name.startsWith(`${category}-`))
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveWebCredsPath(authDir: string): string {
|
||||
return path.join(authDir, "creds.json");
|
||||
}
|
||||
|
||||
@@ -4,16 +4,7 @@ import path from "node:path";
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
import type { ChannelLegacyStateMigrationPlan } from "openclaw/plugin-sdk/channel-contract";
|
||||
import { fileExists } from "openclaw/plugin-sdk/security-runtime";
|
||||
|
||||
function isLegacyWhatsAppAuthFile(name: string): boolean {
|
||||
if (name === "creds.json" || name === "creds.json.bak") {
|
||||
return true;
|
||||
}
|
||||
if (!name.endsWith(".json")) {
|
||||
return false;
|
||||
}
|
||||
return /^(app-state-sync|session|sender-key|pre-key)-/.test(name);
|
||||
}
|
||||
import { isWhatsAppBaileysAuthFileName } from "./creds-files.js";
|
||||
|
||||
export function detectWhatsAppLegacyStateMigrations(params: {
|
||||
oauthDir: string;
|
||||
@@ -28,7 +19,7 @@ export function detectWhatsAppLegacyStateMigrations(params: {
|
||||
})();
|
||||
|
||||
return entries.flatMap((entry) => {
|
||||
if (!entry.isFile() || entry.name === "oauth.json" || !isLegacyWhatsAppAuthFile(entry.name)) {
|
||||
if (!entry.isFile() || !isWhatsAppBaileysAuthFileName(entry.name)) {
|
||||
return [];
|
||||
}
|
||||
const sourcePath = path.join(params.oauthDir, entry.name);
|
||||
|
||||
Reference in New Issue
Block a user