refactor(zalo): split monitor access and webhook logic

This commit is contained in:
Peter Steinberger
2026-02-24 23:40:21 +00:00
parent 58309fd8d9
commit 453664f09d
8 changed files with 486 additions and 289 deletions

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { isAllowedParsedChatSender } from "./allow-from.js";
import { isAllowedParsedChatSender, isNormalizedSenderAllowed } from "./allow-from.js";
function parseAllowTarget(
entry: string,
@@ -71,3 +71,34 @@ describe("isAllowedParsedChatSender", () => {
expect(allowed).toBe(true);
});
});
describe("isNormalizedSenderAllowed", () => {
it("allows wildcard", () => {
expect(
isNormalizedSenderAllowed({
senderId: "attacker",
allowFrom: ["*"],
}),
).toBe(true);
});
it("normalizes case and strips prefixes", () => {
expect(
isNormalizedSenderAllowed({
senderId: "12345",
allowFrom: ["ZALO:12345", "zl:777"],
stripPrefixRe: /^(zalo|zl):/i,
}),
).toBe(true);
});
it("rejects when sender is missing", () => {
expect(
isNormalizedSenderAllowed({
senderId: "999",
allowFrom: ["zl:12345"],
stripPrefixRe: /^(zalo|zl):/i,
}),
).toBe(false);
});
});

View File

@@ -9,6 +9,25 @@ export function formatAllowFromLowercase(params: {
.map((entry) => entry.toLowerCase());
}
export function isNormalizedSenderAllowed(params: {
senderId: string | number;
allowFrom: Array<string | number>;
stripPrefixRe?: RegExp;
}): boolean {
const normalizedAllow = formatAllowFromLowercase({
allowFrom: params.allowFrom,
stripPrefixRe: params.stripPrefixRe,
});
if (normalizedAllow.length === 0) {
return false;
}
if (normalizedAllow.includes("*")) {
return true;
}
const sender = String(params.senderId).trim().toLowerCase();
return normalizedAllow.includes(sender);
}
type ParsedChatAllowTarget =
| { kind: "chat_id"; chatId: number }
| { kind: "chat_guid"; chatGuid: string }

View File

@@ -0,0 +1,69 @@
import { describe, expect, it } from "vitest";
import { evaluateSenderGroupAccess } from "./group-access.js";
describe("evaluateSenderGroupAccess", () => {
it("defaults missing provider config to allowlist", () => {
const decision = evaluateSenderGroupAccess({
providerConfigPresent: false,
configuredGroupPolicy: undefined,
defaultGroupPolicy: "open",
groupAllowFrom: ["123"],
senderId: "123",
isSenderAllowed: () => true,
});
expect(decision).toEqual({
allowed: true,
groupPolicy: "allowlist",
providerMissingFallbackApplied: true,
reason: "allowed",
});
});
it("blocks disabled policy", () => {
const decision = evaluateSenderGroupAccess({
providerConfigPresent: true,
configuredGroupPolicy: "disabled",
defaultGroupPolicy: "open",
groupAllowFrom: ["123"],
senderId: "123",
isSenderAllowed: () => true,
});
expect(decision).toMatchObject({ allowed: false, reason: "disabled", groupPolicy: "disabled" });
});
it("blocks allowlist with empty list", () => {
const decision = evaluateSenderGroupAccess({
providerConfigPresent: true,
configuredGroupPolicy: "allowlist",
defaultGroupPolicy: "open",
groupAllowFrom: [],
senderId: "123",
isSenderAllowed: () => true,
});
expect(decision).toMatchObject({
allowed: false,
reason: "empty_allowlist",
groupPolicy: "allowlist",
});
});
it("blocks sender not allowlisted", () => {
const decision = evaluateSenderGroupAccess({
providerConfigPresent: true,
configuredGroupPolicy: "allowlist",
defaultGroupPolicy: "open",
groupAllowFrom: ["123"],
senderId: "999",
isSenderAllowed: () => false,
});
expect(decision).toMatchObject({
allowed: false,
reason: "sender_not_allowlisted",
groupPolicy: "allowlist",
});
});
});

View File

@@ -0,0 +1,64 @@
import { resolveOpenProviderRuntimeGroupPolicy } from "../config/runtime-group-policy.js";
import type { GroupPolicy } from "../config/types.base.js";
export type SenderGroupAccessReason =
| "allowed"
| "disabled"
| "empty_allowlist"
| "sender_not_allowlisted";
export type SenderGroupAccessDecision = {
allowed: boolean;
groupPolicy: GroupPolicy;
providerMissingFallbackApplied: boolean;
reason: SenderGroupAccessReason;
};
export function evaluateSenderGroupAccess(params: {
providerConfigPresent: boolean;
configuredGroupPolicy?: GroupPolicy;
defaultGroupPolicy?: GroupPolicy;
groupAllowFrom: string[];
senderId: string;
isSenderAllowed: (senderId: string, allowFrom: string[]) => boolean;
}): SenderGroupAccessDecision {
const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: params.providerConfigPresent,
groupPolicy: params.configuredGroupPolicy,
defaultGroupPolicy: params.defaultGroupPolicy,
});
if (groupPolicy === "disabled") {
return {
allowed: false,
groupPolicy,
providerMissingFallbackApplied,
reason: "disabled",
};
}
if (groupPolicy === "allowlist") {
if (params.groupAllowFrom.length === 0) {
return {
allowed: false,
groupPolicy,
providerMissingFallbackApplied,
reason: "empty_allowlist",
};
}
if (!params.isSenderAllowed(params.senderId, params.groupAllowFrom)) {
return {
allowed: false,
groupPolicy,
providerMissingFallbackApplied,
reason: "sender_not_allowlisted",
};
}
}
return {
allowed: true,
groupPolicy,
providerMissingFallbackApplied,
reason: "allowed",
};
}

View File

@@ -181,7 +181,16 @@ export {
normalizeAccountId,
resolveThreadSessionKeys,
} from "../routing/session-key.js";
export { formatAllowFromLowercase, isAllowedParsedChatSender } from "./allow-from.js";
export {
formatAllowFromLowercase,
isAllowedParsedChatSender,
isNormalizedSenderAllowed,
} from "./allow-from.js";
export {
evaluateSenderGroupAccess,
type SenderGroupAccessDecision,
type SenderGroupAccessReason,
} from "./group-access.js";
export { resolveSenderCommandAuthorization } from "./command-auth.js";
export { handleSlackMessageAction } from "./slack-message-actions.js";
export { extractToolSend } from "./tool-send.js";