mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix: harden ClickClack setup credentials
This commit is contained in:
@@ -148,7 +148,7 @@ describe("ClickClack account resolution", () => {
|
||||
expect(resolveClickClackAccount({ cfg, accountId: "work", env }).token).toBe("");
|
||||
});
|
||||
|
||||
it("reads tokenFile credentials and treats them as an implicit default account", () => {
|
||||
it("reads tokenFile credentials without overriding a named account token", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "clickclack-token-"));
|
||||
const tokenFile = path.join(tempDir, "token");
|
||||
fs.writeFileSync(tokenFile, " file-token \n", "utf8");
|
||||
@@ -160,12 +160,18 @@ describe("ClickClack account resolution", () => {
|
||||
baseUrl: "https://app.clickclack.chat",
|
||||
workspace: "wsp_1",
|
||||
tokenFile,
|
||||
accounts: {
|
||||
work: {
|
||||
token: "work-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies CoreConfig;
|
||||
|
||||
expect(listClickClackAccountIds(cfg)).toEqual(["default"]);
|
||||
expect(listClickClackAccountIds(cfg)).toEqual(["default", "work"]);
|
||||
expect(resolveClickClackAccount({ cfg }).token).toBe("file-token");
|
||||
expect(resolveClickClackAccount({ cfg, accountId: "work" }).token).toBe("work-token");
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/account-helpers";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import { resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution";
|
||||
import { resolveNormalizedAccountEntry } from "openclaw/plugin-sdk/account-resolution-runtime";
|
||||
import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { resolveDefaultSecretProviderAlias } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
|
||||
@@ -42,17 +43,35 @@ const {
|
||||
|
||||
export { DEFAULT_ACCOUNT_ID, listClickClackAccountIds, resolveDefaultClickClackAccountId };
|
||||
|
||||
function resolveMergedClickClackAccountConfig(
|
||||
export function resolveClickClackAccountConfig(
|
||||
cfg: CoreConfig,
|
||||
accountId: string,
|
||||
): ClickClackAccountConfig {
|
||||
return resolveMergedAccountConfig<ClickClackAccountConfig>({
|
||||
const channel = cfg.channels?.clickclack;
|
||||
const merged = resolveMergedAccountConfig<ClickClackAccountConfig>({
|
||||
channelConfig: cfg.channels?.clickclack as ClickClackAccountConfig | undefined,
|
||||
accounts: cfg.channels?.clickclack?.accounts,
|
||||
accounts: channel?.accounts,
|
||||
accountId,
|
||||
omitKeys: ["defaultAccount"],
|
||||
normalizeAccountId,
|
||||
});
|
||||
const account = resolveNormalizedAccountEntry(channel?.accounts, accountId, normalizeAccountId);
|
||||
const accountTokenFile = account?.tokenFile?.trim();
|
||||
if (accountTokenFile) {
|
||||
return {
|
||||
...merged,
|
||||
token: account.token,
|
||||
tokenFile: accountTokenFile,
|
||||
};
|
||||
}
|
||||
if (hasConfiguredAccountValue(account?.token)) {
|
||||
return {
|
||||
...merged,
|
||||
token: account?.token,
|
||||
tokenFile: undefined,
|
||||
};
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function resolveClickClackToken(params: {
|
||||
@@ -130,7 +149,7 @@ export function resolveClickClackAccount(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ResolvedClickClackAccount {
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const merged = resolveMergedClickClackAccountConfig(params.cfg, accountId);
|
||||
const merged = resolveClickClackAccountConfig(params.cfg, accountId);
|
||||
const baseEnabled = params.cfg.channels?.clickclack?.enabled !== false;
|
||||
const enabled = baseEnabled && merged.enabled !== false;
|
||||
const baseUrl = merged.baseUrl?.trim().replace(/\/$/, "") ?? "";
|
||||
|
||||
@@ -77,6 +77,19 @@ describe("ClickClack setup adapter", () => {
|
||||
input: { useEnv: true },
|
||||
}),
|
||||
).toBeNull();
|
||||
expect(
|
||||
validate({
|
||||
cfg: {
|
||||
channels: {
|
||||
clickclack: {
|
||||
baseUrl: "ssh://clickclack.example",
|
||||
workspace: "default",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
input: { useEnv: true },
|
||||
}),
|
||||
).toBe("ClickClack base URL must be a valid http(s) URL.");
|
||||
});
|
||||
|
||||
it("rejects malformed base URLs before writing config", () => {
|
||||
@@ -88,7 +101,7 @@ describe("ClickClack setup adapter", () => {
|
||||
workspace: "default",
|
||||
},
|
||||
}),
|
||||
).toBe("ClickClack --base-url must be a valid http(s) URL.");
|
||||
).toBe("ClickClack base URL must be a valid http(s) URL.");
|
||||
});
|
||||
|
||||
it("writes normalized default and named account config", () => {
|
||||
@@ -221,7 +234,8 @@ describe("ClickClack setup adapter", () => {
|
||||
cfg: {
|
||||
channels: {
|
||||
clickclack: {
|
||||
...base.channels?.clickclack,
|
||||
baseUrl: "https://clickclack.example/",
|
||||
workspace: " default ",
|
||||
token: "ccb_old",
|
||||
tokenFile: "/run/secrets/old-token",
|
||||
},
|
||||
@@ -232,6 +246,34 @@ describe("ClickClack setup adapter", () => {
|
||||
});
|
||||
expect(withEnv.channels?.clickclack).not.toHaveProperty("token");
|
||||
expect(withEnv.channels?.clickclack).not.toHaveProperty("tokenFile");
|
||||
expect(withEnv.channels?.clickclack).toMatchObject({
|
||||
baseUrl: "https://clickclack.example",
|
||||
workspace: "default",
|
||||
});
|
||||
|
||||
const namedWithToken = clickClackSetupAdapter.applyAccountConfig({
|
||||
cfg: {
|
||||
channels: {
|
||||
clickclack: {
|
||||
baseUrl: "https://clickclack.example",
|
||||
workspace: "default",
|
||||
tokenFile: "/run/secrets/default-token",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
accountId: "work",
|
||||
input: {
|
||||
token: "ccb_work",
|
||||
baseUrl: "https://clickclack.example",
|
||||
workspace: "work",
|
||||
},
|
||||
});
|
||||
expect(namedWithToken.channels?.clickclack).not.toHaveProperty("tokenFile");
|
||||
expect(namedWithToken.channels?.clickclack?.accounts).toMatchObject({
|
||||
default: { tokenFile: "/run/secrets/default-token" },
|
||||
work: { token: "ccb_work" },
|
||||
});
|
||||
expect(namedWithToken.channels?.clickclack?.accounts?.work).not.toHaveProperty("tokenFile");
|
||||
});
|
||||
|
||||
it("preserves credentials when a partial patch does not select an auth mode", () => {
|
||||
|
||||
@@ -6,14 +6,16 @@ import {
|
||||
applyAccountNameToChannelSection,
|
||||
applySetupAccountConfigPatch,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
moveSingleAccountChannelSectionToDefaultAccount,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { createSetupInputPresenceValidator } from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { resolveClickClackAccount } from "./accounts.js";
|
||||
import { resolveClickClackAccountConfig } from "./accounts.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
const channel = "clickclack" as const;
|
||||
const REQUIRED_INPUT_ERROR =
|
||||
"ClickClack requires --token, --base-url, and --workspace (or --use-env).";
|
||||
const INVALID_BASE_URL_ERROR = "ClickClack base URL must be a valid http(s) URL.";
|
||||
|
||||
export function normalizeClickClackBaseUrl(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
@@ -37,14 +39,22 @@ export function applyClickClackSetupConfigPatch(params: {
|
||||
name?: string;
|
||||
patch: Record<string, unknown>;
|
||||
}): OpenClawConfig {
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const scopedConfig =
|
||||
accountId === DEFAULT_ACCOUNT_ID
|
||||
? params.cfg
|
||||
: moveSingleAccountChannelSectionToDefaultAccount({
|
||||
cfg: params.cfg,
|
||||
channelKey: channel,
|
||||
});
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg: params.cfg,
|
||||
cfg: scopedConfig,
|
||||
channelKey: channel,
|
||||
accountId: params.accountId,
|
||||
accountId,
|
||||
name: params.name,
|
||||
});
|
||||
const next =
|
||||
params.accountId !== DEFAULT_ACCOUNT_ID
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? migrateBaseNameToDefaultAccount({
|
||||
cfg: namedConfig,
|
||||
channelKey: channel,
|
||||
@@ -53,7 +63,7 @@ export function applyClickClackSetupConfigPatch(params: {
|
||||
return applySetupAccountConfigPatch({
|
||||
cfg: next,
|
||||
channelKey: channel,
|
||||
accountId: params.accountId,
|
||||
accountId,
|
||||
patch: params.patch,
|
||||
});
|
||||
}
|
||||
@@ -157,27 +167,31 @@ export const clickClackSetupAdapter: ChannelSetupAdapter = {
|
||||
validate: ({ cfg, accountId, input }) => {
|
||||
const baseUrl = normalizeClickClackBaseUrl(input.baseUrl);
|
||||
if (input.baseUrl && !baseUrl) {
|
||||
return "ClickClack --base-url must be a valid http(s) URL.";
|
||||
return INVALID_BASE_URL_ERROR;
|
||||
}
|
||||
if (!input.useEnv) {
|
||||
return null;
|
||||
}
|
||||
const resolved = resolveClickClackAccount({
|
||||
cfg: cfg as CoreConfig,
|
||||
accountId,
|
||||
});
|
||||
if (!baseUrl && !resolved.baseUrl) {
|
||||
const existing = resolveClickClackAccountConfig(cfg as CoreConfig, accountId);
|
||||
const existingBaseUrl = normalizeClickClackBaseUrl(existing.baseUrl);
|
||||
if (!baseUrl && existing.baseUrl?.trim() && !existingBaseUrl) {
|
||||
return INVALID_BASE_URL_ERROR;
|
||||
}
|
||||
if (!baseUrl && !existingBaseUrl) {
|
||||
return REQUIRED_INPUT_ERROR;
|
||||
}
|
||||
if (!input.workspace?.trim() && !resolved.workspace) {
|
||||
if (!input.workspace?.trim() && !existing.workspace?.trim()) {
|
||||
return REQUIRED_INPUT_ERROR;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const baseUrl = normalizeClickClackBaseUrl(input.baseUrl);
|
||||
const workspace = input.workspace?.trim();
|
||||
const existing = input.useEnv
|
||||
? resolveClickClackAccountConfig(cfg as CoreConfig, accountId)
|
||||
: undefined;
|
||||
const baseUrl = normalizeClickClackBaseUrl(input.baseUrl ?? existing?.baseUrl);
|
||||
const workspace = input.workspace?.trim() || existing?.workspace?.trim();
|
||||
const tokenFile = input.tokenFile?.trim();
|
||||
const token = input.token?.trim();
|
||||
const next = applyClickClackSetupConfigPatch({
|
||||
|
||||
Reference in New Issue
Block a user