@openclaw/crabline 0.1.11 → 0.1.13
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/README.md +137 -23
- package/dist/src/bin/crabline.js +3 -1
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +5 -1
- package/dist/src/cli/program.js +281 -52
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +39 -3
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +20 -0
- package/dist/src/config/schema.js +163 -35
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.d.ts +1 -0
- package/dist/src/core/errors.js +6 -3
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/http-path.d.ts +1 -0
- package/dist/src/core/http-path.js +14 -0
- package/dist/src/core/http-path.js.map +1 -0
- package/dist/src/core/matcher.d.ts +3 -1
- package/dist/src/core/matcher.js +58 -2
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/nonces.d.ts +2 -0
- package/dist/src/core/nonces.js +11 -1
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +5 -0
- package/dist/src/core/reporters.js +26 -2
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -1
- package/dist/src/core/run.js +178 -66
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/index.d.ts +12 -10
- package/dist/src/index.js +6 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/matrix-ids.d.ts +5 -0
- package/dist/src/matrix-ids.js +83 -0
- package/dist/src/matrix-ids.js.map +1 -0
- package/dist/src/openclaw/artifact-generation.d.ts +7 -6
- package/dist/src/openclaw/artifact-generation.js +385 -41
- package/dist/src/openclaw/artifact-generation.js.map +1 -1
- package/dist/src/openclaw/bridges/discord.d.ts +1 -0
- package/dist/src/openclaw/bridges/discord.js +149 -0
- package/dist/src/openclaw/bridges/discord.js.map +1 -0
- package/dist/src/openclaw/bridges/matrix.js +22 -5
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +14 -9
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
- package/dist/src/openclaw/bridges/probe-response.js +5 -0
- package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
- package/dist/src/openclaw/bridges/signal.js +16 -7
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +117 -24
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +46 -20
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +33 -17
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +9 -4
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/outbound-contract.js +3 -0
- package/dist/src/openclaw/outbound-contract.js.map +1 -1
- package/dist/src/openclaw/private-file.d.ts +38 -3
- package/dist/src/openclaw/private-file.js +2008 -87
- package/dist/src/openclaw/private-file.js.map +1 -1
- package/dist/src/openclaw/{smoke-lock.d.ts → provider-readiness-lock.d.ts} +14 -5
- package/dist/src/openclaw/provider-readiness-lock.js +1320 -0
- package/dist/src/openclaw/provider-readiness-lock.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +10 -17
- package/dist/src/openclaw/shared.js +53 -19
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw.d.ts +3 -5
- package/dist/src/openclaw.js +245 -60
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/platform/process-owned-lock.d.ts +11 -0
- package/dist/src/platform/process-owned-lock.js +1698 -0
- package/dist/src/platform/process-owned-lock.js.map +1 -0
- package/dist/src/platform/recorder-directory.d.ts +1 -0
- package/dist/src/platform/recorder-directory.js +14 -0
- package/dist/src/platform/recorder-directory.js.map +1 -0
- package/dist/src/platform/windows-acl.d.ts +18 -0
- package/dist/src/platform/windows-acl.js +1367 -0
- package/dist/src/platform/windows-acl.js.map +1 -0
- package/dist/src/platform/windows-lock-root.d.ts +13 -0
- package/dist/src/platform/windows-lock-root.js +98 -0
- package/dist/src/platform/windows-lock-root.js.map +1 -0
- package/dist/src/providers/builtin/discord.d.ts +4 -0
- package/dist/src/providers/builtin/discord.js +52 -9
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
- package/dist/src/providers/builtin/feishu.d.ts +1 -0
- package/dist/src/providers/builtin/feishu.js +96 -26
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +1 -0
- package/dist/src/providers/builtin/googlechat.js +95 -26
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.js +60 -17
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +88 -35
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +2 -2
- package/dist/src/providers/builtin/matrix.js +78 -21
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +11 -7
- package/dist/src/providers/builtin/mattermost.js +138 -24
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +1 -0
- package/dist/src/providers/builtin/msteams.js +118 -38
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.js +4 -4
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.js +1057 -146
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +8 -1
- package/dist/src/providers/builtin/slack.js +327 -24
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +1 -1
- package/dist/src/providers/builtin/telegram.js +55 -14
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
- package/dist/src/providers/builtin/whatsapp.js +157 -29
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.d.ts +5 -2
- package/dist/src/providers/builtin/zalo.js +31 -7
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +1 -1
- package/dist/src/providers/catalog.js +1 -1
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +4 -0
- package/dist/src/providers/local-mock.js +162 -35
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +2 -0
- package/dist/src/providers/native-ids.js +3 -0
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +14 -3
- package/dist/src/providers/recorder.js +521 -41
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +1 -0
- package/dist/src/providers/registry.js +65 -20
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +6 -1
- package/dist/src/providers/signed-jwt.js +241 -50
- package/dist/src/providers/signed-jwt.js.map +1 -1
- package/dist/src/providers/slack-ids.d.ts +1 -0
- package/dist/src/providers/slack-ids.js +5 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +9 -1
- package/dist/src/providers/target-normalizers.js +177 -58
- package/dist/src/providers/target-normalizers.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +3 -0
- package/dist/src/providers/webhook-server.js +268 -47
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/discord.d.ts +40 -0
- package/dist/src/servers/discord.js +842 -0
- package/dist/src/servers/discord.js.map +1 -0
- package/dist/src/servers/http.d.ts +14 -2
- package/dist/src/servers/http.js +229 -58
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/index.d.ts +10 -4
- package/dist/src/servers/index.js +4 -0
- package/dist/src/servers/index.js.map +1 -1
- package/dist/src/servers/matrix.d.ts +3 -0
- package/dist/src/servers/matrix.js +268 -110
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +389 -91
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/recorder.d.ts +11 -0
- package/dist/src/servers/recorder.js +1072 -69
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +1 -0
- package/dist/src/servers/signal.js +279 -44
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +15 -0
- package/dist/src/servers/slack.js +270 -54
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram-identity.d.ts +6 -0
- package/dist/src/servers/telegram-identity.js +27 -0
- package/dist/src/servers/telegram-identity.js.map +1 -0
- package/dist/src/servers/telegram.d.ts +17 -6
- package/dist/src/servers/telegram.js +1218 -107
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +12 -4
- package/dist/src/servers/webhook-target.js +226 -28
- package/dist/src/servers/webhook-target.js.map +1 -1
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +1 -0
- package/dist/src/servers/whatsapp-jid.js +9 -5
- package/dist/src/servers/whatsapp-jid.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
- package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
- package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js +3 -3
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +7 -1
- package/dist/src/servers/whatsapp.js +149 -25
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +1 -0
- package/dist/src/servers/zalo.js +193 -82
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +251 -64
- package/fixtures/examples/crabline.example.yaml +3 -2
- package/package.json +18 -10
- package/dist/src/openclaw/smoke-lock.js +0 -1004
- package/dist/src/openclaw/smoke-lock.js.map +0 -1
- package/fixtures/examples/openclaw-bridge.yaml +0 -125
|
@@ -1,39 +1,118 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { CrablineError } from "../../core/errors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { isLoopbackHost } from "../../servers/http.js";
|
|
4
|
+
import { LocalMockProviderAdapter, resolveGeneratedLocalMockRecorderPath } from "../local-mock.js";
|
|
5
|
+
import { appendRecordedInbound } from "../recorder.js";
|
|
4
6
|
import { getBuiltinTargetCodec, MATTERMOST_ID_RULE } from "../target-normalizers.js";
|
|
5
|
-
import { authorFromBotFlag, genericMockPayloadWithNativeThread, isRecord, optionalRecord, optionalString, requireNativeInboundId, } from "./native-local-mock.js";
|
|
7
|
+
import { authorFromBotFlag, createSecretVerifier, genericMockPayloadWithNativeThread, isRecord, optionalRecord, optionalString, requireNativeInboundId, } from "./native-local-mock.js";
|
|
6
8
|
import { requireExternalWebhookAuthentication } from "./external-webhook-auth.js";
|
|
7
9
|
export function resolveMattermostAdapterConfig(config, env = process.env) {
|
|
10
|
+
const baseUrl = config.mattermost?.baseUrl ??
|
|
11
|
+
env.MATTERMOST_URL ??
|
|
12
|
+
env.MATTERMOST_BASE_URL ??
|
|
13
|
+
"http://127.0.0.1";
|
|
14
|
+
let parsedBaseUrl;
|
|
15
|
+
try {
|
|
16
|
+
parsedBaseUrl = new URL(baseUrl);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new CrablineError("Mattermost base URL is invalid.", {
|
|
20
|
+
cause: error,
|
|
21
|
+
kind: "config",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (parsedBaseUrl.protocol !== "https:" &&
|
|
25
|
+
!(parsedBaseUrl.protocol === "http:" && isLoopbackHost(parsedBaseUrl.hostname))) {
|
|
26
|
+
throw new CrablineError("Mattermost bearer authentication requires HTTPS; plain HTTP is allowed only for loopback-local servers.", { kind: "config" });
|
|
27
|
+
}
|
|
28
|
+
const configuredWebhookToken = config.mattermost?.webhookToken;
|
|
29
|
+
const webhookToken = configuredWebhookToken ?? env.MATTERMOST_TOKEN;
|
|
30
|
+
if (webhookToken !== undefined && !webhookToken.trim()) {
|
|
31
|
+
throw new CrablineError(configuredWebhookToken === undefined
|
|
32
|
+
? "MATTERMOST_TOKEN must not be empty or whitespace-only."
|
|
33
|
+
: "Mattermost webhookToken must not be empty or whitespace-only.", { kind: "config" });
|
|
34
|
+
}
|
|
8
35
|
return {
|
|
9
|
-
baseUrl
|
|
36
|
+
baseUrl,
|
|
10
37
|
botToken: config.mattermost?.botToken ?? env.MATTERMOST_BOT_TOKEN ?? "local-mock-token",
|
|
11
38
|
userName: config.mattermost?.userName,
|
|
39
|
+
webhookToken,
|
|
12
40
|
};
|
|
13
41
|
}
|
|
14
42
|
export class MattermostProviderAdapter extends LocalMockProviderAdapter {
|
|
15
|
-
constructor(id, config, _userName,
|
|
43
|
+
constructor(id, config, _userName, runtime) {
|
|
44
|
+
const env = runtime?.env ?? process.env;
|
|
45
|
+
const resolvedConfig = resolveMattermostAdapterConfig(config, env);
|
|
46
|
+
const authenticateWebhook = resolvedConfig.webhookToken
|
|
47
|
+
? createSecretVerifier(resolvedConfig.webhookToken)
|
|
48
|
+
: undefined;
|
|
16
49
|
requireExternalWebhookAuthentication({
|
|
17
|
-
authenticated:
|
|
50
|
+
authenticated: Boolean(authenticateWebhook),
|
|
18
51
|
provider: "Mattermost",
|
|
19
|
-
requirement: "
|
|
52
|
+
requirement: "webhookToken or MATTERMOST_TOKEN",
|
|
20
53
|
webhook: config.mattermost?.webhook,
|
|
21
54
|
});
|
|
55
|
+
const recorderPath = config.mattermost?.recorder.path
|
|
56
|
+
? path.resolve(config.mattermost.recorder.path)
|
|
57
|
+
: resolveGeneratedLocalMockRecorderPath(id);
|
|
22
58
|
super({
|
|
23
59
|
codec: getBuiltinTargetCodec("mattermost"),
|
|
24
60
|
config,
|
|
25
61
|
id,
|
|
26
62
|
options: {
|
|
63
|
+
...(authenticateWebhook
|
|
64
|
+
? {
|
|
65
|
+
authenticateWebhookRequest(request, rawBody) {
|
|
66
|
+
return authenticateWebhook(readMattermostWebhookToken(request, rawBody))
|
|
67
|
+
? undefined
|
|
68
|
+
: new Response("unauthorized", { status: 401 });
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
: {}),
|
|
27
72
|
defaultWebhook: { host: "127.0.0.1", path: "/mattermost/webhook", port: 8793 },
|
|
28
73
|
endpointLabel: "webhook endpoint",
|
|
29
74
|
matchesThread: matchesMattermostThread,
|
|
75
|
+
handleWebhookPayload: async (payload, request) => {
|
|
76
|
+
const mediaType = request.headers
|
|
77
|
+
.get("content-type")
|
|
78
|
+
?.split(";", 1)[0]
|
|
79
|
+
?.trim()
|
|
80
|
+
.toLowerCase();
|
|
81
|
+
if (mediaType !== "application/x-www-form-urlencoded" || typeof payload !== "string") {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
let normalized;
|
|
85
|
+
try {
|
|
86
|
+
normalized = normalizeMattermostWebhookPayload(Object.fromEntries(new URLSearchParams(payload).entries()));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error instanceof CrablineError && error.kind === "inbound") {
|
|
90
|
+
return new Response(error.message, { status: 400 });
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
const messageId = normalized.id ??
|
|
95
|
+
`mattermost-mock-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
96
|
+
await appendRecordedInbound(recorderPath, {
|
|
97
|
+
author: normalized.author,
|
|
98
|
+
id: messageId,
|
|
99
|
+
provider: id,
|
|
100
|
+
raw: normalized.raw,
|
|
101
|
+
recordedDirection: "inbound",
|
|
102
|
+
sentAt: new Date().toISOString(),
|
|
103
|
+
text: normalized.text,
|
|
104
|
+
threadId: normalized.threadId,
|
|
105
|
+
});
|
|
106
|
+
return new Response(JSON.stringify({ id: messageId, ok: true }), {
|
|
107
|
+
headers: { "content-type": "application/json" },
|
|
108
|
+
});
|
|
109
|
+
},
|
|
30
110
|
normalizeWebhookPayload: normalizeMattermostWebhookPayload,
|
|
31
111
|
platform: "mattermost",
|
|
32
112
|
publicUrl: config.mattermost?.webhook.publicUrl,
|
|
33
|
-
recorderPath
|
|
34
|
-
? path.resolve(config.mattermost.recorder.path)
|
|
35
|
-
: undefined,
|
|
113
|
+
recorderPath,
|
|
36
114
|
webhook: config.mattermost?.webhook,
|
|
115
|
+
webhookMethods: ["POST"],
|
|
37
116
|
},
|
|
38
117
|
});
|
|
39
118
|
}
|
|
@@ -45,31 +124,66 @@ export function matchesMattermostThread(candidateThreadId, expectedThreadId, tar
|
|
|
45
124
|
if (!expectedThreadId) {
|
|
46
125
|
return true;
|
|
47
126
|
}
|
|
48
|
-
|
|
127
|
+
if (target.channelId &&
|
|
49
128
|
MATTERMOST_ID_RULE.pattern.test(expectedThreadId) &&
|
|
50
|
-
expectedThreadId !== target.channelId
|
|
51
|
-
|
|
52
|
-
|
|
129
|
+
expectedThreadId !== target.channelId) {
|
|
130
|
+
return candidateThreadId === mattermostThreadKey(target.channelId, expectedThreadId);
|
|
131
|
+
}
|
|
53
132
|
return (candidateThreadId === expectedThreadId ||
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
133
|
+
(MATTERMOST_ID_RULE.pattern.test(expectedThreadId) &&
|
|
134
|
+
candidateThreadId.startsWith(`${expectedThreadId}:thread:`)));
|
|
135
|
+
}
|
|
136
|
+
function readMattermostWebhookToken(request, rawBody) {
|
|
137
|
+
const mediaType = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
138
|
+
if (mediaType === "application/x-www-form-urlencoded") {
|
|
139
|
+
return new URLSearchParams(rawBody).get("token");
|
|
140
|
+
}
|
|
141
|
+
if (mediaType !== "application/json") {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const payload = JSON.parse(rawBody);
|
|
146
|
+
return isRecord(payload) && typeof payload.token === "string" ? payload.token : null;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function withoutMattermostWebhookToken(payload) {
|
|
153
|
+
const { token: _token, ...safePayload } = payload;
|
|
154
|
+
return safePayload;
|
|
57
155
|
}
|
|
58
156
|
export function normalizeMattermostWebhookPayload(payload) {
|
|
59
157
|
if (!isRecord(payload)) {
|
|
60
158
|
throw new CrablineError("Mattermost webhook payload must be an object", { kind: "inbound" });
|
|
61
159
|
}
|
|
62
|
-
|
|
63
|
-
|
|
160
|
+
const safePayload = withoutMattermostWebhookToken(payload);
|
|
161
|
+
const genericMessage = optionalRecord(safePayload, "message");
|
|
162
|
+
if (genericMessage) {
|
|
163
|
+
const normalized = genericMockPayloadWithNativeThread({
|
|
64
164
|
channelRule: MATTERMOST_ID_RULE,
|
|
65
|
-
payload,
|
|
165
|
+
payload: safePayload,
|
|
66
166
|
threadRule: MATTERMOST_ID_RULE,
|
|
67
167
|
});
|
|
168
|
+
const channelId = optionalString(genericMessage, "channelId") ?? optionalString(safePayload, "channelId");
|
|
169
|
+
const rootId = optionalString(genericMessage, "threadId") ?? optionalString(safePayload, "threadId");
|
|
170
|
+
if (!channelId || !rootId || channelId === rootId) {
|
|
171
|
+
// Without channelId, the generic threadId is the channel-level conversation.
|
|
172
|
+
return normalized;
|
|
173
|
+
}
|
|
174
|
+
const scopedThreadId = mattermostThreadKey(requireNativeInboundId(channelId, MATTERMOST_ID_RULE, "Mattermost channelId"), requireNativeInboundId(rootId, MATTERMOST_ID_RULE, "Mattermost threadId"));
|
|
175
|
+
return {
|
|
176
|
+
...normalized,
|
|
177
|
+
...("message" in normalized && isRecord(normalized.message)
|
|
178
|
+
? { message: { ...normalized.message, threadId: scopedThreadId } }
|
|
179
|
+
: {}),
|
|
180
|
+
threadId: scopedThreadId,
|
|
181
|
+
};
|
|
68
182
|
}
|
|
69
|
-
const channelId = optionalString(
|
|
70
|
-
const postId = optionalString(
|
|
71
|
-
const rootId = optionalString(
|
|
72
|
-
const text = optionalString(
|
|
183
|
+
const channelId = optionalString(safePayload, "channel_id");
|
|
184
|
+
const postId = optionalString(safePayload, "post_id");
|
|
185
|
+
const rootId = optionalString(safePayload, "root_id");
|
|
186
|
+
const text = optionalString(safePayload, "text");
|
|
73
187
|
if (!channelId || !text) {
|
|
74
188
|
throw new CrablineError("Mattermost webhook payload requires channel_id and text", {
|
|
75
189
|
kind: "inbound",
|
|
@@ -80,7 +194,7 @@ export function normalizeMattermostWebhookPayload(payload) {
|
|
|
80
194
|
...(postId
|
|
81
195
|
? { id: requireNativeInboundId(postId, MATTERMOST_ID_RULE, "Mattermost post_id") }
|
|
82
196
|
: {}),
|
|
83
|
-
raw:
|
|
197
|
+
raw: safePayload,
|
|
84
198
|
text,
|
|
85
199
|
threadId: rootId
|
|
86
200
|
? mattermostThreadKey(requireNativeInboundId(channelId, MATTERMOST_ID_RULE, "Mattermost channel_id"), requireNativeInboundId(rootId, MATTERMOST_ID_RULE, "Mattermost root_id"))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mattermost.js","sourceRoot":"","sources":["../../../../src/providers/builtin/mattermost.ts"],"names":[],"mappings":"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","sourcesContent":["import path from \"node:path\";\nimport { CrablineError } from \"../../core/errors.js\";\nimport type { ProviderConfig } from \"../../config/schema.js\";\nimport { LocalMockProviderAdapter } from \"../local-mock.js\";\nimport type { ProviderAdapter } from \"../types.js\";\nimport { getBuiltinTargetCodec, MATTERMOST_ID_RULE } from \"../target-normalizers.js\";\nimport {\n authorFromBotFlag,\n genericMockPayloadWithNativeThread,\n isRecord,\n optionalRecord,\n optionalString,\n requireNativeInboundId,\n} from \"./native-local-mock.js\";\nimport { requireExternalWebhookAuthentication } from \"./external-webhook-auth.js\";\n\nexport function resolveMattermostAdapterConfig(\n config: ProviderConfig,\n env: NodeJS.ProcessEnv = process.env,\n) {\n return {\n baseUrl: config.mattermost?.baseUrl ?? env.MATTERMOST_BASE_URL ?? \"http://mattermost.local\",\n botToken: config.mattermost?.botToken ?? env.MATTERMOST_BOT_TOKEN ?? \"local-mock-token\",\n userName: config.mattermost?.userName,\n };\n}\n\nexport class MattermostProviderAdapter extends LocalMockProviderAdapter implements ProviderAdapter {\n constructor(id: string, config: ProviderConfig, _userName: string, _runtime?: unknown) {\n requireExternalWebhookAuthentication({\n authenticated: false,\n provider: \"Mattermost\",\n requirement:\n \"a provider-native authenticated ingress mode, which this adapter does not support\",\n webhook: config.mattermost?.webhook,\n });\n super({\n codec: getBuiltinTargetCodec(\"mattermost\"),\n config,\n id,\n options: {\n defaultWebhook: { host: \"127.0.0.1\", path: \"/mattermost/webhook\", port: 8793 },\n endpointLabel: \"webhook endpoint\",\n matchesThread: matchesMattermostThread,\n normalizeWebhookPayload: normalizeMattermostWebhookPayload,\n platform: \"mattermost\",\n publicUrl: config.mattermost?.webhook.publicUrl,\n recorderPath: config.mattermost?.recorder.path\n ? path.resolve(config.mattermost.recorder.path)\n : undefined,\n webhook: config.mattermost?.webhook,\n },\n });\n }\n}\n\nfunction mattermostThreadKey(channelId: string, rootId: string): string {\n return `${channelId}:thread:${rootId}`;\n}\n\nexport function matchesMattermostThread(\n candidateThreadId: string,\n expectedThreadId: string | undefined,\n target: { channelId?: string | undefined },\n): boolean {\n if (!expectedThreadId) {\n return true;\n }\n const scopedExpected =\n target.channelId &&\n MATTERMOST_ID_RULE.pattern.test(expectedThreadId) &&\n expectedThreadId !== target.channelId\n ? mattermostThreadKey(target.channelId, expectedThreadId)\n : expectedThreadId;\n return (\n candidateThreadId === expectedThreadId ||\n candidateThreadId === scopedExpected ||\n (MATTERMOST_ID_RULE.pattern.test(scopedExpected) &&\n candidateThreadId.startsWith(`${scopedExpected}:thread:`))\n );\n}\n\nexport function normalizeMattermostWebhookPayload(payload: unknown) {\n if (!isRecord(payload)) {\n throw new CrablineError(\"Mattermost webhook payload must be an object\", { kind: \"inbound\" });\n }\n\n if (optionalRecord(payload, \"message\")) {\n return genericMockPayloadWithNativeThread({\n channelRule: MATTERMOST_ID_RULE,\n payload,\n threadRule: MATTERMOST_ID_RULE,\n });\n }\n\n const channelId = optionalString(payload, \"channel_id\");\n const postId = optionalString(payload, \"post_id\");\n const rootId = optionalString(payload, \"root_id\");\n const text = optionalString(payload, \"text\");\n if (!channelId || !text) {\n throw new CrablineError(\"Mattermost webhook payload requires channel_id and text\", {\n kind: \"inbound\",\n });\n }\n\n return {\n author: authorFromBotFlag(false),\n ...(postId\n ? { id: requireNativeInboundId(postId, MATTERMOST_ID_RULE, \"Mattermost post_id\") }\n : {}),\n raw: payload,\n text,\n threadId: rootId\n ? mattermostThreadKey(\n requireNativeInboundId(channelId, MATTERMOST_ID_RULE, \"Mattermost channel_id\"),\n requireNativeInboundId(rootId, MATTERMOST_ID_RULE, \"Mattermost root_id\"),\n )\n : requireNativeInboundId(channelId, MATTERMOST_ID_RULE, \"Mattermost channel_id\"),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"mattermost.js","sourceRoot":"","sources":["../../../../src/providers/builtin/mattermost.ts"],"names":[],"mappings":"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","sourcesContent":["import path from \"node:path\";\nimport { CrablineError } from \"../../core/errors.js\";\nimport type { ProviderConfig } from \"../../config/schema.js\";\nimport { isLoopbackHost } from \"../../servers/http.js\";\nimport { LocalMockProviderAdapter, resolveGeneratedLocalMockRecorderPath } from \"../local-mock.js\";\nimport { appendRecordedInbound } from \"../recorder.js\";\nimport type { ProviderAdapter } from \"../types.js\";\nimport { getBuiltinTargetCodec, MATTERMOST_ID_RULE } from \"../target-normalizers.js\";\nimport {\n authorFromBotFlag,\n createSecretVerifier,\n genericMockPayloadWithNativeThread,\n isRecord,\n optionalRecord,\n optionalString,\n requireNativeInboundId,\n} from \"./native-local-mock.js\";\nimport { requireExternalWebhookAuthentication } from \"./external-webhook-auth.js\";\n\ntype MattermostEnvironment = Partial<\n Pick<\n NodeJS.ProcessEnv,\n \"MATTERMOST_BASE_URL\" | \"MATTERMOST_BOT_TOKEN\" | \"MATTERMOST_TOKEN\" | \"MATTERMOST_URL\"\n >\n>;\n\nexport function resolveMattermostAdapterConfig(\n config: ProviderConfig,\n env: MattermostEnvironment = process.env,\n) {\n const baseUrl =\n config.mattermost?.baseUrl ??\n env.MATTERMOST_URL ??\n env.MATTERMOST_BASE_URL ??\n \"http://127.0.0.1\";\n let parsedBaseUrl: URL;\n try {\n parsedBaseUrl = new URL(baseUrl);\n } catch (error) {\n throw new CrablineError(\"Mattermost base URL is invalid.\", {\n cause: error,\n kind: \"config\",\n });\n }\n if (\n parsedBaseUrl.protocol !== \"https:\" &&\n !(parsedBaseUrl.protocol === \"http:\" && isLoopbackHost(parsedBaseUrl.hostname))\n ) {\n throw new CrablineError(\n \"Mattermost bearer authentication requires HTTPS; plain HTTP is allowed only for loopback-local servers.\",\n { kind: \"config\" },\n );\n }\n const configuredWebhookToken = config.mattermost?.webhookToken;\n const webhookToken = configuredWebhookToken ?? env.MATTERMOST_TOKEN;\n if (webhookToken !== undefined && !webhookToken.trim()) {\n throw new CrablineError(\n configuredWebhookToken === undefined\n ? \"MATTERMOST_TOKEN must not be empty or whitespace-only.\"\n : \"Mattermost webhookToken must not be empty or whitespace-only.\",\n { kind: \"config\" },\n );\n }\n return {\n baseUrl,\n botToken: config.mattermost?.botToken ?? env.MATTERMOST_BOT_TOKEN ?? \"local-mock-token\",\n userName: config.mattermost?.userName,\n webhookToken,\n };\n}\n\nexport class MattermostProviderAdapter extends LocalMockProviderAdapter implements ProviderAdapter {\n constructor(id: string, config: ProviderConfig, _userName: string, runtime?: unknown) {\n const env = (runtime as { env?: MattermostEnvironment } | undefined)?.env ?? process.env;\n const resolvedConfig = resolveMattermostAdapterConfig(config, env);\n const authenticateWebhook = resolvedConfig.webhookToken\n ? createSecretVerifier(resolvedConfig.webhookToken)\n : undefined;\n requireExternalWebhookAuthentication({\n authenticated: Boolean(authenticateWebhook),\n provider: \"Mattermost\",\n requirement: \"webhookToken or MATTERMOST_TOKEN\",\n webhook: config.mattermost?.webhook,\n });\n const recorderPath = config.mattermost?.recorder.path\n ? path.resolve(config.mattermost.recorder.path)\n : resolveGeneratedLocalMockRecorderPath(id);\n super({\n codec: getBuiltinTargetCodec(\"mattermost\"),\n config,\n id,\n options: {\n ...(authenticateWebhook\n ? {\n authenticateWebhookRequest(request: Request, rawBody: string) {\n return authenticateWebhook(readMattermostWebhookToken(request, rawBody))\n ? undefined\n : new Response(\"unauthorized\", { status: 401 });\n },\n }\n : {}),\n defaultWebhook: { host: \"127.0.0.1\", path: \"/mattermost/webhook\", port: 8793 },\n endpointLabel: \"webhook endpoint\",\n matchesThread: matchesMattermostThread,\n handleWebhookPayload: async (payload, request) => {\n const mediaType = request.headers\n .get(\"content-type\")\n ?.split(\";\", 1)[0]\n ?.trim()\n .toLowerCase();\n if (mediaType !== \"application/x-www-form-urlencoded\" || typeof payload !== \"string\") {\n return undefined;\n }\n let normalized: ReturnType<typeof normalizeMattermostWebhookPayload>;\n try {\n normalized = normalizeMattermostWebhookPayload(\n Object.fromEntries(new URLSearchParams(payload).entries()),\n );\n } catch (error) {\n if (error instanceof CrablineError && error.kind === \"inbound\") {\n return new Response(error.message, { status: 400 });\n }\n throw error;\n }\n const messageId =\n normalized.id ??\n `mattermost-mock-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n await appendRecordedInbound(recorderPath, {\n author: normalized.author,\n id: messageId,\n provider: id,\n raw: normalized.raw,\n recordedDirection: \"inbound\",\n sentAt: new Date().toISOString(),\n text: normalized.text,\n threadId: normalized.threadId,\n });\n return new Response(JSON.stringify({ id: messageId, ok: true }), {\n headers: { \"content-type\": \"application/json\" },\n });\n },\n normalizeWebhookPayload: normalizeMattermostWebhookPayload,\n platform: \"mattermost\",\n publicUrl: config.mattermost?.webhook.publicUrl,\n recorderPath,\n webhook: config.mattermost?.webhook,\n webhookMethods: [\"POST\"],\n },\n });\n }\n}\n\nfunction mattermostThreadKey(channelId: string, rootId: string): string {\n return `${channelId}:thread:${rootId}`;\n}\n\nexport function matchesMattermostThread(\n candidateThreadId: string,\n expectedThreadId: string | undefined,\n target: { channelId?: string | undefined },\n): boolean {\n if (!expectedThreadId) {\n return true;\n }\n if (\n target.channelId &&\n MATTERMOST_ID_RULE.pattern.test(expectedThreadId) &&\n expectedThreadId !== target.channelId\n ) {\n return candidateThreadId === mattermostThreadKey(target.channelId, expectedThreadId);\n }\n return (\n candidateThreadId === expectedThreadId ||\n (MATTERMOST_ID_RULE.pattern.test(expectedThreadId) &&\n candidateThreadId.startsWith(`${expectedThreadId}:thread:`))\n );\n}\n\nfunction readMattermostWebhookToken(request: Request, rawBody: string): string | null {\n const mediaType = request.headers.get(\"content-type\")?.split(\";\", 1)[0]?.trim().toLowerCase();\n if (mediaType === \"application/x-www-form-urlencoded\") {\n return new URLSearchParams(rawBody).get(\"token\");\n }\n if (mediaType !== \"application/json\") {\n return null;\n }\n try {\n const payload = JSON.parse(rawBody) as unknown;\n return isRecord(payload) && typeof payload.token === \"string\" ? payload.token : null;\n } catch {\n return null;\n }\n}\n\nfunction withoutMattermostWebhookToken(payload: Record<string, unknown>): Record<string, unknown> {\n const { token: _token, ...safePayload } = payload;\n return safePayload;\n}\n\ntype NormalizedMattermostWebhookPayload = {\n author: \"assistant\" | \"system\" | \"user\";\n id?: string | undefined;\n raw: unknown;\n text: string;\n threadId: string;\n};\n\nexport function normalizeMattermostWebhookPayload(\n payload: unknown,\n): NormalizedMattermostWebhookPayload {\n if (!isRecord(payload)) {\n throw new CrablineError(\"Mattermost webhook payload must be an object\", { kind: \"inbound\" });\n }\n\n const safePayload = withoutMattermostWebhookToken(payload);\n const genericMessage = optionalRecord(safePayload, \"message\");\n if (genericMessage) {\n const normalized = genericMockPayloadWithNativeThread({\n channelRule: MATTERMOST_ID_RULE,\n payload: safePayload,\n threadRule: MATTERMOST_ID_RULE,\n });\n const channelId =\n optionalString(genericMessage, \"channelId\") ?? optionalString(safePayload, \"channelId\");\n const rootId =\n optionalString(genericMessage, \"threadId\") ?? optionalString(safePayload, \"threadId\");\n if (!channelId || !rootId || channelId === rootId) {\n // Without channelId, the generic threadId is the channel-level conversation.\n return normalized as NormalizedMattermostWebhookPayload;\n }\n const scopedThreadId = mattermostThreadKey(\n requireNativeInboundId(channelId, MATTERMOST_ID_RULE, \"Mattermost channelId\"),\n requireNativeInboundId(rootId, MATTERMOST_ID_RULE, \"Mattermost threadId\"),\n );\n return {\n ...normalized,\n ...(\"message\" in normalized && isRecord(normalized.message)\n ? { message: { ...normalized.message, threadId: scopedThreadId } }\n : {}),\n threadId: scopedThreadId,\n } as NormalizedMattermostWebhookPayload;\n }\n\n const channelId = optionalString(safePayload, \"channel_id\");\n const postId = optionalString(safePayload, \"post_id\");\n const rootId = optionalString(safePayload, \"root_id\");\n const text = optionalString(safePayload, \"text\");\n if (!channelId || !text) {\n throw new CrablineError(\"Mattermost webhook payload requires channel_id and text\", {\n kind: \"inbound\",\n });\n }\n\n return {\n author: authorFromBotFlag(false),\n ...(postId\n ? { id: requireNativeInboundId(postId, MATTERMOST_ID_RULE, \"Mattermost post_id\") }\n : {}),\n raw: safePayload,\n text,\n threadId: rootId\n ? mattermostThreadKey(\n requireNativeInboundId(channelId, MATTERMOST_ID_RULE, \"Mattermost channel_id\"),\n requireNativeInboundId(rootId, MATTERMOST_ID_RULE, \"Mattermost root_id\"),\n )\n : requireNativeInboundId(channelId, MATTERMOST_ID_RULE, \"Mattermost channel_id\"),\n };\n}\n"]}
|
|
@@ -2,7 +2,7 @@ import { createPublicKey } from "node:crypto";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { CrablineError } from "../../core/errors.js";
|
|
4
4
|
import { LocalMockProviderAdapter } from "../local-mock.js";
|
|
5
|
-
import { createCachedJwtKeyResolver, readBearerToken, resolveHttpCacheExpiry, verifySignedJwt, } from "../signed-jwt.js";
|
|
5
|
+
import { createCachedJwtKeyResolver, JwtKeyInfrastructureError, readBearerToken, resolveHttpCacheExpiry, verifySignedJwt, } from "../signed-jwt.js";
|
|
6
6
|
import { getBuiltinTargetCodec, MSTEAMS_CONVERSATION_ID_RULE } from "../target-normalizers.js";
|
|
7
7
|
import { authorFromBotFlag, genericMockPayloadWithNativeThread, isRecord, optionalRecord, optionalString, requireNativeInboundId, } from "./native-local-mock.js";
|
|
8
8
|
import { requireExternalWebhookAuthentication } from "./external-webhook-auth.js";
|
|
@@ -17,6 +17,17 @@ export function resolveMsTeamsAdapterConfig(config, env = process.env) {
|
|
|
17
17
|
}
|
|
18
18
|
const BOT_CONNECTOR_ISSUER = "https://api.botframework.com";
|
|
19
19
|
const BOT_CONNECTOR_OPENID_URL = "https://login.botframework.com/v1/.well-known/openidconfiguration";
|
|
20
|
+
function parseBotConnectorKey(value) {
|
|
21
|
+
if (!isRecord(value)) {
|
|
22
|
+
throw new Error("Bot Connector signing key must be an object.");
|
|
23
|
+
}
|
|
24
|
+
if (value.endorsements !== undefined &&
|
|
25
|
+
(!Array.isArray(value.endorsements) ||
|
|
26
|
+
!value.endorsements.every((endorsement) => typeof endorsement === "string"))) {
|
|
27
|
+
throw new Error("Bot Connector signing key endorsements must be a string array.");
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
20
31
|
export function createMsTeamsWebhookAuthenticator(config, runtime = {}) {
|
|
21
32
|
const appId = config.msteams?.appId ?? (runtime.env ?? process.env).TEAMS_APP_ID;
|
|
22
33
|
if (!appId) {
|
|
@@ -25,31 +36,39 @@ export function createMsTeamsWebhookAuthenticator(config, runtime = {}) {
|
|
|
25
36
|
const fetchImpl = runtime.fetch ?? fetch;
|
|
26
37
|
const resolveSigningKey = createCachedJwtKeyResolver({
|
|
27
38
|
async fetchKeys(signal) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
try {
|
|
40
|
+
const metadataRequestedAt = runtime.now?.() ?? Date.now();
|
|
41
|
+
const metadataResponse = await fetchImpl(BOT_CONNECTOR_OPENID_URL, { signal });
|
|
42
|
+
if (!metadataResponse.ok) {
|
|
43
|
+
throw new Error(`Bot Connector metadata fetch failed with HTTP ${metadataResponse.status}.`);
|
|
44
|
+
}
|
|
45
|
+
const metadataExpiry = resolveHttpCacheExpiry(metadataResponse, metadataRequestedAt);
|
|
46
|
+
const metadata = (await metadataResponse.json());
|
|
47
|
+
if (typeof metadata.jwks_uri !== "string") {
|
|
48
|
+
throw new Error("Bot Connector metadata omitted jwks_uri.");
|
|
49
|
+
}
|
|
50
|
+
const keysRequestedAt = runtime.now?.() ?? Date.now();
|
|
51
|
+
const keysResponse = await fetchImpl(metadata.jwks_uri, { signal });
|
|
52
|
+
if (!keysResponse.ok) {
|
|
53
|
+
throw new Error(`Bot Connector key fetch failed with HTTP ${keysResponse.status}.`);
|
|
54
|
+
}
|
|
55
|
+
const keyExpiry = resolveHttpCacheExpiry(keysResponse, keysRequestedAt);
|
|
56
|
+
const keys = (await keysResponse.json());
|
|
57
|
+
if (!Array.isArray(keys.keys)) {
|
|
58
|
+
throw new Error("Bot Connector key response omitted keys.");
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
expiresAt: Math.min(metadataExpiry, keyExpiry),
|
|
62
|
+
values: keys.keys.map(parseBotConnectorKey),
|
|
63
|
+
};
|
|
32
64
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw error instanceof JwtKeyInfrastructureError
|
|
67
|
+
? error
|
|
68
|
+
: new JwtKeyInfrastructureError(error instanceof Error ? error.message : "Bot Connector key fetch failed.", { cause: error });
|
|
37
69
|
}
|
|
38
|
-
const keysResponse = await fetchImpl(metadata.jwks_uri, { signal });
|
|
39
|
-
if (!keysResponse.ok) {
|
|
40
|
-
throw new Error(`Bot Connector key fetch failed with HTTP ${keysResponse.status}.`);
|
|
41
|
-
}
|
|
42
|
-
const keyExpiry = resolveHttpCacheExpiry(keysResponse, fetchedAt);
|
|
43
|
-
const keys = (await keysResponse.json());
|
|
44
|
-
if (!Array.isArray(keys.keys)) {
|
|
45
|
-
throw new Error("Bot Connector key response omitted keys.");
|
|
46
|
-
}
|
|
47
|
-
return {
|
|
48
|
-
expiresAt: Math.min(metadataExpiry, keyExpiry),
|
|
49
|
-
values: keys.keys,
|
|
50
|
-
};
|
|
51
70
|
},
|
|
52
|
-
keyId: (value) => value.kid,
|
|
71
|
+
keyId: (value) => (isRecord(value) && typeof value.kid === "string" ? value.kid : undefined),
|
|
53
72
|
now: runtime.now,
|
|
54
73
|
refreshCooldownMs: runtime.unknownKeyCooldownMs,
|
|
55
74
|
timeoutMs: runtime.keyFetchTimeoutMs,
|
|
@@ -58,10 +77,7 @@ export function createMsTeamsWebhookAuthenticator(config, runtime = {}) {
|
|
|
58
77
|
return async (request, rawBody) => {
|
|
59
78
|
const token = readBearerToken(request);
|
|
60
79
|
if (!token) {
|
|
61
|
-
return
|
|
62
|
-
headers: { "www-authenticate": "Bearer" },
|
|
63
|
-
status: 401,
|
|
64
|
-
});
|
|
80
|
+
return botConnectorAuthenticationResponse(401);
|
|
65
81
|
}
|
|
66
82
|
try {
|
|
67
83
|
const payload = JSON.parse(rawBody);
|
|
@@ -79,10 +95,23 @@ export function createMsTeamsWebhookAuthenticator(config, runtime = {}) {
|
|
|
79
95
|
now: runtime.now,
|
|
80
96
|
async resolveKey(header) {
|
|
81
97
|
const key = await resolveSigningKey(header);
|
|
82
|
-
if (key.endorsements &&
|
|
98
|
+
if (key.endorsements &&
|
|
99
|
+
key.endorsements.length > 0 &&
|
|
100
|
+
!key.endorsements.includes(channelId)) {
|
|
83
101
|
throw new Error("Bot Connector JWT key does not endorse the activity channel.");
|
|
84
102
|
}
|
|
85
|
-
|
|
103
|
+
try {
|
|
104
|
+
const publicKey = createPublicKey({ format: "jwk", key });
|
|
105
|
+
if (publicKey.asymmetricKeyType !== "rsa") {
|
|
106
|
+
throw new Error("Bot Connector JWT signing key must use RSA.");
|
|
107
|
+
}
|
|
108
|
+
return publicKey;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
throw new JwtKeyInfrastructureError("Bot Connector JWT signing key is invalid.", {
|
|
112
|
+
cause: error,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
86
115
|
},
|
|
87
116
|
token,
|
|
88
117
|
});
|
|
@@ -91,14 +120,20 @@ export function createMsTeamsWebhookAuthenticator(config, runtime = {}) {
|
|
|
91
120
|
}
|
|
92
121
|
return undefined;
|
|
93
122
|
}
|
|
94
|
-
catch {
|
|
95
|
-
return
|
|
96
|
-
headers: { "www-authenticate": "Bearer" },
|
|
97
|
-
status: 401,
|
|
98
|
-
});
|
|
123
|
+
catch (error) {
|
|
124
|
+
return botConnectorAuthenticationResponse(error instanceof JwtKeyInfrastructureError ? 503 : 401);
|
|
99
125
|
}
|
|
100
126
|
};
|
|
101
127
|
}
|
|
128
|
+
function botConnectorAuthenticationResponse(status) {
|
|
129
|
+
return new Response(status === 401 ? "unauthorized" : "service unavailable", {
|
|
130
|
+
headers: {
|
|
131
|
+
"cache-control": "no-store",
|
|
132
|
+
...(status === 401 ? { "www-authenticate": "Bearer" } : {}),
|
|
133
|
+
},
|
|
134
|
+
status,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
102
137
|
export class MsTeamsProviderAdapter extends LocalMockProviderAdapter {
|
|
103
138
|
constructor(id, config, _userName, runtime) {
|
|
104
139
|
const authRuntime = runtime ?? {};
|
|
@@ -112,6 +147,7 @@ export class MsTeamsProviderAdapter extends LocalMockProviderAdapter {
|
|
|
112
147
|
...(authenticateWebhookRequest ? { authenticateWebhookRequest } : {}),
|
|
113
148
|
defaultWebhook: { host: "127.0.0.1", path: "/msteams/webhook", port: 8791 },
|
|
114
149
|
endpointLabel: "webhook endpoint",
|
|
150
|
+
handleWebhookPayload: handleMsTeamsWebhookPayload,
|
|
115
151
|
normalizeWebhookPayload: normalizeMsTeamsWebhookPayload,
|
|
116
152
|
platform: "msteams",
|
|
117
153
|
publicUrl: config.msteams?.webhook.publicUrl,
|
|
@@ -132,20 +168,51 @@ function requireExternalMsTeamsWebhookAuthentication(config, env) {
|
|
|
132
168
|
webhook: config.msteams?.webhook,
|
|
133
169
|
});
|
|
134
170
|
}
|
|
171
|
+
function msTeamsAttachmentPlaceholder(payload) {
|
|
172
|
+
if (!Array.isArray(payload.attachments)) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
let attachmentCount = 0;
|
|
176
|
+
let imageCount = 0;
|
|
177
|
+
for (const attachment of payload.attachments) {
|
|
178
|
+
if (!isRecord(attachment)) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const contentType = optionalString(attachment, "contentType");
|
|
182
|
+
const hasContent = contentType !== undefined ||
|
|
183
|
+
optionalString(attachment, "contentUrl") !== undefined ||
|
|
184
|
+
optionalString(attachment, "name") !== undefined ||
|
|
185
|
+
(attachment.content !== undefined && attachment.content !== null);
|
|
186
|
+
if (!hasContent) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
attachmentCount += 1;
|
|
190
|
+
if (contentType?.toLowerCase().startsWith("image/")) {
|
|
191
|
+
imageCount += 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (attachmentCount === 0) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
return imageCount > 0
|
|
198
|
+
? `<media:image>${imageCount > 1 ? ` (${imageCount} images)` : ""}`
|
|
199
|
+
: `<media:document>${attachmentCount > 1 ? ` (${attachmentCount} files)` : ""}`;
|
|
200
|
+
}
|
|
135
201
|
export function normalizeMsTeamsWebhookPayload(payload) {
|
|
136
202
|
if (!isRecord(payload)) {
|
|
137
203
|
throw new CrablineError("Microsoft Teams webhook payload must be an object", {
|
|
138
204
|
kind: "inbound",
|
|
139
205
|
});
|
|
140
206
|
}
|
|
141
|
-
|
|
207
|
+
const activityType = payload.type;
|
|
208
|
+
if (activityType === undefined && optionalRecord(payload, "message")) {
|
|
142
209
|
return genericMockPayloadWithNativeThread({
|
|
143
210
|
channelRule: MSTEAMS_CONVERSATION_ID_RULE,
|
|
144
211
|
payload,
|
|
145
212
|
threadRule: MSTEAMS_CONVERSATION_ID_RULE,
|
|
146
213
|
});
|
|
147
214
|
}
|
|
148
|
-
if (
|
|
215
|
+
if (activityType !== "message") {
|
|
149
216
|
throw new CrablineError("Microsoft Teams activity payload requires type=message", {
|
|
150
217
|
kind: "inbound",
|
|
151
218
|
});
|
|
@@ -154,9 +221,10 @@ export function normalizeMsTeamsWebhookPayload(payload) {
|
|
|
154
221
|
const from = optionalRecord(payload, "from");
|
|
155
222
|
const channelId = optionalString(payload, "channelId");
|
|
156
223
|
const conversationId = conversation ? optionalString(conversation, "id") : undefined;
|
|
157
|
-
const
|
|
224
|
+
const activityText = optionalString(payload, "text");
|
|
225
|
+
const text = activityText?.trim() ? activityText : msTeamsAttachmentPlaceholder(payload);
|
|
158
226
|
if (channelId !== "msteams" || !conversationId || !text) {
|
|
159
|
-
throw new CrablineError("Microsoft Teams activity payload requires channelId=msteams, conversation.id, and text", {
|
|
227
|
+
throw new CrablineError("Microsoft Teams activity payload requires channelId=msteams, conversation.id, and text or attachments", {
|
|
160
228
|
kind: "inbound",
|
|
161
229
|
});
|
|
162
230
|
}
|
|
@@ -168,4 +236,16 @@ export function normalizeMsTeamsWebhookPayload(payload) {
|
|
|
168
236
|
threadId: requireNativeInboundId(conversationId, MSTEAMS_CONVERSATION_ID_RULE, "Microsoft Teams conversation.id"),
|
|
169
237
|
};
|
|
170
238
|
}
|
|
239
|
+
export function handleMsTeamsWebhookPayload(payload) {
|
|
240
|
+
if (isRecord(payload) && payload.type === "invoke") {
|
|
241
|
+
return new Response(null, { status: 501 });
|
|
242
|
+
}
|
|
243
|
+
if (isRecord(payload) &&
|
|
244
|
+
typeof payload.type === "string" &&
|
|
245
|
+
payload.type.length > 0 &&
|
|
246
|
+
payload.type !== "message") {
|
|
247
|
+
return new Response(null, { status: 200 });
|
|
248
|
+
}
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
171
251
|
//# sourceMappingURL=msteams.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"msteams.js","sourceRoot":"","sources":["../../../../src/providers/builtin/msteams.ts"],"names":[],"mappings":"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","sourcesContent":["import { createPublicKey, type JsonWebKey } from \"node:crypto\";\nimport path from \"node:path\";\nimport { CrablineError } from \"../../core/errors.js\";\nimport type { ProviderConfig } from \"../../config/schema.js\";\nimport { LocalMockProviderAdapter } from \"../local-mock.js\";\nimport type { ProviderAdapter } from \"../types.js\";\nimport {\n createCachedJwtKeyResolver,\n readBearerToken,\n resolveHttpCacheExpiry,\n verifySignedJwt,\n} from \"../signed-jwt.js\";\nimport { getBuiltinTargetCodec, MSTEAMS_CONVERSATION_ID_RULE } from \"../target-normalizers.js\";\nimport {\n authorFromBotFlag,\n genericMockPayloadWithNativeThread,\n isRecord,\n optionalRecord,\n optionalString,\n requireNativeInboundId,\n} from \"./native-local-mock.js\";\nimport { requireExternalWebhookAuthentication } from \"./external-webhook-auth.js\";\n\nexport function resolveMsTeamsAdapterConfig(\n config: ProviderConfig,\n env: NodeJS.ProcessEnv = process.env,\n) {\n return {\n appId: config.msteams?.appId ?? env.TEAMS_APP_ID ?? \"local-mock-teams-app\",\n appPassword: config.msteams?.appPassword ?? env.TEAMS_APP_PASSWORD ?? \"local-mock-secret\",\n appTenantId: config.msteams?.appTenantId,\n appType: config.msteams?.appType,\n userName: config.msteams?.userName,\n };\n}\n\nconst BOT_CONNECTOR_ISSUER = \"https://api.botframework.com\";\nconst BOT_CONNECTOR_OPENID_URL =\n \"https://login.botframework.com/v1/.well-known/openidconfiguration\";\n\ntype MsTeamsAuthRuntime = {\n env?: NodeJS.ProcessEnv | undefined;\n fetch?: typeof fetch | undefined;\n keyFetchTimeoutMs?: number | undefined;\n now?: (() => number) | undefined;\n unknownKeyCooldownMs?: number | undefined;\n};\n\ntype BotConnectorKey = JsonWebKey & {\n endorsements?: string[] | undefined;\n kid?: string | undefined;\n};\n\nexport function createMsTeamsWebhookAuthenticator(\n config: ProviderConfig,\n runtime: MsTeamsAuthRuntime = {},\n) {\n const appId = config.msteams?.appId ?? (runtime.env ?? process.env).TEAMS_APP_ID;\n if (!appId) {\n return undefined;\n }\n const fetchImpl = runtime.fetch ?? fetch;\n const resolveSigningKey = createCachedJwtKeyResolver<BotConnectorKey>({\n async fetchKeys(signal) {\n const fetchedAt = runtime.now?.() ?? Date.now();\n const metadataResponse = await fetchImpl(BOT_CONNECTOR_OPENID_URL, { signal });\n if (!metadataResponse.ok) {\n throw new Error(\n `Bot Connector metadata fetch failed with HTTP ${metadataResponse.status}.`,\n );\n }\n const metadataExpiry = resolveHttpCacheExpiry(metadataResponse, fetchedAt);\n const metadata = (await metadataResponse.json()) as { jwks_uri?: unknown };\n if (typeof metadata.jwks_uri !== \"string\") {\n throw new Error(\"Bot Connector metadata omitted jwks_uri.\");\n }\n const keysResponse = await fetchImpl(metadata.jwks_uri, { signal });\n if (!keysResponse.ok) {\n throw new Error(`Bot Connector key fetch failed with HTTP ${keysResponse.status}.`);\n }\n const keyExpiry = resolveHttpCacheExpiry(keysResponse, fetchedAt);\n const keys = (await keysResponse.json()) as { keys?: unknown };\n if (!Array.isArray(keys.keys)) {\n throw new Error(\"Bot Connector key response omitted keys.\");\n }\n return {\n expiresAt: Math.min(metadataExpiry, keyExpiry),\n values: keys.keys as BotConnectorKey[],\n };\n },\n keyId: (value) => value.kid,\n now: runtime.now,\n refreshCooldownMs: runtime.unknownKeyCooldownMs,\n timeoutMs: runtime.keyFetchTimeoutMs,\n unknownKeyMessage: \"Bot Connector JWT signing key is unknown.\",\n });\n return async (request: Request, rawBody: string): Promise<Response | undefined> => {\n const token = readBearerToken(request);\n if (!token) {\n return new Response(\"unauthorized\", {\n headers: { \"www-authenticate\": \"Bearer\" },\n status: 401,\n });\n }\n try {\n const payload: unknown = JSON.parse(rawBody);\n if (!isRecord(payload)) {\n throw new Error(\"Bot Connector activity must be an object.\");\n }\n const channelId = optionalString(payload, \"channelId\");\n const serviceUrl = optionalString(payload, \"serviceUrl\");\n if (channelId !== \"msteams\" || !serviceUrl) {\n throw new Error(\"Bot Connector activity requires channelId=msteams and serviceUrl.\");\n }\n const claims = await verifySignedJwt({\n audience: appId,\n issuers: [BOT_CONNECTOR_ISSUER],\n now: runtime.now,\n async resolveKey(header) {\n const key = await resolveSigningKey(header);\n if (key.endorsements && !key.endorsements.includes(channelId)) {\n throw new Error(\"Bot Connector JWT key does not endorse the activity channel.\");\n }\n return createPublicKey({ format: \"jwk\", key });\n },\n token,\n });\n if (claims.serviceurl !== serviceUrl) {\n throw new Error(\"Bot Connector serviceurl claim does not match the activity.\");\n }\n return undefined;\n } catch {\n return new Response(\"unauthorized\", {\n headers: { \"www-authenticate\": \"Bearer\" },\n status: 401,\n });\n }\n };\n}\n\nexport class MsTeamsProviderAdapter extends LocalMockProviderAdapter implements ProviderAdapter {\n constructor(id: string, config: ProviderConfig, _userName: string, runtime?: unknown) {\n const authRuntime = (runtime as MsTeamsAuthRuntime | undefined) ?? {};\n requireExternalMsTeamsWebhookAuthentication(config, authRuntime.env ?? process.env);\n const authenticateWebhookRequest = createMsTeamsWebhookAuthenticator(config, authRuntime);\n super({\n codec: getBuiltinTargetCodec(\"msteams\"),\n config,\n id,\n options: {\n ...(authenticateWebhookRequest ? { authenticateWebhookRequest } : {}),\n defaultWebhook: { host: \"127.0.0.1\", path: \"/msteams/webhook\", port: 8791 },\n endpointLabel: \"webhook endpoint\",\n normalizeWebhookPayload: normalizeMsTeamsWebhookPayload,\n platform: \"msteams\",\n publicUrl: config.msteams?.webhook.publicUrl,\n recorderPath: config.msteams?.recorder.path\n ? path.resolve(config.msteams.recorder.path)\n : undefined,\n webhook: config.msteams?.webhook,\n },\n });\n }\n}\n\nfunction requireExternalMsTeamsWebhookAuthentication(\n config: ProviderConfig,\n env: NodeJS.ProcessEnv,\n): void {\n const appId = config.msteams?.appId ?? env.TEAMS_APP_ID;\n requireExternalWebhookAuthentication({\n authenticated: Boolean(appId),\n provider: \"Microsoft Teams\",\n requirement: \"msteams.appId or TEAMS_APP_ID\",\n webhook: config.msteams?.webhook,\n });\n}\n\nexport function normalizeMsTeamsWebhookPayload(payload: unknown) {\n if (!isRecord(payload)) {\n throw new CrablineError(\"Microsoft Teams webhook payload must be an object\", {\n kind: \"inbound\",\n });\n }\n\n if (optionalRecord(payload, \"message\")) {\n return genericMockPayloadWithNativeThread({\n channelRule: MSTEAMS_CONVERSATION_ID_RULE,\n payload,\n threadRule: MSTEAMS_CONVERSATION_ID_RULE,\n });\n }\n\n if (optionalString(payload, \"type\") !== \"message\") {\n throw new CrablineError(\"Microsoft Teams activity payload requires type=message\", {\n kind: \"inbound\",\n });\n }\n const conversation = optionalRecord(payload, \"conversation\");\n const from = optionalRecord(payload, \"from\");\n const channelId = optionalString(payload, \"channelId\");\n const conversationId = conversation ? optionalString(conversation, \"id\") : undefined;\n const text = optionalString(payload, \"text\");\n if (channelId !== \"msteams\" || !conversationId || !text) {\n throw new CrablineError(\n \"Microsoft Teams activity payload requires channelId=msteams, conversation.id, and text\",\n {\n kind: \"inbound\",\n },\n );\n }\n\n return {\n author: authorFromBotFlag(optionalString(from ?? {}, \"role\") === \"bot\"),\n ...(optionalString(payload, \"id\") ? { id: optionalString(payload, \"id\") } : {}),\n raw: payload,\n text,\n threadId: requireNativeInboundId(\n conversationId,\n MSTEAMS_CONVERSATION_ID_RULE,\n \"Microsoft Teams conversation.id\",\n ),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"msteams.js","sourceRoot":"","sources":["../../../../src/providers/builtin/msteams.ts"],"names":[],"mappings":"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","sourcesContent":["import { createPublicKey, type JsonWebKey } from \"node:crypto\";\nimport path from \"node:path\";\nimport { CrablineError } from \"../../core/errors.js\";\nimport type { ProviderConfig } from \"../../config/schema.js\";\nimport { LocalMockProviderAdapter } from \"../local-mock.js\";\nimport type { ProviderAdapter } from \"../types.js\";\nimport {\n createCachedJwtKeyResolver,\n JwtKeyInfrastructureError,\n readBearerToken,\n resolveHttpCacheExpiry,\n verifySignedJwt,\n} from \"../signed-jwt.js\";\nimport { getBuiltinTargetCodec, MSTEAMS_CONVERSATION_ID_RULE } from \"../target-normalizers.js\";\nimport {\n authorFromBotFlag,\n genericMockPayloadWithNativeThread,\n isRecord,\n optionalRecord,\n optionalString,\n requireNativeInboundId,\n} from \"./native-local-mock.js\";\nimport { requireExternalWebhookAuthentication } from \"./external-webhook-auth.js\";\n\nexport function resolveMsTeamsAdapterConfig(\n config: ProviderConfig,\n env: NodeJS.ProcessEnv = process.env,\n) {\n return {\n appId: config.msteams?.appId ?? env.TEAMS_APP_ID ?? \"local-mock-teams-app\",\n appPassword: config.msteams?.appPassword ?? env.TEAMS_APP_PASSWORD ?? \"local-mock-secret\",\n appTenantId: config.msteams?.appTenantId,\n appType: config.msteams?.appType,\n userName: config.msteams?.userName,\n };\n}\n\nconst BOT_CONNECTOR_ISSUER = \"https://api.botframework.com\";\nconst BOT_CONNECTOR_OPENID_URL =\n \"https://login.botframework.com/v1/.well-known/openidconfiguration\";\n\ntype MsTeamsAuthRuntime = {\n env?: NodeJS.ProcessEnv | undefined;\n fetch?: typeof fetch | undefined;\n keyFetchTimeoutMs?: number | undefined;\n now?: (() => number) | undefined;\n unknownKeyCooldownMs?: number | undefined;\n};\n\ntype BotConnectorKey = JsonWebKey & {\n endorsements?: string[] | undefined;\n kid?: string | undefined;\n};\n\nfunction parseBotConnectorKey(value: unknown): BotConnectorKey {\n if (!isRecord(value)) {\n throw new Error(\"Bot Connector signing key must be an object.\");\n }\n if (\n value.endorsements !== undefined &&\n (!Array.isArray(value.endorsements) ||\n !value.endorsements.every((endorsement) => typeof endorsement === \"string\"))\n ) {\n throw new Error(\"Bot Connector signing key endorsements must be a string array.\");\n }\n return value as BotConnectorKey;\n}\n\nexport function createMsTeamsWebhookAuthenticator(\n config: ProviderConfig,\n runtime: MsTeamsAuthRuntime = {},\n) {\n const appId = config.msteams?.appId ?? (runtime.env ?? process.env).TEAMS_APP_ID;\n if (!appId) {\n return undefined;\n }\n const fetchImpl = runtime.fetch ?? fetch;\n const resolveSigningKey = createCachedJwtKeyResolver<BotConnectorKey>({\n async fetchKeys(signal) {\n try {\n const metadataRequestedAt = runtime.now?.() ?? Date.now();\n const metadataResponse = await fetchImpl(BOT_CONNECTOR_OPENID_URL, { signal });\n if (!metadataResponse.ok) {\n throw new Error(\n `Bot Connector metadata fetch failed with HTTP ${metadataResponse.status}.`,\n );\n }\n const metadataExpiry = resolveHttpCacheExpiry(metadataResponse, metadataRequestedAt);\n const metadata = (await metadataResponse.json()) as { jwks_uri?: unknown };\n if (typeof metadata.jwks_uri !== \"string\") {\n throw new Error(\"Bot Connector metadata omitted jwks_uri.\");\n }\n const keysRequestedAt = runtime.now?.() ?? Date.now();\n const keysResponse = await fetchImpl(metadata.jwks_uri, { signal });\n if (!keysResponse.ok) {\n throw new Error(`Bot Connector key fetch failed with HTTP ${keysResponse.status}.`);\n }\n const keyExpiry = resolveHttpCacheExpiry(keysResponse, keysRequestedAt);\n const keys = (await keysResponse.json()) as { keys?: unknown };\n if (!Array.isArray(keys.keys)) {\n throw new Error(\"Bot Connector key response omitted keys.\");\n }\n return {\n expiresAt: Math.min(metadataExpiry, keyExpiry),\n values: keys.keys.map(parseBotConnectorKey),\n };\n } catch (error) {\n throw error instanceof JwtKeyInfrastructureError\n ? error\n : new JwtKeyInfrastructureError(\n error instanceof Error ? error.message : \"Bot Connector key fetch failed.\",\n { cause: error },\n );\n }\n },\n keyId: (value) => (isRecord(value) && typeof value.kid === \"string\" ? value.kid : undefined),\n now: runtime.now,\n refreshCooldownMs: runtime.unknownKeyCooldownMs,\n timeoutMs: runtime.keyFetchTimeoutMs,\n unknownKeyMessage: \"Bot Connector JWT signing key is unknown.\",\n });\n return async (request: Request, rawBody: string): Promise<Response | undefined> => {\n const token = readBearerToken(request);\n if (!token) {\n return botConnectorAuthenticationResponse(401);\n }\n try {\n const payload: unknown = JSON.parse(rawBody);\n if (!isRecord(payload)) {\n throw new Error(\"Bot Connector activity must be an object.\");\n }\n const channelId = optionalString(payload, \"channelId\");\n const serviceUrl = optionalString(payload, \"serviceUrl\");\n if (channelId !== \"msteams\" || !serviceUrl) {\n throw new Error(\"Bot Connector activity requires channelId=msteams and serviceUrl.\");\n }\n const claims = await verifySignedJwt({\n audience: appId,\n issuers: [BOT_CONNECTOR_ISSUER],\n now: runtime.now,\n async resolveKey(header) {\n const key = await resolveSigningKey(header);\n if (\n key.endorsements &&\n key.endorsements.length > 0 &&\n !key.endorsements.includes(channelId)\n ) {\n throw new Error(\"Bot Connector JWT key does not endorse the activity channel.\");\n }\n try {\n const publicKey = createPublicKey({ format: \"jwk\", key });\n if (publicKey.asymmetricKeyType !== \"rsa\") {\n throw new Error(\"Bot Connector JWT signing key must use RSA.\");\n }\n return publicKey;\n } catch (error) {\n throw new JwtKeyInfrastructureError(\"Bot Connector JWT signing key is invalid.\", {\n cause: error,\n });\n }\n },\n token,\n });\n if (claims.serviceurl !== serviceUrl) {\n throw new Error(\"Bot Connector serviceurl claim does not match the activity.\");\n }\n return undefined;\n } catch (error) {\n return botConnectorAuthenticationResponse(\n error instanceof JwtKeyInfrastructureError ? 503 : 401,\n );\n }\n };\n}\n\nfunction botConnectorAuthenticationResponse(status: 401 | 503): Response {\n return new Response(status === 401 ? \"unauthorized\" : \"service unavailable\", {\n headers: {\n \"cache-control\": \"no-store\",\n ...(status === 401 ? { \"www-authenticate\": \"Bearer\" } : {}),\n },\n status,\n });\n}\n\nexport class MsTeamsProviderAdapter extends LocalMockProviderAdapter implements ProviderAdapter {\n constructor(id: string, config: ProviderConfig, _userName: string, runtime?: unknown) {\n const authRuntime = (runtime as MsTeamsAuthRuntime | undefined) ?? {};\n requireExternalMsTeamsWebhookAuthentication(config, authRuntime.env ?? process.env);\n const authenticateWebhookRequest = createMsTeamsWebhookAuthenticator(config, authRuntime);\n super({\n codec: getBuiltinTargetCodec(\"msteams\"),\n config,\n id,\n options: {\n ...(authenticateWebhookRequest ? { authenticateWebhookRequest } : {}),\n defaultWebhook: { host: \"127.0.0.1\", path: \"/msteams/webhook\", port: 8791 },\n endpointLabel: \"webhook endpoint\",\n handleWebhookPayload: handleMsTeamsWebhookPayload,\n normalizeWebhookPayload: normalizeMsTeamsWebhookPayload,\n platform: \"msteams\",\n publicUrl: config.msteams?.webhook.publicUrl,\n recorderPath: config.msteams?.recorder.path\n ? path.resolve(config.msteams.recorder.path)\n : undefined,\n webhook: config.msteams?.webhook,\n },\n });\n }\n}\n\nfunction requireExternalMsTeamsWebhookAuthentication(\n config: ProviderConfig,\n env: NodeJS.ProcessEnv,\n): void {\n const appId = config.msteams?.appId ?? env.TEAMS_APP_ID;\n requireExternalWebhookAuthentication({\n authenticated: Boolean(appId),\n provider: \"Microsoft Teams\",\n requirement: \"msteams.appId or TEAMS_APP_ID\",\n webhook: config.msteams?.webhook,\n });\n}\n\nfunction msTeamsAttachmentPlaceholder(payload: Record<string, unknown>): string | undefined {\n if (!Array.isArray(payload.attachments)) {\n return undefined;\n }\n let attachmentCount = 0;\n let imageCount = 0;\n for (const attachment of payload.attachments) {\n if (!isRecord(attachment)) {\n continue;\n }\n const contentType = optionalString(attachment, \"contentType\");\n const hasContent =\n contentType !== undefined ||\n optionalString(attachment, \"contentUrl\") !== undefined ||\n optionalString(attachment, \"name\") !== undefined ||\n (attachment.content !== undefined && attachment.content !== null);\n if (!hasContent) {\n continue;\n }\n attachmentCount += 1;\n if (contentType?.toLowerCase().startsWith(\"image/\")) {\n imageCount += 1;\n }\n }\n if (attachmentCount === 0) {\n return undefined;\n }\n return imageCount > 0\n ? `<media:image>${imageCount > 1 ? ` (${imageCount} images)` : \"\"}`\n : `<media:document>${attachmentCount > 1 ? ` (${attachmentCount} files)` : \"\"}`;\n}\n\nexport function normalizeMsTeamsWebhookPayload(payload: unknown) {\n if (!isRecord(payload)) {\n throw new CrablineError(\"Microsoft Teams webhook payload must be an object\", {\n kind: \"inbound\",\n });\n }\n\n const activityType = payload.type;\n if (activityType === undefined && optionalRecord(payload, \"message\")) {\n return genericMockPayloadWithNativeThread({\n channelRule: MSTEAMS_CONVERSATION_ID_RULE,\n payload,\n threadRule: MSTEAMS_CONVERSATION_ID_RULE,\n });\n }\n\n if (activityType !== \"message\") {\n throw new CrablineError(\"Microsoft Teams activity payload requires type=message\", {\n kind: \"inbound\",\n });\n }\n const conversation = optionalRecord(payload, \"conversation\");\n const from = optionalRecord(payload, \"from\");\n const channelId = optionalString(payload, \"channelId\");\n const conversationId = conversation ? optionalString(conversation, \"id\") : undefined;\n const activityText = optionalString(payload, \"text\");\n const text = activityText?.trim() ? activityText : msTeamsAttachmentPlaceholder(payload);\n if (channelId !== \"msteams\" || !conversationId || !text) {\n throw new CrablineError(\n \"Microsoft Teams activity payload requires channelId=msteams, conversation.id, and text or attachments\",\n {\n kind: \"inbound\",\n },\n );\n }\n\n return {\n author: authorFromBotFlag(optionalString(from ?? {}, \"role\") === \"bot\"),\n ...(optionalString(payload, \"id\") ? { id: optionalString(payload, \"id\") } : {}),\n raw: payload,\n text,\n threadId: requireNativeInboundId(\n conversationId,\n MSTEAMS_CONVERSATION_ID_RULE,\n \"Microsoft Teams conversation.id\",\n ),\n };\n}\n\nexport function handleMsTeamsWebhookPayload(payload: unknown): Response | undefined {\n if (isRecord(payload) && payload.type === \"invoke\") {\n return new Response(null, { status: 501 });\n }\n if (\n isRecord(payload) &&\n typeof payload.type === \"string\" &&\n payload.type.length > 0 &&\n payload.type !== \"message\"\n ) {\n return new Response(null, { status: 200 });\n }\n return undefined;\n}\n"]}
|