lex-microsoft_teams 0.5.4 → 0.5.6
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/CLAUDE.md +25 -7
- data/lib/legion/extensions/microsoft_teams/helpers/browser_auth.rb +67 -12
- data/lib/legion/extensions/microsoft_teams/hooks/auth.rb +21 -0
- data/lib/legion/extensions/microsoft_teams/runners/auth.rb +29 -0
- data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d414b08d6eaabf909ab4de6fe900cc879bdeda369ea386925c2c3258ed9d4a24
|
|
4
|
+
data.tar.gz: 8bdf0da6fb7f666eb9a4489180370e5f4e3124104e9c8736d85e9d0dbe1afbb5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 02b042dea9cc6292403cc504c6bdcfde101e63c24c7120300985a0825ea3dcf11aa988705f64ad9ff5bace61513b4b621f263740f7ce9e40e858b45123017acb
|
|
7
|
+
data.tar.gz: 9d09c3aa2518ccc19655d864c393f01ff2a578c7085c47ff4dfa130b1fb8dcec6a6cb465b2c3ae57787617201f06f9528006f60d6c0373860e2191c5b9a3216d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.6] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- BrowserAuth API hook detection: uses hook URL when Legion::API is running instead of ephemeral CallbackServer
|
|
7
|
+
- `api_hook_available?` and `hook_redirect_uri` methods on BrowserAuth
|
|
8
|
+
- `authenticate_via_hook` path using `Legion::Events` for callback notification
|
|
9
|
+
- `authenticate_via_server` extracted from original `authenticate_browser` as fallback path
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- `authenticate_browser` now delegates to hook path (API running) or server path (standalone)
|
|
13
|
+
|
|
14
|
+
## [0.5.5] - 2026-03-19
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `Hooks::Auth` hook class with `mount '/callback'` for OAuth redirect via expanded hooks system
|
|
18
|
+
- `Runners::Auth#auth_callback` method handling OAuth callback with HTML response and event emission
|
|
19
|
+
- OAuth callback now routes through `Ingress.run` for RBAC and audit support
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- OAuth callback URL moves from hardcoded `/api/oauth/microsoft_teams/callback` to `/api/hooks/lex/microsoft_teams/auth/callback`
|
|
23
|
+
|
|
3
24
|
## [0.5.4] - 2026-03-19
|
|
4
25
|
|
|
5
26
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -10,14 +10,14 @@ Legion Extension that connects LegionIO to Microsoft Teams via Graph API and Bot
|
|
|
10
10
|
|
|
11
11
|
**GitHub**: https://github.com/LegionIO/lex-microsoft_teams
|
|
12
12
|
**License**: MIT
|
|
13
|
-
**Version**: 0.5.
|
|
13
|
+
**Version**: 0.5.5
|
|
14
14
|
|
|
15
15
|
## Architecture
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
Legion::Extensions::MicrosoftTeams
|
|
19
19
|
├── Runners/
|
|
20
|
-
│ ├── Auth # OAuth2 client credentials (Graph + Bot Framework)
|
|
20
|
+
│ ├── Auth # OAuth2 client credentials (Graph + Bot Framework) + auth_callback for hook
|
|
21
21
|
│ ├── Teams # List/get teams, members
|
|
22
22
|
│ ├── Chats # 1:1 and group chat CRUD
|
|
23
23
|
│ ├── Messages # Chat message send/read/reply
|
|
@@ -36,7 +36,9 @@ Legion::Extensions::MicrosoftTeams
|
|
|
36
36
|
│ ├── CacheSync # Every 5min: incremental ingest of new messages
|
|
37
37
|
│ ├── DirectChatPoller # Every 5s: polls bot DM chats via Graph API
|
|
38
38
|
│ ├── ObservedChatPoller # Every 30s: polls subscribed human conversations (compliance-gated)
|
|
39
|
-
│
|
|
39
|
+
│ ├── MessageProcessor # Subscription: consumes AMQP queue, routes by mode
|
|
40
|
+
│ ├── AuthValidator # Once: validates/restores delegated tokens on boot (2s delay)
|
|
41
|
+
│ └── TokenRefresher # Every 15min (configurable): keeps delegated tokens fresh
|
|
40
42
|
├── Transport/
|
|
41
43
|
│ ├── Exchanges/Messages # teams.messages topic exchange
|
|
42
44
|
│ ├── Queues/MessagesProcess # teams.messages.process durable queue
|
|
@@ -50,10 +52,12 @@ Legion::Extensions::MicrosoftTeams
|
|
|
50
52
|
│ ├── HighWaterMark # Per-chat message dedup via legion-cache (with in-memory fallback)
|
|
51
53
|
│ ├── PromptResolver # Layered system prompt resolution (settings -> mode -> per-conversation)
|
|
52
54
|
│ ├── SessionManager # Multi-turn LLM session lifecycle with lex-memory persistence
|
|
53
|
-
│ ├── TokenCache # In-memory OAuth token cache with pre-expiry refresh (app + delegated slots)
|
|
55
|
+
│ ├── TokenCache # In-memory OAuth token cache with pre-expiry refresh (app + delegated slots, authenticated?/previously_authenticated? predicates)
|
|
54
56
|
│ ├── SubscriptionRegistry # Conversation observation subscriptions (in-memory + lex-memory)
|
|
55
57
|
│ ├── BrowserAuth # Delegated OAuth orchestrator (PKCE, headless detection, browser launch)
|
|
56
58
|
│ └── CallbackServer # Ephemeral TCP server for OAuth redirect callback
|
|
59
|
+
├── Hooks/
|
|
60
|
+
│ └── Auth # OAuth callback hook (mount '/callback') → /api/hooks/lex/microsoft_teams/auth/callback
|
|
57
61
|
└── Client # Standalone client (includes all runners)
|
|
58
62
|
```
|
|
59
63
|
|
|
@@ -64,9 +68,21 @@ Opt-in browser-based OAuth for delegated Microsoft Graph permissions. Two flows:
|
|
|
64
68
|
- **Authorization Code + PKCE** (primary): Opens browser for Entra ID login, captures callback on ephemeral local port, exchanges code with PKCE verification
|
|
65
69
|
- **Device Code** (fallback): Auto-selected in headless/SSH environments (no `DISPLAY`/`WAYLAND_DISPLAY`)
|
|
66
70
|
|
|
67
|
-
Tokens stored in Vault (`legionio/microsoft_teams/delegated_token`) with configurable pre-expiry silent refresh. CLI command: `legion auth teams`.
|
|
71
|
+
Tokens stored in Vault (`legionio/microsoft_teams/delegated_token`) with configurable pre-expiry silent refresh. CLI command: `legion auth teams`. Hook route: `GET|POST /api/hooks/lex/microsoft_teams/auth/callback` for daemon re-auth (routed through Ingress for RBAC/audit).
|
|
68
72
|
|
|
69
|
-
Key files: `Helpers::BrowserAuth` (orchestrator), `Helpers::CallbackServer` (ephemeral TCP), `Runners::Auth` (authorize_url, exchange_code, refresh_delegated_token), `Helpers::TokenCache` (delegated slot).
|
|
73
|
+
Key files: `Helpers::BrowserAuth` (orchestrator), `Helpers::CallbackServer` (ephemeral TCP), `Runners::Auth` (authorize_url, exchange_code, refresh_delegated_token, auth_callback), `Helpers::TokenCache` (delegated slot), `Hooks::Auth` (hook class with mount path).
|
|
74
|
+
|
|
75
|
+
## Token Lifecycle (v0.5.4)
|
|
76
|
+
|
|
77
|
+
Automatic delegated token management: validate on boot, refresh on a timer, re-authenticate via browser when a previously authenticated user's token expires.
|
|
78
|
+
|
|
79
|
+
- **AuthValidator** (Once actor, 2s delay): Loads token from Vault/local file on boot, attempts refresh. If refresh fails and user previously authenticated (`previously_authenticated?` — local file exists), fires BrowserAuth. Silent for users who never opted in.
|
|
80
|
+
- **TokenRefresher** (Every actor, 15min default): Guards with `authenticated?` (live token in memory). Refreshes and persists on each tick. On failure, same re-auth logic as AuthValidator.
|
|
81
|
+
- **TokenCache predicates**: `authenticated?` = live token in `@delegated_cache`. `previously_authenticated?` = local token file exists on disk. This distinction controls auto re-auth (returning users only) vs silence (never-authenticated users).
|
|
82
|
+
|
|
83
|
+
Configuration: `settings[:microsoft_teams][:auth][:delegated][:refresh_interval]` (default 900 seconds).
|
|
84
|
+
|
|
85
|
+
Design doc: `docs/plans/2026-03-19-teams-token-lifecycle-design.md`
|
|
70
86
|
|
|
71
87
|
## AI Bot (v0.2.0)
|
|
72
88
|
|
|
@@ -104,6 +120,8 @@ microsoft_teams:
|
|
|
104
120
|
tenant_id: "..."
|
|
105
121
|
client_id: "..."
|
|
106
122
|
client_secret: "vault://secret/teams/client_secret"
|
|
123
|
+
delegated:
|
|
124
|
+
refresh_interval: 900 # seconds (TokenRefresher interval)
|
|
107
125
|
bot:
|
|
108
126
|
bot_id: "28:your-bot-id"
|
|
109
127
|
direct_poll_interval: 5 # seconds
|
|
@@ -197,7 +215,7 @@ Optional framework dependencies (guarded with `defined?`, not in gemspec):
|
|
|
197
215
|
|
|
198
216
|
```bash
|
|
199
217
|
bundle install
|
|
200
|
-
bundle exec rspec #
|
|
218
|
+
bundle exec rspec # 219 specs (as of v0.5.5)
|
|
201
219
|
bundle exec rubocop # Clean
|
|
202
220
|
```
|
|
203
221
|
|
|
@@ -32,6 +32,19 @@ module Legion
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def api_hook_available?
|
|
36
|
+
!!(defined?(Legion::API) && defined?(Legion::Events))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def hook_redirect_uri
|
|
40
|
+
port = if defined?(Legion::Settings)
|
|
41
|
+
Legion::Settings.dig(:api, :port) || 4567
|
|
42
|
+
else
|
|
43
|
+
4567
|
|
44
|
+
end
|
|
45
|
+
"http://127.0.0.1:#{port}/api/hooks/lex/microsoft_teams/auth/callback"
|
|
46
|
+
end
|
|
47
|
+
|
|
35
48
|
def generate_pkce
|
|
36
49
|
verifier = SecureRandom.urlsafe_base64(32)
|
|
37
50
|
challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), padding: false)
|
|
@@ -66,17 +79,62 @@ module Legion
|
|
|
66
79
|
verifier, challenge = generate_pkce
|
|
67
80
|
state = SecureRandom.hex(32)
|
|
68
81
|
|
|
82
|
+
if api_hook_available?
|
|
83
|
+
authenticate_via_hook(verifier: verifier, challenge: challenge, state: state)
|
|
84
|
+
else
|
|
85
|
+
authenticate_via_server(verifier: verifier, challenge: challenge, state: state)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def authenticate_via_hook(verifier:, challenge:, state:)
|
|
90
|
+
callback_uri = hook_redirect_uri
|
|
91
|
+
result_holder = { result: nil }
|
|
92
|
+
mutex = Mutex.new
|
|
93
|
+
cv = ConditionVariable.new
|
|
94
|
+
|
|
95
|
+
listener = Legion::Events.once('microsoft_teams.oauth.callback') do |event|
|
|
96
|
+
mutex.synchronize do
|
|
97
|
+
result_holder[:result] = event
|
|
98
|
+
cv.broadcast
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
url = @auth.authorize_url(
|
|
103
|
+
tenant_id: tenant_id, client_id: client_id,
|
|
104
|
+
redirect_uri: callback_uri, scope: scopes,
|
|
105
|
+
state: state, code_challenge: challenge
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
log_info('Opening browser for authentication (using API hook)...')
|
|
109
|
+
unless open_browser(url)
|
|
110
|
+
Legion::Events.off('microsoft_teams.oauth.callback', listener)
|
|
111
|
+
log_info('Could not open browser. Falling back to device code flow.')
|
|
112
|
+
return authenticate_device_code
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
mutex.synchronize { cv.wait(mutex, 120) unless result_holder[:result] }
|
|
116
|
+
result = result_holder[:result]
|
|
117
|
+
|
|
118
|
+
return { error: 'timeout', description: 'No callback received within timeout' } unless result && result[:code]
|
|
119
|
+
|
|
120
|
+
return { error: 'state_mismatch', description: 'CSRF state parameter mismatch' } unless result[:state] == state
|
|
121
|
+
|
|
122
|
+
@auth.exchange_code(
|
|
123
|
+
tenant_id: tenant_id, client_id: client_id,
|
|
124
|
+
code: result[:code], redirect_uri: callback_uri,
|
|
125
|
+
code_verifier: verifier, scope: scopes
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def authenticate_via_server(verifier:, challenge:, state:)
|
|
69
130
|
server = CallbackServer.new
|
|
70
131
|
server.start
|
|
71
132
|
callback_uri = server.redirect_uri
|
|
72
133
|
|
|
73
134
|
url = @auth.authorize_url(
|
|
74
|
-
tenant_id:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
scope: scopes,
|
|
78
|
-
state: state,
|
|
79
|
-
code_challenge: challenge
|
|
135
|
+
tenant_id: tenant_id, client_id: client_id,
|
|
136
|
+
redirect_uri: callback_uri, scope: scopes,
|
|
137
|
+
state: state, code_challenge: challenge
|
|
80
138
|
)
|
|
81
139
|
|
|
82
140
|
log_info('Opening browser for authentication...')
|
|
@@ -92,12 +150,9 @@ module Legion
|
|
|
92
150
|
return { error: 'state_mismatch', description: 'CSRF state parameter mismatch' } unless result[:state] == state
|
|
93
151
|
|
|
94
152
|
@auth.exchange_code(
|
|
95
|
-
tenant_id:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
redirect_uri: callback_uri,
|
|
99
|
-
code_verifier: verifier,
|
|
100
|
-
scope: scopes
|
|
153
|
+
tenant_id: tenant_id, client_id: client_id,
|
|
154
|
+
code: result[:code], redirect_uri: callback_uri,
|
|
155
|
+
code_verifier: verifier, scope: scopes
|
|
101
156
|
)
|
|
102
157
|
ensure
|
|
103
158
|
server&.shutdown
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MicrosoftTeams
|
|
6
|
+
module Hooks
|
|
7
|
+
class Auth < Legion::Extensions::Hooks::Base
|
|
8
|
+
mount '/callback'
|
|
9
|
+
|
|
10
|
+
def route(_headers, _payload)
|
|
11
|
+
:auth_callback
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def runner_class
|
|
15
|
+
'Legion::Extensions::MicrosoftTeams::Runners::Auth'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -107,6 +107,35 @@ module Legion
|
|
|
107
107
|
{ result: response.body }
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
+
def auth_callback(code: nil, state: nil, **)
|
|
111
|
+
unless code && state
|
|
112
|
+
return {
|
|
113
|
+
result: { error: 'missing_params' },
|
|
114
|
+
response: { status: 400, content_type: 'text/html',
|
|
115
|
+
body: '<html><body><h2>Missing code or state parameter</h2></body></html>' }
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
Legion::Events.emit('microsoft_teams.oauth.callback', code: code, state: state) if defined?(Legion::Events)
|
|
120
|
+
|
|
121
|
+
{
|
|
122
|
+
result: { authenticated: true, code: code, state: state },
|
|
123
|
+
response: { status: 200, content_type: 'text/html',
|
|
124
|
+
body: callback_success_html }
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def callback_success_html
|
|
131
|
+
<<~HTML
|
|
132
|
+
<html><body style="font-family:sans-serif;text-align:center;padding:40px;">
|
|
133
|
+
<h2>Authentication complete</h2>
|
|
134
|
+
<p>You can close this window.</p>
|
|
135
|
+
</body></html>
|
|
136
|
+
HTML
|
|
137
|
+
end
|
|
138
|
+
|
|
110
139
|
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
111
140
|
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
112
141
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-microsoft_teams
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -91,6 +91,7 @@ files:
|
|
|
91
91
|
- lib/legion/extensions/microsoft_teams/helpers/session_manager.rb
|
|
92
92
|
- lib/legion/extensions/microsoft_teams/helpers/subscription_registry.rb
|
|
93
93
|
- lib/legion/extensions/microsoft_teams/helpers/token_cache.rb
|
|
94
|
+
- lib/legion/extensions/microsoft_teams/hooks/auth.rb
|
|
94
95
|
- lib/legion/extensions/microsoft_teams/local_cache/extractor.rb
|
|
95
96
|
- lib/legion/extensions/microsoft_teams/local_cache/record_parser.rb
|
|
96
97
|
- lib/legion/extensions/microsoft_teams/local_cache/sstable_reader.rb
|