@kodelyth/msteams 2026.5.42 → 2026.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/klaw.plugin.json +726 -2
- package/package.json +16 -4
- package/api.ts +0 -3
- package/channel-config-api.ts +0 -1
- package/channel-plugin-api.ts +0 -2
- package/config-api.ts +0 -4
- package/contract-api.ts +0 -4
- package/index.ts +0 -20
- package/runtime-api.ts +0 -66
- package/secret-contract-api.ts +0 -5
- package/setup-entry.ts +0 -13
- package/setup-plugin-api.ts +0 -3
- package/src/ai-entity.ts +0 -7
- package/src/approval-auth.ts +0 -44
- package/src/attachments/bot-framework.test.ts +0 -506
- package/src/attachments/bot-framework.ts +0 -348
- package/src/attachments/download.ts +0 -328
- package/src/attachments/graph.test.ts +0 -441
- package/src/attachments/graph.ts +0 -489
- package/src/attachments/html.ts +0 -122
- package/src/attachments/payload.ts +0 -14
- package/src/attachments/remote-media.test.ts +0 -187
- package/src/attachments/remote-media.ts +0 -86
- package/src/attachments/shared.test.ts +0 -547
- package/src/attachments/shared.ts +0 -655
- package/src/attachments/types.ts +0 -47
- package/src/attachments.graph.test.ts +0 -414
- package/src/attachments.helpers.test.ts +0 -245
- package/src/attachments.test-helpers.ts +0 -17
- package/src/attachments.test.ts +0 -754
- package/src/attachments.ts +0 -18
- package/src/block-streaming-config.test.ts +0 -61
- package/src/channel-api.ts +0 -1
- package/src/channel.actions.test.ts +0 -797
- package/src/channel.directory.test.ts +0 -176
- package/src/channel.message-adapter.test.ts +0 -227
- package/src/channel.runtime.ts +0 -56
- package/src/channel.setup.ts +0 -77
- package/src/channel.test.ts +0 -136
- package/src/channel.ts +0 -1176
- package/src/config-schema.ts +0 -6
- package/src/config-ui-hints.ts +0 -40
- package/src/conversation-store-fs.test.ts +0 -81
- package/src/conversation-store-fs.ts +0 -149
- package/src/conversation-store-helpers.test.ts +0 -202
- package/src/conversation-store-helpers.ts +0 -105
- package/src/conversation-store-memory.ts +0 -51
- package/src/conversation-store.shared.test.ts +0 -260
- package/src/conversation-store.ts +0 -71
- package/src/directory-live.test.ts +0 -156
- package/src/directory-live.ts +0 -111
- package/src/doctor.ts +0 -27
- package/src/errors.test.ts +0 -154
- package/src/errors.ts +0 -270
- package/src/feedback-reflection-prompt.ts +0 -117
- package/src/feedback-reflection-store.ts +0 -113
- package/src/feedback-reflection.test.ts +0 -237
- package/src/feedback-reflection.ts +0 -268
- package/src/file-consent-helpers.test.ts +0 -328
- package/src/file-consent-helpers.ts +0 -115
- package/src/file-consent-invoke.ts +0 -150
- package/src/file-consent.test.ts +0 -378
- package/src/file-consent.ts +0 -223
- package/src/graph-chat.ts +0 -36
- package/src/graph-group-management.test.ts +0 -332
- package/src/graph-group-management.ts +0 -168
- package/src/graph-members.test.ts +0 -89
- package/src/graph-members.ts +0 -48
- package/src/graph-messages.actions.test.ts +0 -253
- package/src/graph-messages.read.test.ts +0 -391
- package/src/graph-messages.search.test.ts +0 -227
- package/src/graph-messages.test-helpers.ts +0 -50
- package/src/graph-messages.ts +0 -534
- package/src/graph-teams.test.ts +0 -222
- package/src/graph-teams.ts +0 -114
- package/src/graph-thread.test.ts +0 -252
- package/src/graph-thread.ts +0 -146
- package/src/graph-upload.test.ts +0 -253
- package/src/graph-upload.ts +0 -531
- package/src/graph-users.ts +0 -29
- package/src/graph.test.ts +0 -540
- package/src/graph.ts +0 -308
- package/src/inbound.test.ts +0 -221
- package/src/inbound.ts +0 -148
- package/src/index.ts +0 -4
- package/src/media-helpers.test.ts +0 -220
- package/src/media-helpers.ts +0 -105
- package/src/mentions.test.ts +0 -254
- package/src/mentions.ts +0 -114
- package/src/messenger.test.ts +0 -961
- package/src/messenger.ts +0 -608
- package/src/monitor-handler/access.ts +0 -136
- package/src/monitor-handler/inbound-media.test.ts +0 -314
- package/src/monitor-handler/inbound-media.ts +0 -180
- package/src/monitor-handler/message-handler-mock-support.test-support.ts +0 -28
- package/src/monitor-handler/message-handler.authz.test.ts +0 -739
- package/src/monitor-handler/message-handler.dm-media.test.ts +0 -54
- package/src/monitor-handler/message-handler.test-support.ts +0 -99
- package/src/monitor-handler/message-handler.thread-parent.test.ts +0 -225
- package/src/monitor-handler/message-handler.thread-session.test.ts +0 -132
- package/src/monitor-handler/message-handler.ts +0 -1003
- package/src/monitor-handler/reaction-handler.test.ts +0 -325
- package/src/monitor-handler/reaction-handler.ts +0 -122
- package/src/monitor-handler/thread-session.ts +0 -30
- package/src/monitor-handler.adaptive-card.test.ts +0 -158
- package/src/monitor-handler.feedback-authz.test.ts +0 -357
- package/src/monitor-handler.file-consent.test.ts +0 -443
- package/src/monitor-handler.sso.test.ts +0 -576
- package/src/monitor-handler.test-helpers.ts +0 -181
- package/src/monitor-handler.ts +0 -538
- package/src/monitor-handler.types.ts +0 -27
- package/src/monitor-types.ts +0 -6
- package/src/monitor.lifecycle.test.ts +0 -457
- package/src/monitor.test.ts +0 -119
- package/src/monitor.ts +0 -476
- package/src/oauth.flow.ts +0 -77
- package/src/oauth.shared.ts +0 -37
- package/src/oauth.test.ts +0 -350
- package/src/oauth.token.ts +0 -162
- package/src/oauth.ts +0 -130
- package/src/outbound.test.ts +0 -400
- package/src/outbound.ts +0 -198
- package/src/pending-uploads-fs.test.ts +0 -261
- package/src/pending-uploads-fs.ts +0 -235
- package/src/pending-uploads.test.ts +0 -186
- package/src/pending-uploads.ts +0 -121
- package/src/policy.test.ts +0 -156
- package/src/policy.ts +0 -245
- package/src/polls-store-memory.ts +0 -32
- package/src/polls.test.ts +0 -169
- package/src/polls.ts +0 -312
- package/src/presentation.ts +0 -93
- package/src/probe.test.ts +0 -79
- package/src/probe.ts +0 -132
- package/src/reply-dispatcher.test.ts +0 -543
- package/src/reply-dispatcher.ts +0 -523
- package/src/reply-stream-controller.test.ts +0 -424
- package/src/reply-stream-controller.ts +0 -334
- package/src/resolve-allowlist.test.ts +0 -253
- package/src/resolve-allowlist.ts +0 -309
- package/src/revoked-context.ts +0 -17
- package/src/runtime.ts +0 -12
- package/src/sdk-types.ts +0 -59
- package/src/sdk.test.ts +0 -727
- package/src/sdk.ts +0 -916
- package/src/secret-contract.ts +0 -49
- package/src/secret-input.ts +0 -7
- package/src/send-context.test.ts +0 -93
- package/src/send-context.ts +0 -269
- package/src/send.test.ts +0 -588
- package/src/send.ts +0 -697
- package/src/sent-message-cache.test.ts +0 -106
- package/src/sent-message-cache.ts +0 -174
- package/src/session-route.ts +0 -40
- package/src/setup-core.ts +0 -162
- package/src/setup-surface.test.ts +0 -175
- package/src/setup-surface.ts +0 -319
- package/src/sso-token-store.test.ts +0 -74
- package/src/sso-token-store.ts +0 -166
- package/src/sso.ts +0 -300
- package/src/storage.ts +0 -25
- package/src/store-fs.ts +0 -42
- package/src/streaming-message.test.ts +0 -323
- package/src/streaming-message.ts +0 -327
- package/src/test-runtime.ts +0 -16
- package/src/thread-parent-context.test.ts +0 -224
- package/src/thread-parent-context.ts +0 -159
- package/src/token-response.ts +0 -11
- package/src/token.test.ts +0 -268
- package/src/token.ts +0 -194
- package/src/user-agent.test.ts +0 -121
- package/src/user-agent.ts +0 -53
- package/src/webhook-timeouts.ts +0 -27
- package/src/welcome-card.test.ts +0 -104
- package/src/welcome-card.ts +0 -57
- package/test-api.ts +0 -1
- package/tsconfig.json +0 -16
package/src/setup-surface.ts
DELETED
|
@@ -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
|
-
});
|
package/src/sso-token-store.ts
DELETED
|
@@ -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
|
-
}
|