lex-microsoft_teams 0.5.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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +16 -0
  3. data/.gitignore +15 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +56 -0
  6. data/CHANGELOG.md +59 -0
  7. data/CLAUDE.md +206 -0
  8. data/Dockerfile +6 -0
  9. data/Gemfile +12 -0
  10. data/Gemfile.lock +103 -0
  11. data/LICENSE +21 -0
  12. data/README.md +183 -0
  13. data/docs/plans/2026-03-15-meetings-transcripts-design.md +506 -0
  14. data/docs/plans/2026-03-16-delegated-oauth-browser-flow-design.md +198 -0
  15. data/docs/plans/2026-03-16-delegated-oauth-browser-flow-plan.md +1176 -0
  16. data/lex-microsoft_teams.gemspec +32 -0
  17. data/lib/legion/extensions/microsoft_teams/actors/cache_bulk_ingest.rb +41 -0
  18. data/lib/legion/extensions/microsoft_teams/actors/cache_sync.rb +54 -0
  19. data/lib/legion/extensions/microsoft_teams/actors/direct_chat_poller.rb +105 -0
  20. data/lib/legion/extensions/microsoft_teams/actors/message_processor.rb +23 -0
  21. data/lib/legion/extensions/microsoft_teams/actors/observed_chat_poller.rb +111 -0
  22. data/lib/legion/extensions/microsoft_teams/client.rb +68 -0
  23. data/lib/legion/extensions/microsoft_teams/helpers/browser_auth.rb +139 -0
  24. data/lib/legion/extensions/microsoft_teams/helpers/callback_server.rb +82 -0
  25. data/lib/legion/extensions/microsoft_teams/helpers/client.rb +38 -0
  26. data/lib/legion/extensions/microsoft_teams/helpers/high_water_mark.rb +59 -0
  27. data/lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb +64 -0
  28. data/lib/legion/extensions/microsoft_teams/helpers/session_manager.rb +150 -0
  29. data/lib/legion/extensions/microsoft_teams/helpers/subscription_registry.rb +140 -0
  30. data/lib/legion/extensions/microsoft_teams/helpers/token_cache.rb +209 -0
  31. data/lib/legion/extensions/microsoft_teams/local_cache/extractor.rb +258 -0
  32. data/lib/legion/extensions/microsoft_teams/local_cache/record_parser.rb +199 -0
  33. data/lib/legion/extensions/microsoft_teams/local_cache/sstable_reader.rb +121 -0
  34. data/lib/legion/extensions/microsoft_teams/runners/adaptive_cards.rb +59 -0
  35. data/lib/legion/extensions/microsoft_teams/runners/auth.rb +116 -0
  36. data/lib/legion/extensions/microsoft_teams/runners/bot.rb +409 -0
  37. data/lib/legion/extensions/microsoft_teams/runners/cache_ingest.rb +122 -0
  38. data/lib/legion/extensions/microsoft_teams/runners/channel_messages.rb +52 -0
  39. data/lib/legion/extensions/microsoft_teams/runners/channels.rb +53 -0
  40. data/lib/legion/extensions/microsoft_teams/runners/chats.rb +51 -0
  41. data/lib/legion/extensions/microsoft_teams/runners/local_cache.rb +62 -0
  42. data/lib/legion/extensions/microsoft_teams/runners/meetings.rb +68 -0
  43. data/lib/legion/extensions/microsoft_teams/runners/messages.rb +48 -0
  44. data/lib/legion/extensions/microsoft_teams/runners/presence.rb +31 -0
  45. data/lib/legion/extensions/microsoft_teams/runners/subscriptions.rb +76 -0
  46. data/lib/legion/extensions/microsoft_teams/runners/teams.rb +33 -0
  47. data/lib/legion/extensions/microsoft_teams/runners/transcripts.rb +45 -0
  48. data/lib/legion/extensions/microsoft_teams/transport/exchanges/messages.rb +15 -0
  49. data/lib/legion/extensions/microsoft_teams/transport/messages/teams_message.rb +16 -0
  50. data/lib/legion/extensions/microsoft_teams/transport/queues/messages_process.rb +16 -0
  51. data/lib/legion/extensions/microsoft_teams/version.rb +9 -0
  52. data/lib/legion/extensions/microsoft_teams.rb +44 -0
  53. metadata +139 -0
@@ -0,0 +1,198 @@
1
+ # Delegated OAuth Browser Flow Design
2
+
3
+ ## Goal
4
+
5
+ Add browser-based delegated OAuth authentication to lex-microsoft_teams so users can authenticate with their own Microsoft account for Graph API access. Authorization Code + PKCE as the primary flow, Device Code as the automatic fallback for headless environments.
6
+
7
+ ## Architecture
8
+
9
+ Opt-in delegated auth: user explicitly triggers via `legion auth teams` CLI command, a bot command (`auth`/`login`), or by enabling `microsoft_teams.auth.delegated.enabled` in settings. Once authenticated, tokens refresh silently. Browser re-opens only when the refresh_token is expired/revoked.
10
+
11
+ Two callback server paths: ephemeral TCPServer for CLI (works without daemon), Sinatra route on the existing API (port 4567) for daemon-initiated re-auth.
12
+
13
+ Tokens stored in HashiCorp Vault via legion-crypt. In-memory cache in TokenCache for fast access.
14
+
15
+ ## Components
16
+
17
+ ### New Files
18
+
19
+ | Component | File | Purpose |
20
+ |-----------|------|---------|
21
+ | `Helpers::BrowserAuth` | `helpers/browser_auth.rb` | Orchestrator: PKCE generation, headless detection, browser opening, flow selection (Auth Code vs Device Code) |
22
+ | `Helpers::CallbackServer` | `helpers/callback_server.rb` | Ephemeral TCPServer on random port, localhost only, receives `?code=&state=`, shuts down after |
23
+
24
+ ### Modified Files
25
+
26
+ | Component | File | Changes |
27
+ |-----------|------|---------|
28
+ | `Runners::Auth` | `runners/auth.rb` | Add `authorize_url`, `exchange_code`, `refresh_delegated_token` methods |
29
+ | `Helpers::TokenCache` | `helpers/token_cache.rb` | Add delegated token slot, refresh_token support, Vault read/write |
30
+
31
+ ### External Files (LegionIO main repo)
32
+
33
+ | Component | File | Purpose |
34
+ |-----------|------|---------|
35
+ | `API::Routes::OAuthCallback` | `api/routes/oauth_callback.rb` | `GET /api/oauth/microsoft_teams/callback` receives code, signals waiting thread |
36
+ | `CLI::Auth` | `cli/auth.rb` | `legion auth teams` command triggers BrowserAuth with ephemeral server |
37
+
38
+ ## Auth Flow
39
+
40
+ ```
41
+ User triggers auth:
42
+ CLI: `legion auth teams`
43
+ Settings: microsoft_teams.auth.delegated.enabled = true
44
+ Bot command: `auth` or `login`
45
+
46
+ ┌──────────────────────────┐
47
+ │ Can we open a browser? │
48
+ └────────────┬─────────────┘
49
+ yes │ no (headless/SSH/TTY check)
50
+ ┌───────────┴───────────┐
51
+ ▼ ▼
52
+ Auth Code + PKCE Device Code
53
+ │ │
54
+ 1. Generate PKCE pair 1. Request device_code
55
+ 2. Start callback server 2. Display URL + code
56
+ 3. Open browser 3. Auto-open browser to
57
+ 4. User signs in devicelogin (if possible)
58
+ 5. Callback receives 4. Poll for token
59
+ ?code=...&state=...
60
+ 6. Exchange code for
61
+ tokens
62
+ │ │
63
+ └───────────┬───────────┘
64
+
65
+ Store tokens in Vault
66
+ (access_token, refresh_token,
67
+ expires_at, scopes)
68
+
69
+
70
+ TokenCache holds in-memory copy
71
+ Silent refresh before expiry
72
+ Re-pop browser only if
73
+ refresh_token is revoked/expired
74
+ ```
75
+
76
+ ### Headless Detection
77
+
78
+ - macOS: always assume GUI available
79
+ - Linux: check `ENV['DISPLAY']` or `ENV['WAYLAND_DISPLAY']`
80
+ - Windows: always assume GUI available
81
+ - Fallback: if `system("open", url)` (or platform equivalent) fails, switch to device code
82
+
83
+ ### Browser Opening
84
+
85
+ Platform detection via `RbConfig::CONFIG['host_os']`:
86
+ - macOS: `system("open", url)`
87
+ - Linux: `system("xdg-open", url)`
88
+ - Windows: `system("start", url)`
89
+
90
+ (Pattern from `references/ruby_llm-mcp/lib/ruby_llm/mcp/auth/browser/opener.rb`)
91
+
92
+ ### PKCE
93
+
94
+ - `code_verifier`: 43-128 character random string (`SecureRandom.urlsafe_base64(32)`)
95
+ - `code_challenge`: `Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)`
96
+ - `code_challenge_method`: `S256`
97
+
98
+ ### State Parameter
99
+
100
+ - `SecureRandom.hex(32)` — verified on callback to prevent CSRF
101
+
102
+ ### Callback Server (Ephemeral)
103
+
104
+ - `TCPServer.new('127.0.0.1', 0)` — OS assigns random available port
105
+ - Reads HTTP request, parses query string for `code` and `state`
106
+ - Returns a simple HTML "You can close this window" response
107
+ - Shuts down immediately after receiving the callback
108
+ - Timeout: 120 seconds (configurable)
109
+
110
+ ### Callback Server (Sinatra/Daemon)
111
+
112
+ - `GET /api/oauth/microsoft_teams/callback`
113
+ - Receives `code` and `state`, signals a waiting `ConditionVariable`
114
+ - Returns HTML redirect or "Authentication complete" page
115
+
116
+ ## Token Lifecycle
117
+
118
+ ### Initial Auth (User-Triggered)
119
+
120
+ 1. User triggers auth
121
+ 2. BrowserAuth generates PKCE pair
122
+ 3. Checks browser availability → Auth Code or Device Code
123
+ 4. Obtains access_token + refresh_token
124
+ 5. Writes to Vault at `vault_path`
125
+ 6. TokenCache loads into memory
126
+
127
+ ### Silent Refresh (Automatic)
128
+
129
+ 1. TokenCache checks `expires_at - refresh_buffer` on every `cached_delegated_token` call
130
+ 2. If within buffer: `POST oauth2/v2.0/token` with `grant_type=refresh_token`
131
+ 3. Microsoft returns new access_token + rotated refresh_token
132
+ 4. Write both back to Vault, update in-memory cache
133
+ 5. No user interaction
134
+
135
+ ### Re-Auth (Refresh Token Expired/Revoked)
136
+
137
+ 1. Refresh returns `invalid_grant`
138
+ 2. TokenCache clears delegated slot
139
+ 3. Daemon running: trigger BrowserAuth via Sinatra callback route
140
+ 4. CLI: prompt user to run `legion auth teams`
141
+ 5. `Legion::Events.emit('microsoft_teams.auth.expired')` for extensions to react
142
+
143
+ ### Daemon Startup
144
+
145
+ 1. If `delegated.enabled: true`, read token from Vault
146
+ 2. Valid (not expired) → load into TokenCache
147
+ 3. Expired but refresh_token present → attempt silent refresh
148
+ 4. No token or refresh fails → emit event, log message
149
+ 5. Never auto-pop browser on startup without prior explicit auth
150
+
151
+ ## Settings
152
+
153
+ ```yaml
154
+ microsoft_teams:
155
+ auth:
156
+ tenant_id: "..."
157
+ client_id: "..."
158
+ client_secret: "..." # client_credentials flow (existing)
159
+ delegated:
160
+ enabled: false # opt-in gate
161
+ scopes: "OnlineMeetings.Read OnlineMeetingTranscript.Read.All offline_access"
162
+ refresh_buffer: 300 # seconds before expiry to refresh
163
+ vault_path: "secret/legionio/microsoft_teams/delegated_token"
164
+ callback_timeout: 120 # seconds to wait for browser callback
165
+ ```
166
+
167
+ ## Vault Storage
168
+
169
+ Path: `secret/legionio/microsoft_teams/delegated_token`
170
+
171
+ ```json
172
+ {
173
+ "access_token": "eyJ...",
174
+ "refresh_token": "0.AR...",
175
+ "expires_at": "2026-03-16T22:30:00Z",
176
+ "scopes": "OnlineMeetings.Read OnlineMeetingTranscript.Read.All offline_access"
177
+ }
178
+ ```
179
+
180
+ ## Entra App Registration Requirements
181
+
182
+ The existing LegionIO Entra app needs:
183
+ - `fallback_public_client_enabled = true` (already set)
184
+ - `public_client.redirect_uris` must include `http://localhost` (wildcard port)
185
+ - Or register specific redirect URIs: `http://localhost:4567/api/oauth/microsoft_teams/callback` for daemon, and `http://localhost` for ephemeral
186
+ - Delegated permissions for desired scopes (requires admin consent in managed tenants)
187
+
188
+ ## Scope
189
+
190
+ Teams-specific only. Build in lex-microsoft_teams, extract to a shared gem later if other extensions need it.
191
+
192
+ ## Dependencies
193
+
194
+ No new gem dependencies. Uses:
195
+ - `SecureRandom`, `Digest::SHA256`, `Base64` (stdlib)
196
+ - `socket` (stdlib, for TCPServer)
197
+ - `legion-crypt` (existing, for Vault access)
198
+ - `faraday` (existing)