@kodelyth/msteams 2026.5.42 → 2026.6.1

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 (177) hide show
  1. package/klaw.plugin.json +726 -2
  2. package/package.json +16 -4
  3. package/api.ts +0 -3
  4. package/channel-config-api.ts +0 -1
  5. package/channel-plugin-api.ts +0 -2
  6. package/config-api.ts +0 -4
  7. package/contract-api.ts +0 -4
  8. package/index.ts +0 -20
  9. package/runtime-api.ts +0 -66
  10. package/secret-contract-api.ts +0 -5
  11. package/setup-entry.ts +0 -13
  12. package/setup-plugin-api.ts +0 -3
  13. package/src/ai-entity.ts +0 -7
  14. package/src/approval-auth.ts +0 -44
  15. package/src/attachments/bot-framework.test.ts +0 -506
  16. package/src/attachments/bot-framework.ts +0 -348
  17. package/src/attachments/download.ts +0 -328
  18. package/src/attachments/graph.test.ts +0 -441
  19. package/src/attachments/graph.ts +0 -489
  20. package/src/attachments/html.ts +0 -122
  21. package/src/attachments/payload.ts +0 -14
  22. package/src/attachments/remote-media.test.ts +0 -187
  23. package/src/attachments/remote-media.ts +0 -86
  24. package/src/attachments/shared.test.ts +0 -547
  25. package/src/attachments/shared.ts +0 -655
  26. package/src/attachments/types.ts +0 -47
  27. package/src/attachments.graph.test.ts +0 -414
  28. package/src/attachments.helpers.test.ts +0 -245
  29. package/src/attachments.test-helpers.ts +0 -17
  30. package/src/attachments.test.ts +0 -754
  31. package/src/attachments.ts +0 -18
  32. package/src/block-streaming-config.test.ts +0 -61
  33. package/src/channel-api.ts +0 -1
  34. package/src/channel.actions.test.ts +0 -797
  35. package/src/channel.directory.test.ts +0 -176
  36. package/src/channel.message-adapter.test.ts +0 -227
  37. package/src/channel.runtime.ts +0 -56
  38. package/src/channel.setup.ts +0 -77
  39. package/src/channel.test.ts +0 -136
  40. package/src/channel.ts +0 -1176
  41. package/src/config-schema.ts +0 -6
  42. package/src/config-ui-hints.ts +0 -40
  43. package/src/conversation-store-fs.test.ts +0 -81
  44. package/src/conversation-store-fs.ts +0 -149
  45. package/src/conversation-store-helpers.test.ts +0 -202
  46. package/src/conversation-store-helpers.ts +0 -105
  47. package/src/conversation-store-memory.ts +0 -51
  48. package/src/conversation-store.shared.test.ts +0 -260
  49. package/src/conversation-store.ts +0 -71
  50. package/src/directory-live.test.ts +0 -156
  51. package/src/directory-live.ts +0 -111
  52. package/src/doctor.ts +0 -27
  53. package/src/errors.test.ts +0 -154
  54. package/src/errors.ts +0 -270
  55. package/src/feedback-reflection-prompt.ts +0 -117
  56. package/src/feedback-reflection-store.ts +0 -113
  57. package/src/feedback-reflection.test.ts +0 -237
  58. package/src/feedback-reflection.ts +0 -268
  59. package/src/file-consent-helpers.test.ts +0 -328
  60. package/src/file-consent-helpers.ts +0 -115
  61. package/src/file-consent-invoke.ts +0 -150
  62. package/src/file-consent.test.ts +0 -378
  63. package/src/file-consent.ts +0 -223
  64. package/src/graph-chat.ts +0 -36
  65. package/src/graph-group-management.test.ts +0 -332
  66. package/src/graph-group-management.ts +0 -168
  67. package/src/graph-members.test.ts +0 -89
  68. package/src/graph-members.ts +0 -48
  69. package/src/graph-messages.actions.test.ts +0 -253
  70. package/src/graph-messages.read.test.ts +0 -391
  71. package/src/graph-messages.search.test.ts +0 -227
  72. package/src/graph-messages.test-helpers.ts +0 -50
  73. package/src/graph-messages.ts +0 -534
  74. package/src/graph-teams.test.ts +0 -222
  75. package/src/graph-teams.ts +0 -114
  76. package/src/graph-thread.test.ts +0 -252
  77. package/src/graph-thread.ts +0 -146
  78. package/src/graph-upload.test.ts +0 -253
  79. package/src/graph-upload.ts +0 -531
  80. package/src/graph-users.ts +0 -29
  81. package/src/graph.test.ts +0 -540
  82. package/src/graph.ts +0 -308
  83. package/src/inbound.test.ts +0 -221
  84. package/src/inbound.ts +0 -148
  85. package/src/index.ts +0 -4
  86. package/src/media-helpers.test.ts +0 -220
  87. package/src/media-helpers.ts +0 -105
  88. package/src/mentions.test.ts +0 -254
  89. package/src/mentions.ts +0 -114
  90. package/src/messenger.test.ts +0 -961
  91. package/src/messenger.ts +0 -608
  92. package/src/monitor-handler/access.ts +0 -136
  93. package/src/monitor-handler/inbound-media.test.ts +0 -314
  94. package/src/monitor-handler/inbound-media.ts +0 -180
  95. package/src/monitor-handler/message-handler-mock-support.test-support.ts +0 -28
  96. package/src/monitor-handler/message-handler.authz.test.ts +0 -739
  97. package/src/monitor-handler/message-handler.dm-media.test.ts +0 -54
  98. package/src/monitor-handler/message-handler.test-support.ts +0 -99
  99. package/src/monitor-handler/message-handler.thread-parent.test.ts +0 -225
  100. package/src/monitor-handler/message-handler.thread-session.test.ts +0 -132
  101. package/src/monitor-handler/message-handler.ts +0 -1003
  102. package/src/monitor-handler/reaction-handler.test.ts +0 -325
  103. package/src/monitor-handler/reaction-handler.ts +0 -122
  104. package/src/monitor-handler/thread-session.ts +0 -30
  105. package/src/monitor-handler.adaptive-card.test.ts +0 -158
  106. package/src/monitor-handler.feedback-authz.test.ts +0 -357
  107. package/src/monitor-handler.file-consent.test.ts +0 -443
  108. package/src/monitor-handler.sso.test.ts +0 -576
  109. package/src/monitor-handler.test-helpers.ts +0 -181
  110. package/src/monitor-handler.ts +0 -538
  111. package/src/monitor-handler.types.ts +0 -27
  112. package/src/monitor-types.ts +0 -6
  113. package/src/monitor.lifecycle.test.ts +0 -457
  114. package/src/monitor.test.ts +0 -119
  115. package/src/monitor.ts +0 -476
  116. package/src/oauth.flow.ts +0 -77
  117. package/src/oauth.shared.ts +0 -37
  118. package/src/oauth.test.ts +0 -350
  119. package/src/oauth.token.ts +0 -162
  120. package/src/oauth.ts +0 -130
  121. package/src/outbound.test.ts +0 -400
  122. package/src/outbound.ts +0 -198
  123. package/src/pending-uploads-fs.test.ts +0 -261
  124. package/src/pending-uploads-fs.ts +0 -235
  125. package/src/pending-uploads.test.ts +0 -186
  126. package/src/pending-uploads.ts +0 -121
  127. package/src/policy.test.ts +0 -156
  128. package/src/policy.ts +0 -245
  129. package/src/polls-store-memory.ts +0 -32
  130. package/src/polls.test.ts +0 -169
  131. package/src/polls.ts +0 -312
  132. package/src/presentation.ts +0 -93
  133. package/src/probe.test.ts +0 -79
  134. package/src/probe.ts +0 -132
  135. package/src/reply-dispatcher.test.ts +0 -543
  136. package/src/reply-dispatcher.ts +0 -523
  137. package/src/reply-stream-controller.test.ts +0 -424
  138. package/src/reply-stream-controller.ts +0 -334
  139. package/src/resolve-allowlist.test.ts +0 -253
  140. package/src/resolve-allowlist.ts +0 -309
  141. package/src/revoked-context.ts +0 -17
  142. package/src/runtime.ts +0 -12
  143. package/src/sdk-types.ts +0 -59
  144. package/src/sdk.test.ts +0 -727
  145. package/src/sdk.ts +0 -916
  146. package/src/secret-contract.ts +0 -49
  147. package/src/secret-input.ts +0 -7
  148. package/src/send-context.test.ts +0 -93
  149. package/src/send-context.ts +0 -269
  150. package/src/send.test.ts +0 -588
  151. package/src/send.ts +0 -697
  152. package/src/sent-message-cache.test.ts +0 -106
  153. package/src/sent-message-cache.ts +0 -174
  154. package/src/session-route.ts +0 -40
  155. package/src/setup-core.ts +0 -162
  156. package/src/setup-surface.test.ts +0 -175
  157. package/src/setup-surface.ts +0 -319
  158. package/src/sso-token-store.test.ts +0 -74
  159. package/src/sso-token-store.ts +0 -166
  160. package/src/sso.ts +0 -300
  161. package/src/storage.ts +0 -25
  162. package/src/store-fs.ts +0 -42
  163. package/src/streaming-message.test.ts +0 -323
  164. package/src/streaming-message.ts +0 -327
  165. package/src/test-runtime.ts +0 -16
  166. package/src/thread-parent-context.test.ts +0 -224
  167. package/src/thread-parent-context.ts +0 -159
  168. package/src/token-response.ts +0 -11
  169. package/src/token.test.ts +0 -268
  170. package/src/token.ts +0 -194
  171. package/src/user-agent.test.ts +0 -121
  172. package/src/user-agent.ts +0 -53
  173. package/src/webhook-timeouts.ts +0 -27
  174. package/src/welcome-card.test.ts +0 -104
  175. package/src/welcome-card.ts +0 -57
  176. package/test-api.ts +0 -1
  177. package/tsconfig.json +0 -16
@@ -1,159 +0,0 @@
1
- // Parent-message context injection for Teams channel thread replies.
2
- //
3
- // When an inbound message arrives as a reply inside a Teams channel thread,
4
- // the triggering message often makes no sense on its own (for example, a
5
- // one-word "yes" or "go ahead"). Per-thread session isolation (PR #62713)
6
- // gives each thread its own session, but the first message in a brand-new
7
- // thread session still has no parent context.
8
- //
9
- // This module fetches the parent message via Graph and prepends a compact
10
- // `Replying to @sender: …` system event to the next agent turn so the agent
11
- // knows what is being responded to. Fetches are cached to avoid repeated
12
- // Graph calls within the same active thread, and per-session dedupe ensures
13
- // the same parent is not re-injected on every subsequent reply in the
14
- // thread.
15
-
16
- import { fetchChannelMessage, stripHtmlFromTeamsMessage } from "./graph-thread.js";
17
- import type { GraphThreadMessage } from "./graph-thread.js";
18
-
19
- // LRU cache for parent message fetches. Keyed by `teamId:channelId:parentId`.
20
- // 5-minute TTL and 100-entry cap keep active-thread chatter fast without
21
- // holding stale data when a thread goes quiet. Eviction uses Map insertion
22
- // order for LRU semantics (get() re-inserts on hit).
23
- const PARENT_CACHE_TTL_MS = 5 * 60 * 1000;
24
- const PARENT_CACHE_MAX = 100;
25
-
26
- type ParentCacheEntry = {
27
- message: GraphThreadMessage | undefined;
28
- expiresAt: number;
29
- };
30
-
31
- const parentCache = new Map<string, ParentCacheEntry>();
32
-
33
- // Per-session dedupe: remembers the most recent parent id we injected for a
34
- // given session key. When the same thread session sees another reply against
35
- // the same parent, we skip re-enqueueing the identical system event. We keep
36
- // a small LRU so idle sessions eventually drop out.
37
- const INJECTED_MAX = 200;
38
- const injectedParents = new Map<string, string>();
39
-
40
- type ThreadParentContextFetcher = (
41
- token: string,
42
- groupId: string,
43
- channelId: string,
44
- messageId: string,
45
- ) => Promise<GraphThreadMessage | undefined>;
46
-
47
- function touchLru<K, V>(map: Map<K, V>, key: K, value: V, max: number): void {
48
- if (map.has(key)) {
49
- map.delete(key);
50
- } else if (map.size >= max) {
51
- // Drop the oldest (first-inserted) entry.
52
- const firstKey = map.keys().next().value;
53
- if (firstKey !== undefined) {
54
- map.delete(firstKey);
55
- }
56
- }
57
- map.set(key, value);
58
- }
59
-
60
- function buildParentCacheKey(groupId: string, channelId: string, parentId: string): string {
61
- return `${groupId}\u0000${channelId}\u0000${parentId}`;
62
- }
63
-
64
- /**
65
- * Fetch a channel parent message with an LRU+TTL cache.
66
- *
67
- * Uses the injected `fetchParent` (defaults to `fetchChannelMessage`) so
68
- * tests can swap in a stub without mocking the Graph transport.
69
- */
70
- export async function fetchParentMessageCached(
71
- token: string,
72
- groupId: string,
73
- channelId: string,
74
- parentId: string,
75
- fetchParent: ThreadParentContextFetcher = fetchChannelMessage,
76
- ): Promise<GraphThreadMessage | undefined> {
77
- const key = buildParentCacheKey(groupId, channelId, parentId);
78
- const now = Date.now();
79
- const cached = parentCache.get(key);
80
- if (cached && cached.expiresAt > now) {
81
- // Refresh LRU ordering on hit.
82
- parentCache.delete(key);
83
- parentCache.set(key, cached);
84
- return cached.message;
85
- }
86
- const message = await fetchParent(token, groupId, channelId, parentId);
87
- touchLru(parentCache, key, { message, expiresAt: now + PARENT_CACHE_TTL_MS }, PARENT_CACHE_MAX);
88
- return message;
89
- }
90
-
91
- type ParentContextSummary = {
92
- /** Display name of the parent message author, or "unknown". */
93
- sender: string;
94
- /** Stripped, single-line parent body text (or empty if unresolved). */
95
- text: string;
96
- };
97
-
98
- const PARENT_TEXT_MAX_CHARS = 400;
99
-
100
- /**
101
- * Extract a compact summary (sender + plain-text body) from a Graph parent
102
- * message. Returns undefined when the parent cannot be summarized (missing
103
- * or blank body).
104
- */
105
- export function summarizeParentMessage(
106
- message: GraphThreadMessage | undefined,
107
- ): ParentContextSummary | undefined {
108
- if (!message) {
109
- return undefined;
110
- }
111
- const sender =
112
- message.from?.user?.displayName ?? message.from?.application?.displayName ?? "unknown";
113
- const contentType = message.body?.contentType ?? "text";
114
- const raw = message.body?.content ?? "";
115
- const text =
116
- contentType === "html" ? stripHtmlFromTeamsMessage(raw) : raw.replace(/\s+/g, " ").trim();
117
- if (!text) {
118
- return undefined;
119
- }
120
- return {
121
- sender,
122
- text:
123
- text.length > PARENT_TEXT_MAX_CHARS ? `${text.slice(0, PARENT_TEXT_MAX_CHARS - 1)}…` : text,
124
- };
125
- }
126
-
127
- /**
128
- * Build the single-line `Replying to @sender: body` system event text.
129
- * Callers should pass this text to `enqueueSystemEvent` together with a
130
- * stable contextKey derived from the parent id.
131
- */
132
- export function formatParentContextEvent(summary: ParentContextSummary): string {
133
- return `Replying to @${summary.sender}: ${summary.text}`;
134
- }
135
-
136
- /**
137
- * Decide whether a parent context event should be enqueued for the current
138
- * session. Returns `false` when we already injected the same parent for this
139
- * session recently (prevents re-prepending identical context on every reply
140
- * in the thread).
141
- */
142
- export function shouldInjectParentContext(sessionKey: string, parentId: string): boolean {
143
- const key = sessionKey;
144
- return injectedParents.get(key) !== parentId;
145
- }
146
-
147
- /**
148
- * Record that `parentId` was just injected for `sessionKey` so subsequent
149
- * replies with the same parent can short-circuit via `shouldInjectParentContext`.
150
- */
151
- export function markParentContextInjected(sessionKey: string, parentId: string): void {
152
- touchLru(injectedParents, sessionKey, parentId, INJECTED_MAX);
153
- }
154
-
155
- // Exported for test isolation.
156
- export function resetThreadParentContextCachesForTest(): void {
157
- parentCache.clear();
158
- injectedParents.clear();
159
- }
@@ -1,11 +0,0 @@
1
- export function readAccessToken(value: unknown): string | null {
2
- if (typeof value === "string") {
3
- return value;
4
- }
5
- if (value && typeof value === "object") {
6
- const token =
7
- (value as { accessToken?: unknown }).accessToken ?? (value as { token?: unknown }).token;
8
- return typeof token === "string" ? token : null;
9
- }
10
- return null;
11
- }
package/src/token.test.ts DELETED
@@ -1,268 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { readAccessToken } from "./token-response.js";
3
- import { hasConfiguredMSTeamsCredentials, resolveMSTeamsCredentials } from "./token.js";
4
-
5
- vi.mock("./secret-input.js", () => ({
6
- normalizeSecretInputString: (v: unknown) =>
7
- typeof v === "string" && v.trim() ? v.trim() : undefined,
8
- normalizeResolvedSecretInputString: (opts: { value: unknown; path: string }) =>
9
- typeof opts.value === "string" && opts.value.trim() ? opts.value.trim() : undefined,
10
- hasConfiguredSecretInput: (v: unknown) => typeof v === "string" && v.trim().length > 0,
11
- }));
12
-
13
- const ENV_KEYS = [
14
- "MSTEAMS_APP_ID",
15
- "MSTEAMS_APP_PASSWORD",
16
- "MSTEAMS_TENANT_ID",
17
- "MSTEAMS_AUTH_TYPE",
18
- "MSTEAMS_CERTIFICATE_PATH",
19
- "MSTEAMS_CERTIFICATE_THUMBPRINT",
20
- "MSTEAMS_USE_MANAGED_IDENTITY",
21
- "MSTEAMS_MANAGED_IDENTITY_CLIENT_ID",
22
- ] as const;
23
-
24
- let savedEnv: Record<string, string | undefined> = {};
25
-
26
- function saveAndClearEnv() {
27
- savedEnv = {};
28
- for (const key of ENV_KEYS) {
29
- savedEnv[key] = process.env[key];
30
- delete process.env[key];
31
- }
32
- }
33
-
34
- function restoreEnv() {
35
- for (const key of ENV_KEYS) {
36
- if (savedEnv[key] !== undefined) {
37
- process.env[key] = savedEnv[key];
38
- } else {
39
- delete process.env[key];
40
- }
41
- }
42
- }
43
-
44
- describe("token – secret credentials", () => {
45
- beforeEach(saveAndClearEnv);
46
- afterEach(restoreEnv);
47
-
48
- it("returns true when appId + appPassword + tenantId are provided in config", () => {
49
- const cfg = { appId: "app-id", appPassword: "app-pw", tenantId: "tenant-id" } as any;
50
- expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(true);
51
- });
52
-
53
- it("returns false when appPassword is missing", () => {
54
- const cfg = { appId: "app-id", tenantId: "tenant-id" } as any;
55
- expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(false);
56
- });
57
-
58
- it("returns false when no config is given and no env vars set", () => {
59
- expect(hasConfiguredMSTeamsCredentials(undefined)).toBe(false);
60
- });
61
-
62
- it("resolves secret credentials from config", () => {
63
- const cfg = { appId: "app-id", appPassword: "app-pw", tenantId: "tenant-id" } as any;
64
- const result = resolveMSTeamsCredentials(cfg);
65
- expect(result).toEqual({
66
- type: "secret",
67
- appId: "app-id",
68
- appPassword: "app-pw",
69
- tenantId: "tenant-id",
70
- });
71
- });
72
-
73
- it("resolves secret credentials from env vars", () => {
74
- process.env.MSTEAMS_APP_ID = "env-app-id";
75
- process.env.MSTEAMS_APP_PASSWORD = "env-app-pw";
76
- process.env.MSTEAMS_TENANT_ID = "env-tenant-id";
77
- const result = resolveMSTeamsCredentials(undefined);
78
- expect(result).toEqual({
79
- type: "secret",
80
- appId: "env-app-id",
81
- appPassword: "env-app-pw",
82
- tenantId: "env-tenant-id",
83
- });
84
- });
85
-
86
- it("returns undefined when appPassword is missing", () => {
87
- const cfg = { appId: "app-id", tenantId: "tenant-id" } as any;
88
- expect(resolveMSTeamsCredentials(cfg)).toBeUndefined();
89
- });
90
- });
91
-
92
- describe("token – federated credentials (certificate)", () => {
93
- beforeEach(saveAndClearEnv);
94
- afterEach(restoreEnv);
95
-
96
- it("hasConfigured returns true when certificate path is provided", () => {
97
- const cfg = {
98
- appId: "app-id",
99
- tenantId: "tenant-id",
100
- authType: "federated",
101
- certificatePath: "/cert.pem",
102
- } as any;
103
- expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(true);
104
- });
105
-
106
- it("hasConfigured returns false when neither cert nor MI is provided", () => {
107
- const cfg = { appId: "app-id", tenantId: "tenant-id", authType: "federated" } as any;
108
- expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(false);
109
- });
110
-
111
- it("resolves federated credentials with certificate from config", () => {
112
- const cfg = {
113
- appId: "app-id",
114
- tenantId: "tenant-id",
115
- authType: "federated",
116
- certificatePath: "/cert.pem",
117
- certificateThumbprint: "AABBCCDD",
118
- } as any;
119
- const result = resolveMSTeamsCredentials(cfg);
120
- expect(result).toEqual({
121
- type: "federated",
122
- appId: "app-id",
123
- tenantId: "tenant-id",
124
- certificatePath: "/cert.pem",
125
- certificateThumbprint: "AABBCCDD",
126
- useManagedIdentity: undefined,
127
- managedIdentityClientId: undefined,
128
- });
129
- });
130
-
131
- it("resolves federated credentials from env vars", () => {
132
- process.env.MSTEAMS_AUTH_TYPE = "federated";
133
- process.env.MSTEAMS_APP_ID = "env-app-id";
134
- process.env.MSTEAMS_TENANT_ID = "env-tenant-id";
135
- process.env.MSTEAMS_CERTIFICATE_PATH = "/env/cert.pem";
136
- process.env.MSTEAMS_CERTIFICATE_THUMBPRINT = "EEFF0011";
137
- const result = resolveMSTeamsCredentials(undefined);
138
- expect(result).toEqual({
139
- type: "federated",
140
- appId: "env-app-id",
141
- tenantId: "env-tenant-id",
142
- certificatePath: "/env/cert.pem",
143
- certificateThumbprint: "EEFF0011",
144
- useManagedIdentity: undefined,
145
- managedIdentityClientId: undefined,
146
- });
147
- });
148
- });
149
-
150
- describe("token – federated credentials (managed identity)", () => {
151
- beforeEach(saveAndClearEnv);
152
- afterEach(restoreEnv);
153
-
154
- it("resolves managed identity from config", () => {
155
- const cfg = {
156
- appId: "app-id",
157
- tenantId: "tenant-id",
158
- authType: "federated",
159
- useManagedIdentity: true,
160
- managedIdentityClientId: "mi-client-id",
161
- } as any;
162
- const result = resolveMSTeamsCredentials(cfg);
163
- expect(result).toEqual({
164
- type: "federated",
165
- appId: "app-id",
166
- tenantId: "tenant-id",
167
- certificatePath: undefined,
168
- certificateThumbprint: undefined,
169
- useManagedIdentity: true,
170
- managedIdentityClientId: "mi-client-id",
171
- });
172
- });
173
-
174
- it("resolves system-assigned managed identity (no clientId)", () => {
175
- const cfg = {
176
- appId: "app-id",
177
- tenantId: "tenant-id",
178
- authType: "federated",
179
- useManagedIdentity: true,
180
- } as any;
181
- const result = resolveMSTeamsCredentials(cfg);
182
- expect(result).toEqual({
183
- type: "federated",
184
- appId: "app-id",
185
- tenantId: "tenant-id",
186
- certificatePath: undefined,
187
- certificateThumbprint: undefined,
188
- useManagedIdentity: true,
189
- managedIdentityClientId: undefined,
190
- });
191
- });
192
-
193
- it("hasConfigured returns true for managed identity via env", () => {
194
- process.env.MSTEAMS_AUTH_TYPE = "federated";
195
- process.env.MSTEAMS_APP_ID = "env-app-id";
196
- process.env.MSTEAMS_TENANT_ID = "env-tenant-id";
197
- process.env.MSTEAMS_USE_MANAGED_IDENTITY = "true";
198
- expect(hasConfiguredMSTeamsCredentials(undefined)).toBe(true);
199
- });
200
-
201
- it("config useManagedIdentity=false overrides env MSTEAMS_USE_MANAGED_IDENTITY=true", () => {
202
- process.env.MSTEAMS_USE_MANAGED_IDENTITY = "true";
203
- const cfg = {
204
- appId: "app-id",
205
- tenantId: "tenant-id",
206
- authType: "federated",
207
- certificatePath: "/cert.pem",
208
- useManagedIdentity: false,
209
- } as any;
210
- const result = resolveMSTeamsCredentials(cfg);
211
- expect(result).toEqual({
212
- type: "federated",
213
- appId: "app-id",
214
- tenantId: "tenant-id",
215
- certificatePath: "/cert.pem",
216
- certificateThumbprint: undefined,
217
- useManagedIdentity: undefined,
218
- managedIdentityClientId: undefined,
219
- });
220
- });
221
- });
222
-
223
- describe("token – backward compatibility", () => {
224
- beforeEach(saveAndClearEnv);
225
- afterEach(restoreEnv);
226
-
227
- it("defaults to secret when authType is absent", () => {
228
- const cfg = { appId: "app-id", appPassword: "pw", tenantId: "tenant-id" } as any;
229
- const result = resolveMSTeamsCredentials(cfg);
230
- expect(result).toEqual({
231
- type: "secret",
232
- appId: "app-id",
233
- appPassword: "pw",
234
- tenantId: "tenant-id",
235
- });
236
- });
237
-
238
- it("explicit authType=secret behaves same as absent", () => {
239
- const cfg = {
240
- appId: "app-id",
241
- appPassword: "pw",
242
- tenantId: "tenant-id",
243
- authType: "secret",
244
- } as any;
245
- const result = resolveMSTeamsCredentials(cfg);
246
- expect(result).toEqual({
247
- type: "secret",
248
- appId: "app-id",
249
- appPassword: "pw",
250
- tenantId: "tenant-id",
251
- });
252
- });
253
- });
254
-
255
- describe("readAccessToken", () => {
256
- it("reads string and object token forms", () => {
257
- expect(readAccessToken("abc")).toBe("abc");
258
- expect(readAccessToken({ accessToken: "access-token" })).toBe("access-token");
259
- expect(readAccessToken({ token: "fallback-token" })).toBe("fallback-token");
260
- });
261
-
262
- it("returns null for unsupported token payloads", () => {
263
- expect(readAccessToken({ accessToken: 123 })).toBeNull();
264
- expect(readAccessToken({ token: false })).toBeNull();
265
- expect(readAccessToken(null)).toBeNull();
266
- expect(readAccessToken(undefined)).toBeNull();
267
- });
268
- });
package/src/token.ts DELETED
@@ -1,194 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { basename, dirname } from "node:path";
3
- import { privateFileStoreSync } from "klaw/plugin-sdk/security-runtime";
4
- import type { MSTeamsConfig } from "../runtime-api.js";
5
- import type { MSTeamsDelegatedTokens } from "./oauth.shared.js";
6
- import { refreshMSTeamsDelegatedTokens } from "./oauth.token.js";
7
- import {
8
- hasConfiguredSecretInput,
9
- normalizeResolvedSecretInputString,
10
- normalizeSecretInputString,
11
- } from "./secret-input.js";
12
- import { resolveMSTeamsStorePath } from "./storage.js";
13
-
14
- // ── Credential types ───────────────────────────────────────────────────────
15
-
16
- export type MSTeamsSecretCredentials = {
17
- type: "secret";
18
- appId: string;
19
- appPassword: string;
20
- tenantId: string;
21
- };
22
-
23
- export type MSTeamsFederatedCredentials = {
24
- type: "federated";
25
- appId: string;
26
- tenantId: string;
27
- certificatePath?: string;
28
- certificateThumbprint?: string;
29
- useManagedIdentity?: boolean;
30
- managedIdentityClientId?: string;
31
- };
32
-
33
- export type MSTeamsCredentials = MSTeamsSecretCredentials | MSTeamsFederatedCredentials;
34
-
35
- // ── Helpers ────────────────────────────────────────────────────────────────
36
-
37
- function resolveAuthType(cfg?: MSTeamsConfig): "secret" | "federated" {
38
- const fromCfg = cfg?.authType;
39
- if (fromCfg === "secret" || fromCfg === "federated") {
40
- return fromCfg;
41
- }
42
-
43
- const fromEnv = process.env.MSTEAMS_AUTH_TYPE;
44
- if (fromEnv === "federated") {
45
- return "federated";
46
- }
47
-
48
- return "secret";
49
- }
50
-
51
- // ── hasConfiguredMSTeamsCredentials ────────────────────────────────────────
52
-
53
- export function hasConfiguredMSTeamsCredentials(cfg?: MSTeamsConfig): boolean {
54
- const authType = resolveAuthType(cfg);
55
-
56
- const hasAppId = Boolean(
57
- normalizeSecretInputString(cfg?.appId) ||
58
- normalizeSecretInputString(process.env.MSTEAMS_APP_ID),
59
- );
60
- const hasTenantId = Boolean(
61
- normalizeSecretInputString(cfg?.tenantId) ||
62
- normalizeSecretInputString(process.env.MSTEAMS_TENANT_ID),
63
- );
64
-
65
- if (authType === "federated") {
66
- const hasCert = Boolean(cfg?.certificatePath || process.env.MSTEAMS_CERTIFICATE_PATH);
67
- const hasManagedIdentity =
68
- cfg?.useManagedIdentity ?? process.env.MSTEAMS_USE_MANAGED_IDENTITY === "true";
69
-
70
- return hasAppId && hasTenantId && (hasCert || hasManagedIdentity);
71
- }
72
-
73
- // "secret" (default) — original logic
74
- return Boolean(
75
- normalizeSecretInputString(cfg?.appId) &&
76
- hasConfiguredSecretInput(cfg?.appPassword) &&
77
- normalizeSecretInputString(cfg?.tenantId),
78
- );
79
- }
80
-
81
- // ── resolveMSTeamsCredentials ─────────────────────────────────────────────
82
-
83
- export function resolveMSTeamsCredentials(cfg?: MSTeamsConfig): MSTeamsCredentials | undefined {
84
- const authType = resolveAuthType(cfg);
85
-
86
- const appId =
87
- normalizeSecretInputString(cfg?.appId) ||
88
- normalizeSecretInputString(process.env.MSTEAMS_APP_ID);
89
-
90
- const tenantId =
91
- normalizeSecretInputString(cfg?.tenantId) ||
92
- normalizeSecretInputString(process.env.MSTEAMS_TENANT_ID);
93
-
94
- if (!appId || !tenantId) {
95
- return undefined;
96
- }
97
-
98
- if (authType === "federated") {
99
- const certificatePath =
100
- cfg?.certificatePath || process.env.MSTEAMS_CERTIFICATE_PATH || undefined;
101
-
102
- const certificateThumbprint =
103
- cfg?.certificateThumbprint || process.env.MSTEAMS_CERTIFICATE_THUMBPRINT || undefined;
104
-
105
- const useManagedIdentity =
106
- cfg?.useManagedIdentity ?? process.env.MSTEAMS_USE_MANAGED_IDENTITY === "true";
107
-
108
- const managedIdentityClientId =
109
- cfg?.managedIdentityClientId || process.env.MSTEAMS_MANAGED_IDENTITY_CLIENT_ID || undefined;
110
-
111
- // At least one federated mechanism must be configured.
112
- if (!certificatePath && !useManagedIdentity) {
113
- return undefined;
114
- }
115
-
116
- return {
117
- type: "federated",
118
- appId,
119
- tenantId,
120
- certificatePath,
121
- certificateThumbprint,
122
- useManagedIdentity: useManagedIdentity || undefined,
123
- managedIdentityClientId,
124
- };
125
- }
126
-
127
- // "secret" (default) — original logic
128
- const appPassword =
129
- normalizeResolvedSecretInputString({
130
- value: cfg?.appPassword,
131
- path: "channels.msteams.appPassword",
132
- }) || normalizeSecretInputString(process.env.MSTEAMS_APP_PASSWORD);
133
-
134
- if (!appPassword) {
135
- return undefined;
136
- }
137
-
138
- return { type: "secret", appId, appPassword, tenantId };
139
- }
140
-
141
- // ---------------------------------------------------------------------------
142
- // Delegated token storage / resolution
143
- // ---------------------------------------------------------------------------
144
-
145
- const DELEGATED_TOKEN_FILENAME = "msteams-delegated.json";
146
-
147
- function resolveDelegatedTokenPath(): string {
148
- return resolveMSTeamsStorePath({ filename: DELEGATED_TOKEN_FILENAME });
149
- }
150
-
151
- export function loadDelegatedTokens(): MSTeamsDelegatedTokens | undefined {
152
- try {
153
- const content = readFileSync(resolveDelegatedTokenPath(), "utf8");
154
- return JSON.parse(content) as MSTeamsDelegatedTokens;
155
- } catch {
156
- return undefined;
157
- }
158
- }
159
-
160
- export function saveDelegatedTokens(tokens: MSTeamsDelegatedTokens): void {
161
- const tokenPath = resolveDelegatedTokenPath();
162
- privateFileStoreSync(dirname(tokenPath)).writeJson(basename(tokenPath), tokens);
163
- }
164
-
165
- export async function resolveDelegatedAccessToken(params: {
166
- tenantId: string;
167
- clientId: string;
168
- clientSecret: string;
169
- }): Promise<string | undefined> {
170
- const tokens = loadDelegatedTokens();
171
- if (!tokens) {
172
- return undefined;
173
- }
174
-
175
- // Token still valid (5-min buffer already baked into expiresAt)
176
- if (tokens.expiresAt > Date.now()) {
177
- return tokens.accessToken;
178
- }
179
-
180
- // Attempt refresh
181
- try {
182
- const refreshed = await refreshMSTeamsDelegatedTokens({
183
- tenantId: params.tenantId,
184
- clientId: params.clientId,
185
- clientSecret: params.clientSecret,
186
- refreshToken: tokens.refreshToken,
187
- scopes: tokens.scopes,
188
- });
189
- saveDelegatedTokens(refreshed);
190
- return refreshed.accessToken;
191
- } catch {
192
- return undefined;
193
- }
194
- }