@openclaw/msteams 2026.2.13 → 2026.2.14
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 +6 -0
- package/package.json +2 -3
- package/src/directory-live.ts +13 -92
- package/src/file-lock.ts +1 -189
- package/src/graph.ts +92 -0
- package/src/resolve-allowlist.ts +10 -92
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/msteams",
|
|
3
|
-
"version": "2026.2.
|
|
3
|
+
"version": "2026.2.14",
|
|
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/directory-live.ts
CHANGED
|
@@ -1,95 +1,16 @@
|
|
|
1
|
-
import type { ChannelDirectoryEntry
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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/resolve-allowlist.ts
CHANGED
|
@@ -1,26 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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[];
|