@open-everywhere/provider-teams 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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,219 @@
|
|
|
1
|
+
// registerTeamsProvider — the ONE boot entry of @open-everywhere/provider-teams. The app's
|
|
2
|
+
// adapter-wiring module (agent/lib/register-adapters.ts) imports this STATICALLY and calls it once
|
|
3
|
+
// at boot; every seam the teams bundle implements registers here and nowhere else. Static imports
|
|
4
|
+
// only: eve bundles the agent definition and its whole import graph into one rolldown chunk — a
|
|
5
|
+
// literal dynamic import() code-splits and trips eve's "expected one authored module" invariant.
|
|
6
|
+
//
|
|
7
|
+
// What teams registers (and what it deliberately does NOT):
|
|
8
|
+
// • identity mapper — bindTeams (chat-surface plane; channels/teams-identity.ts)
|
|
9
|
+
// • capability manifest — midTurnPost via the host-side Connector write seam
|
|
10
|
+
// (channels/teams-connector.ts, OPE-1237); no reaction primitive
|
|
11
|
+
// • post arm — teamsPostArm (mid-turn reply + unified reply + redrive helpers + the
|
|
12
|
+
// 1:1 DM delivery arm, OPE-887)
|
|
13
|
+
// • wake-origin codec — the stored-data teams arm (channels/teams-origin-codec.ts, which also
|
|
14
|
+
// owns the Bot-Connector serviceUrl allowlist isTrustedTeamsServiceUrl)
|
|
15
|
+
// • read adapter — teamsReadAdapter (Microsoft Graph channel/thread history reads)
|
|
16
|
+
// • routine dispatch — resolveTeamsTarget (proactive routine fires need the captured Bot
|
|
17
|
+
// Framework conversation reference + the teams channel object, both of
|
|
18
|
+
// which now live in this bundle, so its resolver moved here too)
|
|
19
|
+
// • teams BRIDGE — the bundle→core seam for the routine fire-eligibility leg's Graph
|
|
20
|
+
// checks (OPE-522, mirroring the slack provider's own bridge):
|
|
21
|
+
// accountEnabled + channel-membership, keyed on the creator's Azure AD
|
|
22
|
+
// object id (channels/teams-fire-eligibility.ts, core channels/
|
|
23
|
+
// teams-bridge.ts)
|
|
24
|
+
// • bot display name — a HANDLE line on a Teams-pinned session, not identity since OPE-610,
|
|
25
|
+
// read from the app's OWN service principal via Graph (OPE-587;
|
|
26
|
+
// channels/teams-bot-name.ts, core identity/bot-display-name.ts)
|
|
27
|
+
// • account directory — the Bot Connector roster of the teams this deployment has seen, for
|
|
28
|
+
// the identity-linking directory transport (OPE-887;
|
|
29
|
+
// channels/teams-directory.ts). NOT Microsoft Graph — Graph has no `29:`
|
|
30
|
+
// id and a link keys on one; the header of that file carries the whole
|
|
31
|
+
// argument
|
|
32
|
+
// • surface message ops — teamsMessageOps: postText/editMessage (OPE-899), the transport behind
|
|
33
|
+
// the stop acknowledgement and the approval decision notice. Resolves
|
|
34
|
+
// its own serviceUrl from the `conversation`-grain address store
|
|
35
|
+
// (channels/teams-conversation-ref.ts), since the registry's callers
|
|
36
|
+
// hand it only a bare conversation id.
|
|
37
|
+
// • reaction surface — INBOUND ONLY (OPE-1502). Teams delivers `messageReaction` activities
|
|
38
|
+
// and refuses an application identity the outbound WRITE, so the
|
|
39
|
+
// feedback ingest is wired (channels/teams-reaction-feedback.ts) while
|
|
40
|
+
// the capability manifest still reads `reactions: false`
|
|
41
|
+
// • NO attachment resolver — the Teams surface carries no attachment plane today
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
registerIdentityMapper,
|
|
45
|
+
registerCapabilityManifest,
|
|
46
|
+
registerSurfaceParity,
|
|
47
|
+
registerProviderEvents,
|
|
48
|
+
registerReactionSurface,
|
|
49
|
+
registerPostArm,
|
|
50
|
+
registerWakeOriginCodec,
|
|
51
|
+
registerReadAdapter,
|
|
52
|
+
registerSeatCatchUp,
|
|
53
|
+
registerRoutineDispatch,
|
|
54
|
+
registerTeamsBridge,
|
|
55
|
+
registerBotDisplayName,
|
|
56
|
+
registerAccountDirectory,
|
|
57
|
+
registerSurfaceMessageOps,
|
|
58
|
+
type DispatchTarget,
|
|
59
|
+
type Routine,
|
|
60
|
+
type UnifiedWakeOrigin,
|
|
61
|
+
registerProviderDocs,
|
|
62
|
+
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
63
|
+
import { PROVIDER_DOCS } from "./docs-generated.js";
|
|
64
|
+
import { TEAMS_SURFACE_PARITY } from "./surface-parity.js";
|
|
65
|
+
import { TEAMS_PROVIDER_EVENTS } from "./provider-events.js";
|
|
66
|
+
import { registerTeamsOps } from "./ops.js";
|
|
67
|
+
import { teamsMessageOps } from "./channels/teams-message-ops.js";
|
|
68
|
+
import teamsChannel from "./channels/teams.js";
|
|
69
|
+
import { bindTeams, teamsMode } from "./channels/teams-identity.js";
|
|
70
|
+
import { teamsPostArm } from "./channels/teams-post-arm.js";
|
|
71
|
+
import { teamsWakeOriginCodec } from "./channels/teams-origin-codec.js";
|
|
72
|
+
import { teamsReadAdapter } from "./channels/teams-read.js";
|
|
73
|
+
import { teamsSeatCatchUpFetch, TEAMS_TIMESTAMP_PRECISION_MS } from "./channels/teams-catch-up.js";
|
|
74
|
+
import { liveTeamsRoomMembership, liveTeamsAccountActive } from "./channels/teams-fire-eligibility.js";
|
|
75
|
+
import { teamsBotDisplayName } from "./channels/teams-bot-name.js";
|
|
76
|
+
import { fetchTeamsDirectory } from "./channels/teams-directory.js";
|
|
77
|
+
|
|
78
|
+
/** One stored-target field as a non-empty string, or undefined (never a coerced non-string — the
|
|
79
|
+
* store row is data, validated on the way out, not trusted). Local copy of the core dispatcher's
|
|
80
|
+
* private helper (a two-line structural read; not a barrel port). */
|
|
81
|
+
function storedString(t: Routine["target"], key: string): string | undefined {
|
|
82
|
+
const v = t?.[key];
|
|
83
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resolve a Teams routine to its proactive-fire DispatchTarget — MOVED VERBATIM out of core's
|
|
88
|
+
* schedules/dispatch.ts (OPE-114 stage 4). Proactive messaging on Teams REQUIRES a Bot Framework
|
|
89
|
+
* conversation reference (serviceUrl + conversation id) captured on a prior INBOUND turn — you
|
|
90
|
+
* cannot cold-send to a Teams user/channel the bot has never heard from. bindTeams captures exactly
|
|
91
|
+
* that off every inbound activity, and create_routine stores it on the routine, so by construction
|
|
92
|
+
* every Teams routine created in-thread carries its reference. A routine WITHOUT one (both fields
|
|
93
|
+
* are required by eve's teamsChannel().receive, which throws otherwise) fails closed to dry-run
|
|
94
|
+
* (null) here.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveTeamsTarget(r: Routine): DispatchTarget | null {
|
|
97
|
+
const serviceUrl = storedString(r.target, "serviceUrl");
|
|
98
|
+
const conversationId = storedString(r.target, "conversationId");
|
|
99
|
+
if (!serviceUrl || !conversationId) return null;
|
|
100
|
+
const conversationType = storedString(r.target, "conversationType");
|
|
101
|
+
const channelId = storedString(r.target, "channelId");
|
|
102
|
+
const teamId = storedString(r.target, "teamId");
|
|
103
|
+
// The creator's Azure AD object id (OPE-522) — stamped by bindTeams onto the routine's STORED
|
|
104
|
+
// target at creation time (channels/teams-identity.ts). Read back here into ATTRIBUTES, never
|
|
105
|
+
// into the fresh `target` below: `target` is what actually rides into the Bot Connector send
|
|
106
|
+
// (routine-dispatch.ts's DispatchTarget doc — "stored-target extras... feed attributes instead,
|
|
107
|
+
// never the adapter call"), and this value has no business in a Connector payload. The
|
|
108
|
+
// dispatcher's fire-eligibility leg reads it off `attributes.aad_object_id`.
|
|
109
|
+
const aadObjectId = storedString(r.target, "aadObjectId");
|
|
110
|
+
// tenant: the stored raw fact, else the surfaceKey's workspace segment (a tenant GUID is seg()-safe,
|
|
111
|
+
// so it round-trips verbatim — unlike the conversation id, whose ":"/"@" get sanitized in the key
|
|
112
|
+
// and MUST come from the stored target).
|
|
113
|
+
const tenantId = storedString(r.target, "tenantId") ?? r.surfaceKey.split("/")[1] ?? "";
|
|
114
|
+
// teamsMode is the one tested scope→mode place (personal→dm, groupChat→mpim); an absent stored
|
|
115
|
+
// scope resolves to "channel" (capped — the conservative arm).
|
|
116
|
+
const mode = teamsMode(conversationType);
|
|
117
|
+
// OPE-486 — the fire's WAKE ORIGIN, parity with resolveSlackTarget (routine-dispatch.ts). Absent
|
|
118
|
+
// before this fix, so a Teams fire could never divert into the unified window (fire-window.ts's
|
|
119
|
+
// planFireWindow fails closed on a missing origin). serviceUrl/conversationId are already
|
|
120
|
+
// non-empty-checked above (isTrustedTeamsServiceUrl re-validates serviceUrl in the codec's
|
|
121
|
+
// validate arm); no replyToActivityId — a proactive fire has no reply chain to anchor to, matching
|
|
122
|
+
// teams-front.ts's own ambient-origin mint (`{ platform: "teams", serviceUrl, conversationId }`).
|
|
123
|
+
const origin: UnifiedWakeOrigin = { platform: "teams", serviceUrl, conversationId, ...(tenantId ? { tenantId } : {}) };
|
|
124
|
+
return {
|
|
125
|
+
channel: teamsChannel,
|
|
126
|
+
// Full TeamsReceiveTarget: conversationType lets eve anchor non-personal threads correctly;
|
|
127
|
+
// tenant/team/channel ids ride into the Connector's channelData.
|
|
128
|
+
target: {
|
|
129
|
+
serviceUrl,
|
|
130
|
+
conversationId,
|
|
131
|
+
...(conversationType ? { conversationType } : {}),
|
|
132
|
+
...(channelId ? { channelId } : {}),
|
|
133
|
+
...(teamId ? { teamId } : {}),
|
|
134
|
+
...(tenantId ? { tenantId } : {}),
|
|
135
|
+
},
|
|
136
|
+
mode,
|
|
137
|
+
// The facts bindTeams reads (workspace = tenant, surface = conversation). NO membership_type is
|
|
138
|
+
// stamped — it was never re-verified at fire time, so teamsTrust yields no verdict and bindFrom
|
|
139
|
+
// fails the fire closed to `external` (shared memory / domain skills stay off), matching what an
|
|
140
|
+
// inbound Teams activity without membershipType gets.
|
|
141
|
+
attributes: {
|
|
142
|
+
conversation_id: conversationId,
|
|
143
|
+
service_url: serviceUrl,
|
|
144
|
+
...(tenantId ? { tenant_id: tenantId } : {}),
|
|
145
|
+
...(channelId ? { channel_id: channelId } : {}),
|
|
146
|
+
...(teamId ? { team_id: teamId } : {}),
|
|
147
|
+
...(conversationType ? { scope: conversationType } : {}),
|
|
148
|
+
...(aadObjectId ? { aad_object_id: aadObjectId } : {}),
|
|
149
|
+
},
|
|
150
|
+
origin,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Perform ALL of the teams bundle's registrations. Idempotent (each register call replaces);
|
|
155
|
+
* cheap and side-effect-free beyond the registry writes. */
|
|
156
|
+
export function registerTeamsProvider(): void {
|
|
157
|
+
registerIdentityMapper("teams", bindTeams);
|
|
158
|
+
// Bot-Connector proactive post wired via the host-side Connector write seam (channels/
|
|
159
|
+
// teams-connector.ts, OPE-1237 — the post arm sends a message activity into the captured
|
|
160
|
+
// conversation reference). No integration reaction primitive.
|
|
161
|
+
// closingProseDropped (OPE-2006): the Teams completion handler DROPS a turn's closing text and
|
|
162
|
+
// audits it. `reply` is the only route out.
|
|
163
|
+
registerCapabilityManifest("teams", { midTurnPost: true, reactions: false, closingProseDropped: true });
|
|
164
|
+
// The OPE-1499 parity ledger: every capability the chat-surface contract names, answered
|
|
165
|
+
// `supported` or `{ unsupported: "<reason>" }`. Gates nothing at runtime — `lint:surface-parity`
|
|
166
|
+
// reds on an unanswered row, and the contract's §7.1/§7.1.1/§7.2 tables are generated from it.
|
|
167
|
+
registerSurfaceParity("teams", TEAMS_SURFACE_PARITY);
|
|
168
|
+
// The OPE-1612 event inventory: every vendor event that could wake a routine, and how far the
|
|
169
|
+
// normalizer plane carries each one. Gates nothing at runtime — `lint:provider-events` reds a
|
|
170
|
+
// bundle that declares none or that promises a match key the normalizer does not declare, and
|
|
171
|
+
// the manual's inventory table is generated from it.
|
|
172
|
+
registerProviderEvents("teams", TEAMS_PROVIDER_EVENTS);
|
|
173
|
+
registerPostArm(teamsPostArm);
|
|
174
|
+
registerWakeOriginCodec("teams", teamsWakeOriginCodec);
|
|
175
|
+
// Teams channel/thread history reads ride Microsoft Graph (Bot Framework has no history API).
|
|
176
|
+
registerReadAdapter(teamsReadAdapter);
|
|
177
|
+
// THE SEAT CATCH-UP FETCH (OPE-1726): the channel (or reply chain) history since the seat's
|
|
178
|
+
// last wake, over Microsoft Graph — the same transport `read_channel` uses, and NOT the Bot
|
|
179
|
+
// Connector the wake arrives on. Core's shared path applies the bounds, the policy and the cap.
|
|
180
|
+
// The surface DECLARES the unit its own message stamps carry, and core widens both catch-up
|
|
181
|
+
// bounds by it (OPE-1847). A wrong or missing unit bounds the seat out of its own window.
|
|
182
|
+
registerSeatCatchUp("teams", teamsSeatCatchUpFetch, { timestampPrecisionMs: TEAMS_TIMESTAMP_PRECISION_MS });
|
|
183
|
+
// Proactive routine fires on the teams surface (needs the captured Bot Framework reference).
|
|
184
|
+
registerRoutineDispatch("teams", { resolveTarget: resolveTeamsTarget });
|
|
185
|
+
// The bundle→core bridge for the routine fire-eligibility leg's Graph checks (OPE-522) — core's
|
|
186
|
+
// routines/fire-eligibility.ts consumes these through channels/teams-bridge.ts, never Graph
|
|
187
|
+
// directly, mirroring the slack provider's own registerSlackBridge call.
|
|
188
|
+
registerTeamsBridge({ roomMembership: liveTeamsRoomMembership, accountActive: liveTeamsAccountActive });
|
|
189
|
+
// The bot's own display name (OPE-587) — core's per-surface registry arm, rendered as a HANDLE
|
|
190
|
+
// line, not identity, since OPE-610 settled the naming model. Reads the app's OWN service
|
|
191
|
+
// principal through the same Graph token the read adapter mints; needs no extra Azure consent
|
|
192
|
+
// (the app's implicit self-read, measured live). See channels/teams-bot-name.ts.
|
|
193
|
+
registerBotDisplayName("teams", teamsBotDisplayName);
|
|
194
|
+
// THE TEAMS ACCOUNT DIRECTORY (OPE-887) — the lookup the reverse-initiated link uses to NAME the
|
|
195
|
+
// other account instead of posting a bearer code where a room can read it. It walks the Bot
|
|
196
|
+
// Connector roster of the teams this deployment has captured a conversation reference for, so the
|
|
197
|
+
// ids it returns are the `29:` ids a Teams link actually keys on. The other half of the transport
|
|
198
|
+
// is the post arm's `deliverDirectMessage` — core requires BOTH before it offers the transport,
|
|
199
|
+
// and both rest on the same captured reference (channels/teams-conversation-ref.ts).
|
|
200
|
+
registerAccountDirectory("teams", (query) => fetchTeamsDirectory(query));
|
|
201
|
+
// THE STOP ACKNOWLEDGEMENT'S TRANSPORT (OPE-899) — postText/editMessage, so turn-anchor.ts's
|
|
202
|
+
// postSurfaceText resolves a real send instead of undefined on a Teams turn. No postFiles (no
|
|
203
|
+
// attachment plane) and no setReaction (no reaction primitive beyond the `angry` stop trigger).
|
|
204
|
+
registerSurfaceMessageOps(teamsMessageOps);
|
|
205
|
+
// REACTION-FEEDBACK INGEST (OPE-1502) — INBOUND only, and that is not a contradiction with the
|
|
206
|
+
// `reactions: false` manifest above. Teams refuses an application identity the outbound WRITE
|
|
207
|
+
// (§7.3's probe), while it delivers `messageReaction` activities inbound freely. The arm that
|
|
208
|
+
// records them is channels/teams-reaction-feedback.ts, reached from the stop arm's fan-out.
|
|
209
|
+
registerReactionSurface("teams");
|
|
210
|
+
// Shipped-manual plane (OPE-138): this bundle's own setup runbook (docs/deploy/surfaces/teams.md,
|
|
211
|
+
// compiled into ./docs-generated.ts because docs/ is not on disk at runtime). De-select the bundle
|
|
212
|
+
// and the module leaves the import graph; registerProviderDocs re-checks the selection gate anyway.
|
|
213
|
+
registerProviderDocs("teams", PROVIDER_DOCS);
|
|
214
|
+
// Ops plane (OPE-428): the live preflight probe that reports which Microsoft boundary governs
|
|
215
|
+
// channel reads. Split into ./ops.js so the STANDALONE ops MCP entry (repo-root
|
|
216
|
+
// scripts/ops-mcp.ts, which serves preflight with no agent boot) registers it without pulling
|
|
217
|
+
// the whole channel graph — the same split slack uses.
|
|
218
|
+
registerTeamsOps();
|
|
219
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// TEAMS' surface-parity declaration (OPE-1499). See the core module's header
|
|
2
|
+
// (packages/core/agent/identity/surface-parity.ts) for what the three cell shapes mean.
|
|
3
|
+
//
|
|
4
|
+
// Some of the gaps below are PERMANENT vendor limits and some are NOT BUILT YET. The distinction is
|
|
5
|
+
// the whole point of writing a reason down: `react` and the ✋ glyph are things Microsoft does not
|
|
6
|
+
// let an application identity do, so no ticket will ever close them. Reaction-feedback ingest was
|
|
7
|
+
// the other kind — unwritten OE work — and OPE-1502 closed it, which is what the row now says.
|
|
8
|
+
|
|
9
|
+
import type { SurfaceParityDeclaration } from "@open-everywhere/core/agent/adapter-api.js";
|
|
10
|
+
|
|
11
|
+
export const TEAMS_SURFACE_PARITY: SurfaceParityDeclaration = {
|
|
12
|
+
// --- registration seams (§7.1) ---------------------------------------------------------------
|
|
13
|
+
"identity-mapper": "supported",
|
|
14
|
+
"capability-manifest": "supported",
|
|
15
|
+
"post-arm": "supported",
|
|
16
|
+
"wake-origin-codec": "supported",
|
|
17
|
+
"unified-action-plane": {
|
|
18
|
+
unsupported:
|
|
19
|
+
"the arm posts a plain message activity, and a button here needs an Adaptive Card PLUS an invoke route to receive the tap. `teams-front.ts` forwards every `invoke` to the native channel, whose HITL route resolves a tap against the TEAMS channel's own session — a unified seat's parked request lives on another session entirely, so an OE-minted card would render a button that settles nothing. Prose, which stands on its own, until that route exists",
|
|
20
|
+
},
|
|
21
|
+
"origin-placement-carry": {
|
|
22
|
+
unsupported:
|
|
23
|
+
"the placement is INSIDE the conversation identity: `threadRef` keys on `replyToActivityId`, the very activity a reply hangs under, so two origins that pass the conversation test already agree on placement and there is no gap to carry",
|
|
24
|
+
},
|
|
25
|
+
"read-adapter": "supported",
|
|
26
|
+
// INBOUND reaction ingest is wired (OPE-1502); the OUTBOUND write below is not, and the two are
|
|
27
|
+
// independent. The note is the honest half: Microsoft delivers `messageReaction` only for messages
|
|
28
|
+
// the agent itself sent, and delivers no author for the reacted message — so the "is this the
|
|
29
|
+
// agent's own reply" test on this surface is that vendor fence plus a present `replyToId`, not the
|
|
30
|
+
// author comparison Slack, Discord and Linear make.
|
|
31
|
+
"reaction-feedback-ingest": { supported: "the vendor fence is the guarantee — Teams delivers the event only for the agent's own messages, and carries no author to compare" },
|
|
32
|
+
"ancillary-ops": "supported",
|
|
33
|
+
// WIRED, AND TENANT-CONDITIONAL — the qualified form, because a bare ✅ over-claimed it against
|
|
34
|
+
// the support matrix's own 🔧 row (OPE-1527). The seam is built and armed on every channel and
|
|
35
|
+
// group chat; whether a PLAIN follow-up ever reaches it is the tenant's RSC consent, which no API
|
|
36
|
+
// can probe (channels/teams-engagement.ts). Without the grant only @mention activities arrive, so
|
|
37
|
+
// the seam wakes nothing and the surface answers once per mention exactly as before.
|
|
38
|
+
"ambient-engagement-seam": {
|
|
39
|
+
supported: "the seam is wired; plain follow-ups reach it only where the tenant consented to `ChannelMessage.Read.Group` / `ChatMessage.Read.Chat`, which no API can probe",
|
|
40
|
+
},
|
|
41
|
+
// The single-window divert writes `plan.window.addressing ?? "channel-watch"` (teams-front.ts), so
|
|
42
|
+
// an engaged follow-up and unowned room chatter leave the same row. No journey reds on it today —
|
|
43
|
+
// `J-ENGAGED-teams` does not gate a dispatch addressing — so the gap is declared here rather than
|
|
44
|
+
// carried as a silent one.
|
|
45
|
+
"engaged-row-promotion": {
|
|
46
|
+
unsupported: "not built yet — the single-window divert writes the plan's own `addressing`, which calls an engaged follow-up and room chatter alike",
|
|
47
|
+
},
|
|
48
|
+
// The reply chain IS the thread on Teams, so the router reads `replyToActivityId` the way it
|
|
49
|
+
// reads Slack's `threadTs`. A plain conversation send starts its own chain and stays top-level.
|
|
50
|
+
// The personal/group-chat DM leg keeps the raw divert — it mints no reply-chain origin.
|
|
51
|
+
"owned-thread-ingress": { supported: "the channel leg only — a reply chain is the thread; the DM leg mints no `replyToActivityId`, so it can own none" },
|
|
52
|
+
|
|
53
|
+
// --- wake legs and conversation shapes -------------------------------------------------------
|
|
54
|
+
"addressed-leg": "supported",
|
|
55
|
+
// Not the engagement seam: the wake is the single-window front's Leg A, a different mechanism.
|
|
56
|
+
// The registry does suppress it, though — the front drops a stood-down conversation above both
|
|
57
|
+
// legs, for a conversation an @mention enrolled (§7.6).
|
|
58
|
+
"ambient-leg": { supported: "the single-window front's Leg A, not the §2.5 engagement registry" },
|
|
59
|
+
// 🔴 OVER GRAPH, NOT THE BOT CONNECTOR. The transport a Teams activity arrives on carries no
|
|
60
|
+
// history API — which is why `teams-supervisor.ts` once said this surface could not catch up at all
|
|
61
|
+
// — but `teams-catch-up.ts` reads `/teams/{team}/channels/{channel}/messages`, the same Graph call
|
|
62
|
+
// `read_channel` uses. A personal or group chat carries no team id, so it takes no catch-up.
|
|
63
|
+
"seat-catch-up": { supported: "over Microsoft Graph (channel activities only — a personal/group chat carries no team id)" },
|
|
64
|
+
// The slot is EMPTY on this surface: `teams-front.ts` builds no `contextLines`, so there is no pour
|
|
65
|
+
// to police. The seat's room context here is the catch-up read above and the carried tail, both of
|
|
66
|
+
// which already pass the domain's policy in core.
|
|
67
|
+
"seat-context-admission": { unsupported: "the hydration fills no context-line slot — the seat's room context is the catch-up read, which core already admits" },
|
|
68
|
+
// The same missing invoke route the component-plane row above names: a card needs an Adaptive Card
|
|
69
|
+
// PLUS a tap that resolves against the UNIFIED seat's parked request, and `teams-front.ts` forwards
|
|
70
|
+
// every `invoke` to the native channel's own session, so nothing here can card in place. See the
|
|
71
|
+
// roster row for what that costs and where it does NOT cost anything.
|
|
72
|
+
"approval-card": { unsupported: "no invoke route resolves a tap against a unified seat's parked request. A governed act that takes the venue walk is redirected to a card-capable room in the SAME domain, or refused when the domain holds none. The paths OUTSIDE that walk do not: the spend checkpoint (spend/checkpoint.ts `requireSessionContinuation`) fails OPEN here and records the crossing in an audit row instead" },
|
|
73
|
+
// THE TRACE ARRIVES EVEN THOUGH THE BUTTON CANNOT (OPE-1854). A judge-lifted act posts its
|
|
74
|
+
// notice here as a plain message activity through the shared road (`postJudgeLiftNotice`), on this
|
|
75
|
+
// surface's own `SurfaceMessageOps` transport. It carries the overrule as an INSTRUCTION in
|
|
76
|
+
// words, with the action id a reviewer finds the act by, because no tap can land here.
|
|
77
|
+
// 🔴 BARE, NOT `{ supported: "…" }`. A noted cell is exempt from the overclaim arm, and the
|
|
78
|
+
// arm is the point: drop this bundle's text transport and the notice silently stops arriving.
|
|
79
|
+
"judge-lift-notice": "supported",
|
|
80
|
+
"name-drop-mention": { unsupported: "not built yet — Leg A wakes on an @mention or room chatter, never on a name-drop match" },
|
|
81
|
+
// 🔴 THE ACTIVITY TYPE IS THE FILTER. Teams sends an edit as a `messageUpdate` activity, and the
|
|
82
|
+
// inbound parser admits `message`, `invoke` and `conversationUpdate` only, so the edit resolves to
|
|
83
|
+
// null before `teams-front.ts` sees it. `teams-front.ts` forwards every non-`message` activity to
|
|
84
|
+
// the native channel for the same reason — there is nothing on this surface to gate.
|
|
85
|
+
"message-edit-wake": { unsupported: "an edit arrives as a `messageUpdate` activity, and the inbound parser admits `message`, `invoke` and `conversationUpdate` only" },
|
|
86
|
+
// The `;messageid=` reply chain (§4.6.1).
|
|
87
|
+
threads: "supported",
|
|
88
|
+
rooms: "supported",
|
|
89
|
+
dm: "supported",
|
|
90
|
+
mpim: "supported",
|
|
91
|
+
|
|
92
|
+
// --- core verbs (§7.2) -----------------------------------------------------------------------
|
|
93
|
+
reply: "supported",
|
|
94
|
+
edit_reply: "supported",
|
|
95
|
+
// PERMANENT. Microsoft Graph carries an outbound reaction route, but it refuses the
|
|
96
|
+
// application-only context OE runs in — there is no primitive an app identity can drive (§7.3).
|
|
97
|
+
react: { unsupported: "vendor limit — Graph's outbound reaction route refuses an application-only identity" },
|
|
98
|
+
no_reply: "supported",
|
|
99
|
+
read_channel: "supported",
|
|
100
|
+
read_thread: "supported",
|
|
101
|
+
search: { unsupported: "no work-search adapter — Teams is a chat surface, not a work corpus (§4.7)" },
|
|
102
|
+
post_message: "supported",
|
|
103
|
+
dm_person: "supported",
|
|
104
|
+
set_dm_delivery: "supported",
|
|
105
|
+
// Teams' six fixed reaction values carry no raised hand, so the stop trigger is `angry` — the one
|
|
106
|
+
// of the six people already use to mean "this is wrong" (§4.8).
|
|
107
|
+
stop: { supported: "the `angry` reaction — Teams' six fixed values carry no ✋" },
|
|
108
|
+
ack_stop_glyph: { unsupported: "vendor limit — Teams' fixed six-value reaction vocabulary has no raised hand" },
|
|
109
|
+
ack_cancel_note: "supported",
|
|
110
|
+
stand_down: { supported: "marks the conversation row; refuses on a 1:1 and when no row exists" },
|
|
111
|
+
post_file: { unsupported: "not built yet — no Teams attachment transport is wired (§7.3)" },
|
|
112
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { BotDisplayNameArm } from "@open-everywhere/core/agent/adapter-api.js";
|
|
2
|
+
import { type TeamsGraphRequester } from "./teams-read.js";
|
|
3
|
+
/**
|
|
4
|
+
* The app's own display name from its service principal, or `undefined` when Graph cannot answer.
|
|
5
|
+
* Pure but for the injected `req` — exported (not just the live wrapper below) so the URL, the
|
|
6
|
+
* shape read and the fail-soft rules are unit-testable with a canned requester, no token mint and
|
|
7
|
+
* no network call. Same DI shape as teams-fire-eligibility.ts's probes.
|
|
8
|
+
*
|
|
9
|
+
* FAIL-SOFT on everything: an empty appId, a thrown request (`liveTeamsGraphRequest` throws on a
|
|
10
|
+
* missing token or a non-ok response), a response that is not an object, and a `displayName` that is
|
|
11
|
+
* absent, non-string or blank all read `undefined`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function teamsBotDisplayNameViaGraph(appId: string, req: TeamsGraphRequester): Promise<string | undefined>;
|
|
14
|
+
/**
|
|
15
|
+
* The Teams app-CATALOG display name (OPE-609) — what the Teams client renders beside a message —
|
|
16
|
+
* or `undefined` when Graph cannot answer. FAIL-SOFT identically to {@link teamsBotDisplayNameViaGraph}:
|
|
17
|
+
* a missing `AppCatalog.Read.All` consent throws a 403 inside `req`, caught here, so an unconsented
|
|
18
|
+
* or de-consented tenant reads `undefined` rather than blocking a turn. An empty `value` array (no
|
|
19
|
+
* catalog entry matches the app id) also reads `undefined`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function teamsCatalogDisplayNameViaGraph(appId: string, req: TeamsGraphRequester): Promise<string | undefined>;
|
|
22
|
+
/**
|
|
23
|
+
* The full resolution order (OPE-609): the catalog name first, the service-principal name second,
|
|
24
|
+
* `undefined` last. Pure but for the injected `req` — one requester serves both legs, so a test can
|
|
25
|
+
* assert exactly how many calls each leg made. Exported so the live arm and its tests share one
|
|
26
|
+
* definition of "what order do the two legs run in."
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveTeamsBotDisplayNameViaGraph(appId: string, req: TeamsGraphRequester): Promise<string | undefined>;
|
|
29
|
+
/** Live arm: {@link resolveTeamsBotDisplayNameViaGraph} through the real app id + Graph transport,
|
|
30
|
+
* resolved once per instance. `resolveTeamsAppId()` throws when MICROSOFT_APP_ID is unset, so it
|
|
31
|
+
* is inside the try — an unconfigured deployment reads `undefined`, never an exception into
|
|
32
|
+
* instruction assembly. */
|
|
33
|
+
export declare const teamsBotDisplayName: BotDisplayNameArm;
|
|
34
|
+
/** Test seam: drop the memoised name so one test cannot inherit another's. */
|
|
35
|
+
export declare function _resetTeamsBotDisplayNameForTest(): void;
|
|
36
|
+
//# sourceMappingURL=teams-bot-name.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teams-bot-name.d.ts","sourceRoot":"","sources":["../../../agent/channels/teams-bot-name.ts"],"names":[],"mappings":"AA8CA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AACpF,OAAO,EAAyB,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AA8BlF;;;;;;;;;GASG;AACH,wBAAsB,2BAA2B,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQtH;AAED;;;;;;GAMG;AACH,wBAAsB,+BAA+B,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAW1H;AAED;;;;;GAKG;AACH,wBAAsB,kCAAkC,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI7H;AAKD;;;4BAG4B;AAC5B,eAAO,MAAM,mBAAmB,EAAE,iBAoBjC,CAAC;AAEF,8EAA8E;AAC9E,wBAAgB,gCAAgC,IAAI,IAAI,CAEvD"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// teams-bot-name — the bot's OWN display name on Teams, read from Microsoft Graph (OPE-587,
|
|
2
|
+
// catalog leg OPE-609). The teams arm of core's per-surface bot-display-name registry
|
|
3
|
+
// (identity/bot-display-name.ts), so a Teams-pinned session introduces the agent by a real name
|
|
4
|
+
// instead of the generic identity line.
|
|
5
|
+
//
|
|
6
|
+
// ── TWO NAMES, TRIED IN ORDER ────────────────────────────────────────────────────────────────
|
|
7
|
+
// 1. THE TEAMS APP-CATALOG NAME — `teamsApps.displayName` — is what the Teams client actually
|
|
8
|
+
// renders beside a message (the manifest's `name.short`). Reading it needs a tenant-admin-
|
|
9
|
+
// consented `AppCatalog.Read.All`; OPE-587 shipped without this leg because that call 403'd
|
|
10
|
+
// (`Missing role permissions`) on the only tenant available at the time. OPE-609 grants that
|
|
11
|
+
// consent, so this arm now tries the catalog first:
|
|
12
|
+
// GET /v1.0/appCatalogs/teamsApps?$filter=externalId eq '{MICROSOFT_APP_ID}'&$expand=appDefinitions
|
|
13
|
+
// NOT RE-MEASURED against a live 200 response as of this change — the consent was granted but a
|
|
14
|
+
// fresh probe of the catalog call itself was not run. The fail-soft path below means a wrong
|
|
15
|
+
// guess about the response shape degrades to the fallback below, never to a broken turn.
|
|
16
|
+
// 2. THE SERVICE PRINCIPAL NAME — the ENTRA APP REGISTRATION display name — is the pre-OPE-609
|
|
17
|
+
// fallback and MEASURED LIVE against the e2e Teams tenant on 2026-08-15:
|
|
18
|
+
// GET /v1.0/servicePrincipals(appId='{MICROSOFT_APP_ID}')?$select=displayName
|
|
19
|
+
// The token carried exactly ONE application role (the RSC grant `Group.Selected`) and this call
|
|
20
|
+
// still returned HTTP 200 — an IMPLICIT self-read of the app's own directory object, needing no
|
|
21
|
+
// directory-read permission at all. `GET /users/{id}?$select=displayName` on the SAME token
|
|
22
|
+
// returned 403, confirming it is not a general directory-read grant. This is the name a
|
|
23
|
+
// deployment gets when `AppCatalog.Read.All` is unconsented, revoked, or the catalog call fails
|
|
24
|
+
// for any other reason — it can differ from the catalog name in case and spacing (measured in
|
|
25
|
+
// the e2e tenant: `open-everywhere` here versus `Open Everywhere` in the catalog / manifest).
|
|
26
|
+
//
|
|
27
|
+
// WHY THE SERVICE PRINCIPAL, NOT `/applications`, FOR LEG 2. `GET /v1.0/applications(appId='…')`
|
|
28
|
+
// answered 200 with the same string in the same probe, so both work for a SINGLE-TENANT bot. They
|
|
29
|
+
// stop being equivalent for a MULTI-TENANT one: the `application` object lives only in the app's
|
|
30
|
+
// HOME tenant, while a `servicePrincipal` exists in EVERY tenant that consented to the app — and
|
|
31
|
+
// the Graph token this arm rides is minted per tenant (`resolveTeamsTenantId`). The service
|
|
32
|
+
// principal is therefore the object that exists wherever the token points. NOT VERIFIED: only one
|
|
33
|
+
// tenant was available, so the foreign-tenant self-read is reasoned, not measured. Either leg fails
|
|
34
|
+
// soft: an unresolvable name renders the generic identity line, which is what a Teams session got
|
|
35
|
+
// before this arm existed.
|
|
36
|
+
//
|
|
37
|
+
// ── CACHING ──────────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
// One memoised promise per instance, matching the slack arm's `botDisplayName()` exactly: the
|
|
39
|
+
// instruction assembler renders the identity line every turn, so an uncached arm would add a Graph
|
|
40
|
+
// round trip per turn and could flip the standing block's bytes mid-session. A rename needs a cold
|
|
41
|
+
// start to take effect. A FAILED resolution is cached too — the same posture — so a deployment
|
|
42
|
+
// missing the token does not re-probe Graph on every turn. The 5s timeout bounds BOTH legs
|
|
43
|
+
// together (one race around the whole two-call chain), not each leg separately — a hung catalog
|
|
44
|
+
// call cannot silently double the turn's worst-case wait by handing the fallback leg its own budget.
|
|
45
|
+
import { resolveTeamsAppId } from "eve/channels/teams";
|
|
46
|
+
import { liveTeamsGraphRequest } from "./teams-read.js";
|
|
47
|
+
const GRAPH_BASE = "https://graph.microsoft.com/v1.0";
|
|
48
|
+
/** A Graph `displayName` field reduced to a usable string, or `undefined` for any other shape
|
|
49
|
+
* (absent, non-string, blank) — shared by both name legs so "what counts as a name" is defined
|
|
50
|
+
* exactly once. */
|
|
51
|
+
function trimmedName(value) {
|
|
52
|
+
if (typeof value !== "string")
|
|
53
|
+
return undefined;
|
|
54
|
+
const trimmed = value.trim();
|
|
55
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The app's own display name from its service principal, or `undefined` when Graph cannot answer.
|
|
59
|
+
* Pure but for the injected `req` — exported (not just the live wrapper below) so the URL, the
|
|
60
|
+
* shape read and the fail-soft rules are unit-testable with a canned requester, no token mint and
|
|
61
|
+
* no network call. Same DI shape as teams-fire-eligibility.ts's probes.
|
|
62
|
+
*
|
|
63
|
+
* FAIL-SOFT on everything: an empty appId, a thrown request (`liveTeamsGraphRequest` throws on a
|
|
64
|
+
* missing token or a non-ok response), a response that is not an object, and a `displayName` that is
|
|
65
|
+
* absent, non-string or blank all read `undefined`.
|
|
66
|
+
*/
|
|
67
|
+
export async function teamsBotDisplayNameViaGraph(appId, req) {
|
|
68
|
+
if (appId.length === 0)
|
|
69
|
+
return undefined;
|
|
70
|
+
try {
|
|
71
|
+
const json = (await req(`${GRAPH_BASE}/servicePrincipals(appId='${encodeURIComponent(appId)}')?$select=displayName`));
|
|
72
|
+
return trimmedName(json?.displayName);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return undefined; // no token / 401 / 403 / 404 / transport failure ⇒ core renders the generic line
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The Teams app-CATALOG display name (OPE-609) — what the Teams client renders beside a message —
|
|
80
|
+
* or `undefined` when Graph cannot answer. FAIL-SOFT identically to {@link teamsBotDisplayNameViaGraph}:
|
|
81
|
+
* a missing `AppCatalog.Read.All` consent throws a 403 inside `req`, caught here, so an unconsented
|
|
82
|
+
* or de-consented tenant reads `undefined` rather than blocking a turn. An empty `value` array (no
|
|
83
|
+
* catalog entry matches the app id) also reads `undefined`.
|
|
84
|
+
*/
|
|
85
|
+
export async function teamsCatalogDisplayNameViaGraph(appId, req) {
|
|
86
|
+
if (appId.length === 0)
|
|
87
|
+
return undefined;
|
|
88
|
+
try {
|
|
89
|
+
const json = (await req(`${GRAPH_BASE}/appCatalogs/teamsApps?$filter=externalId eq '${encodeURIComponent(appId)}'&$expand=appDefinitions`));
|
|
90
|
+
const entry = Array.isArray(json?.value) ? json.value[0] : undefined;
|
|
91
|
+
return trimmedName(entry?.displayName) ?? trimmedName(entry?.appDefinitions?.[0]?.displayName);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined; // no consent / 401 / 403 / 404 / transport failure ⇒ fall back to the service principal
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The full resolution order (OPE-609): the catalog name first, the service-principal name second,
|
|
99
|
+
* `undefined` last. Pure but for the injected `req` — one requester serves both legs, so a test can
|
|
100
|
+
* assert exactly how many calls each leg made. Exported so the live arm and its tests share one
|
|
101
|
+
* definition of "what order do the two legs run in."
|
|
102
|
+
*/
|
|
103
|
+
export async function resolveTeamsBotDisplayNameViaGraph(appId, req) {
|
|
104
|
+
const catalog = await teamsCatalogDisplayNameViaGraph(appId, req);
|
|
105
|
+
if (catalog !== undefined)
|
|
106
|
+
return catalog;
|
|
107
|
+
return teamsBotDisplayNameViaGraph(appId, req);
|
|
108
|
+
}
|
|
109
|
+
/** The memoised per-instance promise — see the CACHING note in the header. */
|
|
110
|
+
let displayNamePromise;
|
|
111
|
+
/** Live arm: {@link resolveTeamsBotDisplayNameViaGraph} through the real app id + Graph transport,
|
|
112
|
+
* resolved once per instance. `resolveTeamsAppId()` throws when MICROSOFT_APP_ID is unset, so it
|
|
113
|
+
* is inside the try — an unconfigured deployment reads `undefined`, never an exception into
|
|
114
|
+
* instruction assembly. */
|
|
115
|
+
export const teamsBotDisplayName = () => {
|
|
116
|
+
if (!displayNamePromise) {
|
|
117
|
+
displayNamePromise = (async () => {
|
|
118
|
+
try {
|
|
119
|
+
// The memoised value is a PROMISE and instruction assembly awaits it on every Teams turn,
|
|
120
|
+
// so a hung Graph call would block every turn on the instance, not one. The race bounds
|
|
121
|
+
// it: a stall settles the memo to `undefined` (the generic line), same as any other miss.
|
|
122
|
+
// The bound wraps BOTH legs (see the CACHING header note) — a slow catalog call eats into
|
|
123
|
+
// the same 5s budget the fallback leg would otherwise need.
|
|
124
|
+
const timeout = new Promise((resolve) => {
|
|
125
|
+
const t = setTimeout(() => resolve(undefined), 5000);
|
|
126
|
+
t.unref?.();
|
|
127
|
+
});
|
|
128
|
+
return await Promise.race([resolveTeamsBotDisplayNameViaGraph(await resolveTeamsAppId(), liveTeamsGraphRequest), timeout]);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
})();
|
|
134
|
+
}
|
|
135
|
+
return displayNamePromise;
|
|
136
|
+
};
|
|
137
|
+
/** Test seam: drop the memoised name so one test cannot inherit another's. */
|
|
138
|
+
export function _resetTeamsBotDisplayNameForTest() {
|
|
139
|
+
displayNamePromise = undefined;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=teams-bot-name.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teams-bot-name.js","sourceRoot":"","sources":["../../../agent/channels/teams-bot-name.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type TeamsGraphRequester } from "./teams-read.js";
|
|
2
|
+
import { type AuditInput, type NormalizedMessage, type SeatCatchUpFetch } from "@open-everywhere/core/agent/adapter-api.js";
|
|
3
|
+
/** Split the `<teamId>/<channelId>` container this surface's catch-up is addressed by. Returns
|
|
4
|
+
* undefined for anything else rather than guessing a half — a partial address would read another
|
|
5
|
+
* team's channel, the silent mis-address the wake-container design refuses. */
|
|
6
|
+
export declare function parseTeamsCatchUpContainer(container: string): {
|
|
7
|
+
teamId: string;
|
|
8
|
+
channelId: string;
|
|
9
|
+
} | undefined;
|
|
10
|
+
/** Injected seam so the fetch drives offline in tests; defaults to the live Graph transport. */
|
|
11
|
+
export interface TeamsCatchUpDeps {
|
|
12
|
+
readonly request: TeamsGraphRequester;
|
|
13
|
+
/** The deployment's own Entra object id, so its prior posts read as `self`. Absent ⇒ they arrive
|
|
14
|
+
* tagged `bot` (an application author), which is visible and honest, never a crash. */
|
|
15
|
+
readonly selfActorId?: string;
|
|
16
|
+
/** The audit writer. Injected so the contract test reads the row this fetch emits instead of a
|
|
17
|
+
* store; live callers get {@link appendAudit}, which never throws by its own contract. */
|
|
18
|
+
readonly audit?: (entry: AuditInput) => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/** The audit action this read records under. One family, one spelling — a query for the next red
|
|
21
|
+
* ship is `action = "teams.catch_up"` and nothing else. */
|
|
22
|
+
export declare const TEAMS_CATCH_UP_ACTION = "teams.catch_up";
|
|
23
|
+
/** The `<platform>/<workspace>/<surface>` domain-grain key the shared path hands down, split for the
|
|
24
|
+
* audit row's own WHERE columns. Returns undefined for an absent key or a non-Teams one rather than
|
|
25
|
+
* guessing a workspace — the row then says `unknown`, which is a fact, not a filled-in blank.
|
|
26
|
+
* Pure; exported so the contract test pins the parse rather than re-deriving it. */
|
|
27
|
+
export declare function teamsCatchUpScopeFromChannelKey(channelKey: string | undefined): {
|
|
28
|
+
workspace: string;
|
|
29
|
+
surface: string;
|
|
30
|
+
} | undefined;
|
|
31
|
+
/** The fetch, with its seams exposed so the contract test drives it with no network. */
|
|
32
|
+
export declare function fetchTeamsCatchUpWindow(req: {
|
|
33
|
+
container: string;
|
|
34
|
+
threadRoot?: string;
|
|
35
|
+
sinceMs: number;
|
|
36
|
+
channelKey?: string;
|
|
37
|
+
}, deps?: TeamsCatchUpDeps): Promise<readonly NormalizedMessage[]>;
|
|
38
|
+
/**
|
|
39
|
+
* A Graph message's `createdDateTime` carries MILLISECOND grain (OPE-1847,
|
|
40
|
+
* `core/agent/channels/seat-catch-up.ts` {@link SeatCatchUpDeclaration.timestampPrecisionMs}).
|
|
41
|
+
* 🔴 ONE MILLISECOND IS NOT ZERO WIDENING. The road widens the LOWER bound by nothing here and the
|
|
42
|
+
* UPPER bound by exactly one millisecond, so a message stamped at the trigger's own instant is
|
|
43
|
+
* INSIDE the window and `excludeId` is the only thing that removes the trigger. A rendered Teams
|
|
44
|
+
* line carries the Graph message id in its `ts` slot, which is the value the wake path passes as
|
|
45
|
+
* `excludeId`.
|
|
46
|
+
* 🔴 IT IS STILL DECLARED, and the parity test requires it. The declaration is what makes "this
|
|
47
|
+
* surface needs the finest unit" a CHECKED claim rather than an omission, and an omission is
|
|
48
|
+
* indistinguishable from the GitHub and Notion defect this whole mechanism exists to close.
|
|
49
|
+
*/
|
|
50
|
+
export declare const TEAMS_TIMESTAMP_PRECISION_MS = 1;
|
|
51
|
+
/** Teams' registered {@link SeatCatchUpFetch} — the one thing this surface contributes to the shared path. */
|
|
52
|
+
export declare const teamsSeatCatchUpFetch: SeatCatchUpFetch;
|
|
53
|
+
//# sourceMappingURL=teams-catch-up.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teams-catch-up.d.ts","sourceRoot":"","sources":["../../../agent/channels/teams-catch-up.ts"],"names":[],"mappings":"AAyCA,OAAO,EAAwG,KAAK,mBAAmB,EAA0B,MAAM,iBAAiB,CAAC;AACzL,OAAO,EAA6C,KAAK,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AAMvK;;gFAEgF;AAChF,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAO/G;AA0CD,gGAAgG;AAChG,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC;4FACwF;IACxF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;+FAC2F;IAC3F,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvD;AAID;4DAC4D;AAC5D,eAAO,MAAM,qBAAqB,mBAAmB,CAAC;AAEtD;;;qFAGqF;AACrF,wBAAgB,+BAA+B,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAKlI;AAmBD,wFAAwF;AACxF,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,EACrF,IAAI,GAAE,gBAA2B,GAChC,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAqEvC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAE9C,8GAA8G;AAC9G,eAAO,MAAM,qBAAqB,EAAE,gBAQhC,CAAC"}
|