fix(security): harden account-key handling against prototype pollution

This commit is contained in:
Peter Steinberger
2026-02-24 01:09:23 +00:00
parent 12cc754332
commit f97c0922e1
24 changed files with 141 additions and 111 deletions

View File

@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Security/Config writes: block reserved prototype keys in account-id normalization and route account config resolution through own-key lookups, hardening `/allowlist` and account-scoped config paths against prototype-chain pollution.
- Security/Exec: harden `safeBins` long-option validation by rejecting unknown/ambiguous GNU long-option abbreviations and denying sort filesystem-dependent flags (`--random-source`, `--temporary-directory`, `-T`), closing safe-bin denylist bypasses. Thanks @jiseoung.
## 2026.2.23 (Unreleased)

View File

@@ -5,6 +5,7 @@
import type { ChannelId } from "../channels/plugins/types.js";
import type { OpenClawConfig } from "../config/config.js";
import { findFenceSpanAt, isSafeFenceBreak, parseFenceSpans } from "../markdown/fences.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { normalizeAccountId } from "../routing/session-key.js";
import { chunkTextByBreakResolver } from "../shared/text-chunking.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
@@ -39,17 +40,10 @@ function resolveChunkLimitForProvider(
const normalizedAccountId = normalizeAccountId(accountId);
const accounts = cfgSection.accounts;
if (accounts && typeof accounts === "object") {
const direct = accounts[normalizedAccountId];
const direct = resolveAccountEntry(accounts, normalizedAccountId);
if (typeof direct?.textChunkLimit === "number") {
return direct.textChunkLimit;
}
const matchKey = Object.keys(accounts).find(
(key) => key.toLowerCase() === normalizedAccountId.toLowerCase(),
);
const match = matchKey ? accounts[matchKey] : undefined;
if (typeof match?.textChunkLimit === "number") {
return match.textChunkLimit;
}
}
return cfgSection.textChunkLimit;
}
@@ -89,17 +83,10 @@ function resolveChunkModeForProvider(
const normalizedAccountId = normalizeAccountId(accountId);
const accounts = cfgSection.accounts;
if (accounts && typeof accounts === "object") {
const direct = accounts[normalizedAccountId];
const direct = resolveAccountEntry(accounts, normalizedAccountId);
if (direct?.chunkMode) {
return direct.chunkMode;
}
const matchKey = Object.keys(accounts).find(
(key) => key.toLowerCase() === normalizedAccountId.toLowerCase(),
);
const match = matchKey ? accounts[matchKey] : undefined;
if (match?.chunkMode) {
return match.chunkMode;
}
}
return cfgSection.chunkMode;
}

View File

@@ -2,6 +2,7 @@ import { getChannelDock } from "../../channels/dock.js";
import { normalizeChannelId } from "../../channels/plugins/index.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { BlockStreamingCoalesceConfig } from "../../config/types.js";
import { resolveAccountEntry } from "../../routing/account-lookup.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import {
INTERNAL_MESSAGE_CHANNEL,
@@ -45,7 +46,7 @@ function resolveProviderBlockStreamingCoalesce(params: {
}
const normalizedAccountId = normalizeAccountId(accountId);
const typed = providerCfg as ProviderBlockStreamingConfig;
const accountCfg = typed.accounts?.[normalizedAccountId];
const accountCfg = resolveAccountEntry(typed.accounts, normalizedAccountId);
return accountCfg?.blockStreamingCoalesce ?? typed.blockStreamingCoalesce;
}

View File

@@ -12,12 +12,17 @@ import {
import { resolveDiscordAccount } from "../../discord/accounts.js";
import { resolveDiscordUserAllowlist } from "../../discord/resolve-users.js";
import { resolveIMessageAccount } from "../../imessage/accounts.js";
import { isBlockedObjectKey } from "../../infra/prototype-keys.js";
import {
addChannelAllowFromStoreEntry,
readChannelAllowFromStore,
removeChannelAllowFromStoreEntry,
} from "../../pairing/pairing-store.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
normalizeOptionalAccountId,
} from "../../routing/session-key.js";
import { resolveSignalAccount } from "../../signal/accounts.js";
import { resolveSlackAccount } from "../../slack/accounts.js";
import { resolveSlackUserAllowlist } from "../../slack/resolve-users.js";
@@ -199,13 +204,22 @@ function resolveAccountTarget(
const channels = (parsed.channels ??= {}) as Record<string, unknown>;
const channel = (channels[channelId] ??= {}) as Record<string, unknown>;
const normalizedAccountId = normalizeAccountId(accountId);
if (isBlockedObjectKey(normalizedAccountId)) {
return { target: channel, pathPrefix: `channels.${channelId}`, accountId: DEFAULT_ACCOUNT_ID };
}
const hasAccounts = Boolean(channel.accounts && typeof channel.accounts === "object");
const useAccount = normalizedAccountId !== DEFAULT_ACCOUNT_ID || hasAccounts;
if (!useAccount) {
return { target: channel, pathPrefix: `channels.${channelId}`, accountId: normalizedAccountId };
}
const accounts = (channel.accounts ??= {}) as Record<string, unknown>;
const account = (accounts[normalizedAccountId] ??= {}) as Record<string, unknown>;
const existingAccount = Object.hasOwn(accounts, normalizedAccountId)
? accounts[normalizedAccountId]
: undefined;
if (!existingAccount || typeof existingAccount !== "object") {
accounts[normalizedAccountId] = {};
}
const account = accounts[normalizedAccountId] as Record<string, unknown>;
return {
target: account,
pathPrefix: `channels.${channelId}.accounts.${normalizedAccountId}`,
@@ -361,6 +375,14 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo
reply: { text: "⚠️ Unknown channel. Add channel=<id> to the command." },
};
}
if (parsed.account?.trim() && !normalizeOptionalAccountId(parsed.account)) {
return {
shouldContinue: false,
reply: {
text: "⚠️ Invalid account id. Reserved keys (__proto__, constructor, prototype) are blocked.",
},
};
}
const accountId = normalizeAccountId(parsed.account ?? params.ctx.AccountId);
const scope = parsed.scope;

View File

@@ -645,6 +645,22 @@ describe("handleCommands /allowlist", () => {
expect(result.reply?.text).toContain("DM allowlist added");
});
it("rejects blocked account ids and keeps Object.prototype clean", async () => {
delete (Object.prototype as Record<string, unknown>).allowFrom;
const cfg = {
commands: { text: true, config: true },
channels: { telegram: { allowFrom: ["123"] } },
} as OpenClawConfig;
const params = buildPolicyParams("/allowlist add dm --account __proto__ 789", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("Invalid account id");
expect((Object.prototype as Record<string, unknown>).allowFrom).toBeUndefined();
expect(writeConfigFileMock).not.toHaveBeenCalled();
});
it("removes DM allowlist entries from canonical allowFrom and deletes legacy dm.allowFrom", async () => {
const cases = [
{

View File

@@ -1,4 +1,5 @@
import type { OpenClawConfig } from "../../config/config.js";
import { resolveAccountEntry } from "../../routing/account-lookup.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import type { ChannelId } from "./types.js";
@@ -8,16 +9,7 @@ type ChannelConfigWithAccounts = {
};
function resolveAccountConfig(accounts: ChannelConfigWithAccounts["accounts"], accountId: string) {
if (!accounts || typeof accounts !== "object") {
return undefined;
}
if (accountId in accounts) {
return accounts[accountId];
}
const matchKey = Object.keys(accounts).find(
(key) => key.toLowerCase() === accountId.toLowerCase(),
);
return matchKey ? accounts[matchKey] : undefined;
return resolveAccountEntry(accounts, accountId);
}
export function resolveChannelConfigWrites(params: {

View File

@@ -1,4 +1,5 @@
import { normalizeChannelId } from "../channels/plugins/index.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { normalizeAccountId } from "../routing/session-key.js";
import type { OpenClawConfig } from "./config.js";
import type { TelegramCapabilitiesConfig } from "./types.telegram.js";
@@ -32,14 +33,7 @@ function resolveAccountCapabilities(params: {
const accounts = cfg.accounts;
if (accounts && typeof accounts === "object") {
const direct = accounts[normalizedAccountId];
if (direct) {
return normalizeCapabilities(direct.capabilities) ?? normalizeCapabilities(cfg.capabilities);
}
const matchKey = Object.keys(accounts).find(
(key) => key.toLowerCase() === normalizedAccountId.toLowerCase(),
);
const match = matchKey ? accounts[matchKey] : undefined;
const match = resolveAccountEntry(accounts, normalizedAccountId);
if (match) {
return normalizeCapabilities(match.capabilities) ?? normalizeCapabilities(cfg.capabilities);
}

View File

@@ -1,4 +1,5 @@
import type { ChannelId } from "../channels/plugins/types.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { normalizeAccountId } from "../routing/session-key.js";
import type { OpenClawConfig } from "./config.js";
import {
@@ -293,13 +294,7 @@ function resolveChannelGroups(
if (!channelConfig) {
return undefined;
}
const accountGroups =
channelConfig.accounts?.[normalizedAccountId]?.groups ??
channelConfig.accounts?.[
Object.keys(channelConfig.accounts ?? {}).find(
(key) => key.toLowerCase() === normalizedAccountId.toLowerCase(),
) ?? ""
]?.groups;
const accountGroups = resolveAccountEntry(channelConfig.accounts, normalizedAccountId)?.groups;
return accountGroups ?? channelConfig.groups;
}
@@ -320,13 +315,10 @@ function resolveChannelGroupPolicyMode(
if (!channelConfig) {
return undefined;
}
const accountPolicy =
channelConfig.accounts?.[normalizedAccountId]?.groupPolicy ??
channelConfig.accounts?.[
Object.keys(channelConfig.accounts ?? {}).find(
(key) => key.toLowerCase() === normalizedAccountId.toLowerCase(),
) ?? ""
]?.groupPolicy;
const accountPolicy = resolveAccountEntry(
channelConfig.accounts,
normalizedAccountId,
)?.groupPolicy;
return accountPolicy ?? channelConfig.groupPolicy;
}

View File

@@ -1,4 +1,5 @@
import { normalizeChannelId } from "../channels/plugins/index.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { normalizeAccountId } from "../routing/session-key.js";
import type { OpenClawConfig } from "./config.js";
import type { MarkdownTableMode } from "./types.base.js";
@@ -31,15 +32,7 @@ function resolveMarkdownModeFromSection(
const normalizedAccountId = normalizeAccountId(accountId);
const accounts = section.accounts;
if (accounts && typeof accounts === "object") {
const direct = accounts[normalizedAccountId];
const directMode = direct?.markdown?.tables;
if (isMarkdownTableMode(directMode)) {
return directMode;
}
const matchKey = Object.keys(accounts).find(
(key) => key.toLowerCase() === normalizedAccountId.toLowerCase(),
);
const match = matchKey ? accounts[matchKey] : undefined;
const match = resolveAccountEntry(accounts, normalizedAccountId);
const matchMode = match?.markdown?.tables;
if (isMarkdownTableMode(matchMode)) {
return matchMode;

View File

@@ -1,5 +1 @@
const BLOCKED_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
export function isBlockedObjectKey(key: string): boolean {
return BLOCKED_OBJECT_KEYS.has(key);
}
export { isBlockedObjectKey } from "../infra/prototype-keys.js";

View File

@@ -2,6 +2,7 @@ import { createAccountActionGate } from "../channels/plugins/account-action-gate
import { createAccountListHelpers } from "../channels/plugins/account-helpers.js";
import type { OpenClawConfig } from "../config/config.js";
import type { DiscordAccountConfig, DiscordActionConfig } from "../config/types.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { normalizeAccountId } from "../routing/session-key.js";
import { resolveDiscordToken } from "./token.js";
@@ -22,11 +23,7 @@ function resolveAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): DiscordAccountConfig | undefined {
const accounts = cfg.channels?.discord?.accounts;
if (!accounts || typeof accounts !== "object") {
return undefined;
}
return accounts[accountId] as DiscordAccountConfig | undefined;
return resolveAccountEntry(cfg.channels?.discord?.accounts, accountId);
}
function mergeDiscordAccountConfig(cfg: OpenClawConfig, accountId: string): DiscordAccountConfig {

View File

@@ -1,6 +1,7 @@
import { resolveTextChunkLimit } from "../auto-reply/chunk.js";
import { getChannelDock } from "../channels/dock.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { normalizeAccountId } from "../routing/session-key.js";
const DEFAULT_DISCORD_DRAFT_STREAM_MIN = 200;
@@ -19,9 +20,8 @@ export function resolveDiscordDraftStreamingChunking(
fallbackLimit: providerChunkLimit,
});
const normalizedAccountId = normalizeAccountId(accountId);
const draftCfg =
cfg?.channels?.discord?.accounts?.[normalizedAccountId]?.draftChunk ??
cfg?.channels?.discord?.draftChunk;
const accountCfg = resolveAccountEntry(cfg?.channels?.discord?.accounts, normalizedAccountId);
const draftCfg = accountCfg?.draftChunk ?? cfg?.channels?.discord?.draftChunk;
const maxRequested = Math.max(
1,

View File

@@ -1,6 +1,7 @@
import { createAccountListHelpers } from "../channels/plugins/account-helpers.js";
import type { OpenClawConfig } from "../config/config.js";
import type { IMessageAccountConfig } from "../config/types.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { normalizeAccountId } from "../routing/session-key.js";
export type ResolvedIMessageAccount = {
@@ -19,11 +20,7 @@ function resolveAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): IMessageAccountConfig | undefined {
const accounts = cfg.channels?.imessage?.accounts;
if (!accounts || typeof accounts !== "object") {
return undefined;
}
return accounts[accountId] as IMessageAccountConfig | undefined;
return resolveAccountEntry(cfg.channels?.imessage?.accounts, accountId);
}
function mergeIMessageAccountConfig(cfg: OpenClawConfig, accountId: string): IMessageAccountConfig {

View File

@@ -0,0 +1,5 @@
const BLOCKED_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
export function isBlockedObjectKey(key: string): boolean {
return BLOCKED_OBJECT_KEYS.has(key);
}

View File

@@ -4,6 +4,7 @@ import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId as normalizeSharedAccountId,
} from "../routing/account-id.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import type {
LineConfig,
LineAccountConfig,
@@ -104,10 +105,12 @@ export function resolveLineAccount(params: {
cfg: OpenClawConfig;
accountId?: string;
}): ResolvedLineAccount {
const { cfg, accountId = DEFAULT_ACCOUNT_ID } = params;
const cfg = params.cfg;
const accountId = normalizeSharedAccountId(params.accountId);
const lineConfig = cfg.channels?.line as LineConfig | undefined;
const accounts = lineConfig?.accounts;
const accountConfig = accountId !== DEFAULT_ACCOUNT_ID ? accounts?.[accountId] : undefined;
const accountConfig =
accountId !== DEFAULT_ACCOUNT_ID ? resolveAccountEntry(accounts, accountId) : undefined;
const { token, tokenSource } = resolveToken({
accountId,

View File

@@ -20,6 +20,15 @@ describe("account id normalization", () => {
expect(normalizeAccountId(" Prod/US East ")).toBe("prod-us-east");
});
it("rejects prototype-pollution key vectors", () => {
expect(normalizeAccountId("__proto__")).toBe(DEFAULT_ACCOUNT_ID);
expect(normalizeAccountId("constructor")).toBe(DEFAULT_ACCOUNT_ID);
expect(normalizeAccountId("prototype")).toBe(DEFAULT_ACCOUNT_ID);
expect(normalizeOptionalAccountId("__proto__")).toBeUndefined();
expect(normalizeOptionalAccountId("constructor")).toBeUndefined();
expect(normalizeOptionalAccountId("prototype")).toBeUndefined();
});
it("preserves optional semantics without forcing default", () => {
expect(normalizeOptionalAccountId(undefined)).toBeUndefined();
expect(normalizeOptionalAccountId(" ")).toBeUndefined();

View File

@@ -1,3 +1,5 @@
import { isBlockedObjectKey } from "../infra/prototype-keys.js";
export const DEFAULT_ACCOUNT_ID = "default";
const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
@@ -17,12 +19,20 @@ function canonicalizeAccountId(value: string): string {
.slice(0, 64);
}
function normalizeCanonicalAccountId(value: string): string | undefined {
const canonical = canonicalizeAccountId(value);
if (!canonical || isBlockedObjectKey(canonical)) {
return undefined;
}
return canonical;
}
export function normalizeAccountId(value: string | undefined | null): string {
const trimmed = (value ?? "").trim();
if (!trimmed) {
return DEFAULT_ACCOUNT_ID;
}
return canonicalizeAccountId(trimmed) || DEFAULT_ACCOUNT_ID;
return normalizeCanonicalAccountId(trimmed) || DEFAULT_ACCOUNT_ID;
}
export function normalizeOptionalAccountId(value: string | undefined | null): string | undefined {
@@ -30,5 +40,5 @@ export function normalizeOptionalAccountId(value: string | undefined | null): st
if (!trimmed) {
return undefined;
}
return canonicalizeAccountId(trimmed) || undefined;
return normalizeCanonicalAccountId(trimmed) || undefined;
}

View File

@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { resolveAccountEntry } from "./account-lookup.js";
describe("resolveAccountEntry", () => {
it("resolves direct and case-insensitive account keys", () => {
const accounts = {
default: { id: "default" },
Business: { id: "business" },
};
expect(resolveAccountEntry(accounts, "default")).toEqual({ id: "default" });
expect(resolveAccountEntry(accounts, "business")).toEqual({ id: "business" });
});
it("ignores prototype-chain values", () => {
const inherited = { default: { id: "polluted" } };
const accounts = Object.create(inherited) as Record<string, { id: string }>;
expect(resolveAccountEntry(accounts, "default")).toBeUndefined();
});
});

View File

@@ -0,0 +1,14 @@
export function resolveAccountEntry<T>(
accounts: Record<string, T> | undefined,
accountId: string,
): T | undefined {
if (!accounts || typeof accounts !== "object") {
return undefined;
}
if (Object.hasOwn(accounts, accountId)) {
return accounts[accountId];
}
const normalized = accountId.toLowerCase();
const matchKey = Object.keys(accounts).find((key) => key.toLowerCase() === normalized);
return matchKey ? accounts[matchKey] : undefined;
}

View File

@@ -1,6 +1,7 @@
import { createAccountListHelpers } from "../channels/plugins/account-helpers.js";
import type { OpenClawConfig } from "../config/config.js";
import type { SignalAccountConfig } from "../config/types.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { normalizeAccountId } from "../routing/session-key.js";
export type ResolvedSignalAccount = {
@@ -20,11 +21,7 @@ function resolveAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): SignalAccountConfig | undefined {
const accounts = cfg.channels?.signal?.accounts;
if (!accounts || typeof accounts !== "object") {
return undefined;
}
return accounts[accountId] as SignalAccountConfig | undefined;
return resolveAccountEntry(cfg.channels?.signal?.accounts, accountId);
}
function mergeSignalAccountConfig(cfg: OpenClawConfig, accountId: string): SignalAccountConfig {

View File

@@ -2,6 +2,7 @@ import { normalizeChatType } from "../channels/chat-type.js";
import { createAccountListHelpers } from "../channels/plugins/account-helpers.js";
import type { OpenClawConfig } from "../config/config.js";
import type { SlackAccountConfig } from "../config/types.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
import { resolveSlackAppToken, resolveSlackBotToken } from "./token.js";
@@ -37,11 +38,7 @@ function resolveAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): SlackAccountConfig | undefined {
const accounts = cfg.channels?.slack?.accounts;
if (!accounts || typeof accounts !== "object") {
return undefined;
}
return accounts[accountId] as SlackAccountConfig | undefined;
return resolveAccountEntry(cfg.channels?.slack?.accounts, accountId);
}
function mergeSlackAccountConfig(cfg: OpenClawConfig, accountId: string): SlackAccountConfig {

View File

@@ -4,6 +4,7 @@ import type { OpenClawConfig } from "../config/config.js";
import type { TelegramAccountConfig, TelegramActionConfig } from "../config/types.js";
import { isTruthyEnvValue } from "../infra/env.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { listBoundAccountIds, resolveDefaultAgentBoundAccountId } from "../routing/bindings.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
import { resolveTelegramToken } from "./token.js";
@@ -78,17 +79,8 @@ function resolveAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): TelegramAccountConfig | undefined {
const accounts = cfg.channels?.telegram?.accounts;
if (!accounts || typeof accounts !== "object") {
return undefined;
}
const direct = accounts[accountId] as TelegramAccountConfig | undefined;
if (direct) {
return direct;
}
const normalized = normalizeAccountId(accountId);
const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized);
return matchKey ? (accounts[matchKey] as TelegramAccountConfig | undefined) : undefined;
return resolveAccountEntry(cfg.channels?.telegram?.accounts, normalized);
}
function mergeTelegramAccountConfig(cfg: OpenClawConfig, accountId: string): TelegramAccountConfig {

View File

@@ -1,6 +1,7 @@
import { resolveTextChunkLimit } from "../auto-reply/chunk.js";
import { getChannelDock } from "../channels/dock.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { normalizeAccountId } from "../routing/session-key.js";
const DEFAULT_TELEGRAM_DRAFT_STREAM_MIN = 200;
@@ -19,9 +20,8 @@ export function resolveTelegramDraftStreamingChunking(
fallbackLimit: providerChunkLimit,
});
const normalizedAccountId = normalizeAccountId(accountId);
const draftCfg =
cfg?.channels?.telegram?.accounts?.[normalizedAccountId]?.draftChunk ??
cfg?.channels?.telegram?.draftChunk;
const accountCfg = resolveAccountEntry(cfg?.channels?.telegram?.accounts, normalizedAccountId);
const draftCfg = accountCfg?.draftChunk ?? cfg?.channels?.telegram?.draftChunk;
const maxRequested = Math.max(
1,

View File

@@ -4,6 +4,7 @@ import { createAccountListHelpers } from "../channels/plugins/account-helpers.js
import type { OpenClawConfig } from "../config/config.js";
import { resolveOAuthDir } from "../config/paths.js";
import type { DmPolicy, GroupPolicy, WhatsAppAccountConfig } from "../config/types.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
import { resolveUserPath } from "../utils.js";
import { hasWebCredsSync } from "./auth-store.js";
@@ -68,12 +69,7 @@ function resolveAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): WhatsAppAccountConfig | undefined {
const accounts = cfg.channels?.whatsapp?.accounts;
if (!accounts || typeof accounts !== "object") {
return undefined;
}
const entry = accounts[accountId] as WhatsAppAccountConfig | undefined;
return entry;
return resolveAccountEntry(cfg.channels?.whatsapp?.accounts, accountId);
}
function resolveDefaultAuthDir(accountId: string): string {