From d076153fc91ea24b6baba91b7b0c3eda3bdef306 Mon Sep 17 00:00:00 2001 From: Moliendo Date: Wed, 1 Apr 2026 15:07:28 -0300 Subject: [PATCH] fix(config): coerce numeric Discord IDs to strings instead of rejecting (#45125) Merged via squash. Prepared head SHA: 099ba514a1f1c7c50ebe7ef31b8098f5997f7356 Co-authored-by: moliendocode <29582793+moliendocode@users.noreply.github.com> Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com> Reviewed-by: @altaywtf --- CHANGELOG.md | 1 + docs/.generated/config-baseline.json | 80 ++------ docs/.generated/config-baseline.jsonl | 32 +-- src/commands/doctor/providers/discord.test.ts | 191 +++++++++++++++++- src/commands/doctor/providers/discord.ts | 107 +++++++++- src/commands/doctor/repair-sequencing.test.ts | 31 +++ src/commands/doctor/repair-sequencing.ts | 6 +- src/config/config.discord.test.ts | 43 +++- src/config/zod-schema.providers-core.ts | 19 +- 9 files changed, 402 insertions(+), 108 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b83d8125ed..c529164474f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Matrix/multi-account: keep room-level `account` scoping, inherited room overrides, and implicit account selection consistent across top-level default auth, named accounts, and cached-credential env setups. (#58449) thanks @Daanvdplas and @gumadeiras. +- Config/Discord: coerce safe integer numeric Discord IDs to strings during config validation, keep unsafe or precision-losing numeric snowflakes rejected, and align `openclaw doctor` repair guidance with the same fail-closed behavior. (#45125) Thanks @moliendocode. ## 2026.4.1 diff --git a/docs/.generated/config-baseline.json b/docs/.generated/config-baseline.json index 749b2cc9fc7..02fffc85fdc 100644 --- a/docs/.generated/config-baseline.json +++ b/docs/.generated/config-baseline.json @@ -10199,10 +10199,7 @@ { "path": "channels.discord.accounts.*.allowFrom.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, @@ -10452,10 +10449,7 @@ { "path": "channels.discord.accounts.*.dm.allowFrom.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, @@ -10485,10 +10479,7 @@ { "path": "channels.discord.accounts.*.dm.groupChannels.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, @@ -10710,10 +10701,7 @@ { "path": "channels.discord.accounts.*.execApprovals.approvers.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, @@ -10937,10 +10925,7 @@ { "path": "channels.discord.accounts.*.guilds.*.channels.*.roles.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, @@ -11140,10 +11125,7 @@ { "path": "channels.discord.accounts.*.guilds.*.channels.*.users.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, @@ -11199,10 +11181,7 @@ { "path": "channels.discord.accounts.*.guilds.*.roles.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, @@ -11382,10 +11361,7 @@ { "path": "channels.discord.accounts.*.guilds.*.users.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, @@ -12625,10 +12601,7 @@ { "path": "channels.discord.allowFrom.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, @@ -12936,10 +12909,7 @@ { "path": "channels.discord.dm.allowFrom.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, @@ -12969,10 +12939,7 @@ { "path": "channels.discord.dm.groupChannels.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, @@ -13240,10 +13207,7 @@ { "path": "channels.discord.execApprovals.approvers.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, @@ -13467,10 +13431,7 @@ { "path": "channels.discord.guilds.*.channels.*.roles.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, @@ -13670,10 +13631,7 @@ { "path": "channels.discord.guilds.*.channels.*.users.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, @@ -13729,10 +13687,7 @@ { "path": "channels.discord.guilds.*.roles.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, @@ -13912,10 +13867,7 @@ { "path": "channels.discord.guilds.*.users.*", "kind": "channel", - "type": [ - "number", - "string" - ], + "type": "string", "required": false, "deprecated": false, "sensitive": false, diff --git a/docs/.generated/config-baseline.jsonl b/docs/.generated/config-baseline.jsonl index d6474b05f61..62dfe1aeee0 100644 --- a/docs/.generated/config-baseline.jsonl +++ b/docs/.generated/config-baseline.jsonl @@ -895,7 +895,7 @@ {"recordType":"path","path":"channels.discord.accounts.*.agentComponents.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.allowBots","kind":"channel","type":["boolean","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.accounts.*.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.accounts.*.allowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.autoPresence","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.accounts.*.autoPresence.degradedText","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.autoPresence.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -919,10 +919,10 @@ {"recordType":"path","path":"channels.discord.accounts.*.defaultTo","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.dm","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.accounts.*.dm.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.accounts.*.dm.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.accounts.*.dm.allowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.dm.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.dm.groupChannels","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.accounts.*.dm.groupChannels.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.accounts.*.dm.groupChannels.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.dm.groupEnabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.dm.policy","kind":"channel","type":"string","required":false,"enumValues":["pairing","allowlist","open","disabled"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.dmHistoryLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -943,7 +943,7 @@ {"recordType":"path","path":"channels.discord.accounts.*.execApprovals.agentFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.accounts.*.execApprovals.agentFilter.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.execApprovals.approvers","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.approvers.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.accounts.*.execApprovals.approvers.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.execApprovals.cleanupAfterResolve","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.execApprovals.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.execApprovals.sessionFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} @@ -963,7 +963,7 @@ {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.includeThreadStarter","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.roles","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.roles.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.roles.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.skills","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.skills.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.systemPrompt","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -983,12 +983,12 @@ {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.channels.*.users.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.ignoreOtherMentions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.reactionNotifications","kind":"channel","type":"string","required":false,"enumValues":["off","own","all","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.roles","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.roles.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.roles.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.slug","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.tools","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.tools.allow","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} @@ -1006,7 +1006,7 @@ {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.guilds.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.users.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.accounts.*.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.accounts.*.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} @@ -1120,7 +1120,7 @@ {"recordType":"path","path":"channels.discord.agentComponents.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.allowBots","kind":"channel","type":["boolean","string"],"required":false,"deprecated":false,"sensitive":false,"tags":["access","channels","network"],"label":"Discord Allow Bot Messages","help":"Allow bot-authored messages to trigger Discord replies (default: false). Set \"mentions\" to only accept bot messages that mention the bot.","hasChildren":false} {"recordType":"path","path":"channels.discord.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.allowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.autoPresence","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.autoPresence.degradedText","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Discord Auto Presence Degraded Text","help":"Optional custom status text while runtime/model availability is degraded or unknown (idle).","hasChildren":false} {"recordType":"path","path":"channels.discord.autoPresence.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Discord Auto Presence Enabled","help":"Enable automatic Discord bot presence updates based on runtime/model availability signals. When enabled: healthy=>online, degraded/unknown=>idle, exhausted/unavailable=>dnd.","hasChildren":false} @@ -1145,10 +1145,10 @@ {"recordType":"path","path":"channels.discord.defaultTo","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.dm","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.dm.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.dm.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.dm.allowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.dm.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.dm.groupChannels","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.dm.groupChannels.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.dm.groupChannels.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.dm.groupEnabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.dm.policy","kind":"channel","type":"string","required":false,"enumValues":["pairing","allowlist","open","disabled"],"deprecated":false,"sensitive":false,"tags":["access","channels","network"],"label":"Discord DM Policy","help":"Direct message access control (\"pairing\" recommended). \"open\" requires channels.discord.allowFrom=[\"*\"] (legacy: channels.discord.dm.allowFrom).","hasChildren":false} {"recordType":"path","path":"channels.discord.dmHistoryLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -1169,7 +1169,7 @@ {"recordType":"path","path":"channels.discord.execApprovals.agentFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.execApprovals.agentFilter.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.execApprovals.approvers","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.execApprovals.approvers.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.execApprovals.approvers.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.execApprovals.cleanupAfterResolve","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.execApprovals.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.execApprovals.sessionFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} @@ -1189,7 +1189,7 @@ {"recordType":"path","path":"channels.discord.guilds.*.channels.*.includeThreadStarter","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.guilds.*.channels.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.guilds.*.channels.*.roles","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.guilds.*.channels.*.roles.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.guilds.*.channels.*.roles.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.guilds.*.channels.*.skills","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.guilds.*.channels.*.skills.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.guilds.*.channels.*.systemPrompt","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -1209,12 +1209,12 @@ {"recordType":"path","path":"channels.discord.guilds.*.channels.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.guilds.*.channels.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.guilds.*.channels.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.guilds.*.channels.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.guilds.*.channels.*.users.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.guilds.*.ignoreOtherMentions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.guilds.*.reactionNotifications","kind":"channel","type":"string","required":false,"enumValues":["off","own","all","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.guilds.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.guilds.*.roles","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.guilds.*.roles.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.guilds.*.roles.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.guilds.*.slug","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.guilds.*.tools","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.guilds.*.tools.allow","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} @@ -1232,7 +1232,7 @@ {"recordType":"path","path":"channels.discord.guilds.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.guilds.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.guilds.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} -{"recordType":"path","path":"channels.discord.guilds.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} +{"recordType":"path","path":"channels.discord.guilds.*.users.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} {"recordType":"path","path":"channels.discord.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"channels.discord.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true} diff --git a/src/commands/doctor/providers/discord.test.ts b/src/commands/doctor/providers/discord.test.ts index c2d14be9888..9e5034b3f67 100644 --- a/src/commands/doctor/providers/discord.test.ts +++ b/src/commands/doctor/providers/discord.test.ts @@ -41,6 +41,26 @@ describe("doctor discord provider repairs", () => { "channels.discord.guilds.main.channels.general.users[0]", "channels.discord.guilds.main.channels.general.roles[0]", ]); + expect(hits.every((hit) => hit.safe)).toBe(true); + }); + + it("marks unsafe numeric ids as not safe", () => { + const cfg = { + channels: { + discord: { + allowFrom: [106232522769186816, -1, 123.45, 42], + }, + }, + } as unknown as OpenClawConfig; + + const hits = scanDiscordNumericIdEntries(cfg); + + expect(hits).toEqual([ + { path: "channels.discord.allowFrom[0]", entry: 106232522769186816, safe: false }, + { path: "channels.discord.allowFrom[1]", entry: -1, safe: false }, + { path: "channels.discord.allowFrom[2]", entry: 123.45, safe: false }, + { path: "channels.discord.allowFrom[3]", entry: 42, safe: true }, + ]); }); it("repairs numeric discord ids into strings", () => { @@ -50,7 +70,33 @@ describe("doctor discord provider repairs", () => { allowFrom: [123], accounts: { work: { + allowFrom: [234], + dm: { allowFrom: [345], groupChannels: [456] }, execApprovals: { approvers: [456] }, + guilds: { + ops: { + users: [567], + roles: [678], + channels: { + alerts: { + users: [789], + roles: [890], + }, + }, + }, + }, + }, + }, + guilds: { + main: { + users: [111], + roles: [222], + channels: { + general: { + users: [333], + roles: [444], + }, + }, }, }, }, @@ -59,27 +105,154 @@ describe("doctor discord provider repairs", () => { const result = maybeRepairDiscordNumericIds(cfg); - expect(result.changes).toEqual([ - expect.stringContaining("channels.discord.allowFrom: converted 1 numeric entry to strings"), - expect.stringContaining( - "channels.discord.accounts.work.execApprovals.approvers: converted 1 numeric entry to strings", - ), - ]); + expect(result.changes).toEqual( + expect.arrayContaining([ + expect.stringContaining("channels.discord.allowFrom: converted 1 numeric entry to strings"), + expect.stringContaining( + "channels.discord.accounts.work.allowFrom: converted 1 numeric entry to strings", + ), + expect.stringContaining( + "channels.discord.accounts.work.dm.allowFrom: converted 1 numeric entry to strings", + ), + expect.stringContaining( + "channels.discord.accounts.work.dm.groupChannels: converted 1 numeric entry to strings", + ), + expect.stringContaining( + "channels.discord.accounts.work.execApprovals.approvers: converted 1 numeric entry to strings", + ), + expect.stringContaining( + "channels.discord.accounts.work.guilds.ops.users: converted 1 numeric entry to strings", + ), + expect.stringContaining( + "channels.discord.accounts.work.guilds.ops.roles: converted 1 numeric entry to strings", + ), + expect.stringContaining( + "channels.discord.accounts.work.guilds.ops.channels.alerts.users: converted 1 numeric entry to strings", + ), + expect.stringContaining( + "channels.discord.accounts.work.guilds.ops.channels.alerts.roles: converted 1 numeric entry to strings", + ), + expect.stringContaining( + "channels.discord.guilds.main.users: converted 1 numeric entry to strings", + ), + expect.stringContaining( + "channels.discord.guilds.main.roles: converted 1 numeric entry to strings", + ), + expect.stringContaining( + "channels.discord.guilds.main.channels.general.users: converted 1 numeric entry to strings", + ), + expect.stringContaining( + "channels.discord.guilds.main.channels.general.roles: converted 1 numeric entry to strings", + ), + ]), + ); expect(result.config.channels?.discord?.allowFrom).toEqual(["123"]); + expect(result.config.channels?.discord?.guilds?.main?.users).toEqual(["111"]); + expect(result.config.channels?.discord?.guilds?.main?.roles).toEqual(["222"]); + expect(result.config.channels?.discord?.guilds?.main?.channels?.general?.users).toEqual([ + "333", + ]); + expect(result.config.channels?.discord?.guilds?.main?.channels?.general?.roles).toEqual([ + "444", + ]); + expect(result.config.channels?.discord?.accounts?.work?.allowFrom).toEqual(["234"]); + expect(result.config.channels?.discord?.accounts?.work?.dm?.allowFrom).toEqual(["345"]); + expect(result.config.channels?.discord?.accounts?.work?.dm?.groupChannels).toEqual(["456"]); expect(result.config.channels?.discord?.accounts?.work?.execApprovals?.approvers).toEqual([ "456", ]); + expect(result.config.channels?.discord?.accounts?.work?.guilds?.ops?.users).toEqual(["567"]); + expect(result.config.channels?.discord?.accounts?.work?.guilds?.ops?.roles).toEqual(["678"]); + expect( + result.config.channels?.discord?.accounts?.work?.guilds?.ops?.channels?.alerts?.users, + ).toEqual(["789"]); + expect( + result.config.channels?.discord?.accounts?.work?.guilds?.ops?.channels?.alerts?.roles, + ).toEqual(["890"]); }); - it("formats numeric id warnings", () => { + it("skips entire list when it contains unsafe numeric ids", () => { + const cfg = { + channels: { + discord: { + allowFrom: [42, 106232522769186816, -1, 123.45], + dm: { allowFrom: [99] }, + }, + }, + } as unknown as OpenClawConfig; + + const result = maybeRepairDiscordNumericIds(cfg); + + expect(result.changes).toEqual([ + expect.stringContaining( + "channels.discord.dm.allowFrom: converted 1 numeric entry to strings", + ), + ]); + expect(result.config.channels?.discord?.allowFrom).toEqual([ + 42, 106232522769186816, -1, 123.45, + ]); + expect(result.config.channels?.discord?.dm?.allowFrom).toEqual(["99"]); + }); + + it("returns repair warnings when unsafe numeric ids block doctor fix", () => { + const cfg = { + channels: { + discord: { + allowFrom: [106232522769186816], + }, + }, + } as unknown as OpenClawConfig; + + const result = maybeRepairDiscordNumericIds(cfg, { + doctorFixCommand: "openclaw doctor --fix", + }); + + expect(result.changes).toEqual([]); + expect(result.warnings).toEqual([ + expect.stringContaining("could not be auto-repaired"), + expect.stringContaining('rerun "openclaw doctor --fix"'), + ]); + }); + + it("formats numeric id warnings for safe entries", () => { const warnings = collectDiscordNumericIdWarnings({ - hits: [{ path: "channels.discord.allowFrom[0]", entry: 123 }], + hits: [{ path: "channels.discord.allowFrom[0]", entry: 123, safe: true }], doctorFixCommand: "openclaw doctor --fix", }); expect(warnings).toEqual([ - expect.stringContaining("Discord allowlists contain 1 numeric entries"), + expect.stringContaining("Discord allowlists contain 1 numeric entry"), expect.stringContaining('run "openclaw doctor --fix"'), ]); }); + + it("formats numeric id warnings for unsafe entries", () => { + const warnings = collectDiscordNumericIdWarnings({ + hits: [{ path: "channels.discord.allowFrom[0]", entry: 106232522769186816, safe: false }], + doctorFixCommand: "openclaw doctor --fix", + }); + + expect(warnings).toEqual([ + expect.stringContaining("cannot be auto-repaired"), + expect.stringContaining("manually quote the original values"), + ]); + }); + + it("formats warnings for mixed safe and unsafe entries", () => { + const warnings = collectDiscordNumericIdWarnings({ + hits: [ + { path: "channels.discord.allowFrom[0]", entry: 123, safe: true }, + { path: "channels.discord.dm.allowFrom[0]", entry: 456, safe: true }, + { path: "channels.discord.dm.allowFrom[1]", entry: 106232522769186816, safe: false }, + ], + doctorFixCommand: "openclaw doctor --fix", + }); + + expect(warnings).toHaveLength(4); + expect(warnings[0]).toContain("1 numeric entry"); + expect(warnings[1]).toContain('run "openclaw doctor --fix"'); + expect(warnings[2]).toContain("2 numeric entries in lists that cannot be auto-repaired"); + expect(warnings[2]).toContain("channels.discord.dm.allowFrom[0]"); + expect(warnings[3]).toContain('rerun "openclaw doctor --fix"'); + }); }); diff --git a/src/commands/doctor/providers/discord.ts b/src/commands/doctor/providers/discord.ts index 2ecc35b556e..8d4ffaf6dd3 100644 --- a/src/commands/doctor/providers/discord.ts +++ b/src/commands/doctor/providers/discord.ts @@ -3,7 +3,7 @@ import { sanitizeForLog } from "../../../terminal/ansi.js"; import { asObjectRecord } from "../shared/object.js"; import type { DoctorAccountRecord } from "../types.js"; -type DiscordNumericIdHit = { path: string; entry: number }; +type DiscordNumericIdHit = { path: string; entry: number; safe: boolean }; type DiscordIdListRef = { pathLabel: string; @@ -102,7 +102,11 @@ export function scanDiscordNumericIdEntries(cfg: OpenClawConfig): DiscordNumeric if (typeof entry !== "number") { continue; } - hits.push({ path: `${pathLabel}[${index}]`, entry }); + hits.push({ + path: `${pathLabel}[${index}]`, + entry, + safe: Number.isSafeInteger(entry) && entry >= 0, + }); } }; @@ -122,17 +126,88 @@ export function collectDiscordNumericIdWarnings(params: { if (params.hits.length === 0) { return []; } - const samplePath = sanitizeForLog(params.hits[0]?.path ?? "channels.discord.allowFrom"); - const sampleEntry = sanitizeForLog(String(params.hits[0]?.entry ?? "")); + const lines: string[] = []; + const hitsByListPath = new Map(); + for (const hit of params.hits) { + const listPath = hit.path.replace(/\[\d+\]$/, ""); + const existing = hitsByListPath.get(listPath); + if (existing) { + existing.push(hit); + continue; + } + hitsByListPath.set(listPath, [hit]); + } + + const repairableHits: DiscordNumericIdHit[] = []; + const blockedHits: DiscordNumericIdHit[] = []; + for (const hits of hitsByListPath.values()) { + if (hits.some((hit) => !hit.safe)) { + blockedHits.push(...hits); + continue; + } + repairableHits.push(...hits); + } + + if (repairableHits.length > 0) { + const sample = repairableHits[0]; + const samplePath = sanitizeForLog(sample.path); + const sampleEntry = sanitizeForLog(String(sample.entry)); + lines.push( + `- Discord allowlists contain ${repairableHits.length} numeric ${repairableHits.length === 1 ? "entry" : "entries"} (e.g. ${samplePath}=${sampleEntry}).`, + `- Discord IDs must be strings; run "${params.doctorFixCommand}" to convert numeric IDs to quoted strings.`, + ); + } + if (blockedHits.length > 0) { + const sample = blockedHits[0]; + const samplePath = sanitizeForLog(sample.path); + lines.push( + `- Discord allowlists contain ${blockedHits.length} numeric ${blockedHits.length === 1 ? "entry" : "entries"} in lists that cannot be auto-repaired (e.g. ${samplePath}).`, + `- These lists include invalid or precision-losing numeric IDs; manually quote the original values in your config file, then rerun "${params.doctorFixCommand}".`, + ); + } + return lines; +} + +function collectBlockedDiscordNumericIdRepairWarnings(params: { + hits: DiscordNumericIdHit[]; + doctorFixCommand: string; +}): string[] { + const hitsByListPath = new Map(); + for (const hit of params.hits) { + const listPath = hit.path.replace(/\[\d+\]$/, ""); + const existing = hitsByListPath.get(listPath); + if (existing) { + existing.push(hit); + continue; + } + hitsByListPath.set(listPath, [hit]); + } + + const blockedHits: DiscordNumericIdHit[] = []; + for (const hits of hitsByListPath.values()) { + if (hits.some((hit) => !hit.safe)) { + blockedHits.push(...hits); + } + } + if (blockedHits.length === 0) { + return []; + } + + const sample = blockedHits[0]; + const samplePath = sanitizeForLog(sample.path); return [ - `- Discord allowlists contain ${params.hits.length} numeric entries (e.g. ${samplePath}=${sampleEntry}).`, - `- Discord IDs must be strings; run "${params.doctorFixCommand}" to convert numeric IDs to quoted strings.`, + `- Discord allowlists contain ${blockedHits.length} numeric ${blockedHits.length === 1 ? "entry" : "entries"} in lists that could not be auto-repaired (e.g. ${samplePath}).`, + `- These lists include invalid or precision-losing numeric IDs; manually quote the original values in your config file, then rerun "${params.doctorFixCommand}".`, ]; } -export function maybeRepairDiscordNumericIds(cfg: OpenClawConfig): { +export function maybeRepairDiscordNumericIds( + cfg: OpenClawConfig, + params?: { doctorFixCommand?: string }, +): { config: OpenClawConfig; changes: string[]; + warnings?: string[]; } { const hits = scanDiscordNumericIdEntries(cfg); if (hits.length === 0) { @@ -147,6 +222,12 @@ export function maybeRepairDiscordNumericIds(cfg: OpenClawConfig): { if (!Array.isArray(raw)) { return; } + const hasUnsafe = raw.some( + (entry) => typeof entry === "number" && (!Number.isSafeInteger(entry) || entry < 0), + ); + if (hasUnsafe) { + return; + } let converted = 0; const updated = raw.map((entry) => { if (typeof entry === "number") { @@ -170,8 +251,16 @@ export function maybeRepairDiscordNumericIds(cfg: OpenClawConfig): { } } - if (changes.length === 0) { + const warnings = + params?.doctorFixCommand === undefined + ? [] + : collectBlockedDiscordNumericIdRepairWarnings({ + hits, + doctorFixCommand: params.doctorFixCommand, + }); + + if (changes.length === 0 && warnings.length === 0) { return { config: cfg, changes: [] }; } - return { config: next, changes }; + return { config: next, changes, warnings }; } diff --git a/src/commands/doctor/repair-sequencing.test.ts b/src/commands/doctor/repair-sequencing.test.ts index eb270190d46..9e9bacb08db 100644 --- a/src/commands/doctor/repair-sequencing.test.ts +++ b/src/commands/doctor/repair-sequencing.test.ts @@ -71,4 +71,35 @@ describe("doctor repair sequencing", () => { expect(result.warningNotes[0]).not.toContain("\u001B"); expect(result.warningNotes[0]).not.toContain("\r"); }); + + it("emits Discord warnings when unsafe numeric ids block repair", async () => { + const result = await runDoctorRepairSequence({ + state: { + cfg: { + channels: { + discord: { + allowFrom: [106232522769186816], + }, + }, + } as unknown as OpenClawConfig, + candidate: { + channels: { + discord: { + allowFrom: [106232522769186816], + }, + }, + } as unknown as OpenClawConfig, + pendingChanges: false, + fixHints: [], + }, + doctorFixCommand: "openclaw doctor --fix", + }); + + expect(result.changeNotes).toEqual([]); + expect(result.warningNotes).toHaveLength(1); + expect(result.warningNotes[0]).toContain("could not be auto-repaired"); + expect(result.warningNotes[0]).toContain('rerun "openclaw doctor --fix"'); + expect(result.state.pendingChanges).toBe(false); + expect(result.state.candidate.channels?.discord?.allowFrom).toEqual([106232522769186816]); + }); }); diff --git a/src/commands/doctor/repair-sequencing.ts b/src/commands/doctor/repair-sequencing.ts index 39248b13c46..f418e6b0d8f 100644 --- a/src/commands/doctor/repair-sequencing.ts +++ b/src/commands/doctor/repair-sequencing.ts @@ -48,7 +48,11 @@ export async function runDoctorRepairSequence(params: { }; applyMutation(await maybeRepairTelegramAllowFromUsernames(state.candidate)); - applyMutation(maybeRepairDiscordNumericIds(state.candidate)); + applyMutation( + maybeRepairDiscordNumericIds(state.candidate, { + doctorFixCommand: params.doctorFixCommand, + }), + ); applyMutation(maybeRepairOpenPolicyAllowFrom(state.candidate)); applyMutation(maybeRepairBundledPluginLoadPaths(state.candidate, process.env)); applyMutation(maybeRepairStalePluginConfig(state.candidate, process.env)); diff --git a/src/config/config.discord.test.ts b/src/config/config.discord.test.ts index 0bf5484dbe3..0954891c65b 100644 --- a/src/config/config.discord.test.ts +++ b/src/config/config.discord.test.ts @@ -59,7 +59,7 @@ describe("config discord", () => { ); }); - it("rejects numeric discord allowlist entries", () => { + it("coerces safe-integer numeric discord allowlist entries to strings", () => { const res = validateConfigObject({ channels: { discord: { @@ -79,11 +79,42 @@ describe("config discord", () => { }, }); - expect(res.ok).toBe(false); - if (!res.ok) { - expect( - res.issues.some((issue) => issue.message.includes("Discord IDs must be strings")), - ).toBe(true); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.config.channels?.discord?.allowFrom).toEqual(["123"]); + expect(res.config.channels?.discord?.dm?.allowFrom).toEqual(["456"]); + expect(res.config.channels?.discord?.dm?.groupChannels).toEqual(["789"]); + expect(res.config.channels?.discord?.guilds?.["123"]?.users).toEqual(["111"]); + expect(res.config.channels?.discord?.guilds?.["123"]?.roles).toEqual(["222"]); + expect(res.config.channels?.discord?.guilds?.["123"]?.channels?.general?.users).toEqual([ + "333", + ]); + expect(res.config.channels?.discord?.guilds?.["123"]?.channels?.general?.roles).toEqual([ + "444", + ]); + expect(res.config.channels?.discord?.execApprovals?.approvers).toEqual(["555"]); + } + }); + + it("rejects numeric discord IDs that are not valid non-negative safe integers", () => { + const cases = [106232522769186816, -1, 123.45]; + for (const id of cases) { + const res = validateConfigObject({ + channels: { + discord: { + allowFrom: [id], + }, + }, + }); + + expect(res.ok).toBe(false); + if (!res.ok) { + expect( + res.issues.some((issue) => + issue.message.includes("not a valid non-negative safe integer"), + ), + ).toBe(true); + } } }); }); diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 5ea6feb0cdb..bea0926c7e5 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -46,9 +46,22 @@ const ToolPolicyBySenderSchema = z.record(z.string(), ToolPolicySchema).optional const DiscordIdSchema = z .union([z.string(), z.number()]) - .refine((value) => typeof value === "string", { - message: "Discord IDs must be strings (wrap numeric IDs in quotes).", - }); + .transform((value, ctx) => { + if (typeof value === "number") { + if (!Number.isSafeInteger(value) || value < 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + `Discord ID "${String(value)}" is not a valid non-negative safe integer. ` + + `Wrap it in quotes in your config file.`, + }); + return z.NEVER; + } + return String(value); + } + return value; + }) + .pipe(z.string()); const DiscordIdListSchema = z.array(DiscordIdSchema); const TelegramInlineButtonsScopeSchema = z.enum(["off", "dm", "group", "all", "allowlist"]);