fix: validate feishu action count params

This commit is contained in:
Peter Steinberger
2026-05-28 15:55:14 -04:00
parent 9cb4e48018
commit fcbc254d0d
2 changed files with 59 additions and 8 deletions

View File

@@ -1041,6 +1041,49 @@ describe("feishuPlugin actions", () => {
expect(requireRecord(peers[0], "peer").id).toBe("ou_1");
});
it("ignores malformed channel-list limits", async () => {
listFeishuDirectoryGroupsLiveMock.mockResolvedValueOnce([{ kind: "group", id: "oc_group_1" }]);
await feishuPlugin.actions?.handleAction?.({
action: "channel-list",
params: { query: "eng", limit: "-1", scope: "groups" },
cfg,
accountId: undefined,
} as never);
expect(listFeishuDirectoryGroupsLiveMock).toHaveBeenCalledWith({
cfg,
query: "eng",
limit: undefined,
fallbackToStatic: false,
accountId: undefined,
});
});
it("ignores non-decimal Feishu action page sizes", async () => {
getChatMembersMock.mockResolvedValueOnce({
chat_id: "oc_group_1",
members: [],
has_more: false,
});
await feishuPlugin.actions?.handleAction?.({
action: "member-info",
params: { chatId: "oc_group_1", pageSize: "0x10" },
cfg,
accountId: undefined,
toolContext: {},
} as never);
expect(getChatMembersMock).toHaveBeenCalledWith(
{ tag: "client" },
"oc_group_1",
undefined,
undefined,
"open_id",
);
});
it("fails channel-list when live discovery fails", async () => {
listFeishuDirectoryGroupsLiveMock.mockRejectedValueOnce(new Error("token expired"));

View File

@@ -556,15 +556,23 @@ function readFirstString(
return undefined;
}
function readOptionalNumber(params: Record<string, unknown>, keys: string[]): number | undefined {
function isPositiveSafeInteger(value: number): boolean {
return Number.isSafeInteger(value) && value > 0;
}
function readOptionalPositiveInteger(
params: Record<string, unknown>,
keys: string[],
): number | undefined {
for (const key of keys) {
const value = params[key];
if (typeof value === "number" && Number.isFinite(value)) {
if (typeof value === "number" && isPositiveSafeInteger(value)) {
return value;
}
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
const trimmed = value.trim();
const parsed = /^\d+$/.test(trimmed) ? Number(trimmed) : Number.NaN;
if (isPositiveSafeInteger(parsed)) {
return parsed;
}
}
@@ -939,7 +947,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
chatId,
startTime: readFirstString(ctx.params, ["startTime", "start_time"]),
endTime: readFirstString(ctx.params, ["endTime", "end_time"]),
pageSize: readOptionalNumber(ctx.params, ["pageSize", "page_size"]),
pageSize: readOptionalPositiveInteger(ctx.params, ["pageSize", "page_size"]),
pageToken: readFirstString(ctx.params, ["pageToken", "page_token"]),
accountId: ctx.accountId ?? undefined,
});
@@ -972,7 +980,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
const members = await runtime.getChatMembers(
client,
chatId,
readOptionalNumber(ctx.params, ["pageSize", "page_size"]),
readOptionalPositiveInteger(ctx.params, ["pageSize", "page_size"]),
readFirstString(ctx.params, ["pageToken", "page_token"]),
resolveFeishuMemberIdType(ctx.params),
);
@@ -1009,7 +1017,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
const members = await runtime.getChatMembers(
client,
chatId,
readOptionalNumber(ctx.params, ["pageSize", "page_size"]),
readOptionalPositiveInteger(ctx.params, ["pageSize", "page_size"]),
readFirstString(ctx.params, ["pageToken", "page_token"]),
resolveFeishuMemberIdType(ctx.params),
);
@@ -1024,7 +1032,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
if (ctx.action === "channel-list") {
const runtime = await loadFeishuChannelRuntime();
const query = readFirstString(ctx.params, ["query"]);
const limit = readOptionalNumber(ctx.params, ["limit"]);
const limit = readOptionalPositiveInteger(ctx.params, ["limit"]);
const scope = readFirstString(ctx.params, ["scope", "kind"]) ?? "all";
if (
scope === "groups" ||