@openclaw/msteams 2026.5.7 → 2026.5.10-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.
@@ -1,9 +1,10 @@
1
- import { A as resolveDmGroupAccessWithLists, F as summarizeMapping, L as getMSTeamsRuntime, N as resolveSenderScopedGroupPolicy, O as resolveChannelMediaMaxBytes, R as getOptionalMSTeamsRuntime, S as mergeAllowlist, T as readStoreAllowFromForDmPolicy, a as buildMediaPayload, c as createChannelPairingController, f as dispatchReplyFromConfigWithSettledDispatcher$1, j as resolveEffectiveAllowFromLists, k as resolveDefaultGroupPolicy, l as createChannelReplyPipeline, n as DEFAULT_WEBHOOK_MAX_BODY_BYTES, p as evaluateSenderGroupAccessForPolicy$1, t as DEFAULT_ACCOUNT_ID, v as isDangerousNameMatchingEnabled, x as logTypingFailure, y as keepHttpServerTaskAlive } from "./runtime-api-DV1iVMn1.js";
2
- import { A as ATTACHMENT_TAG_RE, B as isLikelyImageAttachment, C as loadMSTeamsSdkWithAuth, D as formatMSTeamsSendErrorHint, E as classifyMSTeamsSendError, F as estimateBase64DecodedBytes, G as resolveAttachmentFetchPolicy, H as isUrlAllowed, I as extractHtmlFromAttachment, J as safeFetchWithPolicy, K as resolveMediaSsrfPolicy, L as extractInlineImageCandidates, M as IMG_SRC_RE, N as applyAuthorizationHeaderForUrl, O as formatUnknownError, P as encodeGraphShareId, R as inferPlaceholder, S as createMSTeamsTokenProvider, T as ensureUserAgentHeader, U as normalizeContentType, V as isRecord, W as readNestedString, X as tryBuildGraphSharesUrlForSharedLink, Y as safeHostForUrl, _ as resolveMSTeamsStorePath, a as fetchGraphJson, b as createBotFrameworkJwtValidator, h as resolveMSTeamsCredentials, j as GRAPH_ROOT, q as resolveRequestUrl, w as buildUserAgent, x as createMSTeamsAdapter, z as isDownloadableAttachment } from "./graph-users-9uQJepqr.js";
3
- import { c as resolveMSTeamsUserAllowlist, s as resolveMSTeamsChannelAllowlist } from "./resolve-allowlist-D41JSziq.js";
4
- import { a as resolveMSTeamsRouteConfig, i as resolveMSTeamsReplyPolicy, n as resolveMSTeamsAllowlistMatch, t as isMSTeamsGroupAllowed } from "./policy-DTnU2GR7.js";
5
- import { C as readJsonFile, S as createMSTeamsConversationStoreFs, T as writeJsonFile, _ as buildFileInfoCard, b as createMSTeamsPollStoreFs, c as renderReplyPayloadsToMessages, d as withRevokedProxyFallback, f as resolveGraphChatId, g as removePendingUploadFs, h as getPendingUploadFs, l as sendMSTeamsMessages, m as removePendingUpload, p as getPendingUpload, s as buildConversationReference, u as AI_GENERATED_ENTITY, v as parseFileConsentInvoke, w as withFileLock, x as extractMSTeamsPollVote, y as uploadToConsentUrl } from "./probe-D_H8yFps.js";
1
+ import { A as summarizeMapping, D as resolveDefaultGroupPolicy, E as resolveChannelMediaMaxBytes, M as getMSTeamsRuntime, N as getOptionalMSTeamsRuntime, _ as isDangerousNameMatchingEnabled, a as buildMediaPayload, b as logTypingFailure, c as createChannelMessageReplyPipeline, f as dispatchReplyFromConfigWithSettledDispatcher$1, l as createChannelPairingController, n as DEFAULT_WEBHOOK_MAX_BODY_BYTES, t as DEFAULT_ACCOUNT_ID, v as keepHttpServerTaskAlive, x as mergeAllowlist } from "./runtime-api-C3EIaIpt.js";
2
+ import { A as ATTACHMENT_TAG_RE, B as isLikelyImageAttachment, C as loadMSTeamsSdkWithAuth, D as formatMSTeamsSendErrorHint, E as classifyMSTeamsSendError, F as estimateBase64DecodedBytes, G as resolveAttachmentFetchPolicy, H as isUrlAllowed, I as extractHtmlFromAttachment, J as safeFetchWithPolicy, K as resolveMediaSsrfPolicy, L as extractInlineImageCandidates, M as IMG_SRC_RE, N as applyAuthorizationHeaderForUrl, O as formatUnknownError, P as encodeGraphShareId, R as inferPlaceholder, S as createMSTeamsTokenProvider, T as ensureUserAgentHeader, U as normalizeContentType, V as isRecord$1, W as readNestedString, X as tryBuildGraphSharesUrlForSharedLink, Y as safeHostForUrl, _ as resolveMSTeamsStorePath, a as fetchGraphJson, b as createBotFrameworkJwtValidator, h as resolveMSTeamsCredentials, j as GRAPH_ROOT, q as resolveRequestUrl, w as buildUserAgent, x as createMSTeamsAdapter, z as isDownloadableAttachment } from "./graph-users-CCU0WVMZ.js";
3
+ import { c as resolveMSTeamsUserAllowlist, s as resolveMSTeamsChannelAllowlist } from "./resolve-allowlist-7CHP2hEA.js";
4
+ import { i as resolveMSTeamsRouteConfig, r as resolveMSTeamsReplyPolicy, t as resolveMSTeamsAllowlistMatch } from "./policy-bM71GXRd.js";
5
+ import { C as readJsonFile, S as createMSTeamsConversationStoreFs, T as writeJsonFile, _ as buildFileInfoCard, b as createMSTeamsPollStoreFs, c as renderReplyPayloadsToMessages, d as withRevokedProxyFallback, f as resolveGraphChatId, g as removePendingUploadFs, h as getPendingUploadFs, l as sendMSTeamsMessages, m as removePendingUpload, p as getPendingUpload, s as buildConversationReference, u as AI_GENERATED_ENTITY, v as parseFileConsentInvoke, w as withFileLock, x as extractMSTeamsPollVote, y as uploadToConsentUrl } from "./probe-CQKmxtlj.js";
6
6
  import { formatAllowlistMatchMeta } from "openclaw/plugin-sdk/allow-from";
7
+ import { createLiveMessageState, createPreviewMessageReceipt, defineFinalizableLivePreviewAdapter, deliverWithFinalizableLivePreviewAdapter, markLiveMessageFinalized } from "openclaw/plugin-sdk/channel-message";
7
8
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, readStringValue } from "openclaw/plugin-sdk/text-runtime";
8
9
  import { createDraftStreamLoop } from "openclaw/plugin-sdk/channel-lifecycle";
9
10
  import { readResponseWithLimit } from "openclaw/plugin-sdk/media-runtime";
@@ -11,14 +12,15 @@ import { dispatchReplyFromConfigWithSettledDispatcher, hasFinalInboundReplyDispa
11
12
  import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
12
13
  import { Buffer as Buffer$1 } from "node:buffer";
13
14
  import path from "node:path";
14
- import fs from "node:fs/promises";
15
+ import { appendRegularFile } from "openclaw/plugin-sdk/security-runtime";
16
+ import { writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
15
17
  import { resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
18
+ import fs from "node:fs/promises";
19
+ import { channelIngressRoutes, resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
16
20
  import { logInboundDrop, resolveInboundMentionDecision, resolveInboundSessionEnvelopeContext } from "openclaw/plugin-sdk/channel-inbound";
17
- import { resolveDualTextControlCommandGate } from "openclaw/plugin-sdk/command-gating";
18
21
  import { filterSupplementalContextItems, resolveChannelContextVisibilityMode, shouldIncludeSupplementalContext } from "openclaw/plugin-sdk/context-visibility-runtime";
19
- import { evaluateSenderGroupAccessForPolicy } from "openclaw/plugin-sdk/group-access";
20
22
  import { DEFAULT_GROUP_HISTORY_LIMIT, buildPendingHistoryContextFromMap, recordPendingHistoryEntryIfEnabled } from "openclaw/plugin-sdk/reply-history";
21
- import { createChannelProgressDraftGate, formatChannelProgressDraftLine, formatChannelProgressDraftLineForEntry, formatChannelProgressDraftText, isChannelProgressDraftWorkToolName, resolveChannelPreviewStreamMode, resolveChannelProgressDraftMaxLines, resolveChannelStreamingBlockEnabled, resolveChannelStreamingPreviewToolProgress } from "openclaw/plugin-sdk/channel-streaming";
23
+ import { buildChannelProgressDraftLine, buildChannelProgressDraftLineForEntry, createChannelProgressDraftGate, formatChannelProgressDraftText, isChannelProgressDraftWorkToolName, resolveChannelPreviewStreamMode, resolveChannelProgressDraftMaxLines, resolveChannelStreamingBlockEnabled, resolveChannelStreamingPreviewToolProgress } from "openclaw/plugin-sdk/channel-streaming";
22
24
  //#region extensions/msteams/src/feedback-reflection-prompt.ts
23
25
  /** Max chars of the thumbed-down response to include in the reflection prompt. */
24
26
  const MAX_RESPONSE_CHARS = 500;
@@ -126,8 +128,7 @@ async function storeSessionLearning(params) {
126
128
  let learnings = exists ? existingLearnings : legacyLearnings;
127
129
  learnings.push(params.learning);
128
130
  if (learnings.length > 10) learnings = learnings.slice(-10);
129
- await fs.mkdir(path.dirname(learningsFile), { recursive: true });
130
- await fs.writeFile(learningsFile, JSON.stringify(learnings, null, 2), "utf-8");
131
+ await writeJsonFileAtomically(learningsFile, learnings);
131
132
  if (!exists && legacyLearningsFile !== learningsFile) await fs.rm(legacyLearningsFile, { force: true }).catch(() => void 0);
132
133
  }
133
134
  //#endregion
@@ -490,6 +491,22 @@ async function respondToMSTeamsFileConsentInvoke(context, log) {
490
491
  }
491
492
  //#endregion
492
493
  //#region extensions/msteams/src/monitor-handler/access.ts
494
+ const msteamsIngressIdentity = {
495
+ key: "sender-id",
496
+ normalize: normalizeIngressValue,
497
+ aliases: [{
498
+ key: "sender-name",
499
+ kind: "plugin:msteams-sender-name",
500
+ normalizeEntry: normalizeIngressValue,
501
+ normalizeSubject: normalizeIngressValue,
502
+ dangerous: true
503
+ }],
504
+ isWildcardEntry: (entry) => normalizeIngressValue(entry) === "*",
505
+ resolveEntryId: ({ entryIndex, fieldKey }) => `msteams-entry-${entryIndex + 1}:${fieldKey === "sender-name" ? "name" : "id"}`
506
+ };
507
+ function normalizeIngressValue(value) {
508
+ return normalizeOptionalLowercaseString(value) ?? null;
509
+ }
493
510
  async function resolveMSTeamsSenderAccess(params) {
494
511
  const activity = params.activity;
495
512
  const msteamsCfg = params.cfg.channels?.msteams;
@@ -504,23 +521,10 @@ async function resolveMSTeamsSenderAccess(params) {
504
521
  accountId: DEFAULT_ACCOUNT_ID
505
522
  });
506
523
  const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing";
507
- const storedAllowFrom = await readStoreAllowFromForDmPolicy({
508
- provider: "msteams",
509
- accountId: pairing.accountId,
510
- dmPolicy,
511
- readStore: pairing.readStoreForDmPolicy
512
- });
513
524
  const configuredDmAllowFrom = msteamsCfg?.allowFrom ?? [];
514
525
  const groupAllowFrom = msteamsCfg?.groupAllowFrom;
515
- const resolvedAllowFromLists = resolveEffectiveAllowFromLists({
516
- allowFrom: configuredDmAllowFrom,
517
- groupAllowFrom,
518
- storeAllowFrom: storedAllowFrom,
519
- dmPolicy
520
- });
521
526
  const defaultGroupPolicy = resolveDefaultGroupPolicy(params.cfg);
522
527
  const groupPolicy = !isDirectMessage && msteamsCfg ? msteamsCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist" : "disabled";
523
- const effectiveGroupAllowFrom = resolvedAllowFromLists.effectiveGroupAllowFrom;
524
528
  const allowNameMatching = isDangerousNameMatchingEnabled(msteamsCfg);
525
529
  const channelGate = resolveMSTeamsRouteConfig({
526
530
  cfg: msteamsCfg,
@@ -530,49 +534,55 @@ async function resolveMSTeamsSenderAccess(params) {
530
534
  channelName: activity.channelData?.channel?.name,
531
535
  allowNameMatching
532
536
  });
533
- const senderGroupPolicy = channelGate.allowlistConfigured && effectiveGroupAllowFrom.length === 0 ? groupPolicy : resolveSenderScopedGroupPolicy({
534
- groupPolicy,
535
- groupAllowFrom: effectiveGroupAllowFrom
536
- });
537
- const access = resolveDmGroupAccessWithLists({
538
- isGroup: !isDirectMessage,
539
- dmPolicy,
540
- groupPolicy: senderGroupPolicy,
541
- allowFrom: configuredDmAllowFrom,
542
- groupAllowFrom,
543
- storeAllowFrom: storedAllowFrom,
544
- groupAllowFromFallbackToAllowFrom: false,
545
- isSenderAllowed: (allowFrom) => resolveMSTeamsAllowlistMatch({
546
- allowFrom,
547
- senderId,
548
- senderName,
549
- allowNameMatching
550
- }).allowed
551
- });
552
537
  return {
553
- msteamsCfg,
538
+ ...await resolveStableChannelMessageIngress({
539
+ channelId: "msteams",
540
+ accountId: pairing.accountId,
541
+ identity: msteamsIngressIdentity,
542
+ cfg: params.cfg,
543
+ readStoreAllowFrom: pairing.readAllowFromStore,
544
+ subject: {
545
+ stableId: senderId,
546
+ aliases: { "sender-name": senderName }
547
+ },
548
+ conversation: {
549
+ kind: isDirectMessage ? "direct" : convType === "channel" ? "channel" : "group",
550
+ id: conversationId,
551
+ parentId: activity.channelData?.team?.id
552
+ },
553
+ route: channelIngressRoutes(!isDirectMessage && channelGate.allowlistConfigured && {
554
+ id: "msteams:team-channel",
555
+ kind: "nestedAllowlist",
556
+ allowed: channelGate.allowed,
557
+ precedence: 0,
558
+ matchId: "msteams-route",
559
+ ...channelGate.allowed && groupPolicy === "allowlist" ? {
560
+ senderPolicy: "deny-when-empty",
561
+ senderAllowFromSource: "effective-group"
562
+ } : {}
563
+ }),
564
+ dmPolicy,
565
+ groupPolicy,
566
+ policy: {
567
+ groupAllowFromFallbackToAllowFrom: true,
568
+ mutableIdentifierMatching: allowNameMatching ? "enabled" : "disabled"
569
+ },
570
+ allowFrom: configuredDmAllowFrom,
571
+ groupAllowFrom,
572
+ command: {
573
+ allowTextCommands: true,
574
+ hasControlCommand: params.hasControlCommand === true,
575
+ directGroupAllowFrom: isDirectMessage ? "effective" : "none"
576
+ }
577
+ }),
554
578
  pairing,
555
579
  isDirectMessage,
556
580
  conversationId,
557
581
  senderId,
558
582
  senderName,
583
+ msteamsCfg,
559
584
  dmPolicy,
560
585
  channelGate,
561
- access,
562
- senderGroupAccess: evaluateSenderGroupAccessForPolicy$1({
563
- groupPolicy,
564
- groupAllowFrom: effectiveGroupAllowFrom,
565
- senderId,
566
- isSenderAllowed: (_senderId, allowFrom) => resolveMSTeamsAllowlistMatch({
567
- allowFrom,
568
- senderId,
569
- senderName,
570
- allowNameMatching
571
- }).allowed
572
- }),
573
- configuredDmAllowFrom,
574
- effectiveDmAllowFrom: access.effectiveAllowFrom,
575
- effectiveGroupAllowFrom,
576
586
  allowNameMatching,
577
587
  groupPolicy
578
588
  };
@@ -842,7 +852,7 @@ function resolveDownloadCandidate(att) {
842
852
  const contentType = normalizeContentType(att.contentType);
843
853
  const name = normalizeOptionalString(att.name) ?? "";
844
854
  if (contentType === "application/vnd.microsoft.teams.file.download.info") {
845
- if (!isRecord(att.content)) return null;
855
+ if (!isRecord$1(att.content)) return null;
846
856
  const downloadUrl = normalizeOptionalString(att.content.downloadUrl) ?? "";
847
857
  if (!downloadUrl) return null;
848
858
  const fileType = normalizeOptionalString(att.content.fileType) ?? "";
@@ -1612,6 +1622,7 @@ var TeamsHttpStream = class {
1612
1622
  this.finalized = false;
1613
1623
  this.streamFailed = false;
1614
1624
  this.lastStreamedText = "";
1625
+ this.finalMessageId = void 0;
1615
1626
  this.streamStartedAt = void 0;
1616
1627
  this.sendActivity = options.sendActivity;
1617
1628
  this.feedbackLoopEnabled = options.feedbackLoopEnabled ?? false;
@@ -1678,22 +1689,23 @@ var TeamsHttpStream = class {
1678
1689
  * Finalize the stream — send the final message activity.
1679
1690
  */
1680
1691
  async finalize() {
1681
- if (this.finalized) return;
1692
+ if (this.finalized) return this.finalMessageId;
1682
1693
  this.finalized = true;
1683
1694
  this.stopped = true;
1684
1695
  this.loop.stop();
1685
1696
  await this.loop.waitForInFlight();
1686
- if (!this.accumulatedText.trim()) return;
1697
+ if (!this.accumulatedText.trim()) return this.finalMessageId;
1687
1698
  if (this.streamFailed) {
1688
1699
  if (this.streamId) try {
1689
- await this.sendActivity({
1700
+ const response = await this.sendActivity({
1690
1701
  type: "message",
1691
1702
  text: this.lastStreamedText || "",
1692
1703
  channelData: { feedbackLoopEnabled: this.feedbackLoopEnabled },
1693
1704
  entities: [AI_GENERATED_ENTITY, buildStreamInfoEntity(this.streamId, "final")]
1694
1705
  });
1706
+ this.finalMessageId = extractId(response);
1695
1707
  } catch {}
1696
- return;
1708
+ return this.finalMessageId;
1697
1709
  }
1698
1710
  try {
1699
1711
  const entities = [AI_GENERATED_ENTITY];
@@ -1704,11 +1716,13 @@ var TeamsHttpStream = class {
1704
1716
  channelData: { feedbackLoopEnabled: this.feedbackLoopEnabled },
1705
1717
  entities
1706
1718
  };
1707
- await this.sendActivity(finalActivity);
1719
+ const response = await this.sendActivity(finalActivity);
1720
+ this.finalMessageId = extractId(response);
1708
1721
  } catch (err) {
1709
1722
  this.streamFailed = true;
1710
1723
  this.onError?.(err);
1711
1724
  }
1725
+ return this.finalMessageId;
1712
1726
  }
1713
1727
  /** Whether streaming successfully delivered content (at least one chunk sent, not failed). */
1714
1728
  get hasContent() {
@@ -1726,6 +1740,14 @@ var TeamsHttpStream = class {
1726
1740
  get isFinalized() {
1727
1741
  return this.finalized;
1728
1742
  }
1743
+ /** Platform id returned by the final message activity, when available. */
1744
+ get messageId() {
1745
+ return this.finalMessageId;
1746
+ }
1747
+ /** Stream id returned by the first streaminfo activity, when available. */
1748
+ get previewStreamId() {
1749
+ return this.streamId;
1750
+ }
1729
1751
  /** Whether streaming fell back (not used in this implementation). */
1730
1752
  get isFallback() {
1731
1753
  return false;
@@ -1781,6 +1803,13 @@ function createTeamsReplyStreamController(params) {
1781
1803
  let progressLines = [];
1782
1804
  let lastInformativeText = "";
1783
1805
  let pendingFinalize;
1806
+ let liveState = createLiveMessageState({ canFinalizeInPlace: Boolean(stream) });
1807
+ const markStreamFinalized = () => {
1808
+ if (!stream || stream.isFailed) return;
1809
+ const messageId = stream.messageId ?? stream.previewStreamId;
1810
+ if (!messageId) return;
1811
+ liveState = markLiveMessageFinalized(liveState, createPreviewMessageReceipt({ id: messageId }));
1812
+ };
1784
1813
  const renderInformativeUpdate = async () => {
1785
1814
  if (!stream) return;
1786
1815
  const informativeText = formatChannelProgressDraftText({
@@ -1806,9 +1835,12 @@ function createTeamsReplyStreamController(params) {
1806
1835
  if (!stream || streamMode !== "progress") return;
1807
1836
  if (options?.toolName !== void 0 && !isChannelProgressDraftWorkToolName(options.toolName)) return;
1808
1837
  if (shouldStreamPreviewToolProgress) {
1809
- const normalized = line?.replace(/\s+/g, " ").trim();
1838
+ const normalized = normalizeProgressLineIdentity(line);
1810
1839
  if (normalized) {
1811
- if (progressLines.at(-1) !== normalized) progressLines = [...progressLines, normalized].slice(-resolveChannelProgressDraftMaxLines(params.msteamsConfig));
1840
+ if (normalizeProgressLineIdentity(progressLines.at(-1)) !== normalized) {
1841
+ const progressLine = typeof line === "object" && line !== void 0 ? line : normalized;
1842
+ progressLines = [...progressLines, progressLine].slice(-resolveChannelProgressDraftMaxLines(params.msteamsConfig));
1843
+ }
1812
1844
  }
1813
1845
  }
1814
1846
  await noteProgressWork();
@@ -1827,6 +1859,39 @@ function createTeamsReplyStreamController(params) {
1827
1859
  text: remainingText
1828
1860
  };
1829
1861
  };
1862
+ const finalizeProgressPayload = async (payload, hasMedia) => {
1863
+ if (!stream || !payload.text) return payload;
1864
+ return (await deliverWithFinalizableLivePreviewAdapter({
1865
+ kind: "final",
1866
+ payload,
1867
+ liveState,
1868
+ adapter: defineFinalizableLivePreviewAdapter({
1869
+ draft: {
1870
+ flush: async () => {},
1871
+ clear: async () => {},
1872
+ id: () => stream.previewStreamId
1873
+ },
1874
+ buildFinalEdit: (candidate) => candidate.text ? { text: candidate.text } : void 0,
1875
+ editFinal: async (_previewId, edit) => {
1876
+ const finalized = await stream.replaceInformativeWithFinal(edit.text);
1877
+ informativeUpdateSent = false;
1878
+ if (!finalized || stream.isFailed) throw new Error("Teams progress stream finalization failed");
1879
+ },
1880
+ resolveFinalizedId: (previewId) => stream.messageId ?? stream.previewStreamId ?? previewId,
1881
+ createPreviewReceipt: (id) => createPreviewMessageReceipt({ id }),
1882
+ onPreviewFinalized: (_id, _receipt, state) => {
1883
+ liveState = state;
1884
+ },
1885
+ logPreviewEditFailure: (err) => {
1886
+ params.log.debug?.(`stream finalization failed: ${formatUnknownError(err)}`);
1887
+ }
1888
+ }),
1889
+ deliverNormally: async () => false
1890
+ })).kind === "preview-finalized" ? hasMedia ? {
1891
+ ...payload,
1892
+ text: void 0
1893
+ } : void 0 : payload;
1894
+ };
1830
1895
  return {
1831
1896
  async onReplyStart() {},
1832
1897
  async noteProgressWork(options) {
@@ -1851,13 +1916,7 @@ function createTeamsReplyStreamController(params) {
1851
1916
  const hasMedia = Boolean(payload.mediaUrl || payload.mediaUrls?.length);
1852
1917
  if (stream && streamMode === "progress" && informativeUpdateSent && !stream.isFinalized) {
1853
1918
  if (!payload.text) return payload;
1854
- const finalized = await stream.replaceInformativeWithFinal(payload.text);
1855
- informativeUpdateSent = false;
1856
- if (!finalized || stream.isFailed) return payload;
1857
- return hasMedia ? {
1858
- ...payload,
1859
- text: void 0
1860
- } : void 0;
1919
+ return await finalizeProgressPayload(payload, hasMedia);
1861
1920
  }
1862
1921
  if (!stream || !streamReceivedTokens) return payload;
1863
1922
  if (stream.isFailed) {
@@ -1866,7 +1925,9 @@ function createTeamsReplyStreamController(params) {
1866
1925
  }
1867
1926
  if (!stream.hasContent || stream.isFinalized) return payload;
1868
1927
  streamReceivedTokens = false;
1869
- pendingFinalize = stream.finalize();
1928
+ pendingFinalize = stream.finalize().then(() => {
1929
+ markStreamFinalized();
1930
+ });
1870
1931
  if (!hasMedia) return;
1871
1932
  return {
1872
1933
  ...payload,
@@ -1876,11 +1937,17 @@ function createTeamsReplyStreamController(params) {
1876
1937
  async finalize() {
1877
1938
  progressDraftGate.cancel();
1878
1939
  await pendingFinalize;
1879
- await stream?.finalize();
1940
+ if (!pendingFinalize) {
1941
+ await stream?.finalize();
1942
+ markStreamFinalized();
1943
+ }
1880
1944
  },
1881
1945
  hasStream() {
1882
1946
  return Boolean(stream);
1883
1947
  },
1948
+ liveState() {
1949
+ return liveState;
1950
+ },
1884
1951
  /**
1885
1952
  * Whether the Teams streaming card is currently receiving LLM tokens.
1886
1953
  * Used to gate side-channel keepalive activity so we don't overlay plain
@@ -1905,6 +1972,9 @@ function createTeamsReplyStreamController(params) {
1905
1972
  }
1906
1973
  };
1907
1974
  }
1975
+ function normalizeProgressLineIdentity(line) {
1976
+ return (typeof line === "string" ? line : line?.text)?.replace(/\s+/g, " ").trim() ?? "";
1977
+ }
1908
1978
  //#endregion
1909
1979
  //#region extensions/msteams/src/reply-dispatcher.ts
1910
1980
  function createMSTeamsReplyDispatcher(params) {
@@ -1952,7 +2022,7 @@ function createMSTeamsReplyDispatcher(params) {
1952
2022
  if (streamActiveRef.current()) return;
1953
2023
  await rawSendTypingIndicator();
1954
2024
  } : async () => {};
1955
- const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelReplyPipeline({
2025
+ const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelMessageReplyPipeline({
1956
2026
  cfg: params.cfg,
1957
2027
  agentId: params.agentId,
1958
2028
  channel: "msteams",
@@ -2145,7 +2215,7 @@ function createMSTeamsReplyDispatcher(params) {
2145
2215
  ...streamController.shouldSuppressDefaultToolProgressMessages() ? { suppressDefaultToolProgressMessages: true } : {},
2146
2216
  ...streamController.shouldStreamPreviewToolProgress() ? {
2147
2217
  onToolStart: async (payload) => {
2148
- await streamController.pushProgressLine(formatChannelProgressDraftLineForEntry(msteamsCfg, {
2218
+ await streamController.pushProgressLine(buildChannelProgressDraftLineForEntry(msteamsCfg, {
2149
2219
  event: "tool",
2150
2220
  name: payload.name,
2151
2221
  phase: payload.phase,
@@ -2153,7 +2223,7 @@ function createMSTeamsReplyDispatcher(params) {
2153
2223
  }, payload.detailMode ? { detailMode: payload.detailMode } : void 0), { toolName: payload.name });
2154
2224
  },
2155
2225
  onItemEvent: async (payload) => {
2156
- await streamController.pushProgressLine(formatChannelProgressDraftLineForEntry(msteamsCfg, {
2226
+ await streamController.pushProgressLine(buildChannelProgressDraftLineForEntry(msteamsCfg, {
2157
2227
  event: "item",
2158
2228
  itemKind: payload.kind,
2159
2229
  title: payload.title,
@@ -2167,7 +2237,7 @@ function createMSTeamsReplyDispatcher(params) {
2167
2237
  },
2168
2238
  onPlanUpdate: async (payload) => {
2169
2239
  if (payload.phase !== "update") return;
2170
- await streamController.pushProgressLine(formatChannelProgressDraftLine({
2240
+ await streamController.pushProgressLine(buildChannelProgressDraftLine({
2171
2241
  event: "plan",
2172
2242
  phase: payload.phase,
2173
2243
  title: payload.title,
@@ -2177,7 +2247,7 @@ function createMSTeamsReplyDispatcher(params) {
2177
2247
  },
2178
2248
  onApprovalEvent: async (payload) => {
2179
2249
  if (payload.phase !== "requested") return;
2180
- await streamController.pushProgressLine(formatChannelProgressDraftLine({
2250
+ await streamController.pushProgressLine(buildChannelProgressDraftLine({
2181
2251
  event: "approval",
2182
2252
  phase: payload.phase,
2183
2253
  title: payload.title,
@@ -2188,7 +2258,7 @@ function createMSTeamsReplyDispatcher(params) {
2188
2258
  },
2189
2259
  onCommandOutput: async (payload) => {
2190
2260
  if (payload.phase !== "end") return;
2191
- await streamController.pushProgressLine(formatChannelProgressDraftLine({
2261
+ await streamController.pushProgressLine(buildChannelProgressDraftLine({
2192
2262
  event: "command-output",
2193
2263
  phase: payload.phase,
2194
2264
  title: payload.title,
@@ -2199,7 +2269,7 @@ function createMSTeamsReplyDispatcher(params) {
2199
2269
  },
2200
2270
  onPatchSummary: async (payload) => {
2201
2271
  if (payload.phase !== "end") return;
2202
- await streamController.pushProgressLine(formatChannelProgressDraftLine({
2272
+ await streamController.pushProgressLine(buildChannelProgressDraftLine({
2203
2273
  event: "patch",
2204
2274
  phase: payload.phase,
2205
2275
  title: payload.title,
@@ -2430,13 +2500,29 @@ function extractTextFromHtmlAttachments(attachments) {
2430
2500
  for (const attachment of attachments) {
2431
2501
  if (attachment.contentType !== "text/html") continue;
2432
2502
  const content = attachment.content;
2433
- const raw = typeof content === "string" ? content : isRecord(content) && typeof content.text === "string" ? content.text : isRecord(content) && typeof content.body === "string" ? content.body : "";
2503
+ const raw = typeof content === "string" ? content : isRecord$1(content) && typeof content.text === "string" ? content.text : isRecord$1(content) && typeof content.body === "string" ? content.body : "";
2434
2504
  if (!raw) continue;
2435
2505
  const text = raw.replace(/<at[^>]*>.*?<\/at>/gis, " ").replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gis, "$2 $1").replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/\s+/g, " ").trim();
2436
2506
  if (text) return text;
2437
2507
  }
2438
2508
  return "";
2439
2509
  }
2510
+ function formatMSTeamsSenderReason(params) {
2511
+ switch (params.reasonCode) {
2512
+ case "dm_policy_open": return "dmPolicy=open";
2513
+ case "dm_policy_disabled": return "dmPolicy=disabled";
2514
+ case "dm_policy_pairing_required": return "dmPolicy=pairing (not allowlisted)";
2515
+ case "dm_policy_allowlisted": return `dmPolicy=${params.dmPolicy ?? "allowlist"} (allowlisted)`;
2516
+ case "dm_policy_not_allowlisted": return `dmPolicy=${params.dmPolicy ?? "allowlist"} (not allowlisted)`;
2517
+ case "group_policy_disabled": return "groupPolicy=disabled";
2518
+ case "group_policy_empty_allowlist":
2519
+ case "route_sender_empty": return "groupPolicy=allowlist (empty allowlist)";
2520
+ case "group_policy_not_allowlisted": return "groupPolicy=allowlist (not allowlisted)";
2521
+ case "group_policy_open": return "groupPolicy=open";
2522
+ case "group_policy_allowed": return `groupPolicy=${params.groupPolicy ?? "allowlist"}`;
2523
+ default: return params.reasonCode;
2524
+ }
2525
+ }
2440
2526
  function buildStoredConversationReference(params) {
2441
2527
  const { activity, conversationId, conversationType, teamId, threadId } = params;
2442
2528
  const from = activity.from;
@@ -2532,14 +2618,17 @@ function createMSTeamsMessageHandler(deps) {
2532
2618
  teamId,
2533
2619
  threadId: conversationType === "channel" ? conversationMessageId ?? activity.replyToId ?? void 0 : void 0
2534
2620
  });
2535
- const { dmPolicy, senderId, senderName, pairing, isDirectMessage, channelGate, access, configuredDmAllowFrom, effectiveDmAllowFrom, effectiveGroupAllowFrom, allowNameMatching, groupPolicy } = await resolveMSTeamsSenderAccess({
2621
+ const { dmPolicy, senderId, senderName, pairing, isDirectMessage, channelGate, senderAccess, commandAccess, allowNameMatching, groupPolicy } = await resolveMSTeamsSenderAccess({
2536
2622
  cfg,
2537
- activity
2623
+ activity,
2624
+ hasControlCommand: core.channel.text.hasControlCommand(text, cfg)
2538
2625
  });
2539
- const useAccessGroups = cfg.commands?.useAccessGroups !== false;
2626
+ const commandAuthorized = commandAccess.requested ? commandAccess.authorized : void 0;
2627
+ const effectiveDmAllowFrom = senderAccess.effectiveAllowFrom;
2628
+ const effectiveGroupAllowFrom = senderAccess.effectiveGroupAllowFrom;
2540
2629
  const isChannel = conversationType === "channel";
2541
- if (isDirectMessage && msteamsCfg && access.decision !== "allow") {
2542
- if (access.reason === "dmPolicy=disabled") {
2630
+ if (isDirectMessage && msteamsCfg && senderAccess.decision !== "allow") {
2631
+ if (senderAccess.reasonCode === "dm_policy_disabled") {
2543
2632
  log.info("dropping dm (dms disabled)", {
2544
2633
  sender: senderId,
2545
2634
  label: senderName
@@ -2553,7 +2642,7 @@ function createMSTeamsMessageHandler(deps) {
2553
2642
  senderName,
2554
2643
  allowNameMatching
2555
2644
  });
2556
- if (access.decision === "pairing") {
2645
+ if (senderAccess.decision === "pairing") {
2557
2646
  conversationStore.upsert(conversationId, conversationRef).catch((err) => {
2558
2647
  log.debug?.("failed to save conversation reference", { error: formatUnknownError(err) });
2559
2648
  });
@@ -2574,7 +2663,11 @@ function createMSTeamsMessageHandler(deps) {
2574
2663
  sender: senderId,
2575
2664
  label: senderName,
2576
2665
  dmPolicy,
2577
- reason: access.reason,
2666
+ reason: formatMSTeamsSenderReason({
2667
+ reasonCode: senderAccess.reasonCode,
2668
+ dmPolicy,
2669
+ groupPolicy
2670
+ }),
2578
2671
  allowlistMatch: formatAllowlistMatchMeta(allowMatch)
2579
2672
  });
2580
2673
  return;
@@ -2597,28 +2690,17 @@ function createMSTeamsMessageHandler(deps) {
2597
2690
  });
2598
2691
  return;
2599
2692
  }
2600
- const senderGroupAccess = evaluateSenderGroupAccessForPolicy({
2601
- groupPolicy,
2602
- groupAllowFrom: effectiveGroupAllowFrom,
2603
- senderId,
2604
- isSenderAllowed: (_senderId, allowFrom) => resolveMSTeamsAllowlistMatch({
2605
- allowFrom,
2606
- senderId,
2607
- senderName,
2608
- allowNameMatching
2609
- }).allowed
2610
- });
2611
- if (!senderGroupAccess.allowed && senderGroupAccess.reason === "disabled") {
2693
+ if (!senderAccess.allowed && senderAccess.reasonCode === "group_policy_disabled") {
2612
2694
  log.info("dropping group message (groupPolicy: disabled)", { conversationId });
2613
2695
  log.debug?.("dropping group message (groupPolicy: disabled)", { conversationId });
2614
2696
  return;
2615
2697
  }
2616
- if (!senderGroupAccess.allowed && senderGroupAccess.reason === "empty_allowlist") {
2698
+ if (!senderAccess.allowed && (senderAccess.reasonCode === "group_policy_empty_allowlist" || senderAccess.reasonCode === "route_sender_empty")) {
2617
2699
  log.info("dropping group message (groupPolicy: allowlist, no allowlist)", { conversationId });
2618
2700
  log.debug?.("dropping group message (groupPolicy: allowlist, no allowlist)", { conversationId });
2619
2701
  return;
2620
2702
  }
2621
- if (!senderGroupAccess.allowed && senderGroupAccess.reason === "sender_not_allowlisted") {
2703
+ if (!senderAccess.allowed && senderAccess.reasonCode === "group_policy_not_allowlisted") {
2622
2704
  const allowMatch = resolveMSTeamsAllowlistMatch({
2623
2705
  allowFrom: effectiveGroupAllowFrom,
2624
2706
  senderId,
@@ -2638,30 +2720,7 @@ function createMSTeamsMessageHandler(deps) {
2638
2720
  return;
2639
2721
  }
2640
2722
  }
2641
- const commandDmAllowFrom = isDirectMessage ? effectiveDmAllowFrom : configuredDmAllowFrom;
2642
- const ownerAllowedForCommands = isMSTeamsGroupAllowed({
2643
- groupPolicy: "allowlist",
2644
- allowFrom: commandDmAllowFrom,
2645
- senderId,
2646
- senderName,
2647
- allowNameMatching
2648
- });
2649
- const groupAllowedForCommands = isMSTeamsGroupAllowed({
2650
- groupPolicy: "allowlist",
2651
- allowFrom: effectiveGroupAllowFrom,
2652
- senderId,
2653
- senderName,
2654
- allowNameMatching
2655
- });
2656
- const { commandAuthorized, shouldBlock } = resolveDualTextControlCommandGate({
2657
- useAccessGroups,
2658
- primaryConfigured: commandDmAllowFrom.length > 0,
2659
- primaryAllowed: ownerAllowedForCommands,
2660
- secondaryConfigured: effectiveGroupAllowFrom.length > 0,
2661
- secondaryAllowed: groupAllowedForCommands,
2662
- hasControlCommand: core.channel.text.hasControlCommand(text, cfg)
2663
- });
2664
- if (shouldBlock) {
2723
+ if (commandAccess.shouldBlockControlCommand) {
2665
2724
  logInboundDrop({
2666
2725
  log: logVerboseMessage,
2667
2726
  channel: "msteams",
@@ -3021,7 +3080,7 @@ function createMSTeamsMessageHandler(deps) {
3021
3080
  logVerboseMessage(`msteams: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${teamsTo}`);
3022
3081
  } catch (err) {
3023
3082
  log.error("dispatch failed", { error: formatUnknownError(err) });
3024
- runtime.error?.(`msteams dispatch failed: ${formatUnknownError(err)}`);
3083
+ runtime.error(`msteams dispatch failed: ${formatUnknownError(err)}`);
3025
3084
  try {
3026
3085
  await context.sendActivity("⚠️ Something went wrong. Please try again.");
3027
3086
  } catch {}
@@ -3062,7 +3121,7 @@ function createMSTeamsMessageHandler(deps) {
3062
3121
  });
3063
3122
  },
3064
3123
  onError: (err) => {
3065
- runtime.error?.(`msteams debounce flush failed: ${formatUnknownError(err)}`);
3124
+ runtime.error(`msteams debounce flush failed: ${formatUnknownError(err)}`);
3066
3125
  }
3067
3126
  });
3068
3127
  return async function handleTeamsMessage(context) {
@@ -3114,11 +3173,6 @@ function createMSTeamsReactionHandler(deps) {
3114
3173
  const { cfg, log } = deps;
3115
3174
  const core = getMSTeamsRuntime();
3116
3175
  const msteamsCfg = cfg.channels?.msteams;
3117
- const pairing = createChannelPairingController({
3118
- core,
3119
- channel: "msteams",
3120
- accountId: DEFAULT_ACCOUNT_ID
3121
- });
3122
3176
  return async function handleReaction(context, direction) {
3123
3177
  const activity = context.activity;
3124
3178
  const reactions = direction === "added" ? activity.reactionsAdded ?? [] : activity.reactionsRemoved ?? [];
@@ -3138,74 +3192,19 @@ function createMSTeamsReactionHandler(deps) {
3138
3192
  const isDirectMessage = !isGroupChat && !isChannel;
3139
3193
  const senderId = from.aadObjectId ?? from.id;
3140
3194
  const senderName = from.name ?? from.id;
3141
- const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing";
3142
- const storedAllowFrom = await readStoreAllowFromForDmPolicy({
3143
- provider: "msteams",
3144
- accountId: pairing.accountId,
3145
- dmPolicy,
3146
- readStore: pairing.readStoreForDmPolicy
3147
- });
3148
- const dmAllowFrom = msteamsCfg?.allowFrom ?? [];
3149
- const groupAllowFrom = msteamsCfg?.groupAllowFrom;
3150
- const resolvedAllowFromLists = resolveEffectiveAllowFromLists({
3151
- allowFrom: dmAllowFrom,
3152
- groupAllowFrom,
3153
- storeAllowFrom: storedAllowFrom,
3154
- dmPolicy
3155
- });
3156
- const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
3157
- if (isDirectMessage && msteamsCfg) {
3158
- const access = resolveDmGroupAccessWithLists({
3159
- isGroup: false,
3160
- dmPolicy,
3161
- groupPolicy: msteamsCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist",
3162
- allowFrom: dmAllowFrom,
3163
- groupAllowFrom,
3164
- storeAllowFrom: storedAllowFrom,
3165
- groupAllowFromFallbackToAllowFrom: false,
3166
- isSenderAllowed: (allowFrom) => resolveMSTeamsAllowlistMatch({
3167
- allowFrom,
3168
- senderId,
3169
- senderName,
3170
- allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg)
3171
- }).allowed
3195
+ if (msteamsCfg) {
3196
+ const senderAccess = await resolveMSTeamsSenderAccess({
3197
+ cfg,
3198
+ activity
3172
3199
  });
3173
- if (access.decision !== "allow") {
3174
- log.debug?.("dropping reaction (dm access denied)", {
3200
+ if (senderAccess.senderAccess.decision !== "allow") {
3201
+ log.debug?.("dropping reaction (access denied)", {
3175
3202
  sender: senderId,
3176
- reason: access.reason
3203
+ reason: senderAccess.senderAccess.reasonCode
3177
3204
  });
3178
3205
  return;
3179
3206
  }
3180
3207
  }
3181
- if (!isDirectMessage && msteamsCfg) {
3182
- const teamId = activity.channelData?.team?.id;
3183
- const teamName = activity.channelData?.team?.name;
3184
- const channelName = activity.channelData?.channel?.name;
3185
- const channelGate = resolveMSTeamsRouteConfig({
3186
- cfg: msteamsCfg,
3187
- teamId,
3188
- teamName,
3189
- conversationId,
3190
- channelName,
3191
- allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg)
3192
- });
3193
- if (channelGate.allowlistConfigured && !channelGate.allowed) {
3194
- log.debug?.("dropping reaction (not in team/channel allowlist)", { conversationId });
3195
- return;
3196
- }
3197
- const effectiveGroupAllowFrom = resolvedAllowFromLists.effectiveGroupAllowFrom;
3198
- if (!isMSTeamsGroupAllowed({
3199
- groupPolicy: msteamsCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist",
3200
- allowFrom: effectiveGroupAllowFrom,
3201
- senderId,
3202
- senderName,
3203
- allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg)
3204
- })) {
3205
- log.debug?.("dropping reaction (sender not in group allowlist)", { sender: senderId });
3206
- return;
3207
- }
3208
- }
3209
3208
  const teamId = isDirectMessage ? void 0 : activity.channelData?.team?.id;
3210
3209
  const route = core.channel.routing.resolveAgentRoute({
3211
3210
  cfg,
@@ -3489,7 +3488,7 @@ async function isInvokeAuthorized(params) {
3489
3488
  const { msteamsCfg, isDirectMessage, conversationId, senderId } = resolved;
3490
3489
  if (!msteamsCfg) return true;
3491
3490
  const maybeInvokeName = includeInvokeName ? { name: context.activity.name } : void 0;
3492
- if (isDirectMessage && resolved.access.decision !== "allow") {
3491
+ if (isDirectMessage && resolved.senderAccess.decision !== "allow") {
3493
3492
  deps.log.debug?.(deniedLogs.dm, {
3494
3493
  sender: senderId,
3495
3494
  conversationId,
@@ -3506,7 +3505,7 @@ async function isInvokeAuthorized(params) {
3506
3505
  });
3507
3506
  return false;
3508
3507
  }
3509
- if (!isDirectMessage && !resolved.senderGroupAccess.allowed) {
3508
+ if (!isDirectMessage && !resolved.senderAccess.allowed) {
3510
3509
  deps.log.debug?.(deniedLogs.group, {
3511
3510
  sender: senderId,
3512
3511
  conversationId,
@@ -3603,8 +3602,11 @@ async function handleFeedbackInvoke(context, deps) {
3603
3602
  try {
3604
3603
  const storePath = core.channel.session.resolveStorePath(deps.cfg.session?.store, { agentId: route.agentId });
3605
3604
  const safeKey = route.sessionKey.replace(/[^a-zA-Z0-9_-]/g, "_");
3606
- const transcriptFile = path.join(storePath, `${safeKey}.jsonl`);
3607
- await fs.appendFile(transcriptFile, JSON.stringify(feedbackEvent) + "\n", "utf-8").catch(() => {});
3605
+ await appendRegularFile({
3606
+ filePath: path.join(storePath, `${safeKey}.jsonl`),
3607
+ content: `${JSON.stringify(feedbackEvent)}\n`,
3608
+ rejectSymlinkParents: true
3609
+ }).catch(() => {});
3608
3610
  } catch {}
3609
3611
  const conversationRef = {
3610
3612
  activityId: activity.id,
@@ -3744,7 +3746,7 @@ function registerMSTeamsHandlers(handler, deps) {
3744
3746
  try {
3745
3747
  await handleTeamsMessage(context);
3746
3748
  } catch (err) {
3747
- deps.runtime.error?.(`msteams handler failed: ${formatUnknownError(err)}`);
3749
+ deps.runtime.error(`msteams handler failed: ${formatUnknownError(err)}`);
3748
3750
  }
3749
3751
  await next();
3750
3752
  });
@@ -3788,7 +3790,7 @@ function registerMSTeamsHandlers(handler, deps) {
3788
3790
  try {
3789
3791
  await handleReaction(context, "added");
3790
3792
  } catch (err) {
3791
- deps.runtime.error?.(`msteams reaction handler failed: ${String(err)}`);
3793
+ deps.runtime.error(`msteams reaction handler failed: ${String(err)}`);
3792
3794
  }
3793
3795
  await next();
3794
3796
  });
@@ -3796,7 +3798,7 @@ function registerMSTeamsHandlers(handler, deps) {
3796
3798
  try {
3797
3799
  await handleReaction(context, "removed");
3798
3800
  } catch (err) {
3799
- deps.runtime.error?.(`msteams reaction handler failed: ${String(err)}`);
3801
+ deps.runtime.error(`msteams reaction handler failed: ${String(err)}`);
3800
3802
  }
3801
3803
  await next();
3802
3804
  });
@@ -3941,7 +3943,17 @@ async function monitorMSTeamsProvider(opts) {
3941
3943
  let allowFrom = msteamsCfg.allowFrom;
3942
3944
  let groupAllowFrom = msteamsCfg.groupAllowFrom;
3943
3945
  let teamsConfig = msteamsCfg.teams;
3946
+ const allowNameMatching = isDangerousNameMatchingEnabled(msteamsCfg);
3944
3947
  const cleanAllowEntry = (entry) => entry.replace(/^(msteams|teams):/i, "").replace(/^user:/i, "").trim();
3948
+ const isStableUserId = (entry) => /^[0-9a-fA-F-]{16,}$/.test(entry);
3949
+ const cleanAllowEntries = (entries) => entries?.map((entry) => cleanAllowEntry(entry)).filter((entry) => entry && entry !== "*") ?? [];
3950
+ const mergeStableUserIds = (entries) => {
3951
+ const additions = cleanAllowEntries(entries).filter((entry) => isStableUserId(entry));
3952
+ return additions.length > 0 ? mergeAllowlist({
3953
+ existing: entries,
3954
+ additions
3955
+ }) : entries;
3956
+ };
3945
3957
  const resolveAllowlistUsers = async (label, entries) => {
3946
3958
  if (entries.length === 0) return {
3947
3959
  additions: [],
@@ -3962,23 +3974,27 @@ async function monitorMSTeamsProvider(opts) {
3962
3974
  };
3963
3975
  };
3964
3976
  try {
3965
- const allowEntries = allowFrom?.map((entry) => cleanAllowEntry(entry)).filter((entry) => entry && entry !== "*") ?? [];
3966
- if (allowEntries.length > 0) {
3967
- const { additions } = await resolveAllowlistUsers("msteams users", allowEntries);
3968
- allowFrom = mergeAllowlist({
3969
- existing: allowFrom,
3970
- additions
3971
- });
3972
- }
3973
- if (Array.isArray(groupAllowFrom) && groupAllowFrom.length > 0) {
3974
- const groupEntries = groupAllowFrom.map((entry) => cleanAllowEntry(entry)).filter((entry) => entry && entry !== "*");
3975
- if (groupEntries.length > 0) {
3976
- const { additions } = await resolveAllowlistUsers("msteams group users", groupEntries);
3977
- groupAllowFrom = mergeAllowlist({
3978
- existing: groupAllowFrom,
3977
+ allowFrom = mergeStableUserIds(allowFrom);
3978
+ if (Array.isArray(groupAllowFrom) && groupAllowFrom.length > 0) groupAllowFrom = mergeStableUserIds(groupAllowFrom);
3979
+ if (allowNameMatching) {
3980
+ const allowEntries = cleanAllowEntries(allowFrom).filter((entry) => !isStableUserId(entry));
3981
+ if (allowEntries.length > 0) {
3982
+ const { additions } = await resolveAllowlistUsers("msteams users", allowEntries);
3983
+ allowFrom = mergeAllowlist({
3984
+ existing: allowFrom,
3979
3985
  additions
3980
3986
  });
3981
3987
  }
3988
+ if (Array.isArray(groupAllowFrom) && groupAllowFrom.length > 0) {
3989
+ const groupEntries = cleanAllowEntries(groupAllowFrom).filter((entry) => !isStableUserId(entry));
3990
+ if (groupEntries.length > 0) {
3991
+ const { additions } = await resolveAllowlistUsers("msteams group users", groupEntries);
3992
+ groupAllowFrom = mergeAllowlist({
3993
+ existing: groupAllowFrom,
3994
+ additions
3995
+ });
3996
+ }
3997
+ }
3982
3998
  }
3983
3999
  if (teamsConfig && Object.keys(teamsConfig).length > 0) {
3984
4000
  const entries = [];
@@ -4046,7 +4062,7 @@ async function monitorMSTeamsProvider(opts) {
4046
4062
  }
4047
4063
  }
4048
4064
  } catch (err) {
4049
- runtime.log?.(`msteams resolve failed; using config entries. ${formatUnknownError(err)}`);
4065
+ runtime?.error(`msteams resolve failed; falling back to raw config entries — allowlist members resolved via Graph may be missing. ${formatUnknownError(err)}`);
4050
4066
  }
4051
4067
  msteamsCfg = {
4052
4068
  ...msteamsCfg,
@@ -4116,7 +4132,8 @@ async function monitorMSTeamsProvider(opts) {
4116
4132
  }
4117
4133
  next();
4118
4134
  }).catch((err) => {
4119
- log.debug?.(`JWT validation error: ${formatUnknownError(err)}`);
4135
+ if (err instanceof Error && /ECONNREFUSED|ENOTFOUND|EHOSTUNREACH|ETIMEDOUT|ECONNRESET/i.test(err.code ?? err.message)) runtime?.error(`msteams: JWKS key fetch failed — check egress to login.botframework.com:443 (firewall or DNS may be blocking it). Bot will 401 all inbound requests until this is resolved. Error: ${formatUnknownError(err)}`);
4136
+ else log.debug?.(`JWT validation error: ${formatUnknownError(err)}`);
4120
4137
  res.status(401).json({ error: "Unauthorized" });
4121
4138
  });
4122
4139
  });