@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
package/api.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { msteamsPlugin } from "./src/channel.js";
2
+ export { createMSTeamsSetupWizardBase, msteamsSetupAdapter } from "./src/setup-core.js";
3
+ export { msteamsSetupWizard, openDelegatedOAuthUrl } from "./src/setup-surface.js";
@@ -0,0 +1,15 @@
1
+ {
2
+ "id": "msteams",
3
+ "activation": {
4
+ "onStartup": false
5
+ },
6
+ "channels": ["msteams"],
7
+ "channelEnvVars": {
8
+ "msteams": ["MSTEAMS_APP_ID", "MSTEAMS_APP_PASSWORD", "MSTEAMS_TENANT_ID"]
9
+ },
10
+ "configSchema": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "properties": {}
14
+ }
15
+ }
@@ -0,0 +1 @@
1
+ export { MSTeamsChannelConfigSchema } from "./src/config-schema.js";
@@ -0,0 +1,2 @@
1
+ export { msteamsPlugin } from "./src/channel.js";
2
+ export type { ChannelPlugin } from "./src/channel-api.js";
package/config-api.ts ADDED
@@ -0,0 +1,4 @@
1
+ export {
2
+ buildChannelConfigSchema,
3
+ MSTeamsConfigSchema,
4
+ } from "autobot/plugin-sdk/bundled-channel-config-schema";
@@ -0,0 +1,4 @@
1
+ export {
2
+ collectRuntimeConfigAssignments,
3
+ secretTargetRegistryEntries,
4
+ } from "./src/secret-contract.js";
package/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { defineBundledChannelEntry } from "autobot/plugin-sdk/channel-entry-contract";
2
+
3
+ export default defineBundledChannelEntry({
4
+ id: "msteams",
5
+ name: "Microsoft Teams",
6
+ description: "Microsoft Teams channel plugin (Bot Framework)",
7
+ importMetaUrl: import.meta.url,
8
+ plugin: {
9
+ specifier: "./channel-plugin-api.js",
10
+ exportName: "msteamsPlugin",
11
+ },
12
+ secrets: {
13
+ specifier: "./secret-contract-api.js",
14
+ exportName: "channelSecrets",
15
+ },
16
+ runtime: {
17
+ specifier: "./runtime-api.js",
18
+ exportName: "setMSTeamsRuntime",
19
+ },
20
+ });
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@gakr-gakr/msteams",
3
+ "version": "0.1.0",
4
+ "description": "AutoBot Microsoft Teams channel plugin",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/autobot/autobot"
8
+ },
9
+ "type": "module",
10
+ "dependencies": {
11
+ "@azure/identity": "4.13.1",
12
+ "@microsoft/teams.api": "2.0.11",
13
+ "@microsoft/teams.apps": "2.0.11",
14
+ "express": "5.2.1",
15
+ "jsonwebtoken": "9.0.3",
16
+ "jwks-rsa": "4.0.1",
17
+ "typebox": "1.1.38"
18
+ },
19
+ "devDependencies": {
20
+ "@gakr-gakr/plugin-sdk": "workspace:*",
21
+ "@gakr-gakr/autobot": "workspace:*",
22
+ "@types/jsonwebtoken": "9.0.10",
23
+ "autobot": "workspace:@gakr-gakr/autobot@*"
24
+ },
25
+ "peerDependencies": {
26
+ "@gakr-gakr/autobot": ">=0.1.0"
27
+ },
28
+ "peerDependenciesMeta": {
29
+ "@gakr-gakr/autobot": {
30
+ "optional": true
31
+ }
32
+ },
33
+ "autobot": {
34
+ "extensions": [
35
+ "./index.ts"
36
+ ],
37
+ "setupEntry": "./setup-entry.ts",
38
+ "channel": {
39
+ "id": "msteams",
40
+ "label": "Microsoft Teams",
41
+ "selectionLabel": "Microsoft Teams (Teams SDK)",
42
+ "docsPath": "/channels/msteams",
43
+ "docsLabel": "msteams",
44
+ "blurb": "Teams SDK; enterprise support.",
45
+ "aliases": [
46
+ "teams"
47
+ ],
48
+ "order": 60,
49
+ "doctorCapabilities": {
50
+ "dmAllowFromMode": "topOnly",
51
+ "groupModel": "hybrid",
52
+ "groupAllowFromFallbackToAllowFrom": true,
53
+ "warnOnEmptyGroupSenderAllowlist": true
54
+ }
55
+ },
56
+ "install": {
57
+ "npmSpec": "@gakr-gakr/msteams",
58
+ "defaultChoice": "npm",
59
+ "minHostVersion": ">=2026.4.10"
60
+ },
61
+ "compat": {
62
+ "pluginApi": ">=2026.5.19"
63
+ },
64
+ "build": {
65
+ "autobotVersion": "2026.5.19"
66
+ },
67
+ "release": {
68
+ "publishToClawHub": true,
69
+ "publishToNpm": true
70
+ }
71
+ }
72
+ }
package/runtime-api.ts ADDED
@@ -0,0 +1,66 @@
1
+ // Private runtime barrel for the bundled Microsoft Teams extension.
2
+ // Keep this barrel thin and aligned with the local extension surface.
3
+
4
+ export { DEFAULT_ACCOUNT_ID } from "autobot/plugin-sdk/account-id";
5
+ export type { AllowlistMatch } from "autobot/plugin-sdk/allow-from";
6
+ export {
7
+ mergeAllowlist,
8
+ resolveAllowlistMatchSimple,
9
+ summarizeMapping,
10
+ } from "autobot/plugin-sdk/allow-from";
11
+ export type {
12
+ BaseProbeResult,
13
+ ChannelDirectoryEntry,
14
+ ChannelGroupContext,
15
+ ChannelMessageActionName,
16
+ ChannelOutboundAdapter,
17
+ } from "autobot/plugin-sdk/channel-contract";
18
+ export type { ChannelPlugin } from "autobot/plugin-sdk/channel-core";
19
+ export { logTypingFailure } from "autobot/plugin-sdk/channel-logging";
20
+ export { createChannelPairingController } from "autobot/plugin-sdk/channel-pairing";
21
+ export { resolveToolsBySender } from "autobot/plugin-sdk/channel-policy";
22
+ export { createChannelMessageReplyPipeline } from "autobot/plugin-sdk/channel-message";
23
+ export {
24
+ PAIRING_APPROVED_MESSAGE,
25
+ buildProbeChannelStatusSummary,
26
+ createDefaultChannelRuntimeState,
27
+ } from "autobot/plugin-sdk/channel-status";
28
+ export {
29
+ buildChannelKeyCandidates,
30
+ normalizeChannelSlug,
31
+ resolveChannelEntryMatchWithFallback,
32
+ resolveNestedAllowlistDecision,
33
+ } from "autobot/plugin-sdk/channel-targets";
34
+ export type {
35
+ GroupPolicy,
36
+ GroupToolPolicyConfig,
37
+ MSTeamsChannelConfig,
38
+ MSTeamsConfig,
39
+ MSTeamsReplyStyle,
40
+ MSTeamsTeamConfig,
41
+ MarkdownTableMode,
42
+ AutoBotConfig,
43
+ } from "autobot/plugin-sdk/config-contracts";
44
+ export { isDangerousNameMatchingEnabled } from "autobot/plugin-sdk/dangerous-name-runtime";
45
+ export { resolveDefaultGroupPolicy } from "autobot/plugin-sdk/runtime-group-policy";
46
+ export { withFileLock } from "autobot/plugin-sdk/file-lock";
47
+ export { keepHttpServerTaskAlive } from "autobot/plugin-sdk/channel-lifecycle";
48
+ export {
49
+ detectMime,
50
+ extensionForMime,
51
+ extractOriginalFilename,
52
+ getFileExtension,
53
+ resolveChannelMediaMaxBytes,
54
+ } from "autobot/plugin-sdk/media-runtime";
55
+ export { dispatchReplyFromConfigWithSettledDispatcher } from "autobot/plugin-sdk/inbound-reply-dispatch";
56
+ export { loadOutboundMediaFromUrl } from "autobot/plugin-sdk/outbound-media";
57
+ export { buildMediaPayload } from "autobot/plugin-sdk/reply-payload";
58
+ export type { ReplyPayload } from "autobot/plugin-sdk/reply-payload";
59
+ export type { PluginRuntime } from "autobot/plugin-sdk/runtime-store";
60
+ export type { RuntimeEnv } from "autobot/plugin-sdk/runtime";
61
+ export type { SsrFPolicy } from "autobot/plugin-sdk/ssrf-runtime";
62
+ export { fetchWithSsrFGuard } from "autobot/plugin-sdk/ssrf-runtime";
63
+ export { normalizeStringEntries } from "autobot/plugin-sdk/string-normalization-runtime";
64
+ export { chunkTextForOutbound } from "autobot/plugin-sdk/text-chunking";
65
+ export { DEFAULT_WEBHOOK_MAX_BODY_BYTES } from "autobot/plugin-sdk/webhook-ingress";
66
+ export { setMSTeamsRuntime } from "./src/runtime.js";
@@ -0,0 +1,5 @@
1
+ export {
2
+ channelSecrets,
3
+ collectRuntimeConfigAssignments,
4
+ secretTargetRegistryEntries,
5
+ } from "./src/secret-contract.js";
package/setup-entry.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { defineBundledChannelSetupEntry } from "autobot/plugin-sdk/channel-entry-contract";
2
+
3
+ export default defineBundledChannelSetupEntry({
4
+ importMetaUrl: import.meta.url,
5
+ plugin: {
6
+ specifier: "./setup-plugin-api.js",
7
+ exportName: "msteamsSetupPlugin",
8
+ },
9
+ secrets: {
10
+ specifier: "./secret-contract-api.js",
11
+ exportName: "channelSecrets",
12
+ },
13
+ });
@@ -0,0 +1,3 @@
1
+ // Keep bundled setup entry imports narrow so setup loads do not pull the
2
+ // broader Teams channel plugin surface.
3
+ export { msteamsSetupPlugin } from "./src/channel.setup.js";
@@ -0,0 +1,7 @@
1
+ /** AI-generated content entity added to every outbound AI message. */
2
+ export const AI_GENERATED_ENTITY = {
3
+ type: "https://schema.org/Message",
4
+ "@type": "Message",
5
+ "@id": "",
6
+ additionalType: ["AIGeneratedContent"],
7
+ };
@@ -0,0 +1,44 @@
1
+ import {
2
+ createResolvedApproverActionAuthAdapter,
3
+ resolveApprovalApprovers,
4
+ } from "autobot/plugin-sdk/approval-auth-runtime";
5
+ import { normalizeOptionalLowercaseString } from "autobot/plugin-sdk/string-coerce-runtime";
6
+ import type { AutoBotConfig } from "../runtime-api.js";
7
+ import { normalizeMSTeamsMessagingTarget } from "./resolve-allowlist.js";
8
+
9
+ const MSTEAMS_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10
+
11
+ function normalizeMSTeamsApproverId(value: string | number): string | undefined {
12
+ const normalized = normalizeMSTeamsMessagingTarget(String(value));
13
+ if (!normalized?.startsWith("user:")) {
14
+ return undefined;
15
+ }
16
+ const id = normalizeOptionalLowercaseString(normalized.slice("user:".length));
17
+ if (!id) {
18
+ return undefined;
19
+ }
20
+ return MSTEAMS_ID_RE.test(id) ? id : undefined;
21
+ }
22
+
23
+ function resolveMSTeamsChannelConfig(cfg: AutoBotConfig) {
24
+ return cfg.channels?.msteams;
25
+ }
26
+
27
+ export const msTeamsApprovalAuth = createResolvedApproverActionAuthAdapter({
28
+ channelLabel: "Microsoft Teams",
29
+ resolveApprovers: ({ cfg }) => {
30
+ const channel = resolveMSTeamsChannelConfig(cfg);
31
+ return resolveApprovalApprovers({
32
+ allowFrom: channel?.allowFrom,
33
+ defaultTo: channel?.defaultTo,
34
+ normalizeApprover: normalizeMSTeamsApproverId,
35
+ });
36
+ },
37
+ normalizeSenderId: (value) => {
38
+ const trimmed = normalizeOptionalLowercaseString(value);
39
+ if (!trimmed) {
40
+ return undefined;
41
+ }
42
+ return MSTEAMS_ID_RE.test(trimmed) ? trimmed : undefined;
43
+ },
44
+ });
@@ -0,0 +1,348 @@
1
+ import { getMSTeamsRuntime } from "../runtime.js";
2
+ import { ensureUserAgentHeader } from "../user-agent.js";
3
+ import {
4
+ inferPlaceholder,
5
+ isUrlAllowed,
6
+ type MSTeamsAttachmentDownloadLogger,
7
+ type MSTeamsAttachmentFetchPolicy,
8
+ type MSTeamsAttachmentResolveFn,
9
+ resolveAttachmentFetchPolicy,
10
+ safeFetchWithPolicy,
11
+ } from "./shared.js";
12
+ import type {
13
+ MSTeamsAccessTokenProvider,
14
+ MSTeamsGraphMediaResult,
15
+ MSTeamsInboundMedia,
16
+ } from "./types.js";
17
+
18
+ /**
19
+ * Bot Framework Service token scope for requesting a token used against
20
+ * the Bot Connector (v3) REST endpoints such as `/v3/attachments/{id}`.
21
+ */
22
+ const BOT_FRAMEWORK_SCOPE = "https://api.botframework.com";
23
+
24
+ /**
25
+ * Detect Bot Framework personal chat ("a:") and MSA orgid ("8:orgid:") conversation
26
+ * IDs. These identifiers are not recognized by Graph's `/chats/{id}` endpoint, so we
27
+ * must fetch media via the Bot Framework v3 attachments endpoint instead.
28
+ *
29
+ * Graph-compatible IDs start with `19:` and are left untouched by this detector.
30
+ */
31
+ export function isBotFrameworkPersonalChatId(conversationId: string | null | undefined): boolean {
32
+ if (typeof conversationId !== "string") {
33
+ return false;
34
+ }
35
+ const trimmed = conversationId.trim();
36
+ return trimmed.startsWith("a:") || trimmed.startsWith("8:orgid:");
37
+ }
38
+
39
+ type BotFrameworkView = {
40
+ viewId?: string | null;
41
+ size?: number | null;
42
+ };
43
+
44
+ type BotFrameworkAttachmentInfo = {
45
+ name?: string | null;
46
+ type?: string | null;
47
+ views?: BotFrameworkView[] | null;
48
+ };
49
+
50
+ function normalizeServiceUrl(serviceUrl: string): string {
51
+ // Bot Framework service URLs sometimes carry a trailing slash; normalize so
52
+ // we can safely append `/v3/attachments/...` below.
53
+ return serviceUrl.replace(/\/+$/, "");
54
+ }
55
+
56
+ async function fetchBotFrameworkAttachmentInfo(params: {
57
+ serviceUrl: string;
58
+ attachmentId: string;
59
+ accessToken: string;
60
+ policy: MSTeamsAttachmentFetchPolicy;
61
+ fetchFn?: typeof fetch;
62
+ resolveFn?: MSTeamsAttachmentResolveFn;
63
+ logger?: MSTeamsAttachmentDownloadLogger;
64
+ }): Promise<BotFrameworkAttachmentInfo | undefined> {
65
+ const url = `${normalizeServiceUrl(params.serviceUrl)}/v3/attachments/${encodeURIComponent(params.attachmentId)}`;
66
+ // Use `safeFetchWithPolicy` instead of `fetchWithSsrFGuard`. The strict
67
+ // pinned undici dispatcher used by `fetchWithSsrFGuard` is incompatible
68
+ // with Node 24+'s built-in undici v7 and silently breaks Bot Framework
69
+ // attachment downloads (same root cause as the SharePoint fix in #63396).
70
+ // `safeFetchWithPolicy` already enforces hostname allowlist validation
71
+ // across every redirect hop, which is sufficient for these attachment
72
+ // service URLs.
73
+ let response: Response;
74
+ try {
75
+ response = await safeFetchWithPolicy({
76
+ url,
77
+ policy: params.policy,
78
+ fetchFn: params.fetchFn,
79
+ resolveFn: params.resolveFn,
80
+ requestInit: {
81
+ headers: ensureUserAgentHeader({ Authorization: `Bearer ${params.accessToken}` }),
82
+ },
83
+ });
84
+ } catch (err) {
85
+ params.logger?.warn?.("msteams botFramework attachmentInfo fetch failed", {
86
+ error: err instanceof Error ? err.message : String(err),
87
+ });
88
+ return undefined;
89
+ }
90
+ if (!response.ok) {
91
+ params.logger?.warn?.("msteams botFramework attachmentInfo non-ok", {
92
+ status: response.status,
93
+ });
94
+ return undefined;
95
+ }
96
+ try {
97
+ return (await response.json()) as BotFrameworkAttachmentInfo;
98
+ } catch (err) {
99
+ params.logger?.warn?.("msteams botFramework attachmentInfo parse failed", {
100
+ error: err instanceof Error ? err.message : String(err),
101
+ });
102
+ return undefined;
103
+ }
104
+ }
105
+
106
+ async function saveBotFrameworkAttachmentView(params: {
107
+ serviceUrl: string;
108
+ attachmentId: string;
109
+ viewId: string;
110
+ accessToken: string;
111
+ maxBytes: number;
112
+ fileNameHint?: string;
113
+ contentTypeHint?: string;
114
+ preserveFilenames?: boolean;
115
+ policy: MSTeamsAttachmentFetchPolicy;
116
+ fetchFn?: typeof fetch;
117
+ resolveFn?: MSTeamsAttachmentResolveFn;
118
+ logger?: MSTeamsAttachmentDownloadLogger;
119
+ }): Promise<{ path: string; contentType?: string } | undefined> {
120
+ const url = `${normalizeServiceUrl(params.serviceUrl)}/v3/attachments/${encodeURIComponent(params.attachmentId)}/views/${encodeURIComponent(params.viewId)}`;
121
+ // See `fetchBotFrameworkAttachmentInfo` for why this uses
122
+ // `safeFetchWithPolicy` instead of `fetchWithSsrFGuard` on Node 24+ (#63396).
123
+ let response: Response;
124
+ try {
125
+ response = await safeFetchWithPolicy({
126
+ url,
127
+ policy: params.policy,
128
+ fetchFn: params.fetchFn,
129
+ resolveFn: params.resolveFn,
130
+ requestInit: {
131
+ headers: ensureUserAgentHeader({ Authorization: `Bearer ${params.accessToken}` }),
132
+ },
133
+ });
134
+ } catch (err) {
135
+ params.logger?.warn?.("msteams botFramework attachmentView fetch failed", {
136
+ error: err instanceof Error ? err.message : String(err),
137
+ });
138
+ return undefined;
139
+ }
140
+ if (!response.ok) {
141
+ params.logger?.warn?.("msteams botFramework attachmentView non-ok", {
142
+ status: response.status,
143
+ });
144
+ return undefined;
145
+ }
146
+ const contentLength = response.headers.get("content-length");
147
+ if (contentLength && Number(contentLength) > params.maxBytes) {
148
+ return undefined;
149
+ }
150
+ try {
151
+ return await getMSTeamsRuntime().channel.media.saveResponseMedia(response, {
152
+ sourceUrl: url,
153
+ filePathHint: params.fileNameHint,
154
+ maxBytes: params.maxBytes,
155
+ fallbackContentType: params.contentTypeHint,
156
+ subdir: "inbound",
157
+ originalFilename: params.preserveFilenames ? params.fileNameHint : undefined,
158
+ });
159
+ } catch (err) {
160
+ params.logger?.warn?.("msteams botFramework attachmentView save failed", {
161
+ error: err instanceof Error ? err.message : String(err),
162
+ });
163
+ return undefined;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Download media for a single attachment via the Bot Framework v3 attachments
169
+ * endpoint. Used for personal DM conversations where the Graph `/chats/{id}`
170
+ * path is not usable because the Bot Framework conversation ID (`a:...`) is
171
+ * not a valid Graph chat identifier.
172
+ */
173
+ export async function downloadMSTeamsBotFrameworkAttachment(params: {
174
+ serviceUrl: string;
175
+ attachmentId: string;
176
+ tokenProvider?: MSTeamsAccessTokenProvider;
177
+ maxBytes: number;
178
+ allowHosts?: string[];
179
+ authAllowHosts?: string[];
180
+ fetchFn?: typeof fetch;
181
+ resolveFn?: MSTeamsAttachmentResolveFn;
182
+ fileNameHint?: string | null;
183
+ contentTypeHint?: string | null;
184
+ preserveFilenames?: boolean;
185
+ logger?: MSTeamsAttachmentDownloadLogger;
186
+ }): Promise<MSTeamsInboundMedia | undefined> {
187
+ if (!params.serviceUrl || !params.attachmentId || !params.tokenProvider) {
188
+ return undefined;
189
+ }
190
+ const policy: MSTeamsAttachmentFetchPolicy = resolveAttachmentFetchPolicy({
191
+ allowHosts: params.allowHosts,
192
+ authAllowHosts: params.authAllowHosts,
193
+ });
194
+ const baseUrl = `${normalizeServiceUrl(params.serviceUrl)}/v3/attachments/${encodeURIComponent(params.attachmentId)}`;
195
+ if (!isUrlAllowed(baseUrl, policy.allowHosts)) {
196
+ return undefined;
197
+ }
198
+
199
+ let accessToken: string;
200
+ try {
201
+ accessToken = await params.tokenProvider.getAccessToken(BOT_FRAMEWORK_SCOPE);
202
+ } catch (err) {
203
+ params.logger?.warn?.("msteams botFramework token acquisition failed", {
204
+ error: err instanceof Error ? err.message : String(err),
205
+ });
206
+ return undefined;
207
+ }
208
+ if (!accessToken) {
209
+ return undefined;
210
+ }
211
+
212
+ const info = await fetchBotFrameworkAttachmentInfo({
213
+ serviceUrl: params.serviceUrl,
214
+ attachmentId: params.attachmentId,
215
+ accessToken,
216
+ policy,
217
+ fetchFn: params.fetchFn,
218
+ resolveFn: params.resolveFn,
219
+ logger: params.logger,
220
+ });
221
+ if (!info) {
222
+ return undefined;
223
+ }
224
+
225
+ const views = Array.isArray(info.views) ? info.views : [];
226
+ // Prefer the "original" view when present, otherwise fall back to the first
227
+ // view the Bot Framework service returned.
228
+ const original = views.find((view) => view?.viewId === "original");
229
+ const candidateView = original ?? views.find((view) => typeof view?.viewId === "string");
230
+ const viewId =
231
+ typeof candidateView?.viewId === "string" && candidateView.viewId
232
+ ? candidateView.viewId
233
+ : undefined;
234
+ if (!viewId) {
235
+ return undefined;
236
+ }
237
+ if (
238
+ typeof candidateView?.size === "number" &&
239
+ candidateView.size > 0 &&
240
+ candidateView.size > params.maxBytes
241
+ ) {
242
+ return undefined;
243
+ }
244
+
245
+ const fileNameHint =
246
+ (typeof params.fileNameHint === "string" && params.fileNameHint) ||
247
+ (typeof info.name === "string" && info.name) ||
248
+ undefined;
249
+ const contentTypeHint =
250
+ (typeof params.contentTypeHint === "string" && params.contentTypeHint) ||
251
+ (typeof info.type === "string" && info.type) ||
252
+ undefined;
253
+
254
+ const saved = await saveBotFrameworkAttachmentView({
255
+ serviceUrl: params.serviceUrl,
256
+ attachmentId: params.attachmentId,
257
+ viewId,
258
+ accessToken,
259
+ maxBytes: params.maxBytes,
260
+ fileNameHint,
261
+ contentTypeHint,
262
+ preserveFilenames: params.preserveFilenames,
263
+ policy,
264
+ fetchFn: params.fetchFn,
265
+ resolveFn: params.resolveFn,
266
+ logger: params.logger,
267
+ });
268
+ if (!saved) {
269
+ return undefined;
270
+ }
271
+
272
+ return {
273
+ path: saved.path,
274
+ contentType: saved.contentType,
275
+ placeholder: inferPlaceholder({ contentType: saved.contentType, fileName: fileNameHint }),
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Download media for every attachment referenced by a Bot Framework personal
281
+ * chat activity. Returns all successfully fetched media along with diagnostics
282
+ * compatible with `downloadMSTeamsGraphMedia`'s result shape so callers can
283
+ * reuse the existing logging path.
284
+ */
285
+ export async function downloadMSTeamsBotFrameworkAttachments(params: {
286
+ serviceUrl: string;
287
+ attachmentIds: string[];
288
+ tokenProvider?: MSTeamsAccessTokenProvider;
289
+ maxBytes: number;
290
+ allowHosts?: string[];
291
+ authAllowHosts?: string[];
292
+ fetchFn?: typeof fetch;
293
+ resolveFn?: MSTeamsAttachmentResolveFn;
294
+ fileNameHint?: string | null;
295
+ contentTypeHint?: string | null;
296
+ preserveFilenames?: boolean;
297
+ logger?: MSTeamsAttachmentDownloadLogger;
298
+ }): Promise<MSTeamsGraphMediaResult> {
299
+ const seen = new Set<string>();
300
+ const unique: string[] = [];
301
+ for (const id of params.attachmentIds ?? []) {
302
+ if (typeof id !== "string") {
303
+ continue;
304
+ }
305
+ const trimmed = id.trim();
306
+ if (!trimmed || seen.has(trimmed)) {
307
+ continue;
308
+ }
309
+ seen.add(trimmed);
310
+ unique.push(trimmed);
311
+ }
312
+ if (unique.length === 0 || !params.serviceUrl || !params.tokenProvider) {
313
+ return { media: [], attachmentCount: unique.length };
314
+ }
315
+
316
+ const media: MSTeamsInboundMedia[] = [];
317
+ for (const attachmentId of unique) {
318
+ try {
319
+ const item = await downloadMSTeamsBotFrameworkAttachment({
320
+ serviceUrl: params.serviceUrl,
321
+ attachmentId,
322
+ tokenProvider: params.tokenProvider,
323
+ maxBytes: params.maxBytes,
324
+ allowHosts: params.allowHosts,
325
+ authAllowHosts: params.authAllowHosts,
326
+ fetchFn: params.fetchFn,
327
+ resolveFn: params.resolveFn,
328
+ fileNameHint: params.fileNameHint,
329
+ contentTypeHint: params.contentTypeHint,
330
+ preserveFilenames: params.preserveFilenames,
331
+ logger: params.logger,
332
+ });
333
+ if (item) {
334
+ media.push(item);
335
+ }
336
+ } catch (err) {
337
+ params.logger?.warn?.("msteams botFramework attachment download failed", {
338
+ error: err instanceof Error ? err.message : String(err),
339
+ attachmentId,
340
+ });
341
+ }
342
+ }
343
+
344
+ return {
345
+ media,
346
+ attachmentCount: unique.length,
347
+ };
348
+ }