fix: replace stale ClickClack credentials

This commit is contained in:
Shakker
2026-07-15 13:05:10 +01:00
committed by Shakker
parent 80909e6fff
commit c3ab3133d8
2 changed files with 180 additions and 3 deletions
+92 -1
View File
@@ -2,7 +2,11 @@
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { describe, expect, it } from "vitest";
import { clickClackSetupAdapter, normalizeClickClackBaseUrl } from "./setup-core.js";
import {
applyClickClackCredentialConfig,
clickClackSetupAdapter,
normalizeClickClackBaseUrl,
} from "./setup-core.js";
function validate(params: {
cfg?: OpenClawConfig;
@@ -162,4 +166,91 @@ describe("ClickClack setup adapter", () => {
},
});
});
it("clears stale competing credentials when the auth mode changes", () => {
const base = {
channels: {
clickclack: {
baseUrl: "https://clickclack.example",
workspace: "default",
},
},
} as OpenClawConfig;
const withToken = clickClackSetupAdapter.applyAccountConfig({
cfg: {
channels: {
clickclack: {
...base.channels?.clickclack,
tokenFile: "/run/secrets/old-token",
},
},
} as OpenClawConfig,
accountId: DEFAULT_ACCOUNT_ID,
input: {
token: "ccb_new",
baseUrl: "https://clickclack.example",
workspace: "default",
},
});
expect(withToken.channels?.clickclack).toMatchObject({ token: "ccb_new" });
expect(withToken.channels?.clickclack).not.toHaveProperty("tokenFile");
const withFile = clickClackSetupAdapter.applyAccountConfig({
cfg: {
channels: {
clickclack: {
...base.channels?.clickclack,
token: "ccb_old",
},
},
} as OpenClawConfig,
accountId: DEFAULT_ACCOUNT_ID,
input: {
tokenFile: "/run/secrets/new-token",
baseUrl: "https://clickclack.example",
workspace: "default",
},
});
expect(withFile.channels?.clickclack).toMatchObject({
tokenFile: "/run/secrets/new-token",
});
expect(withFile.channels?.clickclack).not.toHaveProperty("token");
const withEnv = clickClackSetupAdapter.applyAccountConfig({
cfg: {
channels: {
clickclack: {
...base.channels?.clickclack,
token: "ccb_old",
tokenFile: "/run/secrets/old-token",
},
},
} as OpenClawConfig,
accountId: DEFAULT_ACCOUNT_ID,
input: { useEnv: true },
});
expect(withEnv.channels?.clickclack).not.toHaveProperty("token");
expect(withEnv.channels?.clickclack).not.toHaveProperty("tokenFile");
});
it("preserves credentials when a partial patch does not select an auth mode", () => {
const cfg = {
channels: {
clickclack: {
tokenFile: "/run/secrets/clickclack",
},
},
} as OpenClawConfig;
expect(
applyClickClackCredentialConfig({
cfg,
accountId: DEFAULT_ACCOUNT_ID,
}).channels?.clickclack,
).toMatchObject({
enabled: true,
tokenFile: "/run/secrets/clickclack",
});
});
});
+88 -2
View File
@@ -58,6 +58,86 @@ export function applyClickClackSetupConfigPatch(params: {
});
}
function clearClickClackSetupConfigFields(params: {
cfg: OpenClawConfig;
accountId: string;
fields: string[];
}): OpenClawConfig {
const clickclack = (params.cfg.channels as Record<string, unknown> | undefined)?.clickclack as
| (Record<string, unknown> & { accounts?: Record<string, Record<string, unknown>> })
| undefined;
if (!clickclack) {
return params.cfg;
}
const accountId = normalizeAccountId(params.accountId);
if (accountId === DEFAULT_ACCOUNT_ID) {
const nextClickClack = { ...clickclack };
for (const field of params.fields) {
delete nextClickClack[field];
}
return {
...params.cfg,
channels: {
...params.cfg.channels,
clickclack: nextClickClack,
},
} as OpenClawConfig;
}
const currentAccount = clickclack.accounts?.[accountId];
if (!currentAccount) {
return params.cfg;
}
const nextAccount = { ...currentAccount };
for (const field of params.fields) {
delete nextAccount[field];
}
return {
...params.cfg,
channels: {
...params.cfg.channels,
clickclack: {
...clickclack,
accounts: {
...clickclack.accounts,
[accountId]: nextAccount,
},
},
},
} as OpenClawConfig;
}
export function applyClickClackCredentialConfig(params: {
cfg: OpenClawConfig;
accountId: string;
token?: unknown;
tokenFile?: string;
useEnv?: boolean;
}): OpenClawConfig {
const fieldsToClear = params.useEnv
? ["token", "tokenFile"]
: params.tokenFile
? ["token"]
: params.token !== undefined
? ["tokenFile"]
: [];
const next = applyClickClackSetupConfigPatch({
cfg: params.cfg,
accountId: params.accountId,
patch: params.useEnv
? {}
: params.tokenFile
? { tokenFile: params.tokenFile }
: params.token !== undefined
? { token: params.token }
: {},
});
return clearClickClackSetupConfigFields({
cfg: next,
accountId: params.accountId,
fields: fieldsToClear,
});
}
export const clickClackSetupAdapter: ChannelSetupAdapter = {
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
applyAccountName: ({ cfg, accountId, name }) =>
@@ -100,15 +180,21 @@ export const clickClackSetupAdapter: ChannelSetupAdapter = {
const workspace = input.workspace?.trim();
const tokenFile = input.tokenFile?.trim();
const token = input.token?.trim();
return applyClickClackSetupConfigPatch({
const next = applyClickClackSetupConfigPatch({
cfg,
accountId,
name: input.name,
patch: {
...(baseUrl ? { baseUrl } : {}),
...(workspace ? { workspace } : {}),
...(!input.useEnv && tokenFile ? { tokenFile } : !input.useEnv && token ? { token } : {}),
},
});
return applyClickClackCredentialConfig({
cfg: next,
accountId,
token,
tokenFile,
useEnv: input.useEnv,
});
},
};