mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-28 02:12:07 +00:00
fix: resolve telegram token fallback for binding-created accounts (#54362) (thanks @openperf)
* fix(telegram): resolve channel-level token fallthrough for binding-created accountIds Fixes #53876 * fix(telegram): align isConfigured with resolveTelegramToken multi-bot guard * fix(telegram): use normalized account lookup and require available token
This commit is contained in:
@@ -476,6 +476,97 @@ describe("telegramPlugin duplicate token guard", () => {
|
||||
expect(await telegramPlugin.config.isConfigured!(alertsAccount, cfg)).toBe(true);
|
||||
});
|
||||
|
||||
// Regression: https://github.com/openclaw/openclaw/issues/53876
|
||||
// Single-bot setup with channel-level token should report configured.
|
||||
it("reports configured for single-bot setup with channel-level token", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "single-bot-token",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const account = resolveAccount(cfg, "default");
|
||||
expect(await telegramPlugin.config.isConfigured!(account, cfg)).toBe(true);
|
||||
});
|
||||
|
||||
// Regression: https://github.com/openclaw/openclaw/issues/53876
|
||||
// Binding-created non-default accountId in single-bot setup should report configured.
|
||||
it("reports configured for binding-created accountId in single-bot setup", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "single-bot-token",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const account = resolveAccount(cfg, "bot-main");
|
||||
expect(account.token).toBe("single-bot-token");
|
||||
expect(await telegramPlugin.config.isConfigured!(account, cfg)).toBe(true);
|
||||
});
|
||||
|
||||
// Regression: multi-bot guard — unknown binding-created accountId in multi-bot
|
||||
// setup must NOT be reported as configured, matching resolveTelegramToken behaviour.
|
||||
it("reports not configured for unknown binding-created accountId in multi-bot setup", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "channel-level-token",
|
||||
enabled: true,
|
||||
accounts: {
|
||||
knownBot: { botToken: "known-bot-token" },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const account = resolveAccount(cfg, "unknownBot");
|
||||
expect(await telegramPlugin.config.isConfigured!(account, cfg)).toBe(false);
|
||||
expect(telegramPlugin.config.unconfiguredReason?.(account, cfg)).toContain("unknown accountId");
|
||||
});
|
||||
|
||||
// Regression: multi-bot guard must use full normalization (same as resolveTelegramToken)
|
||||
// so that account keys like "Carey Notifications" resolve to "carey-notifications".
|
||||
it("multi-bot guard normalizes account keys with spaces and mixed case", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "channel-level-token",
|
||||
enabled: true,
|
||||
accounts: {
|
||||
"Carey Notifications": { botToken: "carey-token" },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
// "carey-notifications" is the normalized form of "Carey Notifications"
|
||||
const account = resolveAccount(cfg, "carey-notifications");
|
||||
expect(await telegramPlugin.config.isConfigured!(account, cfg)).toBe(true);
|
||||
});
|
||||
|
||||
// Regression: configured_unavailable token (e.g. unreadable tokenFile) should
|
||||
// NOT be reported as configured — runtime would fail to authenticate.
|
||||
it("reports not configured when token is configured_unavailable", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
tokenFile: "/nonexistent/path/to/token",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const account = resolveAccount(cfg, "default");
|
||||
// tokenFile is configured but file doesn't exist → configured_unavailable
|
||||
expect(await telegramPlugin.config.isConfigured!(account, cfg)).toBe(false);
|
||||
expect(telegramPlugin.config.unconfiguredReason?.(account, cfg)).toContain("unavailable");
|
||||
});
|
||||
|
||||
it("does not crash startup when a resolved account token is undefined", async () => {
|
||||
const { monitorTelegramProvider, probeTelegram } = installGatewayRuntime({
|
||||
probeOk: false,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { resolveNormalizedAccountEntry } from "openclaw/plugin-sdk/account-resolution";
|
||||
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
|
||||
import {
|
||||
adaptScopedAccountAccessor,
|
||||
createScopedChannelConfigAdapter,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
getChatChannelMeta,
|
||||
@@ -56,6 +58,39 @@ export function formatDuplicateTelegramTokenReason(params: {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the runtime token resolver (`resolveTelegramToken`) would
|
||||
* block channel-level fallthrough for the given accountId. This mirrors the
|
||||
* guard in `token.ts` so that status-check functions (`isConfigured`,
|
||||
* `unconfiguredReason`, `describeAccount`) stay consistent with the gateway
|
||||
* runtime behaviour.
|
||||
*
|
||||
* The guard fires when:
|
||||
* 1. The accountId is not the default account, AND
|
||||
* 2. The config has an explicit `accounts` section with entries, AND
|
||||
* 3. The accountId is not found in that `accounts` section.
|
||||
*
|
||||
* See: https://github.com/openclaw/openclaw/issues/53876
|
||||
*/
|
||||
function isBlockedByMultiBotGuard(cfg: OpenClawConfig, accountId: string): boolean {
|
||||
if (normalizeAccountId(accountId) === DEFAULT_ACCOUNT_ID) {
|
||||
return false;
|
||||
}
|
||||
const accounts = cfg.channels?.telegram?.accounts;
|
||||
const hasConfiguredAccounts =
|
||||
!!accounts &&
|
||||
typeof accounts === "object" &&
|
||||
!Array.isArray(accounts) &&
|
||||
Object.keys(accounts).length > 0;
|
||||
if (!hasConfiguredAccounts) {
|
||||
return false;
|
||||
}
|
||||
// Use resolveNormalizedAccountEntry (same as resolveTelegramToken in token.ts)
|
||||
// instead of resolveAccountEntry to handle keys that require full normalization
|
||||
// (e.g. "Carey Notifications" → "carey-notifications").
|
||||
return !resolveNormalizedAccountEntry(accounts, accountId, normalizeAccountId);
|
||||
}
|
||||
|
||||
export const telegramConfigAdapter = createScopedChannelConfigAdapter<ResolvedTelegramAccount>({
|
||||
sectionKey: TELEGRAM_CHANNEL,
|
||||
listAccountIds: listTelegramAccountIds,
|
||||
@@ -97,13 +132,32 @@ export function createTelegramPluginBase(params: {
|
||||
config: {
|
||||
...telegramConfigAdapter,
|
||||
isConfigured: (account, cfg) => {
|
||||
if (!account.token?.trim()) {
|
||||
// Use inspectTelegramAccount for a complete token resolution that includes
|
||||
// channel-level fallback paths not available in resolveTelegramAccount.
|
||||
// This ensures binding-created accountIds that inherit the channel-level
|
||||
// token are correctly detected as configured.
|
||||
// See: https://github.com/openclaw/openclaw/issues/53876
|
||||
if (isBlockedByMultiBotGuard(cfg, account.accountId)) {
|
||||
return false;
|
||||
}
|
||||
const inspected = inspectTelegramAccount({ cfg, accountId: account.accountId });
|
||||
// Gate on actually available token, not just "configured" — the latter
|
||||
// includes "configured_unavailable" (unreadable tokenFile, unresolved
|
||||
// SecretRef) which would pass here but fail at runtime.
|
||||
if (!inspected.token?.trim()) {
|
||||
return false;
|
||||
}
|
||||
return !findTelegramTokenOwnerAccountId({ cfg, accountId: account.accountId });
|
||||
},
|
||||
unconfiguredReason: (account, cfg) => {
|
||||
if (!account.token?.trim()) {
|
||||
if (isBlockedByMultiBotGuard(cfg, account.accountId)) {
|
||||
return `not configured: unknown accountId "${account.accountId}" in multi-bot setup`;
|
||||
}
|
||||
const inspected = inspectTelegramAccount({ cfg, accountId: account.accountId });
|
||||
if (!inspected.token?.trim()) {
|
||||
if (inspected.tokenStatus === "configured_unavailable") {
|
||||
return `not configured: token ${inspected.tokenSource} is configured but unavailable`;
|
||||
}
|
||||
return "not configured";
|
||||
}
|
||||
const ownerAccountId = findTelegramTokenOwnerAccountId({
|
||||
@@ -118,15 +172,27 @@ export function createTelegramPluginBase(params: {
|
||||
ownerAccountId,
|
||||
});
|
||||
},
|
||||
describeAccount: (account, cfg) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured:
|
||||
Boolean(account.token?.trim()) &&
|
||||
!findTelegramTokenOwnerAccountId({ cfg, accountId: account.accountId }),
|
||||
tokenSource: account.tokenSource,
|
||||
}),
|
||||
describeAccount: (account, cfg) => {
|
||||
if (isBlockedByMultiBotGuard(cfg, account.accountId)) {
|
||||
return {
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: false,
|
||||
tokenSource: "none" as const,
|
||||
};
|
||||
}
|
||||
const inspected = inspectTelegramAccount({ cfg, accountId: account.accountId });
|
||||
return {
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured:
|
||||
!!inspected.token?.trim() &&
|
||||
!findTelegramTokenOwnerAccountId({ cfg, accountId: account.accountId }),
|
||||
tokenSource: inspected.tokenSource,
|
||||
};
|
||||
},
|
||||
},
|
||||
setup: params.setup,
|
||||
}) as Pick<
|
||||
|
||||
@@ -236,6 +236,42 @@ describe("resolveTelegramToken", () => {
|
||||
/channels\.telegram\.botToken: unresolved SecretRef/i,
|
||||
);
|
||||
});
|
||||
|
||||
// Regression: https://github.com/openclaw/openclaw/issues/53876
|
||||
// Binding-created accountIds should inherit the channel-level token in
|
||||
// single-bot setups (no accounts section).
|
||||
it("falls through to channel-level token for binding-created accountId without accounts section", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "channel-level-token",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const res = resolveTelegramToken(cfg, { accountId: "bot-main" });
|
||||
expect(res.token).toBe("channel-level-token");
|
||||
expect(res.source).toBe("config");
|
||||
});
|
||||
|
||||
it("still blocks fallthrough for unknown accountId when accounts section exists", () => {
|
||||
vi.stubEnv("TELEGRAM_BOT_TOKEN", "");
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "wrong-bot-token",
|
||||
accounts: {
|
||||
knownBot: { botToken: "known-bot-token" },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const res = resolveTelegramToken(cfg, { accountId: "unknownBot" });
|
||||
expect(res.token).toBe("");
|
||||
expect(res.source).toBe("none");
|
||||
});
|
||||
});
|
||||
|
||||
describe("telegram update offset store", () => {
|
||||
|
||||
@@ -39,13 +39,28 @@ export function resolveTelegramToken(
|
||||
);
|
||||
|
||||
// When a non-default accountId is explicitly specified but not found in config,
|
||||
// return empty immediately — do NOT fall through to channel-level defaults,
|
||||
// which would silently route the message via the wrong bot's token.
|
||||
// decide whether to fall through to channel-level defaults based on whether
|
||||
// the config has an explicit accounts section (multi-bot setup).
|
||||
//
|
||||
// Multi-bot: accounts section exists with entries → block fallthrough to prevent
|
||||
// routing via the wrong bot's token.
|
||||
//
|
||||
// Single-bot: no accounts section (or empty) → allow fallthrough so that
|
||||
// binding-created accountIds inherit the channel-level token.
|
||||
// See: https://github.com/openclaw/openclaw/issues/53876
|
||||
if (accountId !== DEFAULT_ACCOUNT_ID && !accountCfg) {
|
||||
opts.logMissingFile?.(
|
||||
`channels.telegram.accounts: unknown accountId "${accountId}" — not found in config, refusing channel-level fallback`,
|
||||
);
|
||||
return { token: "", source: "none" };
|
||||
const accounts = telegramCfg?.accounts;
|
||||
const hasConfiguredAccounts =
|
||||
!!accounts &&
|
||||
typeof accounts === "object" &&
|
||||
!Array.isArray(accounts) &&
|
||||
Object.keys(accounts).length > 0;
|
||||
if (hasConfiguredAccounts) {
|
||||
opts.logMissingFile?.(
|
||||
`channels.telegram.accounts: unknown accountId "${accountId}" — not found in config, refusing channel-level fallback`,
|
||||
);
|
||||
return { token: "", source: "none" };
|
||||
}
|
||||
}
|
||||
|
||||
const accountTokenFile = accountCfg?.tokenFile?.trim();
|
||||
|
||||
Reference in New Issue
Block a user