@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.
- package/dist/accounts-C4Azn6rA.js +83 -0
- package/dist/api.js +3 -2
- package/dist/channel-plugin-api.js +3 -3
- package/dist/{channel-plugin-runtime-DhS8rwfs.js → channel-plugin-runtime-BXmjNn6B.js} +131 -167
- package/dist/channel-plugin-runtime.js +1 -1
- package/dist/{channel.runtime-D0SJSEei.js → channel.runtime-CKb2-z0p.js} +560 -296
- package/dist/{accounts-ITTlduDO.js → client-BzLj4dyf.js} +52 -102
- package/dist/doctor-contract-BaUvVh8e.js +19 -0
- package/dist/doctor-contract-api.js +1 -1
- package/dist/gateway-auth-api.js +1 -1
- package/dist/{gateway-auth-bypass-BIXLORHU.js → gateway-auth-bypass-CL1PxV3z.js} +3 -2
- package/dist/interactions-DO7J-ND1.js +375 -0
- package/dist/policy-api.js +1 -1
- package/dist/secret-contract-B4R5Bmhv.js +28 -0
- package/dist/secret-contract-api.js +1 -1
- package/dist/slash-route-api.js +1 -1
- package/dist/{slash-state-BfOSlkmn.js → slash-state-C9bpBN40.js} +124 -470
- package/npm-shrinkwrap.json +3 -3
- package/openclaw.plugin.json +209 -209
- package/package.json +4 -4
- package/dist/doctor-contract-ttH0DCuq.js +0 -7
- package/dist/secret-contract-Cx0LUNXy.js +0 -44
- package/dist/{monitor-auth-BiDuyvOc.js → monitor-auth-dEHa1_On.js} +3 -3
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
1
|
+
import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-BaUvVh8e.js";
|
|
2
2
|
export { legacyConfigRules, normalizeCompatibilityConfig };
|
package/dist/gateway-auth-api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as resolveMattermostGatewayAuthBypassPaths } from "./gateway-auth-bypass-
|
|
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(
|
|
27
|
-
const
|
|
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 };
|
package/dist/policy-api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { r as isMattermostSenderAllowed } from "./monitor-auth-
|
|
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-
|
|
1
|
+
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries, t as channelSecrets } from "./secret-contract-B4R5Bmhv.js";
|
|
2
2
|
export { channelSecrets, collectRuntimeConfigAssignments, secretTargetRegistryEntries };
|