@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.
Files changed (214) hide show
  1. package/README.md +137 -23
  2. package/dist/src/bin/crabline.js +3 -1
  3. package/dist/src/bin/crabline.js.map +1 -1
  4. package/dist/src/cli/program.d.ts +5 -1
  5. package/dist/src/cli/program.js +281 -52
  6. package/dist/src/cli/program.js.map +1 -1
  7. package/dist/src/config/load.js +39 -3
  8. package/dist/src/config/load.js.map +1 -1
  9. package/dist/src/config/schema.d.ts +20 -0
  10. package/dist/src/config/schema.js +163 -35
  11. package/dist/src/config/schema.js.map +1 -1
  12. package/dist/src/core/errors.d.ts +1 -0
  13. package/dist/src/core/errors.js +6 -3
  14. package/dist/src/core/errors.js.map +1 -1
  15. package/dist/src/core/http-path.d.ts +1 -0
  16. package/dist/src/core/http-path.js +14 -0
  17. package/dist/src/core/http-path.js.map +1 -0
  18. package/dist/src/core/matcher.d.ts +3 -1
  19. package/dist/src/core/matcher.js +58 -2
  20. package/dist/src/core/matcher.js.map +1 -1
  21. package/dist/src/core/nonces.d.ts +2 -0
  22. package/dist/src/core/nonces.js +11 -1
  23. package/dist/src/core/nonces.js.map +1 -1
  24. package/dist/src/core/reporters.d.ts +5 -0
  25. package/dist/src/core/reporters.js +26 -2
  26. package/dist/src/core/reporters.js.map +1 -1
  27. package/dist/src/core/run.d.ts +3 -1
  28. package/dist/src/core/run.js +178 -66
  29. package/dist/src/core/run.js.map +1 -1
  30. package/dist/src/index.d.ts +12 -10
  31. package/dist/src/index.js +6 -5
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/matrix-ids.d.ts +5 -0
  34. package/dist/src/matrix-ids.js +83 -0
  35. package/dist/src/matrix-ids.js.map +1 -0
  36. package/dist/src/openclaw/artifact-generation.d.ts +7 -6
  37. package/dist/src/openclaw/artifact-generation.js +385 -41
  38. package/dist/src/openclaw/artifact-generation.js.map +1 -1
  39. package/dist/src/openclaw/bridges/discord.d.ts +1 -0
  40. package/dist/src/openclaw/bridges/discord.js +149 -0
  41. package/dist/src/openclaw/bridges/discord.js.map +1 -0
  42. package/dist/src/openclaw/bridges/matrix.js +22 -5
  43. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  44. package/dist/src/openclaw/bridges/mattermost.js +14 -9
  45. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  46. package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
  47. package/dist/src/openclaw/bridges/probe-response.js +5 -0
  48. package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
  49. package/dist/src/openclaw/bridges/signal.js +16 -7
  50. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  51. package/dist/src/openclaw/bridges/slack.js +117 -24
  52. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  53. package/dist/src/openclaw/bridges/telegram.js +46 -20
  54. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  55. package/dist/src/openclaw/bridges/whatsapp.js +33 -17
  56. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  57. package/dist/src/openclaw/bridges/zalo.js +9 -4
  58. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  59. package/dist/src/openclaw/outbound-contract.js +3 -0
  60. package/dist/src/openclaw/outbound-contract.js.map +1 -1
  61. package/dist/src/openclaw/private-file.d.ts +38 -3
  62. package/dist/src/openclaw/private-file.js +2008 -87
  63. package/dist/src/openclaw/private-file.js.map +1 -1
  64. package/dist/src/openclaw/{smoke-lock.d.ts → provider-readiness-lock.d.ts} +14 -5
  65. package/dist/src/openclaw/provider-readiness-lock.js +1320 -0
  66. package/dist/src/openclaw/provider-readiness-lock.js.map +1 -0
  67. package/dist/src/openclaw/shared.d.ts +10 -17
  68. package/dist/src/openclaw/shared.js +53 -19
  69. package/dist/src/openclaw/shared.js.map +1 -1
  70. package/dist/src/openclaw.d.ts +3 -5
  71. package/dist/src/openclaw.js +245 -60
  72. package/dist/src/openclaw.js.map +1 -1
  73. package/dist/src/platform/process-owned-lock.d.ts +11 -0
  74. package/dist/src/platform/process-owned-lock.js +1698 -0
  75. package/dist/src/platform/process-owned-lock.js.map +1 -0
  76. package/dist/src/platform/recorder-directory.d.ts +1 -0
  77. package/dist/src/platform/recorder-directory.js +14 -0
  78. package/dist/src/platform/recorder-directory.js.map +1 -0
  79. package/dist/src/platform/windows-acl.d.ts +18 -0
  80. package/dist/src/platform/windows-acl.js +1367 -0
  81. package/dist/src/platform/windows-acl.js.map +1 -0
  82. package/dist/src/platform/windows-lock-root.d.ts +13 -0
  83. package/dist/src/platform/windows-lock-root.js +98 -0
  84. package/dist/src/platform/windows-lock-root.js.map +1 -0
  85. package/dist/src/providers/builtin/discord.d.ts +4 -0
  86. package/dist/src/providers/builtin/discord.js +52 -9
  87. package/dist/src/providers/builtin/discord.js.map +1 -1
  88. package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
  89. package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
  90. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
  91. package/dist/src/providers/builtin/feishu.d.ts +1 -0
  92. package/dist/src/providers/builtin/feishu.js +96 -26
  93. package/dist/src/providers/builtin/feishu.js.map +1 -1
  94. package/dist/src/providers/builtin/googlechat.d.ts +1 -0
  95. package/dist/src/providers/builtin/googlechat.js +95 -26
  96. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  97. package/dist/src/providers/builtin/imessage.js +60 -17
  98. package/dist/src/providers/builtin/imessage.js.map +1 -1
  99. package/dist/src/providers/builtin/loopback.js +88 -35
  100. package/dist/src/providers/builtin/loopback.js.map +1 -1
  101. package/dist/src/providers/builtin/matrix.d.ts +2 -2
  102. package/dist/src/providers/builtin/matrix.js +78 -21
  103. package/dist/src/providers/builtin/matrix.js.map +1 -1
  104. package/dist/src/providers/builtin/mattermost.d.ts +11 -7
  105. package/dist/src/providers/builtin/mattermost.js +138 -24
  106. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  107. package/dist/src/providers/builtin/msteams.d.ts +1 -0
  108. package/dist/src/providers/builtin/msteams.js +118 -38
  109. package/dist/src/providers/builtin/msteams.js.map +1 -1
  110. package/dist/src/providers/builtin/native-local-mock.js +4 -4
  111. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  112. package/dist/src/providers/builtin/script.js +1057 -146
  113. package/dist/src/providers/builtin/script.js.map +1 -1
  114. package/dist/src/providers/builtin/slack.d.ts +8 -1
  115. package/dist/src/providers/builtin/slack.js +327 -24
  116. package/dist/src/providers/builtin/slack.js.map +1 -1
  117. package/dist/src/providers/builtin/telegram.d.ts +1 -1
  118. package/dist/src/providers/builtin/telegram.js +55 -14
  119. package/dist/src/providers/builtin/telegram.js.map +1 -1
  120. package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
  121. package/dist/src/providers/builtin/whatsapp.js +157 -29
  122. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  123. package/dist/src/providers/builtin/zalo.d.ts +5 -2
  124. package/dist/src/providers/builtin/zalo.js +31 -7
  125. package/dist/src/providers/builtin/zalo.js.map +1 -1
  126. package/dist/src/providers/catalog.d.ts +1 -1
  127. package/dist/src/providers/catalog.js +1 -1
  128. package/dist/src/providers/catalog.js.map +1 -1
  129. package/dist/src/providers/local-mock.d.ts +4 -0
  130. package/dist/src/providers/local-mock.js +162 -35
  131. package/dist/src/providers/local-mock.js.map +1 -1
  132. package/dist/src/providers/native-ids.d.ts +2 -0
  133. package/dist/src/providers/native-ids.js +3 -0
  134. package/dist/src/providers/native-ids.js.map +1 -1
  135. package/dist/src/providers/recorder.d.ts +14 -3
  136. package/dist/src/providers/recorder.js +521 -41
  137. package/dist/src/providers/recorder.js.map +1 -1
  138. package/dist/src/providers/registry.d.ts +1 -0
  139. package/dist/src/providers/registry.js +65 -20
  140. package/dist/src/providers/registry.js.map +1 -1
  141. package/dist/src/providers/signed-jwt.d.ts +6 -1
  142. package/dist/src/providers/signed-jwt.js +241 -50
  143. package/dist/src/providers/signed-jwt.js.map +1 -1
  144. package/dist/src/providers/slack-ids.d.ts +1 -0
  145. package/dist/src/providers/slack-ids.js +5 -0
  146. package/dist/src/providers/slack-ids.js.map +1 -1
  147. package/dist/src/providers/target-normalizers.d.ts +9 -1
  148. package/dist/src/providers/target-normalizers.js +177 -58
  149. package/dist/src/providers/target-normalizers.js.map +1 -1
  150. package/dist/src/providers/webhook-server.d.ts +3 -0
  151. package/dist/src/providers/webhook-server.js +268 -47
  152. package/dist/src/providers/webhook-server.js.map +1 -1
  153. package/dist/src/servers/discord.d.ts +40 -0
  154. package/dist/src/servers/discord.js +842 -0
  155. package/dist/src/servers/discord.js.map +1 -0
  156. package/dist/src/servers/http.d.ts +14 -2
  157. package/dist/src/servers/http.js +229 -58
  158. package/dist/src/servers/http.js.map +1 -1
  159. package/dist/src/servers/index.d.ts +10 -4
  160. package/dist/src/servers/index.js +4 -0
  161. package/dist/src/servers/index.js.map +1 -1
  162. package/dist/src/servers/matrix.d.ts +3 -0
  163. package/dist/src/servers/matrix.js +268 -110
  164. package/dist/src/servers/matrix.js.map +1 -1
  165. package/dist/src/servers/mattermost.d.ts +5 -0
  166. package/dist/src/servers/mattermost.js +389 -91
  167. package/dist/src/servers/mattermost.js.map +1 -1
  168. package/dist/src/servers/recorder.d.ts +11 -0
  169. package/dist/src/servers/recorder.js +1072 -69
  170. package/dist/src/servers/recorder.js.map +1 -1
  171. package/dist/src/servers/signal.d.ts +1 -0
  172. package/dist/src/servers/signal.js +279 -44
  173. package/dist/src/servers/signal.js.map +1 -1
  174. package/dist/src/servers/slack.d.ts +15 -0
  175. package/dist/src/servers/slack.js +270 -54
  176. package/dist/src/servers/slack.js.map +1 -1
  177. package/dist/src/servers/telegram-identity.d.ts +6 -0
  178. package/dist/src/servers/telegram-identity.js +27 -0
  179. package/dist/src/servers/telegram-identity.js.map +1 -0
  180. package/dist/src/servers/telegram.d.ts +17 -6
  181. package/dist/src/servers/telegram.js +1218 -107
  182. package/dist/src/servers/telegram.js.map +1 -1
  183. package/dist/src/servers/webhook-target.d.ts +12 -4
  184. package/dist/src/servers/webhook-target.js +226 -28
  185. package/dist/src/servers/webhook-target.js.map +1 -1
  186. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
  187. package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
  188. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  189. package/dist/src/servers/whatsapp-jid.d.ts +1 -0
  190. package/dist/src/servers/whatsapp-jid.js +9 -5
  191. package/dist/src/servers/whatsapp-jid.js.map +1 -1
  192. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
  193. package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
  194. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  195. package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
  196. package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
  197. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  198. package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
  199. package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
  200. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  201. package/dist/src/servers/whatsapp-wire/signal.js +3 -3
  202. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  203. package/dist/src/servers/whatsapp.d.ts +7 -1
  204. package/dist/src/servers/whatsapp.js +149 -25
  205. package/dist/src/servers/whatsapp.js.map +1 -1
  206. package/dist/src/servers/zalo.d.ts +1 -0
  207. package/dist/src/servers/zalo.js +193 -82
  208. package/dist/src/servers/zalo.js.map +1 -1
  209. package/docs/channel-setup.md +251 -64
  210. package/fixtures/examples/crabline.example.yaml +3 -2
  211. package/package.json +18 -10
  212. package/dist/src/openclaw/smoke-lock.js +0 -1004
  213. package/dist/src/openclaw/smoke-lock.js.map +0 -1
  214. 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 { LocalMockProviderAdapter } from "../local-mock.js";
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: config.mattermost?.baseUrl ?? env.MATTERMOST_BASE_URL ?? "http://mattermost.local",
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, _runtime) {
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: false,
50
+ authenticated: Boolean(authenticateWebhook),
18
51
  provider: "Mattermost",
19
- requirement: "a provider-native authenticated ingress mode, which this adapter does not support",
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: config.mattermost?.recorder.path
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
- const scopedExpected = target.channelId &&
127
+ if (target.channelId &&
49
128
  MATTERMOST_ID_RULE.pattern.test(expectedThreadId) &&
50
- expectedThreadId !== target.channelId
51
- ? mattermostThreadKey(target.channelId, expectedThreadId)
52
- : expectedThreadId;
129
+ expectedThreadId !== target.channelId) {
130
+ return candidateThreadId === mattermostThreadKey(target.channelId, expectedThreadId);
131
+ }
53
132
  return (candidateThreadId === expectedThreadId ||
54
- candidateThreadId === scopedExpected ||
55
- (MATTERMOST_ID_RULE.pattern.test(scopedExpected) &&
56
- candidateThreadId.startsWith(`${scopedExpected}:thread:`)));
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
- if (optionalRecord(payload, "message")) {
63
- return genericMockPayloadWithNativeThread({
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(payload, "channel_id");
70
- const postId = optionalString(payload, "post_id");
71
- const rootId = optionalString(payload, "root_id");
72
- const text = optionalString(payload, "text");
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: payload,
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":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAE5D,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACrF,OAAO,EACL,iBAAiB,EACjB,kCAAkC,EAClC,QAAQ,EACR,cAAc,EACd,cAAc,EACd,sBAAsB,GACvB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,oCAAoC,EAAE,MAAM,4BAA4B,CAAC;AAElF,MAAM,UAAU,8BAA8B,CAC5C,MAAsB,EACtB,GAAG,GAAsB,OAAO,CAAC,GAAG;IAEpC,OAAO;QACL,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,IAAI,GAAG,CAAC,mBAAmB,IAAI,yBAAyB;QAC3F,QAAQ,EAAE,MAAM,CAAC,UAAU,EAAE,QAAQ,IAAI,GAAG,CAAC,oBAAoB,IAAI,kBAAkB;QACvF,QAAQ,EAAE,MAAM,CAAC,UAAU,EAAE,QAAQ;KACtC,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,yBAA0B,SAAQ,wBAAwB;IACrE,YAAY,EAAU,EAAE,MAAsB,EAAE,SAAiB,EAAE,QAAkB;QACnF,oCAAoC,CAAC;YACnC,aAAa,EAAE,KAAK;YACpB,QAAQ,EAAE,YAAY;YACtB,WAAW,EACT,mFAAmF;YACrF,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO;SACpC,CAAC,CAAC;QACH,KAAK,CAAC;YACJ,KAAK,EAAE,qBAAqB,CAAC,YAAY,CAAC;YAC1C,MAAM;YACN,EAAE;YACF,OAAO,EAAE;gBACP,cAAc,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,qBAAqB,EAAE,IAAI,EAAE,IAAI,EAAE;gBAC9E,aAAa,EAAE,kBAAkB;gBACjC,aAAa,EAAE,uBAAuB;gBACtC,uBAAuB,EAAE,iCAAiC;gBAC1D,QAAQ,EAAE,YAAY;gBACtB,SAAS,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,SAAS;gBAC/C,YAAY,EAAE,MAAM,CAAC,UAAU,EAAE,QAAQ,CAAC,IAAI;oBAC5C,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC;oBAC/C,CAAC,CAAC,SAAS;gBACb,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO;aACpC;SACF,CAAC,CAAC;IACL,CAAC;CACF;AAED,SAAS,mBAAmB,CAAC,SAAiB,EAAE,MAAc;IAC5D,OAAO,GAAG,SAAS,WAAW,MAAM,EAAE,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,iBAAyB,EACzB,gBAAoC,EACpC,MAA0C;IAE1C,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,cAAc,GAClB,MAAM,CAAC,SAAS;QAChB,kBAAkB,CAAC,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC;QACjD,gBAAgB,KAAK,MAAM,CAAC,SAAS;QACnC,CAAC,CAAC,mBAAmB,CAAC,MAAM,CAAC,SAAS,EAAE,gBAAgB,CAAC;QACzD,CAAC,CAAC,gBAAgB,CAAC;IACvB,OAAO,CACL,iBAAiB,KAAK,gBAAgB;QACtC,iBAAiB,KAAK,cAAc;QACpC,CAAC,kBAAkB,CAAC,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC;YAC9C,iBAAiB,CAAC,UAAU,CAAC,GAAG,cAAc,UAAU,CAAC,CAAC,CAC7D,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iCAAiC,CAAC,OAAgB;IAChE,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,aAAa,CAAC,8CAA8C,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;IAC/F,CAAC;IAED,IAAI,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;QACvC,OAAO,kCAAkC,CAAC;YACxC,WAAW,EAAE,kBAAkB;YAC/B,OAAO;YACP,UAAU,EAAE,kBAAkB;SAC/B,CAAC,CAAC;IACL,CAAC;IAED,MAAM,SAAS,GAAG,cAAc,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IACxD,MAAM,MAAM,GAAG,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IAClD,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC7C,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,MAAM,IAAI,aAAa,CAAC,yDAAyD,EAAE;YACjF,IAAI,EAAE,SAAS;SAChB,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,MAAM,EAAE,iBAAiB,CAAC,KAAK,CAAC;QAChC,GAAG,CAAC,MAAM;YACR,CAAC,CAAC,EAAE,EAAE,EAAE,sBAAsB,CAAC,MAAM,EAAE,kBAAkB,EAAE,oBAAoB,CAAC,EAAE;YAClF,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,EAAE,OAAO;QACZ,IAAI;QACJ,QAAQ,EAAE,MAAM;YACd,CAAC,CAAC,mBAAmB,CACjB,sBAAsB,CAAC,SAAS,EAAE,kBAAkB,EAAE,uBAAuB,CAAC,EAC9E,sBAAsB,CAAC,MAAM,EAAE,kBAAkB,EAAE,oBAAoB,CAAC,CACzE;YACH,CAAC,CAAC,sBAAsB,CAAC,SAAS,EAAE,kBAAkB,EAAE,uBAAuB,CAAC;KACnF,CAAC;AACJ,CAAC","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"]}
@@ -26,4 +26,5 @@ export declare function normalizeMsTeamsWebhookPayload(payload: unknown): {
26
26
  raw: {};
27
27
  text?: string | undefined;
28
28
  };
29
+ export declare function handleMsTeamsWebhookPayload(payload: unknown): Response | undefined;
29
30
  export {};
@@ -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
- const fetchedAt = runtime.now?.() ?? Date.now();
29
- const metadataResponse = await fetchImpl(BOT_CONNECTOR_OPENID_URL, { signal });
30
- if (!metadataResponse.ok) {
31
- throw new Error(`Bot Connector metadata fetch failed with HTTP ${metadataResponse.status}.`);
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
- const metadataExpiry = resolveHttpCacheExpiry(metadataResponse, fetchedAt);
34
- const metadata = (await metadataResponse.json());
35
- if (typeof metadata.jwks_uri !== "string") {
36
- throw new Error("Bot Connector metadata omitted jwks_uri.");
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 new Response("unauthorized", {
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 && !key.endorsements.includes(channelId)) {
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
- return createPublicKey({ format: "jwk", key });
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 new Response("unauthorized", {
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
- if (optionalRecord(payload, "message")) {
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 (optionalString(payload, "type") !== "message") {
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 text = optionalString(payload, "text");
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"]}