mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 06:32:43 +00:00
CLI: make read-only SecretRef status flows degrade safely (#37023)
* CLI: add read-only SecretRef inspection * CLI: fix read-only SecretRef status regressions * CLI: preserve read-only SecretRef status fallbacks * Docs: document read-only channel inspection hook * CLI: preserve audit coverage for read-only SecretRefs * CLI: fix read-only status account selection * CLI: fix targeted gateway fallback analysis * CLI: fix Slack HTTP read-only inspection * CLI: align audit credential status checks * CLI: restore Telegram read-only fallback semantics
This commit is contained in:
@@ -50,6 +50,12 @@ function makeSlackPlugin(params?: { botToken?: string; appToken?: string }): Cha
|
||||
config: {
|
||||
listAccountIds: () => ["primary"],
|
||||
defaultAccountId: () => "primary",
|
||||
inspectAccount: () => ({
|
||||
name: "Primary",
|
||||
enabled: true,
|
||||
botToken: params?.botToken ?? "bot-token",
|
||||
appToken: params?.appToken ?? "app-token",
|
||||
}),
|
||||
resolveAccount: () => ({
|
||||
name: "Primary",
|
||||
enabled: true,
|
||||
@@ -65,6 +71,196 @@ function makeSlackPlugin(params?: { botToken?: string; appToken?: string }): Cha
|
||||
};
|
||||
}
|
||||
|
||||
function makeUnavailableSlackPlugin(): ChannelPlugin {
|
||||
return {
|
||||
id: "slack",
|
||||
meta: {
|
||||
id: "slack",
|
||||
label: "Slack",
|
||||
selectionLabel: "Slack",
|
||||
docsPath: "/channels/slack",
|
||||
blurb: "test",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => ["primary"],
|
||||
defaultAccountId: () => "primary",
|
||||
inspectAccount: () => ({
|
||||
name: "Primary",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
botToken: "",
|
||||
appToken: "",
|
||||
botTokenSource: "config",
|
||||
appTokenSource: "config",
|
||||
botTokenStatus: "configured_unavailable",
|
||||
appTokenStatus: "configured_unavailable",
|
||||
}),
|
||||
resolveAccount: () => ({
|
||||
name: "Primary",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
botToken: "",
|
||||
appToken: "",
|
||||
botTokenSource: "config",
|
||||
appTokenSource: "config",
|
||||
botTokenStatus: "configured_unavailable",
|
||||
appTokenStatus: "configured_unavailable",
|
||||
}),
|
||||
isConfigured: () => true,
|
||||
isEnabled: () => true,
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeSourceAwareUnavailablePlugin(): ChannelPlugin {
|
||||
return {
|
||||
id: "slack",
|
||||
meta: {
|
||||
id: "slack",
|
||||
label: "Slack",
|
||||
selectionLabel: "Slack",
|
||||
docsPath: "/channels/slack",
|
||||
blurb: "test",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => ["primary"],
|
||||
defaultAccountId: () => "primary",
|
||||
inspectAccount: (cfg) =>
|
||||
(cfg as { marker?: string }).marker === "source"
|
||||
? {
|
||||
name: "Primary",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
botToken: "",
|
||||
appToken: "",
|
||||
botTokenSource: "config",
|
||||
appTokenSource: "config",
|
||||
botTokenStatus: "configured_unavailable",
|
||||
appTokenStatus: "configured_unavailable",
|
||||
}
|
||||
: {
|
||||
name: "Primary",
|
||||
enabled: true,
|
||||
configured: false,
|
||||
botToken: "",
|
||||
appToken: "",
|
||||
botTokenSource: "none",
|
||||
appTokenSource: "none",
|
||||
},
|
||||
resolveAccount: () => ({
|
||||
name: "Primary",
|
||||
enabled: true,
|
||||
botToken: "",
|
||||
appToken: "",
|
||||
}),
|
||||
isConfigured: (account) => Boolean((account as { configured?: boolean }).configured),
|
||||
isEnabled: () => true,
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeSourceUnavailableResolvedAvailablePlugin(): ChannelPlugin {
|
||||
return {
|
||||
id: "discord",
|
||||
meta: {
|
||||
id: "discord",
|
||||
label: "Discord",
|
||||
selectionLabel: "Discord",
|
||||
docsPath: "/channels/discord",
|
||||
blurb: "test",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => ["primary"],
|
||||
defaultAccountId: () => "primary",
|
||||
inspectAccount: (cfg) =>
|
||||
(cfg as { marker?: string }).marker === "source"
|
||||
? {
|
||||
name: "Primary",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
tokenSource: "config",
|
||||
tokenStatus: "configured_unavailable",
|
||||
}
|
||||
: {
|
||||
name: "Primary",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
tokenSource: "config",
|
||||
tokenStatus: "available",
|
||||
},
|
||||
resolveAccount: () => ({
|
||||
name: "Primary",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
tokenSource: "config",
|
||||
tokenStatus: "available",
|
||||
}),
|
||||
isConfigured: (account) => Boolean((account as { configured?: boolean }).configured),
|
||||
isEnabled: () => true,
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeHttpSlackUnavailablePlugin(): ChannelPlugin {
|
||||
return {
|
||||
id: "slack",
|
||||
meta: {
|
||||
id: "slack",
|
||||
label: "Slack",
|
||||
selectionLabel: "Slack",
|
||||
docsPath: "/channels/slack",
|
||||
blurb: "test",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => ["primary"],
|
||||
defaultAccountId: () => "primary",
|
||||
inspectAccount: () => ({
|
||||
accountId: "primary",
|
||||
name: "Primary",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
mode: "http",
|
||||
botToken: "xoxb-http",
|
||||
signingSecret: "",
|
||||
botTokenSource: "config",
|
||||
signingSecretSource: "config",
|
||||
botTokenStatus: "available",
|
||||
signingSecretStatus: "configured_unavailable",
|
||||
}),
|
||||
resolveAccount: () => ({
|
||||
name: "Primary",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
mode: "http",
|
||||
botToken: "xoxb-http",
|
||||
signingSecret: "",
|
||||
botTokenSource: "config",
|
||||
signingSecretSource: "config",
|
||||
botTokenStatus: "available",
|
||||
signingSecretStatus: "configured_unavailable",
|
||||
}),
|
||||
isConfigured: () => true,
|
||||
isEnabled: () => true,
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeTokenPlugin(): ChannelPlugin {
|
||||
return {
|
||||
id: "token-only",
|
||||
@@ -122,6 +318,90 @@ describe("buildChannelsTable - mattermost token summary", () => {
|
||||
expect(slackRow?.detail).toContain("need bot+app");
|
||||
});
|
||||
|
||||
it("reports configured-but-unavailable Slack credentials as warn", async () => {
|
||||
vi.mocked(listChannelPlugins).mockReturnValue([makeUnavailableSlackPlugin()]);
|
||||
|
||||
const table = await buildChannelsTable({ channels: {} } as never, {
|
||||
showSecrets: false,
|
||||
});
|
||||
|
||||
const slackRow = table.rows.find((row) => row.id === "slack");
|
||||
expect(slackRow).toBeDefined();
|
||||
expect(slackRow?.state).toBe("warn");
|
||||
expect(slackRow?.detail).toContain("unavailable in this command path");
|
||||
});
|
||||
|
||||
it("preserves unavailable credential state from the source config snapshot", async () => {
|
||||
vi.mocked(listChannelPlugins).mockReturnValue([makeSourceAwareUnavailablePlugin()]);
|
||||
|
||||
const table = await buildChannelsTable({ marker: "resolved", channels: {} } as never, {
|
||||
showSecrets: false,
|
||||
sourceConfig: { marker: "source", channels: {} } as never,
|
||||
});
|
||||
|
||||
const slackRow = table.rows.find((row) => row.id === "slack");
|
||||
expect(slackRow).toBeDefined();
|
||||
expect(slackRow?.state).toBe("warn");
|
||||
expect(slackRow?.detail).toContain("unavailable in this command path");
|
||||
|
||||
const slackDetails = table.details.find((detail) => detail.title === "Slack accounts");
|
||||
expect(slackDetails).toBeDefined();
|
||||
expect(slackDetails?.rows).toEqual([
|
||||
{
|
||||
Account: "primary (Primary)",
|
||||
Notes: "bot:config · app:config · secret unavailable in this command path",
|
||||
Status: "WARN",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats status-only available credentials as resolved", async () => {
|
||||
vi.mocked(listChannelPlugins).mockReturnValue([makeSourceUnavailableResolvedAvailablePlugin()]);
|
||||
|
||||
const table = await buildChannelsTable({ marker: "resolved", channels: {} } as never, {
|
||||
showSecrets: false,
|
||||
sourceConfig: { marker: "source", channels: {} } as never,
|
||||
});
|
||||
|
||||
const discordRow = table.rows.find((row) => row.id === "discord");
|
||||
expect(discordRow).toBeDefined();
|
||||
expect(discordRow?.state).toBe("ok");
|
||||
expect(discordRow?.detail).toBe("configured");
|
||||
|
||||
const discordDetails = table.details.find((detail) => detail.title === "Discord accounts");
|
||||
expect(discordDetails).toBeDefined();
|
||||
expect(discordDetails?.rows).toEqual([
|
||||
{
|
||||
Account: "primary (Primary)",
|
||||
Notes: "token:config",
|
||||
Status: "OK",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats Slack HTTP signing-secret availability as required config", async () => {
|
||||
vi.mocked(listChannelPlugins).mockReturnValue([makeHttpSlackUnavailablePlugin()]);
|
||||
|
||||
const table = await buildChannelsTable({ channels: {} } as never, {
|
||||
showSecrets: false,
|
||||
});
|
||||
|
||||
const slackRow = table.rows.find((row) => row.id === "slack");
|
||||
expect(slackRow).toBeDefined();
|
||||
expect(slackRow?.state).toBe("warn");
|
||||
expect(slackRow?.detail).toContain("configured http credentials unavailable");
|
||||
|
||||
const slackDetails = table.details.find((detail) => detail.title === "Slack accounts");
|
||||
expect(slackDetails).toBeDefined();
|
||||
expect(slackDetails?.rows).toEqual([
|
||||
{
|
||||
Account: "primary (Primary)",
|
||||
Notes: "bot:config · signing:config · secret unavailable in this command path",
|
||||
Status: "WARN",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("still reports single-token channels as ok", async () => {
|
||||
vi.mocked(listChannelPlugins).mockReturnValue([makeTokenPlugin()]);
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import fs from "node:fs";
|
||||
import {
|
||||
hasConfiguredUnavailableCredentialStatus,
|
||||
hasResolvedCredentialValue,
|
||||
} from "../../channels/account-snapshot-fields.js";
|
||||
import {
|
||||
buildChannelAccountSnapshot,
|
||||
formatChannelAllowFrom,
|
||||
@@ -12,6 +16,7 @@ import type {
|
||||
ChannelId,
|
||||
ChannelPlugin,
|
||||
} from "../../channels/plugins/types.js";
|
||||
import { inspectReadOnlyChannelAccount } from "../../channels/read-only-account-inspect.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { sha256HexPrefix } from "../../logging/redact-identifier.js";
|
||||
import { formatTimeAgo } from "./format.js";
|
||||
@@ -32,6 +37,13 @@ type ChannelAccountRow = {
|
||||
snapshot: ChannelAccountSnapshot;
|
||||
};
|
||||
|
||||
type ResolvedChannelAccountRowParams = {
|
||||
plugin: ChannelPlugin;
|
||||
cfg: OpenClawConfig;
|
||||
sourceConfig: OpenClawConfig;
|
||||
accountId: string;
|
||||
};
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> =>
|
||||
value && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
||||
|
||||
@@ -79,6 +91,61 @@ function formatTokenHint(token: string, opts: { showSecrets: boolean }): string
|
||||
return `${head}…${tail} · len ${t.length}`;
|
||||
}
|
||||
|
||||
function inspectChannelAccount(plugin: ChannelPlugin, cfg: OpenClawConfig, accountId: string) {
|
||||
return (
|
||||
plugin.config.inspectAccount?.(cfg, accountId) ??
|
||||
inspectReadOnlyChannelAccount({
|
||||
channelId: plugin.id,
|
||||
cfg,
|
||||
accountId,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveChannelAccountRow(
|
||||
params: ResolvedChannelAccountRowParams,
|
||||
): Promise<ChannelAccountRow> {
|
||||
const { plugin, cfg, sourceConfig, accountId } = params;
|
||||
const sourceInspectedAccount = inspectChannelAccount(plugin, sourceConfig, accountId);
|
||||
const resolvedInspectedAccount = inspectChannelAccount(plugin, cfg, accountId);
|
||||
const resolvedInspection = resolvedInspectedAccount as {
|
||||
enabled?: boolean;
|
||||
configured?: boolean;
|
||||
} | null;
|
||||
const sourceInspection = sourceInspectedAccount as {
|
||||
enabled?: boolean;
|
||||
configured?: boolean;
|
||||
} | null;
|
||||
const resolvedAccount = resolvedInspectedAccount ?? plugin.config.resolveAccount(cfg, accountId);
|
||||
const useSourceUnavailableAccount = Boolean(
|
||||
sourceInspectedAccount &&
|
||||
hasConfiguredUnavailableCredentialStatus(sourceInspectedAccount) &&
|
||||
(!hasResolvedCredentialValue(resolvedAccount) ||
|
||||
(sourceInspection?.configured === true && resolvedInspection?.configured === false)),
|
||||
);
|
||||
const account = useSourceUnavailableAccount ? sourceInspectedAccount : resolvedAccount;
|
||||
const selectedInspection = useSourceUnavailableAccount ? sourceInspection : resolvedInspection;
|
||||
const enabled =
|
||||
selectedInspection?.enabled ?? resolveChannelAccountEnabled({ plugin, account, cfg });
|
||||
const configured =
|
||||
selectedInspection?.configured ??
|
||||
(await resolveChannelAccountConfigured({
|
||||
plugin,
|
||||
account,
|
||||
cfg,
|
||||
readAccountConfiguredField: true,
|
||||
}));
|
||||
const snapshot = buildChannelAccountSnapshot({
|
||||
plugin,
|
||||
cfg,
|
||||
accountId,
|
||||
account,
|
||||
enabled,
|
||||
configured,
|
||||
});
|
||||
return { accountId, account, enabled, configured, snapshot };
|
||||
}
|
||||
|
||||
const formatAccountLabel = (params: { accountId: string; name?: string }) => {
|
||||
const base = params.accountId || "default";
|
||||
if (params.name?.trim()) {
|
||||
@@ -110,6 +177,12 @@ const buildAccountNotes = (params: {
|
||||
if (snapshot.appTokenSource && snapshot.appTokenSource !== "none") {
|
||||
notes.push(`app:${snapshot.appTokenSource}`);
|
||||
}
|
||||
if (snapshot.signingSecretSource && snapshot.signingSecretSource !== "none") {
|
||||
notes.push(`signing:${snapshot.signingSecretSource}`);
|
||||
}
|
||||
if (hasConfiguredUnavailableCredentialStatus(entry.account)) {
|
||||
notes.push("secret unavailable in this command path");
|
||||
}
|
||||
if (snapshot.baseUrl) {
|
||||
notes.push(snapshot.baseUrl);
|
||||
}
|
||||
@@ -191,13 +264,90 @@ function summarizeTokenConfig(params: {
|
||||
const accountRecs = enabled.map((a) => asRecord(a.account));
|
||||
const hasBotTokenField = accountRecs.some((r) => "botToken" in r);
|
||||
const hasAppTokenField = accountRecs.some((r) => "appToken" in r);
|
||||
const hasSigningSecretField = accountRecs.some(
|
||||
(r) => "signingSecret" in r || "signingSecretSource" in r || "signingSecretStatus" in r,
|
||||
);
|
||||
const hasTokenField = accountRecs.some((r) => "token" in r);
|
||||
|
||||
if (!hasBotTokenField && !hasAppTokenField && !hasTokenField) {
|
||||
if (!hasBotTokenField && !hasAppTokenField && !hasSigningSecretField && !hasTokenField) {
|
||||
return { state: null, detail: null };
|
||||
}
|
||||
|
||||
const accountIsHttpMode = (rec: Record<string, unknown>) =>
|
||||
typeof rec.mode === "string" && rec.mode.trim() === "http";
|
||||
const hasCredentialAvailable = (
|
||||
rec: Record<string, unknown>,
|
||||
valueKey: string,
|
||||
statusKey: string,
|
||||
) => {
|
||||
const value = rec[valueKey];
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
return true;
|
||||
}
|
||||
return rec[statusKey] === "available";
|
||||
};
|
||||
|
||||
if (
|
||||
hasBotTokenField &&
|
||||
hasSigningSecretField &&
|
||||
enabled.every((a) => accountIsHttpMode(asRecord(a.account)))
|
||||
) {
|
||||
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
|
||||
const ready = enabled.filter((a) => {
|
||||
const rec = asRecord(a.account);
|
||||
return (
|
||||
hasCredentialAvailable(rec, "botToken", "botTokenStatus") &&
|
||||
hasCredentialAvailable(rec, "signingSecret", "signingSecretStatus")
|
||||
);
|
||||
});
|
||||
const partial = enabled.filter((a) => {
|
||||
const rec = asRecord(a.account);
|
||||
const hasBot = hasCredentialAvailable(rec, "botToken", "botTokenStatus");
|
||||
const hasSigning = hasCredentialAvailable(rec, "signingSecret", "signingSecretStatus");
|
||||
return (hasBot && !hasSigning) || (!hasBot && hasSigning);
|
||||
});
|
||||
|
||||
if (unavailable.length > 0) {
|
||||
return {
|
||||
state: "warn",
|
||||
detail: `configured http credentials unavailable in this command path · accounts ${unavailable.length}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (partial.length > 0) {
|
||||
return {
|
||||
state: "warn",
|
||||
detail: `partial credentials (need bot+signing) · accounts ${partial.length}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (ready.length === 0) {
|
||||
return { state: "setup", detail: "no credentials (need bot+signing)" };
|
||||
}
|
||||
|
||||
const botSources = summarizeSources(ready.map((a) => a.snapshot.botTokenSource ?? "none"));
|
||||
const signingSources = summarizeSources(
|
||||
ready.map((a) => a.snapshot.signingSecretSource ?? "none"),
|
||||
);
|
||||
const sample = ready[0]?.account ? asRecord(ready[0].account) : {};
|
||||
const botToken = typeof sample.botToken === "string" ? sample.botToken : "";
|
||||
const signingSecret = typeof sample.signingSecret === "string" ? sample.signingSecret : "";
|
||||
const botHint = botToken.trim()
|
||||
? formatTokenHint(botToken, { showSecrets: params.showSecrets })
|
||||
: "";
|
||||
const signingHint = signingSecret.trim()
|
||||
? formatTokenHint(signingSecret, { showSecrets: params.showSecrets })
|
||||
: "";
|
||||
const hint =
|
||||
botHint || signingHint ? ` (bot ${botHint || "?"}, signing ${signingHint || "?"})` : "";
|
||||
return {
|
||||
state: "ok",
|
||||
detail: `credentials ok (bot ${botSources.label}, signing ${signingSources.label})${hint} · accounts ${ready.length}/${enabled.length || 1}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (hasBotTokenField && hasAppTokenField) {
|
||||
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
|
||||
const ready = enabled.filter((a) => {
|
||||
const rec = asRecord(a.account);
|
||||
const bot = typeof rec.botToken === "string" ? rec.botToken.trim() : "";
|
||||
@@ -220,6 +370,13 @@ function summarizeTokenConfig(params: {
|
||||
};
|
||||
}
|
||||
|
||||
if (unavailable.length > 0) {
|
||||
return {
|
||||
state: "warn",
|
||||
detail: `configured tokens unavailable in this command path · accounts ${unavailable.length}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (ready.length === 0) {
|
||||
return { state: "setup", detail: "no tokens (need bot+app)" };
|
||||
}
|
||||
@@ -245,12 +402,20 @@ function summarizeTokenConfig(params: {
|
||||
}
|
||||
|
||||
if (hasBotTokenField) {
|
||||
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
|
||||
const ready = enabled.filter((a) => {
|
||||
const rec = asRecord(a.account);
|
||||
const bot = typeof rec.botToken === "string" ? rec.botToken.trim() : "";
|
||||
return Boolean(bot);
|
||||
});
|
||||
|
||||
if (unavailable.length > 0) {
|
||||
return {
|
||||
state: "warn",
|
||||
detail: `configured bot token unavailable in this command path · accounts ${unavailable.length}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (ready.length === 0) {
|
||||
return { state: "setup", detail: "no bot token" };
|
||||
}
|
||||
@@ -268,10 +433,17 @@ function summarizeTokenConfig(params: {
|
||||
};
|
||||
}
|
||||
|
||||
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
|
||||
const ready = enabled.filter((a) => {
|
||||
const rec = asRecord(a.account);
|
||||
return typeof rec.token === "string" ? Boolean(rec.token.trim()) : false;
|
||||
});
|
||||
if (unavailable.length > 0) {
|
||||
return {
|
||||
state: "warn",
|
||||
detail: `configured token unavailable in this command path · accounts ${unavailable.length}`,
|
||||
};
|
||||
}
|
||||
if (ready.length === 0) {
|
||||
return { state: "setup", detail: "no token" };
|
||||
}
|
||||
@@ -292,7 +464,7 @@ function summarizeTokenConfig(params: {
|
||||
// Keep this generic: channel-specific rules belong in the channel plugin.
|
||||
export async function buildChannelsTable(
|
||||
cfg: OpenClawConfig,
|
||||
opts?: { showSecrets?: boolean },
|
||||
opts?: { showSecrets?: boolean; sourceConfig?: OpenClawConfig },
|
||||
): Promise<{
|
||||
rows: ChannelRow[];
|
||||
details: Array<{
|
||||
@@ -319,29 +491,24 @@ export async function buildChannelsTable(
|
||||
const resolvedAccountIds = accountIds.length > 0 ? accountIds : [defaultAccountId];
|
||||
|
||||
const accounts: ChannelAccountRow[] = [];
|
||||
const sourceConfig = opts?.sourceConfig ?? cfg;
|
||||
for (const accountId of resolvedAccountIds) {
|
||||
const account = plugin.config.resolveAccount(cfg, accountId);
|
||||
const enabled = resolveChannelAccountEnabled({ plugin, account, cfg });
|
||||
const configured = await resolveChannelAccountConfigured({
|
||||
plugin,
|
||||
account,
|
||||
cfg,
|
||||
readAccountConfiguredField: true,
|
||||
});
|
||||
const snapshot = buildChannelAccountSnapshot({
|
||||
plugin,
|
||||
cfg,
|
||||
accountId,
|
||||
account,
|
||||
enabled,
|
||||
configured,
|
||||
});
|
||||
accounts.push({ accountId, account, enabled, configured, snapshot });
|
||||
accounts.push(
|
||||
await resolveChannelAccountRow({
|
||||
plugin,
|
||||
cfg,
|
||||
sourceConfig,
|
||||
accountId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const anyEnabled = accounts.some((a) => a.enabled);
|
||||
const enabledAccounts = accounts.filter((a) => a.enabled);
|
||||
const configuredAccounts = enabledAccounts.filter((a) => a.configured);
|
||||
const unavailableConfiguredAccounts = enabledAccounts.filter((a) =>
|
||||
hasConfiguredUnavailableCredentialStatus(a.account),
|
||||
);
|
||||
const defaultEntry = accounts.find((a) => a.accountId === defaultAccountId) ?? accounts[0];
|
||||
|
||||
const summary = plugin.status?.buildChannelSummary
|
||||
@@ -379,6 +546,9 @@ export async function buildChannelsTable(
|
||||
if (issues.length > 0) {
|
||||
return "warn";
|
||||
}
|
||||
if (unavailableConfiguredAccounts.length > 0) {
|
||||
return "warn";
|
||||
}
|
||||
if (link.linked === false) {
|
||||
return "setup";
|
||||
}
|
||||
@@ -423,6 +593,13 @@ export async function buildChannelsTable(
|
||||
return extra.length > 0 ? `${base} · ${extra.join(" · ")}` : base;
|
||||
}
|
||||
|
||||
if (unavailableConfiguredAccounts.length > 0) {
|
||||
if (tokenSummary.detail?.includes("unavailable")) {
|
||||
return tokenSummary.detail;
|
||||
}
|
||||
return `configured credentials unavailable in this command path · accounts ${unavailableConfiguredAccounts.length}`;
|
||||
}
|
||||
|
||||
if (tokenSummary.detail) {
|
||||
return tokenSummary.detail;
|
||||
}
|
||||
@@ -461,7 +638,10 @@ export async function buildChannelsTable(
|
||||
accountId: entry.accountId,
|
||||
name: entry.snapshot.name,
|
||||
}),
|
||||
Status: entry.enabled ? "OK" : "WARN",
|
||||
Status:
|
||||
entry.enabled && !hasConfiguredUnavailableCredentialStatus(entry.account)
|
||||
? "OK"
|
||||
: "WARN",
|
||||
Notes: notes.join(" · "),
|
||||
};
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user