Skip to content

Commit bcfec22

Browse files
fix(discord): resolve env SecretRefs during token resolution
Align resolveDiscordToken with Telegram: inspect SecretRefs, resolve env:provider:id via secrets.providers/allowlists, keep strict unresolved non-env refs. Fixes #76371. Co-authored-by: Cursor <[email protected]>
1 parent 3d64fca commit bcfec22

3 files changed

Lines changed: 118 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
1313

1414
### Fixes
1515

16+
- Discord: resolve `channels.discord` env-backed `SecretRef` tokens against `secrets.providers`/allowlists during channel startup instead of rejecting them as unresolved strict refs before env fallback ran. Fixes #76371.
1617
- Gateway: preserve stack diagnostics when `chat.send` or agent attachment parsing/staging fails, improving image-send failure triage. Refs #63432. (#75135) Thanks @keen0206.
1718
- Maintainer workflow: push prepared PR heads through GitHub's verified commit API by default and require an explicit override before git-protocol pushes can publish unsigned commits. Thanks @BunsDev.
1819
- Feishu: resolve setup/status probes through the selected/default account so multi-account configs with account-scoped app credentials show as configured and probeable. Fixes #72930. Thanks @brokemac79.

extensions/discord/src/token.test.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ describe("resolveDiscordToken", () => {
9191
expect(res.source).toBe("config");
9292
});
9393

94-
it("throws when token is an unresolved SecretRef object", () => {
94+
it("resolves env-backed SecretRefs from process.env", () => {
95+
vi.stubEnv("DISCORD_BOT_TOKEN", "secretref-env-token");
9596
const cfg = {
9697
channels: {
9798
discord: {
@@ -100,8 +101,23 @@ describe("resolveDiscordToken", () => {
100101
},
101102
} as unknown as OpenClawConfig;
102103

103-
expect(() => resolveDiscordToken(cfg)).toThrow(
104-
/channels\.discord\.token: unresolved SecretRef/i,
105-
);
104+
expect(resolveDiscordToken(cfg)).toEqual({
105+
token: "secretref-env-token",
106+
source: "config",
107+
});
108+
});
109+
110+
it("does not fall back when an explicit env SecretRef is configured but unavailable", () => {
111+
vi.stubEnv("DISCORD_BOT_TOKEN", "fallback-env-token");
112+
vi.stubEnv("DISCORD_REF_TOKEN", "");
113+
const cfg = {
114+
channels: {
115+
discord: {
116+
token: { source: "env", provider: "default", id: "DISCORD_REF_TOKEN" },
117+
},
118+
},
119+
} as unknown as OpenClawConfig;
120+
121+
expect(resolveDiscordToken(cfg)).toEqual({ token: "", source: "none" });
106122
});
107123
});

extensions/discord/src/token.ts

Lines changed: 97 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,93 @@
11
import type { BaseTokenResolution } from "openclaw/plugin-sdk/channel-contract";
22
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
3-
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/routing";
4-
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
5-
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
3+
import { resolveDefaultSecretProviderAlias } from "openclaw/plugin-sdk/provider-auth";
4+
import {
5+
DEFAULT_ACCOUNT_ID,
6+
normalizeAccountId,
7+
resolveAccountEntry,
8+
} from "openclaw/plugin-sdk/routing";
9+
import {
10+
normalizeResolvedSecretInputString,
11+
normalizeSecretInputString,
12+
resolveSecretInputString,
13+
} from "openclaw/plugin-sdk/secret-input";
614

715
type DiscordTokenSource = "env" | "config" | "none";
816

917
export type DiscordTokenResolution = BaseTokenResolution & {
1018
source: DiscordTokenSource;
1119
};
1220

21+
const stripDiscordBotPrefix = (value: string) => value.replace(/^Bot\s+/i, "");
22+
1323
export function normalizeDiscordToken(raw: unknown, path: string): string | undefined {
1424
const trimmed = normalizeResolvedSecretInputString({ value: raw, path });
15-
if (!trimmed) {
16-
return undefined;
25+
return trimmed ? stripDiscordBotPrefix(trimmed) : undefined;
26+
}
27+
28+
function resolveDiscordEnvSecretRefValue(params: {
29+
cfg?: Pick<OpenClawConfig, "secrets">;
30+
provider: string;
31+
id: string;
32+
env?: NodeJS.ProcessEnv;
33+
}): string | undefined {
34+
const prov = params.cfg?.secrets?.providers?.[params.provider];
35+
if (prov) {
36+
if (prov.source !== "env") {
37+
throw new Error(
38+
`Secret provider "${params.provider}" has source "${prov.source}" but ref requests "env".`,
39+
);
40+
}
41+
if (prov.allowlist && !prov.allowlist.includes(params.id)) {
42+
throw new Error(
43+
`Environment variable "${params.id}" is not allowlisted in secrets.providers.${params.provider}.allowlist.`,
44+
);
45+
}
46+
} else if (
47+
params.provider !== resolveDefaultSecretProviderAlias({ secrets: params.cfg?.secrets }, "env")
48+
) {
49+
throw new Error(
50+
`Secret provider "${params.provider}" is not configured (ref: env:${params.provider}:${params.id}).`,
51+
);
52+
}
53+
return normalizeSecretInputString((params.env ?? process.env)[params.id]);
54+
}
55+
56+
function resolveDiscordConfiguredToken(params: {
57+
cfg?: Pick<OpenClawConfig, "secrets">;
58+
value: unknown;
59+
path: string;
60+
}) {
61+
const d = params.cfg?.secrets?.defaults;
62+
const r = resolveSecretInputString({
63+
value: params.value,
64+
path: params.path,
65+
defaults: d,
66+
mode: "inspect",
67+
});
68+
if (r.status === "available") {
69+
return { status: "available" as const, value: stripDiscordBotPrefix(r.value) };
70+
}
71+
if (r.status === "missing") {
72+
return { status: "missing" as const };
73+
}
74+
if (r.ref.source !== "env") {
75+
resolveSecretInputString({
76+
value: params.value,
77+
path: params.path,
78+
defaults: d,
79+
mode: "strict",
80+
});
81+
return { status: "configured_unavailable" as const };
1782
}
18-
return trimmed.replace(/^Bot\s+/i, "");
83+
const envVal = resolveDiscordEnvSecretRefValue({
84+
cfg: params.cfg,
85+
provider: r.ref.provider,
86+
id: r.ref.id,
87+
});
88+
return envVal
89+
? { status: "available" as const, value: stripDiscordBotPrefix(envVal) }
90+
: { status: "configured_unavailable" as const };
1991
}
2092

2193
export function resolveDiscordToken(
@@ -29,32 +101,31 @@ export function resolveDiscordToken(
29101
accountCfg &&
30102
Object.prototype.hasOwnProperty.call(accountCfg as Record<string, unknown>, "token"),
31103
);
32-
const accountToken = normalizeDiscordToken(
33-
(accountCfg as { token?: unknown } | undefined)?.token ?? undefined,
34-
`channels.discord.accounts.${accountId}.token`,
35-
);
36-
if (accountToken) {
37-
return { token: accountToken, source: "config" };
104+
const accountResolved = resolveDiscordConfiguredToken({
105+
cfg,
106+
value: (accountCfg as { token?: unknown } | undefined)?.token,
107+
path: `channels.discord.accounts.${accountId}.token`,
108+
});
109+
if (accountResolved.status === "available") {
110+
return { token: accountResolved.value, source: "config" };
38111
}
39-
if (hasAccountToken) {
112+
if (accountResolved.status === "configured_unavailable" || hasAccountToken) {
40113
return { token: "", source: "none" };
41114
}
42-
43-
const configToken = normalizeDiscordToken(
44-
discordCfg?.token ?? undefined,
45-
"channels.discord.token",
46-
);
47-
if (configToken) {
48-
return { token: configToken, source: "config" };
115+
const channelResolved = resolveDiscordConfiguredToken({
116+
cfg,
117+
value: discordCfg?.token ?? undefined,
118+
path: "channels.discord.token",
119+
});
120+
if (channelResolved.status === "available") {
121+
return { token: channelResolved.value, source: "config" };
122+
}
123+
if (channelResolved.status === "configured_unavailable") {
124+
return { token: "", source: "none" };
49125
}
50-
51126
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
52127
const envToken = allowEnv
53128
? normalizeDiscordToken(opts.envToken ?? process.env.DISCORD_BOT_TOKEN, "DISCORD_BOT_TOKEN")
54129
: undefined;
55-
if (envToken) {
56-
return { token: envToken, source: "env" };
57-
}
58-
59-
return { token: "", source: "none" };
130+
return envToken ? { token: envToken, source: "env" } : { token: "", source: "none" };
60131
}

0 commit comments

Comments
 (0)