refactor: collapse zod setup validators

This commit is contained in:
Peter Steinberger
2026-03-27 03:48:12 +00:00
parent e25965ed4a
commit ef56d79a6a
11 changed files with 116 additions and 148 deletions

View File

@@ -1,5 +1,5 @@
import {
createZodSetupInputValidator,
createSetupInputPresenceValidator,
createTopLevelChannelDmPolicySetter,
normalizeAccountId,
patchScopedAccountConfig,
@@ -8,7 +8,6 @@ import {
type DmPolicy,
type OpenClawConfig,
} from "openclaw/plugin-sdk/setup";
import { z } from "zod";
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
const channel = "bluebubbles" as const;
@@ -16,13 +15,6 @@ const setBlueBubblesTopLevelDmPolicy = createTopLevelChannelDmPolicySetter({
channel,
});
const BlueBubblesSetupInputSchema = z
.object({
httpUrl: z.string().optional(),
password: z.string().optional(),
})
.passthrough();
export function setBlueBubblesDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy): OpenClawConfig {
return setBlueBubblesTopLevelDmPolicy(cfg, dmPolicy);
}
@@ -51,8 +43,7 @@ export const blueBubblesSetupAdapter: ChannelSetupAdapter = {
accountId,
name,
}),
validateInput: createZodSetupInputValidator({
schema: BlueBubblesSetupInputSchema,
validateInput: createSetupInputPresenceValidator({
validate: ({ input }) => {
if (!input.httpUrl && !input.password) {
return "BlueBubbles requires --http-url and --password.";

View File

@@ -1,33 +1,22 @@
import {
createPatchedAccountSetupAdapter,
createZodSetupInputValidator,
createSetupInputPresenceValidator,
DEFAULT_ACCOUNT_ID,
} from "openclaw/plugin-sdk/setup";
import { z } from "zod";
const channel = "googlechat" as const;
const GoogleChatSetupInputSchema = z
.object({
useEnv: z.boolean().optional(),
token: z.string().optional(),
tokenFile: z.string().optional(),
})
.passthrough();
export const googlechatSetupAdapter = createPatchedAccountSetupAdapter({
channelKey: channel,
validateInput: createZodSetupInputValidator({
schema: GoogleChatSetupInputSchema,
validate: ({ accountId, input }) => {
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
return "GOOGLE_CHAT_SERVICE_ACCOUNT env vars can only be used for the default account.";
}
if (!input.useEnv && !input.token && !input.tokenFile) {
return "Google Chat requires --token (service account JSON) or --token-file.";
}
return null;
},
validateInput: createSetupInputPresenceValidator({
defaultAccountOnlyEnvError:
"GOOGLE_CHAT_SERVICE_ACCOUNT env vars can only be used for the default account.",
whenNotUseEnv: [
{
someOf: ["token", "tokenFile"],
message: "Google Chat requires --token (service account JSON) or --token-file.",
},
],
}),
buildPatch: (input) => {
const patch = input.useEnv

View File

@@ -3,12 +3,11 @@ import type { DmPolicy } from "openclaw/plugin-sdk/config-runtime";
import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
import {
applyAccountNameToChannelSection,
createZodSetupInputValidator,
createSetupInputPresenceValidator,
createTopLevelChannelAllowFromSetter,
createTopLevelChannelDmPolicySetter,
patchScopedAccountConfig,
} from "openclaw/plugin-sdk/setup";
import { z } from "zod";
import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js";
const channel = "irc" as const;
@@ -30,13 +29,6 @@ type IrcSetupInput = ChannelSetupInput & {
password?: string;
};
const IrcSetupInputSchema = z
.object({
host: z.string().trim().min(1, "IRC requires host."),
nick: z.string().trim().min(1, "IRC requires nick."),
})
.passthrough() as z.ZodType<IrcSetupInput>;
export function parsePort(raw: string, fallback: number): number {
const trimmed = raw.trim();
if (!trimmed) {
@@ -110,7 +102,12 @@ export const ircSetupAdapter: ChannelSetupAdapter = {
accountId,
name,
}),
validateInput: createZodSetupInputValidator({ schema: IrcSetupInputSchema }),
validateInput: createSetupInputPresenceValidator({
whenNotUseEnv: [
{ someOf: ["host"], message: "IRC requires host." },
{ someOf: ["nick"], message: "IRC requires nick." },
],
}),
applyAccountConfig: ({ cfg, accountId, input }) => {
const setupInput = input as IrcSetupInput;
const namedConfig = applyAccountNameToChannelSection({

View File

@@ -1,6 +1,5 @@
import type { ChannelSetupAdapter, OpenClawConfig } from "openclaw/plugin-sdk/setup";
import { createZodSetupInputValidator } from "openclaw/plugin-sdk/setup";
import { z } from "zod";
import { createSetupInputPresenceValidator } from "openclaw/plugin-sdk/setup";
import { hasLineCredentials, parseLineAllowFromId } from "./account-helpers.js";
import {
DEFAULT_ACCOUNT_ID,
@@ -12,16 +11,6 @@ import {
const channel = "line" as const;
const LineSetupInputSchema = z
.object({
useEnv: z.boolean().optional(),
channelAccessToken: z.string().optional(),
channelSecret: z.string().optional(),
tokenFile: z.string().optional(),
secretFile: z.string().optional(),
})
.passthrough();
export function patchLineAccountConfig(params: {
cfg: OpenClawConfig;
accountId: string;
@@ -92,20 +81,19 @@ export const lineSetupAdapter: ChannelSetupAdapter = {
accountId,
patch: name?.trim() ? { name: name.trim() } : {},
}),
validateInput: createZodSetupInputValidator({
schema: LineSetupInputSchema,
validate: ({ accountId, input }) => {
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
return "LINE_CHANNEL_ACCESS_TOKEN can only be used for the default account.";
}
if (!input.useEnv && !input.channelAccessToken && !input.tokenFile) {
return "LINE requires channelAccessToken or --token-file (or --use-env).";
}
if (!input.useEnv && !input.channelSecret && !input.secretFile) {
return "LINE requires channelSecret or --secret-file (or --use-env).";
}
return null;
},
validateInput: createSetupInputPresenceValidator({
defaultAccountOnlyEnvError:
"LINE_CHANNEL_ACCESS_TOKEN can only be used for the default account.",
whenNotUseEnv: [
{
someOf: ["channelAccessToken", "tokenFile"],
message: "LINE requires channelAccessToken or --token-file (or --use-env).",
},
{
someOf: ["channelSecret", "secretFile"],
message: "LINE requires channelSecret or --secret-file (or --use-env).",
},
],
}),
applyAccountConfig: ({ cfg, accountId, input }) => {
const typedInput = input as {

View File

@@ -1,6 +1,5 @@
import type { ChannelSetupAdapter } from "openclaw/plugin-sdk/channel-setup";
import { createZodSetupInputValidator } from "openclaw/plugin-sdk/setup";
import { z } from "zod";
import { createSetupInputPresenceValidator } from "openclaw/plugin-sdk/setup";
import { resolveMattermostAccount, type ResolvedMattermostAccount } from "./mattermost/accounts.js";
import { normalizeMattermostBaseUrl } from "./mattermost/client.js";
import {
@@ -15,15 +14,6 @@ import { hasConfiguredSecretInput } from "./secret-input.js";
const channel = "mattermost" as const;
const MattermostSetupInputSchema = z
.object({
useEnv: z.boolean().optional(),
botToken: z.string().optional(),
token: z.string().optional(),
httpUrl: z.string().optional(),
})
.passthrough();
export function isMattermostConfigured(account: ResolvedMattermostAccount): boolean {
const tokenConfigured =
Boolean(account.botToken?.trim()) || hasConfiguredSecretInput(account.config.botToken);
@@ -47,14 +37,21 @@ export const mattermostSetupAdapter: ChannelSetupAdapter = {
accountId,
name,
}),
validateInput: createZodSetupInputValidator({
schema: MattermostSetupInputSchema,
validateInput: createSetupInputPresenceValidator({
defaultAccountOnlyEnvError: "Mattermost env vars can only be used for the default account.",
whenNotUseEnv: [
{
someOf: ["botToken", "token"],
message: "Mattermost requires --bot-token and --http-url (or --use-env).",
},
{
someOf: ["httpUrl"],
message: "Mattermost requires --bot-token and --http-url (or --use-env).",
},
],
validate: ({ accountId, input }) => {
const token = input.botToken ?? input.token;
const baseUrl = normalizeMattermostBaseUrl(input.httpUrl);
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
return "Mattermost env vars can only be used for the default account.";
}
if (!input.useEnv && (!token || !baseUrl)) {
return "Mattermost requires --bot-token and --http-url (or --use-env).";
}

View File

@@ -3,7 +3,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/routing";
import {
applyAccountNameToChannelSection,
createZodSetupInputValidator,
createSetupInputPresenceValidator,
patchScopedAccountConfig,
} from "openclaw/plugin-sdk/setup";
import {
@@ -17,7 +17,6 @@ import type { ChannelSetupDmPolicy } from "openclaw/plugin-sdk/setup";
import { type ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
import { formatDocsLink } from "openclaw/plugin-sdk/setup";
import type { WizardPrompter } from "openclaw/plugin-sdk/setup";
import { z } from "zod";
import {
listNextcloudTalkAccountIds,
resolveDefaultNextcloudTalkAccountId,
@@ -34,15 +33,6 @@ type NextcloudSetupInput = ChannelSetupInput & {
};
type NextcloudTalkSection = NonNullable<CoreConfig["channels"]>["nextcloud-talk"];
const NextcloudSetupInputSchema = z
.object({
useEnv: z.boolean().optional(),
baseUrl: z.string().optional(),
secret: z.string().optional(),
secretFile: z.string().optional(),
})
.passthrough() as z.ZodType<NextcloudSetupInput>;
export function normalizeNextcloudTalkBaseUrl(value: string | undefined): string {
return value?.trim().replace(/\/+$/, "") ?? "";
}
@@ -192,12 +182,10 @@ export const nextcloudTalkSetupAdapter: ChannelSetupAdapter = {
accountId,
name,
}),
validateInput: createZodSetupInputValidator({
schema: NextcloudSetupInputSchema,
validateInput: createSetupInputPresenceValidator({
defaultAccountOnlyEnvError:
"NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.",
validate: ({ accountId, input }) => {
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
return "NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.";
}
if (!input.useEnv && !input.secret && !input.secretFile) {
return "Nextcloud Talk requires bot secret or --secret-file (or --use-env).";
}

View File

@@ -3,7 +3,7 @@ import {
createDelegatedSetupWizardProxy,
createDelegatedTextInputShouldPrompt,
createPatchedAccountSetupAdapter,
createZodSetupInputValidator,
createSetupInputPresenceValidator,
createTopLevelChannelDmPolicy,
normalizeE164,
parseSetupEntriesAllowingWildcard,
@@ -19,7 +19,6 @@ import type {
ChannelSetupWizardTextInput,
} from "openclaw/plugin-sdk/setup";
import { formatCliCommand, formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
import { z } from "zod";
import {
listSignalAccountIds,
resolveDefaultSignalAccountId,
@@ -33,16 +32,6 @@ const DIGITS_ONLY = /^\d+$/;
const INVALID_SIGNAL_ACCOUNT_ERROR =
"Invalid E.164 phone number (must start with + and country code, e.g. +15555550123)";
const SignalSetupInputSchema = z
.object({
signalNumber: z.string().optional(),
cliPath: z.string().optional(),
httpUrl: z.string().optional(),
httpHost: z.string().optional(),
httpPort: z.string().optional(),
})
.passthrough();
export function normalizeSignalAccountInput(value: string | null | undefined): string | null {
const trimmed = value?.trim();
if (!trimmed) {
@@ -196,8 +185,7 @@ export const signalCompletionNote = {
export const signalSetupAdapter: ChannelSetupAdapter = createPatchedAccountSetupAdapter({
channelKey: channel,
validateInput: createZodSetupInputValidator({
schema: SignalSetupInputSchema,
validateInput: createSetupInputPresenceValidator({
validate: ({ input }) => {
if (
!input.signalNumber &&

View File

@@ -4,13 +4,12 @@ import {
normalizeAccountId,
patchScopedAccountConfig,
prepareScopedSetupConfig,
createZodSetupInputValidator,
createSetupInputPresenceValidator,
type ChannelSetupAdapter,
type ChannelSetupInput,
type ChannelSetupWizard,
type OpenClawConfig,
} from "openclaw/plugin-sdk/setup";
import { z } from "zod";
import { buildTlonAccountFields } from "./account-fields.js";
import { normalizeShip } from "./targets.js";
import { listTlonAccountIds, resolveTlonAccount, type TlonResolvedAccount } from "./types.js";
@@ -31,14 +30,6 @@ export type TlonSetupInput = ChannelSetupInput & {
ownerShip?: string;
};
const TlonSetupInputSchema = z
.object({
ship: z.string().optional(),
url: z.string().optional(),
code: z.string().optional(),
})
.passthrough() as z.ZodType<TlonSetupInput>;
function isConfigured(account: TlonResolvedAccount): boolean {
return Boolean(account.ship && account.url && account.code);
}
@@ -196,8 +187,7 @@ export const tlonSetupAdapter: ChannelSetupAdapter = {
accountId,
name,
}),
validateInput: createZodSetupInputValidator({
schema: TlonSetupInputSchema,
validateInput: createSetupInputPresenceValidator({
validate: ({ cfg, accountId, input }) => {
const resolved = resolveTlonAccount(cfg, accountId ?? undefined);
const ship = input.ship?.trim() || resolved.ship;

View File

@@ -1,33 +1,21 @@
import {
createPatchedAccountSetupAdapter,
createZodSetupInputValidator,
createSetupInputPresenceValidator,
DEFAULT_ACCOUNT_ID,
} from "openclaw/plugin-sdk/setup";
import { z } from "zod";
const channel = "zalo" as const;
const ZaloSetupInputSchema = z
.object({
useEnv: z.boolean().optional(),
token: z.string().optional(),
tokenFile: z.string().optional(),
})
.passthrough();
export const zaloSetupAdapter = createPatchedAccountSetupAdapter({
channelKey: channel,
validateInput: createZodSetupInputValidator({
schema: ZaloSetupInputSchema,
validate: ({ accountId, input }) => {
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
return "ZALO_BOT_TOKEN can only be used for the default account.";
}
if (!input.useEnv && !input.token && !input.tokenFile) {
return "Zalo requires token or --token-file (or --use-env).";
}
return null;
},
validateInput: createSetupInputPresenceValidator({
defaultAccountOnlyEnvError: "ZALO_BOT_TOKEN can only be used for the default account.",
whenNotUseEnv: [
{
someOf: ["token", "tokenFile"],
message: "Zalo requires token or --token-file (or --use-env).",
},
],
}),
buildPatch: (input) =>
input.useEnv

View File

@@ -1,4 +1,4 @@
import type { ZodType } from "zod";
import { z, type ZodType } from "zod";
import type { OpenClawConfig } from "../../config/config.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import type { ChannelSetupAdapter } from "./types.adapters.js";
@@ -224,6 +224,57 @@ export function createZodSetupInputValidator<T extends ChannelSetupInput>(params
};
}
const GenericSetupInputSchema = z
.object({
useEnv: z.boolean().optional(),
})
.passthrough() as ZodType<ChannelSetupInput>;
type SetupInputPresenceRequirement = {
someOf: string[];
message: string;
};
function hasPresentSetupValue(value: unknown): boolean {
if (typeof value === "string") {
return value.trim().length > 0;
}
return value !== undefined && value !== null;
}
export function createSetupInputPresenceValidator(params: {
defaultAccountOnlyEnvError?: string;
whenNotUseEnv?: SetupInputPresenceRequirement[];
validate?: (params: {
cfg: OpenClawConfig;
accountId: string;
input: ChannelSetupInput;
}) => string | null;
}): NonNullable<ChannelSetupAdapter["validateInput"]> {
return createZodSetupInputValidator({
schema: GenericSetupInputSchema,
validate: (inputParams) => {
if (
params.defaultAccountOnlyEnvError &&
inputParams.input.useEnv &&
inputParams.accountId !== DEFAULT_ACCOUNT_ID
) {
return params.defaultAccountOnlyEnvError;
}
if (!inputParams.input.useEnv) {
const inputRecord = inputParams.input as Record<string, unknown>;
for (const requirement of params.whenNotUseEnv ?? []) {
if (requirement.someOf.some((key) => hasPresentSetupValue(inputRecord[key]))) {
continue;
}
return requirement.message;
}
}
return params.validate?.(inputParams) ?? null;
},
});
}
export function createEnvPatchedAccountSetupAdapter(params: {
channelKey: string;
alwaysUseAccounts?: boolean;

View File

@@ -28,6 +28,7 @@ export {
applyAccountNameToChannelSection,
applySetupAccountConfigPatch,
createEnvPatchedAccountSetupAdapter,
createSetupInputPresenceValidator,
createPatchedAccountSetupAdapter,
createZodSetupInputValidator,
migrateBaseNameToDefaultAccount,