@kodelyth/msteams 2026.5.42 → 2026.6.2

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 +18 -6
  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,319 +0,0 @@
1
- import {
2
- createTopLevelChannelAllowFromSetter,
3
- createTopLevelChannelDmPolicy,
4
- createTopLevelChannelGroupPolicySetter,
5
- mergeAllowFromEntries,
6
- splitSetupEntries,
7
- createSetupTranslator,
8
- type ChannelSetupDmPolicy,
9
- type ChannelSetupWizard,
10
- type KlawConfig,
11
- type WizardPrompter,
12
- } from "klaw/plugin-sdk/setup";
13
- import type { MSTeamsTeamConfig } from "../runtime-api.js";
14
- import { formatUnknownError } from "./errors.js";
15
- import {
16
- parseMSTeamsTeamEntry,
17
- resolveMSTeamsChannelAllowlist,
18
- resolveMSTeamsUserAllowlist,
19
- } from "./resolve-allowlist.js";
20
- import { createMSTeamsSetupWizardBase } from "./setup-core.js";
21
- import { resolveMSTeamsCredentials, saveDelegatedTokens } from "./token.js";
22
-
23
- const t = createSetupTranslator();
24
-
25
- const channel = "msteams" as const;
26
- const setMSTeamsAllowFrom = createTopLevelChannelAllowFromSetter({
27
- channel,
28
- });
29
- const setMSTeamsGroupPolicy = createTopLevelChannelGroupPolicySetter({
30
- channel,
31
- enabled: true,
32
- });
33
-
34
- export function openDelegatedOAuthUrl(url: string): Promise<void> {
35
- return Promise.reject(
36
- new Error(`Automatic browser launch is not available. Open this URL manually: ${url}`),
37
- );
38
- }
39
-
40
- function looksLikeGuid(value: string): boolean {
41
- return /^[0-9a-fA-F-]{16,}$/.test(value);
42
- }
43
-
44
- async function promptMSTeamsAllowFrom(params: {
45
- cfg: KlawConfig;
46
- prompter: WizardPrompter;
47
- }): Promise<KlawConfig> {
48
- const existing = params.cfg.channels?.msteams?.allowFrom ?? [];
49
- await params.prompter.note(
50
- [
51
- t("wizard.msteams.allowlistIntro"),
52
- t("wizard.msteams.allowlistResolve"),
53
- t("wizard.msteams.examples"),
54
- "- alex@example.com",
55
- "- Alex Johnson",
56
- "- 00000000-0000-0000-0000-000000000000",
57
- ].join("\n"),
58
- t("wizard.msteams.allowlistTitle"),
59
- );
60
-
61
- while (true) {
62
- const entry = await params.prompter.text({
63
- message: t("wizard.msteams.allowFromPrompt"),
64
- placeholder: "alex@example.com, Alex Johnson",
65
- initialValue: existing[0] ? existing[0] : undefined,
66
- validate: (value) => (value.trim() ? undefined : t("common.required")),
67
- });
68
- const parts = splitSetupEntries(entry);
69
- if (parts.length === 0) {
70
- await params.prompter.note(
71
- t("wizard.msteams.enterAtLeastOneUser"),
72
- t("wizard.msteams.allowlistTitle"),
73
- );
74
- continue;
75
- }
76
-
77
- const resolved = await resolveMSTeamsUserAllowlist({
78
- cfg: params.cfg,
79
- entries: parts,
80
- }).catch(() => null);
81
-
82
- if (!resolved) {
83
- const ids = parts.filter((part) => looksLikeGuid(part));
84
- if (ids.length !== parts.length) {
85
- await params.prompter.note(
86
- t("wizard.msteams.graphLookupUnavailable"),
87
- t("wizard.msteams.allowlistTitle"),
88
- );
89
- continue;
90
- }
91
- const unique = mergeAllowFromEntries(existing, ids);
92
- return setMSTeamsAllowFrom(params.cfg, unique);
93
- }
94
-
95
- const unresolved = resolved.filter((item) => !item.resolved || !item.id);
96
- if (unresolved.length > 0) {
97
- await params.prompter.note(
98
- t("wizard.msteams.couldNotResolve", {
99
- entries: unresolved.map((item) => item.input).join(", "),
100
- }),
101
- t("wizard.msteams.allowlistTitle"),
102
- );
103
- continue;
104
- }
105
-
106
- const ids = resolved.map((item) => item.id as string);
107
- const unique = mergeAllowFromEntries(existing, ids);
108
- return setMSTeamsAllowFrom(params.cfg, unique);
109
- }
110
- }
111
-
112
- function setMSTeamsTeamsAllowlist(
113
- cfg: KlawConfig,
114
- entries: Array<{ teamKey: string; channelKey?: string }>,
115
- ): KlawConfig {
116
- const baseTeams = cfg.channels?.msteams?.teams ?? {};
117
- const teams: Record<string, { channels?: Record<string, unknown> }> = { ...baseTeams };
118
- for (const entry of entries) {
119
- const teamKey = entry.teamKey;
120
- if (!teamKey) {
121
- continue;
122
- }
123
- const existing = teams[teamKey] ?? {};
124
- if (entry.channelKey) {
125
- const channels = { ...existing.channels };
126
- channels[entry.channelKey] = channels[entry.channelKey] ?? {};
127
- teams[teamKey] = { ...existing, channels };
128
- } else {
129
- teams[teamKey] = existing;
130
- }
131
- }
132
- return {
133
- ...cfg,
134
- channels: {
135
- ...cfg.channels,
136
- msteams: {
137
- ...cfg.channels?.msteams,
138
- enabled: true,
139
- teams: teams as Record<string, MSTeamsTeamConfig>,
140
- },
141
- },
142
- };
143
- }
144
-
145
- function listMSTeamsGroupEntries(cfg: KlawConfig): string[] {
146
- return Object.entries(cfg.channels?.msteams?.teams ?? {}).flatMap(([teamKey, value]) => {
147
- const channels = value?.channels ?? {};
148
- const channelKeys = Object.keys(channels);
149
- if (channelKeys.length === 0) {
150
- return [teamKey];
151
- }
152
- return channelKeys.map((channelKey) => `${teamKey}/${channelKey}`);
153
- });
154
- }
155
-
156
- async function resolveMSTeamsGroupAllowlist(params: {
157
- cfg: KlawConfig;
158
- entries: string[];
159
- prompter: Pick<WizardPrompter, "note">;
160
- }): Promise<Array<{ teamKey: string; channelKey?: string }>> {
161
- let resolvedEntries = params.entries
162
- .map((entry) => parseMSTeamsTeamEntry(entry))
163
- .filter(Boolean) as Array<{ teamKey: string; channelKey?: string }>;
164
- if (params.entries.length === 0 || !resolveMSTeamsCredentials(params.cfg.channels?.msteams)) {
165
- return resolvedEntries;
166
- }
167
- try {
168
- const lookups = await resolveMSTeamsChannelAllowlist({
169
- cfg: params.cfg,
170
- entries: params.entries,
171
- });
172
- const resolvedChannels = lookups.filter(
173
- (entry) => entry.resolved && entry.teamId && entry.channelId,
174
- );
175
- const resolvedTeams = lookups.filter(
176
- (entry) => entry.resolved && entry.teamId && !entry.channelId,
177
- );
178
- const unresolved = lookups.filter((entry) => !entry.resolved).map((entry) => entry.input);
179
- resolvedEntries = [
180
- ...resolvedChannels.map((entry) => ({
181
- teamKey: entry.teamId as string,
182
- channelKey: entry.channelId as string,
183
- })),
184
- ...resolvedTeams.map((entry) => ({
185
- teamKey: entry.teamId as string,
186
- })),
187
- ...unresolved.map((entry) => parseMSTeamsTeamEntry(entry)).filter(Boolean),
188
- ] as Array<{ teamKey: string; channelKey?: string }>;
189
- const summary: string[] = [];
190
- if (resolvedChannels.length > 0) {
191
- summary.push(
192
- t("wizard.msteams.resolvedChannels", {
193
- entries: resolvedChannels
194
- .map((entry) => entry.channelId)
195
- .filter(Boolean)
196
- .join(", "),
197
- }),
198
- );
199
- }
200
- if (resolvedTeams.length > 0) {
201
- summary.push(
202
- t("wizard.msteams.resolvedTeams", {
203
- entries: resolvedTeams
204
- .map((entry) => entry.teamId)
205
- .filter(Boolean)
206
- .join(", "),
207
- }),
208
- );
209
- }
210
- if (unresolved.length > 0) {
211
- summary.push(t("wizard.msteams.unresolvedKept", { entries: unresolved.join(", ") }));
212
- }
213
- if (summary.length > 0) {
214
- await params.prompter.note(summary.join("\n"), t("wizard.msteams.channelsLabel"));
215
- }
216
- return resolvedEntries;
217
- } catch (err) {
218
- await params.prompter.note(
219
- t("wizard.msteams.channelLookupFailed", { error: formatUnknownError(err) }),
220
- t("wizard.msteams.channelsLabel"),
221
- );
222
- return resolvedEntries;
223
- }
224
- }
225
-
226
- const msteamsGroupAccess: NonNullable<ChannelSetupWizard["groupAccess"]> = {
227
- label: t("wizard.msteams.channelsLabel"),
228
- placeholder: "Team Name/Channel Name, teamId/conversationId",
229
- currentPolicy: ({ cfg }) => cfg.channels?.msteams?.groupPolicy ?? "allowlist",
230
- currentEntries: ({ cfg }) => listMSTeamsGroupEntries(cfg),
231
- updatePrompt: ({ cfg }) => Boolean(cfg.channels?.msteams?.teams),
232
- setPolicy: ({ cfg, policy }) => setMSTeamsGroupPolicy(cfg, policy),
233
- resolveAllowlist: async ({ cfg, entries, prompter }) =>
234
- await resolveMSTeamsGroupAllowlist({ cfg, entries, prompter }),
235
- applyAllowlist: ({ cfg, resolved }) =>
236
- setMSTeamsTeamsAllowlist(cfg, resolved as Array<{ teamKey: string; channelKey?: string }>),
237
- };
238
-
239
- const msteamsDmPolicy: ChannelSetupDmPolicy = createTopLevelChannelDmPolicy({
240
- label: "MS Teams",
241
- channel,
242
- policyKey: "channels.msteams.dmPolicy",
243
- allowFromKey: "channels.msteams.allowFrom",
244
- getCurrent: (cfg) => cfg.channels?.msteams?.dmPolicy ?? "pairing",
245
- promptAllowFrom: promptMSTeamsAllowFrom,
246
- });
247
-
248
- const msteamsSetupWizardBase = createMSTeamsSetupWizardBase();
249
-
250
- export const msteamsSetupWizard: ChannelSetupWizard = {
251
- ...msteamsSetupWizardBase,
252
- // Override finalize to layer on the optional delegated-auth bootstrap after
253
- // the base wizard collects app credentials. This preserves main's shared
254
- // setup-core flow while keeping the delegated OAuth step from this PR.
255
- finalize: async (params) => {
256
- // setup-core always provides a finalize; the type is optional only because
257
- // ChannelSetupWizard.finalize is generally optional. Fall back to the
258
- // incoming cfg if the base ever returns void for forward-compat.
259
- const baseFinalize = msteamsSetupWizardBase.finalize;
260
- const baseResult = baseFinalize ? await baseFinalize(params) : undefined;
261
- let next = baseResult?.cfg ?? params.cfg;
262
- const finalCreds = resolveMSTeamsCredentials(next.channels?.msteams);
263
- if (finalCreds?.type === "secret") {
264
- const enableDelegated = await params.prompter.confirm({
265
- message: t("wizard.msteams.delegatedAuthPrompt"),
266
- initialValue: false,
267
- });
268
- if (enableDelegated) {
269
- next = {
270
- ...next,
271
- channels: {
272
- ...next.channels,
273
- msteams: {
274
- ...next.channels?.msteams,
275
- delegatedAuth: { enabled: true },
276
- },
277
- },
278
- };
279
- try {
280
- const { loginMSTeamsDelegated } = await import("./oauth.js");
281
- const progress = params.prompter.progress(t("wizard.msteams.delegatedOAuthProgress"));
282
- const tokens = await loginMSTeamsDelegated(
283
- {
284
- isRemote: true,
285
- openUrl: openDelegatedOAuthUrl,
286
- log: (msg) => params.prompter.note(msg),
287
- note: (msg, title) => params.prompter.note(msg, title),
288
- prompt: (msg) => params.prompter.text({ message: msg }),
289
- progress,
290
- },
291
- {
292
- tenantId: finalCreds.tenantId,
293
- clientId: finalCreds.appId,
294
- clientSecret: finalCreds.appPassword,
295
- },
296
- );
297
- saveDelegatedTokens(tokens);
298
- progress.stop(t("wizard.msteams.delegatedAuthConfigured"));
299
- } catch (err) {
300
- await params.prompter.note(
301
- `Delegated auth setup failed: ${formatUnknownError(err)}\n` +
302
- t("wizard.msteams.delegatedAuthRetry"),
303
- t("wizard.msteams.delegatedAuthTitle"),
304
- );
305
- }
306
- }
307
- }
308
- return { ...baseResult, cfg: next };
309
- },
310
- dmPolicy: msteamsDmPolicy,
311
- groupAccess: msteamsGroupAccess,
312
- disable: (cfg) => ({
313
- ...cfg,
314
- channels: {
315
- ...cfg.channels,
316
- msteams: { ...cfg.channels?.msteams, enabled: false },
317
- },
318
- }),
319
- };
@@ -1,74 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { describe, expect, it } from "vitest";
5
- import { createMSTeamsSsoTokenStoreFs } from "./sso-token-store.js";
6
-
7
- describe("msteams sso token store (fs)", () => {
8
- it("keeps distinct tokens when connectionName and userId contain the legacy delimiter", async () => {
9
- const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "klaw-msteams-sso-"));
10
- const storePath = path.join(stateDir, "msteams-sso-tokens.json");
11
- const store = createMSTeamsSsoTokenStoreFs({ storePath });
12
-
13
- const first = {
14
- connectionName: "conn::alpha",
15
- userId: "user",
16
- token: "token-a",
17
- updatedAt: "2026-04-10T00:00:00.000Z",
18
- } as const;
19
- const second = {
20
- connectionName: "conn",
21
- userId: "alpha::user",
22
- token: "token-b",
23
- updatedAt: "2026-04-10T00:00:01.000Z",
24
- } as const;
25
-
26
- await store.save(first);
27
- await store.save(second);
28
-
29
- expect(await store.get(first)).toEqual(first);
30
- expect(await store.get(second)).toEqual(second);
31
-
32
- const raw = JSON.parse(await fs.readFile(storePath, "utf8")) as {
33
- tokens: Record<string, unknown>;
34
- };
35
- expect(Object.keys(raw.tokens)).toHaveLength(2);
36
- });
37
-
38
- it("loads legacy flat-key files by rebuilding keys from stored token payloads", async () => {
39
- const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "klaw-msteams-sso-legacy-"));
40
- const storePath = path.join(stateDir, "msteams-sso-tokens.json");
41
- await fs.writeFile(
42
- storePath,
43
- `${JSON.stringify(
44
- {
45
- version: 1,
46
- tokens: {
47
- "legacy::wrong-key": {
48
- connectionName: "conn",
49
- userId: "user-1",
50
- token: "token-1",
51
- updatedAt: "2026-04-10T00:00:00.000Z",
52
- },
53
- },
54
- },
55
- null,
56
- 2,
57
- )}\n`,
58
- "utf8",
59
- );
60
-
61
- const store = createMSTeamsSsoTokenStoreFs({ storePath });
62
- expect(
63
- await store.get({
64
- connectionName: "conn",
65
- userId: "user-1",
66
- }),
67
- ).toEqual({
68
- connectionName: "conn",
69
- userId: "user-1",
70
- token: "token-1",
71
- updatedAt: "2026-04-10T00:00:00.000Z",
72
- });
73
- });
74
- });
@@ -1,166 +0,0 @@
1
- /**
2
- * File-backed store for Bot Framework OAuth SSO tokens.
3
- *
4
- * Tokens are keyed by (connectionName, userId). `userId` should be the
5
- * stable AAD object ID (`activity.from.aadObjectId`) when available,
6
- * falling back to the Bot Framework `activity.from.id`.
7
- *
8
- * The store is intentionally minimal: it persists the exchanged user
9
- * token plus its expiration so consumers (for example tool handlers
10
- * that call Microsoft Graph with delegated permissions) can fetch a
11
- * valid token without reaching back into Bot Framework every turn.
12
- */
13
-
14
- import { resolveMSTeamsStorePath } from "./storage.js";
15
- import { readJsonFile, withFileLock, writeJsonFile } from "./store-fs.js";
16
-
17
- type MSTeamsSsoStoredToken = {
18
- /** Connection name from the Bot Framework OAuth connection setting. */
19
- connectionName: string;
20
- /** Stable user identifier (AAD object ID preferred). */
21
- userId: string;
22
- /** Exchanged user access token. */
23
- token: string;
24
- /** Expiration (ISO 8601) when the Bot Framework user token service reports one. */
25
- expiresAt?: string;
26
- /** ISO 8601 timestamp for the last successful exchange. */
27
- updatedAt: string;
28
- };
29
-
30
- export type MSTeamsSsoTokenStore = {
31
- get(params: { connectionName: string; userId: string }): Promise<MSTeamsSsoStoredToken | null>;
32
- save(token: MSTeamsSsoStoredToken): Promise<void>;
33
- remove(params: { connectionName: string; userId: string }): Promise<boolean>;
34
- };
35
-
36
- type SsoStoreData = {
37
- version: 1;
38
- // Keyed by `${connectionName}::${userId}` for a simple flat map on disk.
39
- tokens: Record<string, MSTeamsSsoStoredToken>;
40
- };
41
-
42
- const STORE_FILENAME = "msteams-sso-tokens.json";
43
- const STORE_KEY_VERSION_PREFIX = "v2:";
44
-
45
- function makeKey(connectionName: string, userId: string): string {
46
- return `${STORE_KEY_VERSION_PREFIX}${Buffer.from(
47
- JSON.stringify([connectionName, userId]),
48
- "utf8",
49
- ).toString("base64url")}`;
50
- }
51
-
52
- function normalizeStoredToken(value: unknown): MSTeamsSsoStoredToken | null {
53
- if (!value || typeof value !== "object") {
54
- return null;
55
- }
56
- const token = value as Partial<MSTeamsSsoStoredToken>;
57
- if (
58
- typeof token.connectionName !== "string" ||
59
- !token.connectionName ||
60
- typeof token.userId !== "string" ||
61
- !token.userId ||
62
- typeof token.token !== "string" ||
63
- !token.token ||
64
- typeof token.updatedAt !== "string" ||
65
- !token.updatedAt
66
- ) {
67
- return null;
68
- }
69
- return {
70
- connectionName: token.connectionName,
71
- userId: token.userId,
72
- token: token.token,
73
- ...(typeof token.expiresAt === "string" ? { expiresAt: token.expiresAt } : {}),
74
- updatedAt: token.updatedAt,
75
- };
76
- }
77
-
78
- function isSsoStoreData(value: unknown): value is SsoStoreData {
79
- if (!value || typeof value !== "object") {
80
- return false;
81
- }
82
- const obj = value as Record<string, unknown>;
83
- return obj.version === 1 && typeof obj.tokens === "object" && obj.tokens !== null;
84
- }
85
-
86
- export function createMSTeamsSsoTokenStoreFs(params?: {
87
- env?: NodeJS.ProcessEnv;
88
- homedir?: () => string;
89
- stateDir?: string;
90
- storePath?: string;
91
- }): MSTeamsSsoTokenStore {
92
- const filePath = resolveMSTeamsStorePath({
93
- filename: STORE_FILENAME,
94
- env: params?.env,
95
- homedir: params?.homedir,
96
- stateDir: params?.stateDir,
97
- storePath: params?.storePath,
98
- });
99
-
100
- const empty: SsoStoreData = { version: 1, tokens: {} };
101
-
102
- const readStore = async (): Promise<SsoStoreData> => {
103
- const { value } = await readJsonFile(filePath, empty);
104
- if (!isSsoStoreData(value)) {
105
- return { version: 1, tokens: {} };
106
- }
107
- const tokens: Record<string, MSTeamsSsoStoredToken> = {};
108
- for (const stored of Object.values(value.tokens)) {
109
- const normalized = normalizeStoredToken(stored);
110
- if (!normalized) {
111
- continue;
112
- }
113
- tokens[makeKey(normalized.connectionName, normalized.userId)] = normalized;
114
- }
115
- return {
116
- version: 1,
117
- tokens,
118
- };
119
- };
120
-
121
- return {
122
- async get({ connectionName, userId }) {
123
- const store = await readStore();
124
- return store.tokens[makeKey(connectionName, userId)] ?? null;
125
- },
126
-
127
- async save(token) {
128
- await withFileLock(filePath, empty, async () => {
129
- const store = await readStore();
130
- const key = makeKey(token.connectionName, token.userId);
131
- store.tokens[key] = { ...token };
132
- await writeJsonFile(filePath, store);
133
- });
134
- },
135
-
136
- async remove({ connectionName, userId }) {
137
- let removed = false;
138
- await withFileLock(filePath, empty, async () => {
139
- const store = await readStore();
140
- const key = makeKey(connectionName, userId);
141
- if (store.tokens[key]) {
142
- delete store.tokens[key];
143
- removed = true;
144
- await writeJsonFile(filePath, store);
145
- }
146
- });
147
- return removed;
148
- },
149
- };
150
- }
151
-
152
- /** In-memory store, primarily useful for tests. */
153
- export function createMSTeamsSsoTokenStoreMemory(): MSTeamsSsoTokenStore {
154
- const tokens = new Map<string, MSTeamsSsoStoredToken>();
155
- return {
156
- async get({ connectionName, userId }) {
157
- return tokens.get(makeKey(connectionName, userId)) ?? null;
158
- },
159
- async save(token) {
160
- tokens.set(makeKey(token.connectionName, token.userId), { ...token });
161
- },
162
- async remove({ connectionName, userId }) {
163
- return tokens.delete(makeKey(connectionName, userId));
164
- },
165
- };
166
- }