@open-everywhere/provider-teams 0.0.0-stage → 0.1.0
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/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +46 -2
- package/agent/channels/teams-bot-name.ts +162 -0
- package/agent/channels/teams-catch-up.ts +244 -0
- package/agent/channels/teams-connector.ts +183 -0
- package/agent/channels/teams-conversation-ref.ts +398 -0
- package/agent/channels/teams-directory.ts +193 -0
- package/agent/channels/teams-engagement.ts +391 -0
- package/agent/channels/teams-fire-eligibility.ts +152 -0
- package/agent/channels/teams-front.ts +1404 -0
- package/agent/channels/teams-graph-boundary.ts +178 -0
- package/agent/channels/teams-graph-team-id.ts +106 -0
- package/agent/channels/teams-id-restore.ts +131 -0
- package/agent/channels/teams-identity.ts +395 -0
- package/agent/channels/teams-input-response.ts +129 -0
- package/agent/channels/teams-message-ops.ts +79 -0
- package/agent/channels/teams-origin-codec.ts +104 -0
- package/agent/channels/teams-post-arm.ts +323 -0
- package/agent/channels/teams-reaction-feedback.ts +127 -0
- package/agent/channels/teams-read.ts +510 -0
- package/agent/channels/teams-roster.ts +136 -0
- package/agent/channels/teams-stop.ts +319 -0
- package/agent/channels/teams-supervisor.ts +638 -0
- package/agent/channels/teams.ts +270 -0
- package/agent/contributions.ts +31 -0
- package/agent/docs-generated.ts +15 -0
- package/agent/ops.ts +129 -0
- package/agent/provider-events.ts +69 -0
- package/agent/register.ts +219 -0
- package/agent/surface-parity.ts +112 -0
- package/dist/agent/channels/teams-bot-name.d.ts +36 -0
- package/dist/agent/channels/teams-bot-name.d.ts.map +1 -0
- package/dist/agent/channels/teams-bot-name.js +141 -0
- package/dist/agent/channels/teams-bot-name.js.map +1 -0
- package/dist/agent/channels/teams-catch-up.d.ts +53 -0
- package/dist/agent/channels/teams-catch-up.d.ts.map +1 -0
- package/dist/agent/channels/teams-catch-up.js +221 -0
- package/dist/agent/channels/teams-catch-up.js.map +1 -0
- package/dist/agent/channels/teams-connector.d.ts +57 -0
- package/dist/agent/channels/teams-connector.d.ts.map +1 -0
- package/dist/agent/channels/teams-connector.js +132 -0
- package/dist/agent/channels/teams-connector.js.map +1 -0
- package/dist/agent/channels/teams-conversation-ref.d.ts +123 -0
- package/dist/agent/channels/teams-conversation-ref.d.ts.map +1 -0
- package/dist/agent/channels/teams-conversation-ref.js +368 -0
- package/dist/agent/channels/teams-conversation-ref.js.map +1 -0
- package/dist/agent/channels/teams-directory.d.ts +11 -0
- package/dist/agent/channels/teams-directory.d.ts.map +1 -0
- package/dist/agent/channels/teams-directory.js +179 -0
- package/dist/agent/channels/teams-directory.js.map +1 -0
- package/dist/agent/channels/teams-engagement.d.ts +174 -0
- package/dist/agent/channels/teams-engagement.d.ts.map +1 -0
- package/dist/agent/channels/teams-engagement.js +336 -0
- package/dist/agent/channels/teams-engagement.js.map +1 -0
- package/dist/agent/channels/teams-fire-eligibility.d.ts +26 -0
- package/dist/agent/channels/teams-fire-eligibility.d.ts.map +1 -0
- package/dist/agent/channels/teams-fire-eligibility.js +126 -0
- package/dist/agent/channels/teams-fire-eligibility.js.map +1 -0
- package/dist/agent/channels/teams-front.d.ts +249 -0
- package/dist/agent/channels/teams-front.d.ts.map +1 -0
- package/dist/agent/channels/teams-front.js +1143 -0
- package/dist/agent/channels/teams-front.js.map +1 -0
- package/dist/agent/channels/teams-graph-boundary.d.ts +45 -0
- package/dist/agent/channels/teams-graph-boundary.d.ts.map +1 -0
- package/dist/agent/channels/teams-graph-boundary.js +168 -0
- package/dist/agent/channels/teams-graph-boundary.js.map +1 -0
- package/dist/agent/channels/teams-graph-team-id.d.ts +30 -0
- package/dist/agent/channels/teams-graph-team-id.d.ts.map +1 -0
- package/dist/agent/channels/teams-graph-team-id.js +89 -0
- package/dist/agent/channels/teams-graph-team-id.js.map +1 -0
- package/dist/agent/channels/teams-id-restore.d.ts +34 -0
- package/dist/agent/channels/teams-id-restore.d.ts.map +1 -0
- package/dist/agent/channels/teams-id-restore.js +123 -0
- package/dist/agent/channels/teams-id-restore.js.map +1 -0
- package/dist/agent/channels/teams-identity.d.ts +148 -0
- package/dist/agent/channels/teams-identity.d.ts.map +1 -0
- package/dist/agent/channels/teams-identity.js +336 -0
- package/dist/agent/channels/teams-identity.js.map +1 -0
- package/dist/agent/channels/teams-input-response.d.ts +23 -0
- package/dist/agent/channels/teams-input-response.d.ts.map +1 -0
- package/dist/agent/channels/teams-input-response.js +102 -0
- package/dist/agent/channels/teams-input-response.js.map +1 -0
- package/dist/agent/channels/teams-message-ops.d.ts +3 -0
- package/dist/agent/channels/teams-message-ops.d.ts.map +1 -0
- package/dist/agent/channels/teams-message-ops.js +72 -0
- package/dist/agent/channels/teams-message-ops.js.map +1 -0
- package/dist/agent/channels/teams-origin-codec.d.ts +16 -0
- package/dist/agent/channels/teams-origin-codec.d.ts.map +1 -0
- package/dist/agent/channels/teams-origin-codec.js +99 -0
- package/dist/agent/channels/teams-origin-codec.js.map +1 -0
- package/dist/agent/channels/teams-post-arm.d.ts +37 -0
- package/dist/agent/channels/teams-post-arm.d.ts.map +1 -0
- package/dist/agent/channels/teams-post-arm.js +280 -0
- package/dist/agent/channels/teams-post-arm.js.map +1 -0
- package/dist/agent/channels/teams-reaction-feedback.d.ts +22 -0
- package/dist/agent/channels/teams-reaction-feedback.d.ts.map +1 -0
- package/dist/agent/channels/teams-reaction-feedback.js +108 -0
- package/dist/agent/channels/teams-reaction-feedback.js.map +1 -0
- package/dist/agent/channels/teams-read.d.ts +127 -0
- package/dist/agent/channels/teams-read.d.ts.map +1 -0
- package/dist/agent/channels/teams-read.js +424 -0
- package/dist/agent/channels/teams-read.js.map +1 -0
- package/dist/agent/channels/teams-roster.d.ts +54 -0
- package/dist/agent/channels/teams-roster.d.ts.map +1 -0
- package/dist/agent/channels/teams-roster.js +121 -0
- package/dist/agent/channels/teams-roster.js.map +1 -0
- package/dist/agent/channels/teams-stop.d.ts +74 -0
- package/dist/agent/channels/teams-stop.d.ts.map +1 -0
- package/dist/agent/channels/teams-stop.js +252 -0
- package/dist/agent/channels/teams-stop.js.map +1 -0
- package/dist/agent/channels/teams-supervisor.d.ts +351 -0
- package/dist/agent/channels/teams-supervisor.d.ts.map +1 -0
- package/dist/agent/channels/teams-supervisor.js +366 -0
- package/dist/agent/channels/teams-supervisor.js.map +1 -0
- package/dist/agent/channels/teams.d.ts +5 -0
- package/dist/agent/channels/teams.d.ts.map +1 -0
- package/dist/agent/channels/teams.js +246 -0
- package/dist/agent/channels/teams.js.map +1 -0
- package/dist/agent/contributions.d.ts +12 -0
- package/dist/agent/contributions.d.ts.map +1 -0
- package/dist/agent/contributions.js +31 -0
- package/dist/agent/contributions.js.map +1 -0
- package/dist/agent/docs-generated.d.ts +3 -0
- package/dist/agent/docs-generated.d.ts.map +1 -0
- package/dist/agent/docs-generated.js +13 -0
- package/dist/agent/docs-generated.js.map +1 -0
- package/dist/agent/ops.d.ts +14 -0
- package/dist/agent/ops.d.ts.map +1 -0
- package/dist/agent/ops.js +122 -0
- package/dist/agent/ops.js.map +1 -0
- package/dist/agent/provider-events.d.ts +3 -0
- package/dist/agent/provider-events.d.ts.map +1 -0
- package/dist/agent/provider-events.js +67 -0
- package/dist/agent/provider-events.js.map +1 -0
- package/dist/agent/register.d.ts +16 -0
- package/dist/agent/register.d.ts.map +1 -0
- package/dist/agent/register.js +198 -0
- package/dist/agent/register.js.map +1 -0
- package/dist/agent/surface-parity.d.ts +3 -0
- package/dist/agent/surface-parity.d.ts.map +1 -0
- package/dist/agent/surface-parity.js +106 -0
- package/dist/agent/surface-parity.js.map +1 -0
- package/package.json +183 -4
- package/tsconfig.build.json +22 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// Microsoft Teams channel/thread READ adapter for the generic read verbs (channel-read.ts) — the
|
|
2
|
+
// third wired surface after Slack (slack-read.ts, reused verbatim) and Discord (inline in
|
|
3
|
+
// channel-read.ts). Teams was DECLARED UNSUPPORTED at the read core's introduction because eve
|
|
4
|
+
// exposes no Teams message-history API; this module closes that gap via Microsoft GRAPH, the API
|
|
5
|
+
// that actually carries Teams channel/thread history (Bot Framework does not).
|
|
6
|
+
//
|
|
7
|
+
// WHY A SEPARATE GRAPH TOKEN (not eve's own Teams token): eve's `resolveTeamsAccessToken`
|
|
8
|
+
// (eve/channels/teams, used by reply-post.ts's sendTeamsActivity/replyToTeamsActivity) mints a
|
|
9
|
+
// token scoped to `https://api.botframework.com/.default` — the Bot Framework CONNECTOR audience,
|
|
10
|
+
// which can POST activities but exposes no history-read endpoint. Reading channel messages / thread
|
|
11
|
+
// replies instead needs `https://graph.microsoft.com/.default` — a DIFFERENT audience, so it needs
|
|
12
|
+
// its OWN token. We reuse the EXACT SAME client-credential FLOW + env resolution eve's mint uses —
|
|
13
|
+
// same `MICROSOFT_APP_ID`/`MICROSOFT_APP_PASSWORD`/`MICROSOFT_TENANT_ID` (with the `TEAMS_APP_*`
|
|
14
|
+
// fallbacks), same `login.microsoftonline.com/{tenant}/oauth2/v2.0/token` endpoint, same
|
|
15
|
+
// `grant_type=client_credentials` — by calling eve's OWN public `resolveTeamsAppId` /
|
|
16
|
+
// `resolveTeamsAppPassword` / `resolveTeamsTenantId` for id/password/tenant resolution (never
|
|
17
|
+
// re-implemented), and only requesting Graph's scope instead of the Connector's. The app
|
|
18
|
+
// registration must ALSO hold a Graph permission that can read channel messages
|
|
19
|
+
// (`ChannelMessage.Read.All`, admin-consented) OR already have the per-team RSC grant
|
|
20
|
+
// `ChannelMessage.Read.Group` this deployment's own Teams app manifest already requests for
|
|
21
|
+
// ambient watching (teams-app/manifest.json, docs/deploy/surfaces/teams.md) — when a team already
|
|
22
|
+
// has that grant, THIS adapter needs no extra Azure configuration at all. Undocumented/absent
|
|
23
|
+
// either way surfaces as a Graph 401/403, fail-loud (see below), never a silent empty read.
|
|
24
|
+
//
|
|
25
|
+
// SCOPE — TEAM CHANNELS ONLY. The manifest also carries the CHAT RSC grant `ChatMessage.Read.Chat`,
|
|
26
|
+
// whose Graph analogue is `/chats/{chat-id}/messages`: a different id shape (a chat has no team, so
|
|
27
|
+
// the TWO-LEVEL ID note below doesn't apply) and a different allowlist grain. Reading a group
|
|
28
|
+
// chat's history is therefore NOT wired here — that grant exists for the delivery side
|
|
29
|
+
// (teams-front.ts). A chat-read adapter would be its own arm, not a parameter of these calls.
|
|
30
|
+
//
|
|
31
|
+
// TWO-LEVEL ID: unlike Slack/Discord's single channel id, a Graph channel read needs BOTH a team id
|
|
32
|
+
// AND a channel id (`/teams/{team}/channels/{channel}/…`). The domain allowlist (like every other
|
|
33
|
+
// platform) lists only the CHANNEL id — the same stable per-channel grain memory resolution already
|
|
34
|
+
// keys on (teams-identity.ts's `domainSurface`; see memory/resolve.ts's domainKeyOf comment) — so
|
|
35
|
+
// the team id is resolved from the LIVE turn's OWN binding (always known on an engaged Teams
|
|
36
|
+
// session; see identity/binding.ts's bindTeams `target.teamId`), never tracked per allowed channel.
|
|
37
|
+
// 🔴 AND IT IS RESOLVED AGAIN BEFORE IT REACHES A URL. The binding carries the BOT CONNECTOR's team
|
|
38
|
+
// id (`19:…@thread.tacv2`); Graph's `/teams/{id}` segment takes the team's Microsoft 365 GROUP id and
|
|
39
|
+
// nothing else. `channels/teams-graph-team-id.ts` maps the one to the other over the Connector and
|
|
40
|
+
// caches it (OPE-1808) — `currentGraphTeamId` below is the only place these reads take a team id from.
|
|
41
|
+
// Reading a channel OTHER than the current one therefore assumes it lives in the SAME team as the
|
|
42
|
+
// turn (the common case — a domain's Teams channels are one team's channels, exactly like a Slack
|
|
43
|
+
// domain's channels being one workspace's); a genuine cross-team domain surfaces a clear Graph
|
|
44
|
+
// 404/403 rather than silently reading the wrong team.
|
|
45
|
+
//
|
|
46
|
+
// GRAPH QUIRKS (verified against Microsoft's own docs, 2026-07):
|
|
47
|
+
// - Channel messages (`GET .../channels/{id}/messages`) and thread replies (`GET
|
|
48
|
+
// .../messages/{id}/replies`) support `$top` (max 50) but NOT `$orderby`/`$filter` — so unlike
|
|
49
|
+
// Slack/Discord's guaranteed history order, we sort explicitly by `createdDateTime` before
|
|
50
|
+
// rendering "oldest first" rather than trusting the API's order.
|
|
51
|
+
// - Both endpoints observably return NEWEST-first (root messages by the whole reply chain's last-
|
|
52
|
+
// modified time; replies by their own createdDateTime) — the OPPOSITE of Slack's
|
|
53
|
+
// conversations.replies (oldest-first). Our explicit sort makes this a non-issue either way.
|
|
54
|
+
// - Pagination is `@odata.nextLink` (an absolute follow-up URL), bounded here at
|
|
55
|
+
// {@link TEAMS_GRAPH_MAX_PAGES} pages so a busy channel can't fan out unboundedly (mirrors
|
|
56
|
+
// discord-backscroll.ts's DISCORD_RAW_FETCH_MAX_PAGES).
|
|
57
|
+
// - Message bodies are always `body.contentType:"html"`; `stripHtml` below strips tags (mention
|
|
58
|
+
// `<at>` spans included) and decodes the common entities — a self-contained copy of the Teams
|
|
59
|
+
// e2e driver's tag-strip (scripts/e2e/teams-driver.ts), kept LOCAL rather than imported (agent/
|
|
60
|
+
// code must not depend on scripts/, the same DAG discipline discord-backscroll.ts's
|
|
61
|
+
// isDiscordBackscrollVisible note documents for Slack-vs-Discord).
|
|
62
|
+
// - A `deletedDateTime` message or a non-"message" `messageType` (systemEventMessage, chatEvent,
|
|
63
|
+
// …) is dropped — the Graph analogue of Slack's `subtype` filter / Discord's `isConversationType`.
|
|
64
|
+
//
|
|
65
|
+
// FAIL LOUD (unlike Discord backscroll's fail-soft posture): a read must surface its error, never
|
|
66
|
+
// silently return an empty page — matching discordReadAdapter's posture in channel-read.ts.
|
|
67
|
+
import { resolveTeamsAppId, resolveTeamsAppPassword, resolveTeamsTenantId } from "eve/channels/teams";
|
|
68
|
+
import { auditChannelReadRefusal, bindFrom, resolvePartition, domainKeyOf, domainForChannel, getConfig, refutedReadAuthorIds, ChannelScopeError, warmCredential, domainRoster, } from "@open-everywhere/core/agent/adapter-api.js";
|
|
69
|
+
import { noteTeamsReadBoundary, teamsReadBoundary, _resetTeamsReadBoundaryForTest } from "./teams-graph-boundary.js";
|
|
70
|
+
import { teamsGraphTeamId } from "./teams-graph-team-id.js";
|
|
71
|
+
// OPE-929 — the reply-chain half of a channel conversation id; the suffix is spelled once, there.
|
|
72
|
+
import { teamsReplyChainRoot } from "./teams-conversation-ref.js";
|
|
73
|
+
const GRAPH_BASE = "https://graph.microsoft.com/v1.0";
|
|
74
|
+
const GRAPH_TOKEN_SCOPE = "https://graph.microsoft.com/.default";
|
|
75
|
+
const TOKEN_LOGIN_BASE = "https://login.microsoftonline.com";
|
|
76
|
+
/** Refresh the cached Graph token this long before its stated expiry, so a call never races it. */
|
|
77
|
+
const TOKEN_SKEW_MS = 60_000;
|
|
78
|
+
/** Microsoft's documented app-only token lifetime — used only when a token response omits
|
|
79
|
+
* `expires_in` (every real one carries it). */
|
|
80
|
+
const GRAPH_TOKEN_FALLBACK_TTL_SEC = 3600;
|
|
81
|
+
const TEAMS_CHANNEL_DEFAULT_LIMIT = 50;
|
|
82
|
+
const TEAMS_THREAD_DEFAULT_LIMIT = 100;
|
|
83
|
+
/** Graph's own hard cap on `$top` for both the channel-messages and thread-replies endpoints. */
|
|
84
|
+
const TEAMS_GRAPH_PAGE_SIZE = 50;
|
|
85
|
+
/** Bounds a busy channel's fetch at ≤5 pages (≤250 messages) — mirrors Discord's page bound. */
|
|
86
|
+
const TEAMS_GRAPH_MAX_PAGES = 5;
|
|
87
|
+
// --- Graph app-only token (client-credentials, Graph scope) --------------------------------
|
|
88
|
+
/**
|
|
89
|
+
* Mint an app-only Microsoft Graph access token via the client-credentials flow — see the file
|
|
90
|
+
* header for why this can't reuse eve's `resolveTeamsAccessToken` (wrong audience). Fails LOUD
|
|
91
|
+
* (throws) on a missing tenant id or a failed token exchange; `resolveTeamsAppId` /
|
|
92
|
+
* `resolveTeamsAppPassword` already throw their own actionable "MICROSOFT_APP_ID is required" etc.
|
|
93
|
+
* errors when unset, so this only adds the tenant-id check (eve's Connector flow tolerates an absent
|
|
94
|
+
* tenant by falling back to the `botframework.com` placeholder tenant; Graph's client-credentials
|
|
95
|
+
* flow has no such fallback — it is genuinely tenant-scoped). Never echoes the app password or the
|
|
96
|
+
* minted token: this message is written to an audit row.
|
|
97
|
+
*/
|
|
98
|
+
async function mintTeamsGraphTokenOnce() {
|
|
99
|
+
const [appId, appPassword, tenantId] = await Promise.all([resolveTeamsAppId(), resolveTeamsAppPassword(), resolveTeamsTenantId()]);
|
|
100
|
+
if (!tenantId) {
|
|
101
|
+
throw new Error("read: Teams channel reads need MICROSOFT_TENANT_ID (or TEAMS_TENANT_ID) set — Graph's " +
|
|
102
|
+
"client-credential token is tenant-scoped (unlike the Bot Connector token, which tolerates an absent tenant).");
|
|
103
|
+
}
|
|
104
|
+
const body = new URLSearchParams({ client_id: appId, client_secret: appPassword, grant_type: "client_credentials", scope: GRAPH_TOKEN_SCOPE });
|
|
105
|
+
const res = await fetch(`${TOKEN_LOGIN_BASE}/${encodeURIComponent(tenantId)}/oauth2/v2.0/token`, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
108
|
+
body: body.toString(),
|
|
109
|
+
});
|
|
110
|
+
const json = (await res.json().catch(() => ({})));
|
|
111
|
+
if (!res.ok || !json.access_token) {
|
|
112
|
+
throw new Error(`read: Teams Graph token request failed — ${json.error_description ?? json.error ?? `HTTP ${res.status}`}.`);
|
|
113
|
+
}
|
|
114
|
+
// WHICH BOUNDARY GOVERNS THIS DEPLOYMENT'S READS (OPE-428). The mint is the ONE place the answer
|
|
115
|
+
// is knowable without an extra Graph call — the token's own `roles` claim says whether a
|
|
116
|
+
// TENANT-WIDE application permission is consented, which is what removes Microsoft's per-team
|
|
117
|
+
// check. teams-graph-boundary.ts holds the determination for the refusal rows and audits it when
|
|
118
|
+
// it changes. Best-effort by construction: it never throws, so a read can't fail over a marker.
|
|
119
|
+
await noteTeamsReadBoundary(json.access_token);
|
|
120
|
+
return { value: json.access_token, expiresAtMs: Date.now() + (json.expires_in ?? GRAPH_TOKEN_FALLBACK_TTL_SEC) * 1000 };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The Graph app-only token, held warm by the SHARED seam (core credentials/warm.ts, OPE-134).
|
|
124
|
+
*
|
|
125
|
+
* ⚠️ THE NO-REFRESH RHYTHM IS EXACTLY THE `"re-mint"` POSTURE. An app-only token lives ~1h and
|
|
126
|
+
* carries NO refresh token — the only renewal is another client-credentials exchange from the app
|
|
127
|
+
* registration's own secret, which is what `"re-mint"` means (contrast `"refresh-token"`, the OAuth
|
|
128
|
+
* dance the seam deliberately refuses). "No refresh" describes the absent refresh TOKEN, not an
|
|
129
|
+
* absent renewal: nothing about this credential needs a rhythm the seam doesn't already have.
|
|
130
|
+
*
|
|
131
|
+
* Keyed by nothing — one deployment = one app registration = one tenant. The 60s margin is kept
|
|
132
|
+
* from the pre-unification code rather than taking the seam's 5-minute default: this credential's
|
|
133
|
+
* behaviour must not change under a refactor, and 60s is a defensible margin for a 1h token.
|
|
134
|
+
*
|
|
135
|
+
* WHAT UNIFYING BOUGHT THIS SURFACE: single-flight (concurrent reads in one turn used to each
|
|
136
|
+
* kick off their own token exchange) and, far more importantly, a LOUD failure — a broken app
|
|
137
|
+
* password now leaves a durable, queryable `teams.token_mint_failed` audit row instead of only an
|
|
138
|
+
* exception inside whichever read happened to trip over it.
|
|
139
|
+
*/
|
|
140
|
+
const teamsGraphToken = warmCredential({
|
|
141
|
+
name: "teams.graph-app-only",
|
|
142
|
+
platform: "teams",
|
|
143
|
+
auditAction: "teams.token_mint_failed",
|
|
144
|
+
refresh: "re-mint",
|
|
145
|
+
refreshMarginMs: TOKEN_SKEW_MS,
|
|
146
|
+
fallbackTtlSeconds: GRAPH_TOKEN_FALLBACK_TTL_SEC,
|
|
147
|
+
mint: mintTeamsGraphTokenOnce,
|
|
148
|
+
});
|
|
149
|
+
/** Test-only reset — drops the held Graph token AND the boundary determination it produced (the
|
|
150
|
+
* two are minted together; leaving the boundary behind would let one test's token decide the next
|
|
151
|
+
* test's audit marker). */
|
|
152
|
+
export function _resetTeamsGraphTokenCache() {
|
|
153
|
+
teamsGraphToken.reset();
|
|
154
|
+
_resetTeamsReadBoundaryForTest();
|
|
155
|
+
}
|
|
156
|
+
/** Exported (OPE-522) so channels/teams-fire-eligibility.ts's Graph probes reuse the SAME token
|
|
157
|
+
* mint + GET plumbing rather than standing up a second one — the routine fire-eligibility leg's
|
|
158
|
+
* requests are ordinary Graph reads, no different in shape from a channel-history read. Callers
|
|
159
|
+
* that need "unresolvable" rather than a thrown error (fire-eligibility's fail-open posture) catch
|
|
160
|
+
* around it themselves — this function's own fail-LOUD contract is unchanged. */
|
|
161
|
+
export const liveTeamsGraphRequest = async (url) => {
|
|
162
|
+
const token = await teamsGraphToken.get();
|
|
163
|
+
const res = await fetch(url, { headers: { authorization: `Bearer ${token}` } });
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
const text = await res.text().catch(() => "");
|
|
166
|
+
throw new Error(`read: Teams Graph GET failed (HTTP ${res.status}) for ${url}${text ? ` — ${text.slice(0, 200)}` : ""}`);
|
|
167
|
+
}
|
|
168
|
+
return res.json();
|
|
169
|
+
};
|
|
170
|
+
/** HTML message body → plain text: drop tags (mention `<at>…</at>` spans included), decode the
|
|
171
|
+
* five common entities, collapse whitespace. Pure — a self-contained copy of the Teams e2e
|
|
172
|
+
* driver's stripHtml (scripts/e2e/teams-driver.ts); see the file header for why it's not shared. */
|
|
173
|
+
export function stripHtml(html) {
|
|
174
|
+
return html
|
|
175
|
+
.replace(/<[^>]*>/g, " ")
|
|
176
|
+
.replace(/&/g, "&")
|
|
177
|
+
.replace(/</g, "<")
|
|
178
|
+
.replace(/>/g, ">")
|
|
179
|
+
.replace(/"/g, '"')
|
|
180
|
+
.replace(/'/g, "'")
|
|
181
|
+
.replace(/\s+/g, " ")
|
|
182
|
+
.trim();
|
|
183
|
+
}
|
|
184
|
+
function authorOf(m) {
|
|
185
|
+
return m.from?.user?.displayName ?? m.from?.user?.id ?? m.from?.application?.displayName ?? m.from?.application?.id ?? "(unknown)";
|
|
186
|
+
}
|
|
187
|
+
/** A message kept as ordinary conversation content: not deleted, and either no `messageType` or
|
|
188
|
+
* exactly "message" (system events — teamDescriptionUpdated, chatEvent, etc. — carry other
|
|
189
|
+
* values and are dropped, the Graph analogue of Slack's `subtype` filter). */
|
|
190
|
+
function isConversationMessage(m) {
|
|
191
|
+
if (m.deletedDateTime)
|
|
192
|
+
return false;
|
|
193
|
+
return m.messageType === undefined || m.messageType === "message";
|
|
194
|
+
}
|
|
195
|
+
function toIntermediate(m) {
|
|
196
|
+
if (!isConversationMessage(m))
|
|
197
|
+
return undefined;
|
|
198
|
+
const text = stripHtml(m.body?.content ?? "");
|
|
199
|
+
if (!text)
|
|
200
|
+
return undefined;
|
|
201
|
+
const parsed = m.createdDateTime ? Date.parse(m.createdDateTime) : Number.NaN;
|
|
202
|
+
// OPE-474 grading fields, additive: `author` (the display string) is untouched. A `from.user.id`
|
|
203
|
+
// is the person; a `from.application` with no user is a machine; neither present is unattributable.
|
|
204
|
+
const userId = m.from?.user?.id;
|
|
205
|
+
const isApp = m.from?.user === undefined && m.from?.application !== undefined;
|
|
206
|
+
return {
|
|
207
|
+
author: authorOf(m),
|
|
208
|
+
ts: m.id ?? "",
|
|
209
|
+
text,
|
|
210
|
+
createdMs: Number.isFinite(parsed) ? parsed : 0,
|
|
211
|
+
...(isApp ? { bot: true } : typeof userId === "string" && userId.length > 0 ? { authorId: userId } : {}),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Normalize a raw Graph messages array (either endpoint's `value`) into oldest-first
|
|
216
|
+
* {@link TeamsRenderedLine}[] — dropping deleted/system messages and empty bodies, HTML-stripped.
|
|
217
|
+
* Graph guarantees neither endpoint's order (no `$orderby` support, and both are observed
|
|
218
|
+
* newest-first in practice — see the file header), so this ALWAYS sorts by `createdDateTime`
|
|
219
|
+
* rather than trusting API order. Pure; exported for the render test (mirrors discordLinesFromRaw).
|
|
220
|
+
*/
|
|
221
|
+
export function teamsLinesFromRaw(raw) {
|
|
222
|
+
// `createdMs` is the sort key and stops here; the OPE-474 grading fields ride on.
|
|
223
|
+
return teamsTimedLinesFromRaw(raw).map(({ createdMs: _createdMs, ...line }) => line);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The same normalization, with each line's CREATE TIME kept (epoch ms). The read verbs do not need it
|
|
227
|
+
* — a rendered line carries the message id in `ts`, not a timestamp — but the seat catch-up does: it
|
|
228
|
+
* bounds its window by time and core orders the whole window by it (channels/seat-catch-up.ts).
|
|
229
|
+
* Exported so `teams-catch-up.ts` shares this one normalization rather than re-deriving the sender
|
|
230
|
+
* kind and the system-message filter, which is the whole way those two could disagree about a
|
|
231
|
+
* message. `createdMs` is 0 when Graph sent no readable `createdDateTime`; the caller drops it.
|
|
232
|
+
*/
|
|
233
|
+
export function teamsTimedLinesFromRaw(raw) {
|
|
234
|
+
if (!Array.isArray(raw))
|
|
235
|
+
return [];
|
|
236
|
+
const lines = raw.map(toIntermediate).filter((l) => l !== undefined);
|
|
237
|
+
lines.sort((a, b) => a.createdMs - b.createdMs);
|
|
238
|
+
return lines;
|
|
239
|
+
}
|
|
240
|
+
/** Follow `@odata.nextLink` until `limit` messages are in hand or {@link TEAMS_GRAPH_MAX_PAGES}
|
|
241
|
+
* pages are exhausted — a hard bound so a busy channel/thread can't fan out unboundedly (mirrors
|
|
242
|
+
* discord-backscroll.ts's DISCORD_RAW_FETCH_MAX_PAGES). Returns the raw messages, unsorted
|
|
243
|
+
* (teamsLinesFromRaw sorts). Propagates any requester error (fail-loud). */
|
|
244
|
+
export async function fetchGraphPages(req, initialUrl, limit) {
|
|
245
|
+
const all = [];
|
|
246
|
+
let url = initialUrl;
|
|
247
|
+
for (let page = 0; page < TEAMS_GRAPH_MAX_PAGES && url && all.length < limit; page++) {
|
|
248
|
+
const json = (await req(url));
|
|
249
|
+
all.push(...(Array.isArray(json.value) ? json.value : []));
|
|
250
|
+
url = typeof json["@odata.nextLink"] === "string" ? json["@odata.nextLink"] : undefined;
|
|
251
|
+
}
|
|
252
|
+
return all;
|
|
253
|
+
}
|
|
254
|
+
export function channelMessagesUrl(teamId, channelId) {
|
|
255
|
+
return `${GRAPH_BASE}/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages?$top=${TEAMS_GRAPH_PAGE_SIZE}`;
|
|
256
|
+
}
|
|
257
|
+
export function threadRepliesUrl(teamId, channelId, rootMessageId) {
|
|
258
|
+
return `${GRAPH_BASE}/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(rootMessageId)}/replies?$top=${TEAMS_GRAPH_PAGE_SIZE}`;
|
|
259
|
+
}
|
|
260
|
+
// --- the Teams-specific channel allowlist (channel grain, not the ephemeral conversation grain) --
|
|
261
|
+
//
|
|
262
|
+
// channel-read.ts's shared allowedSurfaceIdsFrom keys "own" off the raw surfaceKey — correct for
|
|
263
|
+
// Slack/Discord (whose surfaceKey IS the readable unit) but wrong for Teams, whose surfaceKey is a
|
|
264
|
+
// per-reply-chain CONVERSATION id (bindTeams's `surface`), not the stable CHANNEL grain a Graph read
|
|
265
|
+
// actually operates on. domainKeyOf resolves that stable grain already (teams-identity.ts's
|
|
266
|
+
// `domainSurface`; see memory/resolve.ts's own domainKeyOf comment) — this reuses THAT, not the raw
|
|
267
|
+
// surfaceKey. Kept local rather than importing channel-read.ts's helper: channel-read.ts registers
|
|
268
|
+
// this adapter, so importing back would cycle the module graph (the same DAG discipline
|
|
269
|
+
// discord-backscroll.ts's isDiscordBackscrollVisible note documents).
|
|
270
|
+
//
|
|
271
|
+
// SANITIZATION QUIRK (Teams-only): a Teams channel id looks like `19:abc…@thread.tacv2` — binding.ts's
|
|
272
|
+
// `bindFrom` runs every surface/domainSurface segment through its filesystem-safe `seg()` sanitizer
|
|
273
|
+
// (`[^a-zA-Z0-9._-]+` → "-"), so the value domainKeyOf and a domain's exact `reach` entries actually carry is the SANITIZED
|
|
274
|
+
// form (e.g. `19-abc-thread.tacv2`), not the raw Graph id. Slack/Discord ids are already safe (no
|
|
275
|
+
// sanitization visible), so this is invisible there; Teams is the first surface where it bites. The
|
|
276
|
+
// GRAPH CALL needs the RAW id verbatim (Graph literally requires the colons/`@`), so `readChannel`/
|
|
277
|
+
// `readThread` pass the caller's raw `channel` straight into the URL — but the ALLOWLIST check below
|
|
278
|
+
// sanitizes it first (mirroring binding.ts's `seg()`, kept local — same DAG-avoidance rationale as
|
|
279
|
+
// above) so it compares like-for-like against the already-sanitized grain. Consequence for deployers:
|
|
280
|
+
// a Teams domain's `reach` entries must list the SANITIZED channel id (what domainKeyOf actually
|
|
281
|
+
// produces), not the raw `19:…@thread.tacv2` string — documented in docs/deploy/surfaces/teams.md.
|
|
282
|
+
const UNSAFE_SEGMENT_CHARS = /[^a-zA-Z0-9._-]+/g;
|
|
283
|
+
/** Mirrors binding.ts's private `seg()` sanitizer exactly (same regex, same trim, same 100-char
|
|
284
|
+
* cap) so a raw Teams channel id compares correctly against the already-sanitized domain grain.
|
|
285
|
+
* Pure; exported for the allowlist test. */
|
|
286
|
+
export function sanitizeTeamsChannelId(value) {
|
|
287
|
+
return value.trim().replace(UNSAFE_SEGMENT_CHARS, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 100);
|
|
288
|
+
}
|
|
289
|
+
/** The Teams CHANNEL ids this session may read (SANITIZED — see the header note): the current
|
|
290
|
+
* channel (its channel grain) plus every Teams channel this session's explicit domain lists. Pure
|
|
291
|
+
* but for the injected `domains` — mirrors channel-read.ts's allowedSurfaceIdsFrom shape so it is
|
|
292
|
+
* unit-testable the same way. */
|
|
293
|
+
export function allowedTeamsChannelIds(channelGrainKey, domains) {
|
|
294
|
+
const ids = new Set();
|
|
295
|
+
const own = channelGrainKey.split("/")[2];
|
|
296
|
+
if (own)
|
|
297
|
+
ids.add(own);
|
|
298
|
+
const domain = domainForChannel(channelGrainKey, domains);
|
|
299
|
+
if (domain) {
|
|
300
|
+
for (const key of domainRoster(domain)) {
|
|
301
|
+
const parts = key.split("/");
|
|
302
|
+
if (parts.length === 3 && parts[0] === "teams" && parts[2])
|
|
303
|
+
ids.add(parts[2]);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return { ids, domain: domain?.name };
|
|
307
|
+
}
|
|
308
|
+
/** Assert a requested (raw) Teams channel id is readable by this session — else AUDIT the refusal
|
|
309
|
+
* and throw (fail closed), the same ChannelScopeError shape slack-read/channel-read raise.
|
|
310
|
+
* Sanitizes `channelId` before comparing (see the header note) — the RAW value is what the Graph
|
|
311
|
+
* call itself uses, and the SANITIZED one is what the row records, so a row compares like-for-like
|
|
312
|
+
* against the domain's `reach`. The decision is unchanged; only the row is new. */
|
|
313
|
+
export async function assertTeamsChannelAllowed(ctx, channelId, deps) {
|
|
314
|
+
const p = resolvePartition(ctx);
|
|
315
|
+
const domains = getConfig().loadDomains();
|
|
316
|
+
const callerKey = domainKeyOf(p);
|
|
317
|
+
const { ids, domain } = allowedTeamsChannelIds(callerKey, domains);
|
|
318
|
+
const wanted = sanitizeTeamsChannelId(channelId);
|
|
319
|
+
if (ids.has(wanted))
|
|
320
|
+
return;
|
|
321
|
+
// The refused read never reached Graph, so no Microsoft boundary governed THIS call — OE's own
|
|
322
|
+
// fence did. The marker still belongs on the row: it says which boundary governs the reads this
|
|
323
|
+
// deployment DOES let through, which is the fact a defender reading a refusal wants next
|
|
324
|
+
// (OPE-428). `"unknown"` when no Graph token has been minted in this process yet — the fence must
|
|
325
|
+
// not make a network call to answer.
|
|
326
|
+
await auditChannelReadRefusal(ctx, { platform: "teams", surfaceId: wanted, callerKey, callerDomain: domain, domains, boundary: teamsReadBoundary() }, deps);
|
|
327
|
+
const scope = domain ? `domain "${domain}"` : "this channel's (no multi-channel domain)";
|
|
328
|
+
throw new ChannelScopeError(`channel "${channelId}" is not in ${scope} teams read scope. The agent may only read channels in its ` +
|
|
329
|
+
`memory domain (memory/config/domains.ts). Add the channel to the domain's \`channels\` to grant it.`);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* The current turn's team id IN GRAPH'S SPELLING — the Microsoft 365 group id every `/teams/{id}/…`
|
|
333
|
+
* URL below takes.
|
|
334
|
+
*
|
|
335
|
+
* 🔴 THE BINDING CARRIES THE OTHER ONE. `bindTeams`'s `target.teamId` is the inbound activity's
|
|
336
|
+
* `channelData.team.id`, which is the BOT CONNECTOR's team id (`19:…@thread.tacv2`) — Graph keeps
|
|
337
|
+
* that spelling as a team's `internalId` and addresses nothing by it. So the binding's id is
|
|
338
|
+
* RESOLVED through the Connector before it reaches a Graph URL (channels/teams-graph-team-id.ts,
|
|
339
|
+
* OPE-1808); the resolution is cached per team, so a read pays at most one extra round trip per
|
|
340
|
+
* process. See the file header for why a DIFFERENT channel in the domain is assumed to share this
|
|
341
|
+
* team (Graph needs both team + channel; the domain allowlist tracks only the latter).
|
|
342
|
+
*/
|
|
343
|
+
async function currentGraphTeamId(ctx) {
|
|
344
|
+
const t = bindFrom(ctx).target;
|
|
345
|
+
const botTeamId = typeof t?.teamId === "string" && t.teamId.length > 0 ? t.teamId : undefined;
|
|
346
|
+
const serviceUrl = typeof t?.serviceUrl === "string" && t.serviceUrl.length > 0 ? t.serviceUrl : undefined;
|
|
347
|
+
return await teamsGraphTeamId(botTeamId, serviceUrl);
|
|
348
|
+
}
|
|
349
|
+
function noTeamIdError(what) {
|
|
350
|
+
return new Error(`read: couldn't resolve this Teams channel's team id from the live session — Teams ${what} need an engaged team-channel turn, ` +
|
|
351
|
+
`and the team's Microsoft 365 group id must be resolvable from the Bot Connector.`);
|
|
352
|
+
}
|
|
353
|
+
// --- the OPE-474 non-member author leg ---------------------------------------------------------
|
|
354
|
+
//
|
|
355
|
+
// 🔴 THE REGISTER IS THE WHOLE LEG HERE, AND THAT IS A LIMIT WORTH STATING RATHER THAN PAPERING
|
|
356
|
+
// OVER. Teams CAN refute an author in principle — Entra reports `userType: "guest"` — and the WAKE
|
|
357
|
+
// path already reads it (channels/teams.ts `resolveTeamsUserType`). That probe is not reachable from
|
|
358
|
+
// a read, for two reasons that are both structural:
|
|
359
|
+
// • it is keyed by CONVERSATION (`/v3/conversations/{id}/members/{userId}`) and needs the Bot
|
|
360
|
+
// Connector handle off the inbound activity, which a read tool does not hold;
|
|
361
|
+
// • a read may target a DIFFERENT channel from the turn's own conversation, so the current
|
|
362
|
+
// conversation's roster is not the right question to ask about that page's authors anyway.
|
|
363
|
+
// Adding a Graph `/users/{id}` call would be a NEW privileged permission (`User.Read.All`) for one
|
|
364
|
+
// taint bit, so it is deliberately not invented here. A Teams deployment that wants the author leg
|
|
365
|
+
// on reads arms the author-trust register, exactly as Discord and Notion do.
|
|
366
|
+
//
|
|
367
|
+
// The consequence, stated plainly: on an UNARMED Teams tenant a channel read never marks the turn,
|
|
368
|
+
// even over a guest's message. The fence still rides every read, and the wake leg still refutes the
|
|
369
|
+
// same guest when they SPEAK. Under-marking is the direction this whole plane fails in.
|
|
370
|
+
// --- the adapter ------------------------------------------------------------------------------
|
|
371
|
+
export const teamsReadAdapter = {
|
|
372
|
+
platform: "teams",
|
|
373
|
+
/**
|
|
374
|
+
* OPE-929 — Teams' own spelling of "which room and which thread is this turn in", and the one of
|
|
375
|
+
* the three that no core-side guess could have got right.
|
|
376
|
+
*
|
|
377
|
+
* 🔴 THE BINDING'S CONVERSATION IS NOT THE READ CONTAINER. A Teams CHANNEL activity binds the
|
|
378
|
+
* REPLY CHAIN as its `surface` / `anchorChannelId` (the conversation id carries
|
|
379
|
+
* `;messageid=<root>`), while both Graph read URLs below take the CHANNEL id
|
|
380
|
+
* (`/teams/{team}/channels/{channel}/messages[/{root}/replies]`). So the container is
|
|
381
|
+
* `target.channelId` and the thread root is the `;messageid=` half of the conversation id — the
|
|
382
|
+
* two halves of one value, pulled apart here rather than anywhere else.
|
|
383
|
+
*
|
|
384
|
+
* A personal or group chat carries no channel id and no reply-chain suffix, so it reports no
|
|
385
|
+
* address at all: Graph's chat-message read is delegated-only, and this adapter reads channels.
|
|
386
|
+
* The read then refuses loudly, which is the honest answer for a surface it cannot serve.
|
|
387
|
+
*/
|
|
388
|
+
wakeAddress(ctx) {
|
|
389
|
+
const b = bindFrom(ctx);
|
|
390
|
+
const container = typeof b.target?.["channelId"] === "string" ? b.target["channelId"] : undefined;
|
|
391
|
+
if (!container)
|
|
392
|
+
return undefined;
|
|
393
|
+
const conversation = typeof b.target?.["conversationId"] === "string" ? b.target["conversationId"] : undefined;
|
|
394
|
+
const root = conversation ? teamsReplyChainRoot(conversation) : undefined;
|
|
395
|
+
return { container, ...(root ? { threadRoot: root } : {}) };
|
|
396
|
+
},
|
|
397
|
+
async readChannel(ctx, channel, limit, req = liveTeamsGraphRequest) {
|
|
398
|
+
await assertTeamsChannelAllowed(ctx, channel);
|
|
399
|
+
const teamId = await currentGraphTeamId(ctx);
|
|
400
|
+
if (!teamId)
|
|
401
|
+
throw noTeamIdError("channel reads");
|
|
402
|
+
const n = limit ?? TEAMS_CHANNEL_DEFAULT_LIMIT;
|
|
403
|
+
const raw = await fetchGraphPages(req, channelMessagesUrl(teamId, channel), n);
|
|
404
|
+
return teamsLinesFromRaw(raw).slice(-n); // most recent n, oldest-first (matches Slack/Discord)
|
|
405
|
+
},
|
|
406
|
+
async readThread(ctx, channel, threadTs, limit, req = liveTeamsGraphRequest) {
|
|
407
|
+
await assertTeamsChannelAllowed(ctx, channel);
|
|
408
|
+
const teamId = await currentGraphTeamId(ctx);
|
|
409
|
+
if (!teamId)
|
|
410
|
+
throw noTeamIdError("thread reads");
|
|
411
|
+
const n = limit ?? TEAMS_THREAD_DEFAULT_LIMIT;
|
|
412
|
+
const raw = await fetchGraphPages(req, threadRepliesUrl(teamId, channel, threadTs), n);
|
|
413
|
+
return teamsLinesFromRaw(raw).slice(-n);
|
|
414
|
+
},
|
|
415
|
+
/** OPE-474 / OPE-534 — the author-trust register only; see the note above for why the Entra probe
|
|
416
|
+
* is not reachable from a read. No `memberships` leg: an armed Teams tenant matches on principal
|
|
417
|
+
* keys (`teams/<entraObjectId>`), which is what a Teams declaration lists. It answers with the
|
|
418
|
+
* refuted AUTHOR IDS, because core reads the one answer twice: the turn mark and the `withhold`
|
|
419
|
+
* body replacement. */
|
|
420
|
+
async refutedAuthorIds(ctx, lines) {
|
|
421
|
+
return await refutedReadAuthorIds(lines, resolvePartition(ctx).workspaceKey);
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
//# sourceMappingURL=teams-read.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teams-read.js","sourceRoot":"","sources":["../../../agent/channels/teams-read.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type TeamsHandle } from "eve/channels/teams";
|
|
2
|
+
/** The subset of eve's `TeamsApiResponse` the probe reads. */
|
|
3
|
+
export interface TeamsRosterResponse {
|
|
4
|
+
readonly ok: boolean;
|
|
5
|
+
readonly body: unknown;
|
|
6
|
+
}
|
|
7
|
+
/** One GET against the Bot Connector, addressed at an already-resolved serviceUrl. Injected so the
|
|
8
|
+
* probe's caching, its fail-open branches and its path are testable with no network. */
|
|
9
|
+
export type TeamsRosterProbe = (path: string) => Promise<TeamsRosterResponse>;
|
|
10
|
+
/** The roster member path for one (conversation, user) — the ONE place it is built. */
|
|
11
|
+
export declare function teamsRosterMemberPath(conversationId: string, userId: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* The probe a NATIVE turn has: `teamsChannel` hands the handler a handle that already holds both the
|
|
14
|
+
* credential and the conversation address, so its `request` escape hatch is the whole transport.
|
|
15
|
+
*/
|
|
16
|
+
export declare function teamsHandleRosterProbe(teams: Pick<TeamsHandle, "request">): TeamsRosterProbe;
|
|
17
|
+
/**
|
|
18
|
+
* The probe the FRONT has. The front route is a plain HTTP handler with no channel handle, so it
|
|
19
|
+
* addresses the connector by the activity's own `serviceUrl` and mints the token through eve's
|
|
20
|
+
* `callTeamsConnectorApi` — the SAME client-credentials mint the native handle uses, not a second
|
|
21
|
+
* bespoke credential path.
|
|
22
|
+
*
|
|
23
|
+
* 🔴 THE MINT MUST BE TENANT-SCOPED. eve's `resolveTeamsAccessToken` reads `MICROSOFT_TENANT_ID` /
|
|
24
|
+
* `TEAMS_TENANT_ID` and falls back to the `botframework.com` placeholder tenant only when neither is
|
|
25
|
+
* set. A single-tenant app registration refuses the placeholder with AADSTS700016, so a deployment
|
|
26
|
+
* whose bot app is single-tenant must set the tenant id. The tenant is never hardcoded here.
|
|
27
|
+
*
|
|
28
|
+
* `undefined` when the activity carries no serviceUrl — there is nothing to address, so there is no
|
|
29
|
+
* verdict to resolve.
|
|
30
|
+
*/
|
|
31
|
+
export declare function teamsServiceUrlRosterProbe(serviceUrl: string | undefined): TeamsRosterProbe | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* The acting user's tenant membership type, cached and fail-open. `undefined` means NEVER DETERMINED —
|
|
34
|
+
* the caller then leaves `userType` unset, and every downstream verdict is byte-identical to a
|
|
35
|
+
* deployment with no probe at all.
|
|
36
|
+
*
|
|
37
|
+
* 🔴 OPE-572 — THE CACHE KEY AND THE WIRE CALL USE DIFFERENT IDS, ON PURPOSE.
|
|
38
|
+
* - THE KEY strips the reply-chain suffix. On a CHANNEL activity `conversation.id` is the
|
|
39
|
+
* REPLY-CHAIN id, so keying on it verbatim made a busy channel pay one connector call per
|
|
40
|
+
* (THREAD, user) per TTL instead of per (CHANNEL, user). The verdict is a property of the
|
|
41
|
+
* TENANT — a user is a guest of the tenant, never of one reply chain — so the coarser key
|
|
42
|
+
* answers the same question with fewer calls. Two reply chains collapsing onto one channel key
|
|
43
|
+
* is the POINT, not a collision.
|
|
44
|
+
* - THE WIRE CALL keeps the id EXACTLY as the caller passed it. That is the shape the 2026-08-15
|
|
45
|
+
* live-tenant probe measured. Whether the connector ALSO accepts a thread-qualified id on
|
|
46
|
+
* `/v3/conversations/{id}/members/{userId}` is NOT established, and one live GET against a real
|
|
47
|
+
* threaded channel reply is what settles it. Until then, changing the wire shape would trade a
|
|
48
|
+
* measured behaviour for an unmeasured one. A refusal fails open like any other non-2xx.
|
|
49
|
+
* scripts/teams-roster-probe-test.ts pins BOTH halves — the shared key and the unstripped path.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveTeamsUserType(probe: TeamsRosterProbe | undefined, conversationId: string | undefined, userId: string | undefined): Promise<"guest" | "member" | undefined>;
|
|
52
|
+
/** Test seam: drop the cached roster verdicts (a fresh test must not inherit another's). */
|
|
53
|
+
export declare function _resetTeamsUserTypeCacheForTest(): void;
|
|
54
|
+
//# sourceMappingURL=teams-roster.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teams-roster.d.ts","sourceRoot":"","sources":["../../../agent/channels/teams-roster.ts"],"names":[],"mappings":"AA8BA,OAAO,EAAyB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAK7E,8DAA8D;AAC9D,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;yFACyF;AACzF,MAAM,MAAM,gBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAE9E,uFAAuF;AACvF,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEpF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,GAAG,gBAAgB,CAM5F;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,GAAG,SAAS,CAGvG;AAcD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,gBAAgB,GAAG,SAAS,EACnC,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,OAAO,CAAC,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC,CAczC;AAED,4FAA4F;AAC5F,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// THE TEAMS ROSTER PROBE (OPE-60 mechanism, OPE-493 reach) — "is the acting user a tenant GUEST?".
|
|
2
|
+
//
|
|
3
|
+
// The inbound Bot Framework activity carries NO guest bit (`from.role` is only "user"/"bot"), so the
|
|
4
|
+
// answer comes from the Bot Connector ROSTER: `GET {serviceUrl}/v3/conversations/{id}/members/{userId}`
|
|
5
|
+
// returns a member record whose `userRole` is "guest" for a B2B guest and "user" for a tenant member.
|
|
6
|
+
// MEASURED against a live tenant on 2026-08-15, so this is a real discriminator, not a documented one.
|
|
7
|
+
// It needs no Microsoft Graph app registration and no new privileged permission — the bot's own
|
|
8
|
+
// client-credentials token already carries it.
|
|
9
|
+
//
|
|
10
|
+
// WHY THIS IS ITS OWN MODULE. Two live paths need the probe and neither can host it:
|
|
11
|
+
// - `teams-identity.ts` imports no eve types on purpose (its header states it), which keeps the
|
|
12
|
+
// security-critical mapping trivially unit-testable. The probe needs eve's connector client, so
|
|
13
|
+
// putting it there would take that property away.
|
|
14
|
+
// - `teams.ts` (the native channel) already imports `teams-front.ts` for `frontTeamsChannel`, so
|
|
15
|
+
// the front cannot import back from `teams.ts` without a cycle.
|
|
16
|
+
// This module imports `teams-identity.ts` and eve, and both call sites import this module. One
|
|
17
|
+
// direction, no cycle, and `teamsUserTypeFromMember` stays the ONE reader of Teams' guest vocabulary.
|
|
18
|
+
//
|
|
19
|
+
// 🔴 THE PATH NEEDS ITS LEADING SLASH. eve composes the connector URL as
|
|
20
|
+
// `trimTrailingSlash(serviceUrl) + path` (node_modules/eve/dist/src/public/channels/teams/api.js), and
|
|
21
|
+
// eve's own callers pass "/v3/…". A path written without the slash composes
|
|
22
|
+
// `https://smba.trafficmanager.net/emeav3/conversations/…` — a malformed URL that answers non-2xx, so
|
|
23
|
+
// the probe fails open and reports NO verdict on every call. `teamsRosterMemberPath` is the ONE place
|
|
24
|
+
// the path is built, and its leading slash is pinned by a test.
|
|
25
|
+
//
|
|
26
|
+
// FAIL-OPEN EVERYWHERE. No serviceUrl, no conversation, no user, an HTTP error, a transport throw or
|
|
27
|
+
// an unrecognised body all yield `undefined` — "never determined". `teamsTrust` then leaves the
|
|
28
|
+
// existing channel/vouch verdict intact, which is the pre-probe behaviour. The probe only ever
|
|
29
|
+
// DEMOTES; it can never raise trust.
|
|
30
|
+
import { callTeamsConnectorApi } from "eve/channels/teams";
|
|
31
|
+
import { dmTrustTtlMs } from "@open-everywhere/core/agent/adapter-api.js";
|
|
32
|
+
import { teamsUserTypeFromMember } from "./teams-identity.js";
|
|
33
|
+
import { teamsChannelGrainConversationId } from "./teams-conversation-ref.js";
|
|
34
|
+
/** The roster member path for one (conversation, user) — the ONE place it is built. */
|
|
35
|
+
export function teamsRosterMemberPath(conversationId, userId) {
|
|
36
|
+
return `/v3/conversations/${encodeURIComponent(conversationId)}/members/${encodeURIComponent(userId)}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The probe a NATIVE turn has: `teamsChannel` hands the handler a handle that already holds both the
|
|
40
|
+
* credential and the conversation address, so its `request` escape hatch is the whole transport.
|
|
41
|
+
*/
|
|
42
|
+
export function teamsHandleRosterProbe(teams) {
|
|
43
|
+
// The body MUST be absent, not `{}`: eve's request forwards any defined body into the fetch init
|
|
44
|
+
// (`body: "{}"`), and fetch throws `Request with GET/HEAD method cannot have body` — a throw the
|
|
45
|
+
// resolver's fail-open catch then swallows, leaving this probe permanently inert. The handle type
|
|
46
|
+
// requires a body argument, so the absence is spelled as a cast.
|
|
47
|
+
return (path) => teams.request(path, undefined, { method: "GET" });
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The probe the FRONT has. The front route is a plain HTTP handler with no channel handle, so it
|
|
51
|
+
* addresses the connector by the activity's own `serviceUrl` and mints the token through eve's
|
|
52
|
+
* `callTeamsConnectorApi` — the SAME client-credentials mint the native handle uses, not a second
|
|
53
|
+
* bespoke credential path.
|
|
54
|
+
*
|
|
55
|
+
* 🔴 THE MINT MUST BE TENANT-SCOPED. eve's `resolveTeamsAccessToken` reads `MICROSOFT_TENANT_ID` /
|
|
56
|
+
* `TEAMS_TENANT_ID` and falls back to the `botframework.com` placeholder tenant only when neither is
|
|
57
|
+
* set. A single-tenant app registration refuses the placeholder with AADSTS700016, so a deployment
|
|
58
|
+
* whose bot app is single-tenant must set the tenant id. The tenant is never hardcoded here.
|
|
59
|
+
*
|
|
60
|
+
* `undefined` when the activity carries no serviceUrl — there is nothing to address, so there is no
|
|
61
|
+
* verdict to resolve.
|
|
62
|
+
*/
|
|
63
|
+
export function teamsServiceUrlRosterProbe(serviceUrl) {
|
|
64
|
+
if (!serviceUrl)
|
|
65
|
+
return undefined;
|
|
66
|
+
return (path) => callTeamsConnectorApi({ serviceUrl, path, method: "GET" });
|
|
67
|
+
}
|
|
68
|
+
// Cached per (CHANNEL, user) because tenant membership changes rarely, on the SAME TTL knob the
|
|
69
|
+
// Slack DM-partner cache uses (`identity.dmTrustTtlMs`). Only a POSITIVE verdict is cached: caching
|
|
70
|
+
// "no verdict" would pin a transport blip for the whole TTL.
|
|
71
|
+
const teamsUserTypeCache = new Map();
|
|
72
|
+
// The channel-grain strip lives in channels/teams-conversation-ref.ts, the module whose subject is
|
|
73
|
+
// Teams conversation addressing, and that header states why it is spelled in this repo at all
|
|
74
|
+
// (eve does not re-export its own copy).
|
|
75
|
+
//
|
|
76
|
+
// 🔴 IN THIS FILE IT IS A CACHE KEY AND NOTHING ELSE. It must never reach the connector — see
|
|
77
|
+
// {@link resolveTeamsUserType}.
|
|
78
|
+
/**
|
|
79
|
+
* The acting user's tenant membership type, cached and fail-open. `undefined` means NEVER DETERMINED —
|
|
80
|
+
* the caller then leaves `userType` unset, and every downstream verdict is byte-identical to a
|
|
81
|
+
* deployment with no probe at all.
|
|
82
|
+
*
|
|
83
|
+
* 🔴 OPE-572 — THE CACHE KEY AND THE WIRE CALL USE DIFFERENT IDS, ON PURPOSE.
|
|
84
|
+
* - THE KEY strips the reply-chain suffix. On a CHANNEL activity `conversation.id` is the
|
|
85
|
+
* REPLY-CHAIN id, so keying on it verbatim made a busy channel pay one connector call per
|
|
86
|
+
* (THREAD, user) per TTL instead of per (CHANNEL, user). The verdict is a property of the
|
|
87
|
+
* TENANT — a user is a guest of the tenant, never of one reply chain — so the coarser key
|
|
88
|
+
* answers the same question with fewer calls. Two reply chains collapsing onto one channel key
|
|
89
|
+
* is the POINT, not a collision.
|
|
90
|
+
* - THE WIRE CALL keeps the id EXACTLY as the caller passed it. That is the shape the 2026-08-15
|
|
91
|
+
* live-tenant probe measured. Whether the connector ALSO accepts a thread-qualified id on
|
|
92
|
+
* `/v3/conversations/{id}/members/{userId}` is NOT established, and one live GET against a real
|
|
93
|
+
* threaded channel reply is what settles it. Until then, changing the wire shape would trade a
|
|
94
|
+
* measured behaviour for an unmeasured one. A refusal fails open like any other non-2xx.
|
|
95
|
+
* scripts/teams-roster-probe-test.ts pins BOTH halves — the shared key and the unstripped path.
|
|
96
|
+
*/
|
|
97
|
+
export async function resolveTeamsUserType(probe, conversationId, userId) {
|
|
98
|
+
if (!probe || !conversationId || !userId)
|
|
99
|
+
return undefined; // nothing to address / to key on → no verdict
|
|
100
|
+
const key = `${teamsChannelGrainConversationId(conversationId)}:${userId}`;
|
|
101
|
+
const hit = teamsUserTypeCache.get(key);
|
|
102
|
+
if (hit && Date.now() - hit.at < dmTrustTtlMs())
|
|
103
|
+
return hit.userType;
|
|
104
|
+
try {
|
|
105
|
+
const res = await probe(teamsRosterMemberPath(conversationId, userId));
|
|
106
|
+
if (!res.ok)
|
|
107
|
+
return undefined; // 4xx/5xx (e.g. member not resolvable) → no verdict
|
|
108
|
+
const userType = teamsUserTypeFromMember(res.body);
|
|
109
|
+
if (userType)
|
|
110
|
+
teamsUserTypeCache.set(key, { userType, at: Date.now() });
|
|
111
|
+
return userType;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return undefined; // transport error → no verdict (mirrors the Slack users.info catch)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Test seam: drop the cached roster verdicts (a fresh test must not inherit another's). */
|
|
118
|
+
export function _resetTeamsUserTypeCacheForTest() {
|
|
119
|
+
teamsUserTypeCache.clear();
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=teams-roster.js.map
|