@gakr-gakr/msteams 0.1.0

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 (107) hide show
  1. package/api.ts +3 -0
  2. package/autobot.plugin.json +15 -0
  3. package/channel-config-api.ts +1 -0
  4. package/channel-plugin-api.ts +2 -0
  5. package/config-api.ts +4 -0
  6. package/contract-api.ts +4 -0
  7. package/index.ts +20 -0
  8. package/package.json +72 -0
  9. package/runtime-api.ts +66 -0
  10. package/secret-contract-api.ts +5 -0
  11. package/setup-entry.ts +13 -0
  12. package/setup-plugin-api.ts +3 -0
  13. package/src/ai-entity.ts +7 -0
  14. package/src/approval-auth.ts +44 -0
  15. package/src/attachments/bot-framework.ts +348 -0
  16. package/src/attachments/download.ts +328 -0
  17. package/src/attachments/graph.ts +489 -0
  18. package/src/attachments/html.ts +122 -0
  19. package/src/attachments/payload.ts +14 -0
  20. package/src/attachments/remote-media.ts +86 -0
  21. package/src/attachments/shared.ts +655 -0
  22. package/src/attachments/types.ts +47 -0
  23. package/src/attachments.ts +18 -0
  24. package/src/channel-api.ts +1 -0
  25. package/src/channel.runtime.ts +56 -0
  26. package/src/channel.setup.ts +77 -0
  27. package/src/channel.ts +1176 -0
  28. package/src/config-schema.ts +6 -0
  29. package/src/config-ui-hints.ts +40 -0
  30. package/src/conversation-store-fs.ts +149 -0
  31. package/src/conversation-store-helpers.ts +105 -0
  32. package/src/conversation-store-memory.ts +51 -0
  33. package/src/conversation-store.ts +71 -0
  34. package/src/directory-live.ts +111 -0
  35. package/src/doctor.ts +27 -0
  36. package/src/errors.ts +270 -0
  37. package/src/feedback-reflection-prompt.ts +117 -0
  38. package/src/feedback-reflection-store.ts +113 -0
  39. package/src/feedback-reflection.ts +271 -0
  40. package/src/file-consent-helpers.ts +115 -0
  41. package/src/file-consent-invoke.ts +150 -0
  42. package/src/file-consent.ts +223 -0
  43. package/src/graph-chat.ts +36 -0
  44. package/src/graph-group-management.ts +168 -0
  45. package/src/graph-members.ts +48 -0
  46. package/src/graph-messages.ts +534 -0
  47. package/src/graph-teams.ts +114 -0
  48. package/src/graph-thread.ts +146 -0
  49. package/src/graph-upload.ts +531 -0
  50. package/src/graph-users.ts +29 -0
  51. package/src/graph.ts +308 -0
  52. package/src/inbound.ts +148 -0
  53. package/src/index.ts +4 -0
  54. package/src/media-helpers.ts +105 -0
  55. package/src/mentions.ts +114 -0
  56. package/src/messenger.ts +608 -0
  57. package/src/monitor-handler/access.ts +136 -0
  58. package/src/monitor-handler/inbound-media.ts +180 -0
  59. package/src/monitor-handler/message-handler-mock-support.test-support.ts +28 -0
  60. package/src/monitor-handler/message-handler.test-support.ts +102 -0
  61. package/src/monitor-handler/message-handler.ts +1015 -0
  62. package/src/monitor-handler/reaction-handler.ts +124 -0
  63. package/src/monitor-handler/thread-session.ts +30 -0
  64. package/src/monitor-handler.ts +538 -0
  65. package/src/monitor-handler.types.ts +27 -0
  66. package/src/monitor-types.ts +6 -0
  67. package/src/monitor.ts +476 -0
  68. package/src/oauth.flow.ts +77 -0
  69. package/src/oauth.shared.ts +37 -0
  70. package/src/oauth.token.ts +162 -0
  71. package/src/oauth.ts +130 -0
  72. package/src/outbound.ts +198 -0
  73. package/src/pending-uploads-fs.ts +235 -0
  74. package/src/pending-uploads.ts +121 -0
  75. package/src/policy.ts +245 -0
  76. package/src/polls-store-memory.ts +32 -0
  77. package/src/polls.ts +312 -0
  78. package/src/presentation.ts +93 -0
  79. package/src/probe.ts +132 -0
  80. package/src/reply-dispatcher.ts +523 -0
  81. package/src/reply-stream-controller.ts +334 -0
  82. package/src/resolve-allowlist.ts +309 -0
  83. package/src/revoked-context.ts +17 -0
  84. package/src/runtime.ts +12 -0
  85. package/src/sdk-types.ts +59 -0
  86. package/src/sdk.ts +916 -0
  87. package/src/secret-contract.ts +49 -0
  88. package/src/secret-input.ts +7 -0
  89. package/src/send-context.ts +269 -0
  90. package/src/send.ts +697 -0
  91. package/src/sent-message-cache.ts +174 -0
  92. package/src/session-route.ts +40 -0
  93. package/src/setup-core.ts +162 -0
  94. package/src/setup-surface.ts +319 -0
  95. package/src/sso-token-store.ts +166 -0
  96. package/src/sso.ts +300 -0
  97. package/src/storage.ts +25 -0
  98. package/src/store-fs.ts +42 -0
  99. package/src/streaming-message.ts +327 -0
  100. package/src/thread-parent-context.ts +159 -0
  101. package/src/token-response.ts +11 -0
  102. package/src/token.ts +194 -0
  103. package/src/user-agent.ts +53 -0
  104. package/src/webhook-timeouts.ts +27 -0
  105. package/src/welcome-card.ts +57 -0
  106. package/test-api.ts +1 -0
  107. package/tsconfig.json +16 -0
@@ -0,0 +1,271 @@
1
+ import { normalizeOptionalLowercaseString } from "autobot/plugin-sdk/string-coerce-runtime";
2
+ import {
3
+ dispatchReplyFromConfigWithSettledDispatcher,
4
+ type AutoBotConfig,
5
+ } from "../runtime-api.js";
6
+ import type { StoredConversationReference } from "./conversation-store.js";
7
+ import { formatUnknownError } from "./errors.js";
8
+ import { buildReflectionPrompt, parseReflectionResponse } from "./feedback-reflection-prompt.js";
9
+ import {
10
+ DEFAULT_COOLDOWN_MS,
11
+ clearReflectionCooldowns,
12
+ isReflectionAllowed,
13
+ loadSessionLearnings,
14
+ recordReflectionTime,
15
+ storeSessionLearning,
16
+ } from "./feedback-reflection-store.js";
17
+ import type { MSTeamsAdapter } from "./messenger.js";
18
+ import { buildConversationReference } from "./messenger.js";
19
+ import type { MSTeamsMonitorLogger } from "./monitor-types.js";
20
+ import { getMSTeamsRuntime } from "./runtime.js";
21
+
22
+ type FeedbackEvent = {
23
+ type: "custom";
24
+ event: "feedback";
25
+ ts: number;
26
+ messageId: string;
27
+ value: "positive" | "negative";
28
+ comment?: string;
29
+ sessionKey: string;
30
+ agentId: string;
31
+ conversationId: string;
32
+ reflectionLearning?: string;
33
+ };
34
+
35
+ export function buildFeedbackEvent(params: {
36
+ messageId: string;
37
+ value: "positive" | "negative";
38
+ comment?: string;
39
+ sessionKey: string;
40
+ agentId: string;
41
+ conversationId: string;
42
+ }): FeedbackEvent {
43
+ return {
44
+ type: "custom",
45
+ event: "feedback",
46
+ ts: Date.now(),
47
+ messageId: params.messageId,
48
+ value: params.value,
49
+ comment: params.comment,
50
+ sessionKey: params.sessionKey,
51
+ agentId: params.agentId,
52
+ conversationId: params.conversationId,
53
+ };
54
+ }
55
+
56
+ type RunFeedbackReflectionParams = {
57
+ cfg: AutoBotConfig;
58
+ adapter: MSTeamsAdapter;
59
+ appId: string;
60
+ conversationRef: StoredConversationReference;
61
+ sessionKey: string;
62
+ agentId: string;
63
+ conversationId: string;
64
+ feedbackMessageId: string;
65
+ thumbedDownResponse?: string;
66
+ userComment?: string;
67
+ log: MSTeamsMonitorLogger;
68
+ };
69
+
70
+ function buildReflectionContext(params: {
71
+ cfg: AutoBotConfig;
72
+ conversationId: string;
73
+ sessionKey: string;
74
+ reflectionPrompt: string;
75
+ }) {
76
+ const core = getMSTeamsRuntime();
77
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(params.cfg);
78
+ const body = core.channel.reply.formatAgentEnvelope({
79
+ channel: "Teams",
80
+ from: "system",
81
+ body: params.reflectionPrompt,
82
+ envelope: envelopeOptions,
83
+ });
84
+
85
+ return {
86
+ ctxPayload: core.channel.reply.finalizeInboundContext({
87
+ Body: body,
88
+ BodyForAgent: params.reflectionPrompt,
89
+ RawBody: params.reflectionPrompt,
90
+ CommandBody: params.reflectionPrompt,
91
+ From: `msteams:system:${params.conversationId}`,
92
+ To: `conversation:${params.conversationId}`,
93
+ SessionKey: params.sessionKey,
94
+ ChatType: "direct" as const,
95
+ SenderName: "system",
96
+ SenderId: "system",
97
+ Provider: "msteams" as const,
98
+ Surface: "msteams" as const,
99
+ Timestamp: Date.now(),
100
+ WasMentioned: true,
101
+ CommandAuthorized: false,
102
+ OriginatingChannel: "msteams" as const,
103
+ OriginatingTo: `conversation:${params.conversationId}`,
104
+ }),
105
+ };
106
+ }
107
+
108
+ function createReflectionCaptureDispatcher(params: {
109
+ cfg: AutoBotConfig;
110
+ agentId: string;
111
+ log: MSTeamsMonitorLogger;
112
+ }) {
113
+ const core = getMSTeamsRuntime();
114
+ let response = "";
115
+ const noopTypingCallbacks = {
116
+ onReplyStart: async () => {},
117
+ onIdle: () => {},
118
+ onCleanup: () => {},
119
+ };
120
+
121
+ const { dispatcher, replyOptions } = core.channel.reply.createReplyDispatcherWithTyping({
122
+ deliver: async (payload) => {
123
+ if (payload.text) {
124
+ response += (response ? "\n" : "") + payload.text;
125
+ }
126
+ },
127
+ typingCallbacks: noopTypingCallbacks,
128
+ humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId),
129
+ onError: (err) => {
130
+ params.log.debug?.("reflection reply error", { error: formatUnknownError(err) });
131
+ },
132
+ });
133
+
134
+ return {
135
+ dispatcher,
136
+ replyOptions,
137
+ readResponse: () => response,
138
+ };
139
+ }
140
+
141
+ async function sendReflectionFollowUp(params: {
142
+ adapter: MSTeamsAdapter;
143
+ appId: string;
144
+ conversationRef: StoredConversationReference;
145
+ userMessage: string;
146
+ }): Promise<void> {
147
+ const baseRef = buildConversationReference(params.conversationRef);
148
+ const proactiveRef = { ...baseRef, activityId: undefined };
149
+
150
+ await params.adapter.continueConversation(params.appId, proactiveRef, async (ctx) => {
151
+ await ctx.sendActivity({
152
+ type: "message",
153
+ text: params.userMessage,
154
+ });
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Run a background reflection after negative feedback.
160
+ * This is designed to be called fire-and-forget (don't await in the invoke handler).
161
+ */
162
+ export async function runFeedbackReflection(params: RunFeedbackReflectionParams): Promise<void> {
163
+ const { cfg, log, sessionKey } = params;
164
+ const cooldownMs = cfg.channels?.msteams?.feedbackReflectionCooldownMs ?? DEFAULT_COOLDOWN_MS;
165
+ if (!isReflectionAllowed(sessionKey, cooldownMs)) {
166
+ log.debug?.("skipping reflection (cooldown active)", { sessionKey });
167
+ return;
168
+ }
169
+
170
+ const reflectionPrompt = buildReflectionPrompt({
171
+ thumbedDownResponse: params.thumbedDownResponse,
172
+ userComment: params.userComment,
173
+ });
174
+ const runtime = getMSTeamsRuntime();
175
+ const storePath = runtime.channel.session.resolveStorePath(cfg.session?.store, {
176
+ agentId: params.agentId,
177
+ });
178
+ const { ctxPayload } = buildReflectionContext({
179
+ cfg,
180
+ conversationId: params.conversationId,
181
+ sessionKey: params.sessionKey,
182
+ reflectionPrompt,
183
+ });
184
+
185
+ const capture = createReflectionCaptureDispatcher({
186
+ cfg,
187
+ agentId: params.agentId,
188
+ log,
189
+ });
190
+
191
+ try {
192
+ await dispatchReplyFromConfigWithSettledDispatcher({
193
+ ctxPayload,
194
+ cfg,
195
+ dispatcher: capture.dispatcher,
196
+ onSettled: () => {},
197
+ replyOptions: capture.replyOptions,
198
+ });
199
+ } catch (err) {
200
+ log.error("reflection dispatch failed", { error: formatUnknownError(err) });
201
+ return;
202
+ }
203
+
204
+ const reflectionResponse = capture.readResponse().trim();
205
+ if (!reflectionResponse) {
206
+ log.debug?.("reflection produced no output");
207
+ return;
208
+ }
209
+
210
+ const parsedReflection = parseReflectionResponse(reflectionResponse);
211
+ if (!parsedReflection) {
212
+ log.debug?.("reflection produced no structured output");
213
+ return;
214
+ }
215
+
216
+ recordReflectionTime(sessionKey, cooldownMs);
217
+ log.info("reflection complete", {
218
+ sessionKey,
219
+ responseLength: reflectionResponse.length,
220
+ followUp: parsedReflection.followUp,
221
+ });
222
+
223
+ try {
224
+ await storeSessionLearning({
225
+ storePath,
226
+ sessionKey: params.sessionKey,
227
+ learning: parsedReflection.learning,
228
+ });
229
+ } catch (err) {
230
+ log.debug?.("failed to store reflection learning", { error: formatUnknownError(err) });
231
+ }
232
+
233
+ const conversationType = normalizeOptionalLowercaseString(
234
+ params.conversationRef.conversation?.conversationType,
235
+ );
236
+ const shouldNotify =
237
+ conversationType === "personal" &&
238
+ parsedReflection.followUp &&
239
+ Boolean(parsedReflection.userMessage);
240
+
241
+ if (!shouldNotify) {
242
+ if (parsedReflection.followUp && conversationType !== "personal") {
243
+ log.debug?.("skipping reflection follow-up outside direct message", {
244
+ sessionKey,
245
+ conversationType,
246
+ });
247
+ }
248
+ return;
249
+ }
250
+
251
+ try {
252
+ await sendReflectionFollowUp({
253
+ adapter: params.adapter,
254
+ appId: params.appId,
255
+ conversationRef: params.conversationRef,
256
+ userMessage: parsedReflection.userMessage!,
257
+ });
258
+ log.info("sent reflection follow-up", { sessionKey });
259
+ } catch (err) {
260
+ log.debug?.("failed to send reflection follow-up", { error: formatUnknownError(err) });
261
+ }
262
+ }
263
+
264
+ export {
265
+ buildReflectionPrompt,
266
+ clearReflectionCooldowns,
267
+ isReflectionAllowed,
268
+ loadSessionLearnings,
269
+ parseReflectionResponse,
270
+ recordReflectionTime,
271
+ };
@@ -0,0 +1,115 @@
1
+ import { normalizeOptionalLowercaseString } from "autobot/plugin-sdk/string-coerce-runtime";
2
+ import { buildFileConsentCard } from "./file-consent.js";
3
+ import { storePendingUploadFs } from "./pending-uploads-fs.js";
4
+ import { storePendingUpload } from "./pending-uploads.js";
5
+
6
+ type FileConsentMedia = {
7
+ buffer: Buffer;
8
+ filename: string;
9
+ contentType?: string;
10
+ };
11
+
12
+ type FileConsentActivityResult = {
13
+ activity: Record<string, unknown>;
14
+ uploadId: string;
15
+ };
16
+
17
+ function buildConsentActivity(params: {
18
+ media: FileConsentMedia;
19
+ description?: string;
20
+ uploadId: string;
21
+ }): Record<string, unknown> {
22
+ const { media, description, uploadId } = params;
23
+ const consentCard = buildFileConsentCard({
24
+ filename: media.filename,
25
+ description: description || `File: ${media.filename}`,
26
+ sizeInBytes: media.buffer.length,
27
+ context: { uploadId },
28
+ });
29
+ return {
30
+ type: "message",
31
+ attachments: [consentCard],
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Prepare a FileConsentCard activity for large files or non-images in personal chats.
37
+ * Returns the activity object and uploadId - caller is responsible for sending.
38
+ *
39
+ * This variant only writes to the in-memory store. Use it when the caller and
40
+ * the `fileConsent/invoke` handler share the same process (for example the
41
+ * messenger reply path). For proactive CLI sends where the invoke arrives in
42
+ * a different process, use {@link prepareFileConsentActivityFs} instead.
43
+ */
44
+ export function prepareFileConsentActivity(params: {
45
+ media: FileConsentMedia;
46
+ conversationId: string;
47
+ description?: string;
48
+ }): FileConsentActivityResult {
49
+ const { media, conversationId, description } = params;
50
+
51
+ const uploadId = storePendingUpload({
52
+ buffer: media.buffer,
53
+ filename: media.filename,
54
+ contentType: media.contentType,
55
+ conversationId,
56
+ });
57
+
58
+ const activity = buildConsentActivity({ media, description, uploadId });
59
+ return { activity, uploadId };
60
+ }
61
+
62
+ /**
63
+ * Prepare a FileConsentCard activity and persist the pending upload to the
64
+ * filesystem so a different process can read it when the user accepts.
65
+ *
66
+ * This is used by the proactive CLI `message send --media` path: the CLI
67
+ * process sends the card and exits, but the `fileConsent/invoke` callback is
68
+ * delivered to the long-lived gateway monitor process. The FS-backed store
69
+ * bridges those two processes. The in-memory store is also populated so
70
+ * same-process flows keep the fast path.
71
+ */
72
+ export async function prepareFileConsentActivityFs(params: {
73
+ media: FileConsentMedia;
74
+ conversationId: string;
75
+ description?: string;
76
+ }): Promise<FileConsentActivityResult> {
77
+ const { media, conversationId, description } = params;
78
+
79
+ // Populate the in-memory store first so the uploadId is consistent, then
80
+ // mirror the same entry to the FS store under the same id so an invoke
81
+ // handler in another process can find it.
82
+ const uploadId = storePendingUpload({
83
+ buffer: media.buffer,
84
+ filename: media.filename,
85
+ contentType: media.contentType,
86
+ conversationId,
87
+ });
88
+
89
+ await storePendingUploadFs({
90
+ id: uploadId,
91
+ buffer: media.buffer,
92
+ filename: media.filename,
93
+ contentType: media.contentType,
94
+ conversationId,
95
+ });
96
+
97
+ const activity = buildConsentActivity({ media, description, uploadId });
98
+ return { activity, uploadId };
99
+ }
100
+
101
+ /**
102
+ * Check if a file requires FileConsentCard flow.
103
+ * True for: personal chat AND (large file OR non-image)
104
+ */
105
+ export function requiresFileConsent(params: {
106
+ conversationType: string | undefined;
107
+ contentType: string | undefined;
108
+ bufferSize: number;
109
+ thresholdBytes: number;
110
+ }): boolean {
111
+ const isPersonal = normalizeOptionalLowercaseString(params.conversationType) === "personal";
112
+ const isImage = params.contentType?.startsWith("image/") ?? false;
113
+ const isLargeFile = params.bufferSize >= params.thresholdBytes;
114
+ return isPersonal && (isLargeFile || !isImage);
115
+ }
@@ -0,0 +1,150 @@
1
+ import { formatUnknownError } from "./errors.js";
2
+ import { buildFileInfoCard, parseFileConsentInvoke, uploadToConsentUrl } from "./file-consent.js";
3
+ import { normalizeMSTeamsConversationId } from "./inbound.js";
4
+ import type { MSTeamsMonitorLogger } from "./monitor-types.js";
5
+ import { getPendingUploadFs, removePendingUploadFs } from "./pending-uploads-fs.js";
6
+ import { getPendingUpload, removePendingUpload } from "./pending-uploads.js";
7
+ import { withRevokedProxyFallback } from "./revoked-context.js";
8
+ import type { MSTeamsTurnContext } from "./sdk-types.js";
9
+
10
+ /**
11
+ * Handle fileConsent/invoke activities for large file uploads.
12
+ */
13
+ async function handleMSTeamsFileConsentInvoke(
14
+ context: MSTeamsTurnContext,
15
+ log: MSTeamsMonitorLogger,
16
+ ): Promise<boolean> {
17
+ const expiredUploadMessage =
18
+ "The file upload request has expired. Please try sending the file again.";
19
+ const activity = context.activity;
20
+ if (activity.type !== "invoke" || activity.name !== "fileConsent/invoke") {
21
+ return false;
22
+ }
23
+
24
+ const consentResponse = parseFileConsentInvoke(activity);
25
+ if (!consentResponse) {
26
+ log.debug?.("invalid file consent invoke", { value: activity.value });
27
+ return false;
28
+ }
29
+
30
+ const uploadId =
31
+ typeof consentResponse.context?.uploadId === "string"
32
+ ? consentResponse.context.uploadId
33
+ : undefined;
34
+ // Prefer the in-memory store (same-process reply path); fall back to the
35
+ // FS-backed store so CLI `message send --media` flows work even when the
36
+ // invoke callback is delivered to a different process.
37
+ const inMemoryFile = getPendingUpload(uploadId);
38
+ const fsFile = inMemoryFile ? undefined : await getPendingUploadFs(uploadId);
39
+ const pendingFile:
40
+ | {
41
+ buffer: Buffer;
42
+ filename: string;
43
+ contentType?: string;
44
+ conversationId: string;
45
+ consentCardActivityId?: string;
46
+ }
47
+ | undefined = inMemoryFile ?? fsFile;
48
+ if (pendingFile) {
49
+ const pendingConversationId = normalizeMSTeamsConversationId(pendingFile.conversationId);
50
+ const invokeConversationId = normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
51
+ if (!invokeConversationId || pendingConversationId !== invokeConversationId) {
52
+ log.info("file consent conversation mismatch", {
53
+ uploadId,
54
+ expectedConversationId: pendingConversationId,
55
+ receivedConversationId: invokeConversationId || undefined,
56
+ });
57
+ if (consentResponse.action === "accept") {
58
+ await context.sendActivity(expiredUploadMessage);
59
+ }
60
+ return true;
61
+ }
62
+ }
63
+
64
+ if (consentResponse.action === "accept" && consentResponse.uploadInfo) {
65
+ if (pendingFile) {
66
+ log.debug?.("user accepted file consent, uploading", {
67
+ uploadId,
68
+ filename: pendingFile.filename,
69
+ size: pendingFile.buffer.length,
70
+ });
71
+
72
+ try {
73
+ await uploadToConsentUrl({
74
+ url: consentResponse.uploadInfo.uploadUrl,
75
+ buffer: pendingFile.buffer,
76
+ contentType: pendingFile.contentType,
77
+ });
78
+
79
+ const fileInfoCard = buildFileInfoCard({
80
+ filename: consentResponse.uploadInfo.name,
81
+ contentUrl: consentResponse.uploadInfo.contentUrl,
82
+ uniqueId: consentResponse.uploadInfo.uniqueId,
83
+ fileType: consentResponse.uploadInfo.fileType,
84
+ });
85
+
86
+ if (!pendingFile.consentCardActivityId) {
87
+ await context.sendActivity({
88
+ type: "message",
89
+ attachments: [fileInfoCard],
90
+ });
91
+ }
92
+
93
+ if (pendingFile.consentCardActivityId) {
94
+ try {
95
+ await context.updateActivity({
96
+ id: pendingFile.consentCardActivityId,
97
+ type: "message",
98
+ attachments: [fileInfoCard],
99
+ });
100
+ } catch {
101
+ await context.sendActivity({
102
+ type: "message",
103
+ attachments: [fileInfoCard],
104
+ });
105
+ }
106
+ }
107
+
108
+ log.info("file upload complete", {
109
+ uploadId,
110
+ filename: consentResponse.uploadInfo.name,
111
+ uniqueId: consentResponse.uploadInfo.uniqueId,
112
+ });
113
+ } catch (err) {
114
+ log.error("file upload failed", { uploadId, error: formatUnknownError(err) });
115
+ await context.sendActivity("File upload failed. Please try again.");
116
+ } finally {
117
+ removePendingUpload(uploadId);
118
+ await removePendingUploadFs(uploadId);
119
+ }
120
+ } else {
121
+ log.debug?.("pending file not found for consent", { uploadId });
122
+ await context.sendActivity(expiredUploadMessage);
123
+ }
124
+ } else {
125
+ log.debug?.("user declined file consent", { uploadId });
126
+ removePendingUpload(uploadId);
127
+ await removePendingUploadFs(uploadId);
128
+ }
129
+
130
+ return true;
131
+ }
132
+
133
+ export async function respondToMSTeamsFileConsentInvoke(
134
+ context: MSTeamsTurnContext,
135
+ log: MSTeamsMonitorLogger,
136
+ ): Promise<void> {
137
+ await context.sendActivity({ type: "invokeResponse", value: { status: 200 } });
138
+
139
+ try {
140
+ await withRevokedProxyFallback({
141
+ run: async () => await handleMSTeamsFileConsentInvoke(context, log),
142
+ onRevoked: async () => true,
143
+ onRevokedLog: () => {
144
+ log.debug?.("turn context revoked during file consent invoke; skipping delayed response");
145
+ },
146
+ });
147
+ } catch (err) {
148
+ log.debug?.("file consent handler error", { error: formatUnknownError(err) });
149
+ }
150
+ }