From 6320f739d4e70533537832b06d0a4236c215e736 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 15 Jan 2026 04:00:50 +0000 Subject: [PATCH] refactor: centralize whatsapp auth detection --- src/commands/doctor-legacy-config.ts | 49 +------------------- src/web/accounts.ts | 28 ++++++++++++ src/web/accounts.whatsapp-auth.test.ts | 63 ++++++++++++++++++++++++++ src/web/auth-store.ts | 9 ++++ 4 files changed, 102 insertions(+), 47 deletions(-) create mode 100644 src/web/accounts.whatsapp-auth.test.ts diff --git a/src/commands/doctor-legacy-config.ts b/src/commands/doctor-legacy-config.ts index 6e36550e3..ee3fb7f52 100644 --- a/src/commands/doctor-legacy-config.ts +++ b/src/commands/doctor-legacy-config.ts @@ -1,4 +1,3 @@ -import fs from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -10,12 +9,10 @@ import { readConfigFileSnapshot, writeConfigFile, } from "../config/config.js"; -import { resolveOAuthDir } from "../config/paths.js"; -import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; import { note } from "../terminal/note.js"; import { resolveUserPath } from "../utils.js"; -import { resolveWebCredsPath } from "../web/auth-store.js"; +import { hasAnyWhatsAppAuth } from "../web/accounts.js"; function resolveLegacyConfigPath(env: NodeJS.ProcessEnv): string { const override = env.CLAWDIS_CONFIG_PATH?.trim(); @@ -57,48 +54,6 @@ export function replaceModernName(value: string | undefined): string | undefined return value.replace(/clawdbot/g, "clawdis"); } -function hasWebCreds(authDir: string): boolean { - try { - const credsPath = resolveWebCredsPath(authDir); - const stats = fs.statSync(credsPath); - return stats.isFile() && stats.size > 1; - } catch { - return false; - } -} - -function listWhatsAppAuthDirs(cfg: ClawdbotConfig): string[] { - const oauthDir = resolveOAuthDir(); - const whatsappDir = path.join(oauthDir, "whatsapp"); - const authDirs = new Set([oauthDir, path.join(whatsappDir, DEFAULT_ACCOUNT_ID)]); - - const accounts = cfg.channels?.whatsapp?.accounts; - if (accounts && typeof accounts === "object") { - for (const [accountId, accountCfg] of Object.entries(accounts)) { - const configured = accountCfg?.authDir?.trim(); - const authDir = configured ? resolveUserPath(configured) : path.join(whatsappDir, accountId); - authDirs.add(authDir); - } - } - - try { - const entries = fs.readdirSync(whatsappDir, { withFileTypes: true }); - for (const entry of entries) { - if (!entry.isDirectory()) continue; - authDirs.add(path.join(whatsappDir, entry.name)); - } - } catch { - // ignore missing dirs - } - - return Array.from(authDirs); -} - -function hasWhatsAppAuthState(cfg: ClawdbotConfig): boolean { - const authDirs = listWhatsAppAuthDirs(cfg); - return authDirs.some((authDir) => hasWebCreds(authDir)); -} - export function normalizeLegacyConfigValues(cfg: ClawdbotConfig): { config: ClawdbotConfig; changes: string[]; @@ -266,7 +221,7 @@ export function normalizeLegacyConfigValues(cfg: ClawdbotConfig): { const legacyAckReaction = cfg.messages?.ackReaction?.trim(); const hasWhatsAppConfig = cfg.channels?.whatsapp !== undefined; - const hasWhatsAppAuth = hasWhatsAppAuthState(cfg); + const hasWhatsAppAuth = hasAnyWhatsAppAuth(cfg); if (legacyAckReaction && (hasWhatsAppConfig || hasWhatsAppAuth)) { const hasWhatsAppAck = cfg.channels?.whatsapp?.ackReaction !== undefined; if (!hasWhatsAppAck) { diff --git a/src/web/accounts.ts b/src/web/accounts.ts index 682c35d74..d1c4e1722 100644 --- a/src/web/accounts.ts +++ b/src/web/accounts.ts @@ -6,6 +6,7 @@ import { resolveOAuthDir } from "../config/paths.js"; import type { DmPolicy, GroupPolicy, WhatsAppAccountConfig } from "../config/types.js"; import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; import { resolveUserPath } from "../utils.js"; +import { hasWebCredsSync } from "./auth-store.js"; export type ResolvedWhatsAppAccount = { accountId: string; @@ -32,6 +33,33 @@ function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] { return Object.keys(accounts).filter(Boolean); } +export function listWhatsAppAuthDirs(cfg: ClawdbotConfig): string[] { + const oauthDir = resolveOAuthDir(); + const whatsappDir = path.join(oauthDir, "whatsapp"); + const authDirs = new Set([oauthDir, path.join(whatsappDir, DEFAULT_ACCOUNT_ID)]); + + const accountIds = listConfiguredAccountIds(cfg); + for (const accountId of accountIds) { + authDirs.add(resolveWhatsAppAuthDir({ cfg, accountId }).authDir); + } + + try { + const entries = fs.readdirSync(whatsappDir, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isDirectory()) continue; + authDirs.add(path.join(whatsappDir, entry.name)); + } + } catch { + // ignore missing dirs + } + + return Array.from(authDirs); +} + +export function hasAnyWhatsAppAuth(cfg: ClawdbotConfig): boolean { + return listWhatsAppAuthDirs(cfg).some((authDir) => hasWebCredsSync(authDir)); +} + export function listWhatsAppAccountIds(cfg: ClawdbotConfig): string[] { const ids = listConfiguredAccountIds(cfg); if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; diff --git a/src/web/accounts.whatsapp-auth.test.ts b/src/web/accounts.whatsapp-auth.test.ts new file mode 100644 index 000000000..9009025ef --- /dev/null +++ b/src/web/accounts.whatsapp-auth.test.ts @@ -0,0 +1,63 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { hasAnyWhatsAppAuth, listWhatsAppAuthDirs } from "./accounts.js"; + +describe("hasAnyWhatsAppAuth", () => { + let previousOauthDir: string | undefined; + let tempOauthDir: string | undefined; + + const writeCreds = (dir: string) => { + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, "creds.json"), JSON.stringify({ me: {} })); + }; + + beforeEach(() => { + previousOauthDir = process.env.CLAWDBOT_OAUTH_DIR; + tempOauthDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-oauth-")); + process.env.CLAWDBOT_OAUTH_DIR = tempOauthDir; + }); + + afterEach(() => { + if (previousOauthDir === undefined) { + delete process.env.CLAWDBOT_OAUTH_DIR; + } else { + process.env.CLAWDBOT_OAUTH_DIR = previousOauthDir; + } + if (tempOauthDir) { + fs.rmSync(tempOauthDir, { recursive: true, force: true }); + tempOauthDir = undefined; + } + }); + + it("returns false when no auth exists", () => { + expect(hasAnyWhatsAppAuth({})).toBe(false); + }); + + it("returns true when legacy auth exists", () => { + fs.writeFileSync(path.join(tempOauthDir ?? "", "creds.json"), JSON.stringify({ me: {} })); + expect(hasAnyWhatsAppAuth({})).toBe(true); + }); + + it("returns true when non-default auth exists", () => { + writeCreds(path.join(tempOauthDir ?? "", "whatsapp", "work")); + expect(hasAnyWhatsAppAuth({})).toBe(true); + }); + + it("includes authDir overrides", () => { + const customDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-wa-auth-")); + try { + writeCreds(customDir); + const cfg = { + channels: { whatsapp: { accounts: { work: { authDir: customDir } } } }, + }; + + expect(listWhatsAppAuthDirs(cfg)).toContain(customDir); + expect(hasAnyWhatsAppAuth(cfg)).toBe(true); + } finally { + fs.rmSync(customDir, { recursive: true, force: true }); + } + }); +}); diff --git a/src/web/auth-store.ts b/src/web/auth-store.ts index 1ab5285f8..9271f2e76 100644 --- a/src/web/auth-store.ts +++ b/src/web/auth-store.ts @@ -24,6 +24,15 @@ export function resolveWebCredsBackupPath(authDir: string): string { return path.join(authDir, "creds.json.bak"); } +export function hasWebCredsSync(authDir: string): boolean { + try { + const stats = fsSync.statSync(resolveWebCredsPath(authDir)); + return stats.isFile() && stats.size > 1; + } catch { + return false; + } +} + function readCredsJsonRaw(filePath: string): string | null { try { if (!fsSync.existsSync(filePath)) return null;