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.
This commit is contained in:
Marcus Widing
2026-02-26 22:51:13 +01:00
committed by Peter Steinberger
parent cbed0e065c
commit 0fdac31383
2 changed files with 31 additions and 74 deletions

View File

@@ -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', () => {

View File

@@ -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(),