Skip to content

Commit f98e14b

Browse files
committed
Fix Discord SecretRef configured state
1 parent 9891f30 commit f98e14b

9 files changed

Lines changed: 108 additions & 20 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: report unresolved SecretRef bot tokens as configured but unavailable in status snapshots without treating them as runtime-startable tokens.
1617
- Gateway/sessions: keep async `sessions.list` title and preview hydration bounded to transcript head/tail reads so Control UI polling cannot full-scan large session transcripts every refresh. Thanks @vincentkoc.
1718
- Gateway: preserve stack diagnostics when `chat.send` or agent attachment parsing/staging fails, improving image-send failure triage. Refs #63432. (#75135) Thanks @keen0206.
1819
- 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.

extensions/discord/src/accounts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type ResolvedDiscordAccount = {
2222
name?: string;
2323
token: string;
2424
tokenSource: "env" | "config" | "none";
25+
tokenStatus: "available" | "configured_unavailable" | "missing";
2526
config: DiscordAccountConfig;
2627
};
2728

@@ -114,6 +115,7 @@ export function resolveDiscordAccount(params: {
114115
name: normalizeOptionalString(merged.name),
115116
token: tokenResolution.token,
116117
tokenSource: tokenResolution.source,
118+
tokenStatus: tokenResolution.tokenStatus,
117119
config: merged,
118120
};
119121
}

extensions/discord/src/client.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe("createDiscordRestClient", () => {
5858
expect(result.account.config.retry).toMatchObject({ attempts: 7 });
5959
});
6060

61-
it("still throws when no explicit token is provided and config token is unresolved", () => {
61+
it("throws a missing token error when no explicit token is provided and config token is unresolved", () => {
6262
const cfg = {
6363
channels: {
6464
discord: {
@@ -71,6 +71,8 @@ describe("createDiscordRestClient", () => {
7171
},
7272
} as OpenClawConfig;
7373

74-
expect(() => createDiscordRestClient({ cfg, rest: fakeRest })).toThrow(/unresolved SecretRef/i);
74+
expect(() => createDiscordRestClient({ cfg, rest: fakeRest })).toThrow(
75+
/Discord bot token missing/,
76+
);
7577
});
7678
});

extensions/discord/src/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ function resolveAccountWithoutToken(params: {
9292
name: normalizeOptionalString(merged.name),
9393
token: "",
9494
tokenSource: "none",
95+
tokenStatus: "missing",
9596
config: merged,
9697
};
9798
}

extensions/discord/src/security-audit.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function createAccount(
2222
enabled: true,
2323
token: "t",
2424
tokenSource: "config",
25+
tokenStatus: "available",
2526
config,
2627
};
2728
}

extensions/discord/src/shared.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,29 @@ describe("createDiscordPluginBase", () => {
6262
);
6363
expect(plugin.config.isEnabled?.(workAccount, cfg)).toBe(true);
6464
});
65+
66+
it("does not treat unavailable SecretRef tokens as runtime configured", async () => {
67+
const plugin = createDiscordPluginBase({ setup: {} as never });
68+
const cfg = {
69+
channels: {
70+
discord: {
71+
token: {
72+
source: "env",
73+
provider: "default",
74+
id: "DISCORD_BOT_TOKEN",
75+
},
76+
},
77+
},
78+
} as OpenClawConfig;
79+
80+
const account = plugin.config.resolveAccount(cfg, "default");
81+
82+
expect(await plugin.config.isConfigured?.(account, cfg)).toBe(false);
83+
expect(plugin.config.describeAccount?.(account, cfg)).toMatchObject({
84+
configured: true,
85+
tokenStatus: "configured_unavailable",
86+
});
87+
});
6588
});
6689

6790
describe("discordConfigAdapter", () => {

extensions/discord/src/shared.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,14 @@ export function createDiscordPluginBase(params: {
149149
typeof env?.DISCORD_BOT_TOKEN === "string" && env.DISCORD_BOT_TOKEN.trim().length > 0,
150150
isEnabled: (account, cfg) => isDiscordAccountEnabledForRuntime(account, cfg),
151151
disabledReason: (account, cfg) => resolveDiscordAccountDisabledReason(account, cfg),
152-
isConfigured: (account) => Boolean(account.token?.trim()),
152+
isConfigured: (account) => account.tokenStatus === "available",
153153
describeAccount: (account) =>
154154
describeAccountSnapshot({
155155
account,
156-
configured: Boolean(account.token?.trim()),
156+
configured: account.tokenStatus !== "missing",
157157
extra: {
158158
tokenSource: account.tokenSource,
159+
tokenStatus: account.tokenStatus,
159160
},
160161
}),
161162
},

extensions/discord/src/token.test.ts

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

94-
it("throws when token is an unresolved SecretRef object", () => {
94+
it("marks unresolved SecretRef token as configured unavailable", () => {
9595
const cfg = {
9696
channels: {
9797
discord: {
@@ -100,8 +100,27 @@ describe("resolveDiscordToken", () => {
100100
},
101101
} as unknown as OpenClawConfig;
102102

103-
expect(() => resolveDiscordToken(cfg)).toThrow(
104-
/channels\.discord\.token: unresolved SecretRef/i,
105-
);
103+
const res = resolveDiscordToken(cfg);
104+
expect(res.token).toBe("");
105+
expect(res.source).toBe("config");
106+
expect(res.tokenStatus).toBe("configured_unavailable");
107+
});
108+
109+
it("marks explicit blank account token as missing without falling back", () => {
110+
const cfg = {
111+
channels: {
112+
discord: {
113+
token: "base-token",
114+
accounts: {
115+
work: { token: "" },
116+
},
117+
},
118+
},
119+
} as OpenClawConfig;
120+
121+
const res = resolveDiscordToken(cfg, { accountId: "work" });
122+
expect(res.token).toBe("");
123+
expect(res.source).toBe("none");
124+
expect(res.tokenStatus).toBe("missing");
106125
});
107126
});

extensions/discord/src/token.ts

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ import type { BaseTokenResolution } from "openclaw/plugin-sdk/channel-contract";
22
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
33
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/routing";
44
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
5-
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
5+
import {
6+
normalizeResolvedSecretInputString,
7+
resolveSecretInputString,
8+
type SecretInputStringResolution,
9+
} from "openclaw/plugin-sdk/secret-input";
610

711
type DiscordTokenSource = "env" | "config" | "none";
12+
type DiscordCredentialStatus = SecretInputStringResolution["status"];
813

914
export type DiscordTokenResolution = BaseTokenResolution & {
1015
source: DiscordTokenSource;
16+
tokenStatus: DiscordCredentialStatus;
1117
};
1218

1319
export function normalizeDiscordToken(raw: unknown, path: string): string | undefined {
@@ -18,6 +24,20 @@ export function normalizeDiscordToken(raw: unknown, path: string): string | unde
1824
return trimmed.replace(/^Bot\s+/i, "");
1925
}
2026

27+
function inspectDiscordToken(
28+
raw: unknown,
29+
path: string,
30+
): {
31+
token: string;
32+
tokenStatus: DiscordCredentialStatus;
33+
} {
34+
const resolved = resolveSecretInputString({ value: raw, path, mode: "inspect" });
35+
if (resolved.status !== "available") {
36+
return { token: "", tokenStatus: resolved.status };
37+
}
38+
return { token: resolved.value.replace(/^Bot\s+/i, ""), tokenStatus: "available" };
39+
}
40+
2141
export function resolveDiscordToken(
2242
cfg: OpenClawConfig,
2343
opts: { accountId?: string | null; envToken?: string | null } = {},
@@ -29,32 +49,50 @@ export function resolveDiscordToken(
2949
accountCfg &&
3050
Object.prototype.hasOwnProperty.call(accountCfg as Record<string, unknown>, "token"),
3151
);
32-
const accountToken = normalizeDiscordToken(
33-
(accountCfg as { token?: unknown } | undefined)?.token ?? undefined,
52+
const inspectedAccountToken = inspectDiscordToken(
53+
(accountCfg as { token?: unknown } | undefined)?.token,
3454
`channels.discord.accounts.${accountId}.token`,
3555
);
56+
const accountToken = inspectedAccountToken.token;
3657
if (accountToken) {
37-
return { token: accountToken, source: "config" };
58+
return {
59+
token: accountToken,
60+
source: "config",
61+
tokenStatus: inspectedAccountToken.tokenStatus,
62+
};
3863
}
3964
if (hasAccountToken) {
40-
return { token: "", source: "none" };
65+
return {
66+
token: inspectedAccountToken.token,
67+
source: inspectedAccountToken.tokenStatus === "configured_unavailable" ? "config" : "none",
68+
tokenStatus: inspectedAccountToken.tokenStatus,
69+
};
4170
}
4271

43-
const configToken = normalizeDiscordToken(
44-
discordCfg?.token ?? undefined,
45-
"channels.discord.token",
46-
);
72+
const inspectedConfigToken = inspectDiscordToken(discordCfg?.token, "channels.discord.token");
73+
const configToken = inspectedConfigToken.token;
4774
if (configToken) {
48-
return { token: configToken, source: "config" };
75+
return {
76+
token: configToken,
77+
source: "config",
78+
tokenStatus: inspectedConfigToken.tokenStatus,
79+
};
80+
}
81+
if (inspectedConfigToken.tokenStatus === "configured_unavailable") {
82+
return {
83+
token: inspectedConfigToken.token,
84+
source: "config",
85+
tokenStatus: "configured_unavailable",
86+
};
4987
}
5088

5189
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
5290
const envToken = allowEnv
5391
? normalizeDiscordToken(opts.envToken ?? process.env.DISCORD_BOT_TOKEN, "DISCORD_BOT_TOKEN")
5492
: undefined;
5593
if (envToken) {
56-
return { token: envToken, source: "env" };
94+
return { token: envToken, source: "env", tokenStatus: "available" };
5795
}
5896

59-
return { token: "", source: "none" };
97+
return { token: "", source: "none", tokenStatus: "missing" };
6098
}

0 commit comments

Comments
 (0)