fix(mattermost): align slash allowlist normalization + register callbackUrl pathname

- normalizeAllowList/isSenderAllowed in slash-http.ts now matches the
  websocket monitor: strips mattermost:/user:/@  prefixes and supports
  the '*' wildcard, so configs that work for WS also work for slash cmds
- registerSlashCommandRoute extracts pathname from explicit callbackUrl
  and registers it alongside callbackPath, so callbacks hit a registered
  route even when callbackUrl uses a non-default pathname

Addresses Codex review round 5 (P1 + P2).
This commit is contained in:
Echo
2026-02-15 02:02:04 -05:00
committed by Muhammed Mukhthar CM
parent d486f208a2
commit 81087ecb6b
2 changed files with 48 additions and 10 deletions

View File

@@ -70,11 +70,26 @@ function sendJsonResponse(
res.end(JSON.stringify(body));
}
/**
* Normalize a single allowlist entry, matching the websocket monitor behaviour.
* Strips `mattermost:`, `user:`, and `@` prefixes, and preserves the `*` wildcard.
*/
function normalizeAllowEntry(entry: string): string {
const trimmed = entry.trim();
if (!trimmed) {
return "";
}
if (trimmed === "*") {
return "*";
}
return trimmed
.replace(/^(mattermost|user):/i, "")
.replace(/^@/, "")
.toLowerCase();
}
function normalizeAllowList(entries: Array<string | number>): string[] {
const normalized = entries
.map((entry) => String(entry).trim())
.filter(Boolean)
.map((entry) => entry.toLowerCase());
const normalized = entries.map((entry) => normalizeAllowEntry(String(entry))).filter(Boolean);
return Array.from(new Set(normalized));
}
@@ -83,12 +98,16 @@ function isSenderAllowed(params: { senderId: string; senderName: string; allowFr
if (allowFrom.length === 0) {
return false;
}
if (allowFrom.includes("*")) {
return true;
}
const allowed = new Set(allowFrom.map((v) => v.toLowerCase()));
const id = senderId.trim().toLowerCase();
const name = senderName.trim().toLowerCase();
const normalizedId = normalizeAllowEntry(senderId);
const normalizedName = senderName ? normalizeAllowEntry(senderName) : "";
return allowed.has(id) || allowed.has(name);
return allowFrom.some(
(entry) => entry === normalizedId || (normalizedName && entry === normalizedName),
);
}
type SlashInvocationAuth = {

View File

@@ -120,12 +120,31 @@ export function registerSlashCommandRoute(api: OpenClawPluginApi) {
// Collect callback paths from both top-level and per-account config.
// Command registration uses account.config.commands, so the HTTP route
// registration must include any account-specific callbackPath overrides.
// Also extract the pathname from an explicit callbackUrl when it differs
// from callbackPath, so that Mattermost callbacks hit a registered route.
const callbackPaths = new Set<string>();
const addCallbackPaths = (
raw: Partial<import("./slash-commands.js").MattermostSlashCommandConfig> | undefined,
) => {
const resolved = resolveSlashCommandConfig(raw);
callbackPaths.add(resolved.callbackPath);
if (resolved.callbackUrl) {
try {
const urlPath = new URL(resolved.callbackUrl).pathname;
if (urlPath && urlPath !== resolved.callbackPath) {
callbackPaths.add(urlPath);
}
} catch {
// Invalid URL — ignore, will be caught during registration
}
}
};
const commandsRaw = mmConfig?.commands as
| Partial<import("./slash-commands.js").MattermostSlashCommandConfig>
| undefined;
callbackPaths.add(resolveSlashCommandConfig(commandsRaw).callbackPath);
addCallbackPaths(commandsRaw);
const accountsRaw = (mmConfig?.accounts ?? {}) as Record<string, unknown>;
for (const accountId of Object.keys(accountsRaw)) {
@@ -133,7 +152,7 @@ export function registerSlashCommandRoute(api: OpenClawPluginApi) {
const accountCommandsRaw = accountCfg?.commands as
| Partial<import("./slash-commands.js").MattermostSlashCommandConfig>
| undefined;
callbackPaths.add(resolveSlashCommandConfig(accountCommandsRaw).callbackPath);
addCallbackPaths(accountCommandsRaw);
}
const routeHandler = async (req: IncomingMessage, res: ServerResponse) => {