@openclaw/mattermost 2026.7.1 → 2026.7.2-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,23 @@
1
- import { a as normalizeMattermostAllowList, b as readRequestBodyWithLimit, d as createChannelMessageReplyPipeline, g as loadOutboundMediaFromUrl, l as buildModelsProviderData, m as isTrustedProxyAddress, p as isRequestBodyLimitError, t as authorizeMattermostCommandInvocation, v as logTypingFailure, w as resolveClientIp } from "./monitor-auth-BiDuyvOc.js";
2
- import { b as updateMattermostPost, c as createMattermostPost, d as fetchMattermostChannelByName, f as fetchMattermostMe, g as normalizeMattermostBaseUrl, h as fetchMattermostUserTeams, m as fetchMattermostUserByUsername, o as createMattermostClient, p as fetchMattermostUser, r as resolveMattermostAccount, s as createMattermostDirectChannelWithRetry, u as fetchMattermostChannel, v as resolveMattermostReplyDeliveryBarrierTimeoutMs, x as uploadMattermostFile, y as sendMattermostTyping } from "./accounts-ITTlduDO.js";
3
1
  import { t as getMattermostRuntime } from "./runtime-CNB4YGqJ.js";
2
+ import { _ as uploadMattermostFile, c as fetchMattermostMe, d as fetchMattermostUserTeams, f as normalizeMattermostBaseUrl, h as sendMattermostTyping, i as createMattermostPost, l as fetchMattermostUser, m as resolveMattermostReplyDeliveryBarrierTimeoutMs, n as createMattermostClient, o as fetchMattermostChannel, r as createMattermostDirectChannelWithRetry, s as fetchMattermostChannelByName, u as fetchMattermostUserByUsername } from "./client-BzLj4dyf.js";
3
+ import { a as normalizeMattermostAllowList, b as readRequestBodyWithLimit, d as createChannelMessageReplyPipeline, g as loadOutboundMediaFromUrl, l as buildModelsProviderData, p as isRequestBodyLimitError, t as authorizeMattermostCommandInvocation, v as logTypingFailure } from "./monitor-auth-dEHa1_On.js";
4
+ import { c as setInteractionSecret, n as buildButtonProps, o as resolveInteractionCallbackUrl } from "./interactions-DO7J-ND1.js";
5
+ import { r as resolveMattermostAccount } from "./accounts-C4Azn6rA.js";
6
+ import { createHash } from "node:crypto";
7
+ import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
4
8
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringifiedOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
9
+ import { asDateTimestampMs, parseStrictInteger, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
10
+ import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
5
11
  import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
6
12
  import { resolveStoredModelOverride } from "openclaw/plugin-sdk/command-auth-native";
7
13
  import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
8
- import { asDateTimestampMs, parseStrictInteger, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
9
14
  import { deliverTextOrMediaReply, isReasoningReplyPayload, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
10
- import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
11
15
  import { convertMarkdownTables } from "openclaw/plugin-sdk/text-chunking";
12
16
  import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
13
17
  import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
14
18
  import { Readable } from "node:stream";
15
- import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
16
- import { createHash, createHmac } from "node:crypto";
19
+ import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
20
+ import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
17
21
  import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
18
22
  import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
19
23
  const MATTERMOST_COMMAND_DESCRIPTION_MAX_BYTES = 128;
@@ -369,7 +373,9 @@ const ACTION_IDS = {
369
373
  function splitModelRef(modelRef) {
370
374
  const match = normalizeOptionalString(modelRef)?.match(/^([^/]+)\/(.+)$/u);
371
375
  if (!match) return null;
372
- const provider = normalizeProviderId(match[1]);
376
+ const rawProvider = match[1];
377
+ if (!rawProvider) return null;
378
+ const provider = normalizeProviderId(rawProvider);
373
379
  const model = normalizeOptionalString(match[2]);
374
380
  if (!provider || !model) return null;
375
381
  return {
@@ -683,382 +689,84 @@ async function deliverMattermostReplyPayload(params) {
683
689
  });
684
690
  }
685
691
  //#endregion
686
- //#region extensions/mattermost/src/mattermost/interactions.ts
687
- const INTERACTION_MAX_BODY_BYTES = 64 * 1024;
688
- const INTERACTION_BODY_TIMEOUT_MS = 1e4;
689
- const SIGNED_CHANNEL_ID_CONTEXT_KEY = "__openclaw_channel_id";
690
- const callbackUrls = /* @__PURE__ */ new Map();
691
- function setInteractionCallbackUrl(accountId, url) {
692
- callbackUrls.set(accountId, url);
693
- }
694
- function resolveInteractionCallbackPath(accountId) {
695
- return `/mattermost/interactions/${accountId}`;
696
- }
697
- function isWildcardBindHost(rawHost) {
698
- const trimmed = rawHost.trim();
699
- if (!trimmed) return false;
700
- const host = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
701
- return host === "0.0.0.0" || host === "::" || host === "0:0:0:0:0:0:0:0" || host === "::0";
702
- }
703
- function normalizeCallbackBaseUrl(baseUrl) {
704
- return baseUrl.trim().replace(/\/+$/, "");
705
- }
706
- function headerValue(value) {
707
- if (Array.isArray(value)) return normalizeOptionalString(value[0]);
708
- return normalizeOptionalString(value);
709
- }
710
- function isAllowedInteractionSource(params) {
711
- const { allowedSourceIps } = params;
712
- if (!allowedSourceIps?.length) return true;
713
- return isTrustedProxyAddress(resolveClientIp({
714
- remoteAddr: params.req.socket?.remoteAddress,
715
- forwardedFor: headerValue(params.req.headers["x-forwarded-for"]),
716
- realIp: headerValue(params.req.headers["x-real-ip"]),
717
- trustedProxies: params.trustedProxies,
718
- allowRealIpFallback: params.allowRealIpFallback
719
- }), allowedSourceIps);
720
- }
721
- /**
722
- * Resolve the interaction callback URL for an account.
723
- * Falls back to computing it from interactions.callbackBaseUrl or gateway host config.
724
- */
725
- function computeInteractionCallbackUrl(accountId, cfg) {
726
- const path = resolveInteractionCallbackPath(accountId);
727
- const callbackBaseUrl = normalizeOptionalString(cfg?.interactions?.callbackBaseUrl) ?? normalizeOptionalString(cfg?.channels?.mattermost?.interactions?.callbackBaseUrl);
728
- if (callbackBaseUrl) return `${normalizeCallbackBaseUrl(callbackBaseUrl)}${path}`;
729
- const port = typeof cfg?.gateway?.port === "number" ? cfg.gateway.port : 18789;
730
- let host = cfg?.gateway?.customBindHost && !isWildcardBindHost(cfg.gateway.customBindHost) ? cfg.gateway.customBindHost.trim() : "localhost";
731
- if (host.includes(":") && !(host.startsWith("[") && host.endsWith("]"))) host = `[${host}]`;
732
- return `http://${host}:${port}${path}`;
692
+ //#region extensions/mattermost/src/mattermost/target-resolution.ts
693
+ const MATTERMOST_OPAQUE_TARGET_CACHE_MAX_ENTRIES = 1024;
694
+ const mattermostOpaqueTargetCache = /* @__PURE__ */ new Map();
695
+ function cacheMattermostOpaqueTarget(key, kind) {
696
+ mattermostOpaqueTargetCache.set(key, kind);
697
+ pruneMapToMaxSize(mattermostOpaqueTargetCache, MATTERMOST_OPAQUE_TARGET_CACHE_MAX_ENTRIES);
733
698
  }
734
- /**
735
- * Resolve the interaction callback URL for an account.
736
- * Prefers the in-memory registered URL (set by the gateway monitor) so callers outside the
737
- * monitor lifecycle can reuse the runtime-validated callback destination.
738
- */
739
- function resolveInteractionCallbackUrl(accountId, cfg) {
740
- const cached = callbackUrls.get(accountId);
741
- if (cached) return cached;
742
- return computeInteractionCallbackUrl(accountId, cfg);
699
+ function cacheKey$1(baseUrl, token, id) {
700
+ return `${baseUrl}::${token}::${id}`;
743
701
  }
744
- const interactionSecrets = /* @__PURE__ */ new Map();
745
- let defaultInteractionSecret;
746
- function deriveInteractionSecret(botToken) {
747
- return createHmac("sha256", "openclaw-mattermost-interactions").update(botToken).digest("hex");
702
+ /** Mattermost IDs are 26-character lowercase alphanumeric strings. */
703
+ function isMattermostId(value) {
704
+ return /^[a-z0-9]{26}$/.test(value);
748
705
  }
749
- function setInteractionSecret(accountIdOrBotToken, botToken) {
750
- if (typeof botToken === "string") {
751
- interactionSecrets.set(accountIdOrBotToken, deriveInteractionSecret(botToken));
752
- return;
706
+ function parseMattermostTarget(raw) {
707
+ const trimmed = raw.trim();
708
+ if (!trimmed) throw new Error("Recipient is required for Mattermost sends");
709
+ const lower = normalizeLowercaseStringOrEmpty(trimmed);
710
+ if (lower.startsWith("channel:")) {
711
+ const id = trimmed.slice(8).trim();
712
+ if (!id) throw new Error("Channel id is required for Mattermost sends");
713
+ if (id.startsWith("#")) {
714
+ const name = id.slice(1).trim();
715
+ if (!name) throw new Error("Channel name is required for Mattermost sends");
716
+ return {
717
+ kind: "channel-name",
718
+ name
719
+ };
720
+ }
721
+ if (!isMattermostId(id)) return {
722
+ kind: "channel-name",
723
+ name: id
724
+ };
725
+ return {
726
+ kind: "channel",
727
+ id
728
+ };
753
729
  }
754
- defaultInteractionSecret = deriveInteractionSecret(accountIdOrBotToken);
755
- }
756
- function getInteractionSecret(accountId) {
757
- const scoped = accountId ? interactionSecrets.get(accountId) : void 0;
758
- if (scoped) return scoped;
759
- if (defaultInteractionSecret) return defaultInteractionSecret;
760
- if (interactionSecrets.size === 1) {
761
- const first = interactionSecrets.values().next().value;
762
- if (typeof first === "string") return first;
730
+ if (lower.startsWith("user:")) {
731
+ const id = trimmed.slice(5).trim();
732
+ if (!id) throw new Error("User id is required for Mattermost sends");
733
+ return {
734
+ kind: "user",
735
+ id
736
+ };
763
737
  }
764
- throw new Error("Interaction secret not initialized — call setInteractionSecret(accountId, botToken) first");
765
- }
766
- function canonicalizeInteractionContext(value) {
767
- if (Array.isArray(value)) return value.map((item) => canonicalizeInteractionContext(item));
768
- if (value && typeof value === "object") {
769
- const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).toSorted(([left], [right]) => left.localeCompare(right)).map(([key, entryValue]) => [key, canonicalizeInteractionContext(entryValue)]);
770
- return Object.fromEntries(entries);
738
+ if (lower.startsWith("mattermost:")) {
739
+ const id = trimmed.slice(11).trim();
740
+ if (!id) throw new Error("User id is required for Mattermost sends");
741
+ return {
742
+ kind: "user",
743
+ id
744
+ };
771
745
  }
772
- return value;
773
- }
774
- function generateInteractionToken(context, accountId) {
775
- const secret = getInteractionSecret(accountId);
776
- const payload = JSON.stringify(canonicalizeInteractionContext(context));
777
- return createHmac("sha256", secret).update(payload).digest("hex");
778
- }
779
- function verifyInteractionToken(context, token, accountId) {
780
- return safeEqualSecret(generateInteractionToken(context, accountId), token);
781
- }
782
- /**
783
- * Build Mattermost `props.attachments` with interactive buttons.
784
- *
785
- * Each button includes an HMAC token in its integration context so the
786
- * callback handler can verify the request originated from a legitimate
787
- * button click (Mattermost's recommended security pattern).
788
- */
789
- /**
790
- * Sanitize a button ID so Mattermost's action router can match it.
791
- * Mattermost uses the action ID in the URL path `/api/v4/posts/{id}/actions/{actionId}`
792
- * and IDs containing hyphens or underscores break the server-side routing.
793
- * See: https://github.com/mattermost/mattermost/issues/25747
794
- */
795
- function sanitizeActionId(id) {
796
- return id.replace(/[-_]/g, "");
797
- }
798
- function buildButtonAttachments(params) {
799
- const actions = params.buttons.map((btn) => {
800
- const safeId = sanitizeActionId(btn.id);
801
- const context = {
802
- action_id: safeId,
803
- ...btn.context
746
+ if (trimmed.startsWith("@")) {
747
+ const username = trimmed.slice(1).trim();
748
+ if (!username) throw new Error("Username is required for Mattermost sends");
749
+ return {
750
+ kind: "user",
751
+ username
804
752
  };
805
- const token = generateInteractionToken(context, params.accountId);
753
+ }
754
+ if (trimmed.startsWith("#")) {
755
+ const name = trimmed.slice(1).trim();
756
+ if (!name) throw new Error("Channel name is required for Mattermost sends");
806
757
  return {
807
- id: safeId,
808
- type: "button",
809
- name: btn.name,
810
- style: btn.style,
811
- integration: {
812
- url: params.callbackUrl,
813
- context: {
814
- ...context,
815
- _token: token
816
- }
817
- }
758
+ kind: "channel-name",
759
+ name
818
760
  };
819
- });
820
- return [{
821
- text: params.text ?? "",
822
- actions
823
- }];
824
- }
825
- function buildButtonProps(params) {
826
- const buttons = params.buttons.flatMap((item) => Array.isArray(item) ? item : [item]).map((btn) => ({
827
- id: normalizeStringifiedOptionalString(btn.id ?? btn.callback_data) ?? "",
828
- name: normalizeStringifiedOptionalString(btn.text ?? btn.name ?? btn.label) ?? "",
829
- style: btn.style ?? "default",
830
- context: typeof btn.context === "object" && btn.context !== null ? {
831
- ...btn.context,
832
- [SIGNED_CHANNEL_ID_CONTEXT_KEY]: params.channelId
833
- } : { [SIGNED_CHANNEL_ID_CONTEXT_KEY]: params.channelId }
834
- })).filter((btn) => btn.id && btn.name);
835
- if (buttons.length === 0) return;
836
- return { attachments: buildButtonAttachments({
837
- callbackUrl: params.callbackUrl,
838
- accountId: params.accountId,
839
- buttons,
840
- text: params.text
841
- }) };
842
- }
843
- function readInteractionBody(req) {
844
- return readRequestBodyWithLimit(req, {
845
- maxBytes: INTERACTION_MAX_BODY_BYTES,
846
- timeoutMs: INTERACTION_BODY_TIMEOUT_MS
847
- });
848
- }
849
- function createMattermostInteractionHandler(params) {
850
- const { client, accountId, log } = params;
851
- const core = getMattermostRuntime();
852
- function parseInteractionPayload(raw) {
853
- try {
854
- return JSON.parse(raw);
855
- } catch {
856
- throw new Error("Mattermost interaction body was malformed JSON");
857
- }
858
761
  }
859
- return async (req, res) => {
860
- if (req.method !== "POST") {
861
- res.statusCode = 405;
862
- res.setHeader("Allow", "POST");
863
- res.setHeader("Content-Type", "application/json");
864
- res.end(JSON.stringify({ error: "Method Not Allowed" }));
865
- return;
866
- }
867
- if (!isAllowedInteractionSource({
868
- req,
869
- allowedSourceIps: params.allowedSourceIps,
870
- trustedProxies: params.trustedProxies,
871
- allowRealIpFallback: params.allowRealIpFallback
872
- })) {
873
- log?.(`mattermost interaction: rejected callback source remote=${req.socket?.remoteAddress ?? "?"}`);
874
- res.statusCode = 403;
875
- res.setHeader("Content-Type", "application/json");
876
- res.end(JSON.stringify({ error: "Forbidden origin" }));
877
- return;
878
- }
879
- let payload;
880
- try {
881
- payload = parseInteractionPayload(await readInteractionBody(req));
882
- } catch (err) {
883
- log?.(`mattermost interaction: failed to parse body: ${String(err)}`);
884
- res.statusCode = 400;
885
- res.setHeader("Content-Type", "application/json");
886
- res.end(JSON.stringify({ error: "Invalid request body" }));
887
- return;
888
- }
889
- const context = payload.context;
890
- if (!context) {
891
- res.statusCode = 400;
892
- res.setHeader("Content-Type", "application/json");
893
- res.end(JSON.stringify({ error: "Missing context" }));
894
- return;
895
- }
896
- const token = context["_token"];
897
- if (typeof token !== "string") {
898
- log?.("mattermost interaction: missing _token in context");
899
- res.statusCode = 403;
900
- res.setHeader("Content-Type", "application/json");
901
- res.end(JSON.stringify({ error: "Missing token" }));
902
- return;
903
- }
904
- const { _token, ...contextWithoutToken } = context;
905
- if (!verifyInteractionToken(contextWithoutToken, token, accountId)) {
906
- log?.("mattermost interaction: invalid _token");
907
- res.statusCode = 403;
908
- res.setHeader("Content-Type", "application/json");
909
- res.end(JSON.stringify({ error: "Invalid token" }));
910
- return;
911
- }
912
- const actionId = context.action_id;
913
- if (typeof actionId !== "string") {
914
- res.statusCode = 400;
915
- res.setHeader("Content-Type", "application/json");
916
- res.end(JSON.stringify({ error: "Missing action_id in context" }));
917
- return;
918
- }
919
- const signedChannelId = typeof contextWithoutToken[SIGNED_CHANNEL_ID_CONTEXT_KEY] === "string" ? contextWithoutToken[SIGNED_CHANNEL_ID_CONTEXT_KEY].trim() : "";
920
- if (signedChannelId && signedChannelId !== payload.channel_id) {
921
- log?.(`mattermost interaction: signed channel mismatch payload=${payload.channel_id} signed=${signedChannelId}`);
922
- res.statusCode = 403;
923
- res.setHeader("Content-Type", "application/json");
924
- res.end(JSON.stringify({ error: "Channel mismatch" }));
925
- return;
926
- }
927
- const userName = payload.user_name ?? payload.user_id;
928
- let originalMessage;
929
- let originalPost;
930
- let clickedButtonName = null;
931
- try {
932
- originalPost = await client.request(`/posts/${payload.post_id}`);
933
- const postChannelId = originalPost.channel_id?.trim();
934
- if (!postChannelId || postChannelId !== payload.channel_id) {
935
- log?.(`mattermost interaction: post channel mismatch payload=${payload.channel_id} post=${postChannelId ?? "<missing>"}`);
936
- res.statusCode = 403;
937
- res.setHeader("Content-Type", "application/json");
938
- res.end(JSON.stringify({ error: "Post/channel mismatch" }));
939
- return;
940
- }
941
- originalMessage = originalPost.message ?? "";
942
- const postAttachments = Array.isArray(originalPost?.props?.attachments) ? originalPost.props.attachments : [];
943
- for (const att of postAttachments) {
944
- const match = att.actions?.find((a) => a.id === actionId);
945
- if (match?.name) {
946
- clickedButtonName = match.name;
947
- break;
948
- }
949
- }
950
- if (clickedButtonName === null) {
951
- log?.(`mattermost interaction: action ${actionId} not found in post ${payload.post_id}`);
952
- res.statusCode = 403;
953
- res.setHeader("Content-Type", "application/json");
954
- res.end(JSON.stringify({ error: "Unknown action" }));
955
- return;
956
- }
957
- } catch (err) {
958
- log?.(`mattermost interaction: failed to validate post ${payload.post_id}: ${String(err)}`);
959
- res.statusCode = 500;
960
- res.setHeader("Content-Type", "application/json");
961
- res.end(JSON.stringify({ error: "Failed to validate interaction" }));
962
- return;
963
- }
964
- if (!originalPost) {
965
- log?.(`mattermost interaction: missing fetched post ${payload.post_id}`);
966
- res.statusCode = 500;
967
- res.setHeader("Content-Type", "application/json");
968
- res.end(JSON.stringify({ error: "Failed to load interaction post" }));
969
- return;
970
- }
971
- log?.(`mattermost interaction: action=${actionId} user=${payload.user_name ?? payload.user_id} post=${payload.post_id} channel=${payload.channel_id}`);
972
- if (params.authorizeButtonClick) try {
973
- const authorization = await params.authorizeButtonClick({
974
- payload,
975
- post: originalPost
976
- });
977
- if (!authorization.ok) {
978
- res.statusCode = authorization.statusCode ?? 200;
979
- res.setHeader("Content-Type", "application/json");
980
- res.end(JSON.stringify(authorization.response ?? { ephemeral_text: "You are not allowed to use this action here." }));
981
- return;
982
- }
983
- } catch (err) {
984
- log?.(`mattermost interaction: authorization failed: ${String(err)}`);
985
- res.statusCode = 500;
986
- res.setHeader("Content-Type", "application/json");
987
- res.end(JSON.stringify({ error: "Interaction authorization failed" }));
988
- return;
989
- }
990
- if (params.handleInteraction) try {
991
- const response = await params.handleInteraction({
992
- payload,
993
- userName,
994
- actionId,
995
- actionName: clickedButtonName,
996
- originalMessage,
997
- context: contextWithoutToken,
998
- post: originalPost
999
- });
1000
- if (response !== null) {
1001
- res.statusCode = 200;
1002
- res.setHeader("Content-Type", "application/json");
1003
- res.end(JSON.stringify(response));
1004
- return;
1005
- }
1006
- } catch (err) {
1007
- log?.(`mattermost interaction: custom handler failed: ${String(err)}`);
1008
- res.statusCode = 500;
1009
- res.setHeader("Content-Type", "application/json");
1010
- res.end(JSON.stringify({ error: "Interaction handler failed" }));
1011
- return;
1012
- }
1013
- try {
1014
- const eventLabel = `Mattermost button click: action="${actionId}" by ${payload.user_name ?? payload.user_id} in channel ${payload.channel_id}`;
1015
- const sessionKey = params.resolveSessionKey ? await params.resolveSessionKey({
1016
- channelId: payload.channel_id,
1017
- userId: payload.user_id,
1018
- post: originalPost
1019
- }) : `agent:main:mattermost:${accountId}:${payload.channel_id}`;
1020
- core.system.enqueueSystemEvent(eventLabel, {
1021
- sessionKey,
1022
- contextKey: `mattermost:interaction:${payload.post_id}:${actionId}`
1023
- });
1024
- } catch (err) {
1025
- log?.(`mattermost interaction: system event dispatch failed: ${String(err)}`);
1026
- }
1027
- try {
1028
- await updateMattermostPost(client, payload.post_id, {
1029
- message: originalMessage,
1030
- props: { attachments: [{ text: `✓ **${clickedButtonName}** selected by @${userName}` }] }
1031
- });
1032
- } catch (err) {
1033
- log?.(`mattermost interaction: failed to update post ${payload.post_id}: ${String(err)}`);
1034
- }
1035
- res.statusCode = 200;
1036
- res.setHeader("Content-Type", "application/json");
1037
- res.end("{}");
1038
- if (params.dispatchButtonClick) try {
1039
- await params.dispatchButtonClick({
1040
- channelId: payload.channel_id,
1041
- userId: payload.user_id,
1042
- userName,
1043
- actionId,
1044
- actionName: clickedButtonName,
1045
- postId: payload.post_id,
1046
- post: originalPost
1047
- });
1048
- } catch (err) {
1049
- log?.(`mattermost interaction: dispatchButtonClick failed: ${String(err)}`);
1050
- }
762
+ if (!isMattermostId(trimmed)) return {
763
+ kind: "channel-name",
764
+ name: trimmed
765
+ };
766
+ return {
767
+ kind: "channel",
768
+ id: trimmed
1051
769
  };
1052
- }
1053
- //#endregion
1054
- //#region extensions/mattermost/src/mattermost/target-resolution.ts
1055
- const mattermostOpaqueTargetCache = /* @__PURE__ */ new Map();
1056
- function cacheKey$1(baseUrl, token, id) {
1057
- return `${baseUrl}::${token}::${id}`;
1058
- }
1059
- /** Mattermost IDs are 26-character lowercase alphanumeric strings. */
1060
- function isMattermostId(value) {
1061
- return /^[a-z0-9]{26}$/.test(value);
1062
770
  }
1063
771
  function isExplicitMattermostTarget(raw) {
1064
772
  const trimmed = raw.trim();
@@ -1080,20 +788,16 @@ async function resolveMattermostOpaqueTarget(params) {
1080
788
  cfg: params.cfg,
1081
789
  accountId: params.accountId
1082
790
  }) : null;
791
+ if (account && !account.enabled) throw new Error(`Mattermost account "${account.accountId}" is disabled`);
1083
792
  const token = normalizeOptionalString(params.token) ?? normalizeOptionalString(account?.botToken);
1084
793
  const baseUrl = normalizeMattermostBaseUrl(params.baseUrl ?? account?.baseUrl);
1085
794
  if (!token || !baseUrl) return null;
1086
795
  const key = cacheKey$1(baseUrl, token, input);
1087
- const cached = mattermostOpaqueTargetCache.get(key);
1088
- if (cached === true) return {
1089
- kind: "user",
796
+ const cachedKind = mattermostOpaqueTargetCache.get(key);
797
+ if (cachedKind) return {
798
+ kind: cachedKind,
1090
799
  id: input,
1091
- to: `user:${input}`
1092
- };
1093
- if (cached === false) return {
1094
- kind: "channel",
1095
- id: input,
1096
- to: `channel:${input}`
800
+ to: `${cachedKind}:${input}`
1097
801
  };
1098
802
  const client = createMattermostClient({
1099
803
  baseUrl,
@@ -1102,14 +806,14 @@ async function resolveMattermostOpaqueTarget(params) {
1102
806
  });
1103
807
  try {
1104
808
  await fetchMattermostUser(client, input);
1105
- mattermostOpaqueTargetCache.set(key, true);
809
+ cacheMattermostOpaqueTarget(key, "user");
1106
810
  return {
1107
811
  kind: "user",
1108
812
  id: input,
1109
813
  to: `user:${input}`
1110
814
  };
1111
815
  } catch (err) {
1112
- if (parseMattermostApiStatus(err) === 404) mattermostOpaqueTargetCache.set(key, false);
816
+ if (parseMattermostApiStatus(err) === 404) cacheMattermostOpaqueTarget(key, "channel");
1113
817
  return {
1114
818
  kind: "channel",
1115
819
  id: input,
@@ -1119,10 +823,17 @@ async function resolveMattermostOpaqueTarget(params) {
1119
823
  }
1120
824
  //#endregion
1121
825
  //#region extensions/mattermost/src/mattermost/send.ts
826
+ const MATTERMOST_BOT_USER_CACHE_MAX_ENTRIES = 64;
827
+ const MATTERMOST_TARGET_CACHE_MAX_ENTRIES = 1024;
1122
828
  const botUserCache = /* @__PURE__ */ new Map();
1123
829
  const userByNameCache = /* @__PURE__ */ new Map();
1124
830
  const channelByNameCache = /* @__PURE__ */ new Map();
1125
831
  const dmChannelCache = /* @__PURE__ */ new Map();
832
+ function cacheOutboundEntry(cache, key, value, maxEntries) {
833
+ cache.delete(key);
834
+ cache.set(key, value);
835
+ pruneMapToMaxSize(cache, maxEntries);
836
+ }
1126
837
  const getCore = () => getMattermostRuntime();
1127
838
  function createMattermostSendReceipt(params) {
1128
839
  const messageIds = params.messageId.trim() && params.messageId !== "unknown" ? [params.messageId] : [];
@@ -1161,71 +872,6 @@ function normalizeMessage(text, mediaUrl) {
1161
872
  function isHttpUrl(value) {
1162
873
  return /^https?:\/\//i.test(value);
1163
874
  }
1164
- function parseMattermostTarget(raw) {
1165
- const trimmed = raw.trim();
1166
- if (!trimmed) throw new Error("Recipient is required for Mattermost sends");
1167
- const lower = normalizeLowercaseStringOrEmpty(trimmed);
1168
- if (lower.startsWith("channel:")) {
1169
- const id = trimmed.slice(8).trim();
1170
- if (!id) throw new Error("Channel id is required for Mattermost sends");
1171
- if (id.startsWith("#")) {
1172
- const name = id.slice(1).trim();
1173
- if (!name) throw new Error("Channel name is required for Mattermost sends");
1174
- return {
1175
- kind: "channel-name",
1176
- name
1177
- };
1178
- }
1179
- if (!isMattermostId(id)) return {
1180
- kind: "channel-name",
1181
- name: id
1182
- };
1183
- return {
1184
- kind: "channel",
1185
- id
1186
- };
1187
- }
1188
- if (lower.startsWith("user:")) {
1189
- const id = trimmed.slice(5).trim();
1190
- if (!id) throw new Error("User id is required for Mattermost sends");
1191
- return {
1192
- kind: "user",
1193
- id
1194
- };
1195
- }
1196
- if (lower.startsWith("mattermost:")) {
1197
- const id = trimmed.slice(11).trim();
1198
- if (!id) throw new Error("User id is required for Mattermost sends");
1199
- return {
1200
- kind: "user",
1201
- id
1202
- };
1203
- }
1204
- if (trimmed.startsWith("@")) {
1205
- const username = trimmed.slice(1).trim();
1206
- if (!username) throw new Error("Username is required for Mattermost sends");
1207
- return {
1208
- kind: "user",
1209
- username
1210
- };
1211
- }
1212
- if (trimmed.startsWith("#")) {
1213
- const name = trimmed.slice(1).trim();
1214
- if (!name) throw new Error("Channel name is required for Mattermost sends");
1215
- return {
1216
- kind: "channel-name",
1217
- name
1218
- };
1219
- }
1220
- if (!isMattermostId(trimmed)) return {
1221
- kind: "channel-name",
1222
- name: trimmed
1223
- };
1224
- return {
1225
- kind: "channel",
1226
- id: trimmed
1227
- };
1228
- }
1229
875
  async function resolveBotUser(baseUrl, token, allowPrivateNetwork) {
1230
876
  const key = cacheKey(baseUrl, token);
1231
877
  const cached = botUserCache.get(key);
@@ -1235,7 +881,7 @@ async function resolveBotUser(baseUrl, token, allowPrivateNetwork) {
1235
881
  botToken: token,
1236
882
  allowPrivateNetwork
1237
883
  }));
1238
- botUserCache.set(key, user);
884
+ cacheOutboundEntry(botUserCache, key, user, MATTERMOST_BOT_USER_CACHE_MAX_ENTRIES);
1239
885
  return user;
1240
886
  }
1241
887
  async function resolveUserIdByUsername(params) {
@@ -1248,7 +894,7 @@ async function resolveUserIdByUsername(params) {
1248
894
  botToken: token,
1249
895
  allowPrivateNetwork: params.allowPrivateNetwork
1250
896
  }), username);
1251
- userByNameCache.set(key, user);
897
+ cacheOutboundEntry(userByNameCache, key, user, MATTERMOST_TARGET_CACHE_MAX_ENTRIES);
1252
898
  return user.id;
1253
899
  }
1254
900
  async function resolveChannelIdByName(params) {
@@ -1265,7 +911,7 @@ async function resolveChannelIdByName(params) {
1265
911
  for (const team of teams) try {
1266
912
  const channel = await fetchMattermostChannelByName(client, team.id, name);
1267
913
  if (channel?.id) {
1268
- channelByNameCache.set(key, channel.id);
914
+ cacheOutboundEntry(channelByNameCache, key, channel.id, MATTERMOST_TARGET_CACHE_MAX_ENTRIES);
1269
915
  return channel.id;
1270
916
  }
1271
917
  } catch {}
@@ -1313,7 +959,7 @@ async function resolveTargetChannelId(params) {
1313
959
  });
1314
960
  params.onDmChannelResolution?.(resolution);
1315
961
  const channel = await resolution;
1316
- dmChannelCache.set(dmKey, channel.id);
962
+ cacheOutboundEntry(dmChannelCache, dmKey, channel.id, MATTERMOST_TARGET_CACHE_MAX_ENTRIES);
1317
963
  return channel.id;
1318
964
  }
1319
965
  async function resolveMattermostSendContext(to, opts) {
@@ -1495,7 +1141,7 @@ function isDeletedMattermostCommand(command) {
1495
1141
  return typeof command.delete_at === "number" && command.delete_at > 0;
1496
1142
  }
1497
1143
  function sanitizeCommandLookupError(error) {
1498
- return (error instanceof Error ? error.message : String(error)).replace(/[\r\n\t]/gu, " ").replace(/https?:\/\/[^\s)\]}]+/giu, (urlText) => {
1144
+ return truncateUtf16Safe((error instanceof Error ? error.message : String(error)).replace(/[\r\n\t]/gu, " ").replace(/https?:\/\/[^\s)\]}]+/giu, (urlText) => {
1499
1145
  try {
1500
1146
  const url = new URL(urlText);
1501
1147
  if (url.username || url.password) {
@@ -1507,10 +1153,10 @@ function sanitizeCommandLookupError(error) {
1507
1153
  } catch {
1508
1154
  return urlText;
1509
1155
  }
1510
- }).replace(/(^|[^\w-])(Bearer|Token)\s+[A-Za-z0-9._~+/=-]+/giu, "$1$2 [redacted]").replace(/\b(token|authorization|access_token|refresh_token|client_secret|botToken)\b(\s*["']?\s*(?:=|:)\s*["']?)[^"',\s;}]+/giu, "$1$2[redacted]").slice(0, 300);
1156
+ }).replace(/(^|[^\w-])(Bearer|Token)\s+[A-Za-z0-9._~+/=-]+/giu, "$1$2 [redacted]").replace(/\b(token|authorization|access_token|refresh_token|client_secret|botToken)\b(\s*["']?\s*(?:=|:)\s*["']?)[^"',\s;}]+/giu, "$1$2[redacted]"), 300);
1511
1157
  }
1512
1158
  function sanitizeMattermostLogValue(value) {
1513
- return value.replace(/[\r\n\t]/gu, " ").slice(0, 200);
1159
+ return truncateUtf16Safe(value.replace(/[\r\n\t]/gu, " "), 200);
1514
1160
  }
1515
1161
  async function withCommandLookupTimeout(task) {
1516
1162
  const controller = new AbortController();
@@ -2065,12 +1711,16 @@ function resolveSlashHandlerForToken(token) {
2065
1711
  handler: state.handler
2066
1712
  });
2067
1713
  if (matches.length === 0) return { kind: "none" };
2068
- if (matches.length === 1) return {
2069
- kind: "single",
2070
- source: "token",
2071
- handler: matches[0].handler,
2072
- accountIds: [matches[0].accountId]
2073
- };
1714
+ if (matches.length === 1) {
1715
+ const match = matches[0];
1716
+ if (!match) return { kind: "none" };
1717
+ return {
1718
+ kind: "single",
1719
+ source: "token",
1720
+ handler: match.handler,
1721
+ accountIds: [match.accountId]
1722
+ };
1723
+ }
2074
1724
  return {
2075
1725
  kind: "ambiguous",
2076
1726
  source: "token",
@@ -2086,12 +1736,16 @@ function resolveSlashHandlerForCommand(params) {
2086
1736
  handler: state.handler
2087
1737
  });
2088
1738
  if (matches.length === 0) return { kind: "none" };
2089
- if (matches.length === 1) return {
2090
- kind: "single",
2091
- source: "command",
2092
- handler: matches[0].handler,
2093
- accountIds: [matches[0].accountId]
2094
- };
1739
+ if (matches.length === 1) {
1740
+ const match = matches[0];
1741
+ if (!match) return { kind: "none" };
1742
+ return {
1743
+ kind: "single",
1744
+ source: "command",
1745
+ handler: match.handler,
1746
+ accountIds: [match.accountId]
1747
+ };
1748
+ }
2095
1749
  return {
2096
1750
  kind: "ambiguous",
2097
1751
  source: "command",
@@ -2190,8 +1844,8 @@ function registerSlashCommandRoute(api) {
2190
1844
  return;
2191
1845
  }
2192
1846
  if (accountStates.size === 1) {
2193
- const [, state] = [...accountStates.entries()][0];
2194
- if (!state.handler) {
1847
+ const state = accountStates.values().next().value;
1848
+ if (!state?.handler) {
2195
1849
  res.statusCode = 503;
2196
1850
  res.setHeader("Content-Type", "application/json; charset=utf-8");
2197
1851
  res.end(JSON.stringify({
@@ -2270,4 +1924,4 @@ function registerSlashCommandRoute(api) {
2270
1924
  });
2271
1925
  }
2272
1926
  //#endregion
2273
- export { registerSlashCommands as C, isSlashCommandsEnabled as S, resolveSlashCommandConfig as T, renderMattermostModelsPickerView as _, sendMessageMattermost as a, DEFAULT_COMMAND_SPECS as b, computeInteractionCallbackUrl as c, setInteractionCallbackUrl as d, setInteractionSecret as f, parseMattermostModelPickerContext as g, buildMattermostAllowedModelRefs as h, registerSlashCommandRoute as i, createMattermostInteractionHandler as l, deliverMattermostReplyPayload as m, deactivateSlashCommands as n, resolveMattermostOpaqueTarget as o, createMattermostReplyDeliveryBarrier as p, getSlashCommandState as r, buildButtonProps as s, activateSlashCommands as t, resolveInteractionCallbackPath as u, renderMattermostProviderPickerView as v, resolveCallbackUrl as w, cleanupSlashCommands as x, resolveMattermostModelPickerCurrentModel as y };
1927
+ export { registerSlashCommands as _, sendMessageMattermost as a, deliverMattermostReplyPayload as c, renderMattermostModelsPickerView as d, renderMattermostProviderPickerView as f, isSlashCommandsEnabled as g, cleanupSlashCommands as h, registerSlashCommandRoute as i, buildMattermostAllowedModelRefs as l, DEFAULT_COMMAND_SPECS as m, deactivateSlashCommands as n, resolveMattermostOpaqueTarget as o, resolveMattermostModelPickerCurrentModel as p, getSlashCommandState as r, createMattermostReplyDeliveryBarrier as s, activateSlashCommands as t, parseMattermostModelPickerContext as u, resolveCallbackUrl as v, resolveSlashCommandConfig as y };