lex-microsoft_teams 0.5.5 → 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 +11 -0
- data/CLAUDE.md +7 -5
- data/lib/legion/extensions/microsoft_teams/helpers/browser_auth.rb +67 -12
- data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
- metadata +1 -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,16 @@
|
|
|
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
|
+
|
|
3
14
|
## [0.5.5] - 2026-03-19
|
|
4
15
|
|
|
5
16
|
### 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
|
|
@@ -56,6 +56,8 @@ Legion::Extensions::MicrosoftTeams
|
|
|
56
56
|
│ ├── SubscriptionRegistry # Conversation observation subscriptions (in-memory + lex-memory)
|
|
57
57
|
│ ├── BrowserAuth # Delegated OAuth orchestrator (PKCE, headless detection, browser launch)
|
|
58
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
|
|
59
61
|
└── Client # Standalone client (includes all runners)
|
|
60
62
|
```
|
|
61
63
|
|
|
@@ -66,9 +68,9 @@ Opt-in browser-based OAuth for delegated Microsoft Graph permissions. Two flows:
|
|
|
66
68
|
- **Authorization Code + PKCE** (primary): Opens browser for Entra ID login, captures callback on ephemeral local port, exchanges code with PKCE verification
|
|
67
69
|
- **Device Code** (fallback): Auto-selected in headless/SSH environments (no `DISPLAY`/`WAYLAND_DISPLAY`)
|
|
68
70
|
|
|
69
|
-
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).
|
|
70
72
|
|
|
71
|
-
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).
|
|
72
74
|
|
|
73
75
|
## Token Lifecycle (v0.5.4)
|
|
74
76
|
|
|
@@ -213,7 +215,7 @@ Optional framework dependencies (guarded with `defined?`, not in gemspec):
|
|
|
213
215
|
|
|
214
216
|
```bash
|
|
215
217
|
bundle install
|
|
216
|
-
bundle exec rspec #
|
|
218
|
+
bundle exec rspec # 219 specs (as of v0.5.5)
|
|
217
219
|
bundle exec rubocop # Clean
|
|
218
220
|
```
|
|
219
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
|