mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 20:25:16 +00:00
fix: validate discord component numeric limits
This commit is contained in:
@@ -82,8 +82,6 @@ function createModalFieldComponent(
|
||||
customId = field.id;
|
||||
override options = options;
|
||||
override required = field.required;
|
||||
override minValues = field.minValues;
|
||||
override maxValues = field.maxValues;
|
||||
}
|
||||
return new DynamicRadioGroup();
|
||||
}
|
||||
|
||||
@@ -62,10 +62,23 @@ function readOptionalStringArray(value: unknown, label: string): string[] | unde
|
||||
return value.map((entry, index) => readString(entry, `${label}[${index}]`));
|
||||
}
|
||||
|
||||
function readOptionalNumber(value: unknown): number | undefined {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
function readOptionalInteger(
|
||||
value: unknown,
|
||||
label: string,
|
||||
bounds?: { min?: number; max?: number },
|
||||
): number | undefined {
|
||||
if (value == null) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
|
||||
throw new Error(`${label} must be an integer`);
|
||||
}
|
||||
if (bounds?.min !== undefined && value < bounds.min) {
|
||||
throw new Error(`${label} must be at least ${bounds.min}`);
|
||||
}
|
||||
if (bounds?.max !== undefined && value > bounds.max) {
|
||||
throw new Error(`${label} must be at most ${bounds.max}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -197,8 +210,8 @@ function parseSelectSpec(raw: unknown, label: string): DiscordComponentSelectSpe
|
||||
type,
|
||||
callbackData: readOptionalString(obj.callbackData),
|
||||
placeholder: readOptionalString(obj.placeholder),
|
||||
minValues: readOptionalNumber(obj.minValues),
|
||||
maxValues: readOptionalNumber(obj.maxValues),
|
||||
minValues: readOptionalInteger(obj.minValues, `${label}.minValues`, { min: 0, max: 25 }),
|
||||
maxValues: readOptionalInteger(obj.maxValues, `${label}.maxValues`, { min: 1, max: 25 }),
|
||||
options: parseSelectOptions(obj.options, `${label}.options`),
|
||||
allowedUsers: readOptionalStringArray(obj.allowedUsers, `${label}.allowedUsers`),
|
||||
};
|
||||
@@ -224,18 +237,29 @@ function parseModalField(raw: unknown, label: string, index: number): DiscordMod
|
||||
if (["checkbox", "radio", "select"].includes(type) && (!options || options.length === 0)) {
|
||||
throw new Error(`${label}.options is required for ${type} fields`);
|
||||
}
|
||||
if (type === "radio" && (obj.minValues != null || obj.maxValues != null)) {
|
||||
throw new Error(`${label}.minValues/maxValues are not supported for radio fields`);
|
||||
}
|
||||
const required = typeof obj.required === "boolean" ? obj.required : undefined;
|
||||
const maxValues = type === "checkbox" ? 10 : 25;
|
||||
return {
|
||||
type,
|
||||
name: normalizeModalFieldName(readOptionalString(obj.name), index),
|
||||
label: readString(obj.label, `${label}.label`),
|
||||
description: readOptionalString(obj.description),
|
||||
placeholder: readOptionalString(obj.placeholder),
|
||||
required: typeof obj.required === "boolean" ? obj.required : undefined,
|
||||
required,
|
||||
options,
|
||||
minValues: readOptionalNumber(obj.minValues),
|
||||
maxValues: readOptionalNumber(obj.maxValues),
|
||||
minLength: readOptionalNumber(obj.minLength),
|
||||
maxLength: readOptionalNumber(obj.maxLength),
|
||||
minValues: readOptionalInteger(obj.minValues, `${label}.minValues`, {
|
||||
min: required === false ? 0 : 1,
|
||||
max: maxValues,
|
||||
}),
|
||||
maxValues: readOptionalInteger(obj.maxValues, `${label}.maxValues`, {
|
||||
min: 1,
|
||||
max: maxValues,
|
||||
}),
|
||||
minLength: readOptionalInteger(obj.minLength, `${label}.minLength`, { min: 0, max: 4000 }),
|
||||
maxLength: readOptionalInteger(obj.maxLength, `${label}.maxLength`, { min: 1, max: 4000 }),
|
||||
style: readOptionalString(obj.style) as DiscordModalFieldSpec["style"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,6 +106,95 @@ describe("discord components", () => {
|
||||
).toThrow("options");
|
||||
});
|
||||
|
||||
it("rejects malformed component count and length limits", () => {
|
||||
expect(() =>
|
||||
readDiscordComponentSpec({
|
||||
blocks: [
|
||||
{
|
||||
type: "actions",
|
||||
select: {
|
||||
type: "string",
|
||||
minValues: -1,
|
||||
options: [{ label: "One", value: "one" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrow("components.blocks[0].select.minValues");
|
||||
|
||||
expect(() =>
|
||||
readDiscordComponentSpec({
|
||||
modal: {
|
||||
title: "Details",
|
||||
fields: [{ type: "text", label: "Name", maxLength: 0 }],
|
||||
},
|
||||
}),
|
||||
).toThrow("components.modal.fields[0].maxLength");
|
||||
|
||||
expect(() =>
|
||||
readDiscordComponentSpec({
|
||||
modal: {
|
||||
title: "Details",
|
||||
fields: [
|
||||
{
|
||||
type: "select",
|
||||
label: "Priority",
|
||||
minValues: 0,
|
||||
options: [{ label: "High", value: "high" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toThrow("components.modal.fields[0].minValues");
|
||||
|
||||
expect(() =>
|
||||
readDiscordComponentSpec({
|
||||
modal: {
|
||||
title: "Details",
|
||||
fields: [
|
||||
{
|
||||
type: "checkbox",
|
||||
label: "Choices",
|
||||
maxValues: 25,
|
||||
options: [{ label: "One", value: "one" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toThrow("components.modal.fields[0].maxValues");
|
||||
|
||||
expect(() =>
|
||||
readDiscordComponentSpec({
|
||||
blocks: [
|
||||
{
|
||||
type: "actions",
|
||||
select: {
|
||||
type: "string",
|
||||
maxValues: 0,
|
||||
options: [{ label: "One", value: "one" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrow("components.blocks[0].select.maxValues");
|
||||
|
||||
expect(() =>
|
||||
readDiscordComponentSpec({
|
||||
modal: {
|
||||
title: "Details",
|
||||
fields: [
|
||||
{
|
||||
type: "radio",
|
||||
label: "Choice",
|
||||
minValues: 1,
|
||||
options: [{ label: "One", value: "one" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toThrow("components.modal.fields[0].minValues/maxValues");
|
||||
});
|
||||
|
||||
it("requires attachment references for file blocks", () => {
|
||||
expect(() =>
|
||||
readDiscordComponentSpec({
|
||||
|
||||
Reference in New Issue
Block a user