@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.
Files changed (146) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +46 -2
  4. package/agent/channels/teams-bot-name.ts +162 -0
  5. package/agent/channels/teams-catch-up.ts +244 -0
  6. package/agent/channels/teams-connector.ts +183 -0
  7. package/agent/channels/teams-conversation-ref.ts +398 -0
  8. package/agent/channels/teams-directory.ts +193 -0
  9. package/agent/channels/teams-engagement.ts +391 -0
  10. package/agent/channels/teams-fire-eligibility.ts +152 -0
  11. package/agent/channels/teams-front.ts +1404 -0
  12. package/agent/channels/teams-graph-boundary.ts +178 -0
  13. package/agent/channels/teams-graph-team-id.ts +106 -0
  14. package/agent/channels/teams-id-restore.ts +131 -0
  15. package/agent/channels/teams-identity.ts +395 -0
  16. package/agent/channels/teams-input-response.ts +129 -0
  17. package/agent/channels/teams-message-ops.ts +79 -0
  18. package/agent/channels/teams-origin-codec.ts +104 -0
  19. package/agent/channels/teams-post-arm.ts +323 -0
  20. package/agent/channels/teams-reaction-feedback.ts +127 -0
  21. package/agent/channels/teams-read.ts +510 -0
  22. package/agent/channels/teams-roster.ts +136 -0
  23. package/agent/channels/teams-stop.ts +319 -0
  24. package/agent/channels/teams-supervisor.ts +638 -0
  25. package/agent/channels/teams.ts +270 -0
  26. package/agent/contributions.ts +31 -0
  27. package/agent/docs-generated.ts +15 -0
  28. package/agent/ops.ts +129 -0
  29. package/agent/provider-events.ts +69 -0
  30. package/agent/register.ts +219 -0
  31. package/agent/surface-parity.ts +112 -0
  32. package/dist/agent/channels/teams-bot-name.d.ts +36 -0
  33. package/dist/agent/channels/teams-bot-name.d.ts.map +1 -0
  34. package/dist/agent/channels/teams-bot-name.js +141 -0
  35. package/dist/agent/channels/teams-bot-name.js.map +1 -0
  36. package/dist/agent/channels/teams-catch-up.d.ts +53 -0
  37. package/dist/agent/channels/teams-catch-up.d.ts.map +1 -0
  38. package/dist/agent/channels/teams-catch-up.js +221 -0
  39. package/dist/agent/channels/teams-catch-up.js.map +1 -0
  40. package/dist/agent/channels/teams-connector.d.ts +57 -0
  41. package/dist/agent/channels/teams-connector.d.ts.map +1 -0
  42. package/dist/agent/channels/teams-connector.js +132 -0
  43. package/dist/agent/channels/teams-connector.js.map +1 -0
  44. package/dist/agent/channels/teams-conversation-ref.d.ts +123 -0
  45. package/dist/agent/channels/teams-conversation-ref.d.ts.map +1 -0
  46. package/dist/agent/channels/teams-conversation-ref.js +368 -0
  47. package/dist/agent/channels/teams-conversation-ref.js.map +1 -0
  48. package/dist/agent/channels/teams-directory.d.ts +11 -0
  49. package/dist/agent/channels/teams-directory.d.ts.map +1 -0
  50. package/dist/agent/channels/teams-directory.js +179 -0
  51. package/dist/agent/channels/teams-directory.js.map +1 -0
  52. package/dist/agent/channels/teams-engagement.d.ts +174 -0
  53. package/dist/agent/channels/teams-engagement.d.ts.map +1 -0
  54. package/dist/agent/channels/teams-engagement.js +336 -0
  55. package/dist/agent/channels/teams-engagement.js.map +1 -0
  56. package/dist/agent/channels/teams-fire-eligibility.d.ts +26 -0
  57. package/dist/agent/channels/teams-fire-eligibility.d.ts.map +1 -0
  58. package/dist/agent/channels/teams-fire-eligibility.js +126 -0
  59. package/dist/agent/channels/teams-fire-eligibility.js.map +1 -0
  60. package/dist/agent/channels/teams-front.d.ts +249 -0
  61. package/dist/agent/channels/teams-front.d.ts.map +1 -0
  62. package/dist/agent/channels/teams-front.js +1143 -0
  63. package/dist/agent/channels/teams-front.js.map +1 -0
  64. package/dist/agent/channels/teams-graph-boundary.d.ts +45 -0
  65. package/dist/agent/channels/teams-graph-boundary.d.ts.map +1 -0
  66. package/dist/agent/channels/teams-graph-boundary.js +168 -0
  67. package/dist/agent/channels/teams-graph-boundary.js.map +1 -0
  68. package/dist/agent/channels/teams-graph-team-id.d.ts +30 -0
  69. package/dist/agent/channels/teams-graph-team-id.d.ts.map +1 -0
  70. package/dist/agent/channels/teams-graph-team-id.js +89 -0
  71. package/dist/agent/channels/teams-graph-team-id.js.map +1 -0
  72. package/dist/agent/channels/teams-id-restore.d.ts +34 -0
  73. package/dist/agent/channels/teams-id-restore.d.ts.map +1 -0
  74. package/dist/agent/channels/teams-id-restore.js +123 -0
  75. package/dist/agent/channels/teams-id-restore.js.map +1 -0
  76. package/dist/agent/channels/teams-identity.d.ts +148 -0
  77. package/dist/agent/channels/teams-identity.d.ts.map +1 -0
  78. package/dist/agent/channels/teams-identity.js +336 -0
  79. package/dist/agent/channels/teams-identity.js.map +1 -0
  80. package/dist/agent/channels/teams-input-response.d.ts +23 -0
  81. package/dist/agent/channels/teams-input-response.d.ts.map +1 -0
  82. package/dist/agent/channels/teams-input-response.js +102 -0
  83. package/dist/agent/channels/teams-input-response.js.map +1 -0
  84. package/dist/agent/channels/teams-message-ops.d.ts +3 -0
  85. package/dist/agent/channels/teams-message-ops.d.ts.map +1 -0
  86. package/dist/agent/channels/teams-message-ops.js +72 -0
  87. package/dist/agent/channels/teams-message-ops.js.map +1 -0
  88. package/dist/agent/channels/teams-origin-codec.d.ts +16 -0
  89. package/dist/agent/channels/teams-origin-codec.d.ts.map +1 -0
  90. package/dist/agent/channels/teams-origin-codec.js +99 -0
  91. package/dist/agent/channels/teams-origin-codec.js.map +1 -0
  92. package/dist/agent/channels/teams-post-arm.d.ts +37 -0
  93. package/dist/agent/channels/teams-post-arm.d.ts.map +1 -0
  94. package/dist/agent/channels/teams-post-arm.js +280 -0
  95. package/dist/agent/channels/teams-post-arm.js.map +1 -0
  96. package/dist/agent/channels/teams-reaction-feedback.d.ts +22 -0
  97. package/dist/agent/channels/teams-reaction-feedback.d.ts.map +1 -0
  98. package/dist/agent/channels/teams-reaction-feedback.js +108 -0
  99. package/dist/agent/channels/teams-reaction-feedback.js.map +1 -0
  100. package/dist/agent/channels/teams-read.d.ts +127 -0
  101. package/dist/agent/channels/teams-read.d.ts.map +1 -0
  102. package/dist/agent/channels/teams-read.js +424 -0
  103. package/dist/agent/channels/teams-read.js.map +1 -0
  104. package/dist/agent/channels/teams-roster.d.ts +54 -0
  105. package/dist/agent/channels/teams-roster.d.ts.map +1 -0
  106. package/dist/agent/channels/teams-roster.js +121 -0
  107. package/dist/agent/channels/teams-roster.js.map +1 -0
  108. package/dist/agent/channels/teams-stop.d.ts +74 -0
  109. package/dist/agent/channels/teams-stop.d.ts.map +1 -0
  110. package/dist/agent/channels/teams-stop.js +252 -0
  111. package/dist/agent/channels/teams-stop.js.map +1 -0
  112. package/dist/agent/channels/teams-supervisor.d.ts +351 -0
  113. package/dist/agent/channels/teams-supervisor.d.ts.map +1 -0
  114. package/dist/agent/channels/teams-supervisor.js +366 -0
  115. package/dist/agent/channels/teams-supervisor.js.map +1 -0
  116. package/dist/agent/channels/teams.d.ts +5 -0
  117. package/dist/agent/channels/teams.d.ts.map +1 -0
  118. package/dist/agent/channels/teams.js +246 -0
  119. package/dist/agent/channels/teams.js.map +1 -0
  120. package/dist/agent/contributions.d.ts +12 -0
  121. package/dist/agent/contributions.d.ts.map +1 -0
  122. package/dist/agent/contributions.js +31 -0
  123. package/dist/agent/contributions.js.map +1 -0
  124. package/dist/agent/docs-generated.d.ts +3 -0
  125. package/dist/agent/docs-generated.d.ts.map +1 -0
  126. package/dist/agent/docs-generated.js +13 -0
  127. package/dist/agent/docs-generated.js.map +1 -0
  128. package/dist/agent/ops.d.ts +14 -0
  129. package/dist/agent/ops.d.ts.map +1 -0
  130. package/dist/agent/ops.js +122 -0
  131. package/dist/agent/ops.js.map +1 -0
  132. package/dist/agent/provider-events.d.ts +3 -0
  133. package/dist/agent/provider-events.d.ts.map +1 -0
  134. package/dist/agent/provider-events.js +67 -0
  135. package/dist/agent/provider-events.js.map +1 -0
  136. package/dist/agent/register.d.ts +16 -0
  137. package/dist/agent/register.d.ts.map +1 -0
  138. package/dist/agent/register.js +198 -0
  139. package/dist/agent/register.js.map +1 -0
  140. package/dist/agent/surface-parity.d.ts +3 -0
  141. package/dist/agent/surface-parity.d.ts.map +1 -0
  142. package/dist/agent/surface-parity.js +106 -0
  143. package/dist/agent/surface-parity.js.map +1 -0
  144. package/package.json +183 -4
  145. package/tsconfig.build.json +22 -0
  146. package/tsconfig.json +15 -0
@@ -0,0 +1,148 @@
1
+ import type { SessionMode, IdentityMapper } from "@open-everywhere/core/agent/adapter-api.js";
2
+ /** Structural match for eve's SessionAuthContext (attributes + principal). */
3
+ export interface OpenEverywhereTeamsAuth {
4
+ readonly authenticator: string;
5
+ readonly principalId: string;
6
+ readonly principalType: string;
7
+ readonly attributes: Record<string, string>;
8
+ }
9
+ export interface TeamsIdentityInput {
10
+ /** The Teams user (from.id) — the OpenEverywhere principal. */
11
+ readonly userId: string;
12
+ /** Azure AD object id of the actor, when carried — a stable cross-tenant user key. */
13
+ readonly aadObjectId?: string;
14
+ readonly userName?: string;
15
+ /** The tenant (org) — becomes the OpenEverywhere `workspace`, the org boundary. */
16
+ readonly tenantId?: string;
17
+ /** The conversation — becomes the OpenEverywhere `surface`. */
18
+ readonly conversationId: string;
19
+ /** Teams team/channel id (channel conversations only). */
20
+ readonly channelId?: string;
21
+ /** Teams team id (channel conversations only). */
22
+ readonly teamId?: string;
23
+ /** Bot Connector base URL for proactive post-back. */
24
+ readonly serviceUrl?: string;
25
+ /** Normalized conversation scope: "personal" | "groupChat" | "channel" | "unknown". */
26
+ readonly scope?: string;
27
+ /** Channel membership type ("standard" | "private" | "shared"), when positively known. */
28
+ readonly membershipType?: string;
29
+ /**
30
+ * The acting user's tenant membership type ("guest" | "member"), positively resolved via the
31
+ * cached Bot Connector roster probe (teams-roster.ts — the analog of Slack's users.info). A GUEST
32
+ * is a cross-org / B2B-collaboration account inside the tenant, so it demotes the surface's
33
+ * trust to `external` exactly as a restricted Slack DM partner does. `undefined` = never
34
+ * determined (no lookup / lookup failed / field absent) → teamsTrust leaves the existing
35
+ * channel/vouch verdict intact (fail-closed unless the operator vouches), mirroring Slack.
36
+ */
37
+ readonly userType?: string;
38
+ }
39
+ /**
40
+ * Fold a resolved tenant membership type into an ALREADY-BUILT auth capture (OPE-493).
41
+ *
42
+ * The front resolves the roster verdict AFTER it has built the capture, because the probe is a
43
+ * network call and the capture is built synchronously. This is the seam that carries the answer in,
44
+ * so the attribute name still has exactly one spelling.
45
+ *
46
+ * 🔴 NO VERDICT RETURNS THE SAME OBJECT, by reference. That is the fail-open guarantee stated as
47
+ * code: when the probe cannot answer, nothing downstream can tell the probe ran.
48
+ */
49
+ export declare function withTeamsUserType<A extends OpenEverywhereTeamsAuth>(auth: A, userType: string | undefined): A;
50
+ /** Build the auth context OpenEverywhere dispatches with — raw facts as string attributes. */
51
+ export declare function buildTeamsAuth(input: TeamsIdentityInput): OpenEverywhereTeamsAuth;
52
+ /**
53
+ * Pull the channel membershipType out of a Bot Framework activity's `channelData`,
54
+ * which (for channel conversations) may carry `{ channel: { membershipType } }`.
55
+ * Defensive: channelData is raw JSON, so everything is probed, never assumed. Note
56
+ * that inbound message activities frequently OMIT membershipType (it's a Graph/Teams
57
+ * API property, not a guaranteed Activity field) — absence is the common case, and
58
+ * teamsVisibility fails closed to private on it.
59
+ */
60
+ export declare function teamsMembershipType(channelData: Record<string, unknown> | undefined): string | undefined;
61
+ /**
62
+ * The acting user's tenant membership type from a Bot Connector roster member record (or a
63
+ * Microsoft Graph `/users/{id}` object) — the authoritative "is this a GUEST?" signal, the
64
+ * Teams analog of Slack's `partnerFactsFromInfo`. Probes `userType` (Graph: "Guest"/"Member")
65
+ * then `userRole` (Bot roster: "guest" for a guest, else a member role), case-insensitively.
66
+ * Pure + defensive (raw JSON): a missing/failed lookup, an empty role, or an unrecognised value
67
+ * all yield `undefined` (no verdict → the existing channel/vouch trust stands, fail-closed
68
+ * unless vouched). Only a positive "guest" flag → "guest" (→ external demotion); a positively
69
+ * recognised member/owner/admin role → "member".
70
+ *
71
+ * What it PROVES: the tenant classifies this account as a guest (external B2B collaborator) —
72
+ * demote. What it does NOT prove: that a "member" is inside YOUR org boundary if the deployment
73
+ * spans multiple tenants; org-internal trust still requires the channel verdict or an operator
74
+ * vouch. It is a demotion-only signal, never an upgrade.
75
+ */
76
+ export declare function teamsUserTypeFromMember(info: unknown): "guest" | "member" | undefined;
77
+ /**
78
+ * Teams conversation scope → session mode, or UNDEFINED when the scope classifies nothing.
79
+ *
80
+ * 🔴 THE UNDEFINED IS THE POINT (OPE-1867). An inbound activity that carries no `conversationType`
81
+ * — or one Teams spells in a way this mapper does not know — tells us nothing about whether the
82
+ * conversation is a room or a 1:1 chat with one person. Answering "channel" there is a DEFAULT, not
83
+ * a reading, and a reader that publishes the surface must be able to tell the two apart: a personal
84
+ * chat reported as a room names that individual's conversation id in a report posted into a room.
85
+ * {@link teamsMode} applies the default for every reader that needs a usable value; this function
86
+ * is what the identity mapper hands the binding, so the provenance survives.
87
+ */
88
+ export declare function teamsResolvedMode(scope: string | undefined): SessionMode | undefined;
89
+ /** Teams conversation scope → session mode, GAP-FILLED to `channel` when nothing classifies it.
90
+ * Visibility, trust and the seat-grain stamps all need a definite value and all fail closed on
91
+ * their own terms, so the default is right for them. A reader that PUBLISHES the surface wants
92
+ * {@link teamsResolvedMode} instead. */
93
+ export declare function teamsMode(scope: string | undefined): SessionMode;
94
+ /**
95
+ * Teams facts → visibility. FAIL CLOSED, mirroring slackVisibility: anything we
96
+ * can't positively confirm as a team-wide (standard) channel is treated as
97
+ * `private`, so a surface's learnings never leak up to shared memory on an
98
+ * under-specified event. Personal/group chats are never public surfaces. For a team
99
+ * channel, only `membershipType === "standard"` (the org/team-wide channel everyone
100
+ * on the team can see) yields `public`; `private`/`shared`/unknown → `private`.
101
+ * (Inbound activities usually DON'T carry membershipType, so the fail-closed branch
102
+ * is the normal case for channels — private is the safe default.)
103
+ */
104
+ export declare function teamsVisibility(scope: string | undefined, membershipType: string | undefined, mode: SessionMode): Visibility;
105
+ /**
106
+ * Teams membership → trust (memory gate). FAIL CLOSED — only POSITIVE facts yield a
107
+ * verdict. A `shared` channel is a Teams Shared Channel — it can include people from
108
+ * OTHER tenants/orgs (the Teams analog of Slack Connect) — so it's `external` and gets
109
+ * no shared memory. A channel positively identified as `standard`/`private` AND carrying
110
+ * its tenant id is in-tenant → `internal`. Personal/group chats are personal silos
111
+ * (gate N/A → internal; bindFrom's DM-partner gate still fails an unverified DM partner
112
+ * closed). ANYTHING ELSE — absent membershipType (the common case: inbound activities
113
+ * routinely omit it), an unrecognised value, or a missing tenant id — returns
114
+ * `undefined`, so bindFrom FAILS CLOSED to `external` (shared/org memory gated off)
115
+ * unless the operator vouches for the deployment with OE_TRUST=internal — the same
116
+ * layer Discord/Linear use. An ambiguous Teams event must never reach the
117
+ * shared org brain.
118
+ *
119
+ * GUEST DETECTION (OPE-60): a `userType` positively resolved to "guest" (a cross-org / B2B
120
+ * collaboration account inside the tenant, via the cached roster lookup) DEMOTES to `external`
121
+ * FIRST — ahead of every other branch, including the personal-silo and standard-channel
122
+ * `internal` verdicts and the operator vouch (bindFrom's precedence gives a mapper's own
123
+ * `external` verdict priority over the vouch). This is exactly how Slack demotes a restricted
124
+ * DM partner. An absent/unknown `userType` (the common case — no lookup, or a lookup that
125
+ * couldn't confirm) leaves the existing verdict intact, so behaviour is unchanged unless a
126
+ * guest is positively detected.
127
+ */
128
+ export declare function teamsTrust(scope: string | undefined, membershipType: string | undefined, mode: SessionMode, tenantId?: string, userType?: string): Trust | undefined;
129
+ /** Local copies of the binding's unions (kept eve-free + import-cycle-free). */
130
+ type Visibility = "public" | "private";
131
+ type Trust = "internal" | "external";
132
+ /**
133
+ * The trust-registry GROUP rung for a Teams TEAM (OPE-490), or `undefined` when this activity has
134
+ * no team (a personal or group chat) or the id is not a safe key segment.
135
+ *
136
+ * FAIL-CLOSED and REFUSING, never repairing: an id that would need sanitising contributes no rung
137
+ * at all. Repairing it would risk composing a segment that collides with a real key, and a missing
138
+ * rung is simply the pre-OPE-490 behaviour.
139
+ *
140
+ * Exported for the identity test, which pins the marker spelling — the spelling is a config
141
+ * contract an operator types into `agent/identity/surfaces.ts`, so it must not drift silently.
142
+ */
143
+ export declare function teamsTrustGroup(teamId: string | undefined): {
144
+ readonly trustGroup: string;
145
+ } | undefined;
146
+ export declare const bindTeams: IdentityMapper;
147
+ export {};
148
+ //# sourceMappingURL=teams-identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teams-identity.d.ts","sourceRoot":"","sources":["../../../agent/channels/teams-identity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,336 @@
1
+ // Pure Microsoft Teams→OpenEverywhere identity capture. The Teams channel (teams.ts) calls
2
+ // these to stow RAW Teams facts (tenant/conversation/channel/team ids + scope +
3
+ // membershipType + actor) into the session auth context's `attributes`. The
4
+ // INTERPRETATION of those facts into mode/visibility lives here too (teamsMode/
5
+ // teamsVisibility) — kept beside the capture so the mapper in
6
+ // agent/identity/binding.ts (bindTeams) just calls them, exactly as Slack splits
7
+ // capture (slack-identity.ts) from interpretation (binding.ts). This file imports no
8
+ // eve types — only OpenEverywhere's `SessionMode` string-union — so it's trivially
9
+ // unit-testable. The security-critical decisions are ONE tested place each and both
10
+ // fail closed: teamsVisibility ("is this a team-wide (public) channel?") and
11
+ // teamsTrust ("is this surface positively in-tenant?" — unknown → no verdict, so
12
+ // bindFrom binds it external).
13
+ import { rootDmGrainAttrs } from "@open-everywhere/core/agent/adapter-api.js";
14
+ /** The attribute the roster verdict is stowed under — ONE spelling, read by `bindTeams` and by the
15
+ * front's `teamsAuthorRefuted`. */
16
+ const TEAMS_USER_TYPE_ATTR = "user_type";
17
+ /**
18
+ * Fold a resolved tenant membership type into an ALREADY-BUILT auth capture (OPE-493).
19
+ *
20
+ * The front resolves the roster verdict AFTER it has built the capture, because the probe is a
21
+ * network call and the capture is built synchronously. This is the seam that carries the answer in,
22
+ * so the attribute name still has exactly one spelling.
23
+ *
24
+ * 🔴 NO VERDICT RETURNS THE SAME OBJECT, by reference. That is the fail-open guarantee stated as
25
+ * code: when the probe cannot answer, nothing downstream can tell the probe ran.
26
+ */
27
+ export function withTeamsUserType(auth, userType) {
28
+ if (!userType)
29
+ return auth;
30
+ return { ...auth, attributes: { ...auth.attributes, [TEAMS_USER_TYPE_ATTR]: userType } };
31
+ }
32
+ /** Build the auth context OpenEverywhere dispatches with — raw facts as string attributes. */
33
+ export function buildTeamsAuth(input) {
34
+ const attributes = {
35
+ conversation_id: input.conversationId,
36
+ user_id: input.userId,
37
+ };
38
+ if (input.aadObjectId)
39
+ attributes.aad_object_id = input.aadObjectId;
40
+ if (input.userName)
41
+ attributes.user_name = input.userName;
42
+ if (input.tenantId)
43
+ attributes.tenant_id = input.tenantId;
44
+ if (input.channelId)
45
+ attributes.channel_id = input.channelId;
46
+ if (input.teamId)
47
+ attributes.team_id = input.teamId;
48
+ if (input.serviceUrl)
49
+ attributes.service_url = input.serviceUrl;
50
+ if (input.scope)
51
+ attributes.scope = input.scope;
52
+ if (input.membershipType)
53
+ attributes.membership_type = input.membershipType;
54
+ if (input.userType)
55
+ attributes[TEAMS_USER_TYPE_ATTR] = input.userType;
56
+ // Root-DM grain stamp (window-tool-gating §D-DM, widened to group DMs): a 1:1 (scope "personal")
57
+ // chat OR a groupChat (⇒ "mpim") is a STANDING seat ⇒ narrowed hands — both are closed multi-party
58
+ // conversations with no channel identity. teamsMode's "channel" branch (scope "channel"/unknown) is
59
+ // unambiguously excluded, so a real team channel is never over-narrowed.
60
+ const teamsScopeMode = teamsMode(input.scope);
61
+ Object.assign(attributes, rootDmGrainAttrs(teamsScopeMode === "dm" || teamsScopeMode === "mpim"));
62
+ return { authenticator: "teams", principalId: input.userId, principalType: "teams-user", attributes };
63
+ }
64
+ /**
65
+ * Pull the channel membershipType out of a Bot Framework activity's `channelData`,
66
+ * which (for channel conversations) may carry `{ channel: { membershipType } }`.
67
+ * Defensive: channelData is raw JSON, so everything is probed, never assumed. Note
68
+ * that inbound message activities frequently OMIT membershipType (it's a Graph/Teams
69
+ * API property, not a guaranteed Activity field) — absence is the common case, and
70
+ * teamsVisibility fails closed to private on it.
71
+ */
72
+ export function teamsMembershipType(channelData) {
73
+ const ch = channelData?.channel;
74
+ if (!ch || typeof ch !== "object")
75
+ return undefined;
76
+ const mt = ch["membershipType"];
77
+ return typeof mt === "string" && mt.length > 0 ? mt : undefined;
78
+ }
79
+ /**
80
+ * The acting user's tenant membership type from a Bot Connector roster member record (or a
81
+ * Microsoft Graph `/users/{id}` object) — the authoritative "is this a GUEST?" signal, the
82
+ * Teams analog of Slack's `partnerFactsFromInfo`. Probes `userType` (Graph: "Guest"/"Member")
83
+ * then `userRole` (Bot roster: "guest" for a guest, else a member role), case-insensitively.
84
+ * Pure + defensive (raw JSON): a missing/failed lookup, an empty role, or an unrecognised value
85
+ * all yield `undefined` (no verdict → the existing channel/vouch trust stands, fail-closed
86
+ * unless vouched). Only a positive "guest" flag → "guest" (→ external demotion); a positively
87
+ * recognised member/owner/admin role → "member".
88
+ *
89
+ * What it PROVES: the tenant classifies this account as a guest (external B2B collaborator) —
90
+ * demote. What it does NOT prove: that a "member" is inside YOUR org boundary if the deployment
91
+ * spans multiple tenants; org-internal trust still requires the channel verdict or an operator
92
+ * vouch. It is a demotion-only signal, never an upgrade.
93
+ */
94
+ export function teamsUserTypeFromMember(info) {
95
+ if (!info || typeof info !== "object")
96
+ return undefined; // lookup failed / absent → no verdict
97
+ const o = info;
98
+ const raw = o["userType"] ?? o["userRole"]; // Graph userType | Bot Connector roster userRole
99
+ if (typeof raw !== "string" || raw.length === 0)
100
+ return undefined; // field absent → no verdict
101
+ const v = raw.toLowerCase();
102
+ if (v === "guest")
103
+ return "guest";
104
+ if (v === "member" || v === "user" || v === "owner" || v === "admin" || v === "organizer")
105
+ return "member";
106
+ return undefined; // unrecognised value → no verdict (fail toward the existing lower/fail-closed tier)
107
+ }
108
+ /**
109
+ * Teams conversation scope → session mode, or UNDEFINED when the scope classifies nothing.
110
+ *
111
+ * 🔴 THE UNDEFINED IS THE POINT (OPE-1867). An inbound activity that carries no `conversationType`
112
+ * — or one Teams spells in a way this mapper does not know — tells us nothing about whether the
113
+ * conversation is a room or a 1:1 chat with one person. Answering "channel" there is a DEFAULT, not
114
+ * a reading, and a reader that publishes the surface must be able to tell the two apart: a personal
115
+ * chat reported as a room names that individual's conversation id in a report posted into a room.
116
+ * {@link teamsMode} applies the default for every reader that needs a usable value; this function
117
+ * is what the identity mapper hands the binding, so the provenance survives.
118
+ */
119
+ export function teamsResolvedMode(scope) {
120
+ if (scope === "personal")
121
+ return "dm"; // a 1:1 chat with the bot
122
+ if (scope === "groupChat")
123
+ return "mpim"; // an ad-hoc group chat
124
+ if (scope === "channel")
125
+ return "channel"; // a team channel
126
+ return undefined; // "unknown" / unrecognised → nothing classified this conversation
127
+ }
128
+ /** Teams conversation scope → session mode, GAP-FILLED to `channel` when nothing classifies it.
129
+ * Visibility, trust and the seat-grain stamps all need a definite value and all fail closed on
130
+ * their own terms, so the default is right for them. A reader that PUBLISHES the surface wants
131
+ * {@link teamsResolvedMode} instead. */
132
+ export function teamsMode(scope) {
133
+ return teamsResolvedMode(scope) ?? "channel";
134
+ }
135
+ /**
136
+ * Teams facts → visibility. FAIL CLOSED, mirroring slackVisibility: anything we
137
+ * can't positively confirm as a team-wide (standard) channel is treated as
138
+ * `private`, so a surface's learnings never leak up to shared memory on an
139
+ * under-specified event. Personal/group chats are never public surfaces. For a team
140
+ * channel, only `membershipType === "standard"` (the org/team-wide channel everyone
141
+ * on the team can see) yields `public`; `private`/`shared`/unknown → `private`.
142
+ * (Inbound activities usually DON'T carry membershipType, so the fail-closed branch
143
+ * is the normal case for channels — private is the safe default.)
144
+ */
145
+ export function teamsVisibility(scope, membershipType, mode) {
146
+ // 1:1 and group chats are never "public" surfaces.
147
+ if (mode === "dm" || mode === "mpim")
148
+ return "private";
149
+ if (scope === "personal" || scope === "groupChat")
150
+ return "private";
151
+ if (membershipType === "standard")
152
+ return "public"; // an explicit team-wide channel
153
+ return "private"; // private/shared/unknown channel → fail closed
154
+ }
155
+ /**
156
+ * Teams membership → trust (memory gate). FAIL CLOSED — only POSITIVE facts yield a
157
+ * verdict. A `shared` channel is a Teams Shared Channel — it can include people from
158
+ * OTHER tenants/orgs (the Teams analog of Slack Connect) — so it's `external` and gets
159
+ * no shared memory. A channel positively identified as `standard`/`private` AND carrying
160
+ * its tenant id is in-tenant → `internal`. Personal/group chats are personal silos
161
+ * (gate N/A → internal; bindFrom's DM-partner gate still fails an unverified DM partner
162
+ * closed). ANYTHING ELSE — absent membershipType (the common case: inbound activities
163
+ * routinely omit it), an unrecognised value, or a missing tenant id — returns
164
+ * `undefined`, so bindFrom FAILS CLOSED to `external` (shared/org memory gated off)
165
+ * unless the operator vouches for the deployment with OE_TRUST=internal — the same
166
+ * layer Discord/Linear use. An ambiguous Teams event must never reach the
167
+ * shared org brain.
168
+ *
169
+ * GUEST DETECTION (OPE-60): a `userType` positively resolved to "guest" (a cross-org / B2B
170
+ * collaboration account inside the tenant, via the cached roster lookup) DEMOTES to `external`
171
+ * FIRST — ahead of every other branch, including the personal-silo and standard-channel
172
+ * `internal` verdicts and the operator vouch (bindFrom's precedence gives a mapper's own
173
+ * `external` verdict priority over the vouch). This is exactly how Slack demotes a restricted
174
+ * DM partner. An absent/unknown `userType` (the common case — no lookup, or a lookup that
175
+ * couldn't confirm) leaves the existing verdict intact, so behaviour is unchanged unless a
176
+ * guest is positively detected.
177
+ */
178
+ export function teamsTrust(scope, membershipType, mode, tenantId, userType) {
179
+ if (userType === "guest")
180
+ return "external"; // positively-detected guest → demote (wins over vouch)
181
+ if (mode === "dm" || mode === "mpim")
182
+ return "internal"; // personal silo; gate N/A
183
+ if (scope === "personal" || scope === "groupChat")
184
+ return "internal";
185
+ if (membershipType === "shared")
186
+ return "external"; // cross-tenant Shared Channel
187
+ if (tenantId && (membershipType === "standard" || membershipType === "private")) {
188
+ return "internal"; // positively-identified in-tenant channel
189
+ }
190
+ return undefined; // unverifiable membership/tenant → bindFrom fails closed to external
191
+ }
192
+ function teamsAuthOf(ctx) {
193
+ return ctx.session?.auth?.current ?? ctx.session?.auth?.initiator ?? null;
194
+ }
195
+ function teamsAttr(a, keys) {
196
+ if (!a)
197
+ return undefined;
198
+ for (const k of keys) {
199
+ const v = a[k];
200
+ if (typeof v === "string" && v.length > 0)
201
+ return v;
202
+ if (Array.isArray(v) && typeof v[0] === "string" && v[0].length > 0)
203
+ return v[0];
204
+ }
205
+ return undefined;
206
+ }
207
+ function teamsAuthPrincipalId(ctx) {
208
+ const pid = teamsAuthOf(ctx)?.principalId;
209
+ return pid && pid.length > 0 ? pid : undefined;
210
+ }
211
+ /**
212
+ * The trust-registry GROUP rung for a Teams TEAM (OPE-490), or `undefined` when this activity has
213
+ * no team (a personal or group chat) or the id is not a safe key segment.
214
+ *
215
+ * FAIL-CLOSED and REFUSING, never repairing: an id that would need sanitising contributes no rung
216
+ * at all. Repairing it would risk composing a segment that collides with a real key, and a missing
217
+ * rung is simply the pre-OPE-490 behaviour.
218
+ *
219
+ * Exported for the identity test, which pins the marker spelling — the spelling is a config
220
+ * contract an operator types into `agent/identity/surfaces.ts`, so it must not drift silently.
221
+ */
222
+ export function teamsTrustGroup(teamId) {
223
+ if (teamId === undefined || !/^[a-zA-Z0-9._-]+$/.test(teamId))
224
+ return undefined;
225
+ return { trustGroup: `team:${teamId}` };
226
+ }
227
+ export const bindTeams = (ctx) => {
228
+ const a = teamsAuthOf(ctx)?.attributes;
229
+ const scope = teamsAttr(a, ["scope"]);
230
+ const membershipType = teamsAttr(a, ["membership_type", "membershipType"]);
231
+ const userType = teamsAttr(a, ["user_type", "userType"]); // "guest" | "member" — cached roster probe (teams-roster.ts)
232
+ const mode = teamsMode(scope);
233
+ const conversationId = teamsAttr(a, ["conversation_id", "conversationId"]);
234
+ const serviceUrl = teamsAttr(a, ["service_url", "serviceUrl"]);
235
+ const tenantId = teamsAttr(a, ["tenant_id", "tenantId"]);
236
+ return {
237
+ workspace: tenantId,
238
+ surface: conversationId,
239
+ // A channel-scoped Teams CONVERSATION names one reply chain (the conversation id carries
240
+ // `;messageid=<root>`), so the listable/vouchable/spend-cappable container is the Teams
241
+ // CHANNEL — domain membership resolves at the channel grain, mirroring the Discord
242
+ // thread→parent-channel precedent (bindDiscord.domainSurface). Absent (personal/groupChat,
243
+ // or an activity with no channel id) → fall back to the conversation grain, today's exact
244
+ // behaviour (bindFrom only surfaces domainSurface when it differs from surface).
245
+ domainSurface: teamsAttr(a, ["channel_id", "channelId"]),
246
+ // 🔴 THE RESOLVED mode, not the gap-filled one (OPE-1867). An activity with no recognised
247
+ // `conversationType` contributes NO mode here; bindFrom still gap-fills `channel`, so every
248
+ // reader below and downstream is unchanged — but the binding now says the value was a default,
249
+ // and the one reader that publishes a surface refuses it. `visibility` and `trust` keep taking
250
+ // the gap-filled `mode` local above, which is what their own fail-closed rules are written for.
251
+ mode: teamsResolvedMode(scope),
252
+ // 🔴 THE TEAM RUNG (OPE-490). A Teams channel keys as `teams/<tenant>/<conversationId>` and the
253
+ // TEAM is nowhere in that key, so an operator could not declare a whole team internal or public
254
+ // — only enumerate its channels. This contributes the team as a MARKED group segment, which the
255
+ // trust registry matches as a rung BETWEEN the tenant and the surface
256
+ // (identity/surface-key-grammar.ts). An operator then writes:
257
+ //
258
+ // defineSurface({ key: "teams/<tenantId>/team:<teamId>", trust: "internal" })
259
+ //
260
+ // The vendor word "team" lives HERE, in the Teams bundle, never in core — core composes
261
+ // `<platform>/<tenant>/<segment>` and validates only the `<marker>:<id>` shape.
262
+ //
263
+ // It changes NO domain semantics: the surface key, the domain key and every memory/spend/audit
264
+ // grain are byte-identical. `teamId` is present only on a CHANNEL activity, so a personal or
265
+ // group chat contributes no rung and resolves exactly as before.
266
+ ...(teamsTrustGroup(teamsAttr(a, ["team_id", "teamId"])) ?? {}),
267
+ visibility: teamsVisibility(scope, membershipType, mode),
268
+ trust: teamsTrust(scope, membershipType, mode, tenantId, userType),
269
+ principalId: teamsAuthPrincipalId(ctx) ?? teamsAttr(a, ["user_id", "userId"]),
270
+ // 🔴 THE ENGAGEMENT-REGISTRY KEY (OPE-1500), the Discord precedent (`bindDiscord.engagementKey`)
271
+ // in Teams' own vocabulary. It is the CONVERSATION id, which names the REPLY CHAIN in a team
272
+ // channel (the id carries `;messageid=<root>`) and the CHAT in a group chat — the two containers
273
+ // an engagement can be kept per on this surface. The same id is this binding's `surface`, so
274
+ // `teams/<tenant>/<conversationId>` + `<conversationId>` addresses the exact row the seam's wake
275
+ // gate reads back (channels/teams-engagement.ts). Publishing it here is what lets `stand_down`
276
+ // mark that row; without it the verb found no anchor and refused on every Teams turn.
277
+ //
278
+ // NEVER ON A 1:1 CHAT, and the omission is the honest half — Discord's DM reasoning exactly. A
279
+ // personal chat is answered unconditionally (the front's DM divert bypasses the engagement gate
280
+ // by design), so a mark on it would suppress nothing, and `stand_down` says it could not stop
281
+ // rather than writing a row that changes nothing.
282
+ ...(teamsMode(scope) !== "dm" && conversationId ? { engagementKey: conversationId } : {}),
283
+ // 🔴 THE ROOM-GRAIN STOP ANCHOR (OPE-1511), and Teams is the only surface that has one. The
284
+ // single-window front's Leg A wakes on ANY message in a vouched CHANNEL, including a reply chain
285
+ // no @mention ever engaged — a room-grain wake, which the conversation-grain key above cannot
286
+ // address. Publishing the CHANNEL id here is what lets `stand_down` reach the room row that wake
287
+ // stores (channels/teams-engagement.ts), instead of refusing on a wake the person can plainly
288
+ // see. The verb marks it only when the conversation grain held no row, so an ordinary
289
+ // thread-grain stop is unchanged.
290
+ //
291
+ // CHANNEL SCOPE ONLY. A personal or group chat has no Leg A — the front diverts both into their
292
+ // own DM window above `planFor` — so there is no room wake to stop and a mark here would be a
293
+ // write nobody reads. The value is the raw channel id, for the audit trail; the row's address is
294
+ // derived from the partition on both sides, so no spelling can drift.
295
+ ...(mode === "channel" && teamsAttr(a, ["channel_id", "channelId"])
296
+ ? { roomEngagementKey: teamsAttr(a, ["channel_id", "channelId"]) }
297
+ : {}),
298
+ // THE STOP REGISTRY'S ADDRESS FOR THIS TURN (OPE-898) — the CONVERSATION, which on a Teams
299
+ // channel means the REPLY CHAIN, not the channel. A `messageReaction` activity carries
300
+ // `conversation.id`, so the reply chain is the only container a stop trigger can name.
301
+ //
302
+ // 🔴 IT IS NOT `target.channelId`, even though this binding has one. That field holds the Teams
303
+ // CHANNEL id, which is a coarser room than the turn ran in and is absent on a personal chat —
304
+ // so keying the registry on it would both over-reach and leave every DM turn unstoppable.
305
+ // `anchorChannelId` wins over `target.channelId` in hooks/turn-anchor.ts for exactly this case.
306
+ ...(conversationId ? { anchorChannelId: conversationId } : {}),
307
+ // Proactive post-back needs the Bot Connector CONVERSATION REFERENCE (serviceUrl +
308
+ // conversation id), not a thread anchor — Teams can't cold-address a chat by AAD
309
+ // user id (Bot Framework requires a reference captured on a prior inbound turn,
310
+ // which is exactly what this is: every field comes off THIS inbound activity).
311
+ // `conversationType` (the activity's scope: personal/groupChat/channel) rides along
312
+ // so a routine FIRE can re-derive mode without guessing from id shapes — the
313
+ // dispatcher (@open-everywhere/provider-teams register.ts resolveTeamsTarget) stamps it
314
+ // back as the `scope` attribute this very mapper reads.
315
+ target: conversationId && serviceUrl
316
+ ? {
317
+ serviceUrl,
318
+ conversationId,
319
+ tenantId,
320
+ channelId: teamsAttr(a, ["channel_id", "channelId"]),
321
+ teamId: teamsAttr(a, ["team_id", "teamId"]),
322
+ conversationType: scope,
323
+ // The creator's Azure AD object id (OPE-522) — carried on the STORED target so a routine
324
+ // created from this turn can be re-checked at fire time (routines/fire-eligibility.ts's
325
+ // Teams leg): Graph's `/users/{id}` and a channel roster's `userId` are both keyed on
326
+ // this value, a DIFFERENT identifier from the Bot Framework `from.id` this mapper's own
327
+ // `principalId` carries (see buildTeamsAuth's `aad_object_id` attribute, captured above).
328
+ // Absent when the inbound activity didn't carry one (e.g. no Teams AAD linkage) — a
329
+ // routine created from such a turn simply has no Teams eligibility leg to run, matching
330
+ // every other "cannot check" case in this module family.
331
+ aadObjectId: teamsAttr(a, ["aad_object_id", "aadObjectId"]),
332
+ }
333
+ : undefined,
334
+ };
335
+ };
336
+ //# sourceMappingURL=teams-identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teams-identity.js","sourceRoot":"","sources":["../../../agent/channels/teams-identity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { TeamsContext, TeamsInputResponseResult, TeamsInvokeActivity, TeamsMessageActivity } from "eve/channels/teams";
2
+ import { type IngressSpendGateInput, type InputResponseAdmission, type InputResponseScope, type SpendGateDecision } from "@open-everywhere/core/agent/adapter-api.js";
3
+ export interface TeamsInputResponseDeps {
4
+ /** The core admission. Injected in tests; the live default is the shared decision. */
5
+ readonly admit?: (scope: InputResponseScope) => Promise<InputResponseAdmission>;
6
+ /** Positive per-member verdict for leg B. Live default is the Bot Connector member probe. */
7
+ readonly userType?: (activity: TeamsInvokeActivity | TeamsMessageActivity) => Promise<"guest" | "member" | undefined>;
8
+ /** Render the refusal. Teams has no ephemeral primitive on this handle, so it is a thread post. */
9
+ readonly notify?: (text: string) => Promise<void>;
10
+ /** The shared spend-admission decision (OPE-588). Injected in tests; the live default is
11
+ * `ingressSpendGate` (spend/ingress-gate.ts) — the SAME function every ordinary ingress door
12
+ * consults. */
13
+ readonly spendGate?: (input: IngressSpendGateInput) => Promise<SpendGateDecision>;
14
+ }
15
+ /**
16
+ * Admit or refuse one eve-owned HITL answer submitted through Teams.
17
+ *
18
+ * Returns `{ auth }` on an allow — the same auth shape `onMessage` builds, so an admitted answer
19
+ * resumes the parked turn under OE's real Teams identity rather than eve's thinner default.
20
+ * Returns `null` on a refusal, which leaves the request PENDING (eve has no cancel on this seam).
21
+ */
22
+ export declare function admitTeamsInputResponse(ctx: TeamsContext, activity: TeamsInvokeActivity | TeamsMessageActivity, deps?: TeamsInputResponseDeps): Promise<TeamsInputResponseResult>;
23
+ //# sourceMappingURL=teams-input-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teams-input-response.d.ts","sourceRoot":"","sources":["../../../agent/channels/teams-input-response.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,YAAY,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC5H,OAAO,EAKL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACvB,MAAM,4CAA4C,CAAC;AAIpD,MAAM,WAAW,sBAAsB;IACrC,sFAAsF;IACtF,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAChF,6FAA6F;IAC7F,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,mBAAmB,GAAG,oBAAoB,KAAK,OAAO,CAAC,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC,CAAC;IACtH,mGAAmG;IACnG,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD;;oBAEgB;IAChB,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACnF;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,YAAY,EACjB,QAAQ,EAAE,mBAAmB,GAAG,oBAAoB,EACpD,IAAI,GAAE,sBAA2B,GAChC,OAAO,CAAC,wBAAwB,CAAC,CA4DnC"}
@@ -0,0 +1,102 @@
1
+ // THE TEAMS ARM OF THE HITL-ANSWER ADMISSION (OPE-574).
2
+ //
3
+ // 🔴 THE TEAMS PREMISE IS THE OPPOSITE OF SLACK'S, AND THE TICKET SAID OTHERWISE. eve's Teams
4
+ // channel resolves the hook as `onInputResponse ?? (onMessage === undefined ? defaultOnInputResponse
5
+ // : rejectInput)` and `rejectInput` returns `null` for every submission. OE authors `onMessage`
6
+ // (channels/teams.ts), so on Teams NOBODY could answer a HITL card — every Adaptive-Card submit and
7
+ // every card-valued message was dropped, with no post, no row and no log line. The Slack hole
8
+ // (anyone may answer) does not exist here; a silent dead end did. Authoring this hook closes both
9
+ // at once: it opens the path AND governs it. The two eve behaviours are pinned against the
10
+ // installed dist by `packages/core/scripts/eve-install-integrity-test.ts`.
11
+ //
12
+ // LEG B HAS TEETH HERE. `resolveTeamsUserType` reads the Bot Connector's conversation-member
13
+ // record and returns a POSITIVE `guest`/`member` verdict per person — the same probe the inbound
14
+ // message path already runs on every activity — so a guest answering from an ordinary internal
15
+ // channel is refused. That was the residual approvals/clicker-floor.ts named before OPE-589; Slack
16
+ // closed the same gap with its own probe (`slackClickerIsGuest`), so this is no longer a
17
+ // Teams-only capability. An unresolvable probe returns `undefined` and the leg is INERT: "we could
18
+ // not ask" never becomes a refusal.
19
+ //
20
+ // 🔴 FAIL-OPEN ON A THROW. eve logs a thrown hook and drops the answer, which would put Teams back
21
+ // where it started. Anything unexpected therefore returns the default auth and says so in the log.
22
+ //
23
+ // 🔴 SPEND GATE (OPE-588), ahead of the room/guest admission below. This card action RESUMES a
24
+ // parked turn: a decision to spend more, taken through a button, on a scope nobody asked whether it
25
+ // could afford. Every ordinary ingress door consults the ONE shared spend gate before it dispatches
26
+ // a turn (spend/ingress-gate.ts); eve's own Teams invoke route calls `onInputResponse` and then
27
+ // `.respond()` directly, with no call site of that gate anywhere on the seam — verified against the
28
+ // installed eve/dist. A decline returns `null`, the same reject-the-answer outcome a room/guest
29
+ // refusal already leaves — the request stays PENDING.
30
+ import { admitInputResponse, ingressSpendGate, resolvePartition, } from "@open-everywhere/core/agent/adapter-api.js";
31
+ import { buildTeamsAuth, teamsMembershipType } from "./teams-identity.js";
32
+ import { resolveTeamsUserType, teamsHandleRosterProbe } from "./teams-roster.js";
33
+ /**
34
+ * Admit or refuse one eve-owned HITL answer submitted through Teams.
35
+ *
36
+ * Returns `{ auth }` on an allow — the same auth shape `onMessage` builds, so an admitted answer
37
+ * resumes the parked turn under OE's real Teams identity rather than eve's thinner default.
38
+ * Returns `null` on a refusal, which leaves the request PENDING (eve has no cancel on this seam).
39
+ */
40
+ export async function admitTeamsInputResponse(ctx, activity, deps = {}) {
41
+ const probe = deps.userType ?? ((a) => resolveTeamsUserType(teamsHandleRosterProbe(ctx.teams), a.conversation.id, a.from.id));
42
+ let auth;
43
+ try {
44
+ const userType = await probe(activity);
45
+ auth = buildTeamsAuth({
46
+ userId: activity.from.id,
47
+ aadObjectId: activity.from.aadObjectId,
48
+ userName: activity.from.name,
49
+ tenantId: activity.tenantId ?? activity.conversation.tenantId,
50
+ conversationId: activity.conversation.id,
51
+ channelId: activity.teamsChannelId,
52
+ teamId: activity.teamId,
53
+ serviceUrl: activity.serviceUrl,
54
+ scope: activity.scope,
55
+ membershipType: teamsMembershipType(activity.channelData),
56
+ userType,
57
+ });
58
+ const bindable = {
59
+ channel: { kind: "teams" },
60
+ session: { auth: { current: auth } },
61
+ };
62
+ const p = resolvePartition(bindable);
63
+ // SPEND GATE (OPE-588) — ahead of the room/guest admission below (see the header note). A
64
+ // decline returns `null` immediately, before the room/guest floor runs: over budget outranks
65
+ // identity, the same ordering `channels/unified.ts`'s `windowSpendGate` takes ahead of its own
66
+ // `resolveInboundAnswer`.
67
+ const spendGate = deps.spendGate ?? ingressSpendGate;
68
+ const spendVerdict = await spendGate({
69
+ bindable,
70
+ door: "answer",
71
+ principalId: activity.from.id,
72
+ post: async (text) => { await ctx.thread.post(text); },
73
+ });
74
+ if (spendVerdict === "declined")
75
+ return null;
76
+ const verdict = await (deps.admit ?? admitInputResponse)({
77
+ org: p.org,
78
+ platform: p.platform,
79
+ workspace: p.workspace,
80
+ surface: p.surface,
81
+ surfaceKey: p.surfaceKey,
82
+ principalId: activity.from.id,
83
+ // `responses` is deliberately ABSENT: eve decodes the submission AFTER this hook returns, so
84
+ // Teams cannot know the count. A zero here would read as a measured value in the audit row.
85
+ ...(userType === undefined ? {} : { answererIsGuest: userType === "guest" }),
86
+ });
87
+ if (verdict.ok)
88
+ return { auth };
89
+ const notify = deps.notify ?? (async (text) => { await ctx.thread.post(text); });
90
+ await notify(verdict.notice).catch((err) => {
91
+ console.error(`[open-everywhere:teams] HITL refusal notice failed to post: ${String(err)}`);
92
+ });
93
+ return null;
94
+ }
95
+ catch (err) {
96
+ console.error(`[open-everywhere:teams] HITL answer admission failed softly — accepting the answer: ${String(err)}`);
97
+ // FAIL-OPEN with whatever auth we managed to build. `null` here would restore eve's silent
98
+ // `rejectInput`, which is the bug this module exists to close.
99
+ return { auth: auth ?? null };
100
+ }
101
+ }
102
+ //# sourceMappingURL=teams-input-response.js.map