@openclaw/msteams 2026.2.13 → 2026.2.15

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.2.15
4
+
5
+ ### Changes
6
+
7
+ - Version alignment with core OpenClaw release numbers.
8
+
9
+ ## 2026.2.14
10
+
11
+ ### Changes
12
+
13
+ - Version alignment with core OpenClaw release numbers.
14
+
3
15
  ## 2026.2.13
4
16
 
5
17
  ### Changes
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@openclaw/msteams",
3
- "version": "2026.2.13",
3
+ "version": "2026.2.15",
4
4
  "description": "OpenClaw Microsoft Teams channel plugin",
5
5
  "type": "module",
6
6
  "dependencies": {
7
7
  "@microsoft/agents-hosting": "^1.2.3",
8
8
  "@microsoft/agents-hosting-express": "^1.2.3",
9
9
  "@microsoft/agents-hosting-extensions-teams": "^1.2.3",
10
- "express": "^5.2.1",
11
- "proper-lockfile": "^4.1.2"
10
+ "express": "^5.2.1"
12
11
  },
13
12
  "devDependencies": {
14
13
  "openclaw": "workspace:*"
package/src/channel.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { ChannelMessageActionName, ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk";
2
2
  import {
3
+ buildBaseChannelStatusSummary,
3
4
  buildChannelConfigSchema,
5
+ createDefaultChannelRuntimeState,
4
6
  DEFAULT_ACCOUNT_ID,
5
7
  MSTeamsConfigSchema,
6
8
  PAIRING_APPROVED_MESSAGE,
@@ -415,20 +417,9 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
415
417
  },
416
418
  outbound: msteamsOutbound,
417
419
  status: {
418
- defaultRuntime: {
419
- accountId: DEFAULT_ACCOUNT_ID,
420
- running: false,
421
- lastStartAt: null,
422
- lastStopAt: null,
423
- lastError: null,
424
- port: null,
425
- },
420
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }),
426
421
  buildChannelSummary: ({ snapshot }) => ({
427
- configured: snapshot.configured ?? false,
428
- running: snapshot.running ?? false,
429
- lastStartAt: snapshot.lastStartAt ?? null,
430
- lastStopAt: snapshot.lastStopAt ?? null,
431
- lastError: snapshot.lastError ?? null,
422
+ ...buildBaseChannelStatusSummary(snapshot),
432
423
  port: snapshot.port ?? null,
433
424
  probe: snapshot.probe,
434
425
  lastProbeAt: snapshot.lastProbeAt ?? null,
@@ -1,95 +1,16 @@
1
- import type { ChannelDirectoryEntry, MSTeamsConfig } from "openclaw/plugin-sdk";
2
- import { GRAPH_ROOT } from "./attachments/shared.js";
3
- import { loadMSTeamsSdkWithAuth } from "./sdk.js";
4
- import { resolveMSTeamsCredentials } from "./token.js";
5
-
6
- type GraphUser = {
7
- id?: string;
8
- displayName?: string;
9
- userPrincipalName?: string;
10
- mail?: string;
11
- };
12
-
13
- type GraphGroup = {
14
- id?: string;
15
- displayName?: string;
16
- };
17
-
18
- type GraphChannel = {
19
- id?: string;
20
- displayName?: string;
21
- };
22
-
23
- type GraphResponse<T> = { value?: T[] };
24
-
25
- function readAccessToken(value: unknown): string | null {
26
- if (typeof value === "string") {
27
- return value;
28
- }
29
- if (value && typeof value === "object") {
30
- const token =
31
- (value as { accessToken?: unknown }).accessToken ?? (value as { token?: unknown }).token;
32
- return typeof token === "string" ? token : null;
33
- }
34
- return null;
35
- }
36
-
37
- function normalizeQuery(value?: string | null): string {
38
- return value?.trim() ?? "";
39
- }
40
-
41
- function escapeOData(value: string): string {
42
- return value.replace(/'/g, "''");
43
- }
44
-
45
- async function fetchGraphJson<T>(params: {
46
- token: string;
47
- path: string;
48
- headers?: Record<string, string>;
49
- }): Promise<T> {
50
- const res = await fetch(`${GRAPH_ROOT}${params.path}`, {
51
- headers: {
52
- Authorization: `Bearer ${params.token}`,
53
- ...params.headers,
54
- },
55
- });
56
- if (!res.ok) {
57
- const text = await res.text().catch(() => "");
58
- throw new Error(`Graph ${params.path} failed (${res.status}): ${text || "unknown error"}`);
59
- }
60
- return (await res.json()) as T;
61
- }
62
-
63
- async function resolveGraphToken(cfg: unknown): Promise<string> {
64
- const creds = resolveMSTeamsCredentials(
65
- (cfg as { channels?: { msteams?: unknown } })?.channels?.msteams as MSTeamsConfig | undefined,
66
- );
67
- if (!creds) {
68
- throw new Error("MS Teams credentials missing");
69
- }
70
- const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
71
- const tokenProvider = new sdk.MsalTokenProvider(authConfig);
72
- const token = await tokenProvider.getAccessToken("https://graph.microsoft.com");
73
- const accessToken = readAccessToken(token);
74
- if (!accessToken) {
75
- throw new Error("MS Teams graph token unavailable");
76
- }
77
- return accessToken;
78
- }
79
-
80
- async function listTeamsByName(token: string, query: string): Promise<GraphGroup[]> {
81
- const escaped = escapeOData(query);
82
- const filter = `resourceProvisioningOptions/Any(x:x eq 'Team') and startsWith(displayName,'${escaped}')`;
83
- const path = `/groups?$filter=${encodeURIComponent(filter)}&$select=id,displayName`;
84
- const res = await fetchGraphJson<GraphResponse<GraphGroup>>({ token, path });
85
- return res.value ?? [];
86
- }
87
-
88
- async function listChannelsForTeam(token: string, teamId: string): Promise<GraphChannel[]> {
89
- const path = `/teams/${encodeURIComponent(teamId)}/channels?$select=id,displayName`;
90
- const res = await fetchGraphJson<GraphResponse<GraphChannel>>({ token, path });
91
- return res.value ?? [];
92
- }
1
+ import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk";
2
+ import {
3
+ escapeOData,
4
+ fetchGraphJson,
5
+ type GraphChannel,
6
+ type GraphGroup,
7
+ type GraphResponse,
8
+ type GraphUser,
9
+ listChannelsForTeam,
10
+ listTeamsByName,
11
+ normalizeQuery,
12
+ resolveGraphToken,
13
+ } from "./graph.js";
93
14
 
94
15
  export async function listMSTeamsDirectoryPeersLive(params: {
95
16
  cfg: unknown;
package/src/file-lock.ts CHANGED
@@ -1,189 +1 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
-
4
- type FileLockOptions = {
5
- retries: {
6
- retries: number;
7
- factor: number;
8
- minTimeout: number;
9
- maxTimeout: number;
10
- randomize?: boolean;
11
- };
12
- stale: number;
13
- };
14
-
15
- type LockFilePayload = {
16
- pid: number;
17
- createdAt: string;
18
- };
19
-
20
- type HeldLock = {
21
- count: number;
22
- handle: fs.FileHandle;
23
- lockPath: string;
24
- };
25
-
26
- const HELD_LOCKS_KEY = Symbol.for("openclaw.msteamsFileLockHeldLocks");
27
-
28
- function resolveHeldLocks(): Map<string, HeldLock> {
29
- const proc = process as NodeJS.Process & {
30
- [HELD_LOCKS_KEY]?: Map<string, HeldLock>;
31
- };
32
- if (!proc[HELD_LOCKS_KEY]) {
33
- proc[HELD_LOCKS_KEY] = new Map<string, HeldLock>();
34
- }
35
- return proc[HELD_LOCKS_KEY];
36
- }
37
-
38
- const HELD_LOCKS = resolveHeldLocks();
39
-
40
- function isAlive(pid: number): boolean {
41
- if (!Number.isFinite(pid) || pid <= 0) {
42
- return false;
43
- }
44
- try {
45
- process.kill(pid, 0);
46
- return true;
47
- } catch {
48
- return false;
49
- }
50
- }
51
-
52
- function computeDelayMs(retries: FileLockOptions["retries"], attempt: number): number {
53
- const base = Math.min(
54
- retries.maxTimeout,
55
- Math.max(retries.minTimeout, retries.minTimeout * retries.factor ** attempt),
56
- );
57
- const jitter = retries.randomize ? 1 + Math.random() : 1;
58
- return Math.min(retries.maxTimeout, Math.round(base * jitter));
59
- }
60
-
61
- async function readLockPayload(lockPath: string): Promise<LockFilePayload | null> {
62
- try {
63
- const raw = await fs.readFile(lockPath, "utf8");
64
- const parsed = JSON.parse(raw) as Partial<LockFilePayload>;
65
- if (typeof parsed.pid !== "number" || typeof parsed.createdAt !== "string") {
66
- return null;
67
- }
68
- return { pid: parsed.pid, createdAt: parsed.createdAt };
69
- } catch {
70
- return null;
71
- }
72
- }
73
-
74
- async function resolveNormalizedFilePath(filePath: string): Promise<string> {
75
- const resolved = path.resolve(filePath);
76
- const dir = path.dirname(resolved);
77
- await fs.mkdir(dir, { recursive: true });
78
- try {
79
- const realDir = await fs.realpath(dir);
80
- return path.join(realDir, path.basename(resolved));
81
- } catch {
82
- return resolved;
83
- }
84
- }
85
-
86
- async function isStaleLock(lockPath: string, staleMs: number): Promise<boolean> {
87
- const payload = await readLockPayload(lockPath);
88
- if (payload?.pid && !isAlive(payload.pid)) {
89
- return true;
90
- }
91
- if (payload?.createdAt) {
92
- const createdAt = Date.parse(payload.createdAt);
93
- if (!Number.isFinite(createdAt) || Date.now() - createdAt > staleMs) {
94
- return true;
95
- }
96
- }
97
- try {
98
- const stat = await fs.stat(lockPath);
99
- return Date.now() - stat.mtimeMs > staleMs;
100
- } catch {
101
- return true;
102
- }
103
- }
104
-
105
- type FileLockHandle = {
106
- release: () => Promise<void>;
107
- };
108
-
109
- async function acquireFileLock(
110
- filePath: string,
111
- options: FileLockOptions,
112
- ): Promise<FileLockHandle> {
113
- const normalizedFile = await resolveNormalizedFilePath(filePath);
114
- const lockPath = `${normalizedFile}.lock`;
115
- const held = HELD_LOCKS.get(normalizedFile);
116
- if (held) {
117
- held.count += 1;
118
- return {
119
- release: async () => {
120
- const current = HELD_LOCKS.get(normalizedFile);
121
- if (!current) {
122
- return;
123
- }
124
- current.count -= 1;
125
- if (current.count > 0) {
126
- return;
127
- }
128
- HELD_LOCKS.delete(normalizedFile);
129
- await current.handle.close().catch(() => undefined);
130
- await fs.rm(current.lockPath, { force: true }).catch(() => undefined);
131
- },
132
- };
133
- }
134
-
135
- const attempts = Math.max(1, options.retries.retries + 1);
136
- for (let attempt = 0; attempt < attempts; attempt += 1) {
137
- try {
138
- const handle = await fs.open(lockPath, "wx");
139
- await handle.writeFile(
140
- JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2),
141
- "utf8",
142
- );
143
- HELD_LOCKS.set(normalizedFile, { count: 1, handle, lockPath });
144
- return {
145
- release: async () => {
146
- const current = HELD_LOCKS.get(normalizedFile);
147
- if (!current) {
148
- return;
149
- }
150
- current.count -= 1;
151
- if (current.count > 0) {
152
- return;
153
- }
154
- HELD_LOCKS.delete(normalizedFile);
155
- await current.handle.close().catch(() => undefined);
156
- await fs.rm(current.lockPath, { force: true }).catch(() => undefined);
157
- },
158
- };
159
- } catch (err) {
160
- const code = (err as { code?: string }).code;
161
- if (code !== "EEXIST") {
162
- throw err;
163
- }
164
- if (await isStaleLock(lockPath, options.stale)) {
165
- await fs.rm(lockPath, { force: true }).catch(() => undefined);
166
- continue;
167
- }
168
- if (attempt >= attempts - 1) {
169
- break;
170
- }
171
- await new Promise((resolve) => setTimeout(resolve, computeDelayMs(options.retries, attempt)));
172
- }
173
- }
174
-
175
- throw new Error(`file lock timeout for ${normalizedFile}`);
176
- }
177
-
178
- export async function withFileLock<T>(
179
- filePath: string,
180
- options: FileLockOptions,
181
- fn: () => Promise<T>,
182
- ): Promise<T> {
183
- const lock = await acquireFileLock(filePath, options);
184
- try {
185
- return await fn();
186
- } finally {
187
- await lock.release();
188
- }
189
- }
1
+ export { withFileLock } from "openclaw/plugin-sdk";
package/src/graph.ts ADDED
@@ -0,0 +1,92 @@
1
+ import type { MSTeamsConfig } from "openclaw/plugin-sdk";
2
+ import { GRAPH_ROOT } from "./attachments/shared.js";
3
+ import { loadMSTeamsSdkWithAuth } from "./sdk.js";
4
+ import { resolveMSTeamsCredentials } from "./token.js";
5
+
6
+ export type GraphUser = {
7
+ id?: string;
8
+ displayName?: string;
9
+ userPrincipalName?: string;
10
+ mail?: string;
11
+ };
12
+
13
+ export type GraphGroup = {
14
+ id?: string;
15
+ displayName?: string;
16
+ };
17
+
18
+ export type GraphChannel = {
19
+ id?: string;
20
+ displayName?: string;
21
+ };
22
+
23
+ export type GraphResponse<T> = { value?: T[] };
24
+
25
+ function readAccessToken(value: unknown): string | null {
26
+ if (typeof value === "string") {
27
+ return value;
28
+ }
29
+ if (value && typeof value === "object") {
30
+ const token =
31
+ (value as { accessToken?: unknown }).accessToken ?? (value as { token?: unknown }).token;
32
+ return typeof token === "string" ? token : null;
33
+ }
34
+ return null;
35
+ }
36
+
37
+ export function normalizeQuery(value?: string | null): string {
38
+ return value?.trim() ?? "";
39
+ }
40
+
41
+ export function escapeOData(value: string): string {
42
+ return value.replace(/'/g, "''");
43
+ }
44
+
45
+ export async function fetchGraphJson<T>(params: {
46
+ token: string;
47
+ path: string;
48
+ headers?: Record<string, string>;
49
+ }): Promise<T> {
50
+ const res = await fetch(`${GRAPH_ROOT}${params.path}`, {
51
+ headers: {
52
+ Authorization: `Bearer ${params.token}`,
53
+ ...params.headers,
54
+ },
55
+ });
56
+ if (!res.ok) {
57
+ const text = await res.text().catch(() => "");
58
+ throw new Error(`Graph ${params.path} failed (${res.status}): ${text || "unknown error"}`);
59
+ }
60
+ return (await res.json()) as T;
61
+ }
62
+
63
+ export async function resolveGraphToken(cfg: unknown): Promise<string> {
64
+ const creds = resolveMSTeamsCredentials(
65
+ (cfg as { channels?: { msteams?: unknown } })?.channels?.msteams as MSTeamsConfig | undefined,
66
+ );
67
+ if (!creds) {
68
+ throw new Error("MS Teams credentials missing");
69
+ }
70
+ const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
71
+ const tokenProvider = new sdk.MsalTokenProvider(authConfig);
72
+ const token = await tokenProvider.getAccessToken("https://graph.microsoft.com");
73
+ const accessToken = readAccessToken(token);
74
+ if (!accessToken) {
75
+ throw new Error("MS Teams graph token unavailable");
76
+ }
77
+ return accessToken;
78
+ }
79
+
80
+ export async function listTeamsByName(token: string, query: string): Promise<GraphGroup[]> {
81
+ const escaped = escapeOData(query);
82
+ const filter = `resourceProvisioningOptions/Any(x:x eq 'Team') and startsWith(displayName,'${escaped}')`;
83
+ const path = `/groups?$filter=${encodeURIComponent(filter)}&$select=id,displayName`;
84
+ const res = await fetchGraphJson<GraphResponse<GraphGroup>>({ token, path });
85
+ return res.value ?? [];
86
+ }
87
+
88
+ export async function listChannelsForTeam(token: string, teamId: string): Promise<GraphChannel[]> {
89
+ const path = `/teams/${encodeURIComponent(teamId)}/channels?$select=id,displayName`;
90
+ const res = await fetchGraphJson<GraphResponse<GraphChannel>>({ token, path });
91
+ return res.value ?? [];
92
+ }
package/src/onboarding.ts CHANGED
@@ -63,6 +63,32 @@ function looksLikeGuid(value: string): boolean {
63
63
  return /^[0-9a-fA-F-]{16,}$/.test(value);
64
64
  }
65
65
 
66
+ async function promptMSTeamsCredentials(prompter: WizardPrompter): Promise<{
67
+ appId: string;
68
+ appPassword: string;
69
+ tenantId: string;
70
+ }> {
71
+ const appId = String(
72
+ await prompter.text({
73
+ message: "Enter MS Teams App ID",
74
+ validate: (value) => (value?.trim() ? undefined : "Required"),
75
+ }),
76
+ ).trim();
77
+ const appPassword = String(
78
+ await prompter.text({
79
+ message: "Enter MS Teams App Password",
80
+ validate: (value) => (value?.trim() ? undefined : "Required"),
81
+ }),
82
+ ).trim();
83
+ const tenantId = String(
84
+ await prompter.text({
85
+ message: "Enter MS Teams Tenant ID",
86
+ validate: (value) => (value?.trim() ? undefined : "Required"),
87
+ }),
88
+ ).trim();
89
+ return { appId, appPassword, tenantId };
90
+ }
91
+
66
92
  async function promptMSTeamsAllowFrom(params: {
67
93
  cfg: OpenClawConfig;
68
94
  prompter: WizardPrompter;
@@ -251,24 +277,7 @@ export const msteamsOnboardingAdapter: ChannelOnboardingAdapter = {
251
277
  },
252
278
  };
253
279
  } else {
254
- appId = String(
255
- await prompter.text({
256
- message: "Enter MS Teams App ID",
257
- validate: (value) => (value?.trim() ? undefined : "Required"),
258
- }),
259
- ).trim();
260
- appPassword = String(
261
- await prompter.text({
262
- message: "Enter MS Teams App Password",
263
- validate: (value) => (value?.trim() ? undefined : "Required"),
264
- }),
265
- ).trim();
266
- tenantId = String(
267
- await prompter.text({
268
- message: "Enter MS Teams Tenant ID",
269
- validate: (value) => (value?.trim() ? undefined : "Required"),
270
- }),
271
- ).trim();
280
+ ({ appId, appPassword, tenantId } = await promptMSTeamsCredentials(prompter));
272
281
  }
273
282
  } else if (hasConfigCreds) {
274
283
  const keep = await prompter.confirm({
@@ -276,44 +285,10 @@ export const msteamsOnboardingAdapter: ChannelOnboardingAdapter = {
276
285
  initialValue: true,
277
286
  });
278
287
  if (!keep) {
279
- appId = String(
280
- await prompter.text({
281
- message: "Enter MS Teams App ID",
282
- validate: (value) => (value?.trim() ? undefined : "Required"),
283
- }),
284
- ).trim();
285
- appPassword = String(
286
- await prompter.text({
287
- message: "Enter MS Teams App Password",
288
- validate: (value) => (value?.trim() ? undefined : "Required"),
289
- }),
290
- ).trim();
291
- tenantId = String(
292
- await prompter.text({
293
- message: "Enter MS Teams Tenant ID",
294
- validate: (value) => (value?.trim() ? undefined : "Required"),
295
- }),
296
- ).trim();
288
+ ({ appId, appPassword, tenantId } = await promptMSTeamsCredentials(prompter));
297
289
  }
298
290
  } else {
299
- appId = String(
300
- await prompter.text({
301
- message: "Enter MS Teams App ID",
302
- validate: (value) => (value?.trim() ? undefined : "Required"),
303
- }),
304
- ).trim();
305
- appPassword = String(
306
- await prompter.text({
307
- message: "Enter MS Teams App Password",
308
- validate: (value) => (value?.trim() ? undefined : "Required"),
309
- }),
310
- ).trim();
311
- tenantId = String(
312
- await prompter.text({
313
- message: "Enter MS Teams Tenant ID",
314
- validate: (value) => (value?.trim() ? undefined : "Required"),
315
- }),
316
- ).trim();
291
+ ({ appId, appPassword, tenantId } = await promptMSTeamsCredentials(prompter));
317
292
  }
318
293
 
319
294
  if (appId && appPassword && tenantId) {
package/src/policy.ts CHANGED
@@ -11,6 +11,7 @@ import type {
11
11
  import {
12
12
  buildChannelKeyCandidates,
13
13
  normalizeChannelSlug,
14
+ resolveAllowlistMatchSimple,
14
15
  resolveToolsBySender,
15
16
  resolveChannelEntryMatchWithFallback,
16
17
  resolveNestedAllowlistDecision,
@@ -209,24 +210,7 @@ export function resolveMSTeamsAllowlistMatch(params: {
209
210
  senderId: string;
210
211
  senderName?: string | null;
211
212
  }): MSTeamsAllowlistMatch {
212
- const allowFrom = params.allowFrom
213
- .map((entry) => String(entry).trim().toLowerCase())
214
- .filter(Boolean);
215
- if (allowFrom.length === 0) {
216
- return { allowed: false };
217
- }
218
- if (allowFrom.includes("*")) {
219
- return { allowed: true, matchKey: "*", matchSource: "wildcard" };
220
- }
221
- const senderId = params.senderId.toLowerCase();
222
- if (allowFrom.includes(senderId)) {
223
- return { allowed: true, matchKey: senderId, matchSource: "id" };
224
- }
225
- const senderName = params.senderName?.toLowerCase();
226
- if (senderName && allowFrom.includes(senderName)) {
227
- return { allowed: true, matchKey: senderName, matchSource: "name" };
228
- }
229
- return { allowed: false };
213
+ return resolveAllowlistMatchSimple(params);
230
214
  }
231
215
 
232
216
  export function resolveMSTeamsReplyPolicy(params: {
package/src/probe.ts CHANGED
@@ -1,11 +1,9 @@
1
- import type { MSTeamsConfig } from "openclaw/plugin-sdk";
1
+ import type { BaseProbeResult, MSTeamsConfig } from "openclaw/plugin-sdk";
2
2
  import { formatUnknownError } from "./errors.js";
3
3
  import { loadMSTeamsSdkWithAuth } from "./sdk.js";
4
4
  import { resolveMSTeamsCredentials } from "./token.js";
5
5
 
6
- export type ProbeMSTeamsResult = {
7
- ok: boolean;
8
- error?: string;
6
+ export type ProbeMSTeamsResult = BaseProbeResult<string> & {
9
7
  appId?: string;
10
8
  graph?: {
11
9
  ok: boolean;
@@ -1,26 +1,13 @@
1
- import type { MSTeamsConfig } from "openclaw/plugin-sdk";
2
- import { GRAPH_ROOT } from "./attachments/shared.js";
3
- import { loadMSTeamsSdkWithAuth } from "./sdk.js";
4
- import { resolveMSTeamsCredentials } from "./token.js";
5
-
6
- type GraphUser = {
7
- id?: string;
8
- displayName?: string;
9
- userPrincipalName?: string;
10
- mail?: string;
11
- };
12
-
13
- type GraphGroup = {
14
- id?: string;
15
- displayName?: string;
16
- };
17
-
18
- type GraphChannel = {
19
- id?: string;
20
- displayName?: string;
21
- };
22
-
23
- type GraphResponse<T> = { value?: T[] };
1
+ import {
2
+ escapeOData,
3
+ fetchGraphJson,
4
+ type GraphResponse,
5
+ type GraphUser,
6
+ listChannelsForTeam,
7
+ listTeamsByName,
8
+ normalizeQuery,
9
+ resolveGraphToken,
10
+ } from "./graph.js";
24
11
 
25
12
  export type MSTeamsChannelResolution = {
26
13
  input: string;
@@ -40,18 +27,6 @@ export type MSTeamsUserResolution = {
40
27
  note?: string;
41
28
  };
42
29
 
43
- function readAccessToken(value: unknown): string | null {
44
- if (typeof value === "string") {
45
- return value;
46
- }
47
- if (value && typeof value === "object") {
48
- const token =
49
- (value as { accessToken?: unknown }).accessToken ?? (value as { token?: unknown }).token;
50
- return typeof token === "string" ? token : null;
51
- }
52
- return null;
53
- }
54
-
55
30
  function stripProviderPrefix(raw: string): string {
56
31
  return raw.replace(/^(msteams|teams):/i, "");
57
32
  }
@@ -128,63 +103,6 @@ export function parseMSTeamsTeamEntry(
128
103
  };
129
104
  }
130
105
 
131
- function normalizeQuery(value?: string | null): string {
132
- return value?.trim() ?? "";
133
- }
134
-
135
- function escapeOData(value: string): string {
136
- return value.replace(/'/g, "''");
137
- }
138
-
139
- async function fetchGraphJson<T>(params: {
140
- token: string;
141
- path: string;
142
- headers?: Record<string, string>;
143
- }): Promise<T> {
144
- const res = await fetch(`${GRAPH_ROOT}${params.path}`, {
145
- headers: {
146
- Authorization: `Bearer ${params.token}`,
147
- ...params.headers,
148
- },
149
- });
150
- if (!res.ok) {
151
- const text = await res.text().catch(() => "");
152
- throw new Error(`Graph ${params.path} failed (${res.status}): ${text || "unknown error"}`);
153
- }
154
- return (await res.json()) as T;
155
- }
156
-
157
- async function resolveGraphToken(cfg: unknown): Promise<string> {
158
- const creds = resolveMSTeamsCredentials(
159
- (cfg as { channels?: { msteams?: unknown } })?.channels?.msteams as MSTeamsConfig | undefined,
160
- );
161
- if (!creds) {
162
- throw new Error("MS Teams credentials missing");
163
- }
164
- const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
165
- const tokenProvider = new sdk.MsalTokenProvider(authConfig);
166
- const token = await tokenProvider.getAccessToken("https://graph.microsoft.com");
167
- const accessToken = readAccessToken(token);
168
- if (!accessToken) {
169
- throw new Error("MS Teams graph token unavailable");
170
- }
171
- return accessToken;
172
- }
173
-
174
- async function listTeamsByName(token: string, query: string): Promise<GraphGroup[]> {
175
- const escaped = escapeOData(query);
176
- const filter = `resourceProvisioningOptions/Any(x:x eq 'Team') and startsWith(displayName,'${escaped}')`;
177
- const path = `/groups?$filter=${encodeURIComponent(filter)}&$select=id,displayName`;
178
- const res = await fetchGraphJson<GraphResponse<GraphGroup>>({ token, path });
179
- return res.value ?? [];
180
- }
181
-
182
- async function listChannelsForTeam(token: string, teamId: string): Promise<GraphChannel[]> {
183
- const path = `/teams/${encodeURIComponent(teamId)}/channels?$select=id,displayName`;
184
- const res = await fetchGraphJson<GraphResponse<GraphChannel>>({ token, path });
185
- return res.value ?? [];
186
- }
187
-
188
106
  export async function resolveMSTeamsChannelAllowlist(params: {
189
107
  cfg: unknown;
190
108
  entries: string[];