mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix: enforce dm allowFrom inheritance across account channels (#27936) (thanks @widingmarcus-cyber)
This commit is contained in:
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Telegram/DM allowlist runtime inheritance: enforce `dmPolicy: "allowlist"` `allowFrom` requirements using effective account-plus-parent config across account-capable channels (Telegram, Discord, Slack, Signal, iMessage, IRC, BlueBubbles, WhatsApp), and align `openclaw doctor` checks to the same inheritance logic so DM traffic is not silently dropped after upgrades. (#27936) Thanks @widingmarcus-cyber.
|
||||
- Delivery queue/recovery backoff: prevent retry starvation by persisting `lastAttemptAt` on failed sends and deferring recovery retries until each entry's `lastAttemptAt + backoff` window is eligible, while continuing to recover ready entries behind deferred ones. Landed from contributor PR #27710 by @Jimmy-xuzimo. Thanks @Jimmy-xuzimo.
|
||||
- Google Chat/Lifecycle: keep Google Chat `startAccount` pending until abort in webhook mode so startup is no longer interpreted as immediate exit, preventing auto-restart loops and webhook-target churn. (#27384) thanks @junsuwhy.
|
||||
- Temp dirs/Linux umask: force `0700` permissions after temp-dir creation and self-heal existing writable temp dirs before trust checks so `umask 0002` installs no longer crash-loop on startup. Landed from contributor PR #27860 by @stakeswky. (#27853) Thanks @stakeswky.
|
||||
|
||||
@@ -1112,23 +1112,40 @@ function detectEmptyAllowlistPolicy(cfg: OpenClawConfig): string[] {
|
||||
const hasEntries = (list?: Array<string | number>) =>
|
||||
Array.isArray(list) && list.map((v) => String(v).trim()).filter(Boolean).length > 0;
|
||||
|
||||
const checkAccount = (account: Record<string, unknown>, prefix: string) => {
|
||||
const checkAccount = (
|
||||
account: Record<string, unknown>,
|
||||
prefix: string,
|
||||
parent?: Record<string, unknown>,
|
||||
) => {
|
||||
const dmEntry = account.dm;
|
||||
const dm =
|
||||
dmEntry && typeof dmEntry === "object" && !Array.isArray(dmEntry)
|
||||
? (dmEntry as Record<string, unknown>)
|
||||
: undefined;
|
||||
const parentDmEntry = parent?.dm;
|
||||
const parentDm =
|
||||
parentDmEntry && typeof parentDmEntry === "object" && !Array.isArray(parentDmEntry)
|
||||
? (parentDmEntry as Record<string, unknown>)
|
||||
: undefined;
|
||||
const dmPolicy =
|
||||
(account.dmPolicy as string | undefined) ?? (dm?.policy as string | undefined) ?? undefined;
|
||||
(account.dmPolicy as string | undefined) ??
|
||||
(dm?.policy as string | undefined) ??
|
||||
(parent?.dmPolicy as string | undefined) ??
|
||||
(parentDm?.policy as string | undefined) ??
|
||||
undefined;
|
||||
|
||||
if (dmPolicy !== "allowlist") {
|
||||
return;
|
||||
}
|
||||
|
||||
const topAllowFrom = account.allowFrom as Array<string | number> | undefined;
|
||||
const topAllowFrom =
|
||||
(account.allowFrom as Array<string | number> | undefined) ??
|
||||
(parent?.allowFrom as Array<string | number> | undefined);
|
||||
const nestedAllowFrom = dm?.allowFrom as Array<string | number> | undefined;
|
||||
const parentNestedAllowFrom = parentDm?.allowFrom as Array<string | number> | undefined;
|
||||
const effectiveAllowFrom = topAllowFrom ?? nestedAllowFrom ?? parentNestedAllowFrom;
|
||||
|
||||
if (hasEntries(topAllowFrom) || hasEntries(nestedAllowFrom)) {
|
||||
if (hasEntries(effectiveAllowFrom)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1153,7 +1170,7 @@ function detectEmptyAllowlistPolicy(cfg: OpenClawConfig): string[] {
|
||||
if (!account || typeof account !== "object") {
|
||||
continue;
|
||||
}
|
||||
checkAccount(account, `channels.${channelName}.accounts.${accountId}`);
|
||||
checkAccount(account, `channels.${channelName}.accounts.${accountId}`, channelConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +1,109 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { validateConfigObject } from "./config.js";
|
||||
|
||||
describe('dmPolicy="allowlist" requires non-empty allowFrom', () => {
|
||||
describe('dmPolicy="allowlist" requires non-empty effective allowFrom', () => {
|
||||
it('rejects telegram dmPolicy="allowlist" without allowFrom', () => {
|
||||
const res = validateConfigObject({
|
||||
channels: { telegram: { dmPolicy: "allowlist", botToken: "fake" } },
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues.some((i) => i.path.includes("allowFrom"))).toBe(true);
|
||||
expect(res.issues.some((i) => i.path.includes("channels.telegram.allowFrom"))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects telegram dmPolicy="allowlist" with empty allowFrom', () => {
|
||||
const res = validateConfigObject({
|
||||
channels: { telegram: { dmPolicy: "allowlist", allowFrom: [], botToken: "fake" } },
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues.some((i) => i.path.includes("allowFrom"))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts telegram dmPolicy="allowlist" with allowFrom entries', () => {
|
||||
const res = validateConfigObject({
|
||||
channels: { telegram: { dmPolicy: "allowlist", allowFrom: ["12345"], botToken: "fake" } },
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts telegram dmPolicy="pairing" without allowFrom', () => {
|
||||
const res = validateConfigObject({
|
||||
channels: { telegram: { dmPolicy: "pairing", botToken: "fake" } },
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects signal dmPolicy="allowlist" without allowFrom', () => {
|
||||
const res = validateConfigObject({
|
||||
channels: { signal: { dmPolicy: "allowlist" } },
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues.some((i) => i.path.includes("allowFrom"))).toBe(true);
|
||||
expect(res.issues.some((i) => i.path.includes("channels.signal.allowFrom"))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts signal dmPolicy="allowlist" with allowFrom entries', () => {
|
||||
const res = validateConfigObject({
|
||||
channels: { signal: { dmPolicy: "allowlist", allowFrom: ["+1234567890"] } },
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects discord dmPolicy="allowlist" without allowFrom', () => {
|
||||
const res = validateConfigObject({
|
||||
channels: { discord: { dmPolicy: "allowlist" } },
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues.some((i) => i.path.includes("allowFrom"))).toBe(true);
|
||||
expect(
|
||||
res.issues.some((i) => i.path.includes("channels.discord") && i.path.includes("allowFrom")),
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts discord dmPolicy="allowlist" with allowFrom entries', () => {
|
||||
const res = validateConfigObject({
|
||||
channels: { discord: { dmPolicy: "allowlist", allowFrom: ["123456789"] } },
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects whatsapp dmPolicy="allowlist" without allowFrom', () => {
|
||||
const res = validateConfigObject({
|
||||
channels: { whatsapp: { dmPolicy: "allowlist" } },
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues.some((i) => i.path.includes("allowFrom"))).toBe(true);
|
||||
expect(res.issues.some((i) => i.path.includes("channels.whatsapp.allowFrom"))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts whatsapp dmPolicy="allowlist" with allowFrom entries', () => {
|
||||
it('accepts dmPolicy="pairing" without allowFrom', () => {
|
||||
const res = validateConfigObject({
|
||||
channels: { whatsapp: { dmPolicy: "allowlist", allowFrom: ["+1234567890"] } },
|
||||
channels: { telegram: { dmPolicy: "pairing", botToken: "fake" } },
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('account dmPolicy="allowlist" uses inherited allowFrom', () => {
|
||||
it("accepts telegram account allowlist when parent allowFrom exists", () => {
|
||||
const res = validateConfigObject({
|
||||
channels: {
|
||||
telegram: {
|
||||
allowFrom: ["12345"],
|
||||
accounts: { bot1: { dmPolicy: "allowlist", botToken: "fake" } },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
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.
|
||||
it("rejects telegram account allowlist when neither account nor parent has allowFrom", () => {
|
||||
const res = validateConfigObject({
|
||||
channels: { telegram: { accounts: { bot1: { dmPolicy: "allowlist", botToken: "fake" } } } },
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(
|
||||
res.issues.some((i) => i.path.includes("channels.telegram.accounts.bot1.allowFrom")),
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts signal account allowlist when parent allowFrom exists", () => {
|
||||
const res = validateConfigObject({
|
||||
channels: {
|
||||
telegram: {
|
||||
signal: { allowFrom: ["+15550001111"], accounts: { work: { dmPolicy: "allowlist" } } },
|
||||
},
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts discord account allowlist when parent allowFrom exists", () => {
|
||||
const res = validateConfigObject({
|
||||
channels: {
|
||||
discord: { allowFrom: ["123456789"], accounts: { work: { dmPolicy: "allowlist" } } },
|
||||
},
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts slack account allowlist when parent allowFrom exists", () => {
|
||||
const res = validateConfigObject({
|
||||
channels: {
|
||||
slack: {
|
||||
allowFrom: ["U123"],
|
||||
botToken: "xoxb-top",
|
||||
appToken: "xapp-top",
|
||||
accounts: {
|
||||
bot1: { dmPolicy: "allowlist", botToken: "fake" },
|
||||
work: { dmPolicy: "allowlist", botToken: "xoxb-work", appToken: "xapp-work" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -102,14 +111,35 @@ describe('dmPolicy="allowlist" requires non-empty allowFrom', () => {
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts telegram account dmPolicy="allowlist" with allowFrom entries', () => {
|
||||
it("accepts whatsapp account allowlist when parent allowFrom exists", () => {
|
||||
const res = validateConfigObject({
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
bot1: { dmPolicy: "allowlist", allowFrom: ["12345"], botToken: "fake" },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+15550001111"], accounts: { work: { dmPolicy: "allowlist" } } },
|
||||
},
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts imessage account allowlist when parent allowFrom exists", () => {
|
||||
const res = validateConfigObject({
|
||||
channels: {
|
||||
imessage: { allowFrom: ["alice"], accounts: { work: { dmPolicy: "allowlist" } } },
|
||||
},
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts irc account allowlist when parent allowFrom exists", () => {
|
||||
const res = validateConfigObject({
|
||||
channels: { irc: { allowFrom: ["nick"], accounts: { work: { dmPolicy: "allowlist" } } } },
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts bluebubbles account allowlist when parent allowFrom exists", () => {
|
||||
const res = validateConfigObject({
|
||||
channels: {
|
||||
bluebubbles: { allowFrom: ["sender"], accounts: { work: { dmPolicy: "allowlist" } } },
|
||||
},
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
@@ -249,6 +249,32 @@ export const TelegramConfigSchema = TelegramAccountSchemaBase.extend({
|
||||
});
|
||||
validateTelegramCustomCommands(value, ctx);
|
||||
|
||||
if (value.accounts) {
|
||||
for (const [accountId, account] of Object.entries(value.accounts)) {
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const effectivePolicy = account.dmPolicy ?? value.dmPolicy;
|
||||
const effectiveAllowFrom = account.allowFrom ?? value.allowFrom;
|
||||
requireOpenAllowFrom({
|
||||
policy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.telegram.accounts.*.dmPolicy="open" requires channels.telegram.accounts.*.allowFrom (or channels.telegram.allowFrom) to include "*"',
|
||||
});
|
||||
requireAllowlistAllowFrom({
|
||||
policy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.telegram.accounts.*.dmPolicy="allowlist" requires channels.telegram.accounts.*.allowFrom (or channels.telegram.allowFrom) to contain at least one sender ID',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const baseWebhookUrl = typeof value.webhookUrl === "string" ? value.webhookUrl.trim() : "";
|
||||
const baseWebhookSecret =
|
||||
typeof value.webhookSecret === "string" ? value.webhookSecret.trim() : "";
|
||||
@@ -501,30 +527,62 @@ export const DiscordAccountSchema = z
|
||||
});
|
||||
}
|
||||
|
||||
const dmPolicy = value.dmPolicy ?? value.dm?.policy ?? "pairing";
|
||||
const allowFrom = value.allowFrom ?? value.dm?.allowFrom;
|
||||
const allowFromPath =
|
||||
value.allowFrom !== undefined ? (["allowFrom"] as const) : (["dm", "allowFrom"] as const);
|
||||
requireOpenAllowFrom({
|
||||
policy: dmPolicy,
|
||||
allowFrom,
|
||||
ctx,
|
||||
path: [...allowFromPath],
|
||||
message:
|
||||
'channels.discord.dmPolicy="open" requires channels.discord.allowFrom (or channels.discord.dm.allowFrom) to include "*"',
|
||||
});
|
||||
requireAllowlistAllowFrom({
|
||||
policy: dmPolicy,
|
||||
allowFrom,
|
||||
ctx,
|
||||
path: [...allowFromPath],
|
||||
message:
|
||||
'channels.discord.dmPolicy="allowlist" requires channels.discord.allowFrom (or channels.discord.dm.allowFrom) to contain at least one sender ID',
|
||||
});
|
||||
// DM allowlist validation is enforced at DiscordConfigSchema so account entries
|
||||
// can inherit top-level allowFrom via runtime shallow merge.
|
||||
});
|
||||
|
||||
export const DiscordConfigSchema = DiscordAccountSchema.extend({
|
||||
accounts: z.record(z.string(), DiscordAccountSchema.optional()).optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
const dmPolicy = value.dmPolicy ?? value.dm?.policy ?? "pairing";
|
||||
const allowFrom = value.allowFrom ?? value.dm?.allowFrom;
|
||||
const allowFromPath =
|
||||
value.allowFrom !== undefined ? (["allowFrom"] as const) : (["dm", "allowFrom"] as const);
|
||||
requireOpenAllowFrom({
|
||||
policy: dmPolicy,
|
||||
allowFrom,
|
||||
ctx,
|
||||
path: [...allowFromPath],
|
||||
message:
|
||||
'channels.discord.dmPolicy="open" requires channels.discord.allowFrom (or channels.discord.dm.allowFrom) to include "*"',
|
||||
});
|
||||
requireAllowlistAllowFrom({
|
||||
policy: dmPolicy,
|
||||
allowFrom,
|
||||
ctx,
|
||||
path: [...allowFromPath],
|
||||
message:
|
||||
'channels.discord.dmPolicy="allowlist" requires channels.discord.allowFrom (or channels.discord.dm.allowFrom) to contain at least one sender ID',
|
||||
});
|
||||
|
||||
if (!value.accounts) {
|
||||
return;
|
||||
}
|
||||
for (const [accountId, account] of Object.entries(value.accounts)) {
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const effectivePolicy =
|
||||
account.dmPolicy ?? account.dm?.policy ?? value.dmPolicy ?? value.dm?.policy ?? "pairing";
|
||||
const effectiveAllowFrom =
|
||||
account.allowFrom ?? account.dm?.allowFrom ?? value.allowFrom ?? value.dm?.allowFrom;
|
||||
requireOpenAllowFrom({
|
||||
policy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.discord.accounts.*.dmPolicy="open" requires channels.discord.accounts.*.allowFrom (or channels.discord.allowFrom) to include "*"',
|
||||
});
|
||||
requireAllowlistAllowFrom({
|
||||
policy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.discord.accounts.*.dmPolicy="allowlist" requires channels.discord.accounts.*.allowFrom (or channels.discord.allowFrom) to contain at least one sender ID',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const GoogleChatDmSchema = z
|
||||
@@ -724,29 +782,11 @@ export const SlackAccountSchema = z
|
||||
ackReaction: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, ctx) => {
|
||||
.superRefine((value) => {
|
||||
normalizeSlackStreamingConfig(value);
|
||||
|
||||
const dmPolicy = value.dmPolicy ?? value.dm?.policy ?? "pairing";
|
||||
const allowFrom = value.allowFrom ?? value.dm?.allowFrom;
|
||||
const allowFromPath =
|
||||
value.allowFrom !== undefined ? (["allowFrom"] as const) : (["dm", "allowFrom"] as const);
|
||||
requireOpenAllowFrom({
|
||||
policy: dmPolicy,
|
||||
allowFrom,
|
||||
ctx,
|
||||
path: [...allowFromPath],
|
||||
message:
|
||||
'channels.slack.dmPolicy="open" requires channels.slack.allowFrom (or channels.slack.dm.allowFrom) to include "*"',
|
||||
});
|
||||
requireAllowlistAllowFrom({
|
||||
policy: dmPolicy,
|
||||
allowFrom,
|
||||
ctx,
|
||||
path: [...allowFromPath],
|
||||
message:
|
||||
'channels.slack.dmPolicy="allowlist" requires channels.slack.allowFrom (or channels.slack.dm.allowFrom) to contain at least one sender ID',
|
||||
});
|
||||
// DM allowlist validation is enforced at SlackConfigSchema so account entries
|
||||
// can inherit top-level allowFrom via runtime shallow merge.
|
||||
});
|
||||
|
||||
export const SlackConfigSchema = SlackAccountSchema.safeExtend({
|
||||
@@ -756,6 +796,27 @@ export const SlackConfigSchema = SlackAccountSchema.safeExtend({
|
||||
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
||||
accounts: z.record(z.string(), SlackAccountSchema.optional()).optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
const dmPolicy = value.dmPolicy ?? value.dm?.policy ?? "pairing";
|
||||
const allowFrom = value.allowFrom ?? value.dm?.allowFrom;
|
||||
const allowFromPath =
|
||||
value.allowFrom !== undefined ? (["allowFrom"] as const) : (["dm", "allowFrom"] as const);
|
||||
requireOpenAllowFrom({
|
||||
policy: dmPolicy,
|
||||
allowFrom,
|
||||
ctx,
|
||||
path: [...allowFromPath],
|
||||
message:
|
||||
'channels.slack.dmPolicy="open" requires channels.slack.allowFrom (or channels.slack.dm.allowFrom) to include "*"',
|
||||
});
|
||||
requireAllowlistAllowFrom({
|
||||
policy: dmPolicy,
|
||||
allowFrom,
|
||||
ctx,
|
||||
path: [...allowFromPath],
|
||||
message:
|
||||
'channels.slack.dmPolicy="allowlist" requires channels.slack.allowFrom (or channels.slack.dm.allowFrom) to contain at least one sender ID',
|
||||
});
|
||||
|
||||
const baseMode = value.mode ?? "socket";
|
||||
if (baseMode === "http" && !value.signingSecret) {
|
||||
ctx.addIssue({
|
||||
@@ -775,6 +836,26 @@ export const SlackConfigSchema = SlackAccountSchema.safeExtend({
|
||||
continue;
|
||||
}
|
||||
const accountMode = account.mode ?? baseMode;
|
||||
const effectivePolicy =
|
||||
account.dmPolicy ?? account.dm?.policy ?? value.dmPolicy ?? value.dm?.policy ?? "pairing";
|
||||
const effectiveAllowFrom =
|
||||
account.allowFrom ?? account.dm?.allowFrom ?? value.allowFrom ?? value.dm?.allowFrom;
|
||||
requireOpenAllowFrom({
|
||||
policy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.slack.accounts.*.dmPolicy="open" requires channels.slack.accounts.*.allowFrom (or channels.slack.allowFrom) to include "*"',
|
||||
});
|
||||
requireAllowlistAllowFrom({
|
||||
policy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.slack.accounts.*.dmPolicy="allowlist" requires channels.slack.accounts.*.allowFrom (or channels.slack.allowFrom) to contain at least one sender ID',
|
||||
});
|
||||
if (accountMode !== "http") {
|
||||
continue;
|
||||
}
|
||||
@@ -858,6 +939,33 @@ export const SignalConfigSchema = SignalAccountSchemaBase.extend({
|
||||
message:
|
||||
'channels.signal.dmPolicy="allowlist" requires channels.signal.allowFrom to contain at least one sender ID',
|
||||
});
|
||||
|
||||
if (!value.accounts) {
|
||||
return;
|
||||
}
|
||||
for (const [accountId, account] of Object.entries(value.accounts)) {
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const effectivePolicy = account.dmPolicy ?? value.dmPolicy;
|
||||
const effectiveAllowFrom = account.allowFrom ?? value.allowFrom;
|
||||
requireOpenAllowFrom({
|
||||
policy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.signal.accounts.*.dmPolicy="open" requires channels.signal.accounts.*.allowFrom (or channels.signal.allowFrom) to include "*"',
|
||||
});
|
||||
requireAllowlistAllowFrom({
|
||||
policy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.signal.accounts.*.dmPolicy="allowlist" requires channels.signal.accounts.*.allowFrom (or channels.signal.allowFrom) to contain at least one sender ID',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const IrcGroupSchema = z
|
||||
@@ -965,6 +1073,32 @@ export const IrcConfigSchema = IrcAccountSchemaBase.extend({
|
||||
accounts: z.record(z.string(), IrcAccountSchema.optional()).optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
refineIrcAllowFromAndNickserv(value, ctx);
|
||||
if (!value.accounts) {
|
||||
return;
|
||||
}
|
||||
for (const [accountId, account] of Object.entries(value.accounts)) {
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const effectivePolicy = account.dmPolicy ?? value.dmPolicy;
|
||||
const effectiveAllowFrom = account.allowFrom ?? value.allowFrom;
|
||||
requireOpenAllowFrom({
|
||||
policy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.irc.accounts.*.dmPolicy="open" requires channels.irc.accounts.*.allowFrom (or channels.irc.allowFrom) to include "*"',
|
||||
});
|
||||
requireAllowlistAllowFrom({
|
||||
policy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.irc.accounts.*.dmPolicy="allowlist" requires channels.irc.accounts.*.allowFrom (or channels.irc.allowFrom) to contain at least one sender ID',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const IMessageAccountSchemaBase = z
|
||||
@@ -1044,6 +1178,33 @@ export const IMessageConfigSchema = IMessageAccountSchemaBase.extend({
|
||||
message:
|
||||
'channels.imessage.dmPolicy="allowlist" requires channels.imessage.allowFrom to contain at least one sender ID',
|
||||
});
|
||||
|
||||
if (!value.accounts) {
|
||||
return;
|
||||
}
|
||||
for (const [accountId, account] of Object.entries(value.accounts)) {
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const effectivePolicy = account.dmPolicy ?? value.dmPolicy;
|
||||
const effectiveAllowFrom = account.allowFrom ?? value.allowFrom;
|
||||
requireOpenAllowFrom({
|
||||
policy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.imessage.accounts.*.dmPolicy="open" requires channels.imessage.accounts.*.allowFrom (or channels.imessage.allowFrom) to include "*"',
|
||||
});
|
||||
requireAllowlistAllowFrom({
|
||||
policy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.imessage.accounts.*.dmPolicy="allowlist" requires channels.imessage.accounts.*.allowFrom (or channels.imessage.allowFrom) to contain at least one sender ID',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const BlueBubblesAllowFromEntry = z.union([z.string(), z.number()]);
|
||||
@@ -1128,6 +1289,33 @@ export const BlueBubblesConfigSchema = BlueBubblesAccountSchemaBase.extend({
|
||||
message:
|
||||
'channels.bluebubbles.dmPolicy="allowlist" requires channels.bluebubbles.allowFrom to contain at least one sender ID',
|
||||
});
|
||||
|
||||
if (!value.accounts) {
|
||||
return;
|
||||
}
|
||||
for (const [accountId, account] of Object.entries(value.accounts)) {
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const effectivePolicy = account.dmPolicy ?? value.dmPolicy;
|
||||
const effectiveAllowFrom = account.allowFrom ?? value.allowFrom;
|
||||
requireOpenAllowFrom({
|
||||
policy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.bluebubbles.accounts.*.dmPolicy="open" requires channels.bluebubbles.accounts.*.allowFrom (or channels.bluebubbles.allowFrom) to include "*"',
|
||||
});
|
||||
requireAllowlistAllowFrom({
|
||||
policy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.bluebubbles.accounts.*.dmPolicy="allowlist" requires channels.bluebubbles.accounts.*.allowFrom (or channels.bluebubbles.allowFrom) to contain at least one sender ID',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const MSTeamsChannelSchema = z
|
||||
|
||||
@@ -63,6 +63,7 @@ function enforceOpenDmPolicyAllowFromStar(params: {
|
||||
allowFrom: unknown;
|
||||
ctx: z.RefinementCtx;
|
||||
message: string;
|
||||
path?: Array<string | number>;
|
||||
}) {
|
||||
if (params.dmPolicy !== "open") {
|
||||
return;
|
||||
@@ -75,7 +76,7 @@ function enforceOpenDmPolicyAllowFromStar(params: {
|
||||
}
|
||||
params.ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["allowFrom"],
|
||||
path: params.path ?? ["allowFrom"],
|
||||
message: params.message,
|
||||
});
|
||||
}
|
||||
@@ -85,6 +86,7 @@ function enforceAllowlistDmPolicyAllowFrom(params: {
|
||||
allowFrom: unknown;
|
||||
ctx: z.RefinementCtx;
|
||||
message: string;
|
||||
path?: Array<string | number>;
|
||||
}) {
|
||||
if (params.dmPolicy !== "allowlist") {
|
||||
return;
|
||||
@@ -97,7 +99,7 @@ function enforceAllowlistDmPolicyAllowFrom(params: {
|
||||
}
|
||||
params.ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["allowFrom"],
|
||||
path: params.path ?? ["allowFrom"],
|
||||
message: params.message,
|
||||
});
|
||||
}
|
||||
@@ -108,23 +110,7 @@ export const WhatsAppAccountSchema = WhatsAppSharedSchema.extend({
|
||||
/** Override auth directory for this WhatsApp account (Baileys multi-file auth state). */
|
||||
authDir: z.string().optional(),
|
||||
mediaMaxMb: z.number().int().positive().optional(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, ctx) => {
|
||||
enforceOpenDmPolicyAllowFromStar({
|
||||
dmPolicy: value.dmPolicy,
|
||||
allowFrom: value.allowFrom,
|
||||
ctx,
|
||||
message: 'channels.whatsapp.accounts.*.dmPolicy="open" requires allowFrom to include "*"',
|
||||
});
|
||||
enforceAllowlistDmPolicyAllowFrom({
|
||||
dmPolicy: value.dmPolicy,
|
||||
allowFrom: value.allowFrom,
|
||||
ctx,
|
||||
message:
|
||||
'channels.whatsapp.accounts.*.dmPolicy="allowlist" requires allowFrom to contain at least one sender ID',
|
||||
});
|
||||
});
|
||||
}).strict();
|
||||
|
||||
export const WhatsAppConfigSchema = WhatsAppSharedSchema.extend({
|
||||
accounts: z.record(z.string(), WhatsAppAccountSchema.optional()).optional(),
|
||||
@@ -154,4 +140,30 @@ export const WhatsAppConfigSchema = WhatsAppSharedSchema.extend({
|
||||
message:
|
||||
'channels.whatsapp.dmPolicy="allowlist" requires channels.whatsapp.allowFrom to contain at least one sender ID',
|
||||
});
|
||||
if (!value.accounts) {
|
||||
return;
|
||||
}
|
||||
for (const [accountId, account] of Object.entries(value.accounts)) {
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const effectivePolicy = account.dmPolicy ?? value.dmPolicy;
|
||||
const effectiveAllowFrom = account.allowFrom ?? value.allowFrom;
|
||||
enforceOpenDmPolicyAllowFromStar({
|
||||
dmPolicy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.whatsapp.accounts.*.dmPolicy="open" requires channels.whatsapp.accounts.*.allowFrom (or channels.whatsapp.allowFrom) to include "*"',
|
||||
});
|
||||
enforceAllowlistDmPolicyAllowFrom({
|
||||
dmPolicy: effectivePolicy,
|
||||
allowFrom: effectiveAllowFrom,
|
||||
ctx,
|
||||
path: ["accounts", accountId, "allowFrom"],
|
||||
message:
|
||||
'channels.whatsapp.accounts.*.dmPolicy="allowlist" requires channels.whatsapp.accounts.*.allowFrom (or channels.whatsapp.allowFrom) to contain at least one sender ID',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user