UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

247 lines (246 loc) 9.62 kB
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import { t as collectProviderDangerousNameMatchingScopes } from "./dangerous-name-matching-D4QRC91G.js"; import "./runtime-doctor-Bf2V8rFY.js"; import { t as inspectDiscordAccount } from "./account-inspect-DVZXXjia.js"; import { o as resolveDefaultDiscordAccountId } from "./accounts-BI_6uJlY.js"; import { n as normalizeCompatibilityConfig } from "./doctor-contract-BaYPW6iW.js"; import { t as DISCORD_LEGACY_CONFIG_RULES } from "./doctor-shared-cypKrFwC.js"; import { t as isDiscordMutableAllowEntry } from "./security-doctor-uUo8hTD5.js"; //#region extensions/discord/src/doctor.ts function asObjectRecord(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : null; } function sanitizeForLog(value) { return value.replace(/\p{Cc}+/gu, " ").trim(); } function collectDiscordAccountScopes(cfg) { const scopes = []; const discord = asObjectRecord(cfg.channels?.discord); if (!discord) return scopes; scopes.push({ prefix: "channels.discord", account: discord }); const accounts = asObjectRecord(discord.accounts); if (!accounts) return scopes; for (const key of Object.keys(accounts)) { const account = asObjectRecord(accounts[key]); if (account) scopes.push({ prefix: `channels.discord.accounts.${key}`, account }); } return scopes; } function collectDiscordIdLists(prefix, account) { const refs = [{ pathLabel: `${prefix}.allowFrom`, holder: account, key: "allowFrom" }]; const dm = asObjectRecord(account.dm); if (dm) { refs.push({ pathLabel: `${prefix}.dm.allowFrom`, holder: dm, key: "allowFrom" }); refs.push({ pathLabel: `${prefix}.dm.groupChannels`, holder: dm, key: "groupChannels" }); } const execApprovals = asObjectRecord(account.execApprovals); if (execApprovals) refs.push({ pathLabel: `${prefix}.execApprovals.approvers`, holder: execApprovals, key: "approvers" }); const guilds = asObjectRecord(account.guilds); if (!guilds) return refs; for (const guildId of Object.keys(guilds)) { const guild = asObjectRecord(guilds[guildId]); if (!guild) continue; refs.push({ pathLabel: `${prefix}.guilds.${guildId}.users`, holder: guild, key: "users" }); refs.push({ pathLabel: `${prefix}.guilds.${guildId}.roles`, holder: guild, key: "roles" }); const channels = asObjectRecord(guild.channels); if (!channels) continue; for (const channelId of Object.keys(channels)) { const channel = asObjectRecord(channels[channelId]); if (!channel) continue; refs.push({ pathLabel: `${prefix}.guilds.${guildId}.channels.${channelId}.users`, holder: channel, key: "users" }); refs.push({ pathLabel: `${prefix}.guilds.${guildId}.channels.${channelId}.roles`, holder: channel, key: "roles" }); } } return refs; } function scanDiscordNumericIdEntries(cfg) { const hits = []; const scanList = (pathLabel, list) => { if (!Array.isArray(list)) return; for (const [index, entry] of list.entries()) { if (typeof entry !== "number") continue; hits.push({ path: `${pathLabel}[${index}]`, entry, safe: Number.isSafeInteger(entry) && entry >= 0 }); } }; for (const scope of collectDiscordAccountScopes(cfg)) for (const ref of collectDiscordIdLists(scope.prefix, scope.account)) scanList(ref.pathLabel, ref.holder[ref.key]); return hits; } function collectDiscordNumericIdWarnings(params) { if (params.hits.length === 0) return []; const hitsByListPath = /* @__PURE__ */ new Map(); for (const hit of params.hits) { const listPath = hit.path.replace(/\[\d+\]$/, ""); const existing = hitsByListPath.get(listPath); if (existing) existing.push(hit); else hitsByListPath.set(listPath, [hit]); } const repairableHits = []; const blockedHits = []; for (const hits of hitsByListPath.values()) if (hits.some((hit) => !hit.safe)) blockedHits.push(...hits); else repairableHits.push(...hits); const lines = []; if (repairableHits.length > 0) { const sample = repairableHits[0]; lines.push(`- Discord allowlists contain ${repairableHits.length} numeric ${repairableHits.length === 1 ? "entry" : "entries"} (e.g. ${sanitizeForLog(sample.path)}=${sanitizeForLog(String(sample.entry))}).`, `- Discord IDs must be strings; run "${params.doctorFixCommand}" to convert numeric IDs to quoted strings.`); } if (blockedHits.length > 0) { const sample = blockedHits[0]; lines.push(`- Discord allowlists contain ${blockedHits.length} numeric ${blockedHits.length === 1 ? "entry" : "entries"} in lists that cannot be auto-repaired (e.g. ${sanitizeForLog(sample.path)}).`, `- 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 maybeRepairDiscordNumericIds(cfg, doctorFixCommand) { const hits = scanDiscordNumericIdEntries(cfg); if (hits.length === 0) return { config: cfg, changes: [] }; const next = structuredClone(cfg); const changes = []; const repairList = (pathLabel, holder, key) => { const raw = holder[key]; if (!Array.isArray(raw)) return; if (raw.some((entry) => typeof entry === "number" && (!Number.isSafeInteger(entry) || entry < 0))) return; let converted = 0; holder[key] = raw.map((entry) => { if (typeof entry === "number") { converted += 1; return String(entry); } return entry; }); if (converted > 0) changes.push(`- ${sanitizeForLog(pathLabel)}: converted ${converted} numeric ${converted === 1 ? "ID" : "IDs"} to strings`); }; for (const scope of collectDiscordAccountScopes(next)) for (const ref of collectDiscordIdLists(scope.prefix, scope.account)) repairList(ref.pathLabel, ref.holder, ref.key); if (changes.length === 0) return { config: cfg, changes: [], warnings: collectDiscordNumericIdWarnings({ hits, doctorFixCommand }) }; return { config: next, changes, warnings: collectDiscordNumericIdWarnings({ hits: hits.filter((hit) => !hit.safe), doctorFixCommand }) }; } function collectDiscordMissingEnvTokenWarnings(params) { if (resolveDefaultDiscordAccountId(params.cfg) !== "default") return []; const account = inspectDiscordAccount({ cfg: params.cfg, accountId: "default", envToken: params.env?.DISCORD_BOT_TOKEN ?? "" }); if (!account.enabled || account.tokenStatus !== "missing" || account.tokenSource !== "none") return []; return ["- channels.discord: default account has no available bot token, and DISCORD_BOT_TOKEN is absent in this doctor environment. After migration, verify DISCORD_BOT_TOKEN is present in the state-dir .env or configure channels.discord.token / channels.discord.accounts.default.token as a SecretRef."]; } function collectDiscordMutableAllowlistWarnings(cfg) { const hits = []; const addHits = (pathLabel, list) => { if (!Array.isArray(list)) return; for (const entry of list) { const text = normalizeOptionalString(String(entry)) ?? ""; if (!text || text === "*" || !isDiscordMutableAllowEntry(text)) continue; hits.push({ path: pathLabel, entry: text }); } }; for (const scope of collectProviderDangerousNameMatchingScopes(cfg, "discord")) { if (scope.dangerousNameMatchingEnabled) continue; addHits(`${scope.prefix}.allowFrom`, scope.account.allowFrom); const dm = asObjectRecord(scope.account.dm); if (dm) addHits(`${scope.prefix}.dm.allowFrom`, dm.allowFrom); const guilds = asObjectRecord(scope.account.guilds); if (!guilds) continue; for (const [guildId, guildRaw] of Object.entries(guilds)) { const guild = asObjectRecord(guildRaw); if (!guild) continue; addHits(`${scope.prefix}.guilds.${guildId}.users`, guild.users); const channels = asObjectRecord(guild.channels); if (!channels) continue; for (const [channelId, channelRaw] of Object.entries(channels)) { const channel = asObjectRecord(channelRaw); if (channel) addHits(`${scope.prefix}.guilds.${guildId}.channels.${channelId}.users`, channel.users); } } } if (hits.length === 0) return []; const exampleLines = hits.slice(0, 8).map((hit) => `- ${sanitizeForLog(hit.path)}: ${sanitizeForLog(hit.entry)}`); const remaining = hits.length > 8 ? `- +${hits.length - 8} more mutable allowlist entries.` : null; return [ `- Found ${hits.length} mutable allowlist ${hits.length === 1 ? "entry" : "entries"} across discord while name matching is disabled by default.`, ...exampleLines, ...remaining ? [remaining] : [], `- Option A (break-glass): enable channels.discord.dangerousNameMatching=true for the affected scope.`, `- Option B (recommended): resolve names to stable Discord IDs and rewrite the allowlist entries.` ]; } const discordDoctor = { dmAllowFromMode: "topOnly", groupModel: "route", groupAllowFromFallbackToAllowFrom: false, warnOnEmptyGroupSenderAllowlist: false, legacyConfigRules: DISCORD_LEGACY_CONFIG_RULES, normalizeCompatibilityConfig, collectPreviewWarnings: ({ cfg, doctorFixCommand, env }) => [...collectDiscordMissingEnvTokenWarnings({ cfg, env }), ...collectDiscordNumericIdWarnings({ hits: scanDiscordNumericIdEntries(cfg), doctorFixCommand })], collectMutableAllowlistWarnings: ({ cfg }) => collectDiscordMutableAllowlistWarnings(cfg), repairConfig: ({ cfg, doctorFixCommand }) => maybeRepairDiscordNumericIds(cfg, doctorFixCommand) }; //#endregion export { collectDiscordMissingEnvTokenWarnings, collectDiscordNumericIdWarnings, discordDoctor, maybeRepairDiscordNumericIds, scanDiscordNumericIdEntries };