@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,17 +1,14 @@
1
1
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
2
- import { resolveChannelPreviewStreamMode, resolveChannelStreamingBlockCoalesce, resolveChannelStreamingBlockEnabled, resolveChannelStreamingChunkMode } from "openclaw/plugin-sdk/channel-outbound";
2
+ import { buildTimeoutAbortSignal } from "openclaw/plugin-sdk/extension-shared";
3
3
  import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
4
- import { fetchWithSsrFGuard, ssrfPolicyFromPrivateNetworkOptIn } from "openclaw/plugin-sdk/ssrf-runtime";
5
- import { createAccountListHelpers, hasConfiguredAccountValue } from "openclaw/plugin-sdk/account-helpers";
6
- import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
7
- import { resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution";
8
- import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
9
4
  import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
10
5
  import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
11
- import { sleep } from "openclaw/plugin-sdk/runtime-env";
6
+ import { retryAsync } from "openclaw/plugin-sdk/retry-runtime";
7
+ import { fetchWithSsrFGuard, ssrfPolicyFromPrivateNetworkOptIn } from "openclaw/plugin-sdk/ssrf-runtime";
12
8
  import { z } from "zod";
13
9
  //#region extensions/mattermost/src/mattermost/client.ts
14
10
  const MATTERMOST_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
11
+ const MATTERMOST_REQUEST_TIMEOUT_MS = 3e4;
15
12
  const MATTERMOST_TEXT_RESPONSE_LIMIT_BYTES = 64 * 1024;
16
13
  const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([
17
14
  101,
@@ -103,17 +100,42 @@ function createMattermostClient(params) {
103
100
  if (!baseUrl) throw new Error("Mattermost baseUrl is required");
104
101
  const apiBaseUrl = `${baseUrl}/api/v4`;
105
102
  const token = params.botToken.trim();
103
+ const requestTimeoutMs = resolveTimerTimeoutMs(params.timeoutMs, MATTERMOST_REQUEST_TIMEOUT_MS);
106
104
  const externalFetchImpl = params.fetchImpl;
107
105
  const guardedFetchImpl = async (input, init) => {
106
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
107
+ const { timeoutMs: initTimeoutMs, ...requestInit } = init ?? {};
108
+ const timeoutMs = resolveTimerTimeoutMs(initTimeoutMs, requestTimeoutMs);
108
109
  const { response, release } = await fetchWithSsrFGuard({
109
- url: typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url,
110
- init,
110
+ url,
111
+ init: requestInit,
111
112
  auditContext: "mattermost-api",
112
- policy: ssrfPolicyFromPrivateNetworkOptIn(params.allowPrivateNetwork)
113
+ policy: ssrfPolicyFromPrivateNetworkOptIn(params.allowPrivateNetwork),
114
+ signal: requestInit.signal ?? void 0,
115
+ timeoutMs
113
116
  });
114
117
  return responseWithRelease(response, release);
115
118
  };
116
- const fetchImpl = externalFetchImpl ?? guardedFetchImpl;
119
+ const fetchImpl = (externalFetchImpl ? async (input, init) => {
120
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
121
+ const { timeoutMs: initTimeoutMs, ...requestInit } = init ?? {};
122
+ const { signal: timeoutSignal, cleanup } = buildTimeoutAbortSignal({
123
+ timeoutMs: resolveTimerTimeoutMs(initTimeoutMs, requestTimeoutMs),
124
+ operation: "mattermost-api",
125
+ url
126
+ });
127
+ const callerSignal = requestInit.signal ?? void 0;
128
+ const signal = callerSignal && timeoutSignal ? AbortSignal.any([callerSignal, timeoutSignal]) : callerSignal ?? timeoutSignal;
129
+ try {
130
+ return responseWithRelease(await externalFetchImpl(input, {
131
+ ...requestInit,
132
+ signal
133
+ }), async () => cleanup());
134
+ } catch (error) {
135
+ cleanup();
136
+ throw error;
137
+ }
138
+ } : void 0) ?? guardedFetchImpl;
117
139
  const request = async (path, init) => {
118
140
  const url = buildMattermostApiUrl(baseUrl, path);
119
141
  const headers = new Headers(init?.headers);
@@ -163,11 +185,12 @@ async function sendMattermostTyping(client, params) {
163
185
  body: JSON.stringify(payload)
164
186
  });
165
187
  }
166
- async function createMattermostDirectChannel(client, userIds, signal) {
188
+ async function createMattermostDirectChannel(client, userIds, signal, timeoutMs) {
167
189
  return await client.request("/channels/direct", {
168
190
  method: "POST",
169
191
  body: JSON.stringify(userIds),
170
- signal
192
+ signal,
193
+ timeoutMs
171
194
  });
172
195
  }
173
196
  const DM_REPLY_DELIVERY_BARRIER_SLACK_MS = 6e4;
@@ -229,26 +252,24 @@ const RETRYABLE_NETWORK_MESSAGE_SNIPPETS = [
229
252
  async function createMattermostDirectChannelWithRetry(client, userIds, options = {}) {
230
253
  const { maxRetries = 3, initialDelayMs = 1e3, maxDelayMs = 1e4, timeoutMs: rawTimeoutMs = 3e4, onRetry } = options;
231
254
  const timeoutMs = resolveTimerTimeoutMs(rawTimeoutMs, 3e4);
232
- let lastError;
233
- for (let attempt = 0; attempt <= maxRetries; attempt++) try {
255
+ return await retryAsync(async () => {
234
256
  const controller = new AbortController();
235
257
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
236
258
  try {
237
- return await createMattermostDirectChannel(client, userIds, controller.signal);
259
+ return await createMattermostDirectChannel(client, userIds, controller.signal, timeoutMs);
260
+ } catch (err) {
261
+ throw err instanceof Error ? err : new Error(String(err));
238
262
  } finally {
239
263
  clearTimeout(timeoutId);
240
264
  }
241
- } catch (err) {
242
- lastError = err instanceof Error ? err : new Error(String(err));
243
- if (attempt >= maxRetries) break;
244
- if (!isRetryableError(lastError)) throw lastError;
245
- const exponentialDelay = initialDelayMs * 2 ** attempt;
246
- const jitter = Math.random() * exponentialDelay;
247
- const delayMs = Math.min(exponentialDelay + jitter, maxDelayMs);
248
- if (onRetry) onRetry(attempt + 1, delayMs, lastError);
249
- await sleep(delayMs);
250
- }
251
- throw lastError ?? /* @__PURE__ */ new Error("Failed to create DM channel after retries");
265
+ }, {
266
+ attempts: maxRetries + 1,
267
+ minDelayMs: Math.min(initialDelayMs, maxDelayMs),
268
+ maxDelayMs,
269
+ jitter: "full",
270
+ shouldRetry: (err) => isRetryableError(err),
271
+ onRetry: (info) => onRetry?.(info.attempt, info.delayMs, info.err)
272
+ });
252
273
  }
253
274
  function isRetryableError(error) {
254
275
  const candidates = collectErrorCandidates(error);
@@ -258,7 +279,9 @@ function isRetryableError(error) {
258
279
  for (const message of messages) {
259
280
  const clientErrorMatch = message.match(/mattermost api (4\d{2})\b/);
260
281
  if (!clientErrorMatch) continue;
261
- const statusCode = Number.parseInt(clientErrorMatch[1], 10);
282
+ const statusCodeText = clientErrorMatch[1];
283
+ if (!statusCodeText) continue;
284
+ const statusCode = Number.parseInt(statusCodeText, 10);
262
285
  if (statusCode >= 400 && statusCode < 500) return false;
263
286
  }
264
287
  if (messages.some((message) => /mattermost api \d{3}\b/.test(message))) return false;
@@ -360,77 +383,4 @@ async function uploadMattermostFile(client, params) {
360
383
  return info;
361
384
  }
362
385
  //#endregion
363
- //#region extensions/mattermost/src/mattermost/accounts.ts
364
- const mattermostAccountHelpers = createAccountListHelpers("mattermost", { hasImplicitDefaultAccount: (cfg) => {
365
- const mattermost = cfg.channels?.mattermost;
366
- return Boolean(mattermost?.baseUrl?.trim() && (hasConfiguredAccountValue(mattermost.botToken) || process.env.MATTERMOST_BOT_TOKEN?.trim()));
367
- } });
368
- function listMattermostAccountIds(cfg) {
369
- return mattermostAccountHelpers.listAccountIds(cfg);
370
- }
371
- function resolveDefaultMattermostAccountId(cfg) {
372
- return mattermostAccountHelpers.resolveDefaultAccountId(cfg);
373
- }
374
- function mergeMattermostAccountConfig(cfg, accountId) {
375
- return resolveMergedAccountConfig({
376
- channelConfig: cfg.channels?.mattermost,
377
- accounts: cfg.channels?.mattermost?.accounts,
378
- accountId,
379
- omitKeys: ["defaultAccount"],
380
- nestedObjectKeys: ["commands"]
381
- });
382
- }
383
- function resolveMattermostRequireMention(config) {
384
- if (config.chatmode === "oncall") return true;
385
- if (config.chatmode === "onmessage") return false;
386
- if (config.chatmode === "onchar") return true;
387
- return config.requireMention;
388
- }
389
- function resolveMattermostAccount(params) {
390
- const accountId = normalizeAccountId(params.accountId ?? resolveDefaultMattermostAccountId(params.cfg));
391
- const baseEnabled = params.cfg.channels?.mattermost?.enabled !== false;
392
- const merged = mergeMattermostAccountConfig(params.cfg, accountId);
393
- const accountEnabled = merged.enabled !== false;
394
- const enabled = baseEnabled && accountEnabled;
395
- const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
396
- const envToken = allowEnv ? process.env.MATTERMOST_BOT_TOKEN?.trim() : void 0;
397
- const envUrl = allowEnv ? process.env.MATTERMOST_URL?.trim() : void 0;
398
- const configToken = params.allowUnresolvedSecretRef ? normalizeSecretInputString(merged.botToken) : normalizeResolvedSecretInputString({
399
- value: merged.botToken,
400
- path: `channels.mattermost.accounts.${accountId}.botToken`
401
- });
402
- const configUrl = merged.baseUrl?.trim();
403
- const botToken = configToken || envToken;
404
- const baseUrl = normalizeMattermostBaseUrl(configUrl || envUrl);
405
- const requireMention = resolveMattermostRequireMention(merged);
406
- const botTokenSource = configToken ? "config" : envToken ? "env" : "none";
407
- const baseUrlSource = configUrl ? "config" : envUrl ? "env" : "none";
408
- return {
409
- accountId,
410
- enabled,
411
- name: normalizeOptionalString(merged.name),
412
- botToken,
413
- baseUrl,
414
- botTokenSource,
415
- baseUrlSource,
416
- config: merged,
417
- chatmode: merged.chatmode,
418
- oncharPrefixes: merged.oncharPrefixes,
419
- requireMention,
420
- textChunkLimit: merged.textChunkLimit,
421
- chunkMode: resolveChannelStreamingChunkMode(merged) ?? merged.chunkMode,
422
- streamingMode: resolveChannelPreviewStreamMode(merged, "partial"),
423
- blockStreaming: resolveChannelStreamingBlockEnabled(merged) ?? merged.blockStreaming,
424
- blockStreamingCoalesce: resolveChannelStreamingBlockCoalesce(merged) ?? merged.blockStreamingCoalesce
425
- };
426
- }
427
- /**
428
- * Resolve the effective replyToMode for a given chat type.
429
- * Mattermost auto-threading only applies to channel and group messages.
430
- */
431
- function resolveMattermostReplyToMode(account, kind) {
432
- if (kind === "direct") return "off";
433
- return account.config.replyToMode ?? "off";
434
- }
435
- //#endregion
436
- export { hasConfiguredSecretInput as C, buildSecretInputSchema as S, readMattermostError as _, MattermostPostSchema as a, updateMattermostPost as b, createMattermostPost as c, fetchMattermostChannelByName as d, fetchMattermostMe as f, normalizeMattermostBaseUrl as g, fetchMattermostUserTeams as h, resolveMattermostReplyToMode as i, deleteMattermostPost as l, fetchMattermostUserByUsername as m, resolveDefaultMattermostAccountId as n, createMattermostClient as o, fetchMattermostUser as p, resolveMattermostAccount as r, createMattermostDirectChannelWithRetry as s, listMattermostAccountIds as t, fetchMattermostChannel as u, resolveMattermostReplyDeliveryBarrierTimeoutMs as v, uploadMattermostFile as x, sendMattermostTyping as y };
386
+ export { uploadMattermostFile as _, deleteMattermostPost as a, fetchMattermostMe as c, fetchMattermostUserTeams as d, normalizeMattermostBaseUrl as f, updateMattermostPost as g, sendMattermostTyping as h, createMattermostPost as i, fetchMattermostUser as l, resolveMattermostReplyDeliveryBarrierTimeoutMs as m, createMattermostClient as n, fetchMattermostChannel as o, readMattermostError as p, createMattermostDirectChannelWithRetry as r, fetchMattermostChannelByName as s, MattermostPostSchema as t, fetchMattermostUserByUsername as u };
@@ -0,0 +1,19 @@
1
+ import { createLegacyPrivateNetworkDoctorContract } from "openclaw/plugin-sdk/ssrf-runtime";
2
+ import { defineChannelAliasMigration } from "openclaw/plugin-sdk/runtime-doctor";
3
+ //#region extensions/mattermost/src/doctor-contract.ts
4
+ const networkContract = createLegacyPrivateNetworkDoctorContract({ channelKey: "mattermost" });
5
+ const streamingAliasMigration = defineChannelAliasMigration({
6
+ channelId: "mattermost",
7
+ streaming: { defaultMode: "partial" },
8
+ accountStreamingReplacesRoot: true
9
+ });
10
+ const legacyConfigRules = [...networkContract.legacyConfigRules, ...streamingAliasMigration.legacyConfigRules];
11
+ function normalizeCompatibilityConfig({ cfg }) {
12
+ const network = networkContract.normalizeCompatibilityConfig({ cfg });
13
+ return streamingAliasMigration.normalizeChannelConfig({
14
+ cfg: network.config,
15
+ changes: network.changes
16
+ });
17
+ }
18
+ //#endregion
19
+ export { normalizeCompatibilityConfig as n, legacyConfigRules as t };
@@ -1,2 +1,2 @@
1
- import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-ttH0DCuq.js";
1
+ import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-BaUvVh8e.js";
2
2
  export { legacyConfigRules, normalizeCompatibilityConfig };
@@ -1,2 +1,2 @@
1
- import { t as resolveMattermostGatewayAuthBypassPaths } from "./gateway-auth-bypass-BIXLORHU.js";
1
+ import { t as resolveMattermostGatewayAuthBypassPaths } from "./gateway-auth-bypass-CL1PxV3z.js";
2
2
  export { resolveMattermostGatewayAuthBypassPaths as resolveGatewayAuthBypassPaths };
@@ -23,8 +23,9 @@ function collectMattermostSlashCallbackPaths(raw) {
23
23
  } catch {}
24
24
  return [...paths];
25
25
  }
26
- function resolveMattermostGatewayAuthBypassPaths(cfg) {
27
- const base = cfg.channels?.mattermost && typeof cfg.channels.mattermost === "object" ? cfg.channels.mattermost : void 0;
26
+ function resolveMattermostGatewayAuthBypassPaths(params) {
27
+ const channels = params.cfg.channels;
28
+ const base = channels?.mattermost && typeof channels.mattermost === "object" ? channels.mattermost : void 0;
28
29
  const callbackPaths = new Set(collectMattermostSlashCallbackPaths(readMattermostCommands(base?.commands)).filter(isMattermostBypassPath));
29
30
  const accounts = base?.accounts ?? {};
30
31
  for (const account of Object.values(accounts)) {
@@ -0,0 +1,375 @@
1
+ import { t as getMattermostRuntime } from "./runtime-CNB4YGqJ.js";
2
+ import { g as updateMattermostPost } from "./client-BzLj4dyf.js";
3
+ import { b as readRequestBodyWithLimit, m as isTrustedProxyAddress, w as resolveClientIp } from "./monitor-auth-dEHa1_On.js";
4
+ import { createHmac } from "node:crypto";
5
+ import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
6
+ import { normalizeOptionalString, normalizeStringifiedOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
7
+ //#region extensions/mattermost/src/mattermost/interactions.ts
8
+ const INTERACTION_MAX_BODY_BYTES = 64 * 1024;
9
+ const INTERACTION_BODY_TIMEOUT_MS = 1e4;
10
+ const SIGNED_CHANNEL_ID_CONTEXT_KEY = "__openclaw_channel_id";
11
+ const callbackUrls = /* @__PURE__ */ new Map();
12
+ function setInteractionCallbackUrl(accountId, url) {
13
+ callbackUrls.set(accountId, url);
14
+ }
15
+ function resolveInteractionCallbackPath(accountId) {
16
+ return `/mattermost/interactions/${accountId}`;
17
+ }
18
+ function isWildcardBindHost(rawHost) {
19
+ const trimmed = rawHost.trim();
20
+ if (!trimmed) return false;
21
+ const host = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
22
+ return host === "0.0.0.0" || host === "::" || host === "0:0:0:0:0:0:0:0" || host === "::0";
23
+ }
24
+ function normalizeCallbackBaseUrl(baseUrl) {
25
+ return baseUrl.trim().replace(/\/+$/, "");
26
+ }
27
+ function headerValue(value) {
28
+ if (Array.isArray(value)) return normalizeOptionalString(value[0]);
29
+ return normalizeOptionalString(value);
30
+ }
31
+ function isAllowedInteractionSource(params) {
32
+ const { allowedSourceIps } = params;
33
+ if (!allowedSourceIps?.length) return true;
34
+ return isTrustedProxyAddress(resolveClientIp({
35
+ remoteAddr: params.req.socket?.remoteAddress,
36
+ forwardedFor: headerValue(params.req.headers["x-forwarded-for"]),
37
+ realIp: headerValue(params.req.headers["x-real-ip"]),
38
+ trustedProxies: params.trustedProxies,
39
+ allowRealIpFallback: params.allowRealIpFallback
40
+ }), allowedSourceIps);
41
+ }
42
+ /**
43
+ * Resolve the interaction callback URL for an account.
44
+ * Falls back to computing it from interactions.callbackBaseUrl or gateway host config.
45
+ */
46
+ function computeInteractionCallbackUrl(accountId, cfg) {
47
+ const path = resolveInteractionCallbackPath(accountId);
48
+ const callbackBaseUrl = normalizeOptionalString(cfg?.interactions?.callbackBaseUrl) ?? normalizeOptionalString(cfg?.channels?.mattermost?.interactions?.callbackBaseUrl);
49
+ if (callbackBaseUrl) return `${normalizeCallbackBaseUrl(callbackBaseUrl)}${path}`;
50
+ const port = typeof cfg?.gateway?.port === "number" ? cfg.gateway.port : 18789;
51
+ let host = cfg?.gateway?.customBindHost && !isWildcardBindHost(cfg.gateway.customBindHost) ? cfg.gateway.customBindHost.trim() : "localhost";
52
+ if (host.includes(":") && !(host.startsWith("[") && host.endsWith("]"))) host = `[${host}]`;
53
+ return `http://${host}:${port}${path}`;
54
+ }
55
+ /**
56
+ * Resolve the interaction callback URL for an account.
57
+ * Prefers the in-memory registered URL (set by the gateway monitor) so callers outside the
58
+ * monitor lifecycle can reuse the runtime-validated callback destination.
59
+ */
60
+ function resolveInteractionCallbackUrl(accountId, cfg) {
61
+ const cached = callbackUrls.get(accountId);
62
+ if (cached) return cached;
63
+ return computeInteractionCallbackUrl(accountId, cfg);
64
+ }
65
+ const interactionSecrets = /* @__PURE__ */ new Map();
66
+ let defaultInteractionSecret;
67
+ function deriveInteractionSecret(botToken) {
68
+ return createHmac("sha256", "openclaw-mattermost-interactions").update(botToken).digest("hex");
69
+ }
70
+ function setInteractionSecret(accountIdOrBotToken, botToken) {
71
+ if (typeof botToken === "string") {
72
+ interactionSecrets.set(accountIdOrBotToken, deriveInteractionSecret(botToken));
73
+ return;
74
+ }
75
+ defaultInteractionSecret = deriveInteractionSecret(accountIdOrBotToken);
76
+ }
77
+ function getInteractionSecret(accountId) {
78
+ const scoped = accountId ? interactionSecrets.get(accountId) : void 0;
79
+ if (scoped) return scoped;
80
+ if (defaultInteractionSecret) return defaultInteractionSecret;
81
+ if (interactionSecrets.size === 1) {
82
+ const first = interactionSecrets.values().next().value;
83
+ if (typeof first === "string") return first;
84
+ }
85
+ throw new Error("Interaction secret not initialized — call setInteractionSecret(accountId, botToken) first");
86
+ }
87
+ function canonicalizeInteractionContext(value) {
88
+ if (Array.isArray(value)) return value.map((item) => canonicalizeInteractionContext(item));
89
+ if (value && typeof value === "object") {
90
+ const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).toSorted(([left], [right]) => left.localeCompare(right)).map(([key, entryValue]) => [key, canonicalizeInteractionContext(entryValue)]);
91
+ return Object.fromEntries(entries);
92
+ }
93
+ return value;
94
+ }
95
+ function generateInteractionToken(context, accountId) {
96
+ const secret = getInteractionSecret(accountId);
97
+ const payload = JSON.stringify(canonicalizeInteractionContext(context));
98
+ return createHmac("sha256", secret).update(payload).digest("hex");
99
+ }
100
+ function verifyInteractionToken(context, token, accountId) {
101
+ return safeEqualSecret(generateInteractionToken(context, accountId), token);
102
+ }
103
+ /**
104
+ * Build Mattermost `props.attachments` with interactive buttons.
105
+ *
106
+ * Each button includes an HMAC token in its integration context so the
107
+ * callback handler can verify the request originated from a legitimate
108
+ * button click (Mattermost's recommended security pattern).
109
+ */
110
+ /**
111
+ * Sanitize a button ID so Mattermost's action router can match it.
112
+ * Mattermost uses the action ID in the URL path `/api/v4/posts/{id}/actions/{actionId}`
113
+ * and IDs containing hyphens or underscores break the server-side routing.
114
+ * See: https://github.com/mattermost/mattermost/issues/25747
115
+ */
116
+ function sanitizeActionId(id) {
117
+ return id.replace(/[-_]/g, "");
118
+ }
119
+ function buildButtonAttachments(params) {
120
+ const actions = params.buttons.map((btn) => {
121
+ const safeId = sanitizeActionId(btn.id);
122
+ const context = {
123
+ action_id: safeId,
124
+ ...btn.context
125
+ };
126
+ const token = generateInteractionToken(context, params.accountId);
127
+ return {
128
+ id: safeId,
129
+ type: "button",
130
+ name: btn.name,
131
+ style: btn.style,
132
+ integration: {
133
+ url: params.callbackUrl,
134
+ context: {
135
+ ...context,
136
+ _token: token
137
+ }
138
+ }
139
+ };
140
+ });
141
+ return [{
142
+ text: params.text ?? "",
143
+ actions
144
+ }];
145
+ }
146
+ function buildButtonProps(params) {
147
+ const buttons = params.buttons.flatMap((item) => Array.isArray(item) ? item : [item]).map((btn) => ({
148
+ id: normalizeStringifiedOptionalString(btn.id ?? btn.callback_data) ?? "",
149
+ name: normalizeStringifiedOptionalString(btn.text ?? btn.name ?? btn.label) ?? "",
150
+ style: btn.style ?? "default",
151
+ context: typeof btn.context === "object" && btn.context !== null ? {
152
+ ...btn.context,
153
+ [SIGNED_CHANNEL_ID_CONTEXT_KEY]: params.channelId
154
+ } : { [SIGNED_CHANNEL_ID_CONTEXT_KEY]: params.channelId }
155
+ })).filter((btn) => btn.id && btn.name);
156
+ if (buttons.length === 0) return;
157
+ return { attachments: buildButtonAttachments({
158
+ callbackUrl: params.callbackUrl,
159
+ accountId: params.accountId,
160
+ buttons,
161
+ text: params.text
162
+ }) };
163
+ }
164
+ function readInteractionBody(req) {
165
+ return readRequestBodyWithLimit(req, {
166
+ maxBytes: INTERACTION_MAX_BODY_BYTES,
167
+ timeoutMs: INTERACTION_BODY_TIMEOUT_MS
168
+ });
169
+ }
170
+ function createMattermostInteractionHandler(params) {
171
+ const { client, accountId, log } = params;
172
+ const core = getMattermostRuntime();
173
+ function parseInteractionPayload(raw) {
174
+ try {
175
+ return JSON.parse(raw);
176
+ } catch {
177
+ throw new Error("Mattermost interaction body was malformed JSON");
178
+ }
179
+ }
180
+ return async (req, res) => {
181
+ if (req.method !== "POST") {
182
+ res.statusCode = 405;
183
+ res.setHeader("Allow", "POST");
184
+ res.setHeader("Content-Type", "application/json");
185
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
186
+ return;
187
+ }
188
+ if (!isAllowedInteractionSource({
189
+ req,
190
+ allowedSourceIps: params.allowedSourceIps,
191
+ trustedProxies: params.trustedProxies,
192
+ allowRealIpFallback: params.allowRealIpFallback
193
+ })) {
194
+ log?.(`mattermost interaction: rejected callback source remote=${req.socket?.remoteAddress ?? "?"}`);
195
+ res.statusCode = 403;
196
+ res.setHeader("Content-Type", "application/json");
197
+ res.end(JSON.stringify({ error: "Forbidden origin" }));
198
+ return;
199
+ }
200
+ let payload;
201
+ try {
202
+ payload = parseInteractionPayload(await readInteractionBody(req));
203
+ } catch (err) {
204
+ log?.(`mattermost interaction: failed to parse body: ${String(err)}`);
205
+ res.statusCode = 400;
206
+ res.setHeader("Content-Type", "application/json");
207
+ res.end(JSON.stringify({ error: "Invalid request body" }));
208
+ return;
209
+ }
210
+ const context = payload.context;
211
+ if (!context) {
212
+ res.statusCode = 400;
213
+ res.setHeader("Content-Type", "application/json");
214
+ res.end(JSON.stringify({ error: "Missing context" }));
215
+ return;
216
+ }
217
+ const token = context["_token"];
218
+ if (typeof token !== "string") {
219
+ log?.("mattermost interaction: missing _token in context");
220
+ res.statusCode = 403;
221
+ res.setHeader("Content-Type", "application/json");
222
+ res.end(JSON.stringify({ error: "Missing token" }));
223
+ return;
224
+ }
225
+ const { _token, ...contextWithoutToken } = context;
226
+ if (!verifyInteractionToken(contextWithoutToken, token, accountId)) {
227
+ log?.("mattermost interaction: invalid _token");
228
+ res.statusCode = 403;
229
+ res.setHeader("Content-Type", "application/json");
230
+ res.end(JSON.stringify({ error: "Invalid token" }));
231
+ return;
232
+ }
233
+ const actionId = context.action_id;
234
+ if (typeof actionId !== "string") {
235
+ res.statusCode = 400;
236
+ res.setHeader("Content-Type", "application/json");
237
+ res.end(JSON.stringify({ error: "Missing action_id in context" }));
238
+ return;
239
+ }
240
+ const signedChannelId = typeof contextWithoutToken[SIGNED_CHANNEL_ID_CONTEXT_KEY] === "string" ? contextWithoutToken[SIGNED_CHANNEL_ID_CONTEXT_KEY].trim() : "";
241
+ if (signedChannelId && signedChannelId !== payload.channel_id) {
242
+ log?.(`mattermost interaction: signed channel mismatch payload=${payload.channel_id} signed=${signedChannelId}`);
243
+ res.statusCode = 403;
244
+ res.setHeader("Content-Type", "application/json");
245
+ res.end(JSON.stringify({ error: "Channel mismatch" }));
246
+ return;
247
+ }
248
+ const userName = payload.user_name ?? payload.user_id;
249
+ let originalMessage;
250
+ let originalPost;
251
+ let clickedButtonName = null;
252
+ try {
253
+ originalPost = await client.request(`/posts/${payload.post_id}`);
254
+ const postChannelId = originalPost.channel_id?.trim();
255
+ if (!postChannelId || postChannelId !== payload.channel_id) {
256
+ log?.(`mattermost interaction: post channel mismatch payload=${payload.channel_id} post=${postChannelId ?? "<missing>"}`);
257
+ res.statusCode = 403;
258
+ res.setHeader("Content-Type", "application/json");
259
+ res.end(JSON.stringify({ error: "Post/channel mismatch" }));
260
+ return;
261
+ }
262
+ originalMessage = originalPost.message ?? "";
263
+ const postAttachments = Array.isArray(originalPost?.props?.attachments) ? originalPost.props.attachments : [];
264
+ for (const att of postAttachments) {
265
+ const match = att.actions?.find((a) => a.id === actionId);
266
+ if (match?.name) {
267
+ clickedButtonName = match.name;
268
+ break;
269
+ }
270
+ }
271
+ if (clickedButtonName === null) {
272
+ log?.(`mattermost interaction: action ${actionId} not found in post ${payload.post_id}`);
273
+ res.statusCode = 403;
274
+ res.setHeader("Content-Type", "application/json");
275
+ res.end(JSON.stringify({ error: "Unknown action" }));
276
+ return;
277
+ }
278
+ } catch (err) {
279
+ log?.(`mattermost interaction: failed to validate post ${payload.post_id}: ${String(err)}`);
280
+ res.statusCode = 500;
281
+ res.setHeader("Content-Type", "application/json");
282
+ res.end(JSON.stringify({ error: "Failed to validate interaction" }));
283
+ return;
284
+ }
285
+ if (!originalPost) {
286
+ log?.(`mattermost interaction: missing fetched post ${payload.post_id}`);
287
+ res.statusCode = 500;
288
+ res.setHeader("Content-Type", "application/json");
289
+ res.end(JSON.stringify({ error: "Failed to load interaction post" }));
290
+ return;
291
+ }
292
+ log?.(`mattermost interaction: action=${actionId} user=${payload.user_name ?? payload.user_id} post=${payload.post_id} channel=${payload.channel_id}`);
293
+ if (params.authorizeButtonClick) try {
294
+ const authorization = await params.authorizeButtonClick({
295
+ payload,
296
+ post: originalPost
297
+ });
298
+ if (!authorization.ok) {
299
+ res.statusCode = authorization.statusCode ?? 200;
300
+ res.setHeader("Content-Type", "application/json");
301
+ res.end(JSON.stringify(authorization.response ?? { ephemeral_text: "You are not allowed to use this action here." }));
302
+ return;
303
+ }
304
+ } catch (err) {
305
+ log?.(`mattermost interaction: authorization failed: ${String(err)}`);
306
+ res.statusCode = 500;
307
+ res.setHeader("Content-Type", "application/json");
308
+ res.end(JSON.stringify({ error: "Interaction authorization failed" }));
309
+ return;
310
+ }
311
+ if (params.handleInteraction) try {
312
+ const response = await params.handleInteraction({
313
+ payload,
314
+ userName,
315
+ actionId,
316
+ actionName: clickedButtonName,
317
+ originalMessage,
318
+ context: contextWithoutToken,
319
+ post: originalPost
320
+ });
321
+ if (response !== null) {
322
+ res.statusCode = 200;
323
+ res.setHeader("Content-Type", "application/json");
324
+ res.end(JSON.stringify(response));
325
+ return;
326
+ }
327
+ } catch (err) {
328
+ log?.(`mattermost interaction: custom handler failed: ${String(err)}`);
329
+ res.statusCode = 500;
330
+ res.setHeader("Content-Type", "application/json");
331
+ res.end(JSON.stringify({ error: "Interaction handler failed" }));
332
+ return;
333
+ }
334
+ try {
335
+ const eventLabel = `Mattermost button click: action="${actionId}" by ${payload.user_name ?? payload.user_id} in channel ${payload.channel_id}`;
336
+ const sessionKey = params.resolveSessionKey ? await params.resolveSessionKey({
337
+ channelId: payload.channel_id,
338
+ userId: payload.user_id,
339
+ post: originalPost
340
+ }) : `agent:main:mattermost:${accountId}:${payload.channel_id}`;
341
+ core.system.enqueueSystemEvent(eventLabel, {
342
+ sessionKey,
343
+ contextKey: `mattermost:interaction:${payload.post_id}:${actionId}`
344
+ });
345
+ } catch (err) {
346
+ log?.(`mattermost interaction: system event dispatch failed: ${String(err)}`);
347
+ }
348
+ try {
349
+ await updateMattermostPost(client, payload.post_id, {
350
+ message: originalMessage,
351
+ props: { attachments: [{ text: `✓ **${clickedButtonName}** selected by @${userName}` }] }
352
+ });
353
+ } catch (err) {
354
+ log?.(`mattermost interaction: failed to update post ${payload.post_id}: ${String(err)}`);
355
+ }
356
+ res.statusCode = 200;
357
+ res.setHeader("Content-Type", "application/json");
358
+ res.end("{}");
359
+ if (params.dispatchButtonClick) try {
360
+ await params.dispatchButtonClick({
361
+ channelId: payload.channel_id,
362
+ userId: payload.user_id,
363
+ userName,
364
+ actionId,
365
+ actionName: clickedButtonName,
366
+ postId: payload.post_id,
367
+ post: originalPost
368
+ });
369
+ } catch (err) {
370
+ log?.(`mattermost interaction: dispatchButtonClick failed: ${String(err)}`);
371
+ }
372
+ };
373
+ }
374
+ //#endregion
375
+ export { resolveInteractionCallbackPath as a, setInteractionSecret as c, createMattermostInteractionHandler as i, buildButtonProps as n, resolveInteractionCallbackUrl as o, computeInteractionCallbackUrl as r, setInteractionCallbackUrl as s, buildButtonAttachments as t };
@@ -1,2 +1,2 @@
1
- import { r as isMattermostSenderAllowed } from "./monitor-auth-BiDuyvOc.js";
1
+ import { r as isMattermostSenderAllowed } from "./monitor-auth-dEHa1_On.js";
2
2
  export { isMattermostSenderAllowed };
@@ -0,0 +1,28 @@
1
+ import { collectSimpleChannelFieldAssignments, createChannelSecretTargetRegistryEntries, getChannelSurface } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
2
+ //#region extensions/mattermost/src/secret-contract.ts
3
+ const secretTargetRegistryEntries = createChannelSecretTargetRegistryEntries({
4
+ channelKey: "mattermost",
5
+ account: ["botToken"],
6
+ channel: ["botToken"]
7
+ });
8
+ function collectRuntimeConfigAssignments(params) {
9
+ const resolved = getChannelSurface(params.config, "mattermost");
10
+ if (!resolved) return;
11
+ const { channel: mattermost, surface } = resolved;
12
+ collectSimpleChannelFieldAssignments({
13
+ channelKey: "mattermost",
14
+ field: "botToken",
15
+ channel: mattermost,
16
+ surface,
17
+ defaults: params.defaults,
18
+ context: params.context,
19
+ topInactiveReason: "no enabled account inherits this top-level Mattermost botToken.",
20
+ accountInactiveReason: "Mattermost account is disabled."
21
+ });
22
+ }
23
+ const channelSecrets = {
24
+ secretTargetRegistryEntries,
25
+ collectRuntimeConfigAssignments
26
+ };
27
+ //#endregion
28
+ export { collectRuntimeConfigAssignments as n, secretTargetRegistryEntries as r, channelSecrets as t };
@@ -1,2 +1,2 @@
1
- import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries, t as channelSecrets } from "./secret-contract-Cx0LUNXy.js";
1
+ import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries, t as channelSecrets } from "./secret-contract-B4R5Bmhv.js";
2
2
  export { channelSecrets, collectRuntimeConfigAssignments, secretTargetRegistryEntries };