@openclaw/msteams 2026.5.2-beta.2 → 2026.5.3-beta.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 (197) hide show
  1. package/dist/api.js +3 -0
  2. package/dist/channel-D7hdreTh.js +984 -0
  3. package/dist/channel-config-api.js +2 -0
  4. package/dist/channel-plugin-api.js +2 -0
  5. package/dist/channel.runtime-BC1ruIfN.js +573 -0
  6. package/dist/config-schema-B8QezH6t.js +15 -0
  7. package/dist/contract-api.js +2 -0
  8. package/dist/graph-users-9uQJepqr.js +1354 -0
  9. package/dist/index.js +22 -0
  10. package/dist/oauth-BWJyilR1.js +114 -0
  11. package/dist/oauth.token-xxpoLWy5.js +115 -0
  12. package/dist/policy-DTnU2GR7.js +142 -0
  13. package/dist/probe-D_H8yFps.js +2194 -0
  14. package/dist/resolve-allowlist-D41JSziq.js +219 -0
  15. package/dist/runtime-api-DV1iVMn1.js +28 -0
  16. package/dist/runtime-api.js +2 -0
  17. package/dist/secret-contract-BuoEXmPS.js +35 -0
  18. package/dist/secret-contract-api.js +2 -0
  19. package/dist/setup-entry.js +15 -0
  20. package/dist/setup-plugin-api.js +64 -0
  21. package/dist/setup-surface-BLkFQYIQ.js +313 -0
  22. package/dist/src-CFp1QpFd.js +4064 -0
  23. package/dist/test-api.js +2 -0
  24. package/package.json +14 -6
  25. package/api.ts +0 -3
  26. package/channel-config-api.ts +0 -1
  27. package/channel-plugin-api.ts +0 -2
  28. package/config-api.ts +0 -4
  29. package/contract-api.ts +0 -4
  30. package/index.ts +0 -20
  31. package/runtime-api.ts +0 -73
  32. package/secret-contract-api.ts +0 -5
  33. package/setup-entry.ts +0 -13
  34. package/setup-plugin-api.ts +0 -3
  35. package/src/ai-entity.ts +0 -7
  36. package/src/approval-auth.ts +0 -44
  37. package/src/attachments/bot-framework.test.ts +0 -461
  38. package/src/attachments/bot-framework.ts +0 -362
  39. package/src/attachments/download.ts +0 -311
  40. package/src/attachments/graph.test.ts +0 -416
  41. package/src/attachments/graph.ts +0 -484
  42. package/src/attachments/html.ts +0 -122
  43. package/src/attachments/payload.ts +0 -14
  44. package/src/attachments/remote-media.test.ts +0 -137
  45. package/src/attachments/remote-media.ts +0 -112
  46. package/src/attachments/shared.test.ts +0 -530
  47. package/src/attachments/shared.ts +0 -626
  48. package/src/attachments/types.ts +0 -47
  49. package/src/attachments.graph.test.ts +0 -342
  50. package/src/attachments.helpers.test.ts +0 -246
  51. package/src/attachments.test-helpers.ts +0 -17
  52. package/src/attachments.test.ts +0 -687
  53. package/src/attachments.ts +0 -18
  54. package/src/block-streaming-config.test.ts +0 -61
  55. package/src/channel-api.ts +0 -1
  56. package/src/channel.actions.test.ts +0 -742
  57. package/src/channel.directory.test.ts +0 -200
  58. package/src/channel.runtime.ts +0 -56
  59. package/src/channel.setup.ts +0 -77
  60. package/src/channel.test.ts +0 -128
  61. package/src/channel.ts +0 -1136
  62. package/src/config-schema.ts +0 -6
  63. package/src/config-ui-hints.ts +0 -12
  64. package/src/conversation-store-fs.test.ts +0 -74
  65. package/src/conversation-store-fs.ts +0 -149
  66. package/src/conversation-store-helpers.test.ts +0 -202
  67. package/src/conversation-store-helpers.ts +0 -105
  68. package/src/conversation-store-memory.ts +0 -51
  69. package/src/conversation-store.shared.test.ts +0 -225
  70. package/src/conversation-store.ts +0 -71
  71. package/src/directory-live.test.ts +0 -156
  72. package/src/directory-live.ts +0 -111
  73. package/src/doctor.ts +0 -27
  74. package/src/errors.test.ts +0 -133
  75. package/src/errors.ts +0 -246
  76. package/src/feedback-reflection-prompt.ts +0 -117
  77. package/src/feedback-reflection-store.ts +0 -114
  78. package/src/feedback-reflection.test.ts +0 -237
  79. package/src/feedback-reflection.ts +0 -283
  80. package/src/file-consent-helpers.test.ts +0 -326
  81. package/src/file-consent-helpers.ts +0 -126
  82. package/src/file-consent-invoke.ts +0 -150
  83. package/src/file-consent.test.ts +0 -363
  84. package/src/file-consent.ts +0 -287
  85. package/src/graph-chat.ts +0 -55
  86. package/src/graph-group-management.test.ts +0 -318
  87. package/src/graph-group-management.ts +0 -168
  88. package/src/graph-members.test.ts +0 -89
  89. package/src/graph-members.ts +0 -48
  90. package/src/graph-messages.actions.test.ts +0 -243
  91. package/src/graph-messages.read.test.ts +0 -391
  92. package/src/graph-messages.search.test.ts +0 -213
  93. package/src/graph-messages.test-helpers.ts +0 -50
  94. package/src/graph-messages.ts +0 -534
  95. package/src/graph-teams.test.ts +0 -215
  96. package/src/graph-teams.ts +0 -114
  97. package/src/graph-thread.test.ts +0 -246
  98. package/src/graph-thread.ts +0 -146
  99. package/src/graph-upload.test.ts +0 -258
  100. package/src/graph-upload.ts +0 -531
  101. package/src/graph-users.ts +0 -29
  102. package/src/graph.test.ts +0 -516
  103. package/src/graph.ts +0 -293
  104. package/src/inbound.test.ts +0 -221
  105. package/src/inbound.ts +0 -148
  106. package/src/index.ts +0 -4
  107. package/src/media-helpers.test.ts +0 -202
  108. package/src/media-helpers.ts +0 -105
  109. package/src/mentions.test.ts +0 -244
  110. package/src/mentions.ts +0 -114
  111. package/src/messenger.test.ts +0 -865
  112. package/src/messenger.ts +0 -605
  113. package/src/monitor-handler/access.ts +0 -125
  114. package/src/monitor-handler/inbound-media.test.ts +0 -289
  115. package/src/monitor-handler/inbound-media.ts +0 -180
  116. package/src/monitor-handler/message-handler-mock-support.test-support.ts +0 -28
  117. package/src/monitor-handler/message-handler.authz.test.ts +0 -669
  118. package/src/monitor-handler/message-handler.dm-media.test.ts +0 -54
  119. package/src/monitor-handler/message-handler.test-support.ts +0 -100
  120. package/src/monitor-handler/message-handler.thread-parent.test.ts +0 -223
  121. package/src/monitor-handler/message-handler.thread-session.test.ts +0 -77
  122. package/src/monitor-handler/message-handler.ts +0 -1000
  123. package/src/monitor-handler/reaction-handler.test.ts +0 -267
  124. package/src/monitor-handler/reaction-handler.ts +0 -210
  125. package/src/monitor-handler/thread-session.ts +0 -17
  126. package/src/monitor-handler.adaptive-card.test.ts +0 -162
  127. package/src/monitor-handler.feedback-authz.test.ts +0 -314
  128. package/src/monitor-handler.file-consent.test.ts +0 -423
  129. package/src/monitor-handler.sso.test.ts +0 -563
  130. package/src/monitor-handler.test-helpers.ts +0 -180
  131. package/src/monitor-handler.ts +0 -534
  132. package/src/monitor-handler.types.ts +0 -27
  133. package/src/monitor-types.ts +0 -6
  134. package/src/monitor.lifecycle.test.ts +0 -278
  135. package/src/monitor.test.ts +0 -119
  136. package/src/monitor.ts +0 -442
  137. package/src/oauth.flow.ts +0 -77
  138. package/src/oauth.shared.ts +0 -37
  139. package/src/oauth.test.ts +0 -305
  140. package/src/oauth.token.ts +0 -158
  141. package/src/oauth.ts +0 -130
  142. package/src/outbound.test.ts +0 -130
  143. package/src/outbound.ts +0 -71
  144. package/src/pending-uploads-fs.test.ts +0 -246
  145. package/src/pending-uploads-fs.ts +0 -235
  146. package/src/pending-uploads.test.ts +0 -173
  147. package/src/pending-uploads.ts +0 -121
  148. package/src/policy.test.ts +0 -240
  149. package/src/policy.ts +0 -262
  150. package/src/polls-store-memory.ts +0 -32
  151. package/src/polls.test.ts +0 -160
  152. package/src/polls.ts +0 -323
  153. package/src/presentation.ts +0 -68
  154. package/src/probe.test.ts +0 -77
  155. package/src/probe.ts +0 -132
  156. package/src/reply-dispatcher.test.ts +0 -437
  157. package/src/reply-dispatcher.ts +0 -346
  158. package/src/reply-stream-controller.test.ts +0 -235
  159. package/src/reply-stream-controller.ts +0 -147
  160. package/src/resolve-allowlist.test.ts +0 -250
  161. package/src/resolve-allowlist.ts +0 -309
  162. package/src/revoked-context.ts +0 -17
  163. package/src/runtime.ts +0 -9
  164. package/src/sdk-types.ts +0 -59
  165. package/src/sdk.test.ts +0 -666
  166. package/src/sdk.ts +0 -884
  167. package/src/secret-contract.ts +0 -49
  168. package/src/secret-input.ts +0 -7
  169. package/src/send-context.ts +0 -231
  170. package/src/send.test.ts +0 -493
  171. package/src/send.ts +0 -637
  172. package/src/sent-message-cache.test.ts +0 -15
  173. package/src/sent-message-cache.ts +0 -56
  174. package/src/session-route.ts +0 -40
  175. package/src/setup-core.ts +0 -160
  176. package/src/setup-surface.test.ts +0 -202
  177. package/src/setup-surface.ts +0 -320
  178. package/src/sso-token-store.test.ts +0 -72
  179. package/src/sso-token-store.ts +0 -166
  180. package/src/sso.ts +0 -300
  181. package/src/storage.ts +0 -25
  182. package/src/store-fs.ts +0 -44
  183. package/src/streaming-message.test.ts +0 -262
  184. package/src/streaming-message.ts +0 -297
  185. package/src/test-runtime.ts +0 -16
  186. package/src/thread-parent-context.test.ts +0 -224
  187. package/src/thread-parent-context.ts +0 -159
  188. package/src/token-response.ts +0 -11
  189. package/src/token.test.ts +0 -259
  190. package/src/token.ts +0 -195
  191. package/src/user-agent.test.ts +0 -86
  192. package/src/user-agent.ts +0 -53
  193. package/src/webhook-timeouts.ts +0 -27
  194. package/src/welcome-card.test.ts +0 -81
  195. package/src/welcome-card.ts +0 -57
  196. package/test-api.ts +0 -1
  197. package/tsconfig.json +0 -16
@@ -1,320 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import {
3
- createTopLevelChannelAllowFromSetter,
4
- createTopLevelChannelDmPolicy,
5
- createTopLevelChannelGroupPolicySetter,
6
- mergeAllowFromEntries,
7
- splitSetupEntries,
8
- type ChannelSetupDmPolicy,
9
- type ChannelSetupWizard,
10
- type OpenClawConfig,
11
- type WizardPrompter,
12
- } from "openclaw/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 channel = "msteams" as const;
24
- const setMSTeamsAllowFrom = createTopLevelChannelAllowFromSetter({
25
- channel,
26
- });
27
- const setMSTeamsGroupPolicy = createTopLevelChannelGroupPolicySetter({
28
- channel,
29
- enabled: true,
30
- });
31
-
32
- export function openDelegatedOAuthUrl(url: string): Promise<void> {
33
- return new Promise<void>((resolve, reject) => {
34
- const cmd = process.platform === "darwin" ? "open" : "xdg-open";
35
- const child = spawn(cmd, [url], { stdio: "ignore", shell: false });
36
- child.once("error", reject);
37
- child.once("exit", (code, signal) => {
38
- if (code === 0) {
39
- resolve();
40
- return;
41
- }
42
- const reason = signal ? `signal ${signal}` : `code ${code ?? "unknown"}`;
43
- reject(new Error(`${cmd} failed with ${reason}`));
44
- });
45
- });
46
- }
47
-
48
- function looksLikeGuid(value: string): boolean {
49
- return /^[0-9a-fA-F-]{16,}$/.test(value);
50
- }
51
-
52
- async function promptMSTeamsAllowFrom(params: {
53
- cfg: OpenClawConfig;
54
- prompter: WizardPrompter;
55
- }): Promise<OpenClawConfig> {
56
- const existing = params.cfg.channels?.msteams?.allowFrom ?? [];
57
- await params.prompter.note(
58
- [
59
- "Allowlist MS Teams DMs by display name, UPN/email, or user id.",
60
- "We resolve names to user IDs via Microsoft Graph when credentials allow.",
61
- "Examples:",
62
- "- alex@example.com",
63
- "- Alex Johnson",
64
- "- 00000000-0000-0000-0000-000000000000",
65
- ].join("\n"),
66
- "MS Teams allowlist",
67
- );
68
-
69
- while (true) {
70
- const entry = await params.prompter.text({
71
- message: "MS Teams allowFrom (usernames or ids)",
72
- placeholder: "alex@example.com, Alex Johnson",
73
- initialValue: existing[0] ? existing[0] : undefined,
74
- validate: (value) => (value.trim() ? undefined : "Required"),
75
- });
76
- const parts = splitSetupEntries(entry);
77
- if (parts.length === 0) {
78
- await params.prompter.note("Enter at least one user.", "MS Teams allowlist");
79
- continue;
80
- }
81
-
82
- const resolved = await resolveMSTeamsUserAllowlist({
83
- cfg: params.cfg,
84
- entries: parts,
85
- }).catch(() => null);
86
-
87
- if (!resolved) {
88
- const ids = parts.filter((part) => looksLikeGuid(part));
89
- if (ids.length !== parts.length) {
90
- await params.prompter.note(
91
- "Graph lookup unavailable. Use user IDs only.",
92
- "MS Teams allowlist",
93
- );
94
- continue;
95
- }
96
- const unique = mergeAllowFromEntries(existing, ids);
97
- return setMSTeamsAllowFrom(params.cfg, unique);
98
- }
99
-
100
- const unresolved = resolved.filter((item) => !item.resolved || !item.id);
101
- if (unresolved.length > 0) {
102
- await params.prompter.note(
103
- `Could not resolve: ${unresolved.map((item) => item.input).join(", ")}`,
104
- "MS Teams allowlist",
105
- );
106
- continue;
107
- }
108
-
109
- const ids = resolved.map((item) => item.id as string);
110
- const unique = mergeAllowFromEntries(existing, ids);
111
- return setMSTeamsAllowFrom(params.cfg, unique);
112
- }
113
- }
114
-
115
- function setMSTeamsTeamsAllowlist(
116
- cfg: OpenClawConfig,
117
- entries: Array<{ teamKey: string; channelKey?: string }>,
118
- ): OpenClawConfig {
119
- const baseTeams = cfg.channels?.msteams?.teams ?? {};
120
- const teams: Record<string, { channels?: Record<string, unknown> }> = { ...baseTeams };
121
- for (const entry of entries) {
122
- const teamKey = entry.teamKey;
123
- if (!teamKey) {
124
- continue;
125
- }
126
- const existing = teams[teamKey] ?? {};
127
- if (entry.channelKey) {
128
- const channels = { ...existing.channels };
129
- channels[entry.channelKey] = channels[entry.channelKey] ?? {};
130
- teams[teamKey] = { ...existing, channels };
131
- } else {
132
- teams[teamKey] = existing;
133
- }
134
- }
135
- return {
136
- ...cfg,
137
- channels: {
138
- ...cfg.channels,
139
- msteams: {
140
- ...cfg.channels?.msteams,
141
- enabled: true,
142
- teams: teams as Record<string, MSTeamsTeamConfig>,
143
- },
144
- },
145
- };
146
- }
147
-
148
- function listMSTeamsGroupEntries(cfg: OpenClawConfig): string[] {
149
- return Object.entries(cfg.channels?.msteams?.teams ?? {}).flatMap(([teamKey, value]) => {
150
- const channels = value?.channels ?? {};
151
- const channelKeys = Object.keys(channels);
152
- if (channelKeys.length === 0) {
153
- return [teamKey];
154
- }
155
- return channelKeys.map((channelKey) => `${teamKey}/${channelKey}`);
156
- });
157
- }
158
-
159
- async function resolveMSTeamsGroupAllowlist(params: {
160
- cfg: OpenClawConfig;
161
- entries: string[];
162
- prompter: Pick<WizardPrompter, "note">;
163
- }): Promise<Array<{ teamKey: string; channelKey?: string }>> {
164
- let resolvedEntries = params.entries
165
- .map((entry) => parseMSTeamsTeamEntry(entry))
166
- .filter(Boolean) as Array<{ teamKey: string; channelKey?: string }>;
167
- if (params.entries.length === 0 || !resolveMSTeamsCredentials(params.cfg.channels?.msteams)) {
168
- return resolvedEntries;
169
- }
170
- try {
171
- const lookups = await resolveMSTeamsChannelAllowlist({
172
- cfg: params.cfg,
173
- entries: params.entries,
174
- });
175
- const resolvedChannels = lookups.filter(
176
- (entry) => entry.resolved && entry.teamId && entry.channelId,
177
- );
178
- const resolvedTeams = lookups.filter(
179
- (entry) => entry.resolved && entry.teamId && !entry.channelId,
180
- );
181
- const unresolved = lookups.filter((entry) => !entry.resolved).map((entry) => entry.input);
182
- resolvedEntries = [
183
- ...resolvedChannels.map((entry) => ({
184
- teamKey: entry.teamId as string,
185
- channelKey: entry.channelId as string,
186
- })),
187
- ...resolvedTeams.map((entry) => ({
188
- teamKey: entry.teamId as string,
189
- })),
190
- ...unresolved.map((entry) => parseMSTeamsTeamEntry(entry)).filter(Boolean),
191
- ] as Array<{ teamKey: string; channelKey?: string }>;
192
- const summary: string[] = [];
193
- if (resolvedChannels.length > 0) {
194
- summary.push(
195
- `Resolved channels: ${resolvedChannels
196
- .map((entry) => entry.channelId)
197
- .filter(Boolean)
198
- .join(", ")}`,
199
- );
200
- }
201
- if (resolvedTeams.length > 0) {
202
- summary.push(
203
- `Resolved teams: ${resolvedTeams
204
- .map((entry) => entry.teamId)
205
- .filter(Boolean)
206
- .join(", ")}`,
207
- );
208
- }
209
- if (unresolved.length > 0) {
210
- summary.push(`Unresolved (kept as typed): ${unresolved.join(", ")}`);
211
- }
212
- if (summary.length > 0) {
213
- await params.prompter.note(summary.join("\n"), "MS Teams channels");
214
- }
215
- return resolvedEntries;
216
- } catch (err) {
217
- await params.prompter.note(
218
- `Channel lookup failed; keeping entries as typed. ${formatUnknownError(err)}`,
219
- "MS Teams channels",
220
- );
221
- return resolvedEntries;
222
- }
223
- }
224
-
225
- const msteamsGroupAccess: NonNullable<ChannelSetupWizard["groupAccess"]> = {
226
- label: "MS Teams channels",
227
- placeholder: "Team Name/Channel Name, teamId/conversationId",
228
- currentPolicy: ({ cfg }) => cfg.channels?.msteams?.groupPolicy ?? "allowlist",
229
- currentEntries: ({ cfg }) => listMSTeamsGroupEntries(cfg),
230
- updatePrompt: ({ cfg }) => Boolean(cfg.channels?.msteams?.teams),
231
- setPolicy: ({ cfg, policy }) => setMSTeamsGroupPolicy(cfg, policy),
232
- resolveAllowlist: async ({ cfg, entries, prompter }) =>
233
- await resolveMSTeamsGroupAllowlist({ cfg, entries, prompter }),
234
- applyAllowlist: ({ cfg, resolved }) =>
235
- setMSTeamsTeamsAllowlist(cfg, resolved as Array<{ teamKey: string; channelKey?: string }>),
236
- };
237
-
238
- const msteamsDmPolicy: ChannelSetupDmPolicy = createTopLevelChannelDmPolicy({
239
- label: "MS Teams",
240
- channel,
241
- policyKey: "channels.msteams.dmPolicy",
242
- allowFromKey: "channels.msteams.allowFrom",
243
- getCurrent: (cfg) => cfg.channels?.msteams?.dmPolicy ?? "pairing",
244
- promptAllowFrom: promptMSTeamsAllowFrom,
245
- });
246
-
247
- const msteamsSetupWizardBase = createMSTeamsSetupWizardBase();
248
-
249
- export const msteamsSetupWizard: ChannelSetupWizard = {
250
- ...msteamsSetupWizardBase,
251
- // Override finalize to layer on the optional delegated-auth bootstrap after
252
- // the base wizard collects app credentials. This preserves main's shared
253
- // setup-core flow while keeping the delegated OAuth step from this PR.
254
- finalize: async (params) => {
255
- // setup-core always provides a finalize; the type is optional only because
256
- // ChannelSetupWizard.finalize is generally optional. Fall back to the
257
- // incoming cfg if the base ever returns void for forward-compat.
258
- const baseFinalize = msteamsSetupWizardBase.finalize;
259
- const baseResult = baseFinalize ? await baseFinalize(params) : undefined;
260
- let next = baseResult?.cfg ?? params.cfg;
261
- const finalCreds = resolveMSTeamsCredentials(next.channels?.msteams);
262
- if (finalCreds?.type === "secret") {
263
- const enableDelegated = await params.prompter.confirm({
264
- message: "Enable delegated auth? (required for reactions and write operations)",
265
- initialValue: false,
266
- });
267
- if (enableDelegated) {
268
- next = {
269
- ...next,
270
- channels: {
271
- ...next.channels,
272
- msteams: {
273
- ...next.channels?.msteams,
274
- delegatedAuth: { enabled: true },
275
- },
276
- },
277
- };
278
- try {
279
- const { loginMSTeamsDelegated } = await import("./oauth.js");
280
- const { shouldUseManualOAuthFlow } = await import("./oauth.flow.js");
281
- const isRemote = Boolean(process.env.SSH_TTY || process.env.SSH_CONNECTION);
282
- const progress = params.prompter.progress("MSTeams Delegated OAuth");
283
- const tokens = await loginMSTeamsDelegated(
284
- {
285
- isRemote: shouldUseManualOAuthFlow(isRemote),
286
- openUrl: openDelegatedOAuthUrl,
287
- log: (msg) => params.prompter.note(msg),
288
- note: (msg, title) => params.prompter.note(msg, title),
289
- prompt: (msg) => params.prompter.text({ message: msg }),
290
- progress,
291
- },
292
- {
293
- tenantId: finalCreds.tenantId,
294
- clientId: finalCreds.appId,
295
- clientSecret: finalCreds.appPassword,
296
- },
297
- );
298
- saveDelegatedTokens(tokens);
299
- progress.stop("Delegated auth configured");
300
- } catch (err) {
301
- await params.prompter.note(
302
- `Delegated auth setup failed: ${formatUnknownError(err)}\n` +
303
- "You can retry later via the setup wizard.",
304
- "MS Teams delegated auth",
305
- );
306
- }
307
- }
308
- }
309
- return { ...baseResult, cfg: next };
310
- },
311
- dmPolicy: msteamsDmPolicy,
312
- groupAccess: msteamsGroupAccess,
313
- disable: (cfg) => ({
314
- ...cfg,
315
- channels: {
316
- ...cfg.channels,
317
- msteams: { ...cfg.channels?.msteams, enabled: false },
318
- },
319
- }),
320
- };
@@ -1,72 +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(), "openclaw-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(), "openclaw-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
- ).toMatchObject({
68
- token: "token-1",
69
- updatedAt: "2026-04-10T00:00:00.000Z",
70
- });
71
- });
72
- });
@@ -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
- }