mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-20 07:54:36 +00:00
fix(security): harden account-key handling against prototype pollution
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
5
src/infra/prototype-keys.ts
Normal file
5
src/infra/prototype-keys.ts
Normal 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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
19
src/routing/account-lookup.test.ts
Normal file
19
src/routing/account-lookup.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
14
src/routing/account-lookup.ts
Normal file
14
src/routing/account-lookup.ts
Normal 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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user