fix(telegram): require sender ids for allowlist auth

This commit is contained in:
Peter Steinberger
2026-02-14 16:08:41 +01:00
parent 09e1cbc35d
commit e3b432e481
10 changed files with 170 additions and 33 deletions

View File

@@ -2,12 +2,34 @@ import type { AllowlistMatch } from "../channels/allowlist-match.js";
export type NormalizedAllowFrom = {
entries: string[];
entriesLower: string[];
hasWildcard: boolean;
hasEntries: boolean;
invalidEntries: string[];
};
export type AllowFromMatch = AllowlistMatch<"wildcard" | "id" | "username">;
export type AllowFromMatch = AllowlistMatch<"wildcard" | "id">;
const warnedInvalidEntries = new Set<string>();
function warnInvalidAllowFromEntries(entries: string[]) {
if (process.env.VITEST || process.env.NODE_ENV === "test") {
return;
}
for (const entry of entries) {
if (warnedInvalidEntries.has(entry)) {
continue;
}
warnedInvalidEntries.add(entry);
console.warn(
[
"[telegram] Invalid allowFrom entry:",
JSON.stringify(entry),
"- allowFrom/groupAllowFrom authorization requires numeric Telegram sender IDs only.",
'If you had "@username" entries, re-run onboarding (it resolves @username to IDs) or replace them manually.',
].join(" "),
);
}
}
export const normalizeAllowFrom = (list?: Array<string | number>): NormalizedAllowFrom => {
const entries = (list ?? []).map((value) => String(value).trim()).filter(Boolean);
@@ -15,12 +37,16 @@ export const normalizeAllowFrom = (list?: Array<string | number>): NormalizedAll
const normalized = entries
.filter((value) => value !== "*")
.map((value) => value.replace(/^(telegram|tg):/i, ""));
const normalizedLower = normalized.map((value) => value.toLowerCase());
const invalidEntries = normalized.filter((value) => !/^\d+$/.test(value));
if (invalidEntries.length > 0) {
warnInvalidAllowFromEntries([...new Set(invalidEntries)]);
}
const ids = normalized.filter((value) => /^\d+$/.test(value));
return {
entries: normalized,
entriesLower: normalizedLower,
entries: ids,
hasWildcard,
hasEntries: entries.length > 0,
invalidEntries,
};
};
@@ -48,7 +74,7 @@ export const isSenderAllowed = (params: {
senderId?: string;
senderUsername?: string;
}) => {
const { allow, senderId, senderUsername } = params;
const { allow, senderId } = params;
if (!allow.hasEntries) {
return true;
}
@@ -58,11 +84,7 @@ export const isSenderAllowed = (params: {
if (senderId && allow.entries.includes(senderId)) {
return true;
}
const username = senderUsername?.toLowerCase();
if (!username) {
return false;
}
return allow.entriesLower.some((entry) => entry === username || entry === `@${username}`);
return false;
};
export const resolveSenderAllowMatch = (params: {
@@ -70,7 +92,7 @@ export const resolveSenderAllowMatch = (params: {
senderId?: string;
senderUsername?: string;
}): AllowFromMatch => {
const { allow, senderId, senderUsername } = params;
const { allow, senderId } = params;
if (allow.hasWildcard) {
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
}
@@ -80,15 +102,5 @@ export const resolveSenderAllowMatch = (params: {
if (senderId && allow.entries.includes(senderId)) {
return { allowed: true, matchKey: senderId, matchSource: "id" };
}
const username = senderUsername?.toLowerCase();
if (!username) {
return { allowed: false };
}
const entry = allow.entriesLower.find(
(candidate) => candidate === username || candidate === `@${username}`,
);
if (entry) {
return { allowed: true, matchKey: entry, matchSource: "username" };
}
return { allowed: false };
};

View File

@@ -248,7 +248,7 @@ describe("createTelegramBot", () => {
expect(replySpy).toHaveBeenCalledTimes(1);
});
it("allows group messages from senders in allowFrom (by username) when groupPolicy is 'allowlist'", async () => {
it("blocks group messages when allowFrom is configured with @username entries (numeric IDs required)", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
replySpy.mockReset();
@@ -276,7 +276,7 @@ describe("createTelegramBot", () => {
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
expect(replySpy).toHaveBeenCalledTimes(0);
});
it("allows group messages from telegram:-prefixed allowFrom entries when groupPolicy is 'allowlist'", async () => {
onSpy.mockReset();

View File

@@ -159,7 +159,7 @@ describe("createTelegramBot", () => {
// groupPolicy tests
it("matches usernames case-insensitively when groupPolicy is 'allowlist'", async () => {
it("blocks @username allowFrom entries when groupPolicy is 'allowlist' (numeric IDs required)", async () => {
onSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
replySpy.mockReset();
@@ -187,7 +187,7 @@ describe("createTelegramBot", () => {
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
expect(replySpy).toHaveBeenCalledTimes(0);
});
it("allows direct messages regardless of groupPolicy", async () => {
onSpy.mockReset();