From 0fdac31383bb2d3fbc46e785d78bf45bac49e4d2 Mon Sep 17 00:00:00 2001 From: Marcus Widing Date: Thu, 26 Feb 2026 22:51:13 +0100 Subject: [PATCH] fix: skip allowFrom validation at account level (inherits from parent) Account configs inherit channel-level fields at runtime (e.g., resolveTelegramAccount shallow-merges top-level and account values). An account can set dmPolicy='allowlist' and rely on the parent's allowFrom, so validating allowFrom on the account object alone incorrectly rejects valid multi-account configs. Removes requireAllowlistAllowFrom and requireOpenAllowFrom from all account-level schemas (Telegram, Signal, IRC, iMessage, BlueBubbles). Top-level config schemas still enforce the validation. Addresses Codex review feedback on #27936. --- ...onfig.allowlist-requires-allowfrom.test.ts | 9 +- src/config/zod-schema.providers-core.ts | 96 ++++++------------- 2 files changed, 31 insertions(+), 74 deletions(-) diff --git a/src/config/config.allowlist-requires-allowfrom.test.ts b/src/config/config.allowlist-requires-allowfrom.test.ts index ab5b75fb957..a12973a0cd4 100644 --- a/src/config/config.allowlist-requires-allowfrom.test.ts +++ b/src/config/config.allowlist-requires-allowfrom.test.ts @@ -87,7 +87,9 @@ describe('dmPolicy="allowlist" requires non-empty allowFrom', () => { expect(res.ok).toBe(true); }); - it('rejects telegram account dmPolicy="allowlist" without allowFrom', () => { + it('accepts telegram account dmPolicy="allowlist" without own allowFrom (inherits from parent)', () => { + // Account-level schemas skip allowFrom validation because accounts inherit + // allowFrom from the parent channel config at runtime. const res = validateConfigObject({ channels: { telegram: { @@ -97,10 +99,7 @@ describe('dmPolicy="allowlist" requires non-empty allowFrom', () => { }, }, }); - expect(res.ok).toBe(false); - if (!res.ok) { - expect(res.issues.some((i) => i.path.includes("allowFrom"))).toBe(true); - } + expect(res.ok).toBe(true); }); it('accepts telegram account dmPolicy="allowlist" with allowFrom entries', () => { diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 63e39ead5ff..1f0799f782c 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -220,22 +220,10 @@ export const TelegramAccountSchemaBase = z export const TelegramAccountSchema = TelegramAccountSchemaBase.superRefine((value, ctx) => { normalizeTelegramStreamingConfig(value); - requireOpenAllowFrom({ - policy: value.dmPolicy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: - 'channels.telegram.dmPolicy="open" requires channels.telegram.allowFrom to include "*"', - }); - requireAllowlistAllowFrom({ - policy: value.dmPolicy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: - 'channels.telegram.dmPolicy="allowlist" requires channels.telegram.allowFrom to contain at least one sender ID', - }); + // Account-level schemas skip allowFrom validation because accounts inherit + // allowFrom from the parent channel config at runtime (resolveTelegramAccount + // shallow-merges top-level and account values in src/telegram/accounts.ts). + // Validation is enforced at the top-level TelegramConfigSchema instead. validateTelegramCustomCommands(value, ctx); }); @@ -847,23 +835,10 @@ export const SignalAccountSchemaBase = z }) .strict(); -export const SignalAccountSchema = SignalAccountSchemaBase.superRefine((value, ctx) => { - requireOpenAllowFrom({ - policy: value.dmPolicy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: 'channels.signal.dmPolicy="open" requires channels.signal.allowFrom to include "*"', - }); - requireAllowlistAllowFrom({ - policy: value.dmPolicy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: - 'channels.signal.dmPolicy="allowlist" requires channels.signal.allowFrom to contain at least one sender ID', - }); -}); +// Account-level schemas skip allowFrom validation because accounts inherit +// allowFrom from the parent channel config at runtime. +// Validation is enforced at the top-level SignalConfigSchema instead. +export const SignalAccountSchema = SignalAccountSchemaBase; export const SignalConfigSchema = SignalAccountSchemaBase.extend({ accounts: z.record(z.string(), SignalAccountSchema.optional()).optional(), @@ -972,8 +947,18 @@ function refineIrcAllowFromAndNickserv(value: IrcBaseConfig, ctx: z.RefinementCt } } +// Account-level schemas skip allowFrom validation because accounts inherit +// allowFrom from the parent channel config at runtime. +// Validation is enforced at the top-level IrcConfigSchema instead. export const IrcAccountSchema = IrcAccountSchemaBase.superRefine((value, ctx) => { - refineIrcAllowFromAndNickserv(value, ctx); + // Only validate nickserv at account level, not allowFrom (inherited from parent). + if (value.nickserv?.register && !value.nickserv.registerEmail?.trim()) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["nickserv", "registerEmail"], + message: "channels.irc.nickserv.register=true requires channels.irc.nickserv.registerEmail", + }); + } }); export const IrcConfigSchema = IrcAccountSchemaBase.extend({ @@ -1035,24 +1020,10 @@ export const IMessageAccountSchemaBase = z }) .strict(); -export const IMessageAccountSchema = IMessageAccountSchemaBase.superRefine((value, ctx) => { - requireOpenAllowFrom({ - policy: value.dmPolicy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: - 'channels.imessage.dmPolicy="open" requires channels.imessage.allowFrom to include "*"', - }); - requireAllowlistAllowFrom({ - policy: value.dmPolicy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: - 'channels.imessage.dmPolicy="allowlist" requires channels.imessage.allowFrom to contain at least one sender ID', - }); -}); +// Account-level schemas skip allowFrom validation because accounts inherit +// allowFrom from the parent channel config at runtime. +// Validation is enforced at the top-level IMessageConfigSchema instead. +export const IMessageAccountSchema = IMessageAccountSchemaBase; export const IMessageConfigSchema = IMessageAccountSchemaBase.extend({ accounts: z.record(z.string(), IMessageAccountSchema.optional()).optional(), @@ -1132,23 +1103,10 @@ export const BlueBubblesAccountSchemaBase = z }) .strict(); -export const BlueBubblesAccountSchema = BlueBubblesAccountSchemaBase.superRefine((value, ctx) => { - requireOpenAllowFrom({ - policy: value.dmPolicy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: 'channels.bluebubbles.accounts.*.dmPolicy="open" requires allowFrom to include "*"', - }); - requireAllowlistAllowFrom({ - policy: value.dmPolicy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: - 'channels.bluebubbles.accounts.*.dmPolicy="allowlist" requires allowFrom to contain at least one sender ID', - }); -}); +// Account-level schemas skip allowFrom validation because accounts inherit +// allowFrom from the parent channel config at runtime. +// Validation is enforced at the top-level BlueBubblesConfigSchema instead. +export const BlueBubblesAccountSchema = BlueBubblesAccountSchemaBase; export const BlueBubblesConfigSchema = BlueBubblesAccountSchemaBase.extend({ accounts: z.record(z.string(), BlueBubblesAccountSchema.optional()).optional(),