mcp-twake-mail 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 (114) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +332 -0
  3. package/build/auth/index.d.ts +3 -0
  4. package/build/auth/index.js +4 -0
  5. package/build/auth/index.js.map +1 -0
  6. package/build/auth/oidc-flow.d.ts +47 -0
  7. package/build/auth/oidc-flow.js +146 -0
  8. package/build/auth/oidc-flow.js.map +1 -0
  9. package/build/auth/token-refresh.d.ts +56 -0
  10. package/build/auth/token-refresh.js +132 -0
  11. package/build/auth/token-refresh.js.map +1 -0
  12. package/build/auth/token-store.d.ts +33 -0
  13. package/build/auth/token-store.js +63 -0
  14. package/build/auth/token-store.js.map +1 -0
  15. package/build/cli/commands/auth.d.ts +5 -0
  16. package/build/cli/commands/auth.js +49 -0
  17. package/build/cli/commands/auth.js.map +1 -0
  18. package/build/cli/commands/check.d.ts +4 -0
  19. package/build/cli/commands/check.js +125 -0
  20. package/build/cli/commands/check.js.map +1 -0
  21. package/build/cli/commands/setup.d.ts +4 -0
  22. package/build/cli/commands/setup.js +172 -0
  23. package/build/cli/commands/setup.js.map +1 -0
  24. package/build/cli/index.d.ts +15 -0
  25. package/build/cli/index.js +56 -0
  26. package/build/cli/index.js.map +1 -0
  27. package/build/cli/prompts/setup-wizard.d.ts +38 -0
  28. package/build/cli/prompts/setup-wizard.js +121 -0
  29. package/build/cli/prompts/setup-wizard.js.map +1 -0
  30. package/build/config/__tests__/logger.test.d.ts +1 -0
  31. package/build/config/__tests__/logger.test.js +28 -0
  32. package/build/config/__tests__/logger.test.js.map +1 -0
  33. package/build/config/__tests__/schema.test.d.ts +1 -0
  34. package/build/config/__tests__/schema.test.js +101 -0
  35. package/build/config/__tests__/schema.test.js.map +1 -0
  36. package/build/config/logger.d.ts +3 -0
  37. package/build/config/logger.js +9 -0
  38. package/build/config/logger.js.map +1 -0
  39. package/build/config/schema.d.ts +28 -0
  40. package/build/config/schema.js +81 -0
  41. package/build/config/schema.js.map +1 -0
  42. package/build/errors.d.ts +34 -0
  43. package/build/errors.js +154 -0
  44. package/build/errors.js.map +1 -0
  45. package/build/errors.test.d.ts +1 -0
  46. package/build/errors.test.js +234 -0
  47. package/build/errors.test.js.map +1 -0
  48. package/build/index.d.ts +2 -0
  49. package/build/index.js +12 -0
  50. package/build/index.js.map +1 -0
  51. package/build/jmap/client.d.ts +96 -0
  52. package/build/jmap/client.js +267 -0
  53. package/build/jmap/client.js.map +1 -0
  54. package/build/mcp/server.d.ts +24 -0
  55. package/build/mcp/server.js +68 -0
  56. package/build/mcp/server.js.map +1 -0
  57. package/build/mcp/tools/attachment.d.ts +30 -0
  58. package/build/mcp/tools/attachment.js +246 -0
  59. package/build/mcp/tools/attachment.js.map +1 -0
  60. package/build/mcp/tools/attachment.test.d.ts +1 -0
  61. package/build/mcp/tools/attachment.test.js +457 -0
  62. package/build/mcp/tools/attachment.test.js.map +1 -0
  63. package/build/mcp/tools/email-operations.d.ts +10 -0
  64. package/build/mcp/tools/email-operations.js +828 -0
  65. package/build/mcp/tools/email-operations.js.map +1 -0
  66. package/build/mcp/tools/email-operations.test.d.ts +1 -0
  67. package/build/mcp/tools/email-operations.test.js +453 -0
  68. package/build/mcp/tools/email-operations.test.js.map +1 -0
  69. package/build/mcp/tools/email-sending.d.ts +10 -0
  70. package/build/mcp/tools/email-sending.js +682 -0
  71. package/build/mcp/tools/email-sending.js.map +1 -0
  72. package/build/mcp/tools/email.d.ts +10 -0
  73. package/build/mcp/tools/email.js +365 -0
  74. package/build/mcp/tools/email.js.map +1 -0
  75. package/build/mcp/tools/email.test.d.ts +1 -0
  76. package/build/mcp/tools/email.test.js +332 -0
  77. package/build/mcp/tools/email.test.js.map +1 -0
  78. package/build/mcp/tools/index.d.ts +14 -0
  79. package/build/mcp/tools/index.js +29 -0
  80. package/build/mcp/tools/index.js.map +1 -0
  81. package/build/mcp/tools/mailbox.d.ts +10 -0
  82. package/build/mcp/tools/mailbox.js +195 -0
  83. package/build/mcp/tools/mailbox.js.map +1 -0
  84. package/build/mcp/tools/mailbox.test.d.ts +1 -0
  85. package/build/mcp/tools/mailbox.test.js +231 -0
  86. package/build/mcp/tools/mailbox.test.js.map +1 -0
  87. package/build/mcp/tools/thread.d.ts +10 -0
  88. package/build/mcp/tools/thread.js +282 -0
  89. package/build/mcp/tools/thread.js.map +1 -0
  90. package/build/mcp/tools/thread.test.d.ts +1 -0
  91. package/build/mcp/tools/thread.test.js +384 -0
  92. package/build/mcp/tools/thread.test.js.map +1 -0
  93. package/build/transformers/__tests__/email.test.d.ts +1 -0
  94. package/build/transformers/__tests__/email.test.js +438 -0
  95. package/build/transformers/__tests__/email.test.js.map +1 -0
  96. package/build/transformers/__tests__/mailbox.test.d.ts +1 -0
  97. package/build/transformers/__tests__/mailbox.test.js +222 -0
  98. package/build/transformers/__tests__/mailbox.test.js.map +1 -0
  99. package/build/transformers/email.d.ts +76 -0
  100. package/build/transformers/email.js +138 -0
  101. package/build/transformers/email.js.map +1 -0
  102. package/build/transformers/index.d.ts +5 -0
  103. package/build/transformers/index.js +6 -0
  104. package/build/transformers/index.js.map +1 -0
  105. package/build/transformers/mailbox.d.ts +43 -0
  106. package/build/transformers/mailbox.js +70 -0
  107. package/build/transformers/mailbox.js.map +1 -0
  108. package/build/types/dto.d.ts +91 -0
  109. package/build/types/dto.js +9 -0
  110. package/build/types/dto.js.map +1 -0
  111. package/build/types/jmap.d.ts +110 -0
  112. package/build/types/jmap.js +5 -0
  113. package/build/types/jmap.js.map +1 -0
  114. package/package.json +71 -0
package/README.md ADDED
@@ -0,0 +1,332 @@
1
+ # mcp-twake-mail
2
+
3
+ [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE)
4
+ [![Node.js >= 20](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](https://nodejs.org/)
5
+
6
+ **MCP server for [Twake.ai](https://www.twake.ai/) — integrate your sovereign JMAP email server with any MCP-compatible AI assistant**
7
+
8
+ ![Twake Mail](assets/twake-mail-screenshot.png)
9
+
10
+ ## Overview
11
+
12
+ mcp-twake-mail is a Model Context Protocol (MCP) server that connects any MCP-compatible AI assistant (Claude Desktop, Claude Code, etc.) to your JMAP email server. Compatible with JMAP-compliant servers including Apache James, Cyrus IMAP, and other RFC 8620/8621 implementations.
13
+
14
+ **Key benefits:**
15
+ - Your data stays on your own servers — sovereign infrastructure
16
+ - Works with any MCP-compatible AI assistant
17
+ - Full control over email data — read, write, send, and organize
18
+ - Multiple authentication methods: Basic, Bearer token, or OIDC
19
+ - Secure HTTPS-only connections
20
+
21
+ ## Features
22
+
23
+ **Email Read Tools:**
24
+ - `list_emails` - List emails with filtering, search, and pagination
25
+ - `get_email` - Get full email content including body and headers
26
+ - `search_emails` - Search emails by keywords
27
+ - `get_thread` - Get all emails in a conversation thread
28
+
29
+ **Email Compose Tools:**
30
+ - `send_email` - Compose and send a new email (plain text and/or HTML)
31
+ - `reply_email` - Reply to an email with proper threading (In-Reply-To, References)
32
+ - `create_draft` - Create a draft email for later editing or sending
33
+
34
+ **Email Management Tools:**
35
+ - `mark_as_read` - Mark an email as read
36
+ - `mark_as_unread` - Mark an email as unread
37
+ - `delete_email` - Move to trash or permanently delete
38
+ - `move_email` - Move an email to a different mailbox
39
+ - `add_label` - Add a label/mailbox to an email
40
+ - `remove_label` - Remove a label/mailbox from an email
41
+
42
+ **Mailbox Tools:**
43
+ - `list_mailboxes` - List all mailboxes (Inbox, Sent, Drafts, etc.)
44
+
45
+ **Attachment Tools:**
46
+ - `get_attachments` - List attachment metadata for an email
47
+ - `download_attachment` - Download attachment content (auto-saves large files to disk)
48
+
49
+ **Advanced Features:**
50
+ - OIDC authentication with PKCE (S256) for enterprise SSO
51
+ - Automatic token refresh for OIDC sessions
52
+ - Thread-based email grouping
53
+ - Inline vs regular attachment detection
54
+ - Large attachment handling (auto-saves to ~/Downloads for files > 750KB)
55
+ - MCP tool annotations (readOnlyHint, destructiveHint, idempotentHint)
56
+
57
+ ## Prerequisites
58
+
59
+ - **Node.js** >= 20.0.0
60
+ - **JMAP Server** - A JMAP-compliant email server such as:
61
+ - Apache James
62
+ - Cyrus IMAP
63
+ - Stalwart Mail Server
64
+ - Fastmail
65
+ - **MCP-compatible AI assistant** - Claude Desktop, Claude Code, or any MCP client
66
+
67
+ ## Installation
68
+
69
+ **From source:**
70
+ ```bash
71
+ git clone https://github.com/linagora/mcp-twake-mail.git
72
+ cd mcp-twake-mail
73
+ npm install
74
+ npm run build
75
+ ```
76
+
77
+ ## Quick Setup (Recommended)
78
+
79
+ The easiest way to configure mcp-twake-mail is to use the interactive setup wizard:
80
+
81
+ ```bash
82
+ npx mcp-twake-mail setup
83
+ ```
84
+
85
+ The wizard will:
86
+ 1. Ask for your JMAP server session URL
87
+ 2. Ask for your authentication method (Basic, Bearer, or OIDC)
88
+ 3. Collect the required credentials
89
+ 4. Test the connection to your JMAP server
90
+ 5. Generate and optionally write the configuration to your Claude Desktop config file
91
+
92
+ Example session:
93
+ ```
94
+ === MCP Twake Mail Setup Wizard ===
95
+
96
+ JMAP Session URL: https://jmap.example.com/jmap/session
97
+
98
+ Authentication method:
99
+ 1. Basic (username/password)
100
+ 2. Bearer token (JWT)
101
+ 3. OIDC (OpenID Connect)
102
+ Choose [1-3]: 1
103
+
104
+ Username: user@example.com
105
+ Password: ********
106
+
107
+ Testing connection...
108
+ Connected! Account ID: abc123
109
+
110
+ Server name for Claude config [twake-mail]: twake-mail
111
+
112
+ --- Generated Claude Desktop Config ---
113
+ {
114
+ "mcpServers": {
115
+ "twake-mail": {
116
+ "command": "npx",
117
+ "args": ["-y", "mcp-twake-mail"],
118
+ "env": {
119
+ "JMAP_SESSION_URL": "https://jmap.example.com/jmap/session",
120
+ "JMAP_AUTH_METHOD": "basic",
121
+ "JMAP_USERNAME": "user@example.com",
122
+ "JMAP_PASSWORD": "********"
123
+ }
124
+ }
125
+ }
126
+ }
127
+ ---------------------------------------
128
+
129
+ Write to Claude Desktop config? [Y/n]: y
130
+
131
+ Config written successfully!
132
+ Restart Claude Desktop to load the new configuration.
133
+ ```
134
+
135
+ ### CLI Commands
136
+
137
+ | Command | Description |
138
+ |---------|-------------|
139
+ | `mcp-twake-mail` | Start MCP server (default) |
140
+ | `mcp-twake-mail setup` | Interactive configuration wizard |
141
+ | `mcp-twake-mail auth` | Re-run OIDC authentication flow |
142
+ | `mcp-twake-mail check` | Verify configuration and test connection |
143
+
144
+ ## Configuration
145
+
146
+ ### Environment Variables
147
+
148
+ #### Basic Auth
149
+
150
+ | Variable | Required | Description | Example |
151
+ |----------|----------|-------------|---------|
152
+ | `JMAP_SESSION_URL` | Yes | JMAP session endpoint URL | `https://jmap.example.com/jmap/session` |
153
+ | `JMAP_AUTH_METHOD` | No | Set to `basic` (default) | `basic` |
154
+ | `JMAP_USERNAME` | Yes | Username for authentication | `user@example.com` |
155
+ | `JMAP_PASSWORD` | Yes | Password for authentication | `your-password` |
156
+
157
+ #### Bearer Token
158
+
159
+ | Variable | Required | Description | Example |
160
+ |----------|----------|-------------|---------|
161
+ | `JMAP_SESSION_URL` | Yes | JMAP session endpoint URL | `https://jmap.example.com/jmap/session` |
162
+ | `JMAP_AUTH_METHOD` | Yes | Must be set to `bearer` | `bearer` |
163
+ | `JMAP_TOKEN` | Yes | JWT Bearer token | `eyJhbGciOiJSUzI1NiIs...` |
164
+
165
+ #### OIDC Authentication
166
+
167
+ For enterprise SSO with OpenID Connect (PKCE S256):
168
+
169
+ | Variable | Required | Description | Example |
170
+ |----------|----------|-------------|---------|
171
+ | `JMAP_SESSION_URL` | Yes | JMAP session endpoint URL | `https://jmap.example.com/jmap/session` |
172
+ | `JMAP_AUTH_METHOD` | Yes | Must be set to `oidc` | `oidc` |
173
+ | `JMAP_OIDC_ISSUER` | Yes | OIDC provider issuer URL | `https://sso.example.com` |
174
+ | `JMAP_OIDC_CLIENT_ID` | Yes | OIDC client ID | `my-client-id` |
175
+ | `JMAP_OIDC_SCOPE` | No | OIDC scopes | `openid profile email offline_access` |
176
+ | `JMAP_OIDC_REDIRECT_URI` | No | Callback URI for OIDC flow | `http://localhost:5678/callback` |
177
+
178
+ #### Optional
179
+
180
+ | Variable | Description | Default |
181
+ |----------|-------------|---------|
182
+ | `LOG_LEVEL` | Log verbosity: `fatal`, `error`, `warn`, `info`, `debug`, `trace` | `info` |
183
+ | `JMAP_REQUEST_TIMEOUT` | Request timeout in milliseconds | `30000` |
184
+
185
+ ### Claude Desktop Configuration
186
+
187
+ Add the following to your Claude Desktop configuration file:
188
+
189
+ **Configuration file location:**
190
+ - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
191
+ - **Linux:** `~/.config/Claude/claude_desktop_config.json`
192
+ - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
193
+
194
+ **Configuration (Basic Auth):**
195
+
196
+ ```json
197
+ {
198
+ "mcpServers": {
199
+ "twake-mail": {
200
+ "command": "node",
201
+ "args": ["/path/to/mcp-twake-mail/build/index.js"],
202
+ "env": {
203
+ "JMAP_SESSION_URL": "https://jmap.example.com/jmap/session",
204
+ "JMAP_AUTH_METHOD": "basic",
205
+ "JMAP_USERNAME": "user@example.com",
206
+ "JMAP_PASSWORD": "your-password"
207
+ }
208
+ }
209
+ }
210
+ }
211
+ ```
212
+
213
+ **Configuration (OIDC):**
214
+
215
+ ```json
216
+ {
217
+ "mcpServers": {
218
+ "twake-mail": {
219
+ "command": "node",
220
+ "args": ["/path/to/mcp-twake-mail/build/index.js"],
221
+ "env": {
222
+ "JMAP_SESSION_URL": "https://jmap.example.com/jmap/session",
223
+ "JMAP_AUTH_METHOD": "oidc",
224
+ "JMAP_OIDC_ISSUER": "https://sso.example.com",
225
+ "JMAP_OIDC_CLIENT_ID": "my-client-id",
226
+ "JMAP_OIDC_SCOPE": "openid profile email offline_access",
227
+ "JMAP_OIDC_REDIRECT_URI": "http://localhost:5678/callback"
228
+ }
229
+ }
230
+ }
231
+ }
232
+ ```
233
+
234
+ After updating the configuration, restart Claude Desktop for changes to take effect.
235
+
236
+ ## Usage Examples
237
+
238
+ Once configured, you can ask Claude natural language questions about your email:
239
+
240
+ **Email queries:**
241
+ - "What are my unread emails?"
242
+ - "Show me emails from Pierre"
243
+ - "What's the latest email in my inbox?"
244
+ - "Find emails about the budget meeting"
245
+ - "Show the conversation thread for this email"
246
+
247
+ **Email composition:**
248
+ - "Send an email to pierre@example.com about the meeting tomorrow"
249
+ - "Reply to this email thanking them for the information"
250
+ - "Create a draft email to the team about the project update"
251
+
252
+ **Email management:**
253
+ - "Mark this email as read"
254
+ - "Move this email to the Archive folder"
255
+ - "Delete all spam emails"
256
+ - "Add the 'Important' label to this email"
257
+
258
+ **Attachments:**
259
+ - "What attachments are in this email?"
260
+ - "Download the PDF attachment"
261
+
262
+ ## Available Tools
263
+
264
+ | Tool Name | Description |
265
+ |-----------|-------------|
266
+ | `list_emails` | List emails with optional filters (mailbox, limit, search) |
267
+ | `get_email` | Get full email content by ID |
268
+ | `search_emails` | Search emails by keywords |
269
+ | `get_thread` | Get all emails in a thread |
270
+ | `send_email` | Send a new email |
271
+ | `reply_email` | Reply to an email with threading |
272
+ | `create_draft` | Create a draft email |
273
+ | `mark_as_read` | Mark email as read |
274
+ | `mark_as_unread` | Mark email as unread |
275
+ | `delete_email` | Delete or trash an email |
276
+ | `move_email` | Move email to another mailbox |
277
+ | `add_label` | Add mailbox/label to email |
278
+ | `remove_label` | Remove mailbox/label from email |
279
+ | `list_mailboxes` | List all mailboxes |
280
+ | `get_attachments` | List attachment metadata |
281
+ | `download_attachment` | Download attachment content |
282
+
283
+ ## Development
284
+
285
+ ```bash
286
+ git clone https://github.com/linagora/mcp-twake-mail.git
287
+ cd mcp-twake-mail
288
+ npm install
289
+ npm run build # compile TypeScript
290
+ npm test # run tests
291
+ npm run dev # watch mode (auto-rebuild on file changes)
292
+ ```
293
+
294
+ The server uses the MCP stdio transport and communicates via JSON-RPC on stdin/stdout.
295
+
296
+ ## Architecture
297
+
298
+ mcp-twake-mail is built with a layered architecture:
299
+
300
+ 1. **Configuration Layer** - Zod-based environment variable validation with fail-fast behavior
301
+ 2. **Logging Layer** - Pino logger configured for stderr output (prevents stdout contamination in MCP stdio transport)
302
+ 3. **Authentication Layer** - Multi-method auth support (Basic, Bearer, OIDC with PKCE)
303
+ 4. **Token Management** - Automatic token refresh for OIDC with secure token storage
304
+ 5. **JMAP Client Layer** - Session management, request batching, blob download support
305
+ 6. **Transformation Layer** - Email/Mailbox data transformation for AI-friendly output
306
+ 7. **MCP Tool Layer** - 16 MCP tools exposing email functionality with tool annotations
307
+ 8. **Entry Point** - MCP server initialization with stdio transport
308
+
309
+ **Key design decisions:**
310
+ - ESM modules with `.js` import extensions (required by MCP SDK)
311
+ - JMAP RFC 8620/8621 compliance for broad server compatibility
312
+ - AI-friendly error formatting for troubleshooting
313
+ - Large attachment handling (auto-save to disk for files > 750KB)
314
+ - MCP tool annotations for AI clients (readOnlyHint, destructiveHint, idempotentHint)
315
+
316
+ ## License
317
+
318
+ This project is licensed under the **GNU Affero General Public License v3.0 (AGPL-3.0)**.
319
+
320
+ See the [LICENSE](LICENSE) file for details.
321
+
322
+ **Copyright (c) 2026 LINAGORA** <https://linagora.com>
323
+
324
+ ## Contributing
325
+
326
+ Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) for details on the development workflow, code style, and pull request process.
327
+
328
+ ## Support
329
+
330
+ For issues, questions, or feature requests, please open an issue on the GitHub repository.
331
+
332
+ For commercial support or inquiries, contact LINAGORA at <https://linagora.com>.
@@ -0,0 +1,3 @@
1
+ export * from './token-store.js';
2
+ export * from './token-refresh.js';
3
+ export * from './oidc-flow.js';
@@ -0,0 +1,4 @@
1
+ export * from './token-store.js';
2
+ export * from './token-refresh.js';
3
+ export * from './oidc-flow.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,47 @@
1
+ import { type StoredTokens } from './token-store.js';
2
+ /**
3
+ * Options for performing OIDC authorization code flow with PKCE
4
+ */
5
+ export interface OIDCFlowOptions {
6
+ /** OIDC issuer URL (e.g., https://auth.example.com) */
7
+ issuerUrl: string;
8
+ /** OAuth client ID registered with the OIDC provider */
9
+ clientId: string;
10
+ /** OAuth scopes to request (space-separated) */
11
+ scope: string;
12
+ /** Redirect URI (must be registered with the OIDC provider) */
13
+ redirectUri: string;
14
+ /** Local port for callback server (overrides port extracted from redirectUri) */
15
+ localPort?: number;
16
+ }
17
+ /**
18
+ * Perform OIDC authorization code flow with PKCE (S256)
19
+ *
20
+ * This function:
21
+ * 1. Discovers OIDC provider configuration
22
+ * 2. Generates PKCE code verifier and challenge (S256)
23
+ * 3. Builds authorization URL with PKCE
24
+ * 4. Opens browser for user authentication
25
+ * 5. Captures authorization code via localhost callback
26
+ * 6. Exchanges code for tokens
27
+ * 7. Saves tokens to secure storage
28
+ *
29
+ * @param options - OIDC flow configuration
30
+ * @returns Stored tokens after successful authentication
31
+ * @throws JMAPError on OIDC flow failures
32
+ */
33
+ export declare function performOIDCFlow(options: OIDCFlowOptions): Promise<StoredTokens>;
34
+ /**
35
+ * Helper to extract OIDC flow options from environment config
36
+ *
37
+ * @param config - Config object with OIDC fields
38
+ * @returns OIDCFlowOptions or null if OIDC is not configured
39
+ */
40
+ export declare function getOIDCOptionsFromConfig(config: {
41
+ JMAP_AUTH_METHOD: string;
42
+ JMAP_OIDC_ISSUER?: string;
43
+ JMAP_OIDC_CLIENT_ID?: string;
44
+ JMAP_OIDC_SCOPE: string;
45
+ JMAP_OIDC_REDIRECT_URI: string;
46
+ JMAP_OIDC_LOCAL_PORT?: number;
47
+ }): OIDCFlowOptions | null;
@@ -0,0 +1,146 @@
1
+ import * as client from 'openid-client';
2
+ import { getAuthCode } from 'oauth-callback';
3
+ import open from 'open';
4
+ import { saveTokens } from './token-store.js';
5
+ import { JMAPError } from '../errors.js';
6
+ /**
7
+ * Perform OIDC authorization code flow with PKCE (S256)
8
+ *
9
+ * This function:
10
+ * 1. Discovers OIDC provider configuration
11
+ * 2. Generates PKCE code verifier and challenge (S256)
12
+ * 3. Builds authorization URL with PKCE
13
+ * 4. Opens browser for user authentication
14
+ * 5. Captures authorization code via localhost callback
15
+ * 6. Exchanges code for tokens
16
+ * 7. Saves tokens to secure storage
17
+ *
18
+ * @param options - OIDC flow configuration
19
+ * @returns Stored tokens after successful authentication
20
+ * @throws JMAPError on OIDC flow failures
21
+ */
22
+ export async function performOIDCFlow(options) {
23
+ const { issuerUrl, clientId, scope, redirectUri, localPort } = options;
24
+ // Parse redirect URI to extract port and path
25
+ const redirectUrl = new URL(redirectUri);
26
+ const isLocalhost = ['localhost', '127.0.0.1', '::1'].includes(redirectUrl.hostname);
27
+ // Determine callback port: explicit localPort > port from URI > defaults
28
+ let callbackPort;
29
+ if (localPort) {
30
+ // Explicit local port provided (useful for ngrok scenarios)
31
+ callbackPort = localPort;
32
+ }
33
+ else if (redirectUrl.port) {
34
+ // Explicit port in URI - use it
35
+ callbackPort = parseInt(redirectUrl.port, 10);
36
+ }
37
+ else if (isLocalhost) {
38
+ // Localhost without explicit port - use protocol default
39
+ callbackPort = redirectUrl.protocol === 'https:' ? 443 : 80;
40
+ }
41
+ else {
42
+ // Remote URL (ngrok, etc.) without explicit port - use default 3000
43
+ callbackPort = 3000;
44
+ }
45
+ // Extract callback path from redirect URI (default to /callback if no path)
46
+ const callbackPath = redirectUrl.pathname || '/callback';
47
+ // Step 1: OIDC Discovery
48
+ let config;
49
+ try {
50
+ config = await client.discovery(new URL(issuerUrl), clientId, undefined, // No client secret (public client with PKCE)
51
+ client.None() // Public client authentication
52
+ );
53
+ }
54
+ catch (error) {
55
+ throw JMAPError.oidcFlowError('discovery', error instanceof Error ? error.message : String(error));
56
+ }
57
+ // Step 2: Generate PKCE values (S256 - NEVER use plain)
58
+ const codeVerifier = client.randomPKCECodeVerifier();
59
+ const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
60
+ const state = client.randomState();
61
+ // Step 4: Build authorization URL with PKCE S256
62
+ const authParams = {
63
+ redirect_uri: redirectUri,
64
+ scope,
65
+ code_challenge: codeChallenge,
66
+ code_challenge_method: 'S256', // CRITICAL: Always S256, never plain (AUTH-04)
67
+ state,
68
+ };
69
+ const authorizationUrl = client.buildAuthorizationUrl(config, authParams);
70
+ // Step 5: Launch browser and capture callback
71
+ let authCode;
72
+ let returnedState;
73
+ try {
74
+ const result = await getAuthCode({
75
+ port: callbackPort,
76
+ callbackPath,
77
+ authorizationUrl: authorizationUrl.toString(),
78
+ launch: open,
79
+ timeout: 120000, // 2 minutes for user to complete auth
80
+ });
81
+ authCode = result.code;
82
+ // State can be in params (older oauth-callback) or directly on result (newer versions)
83
+ returnedState = result.params?.state ?? result.state;
84
+ }
85
+ catch (error) {
86
+ throw JMAPError.oidcFlowError('callback', error instanceof Error ? error.message : String(error));
87
+ }
88
+ // Step 6: Validate state to prevent CSRF attacks
89
+ if (returnedState !== state) {
90
+ throw JMAPError.oidcFlowError('state validation', 'State parameter mismatch. Possible CSRF attack.');
91
+ }
92
+ // Step 7: Exchange code for tokens
93
+ let tokenResponse;
94
+ try {
95
+ // Build callback URL with the authorization code for authorizationCodeGrant
96
+ const callbackUrl = new URL(redirectUri);
97
+ callbackUrl.searchParams.set('code', authCode);
98
+ callbackUrl.searchParams.set('state', state);
99
+ tokenResponse = await client.authorizationCodeGrant(config, callbackUrl, {
100
+ pkceCodeVerifier: codeVerifier,
101
+ expectedState: state,
102
+ });
103
+ }
104
+ catch (error) {
105
+ if (error instanceof client.AuthorizationResponseError) {
106
+ throw JMAPError.oidcFlowError('authorization', error.error_description || error.error);
107
+ }
108
+ if (error instanceof client.ResponseBodyError) {
109
+ throw JMAPError.oidcFlowError('token exchange', error.message);
110
+ }
111
+ throw JMAPError.oidcFlowError('token exchange', error instanceof Error ? error.message : String(error));
112
+ }
113
+ // Step 8: Build and save StoredTokens
114
+ const tokens = {
115
+ accessToken: tokenResponse.access_token,
116
+ refreshToken: tokenResponse.refresh_token,
117
+ idToken: tokenResponse.id_token,
118
+ expiresAt: tokenResponse.expires_in
119
+ ? Math.floor(Date.now() / 1000) + tokenResponse.expires_in
120
+ : undefined,
121
+ };
122
+ await saveTokens(tokens);
123
+ return tokens;
124
+ }
125
+ /**
126
+ * Helper to extract OIDC flow options from environment config
127
+ *
128
+ * @param config - Config object with OIDC fields
129
+ * @returns OIDCFlowOptions or null if OIDC is not configured
130
+ */
131
+ export function getOIDCOptionsFromConfig(config) {
132
+ if (config.JMAP_AUTH_METHOD !== 'oidc') {
133
+ return null;
134
+ }
135
+ if (!config.JMAP_OIDC_ISSUER || !config.JMAP_OIDC_CLIENT_ID) {
136
+ return null;
137
+ }
138
+ return {
139
+ issuerUrl: config.JMAP_OIDC_ISSUER,
140
+ clientId: config.JMAP_OIDC_CLIENT_ID,
141
+ scope: config.JMAP_OIDC_SCOPE,
142
+ redirectUri: config.JMAP_OIDC_REDIRECT_URI,
143
+ localPort: config.JMAP_OIDC_LOCAL_PORT,
144
+ };
145
+ }
146
+ //# sourceMappingURL=oidc-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc-flow.js","sourceRoot":"","sources":["../../src/auth/oidc-flow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,UAAU,EAAqB,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAkBzC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAwB;IAC5D,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IAEvE,8CAA8C;IAC9C,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;IACzC,MAAM,WAAW,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;IAErF,yEAAyE;IACzE,IAAI,YAAoB,CAAC;IACzB,IAAI,SAAS,EAAE,CAAC;QACd,4DAA4D;QAC5D,YAAY,GAAG,SAAS,CAAC;IAC3B,CAAC;SAAM,IAAI,WAAW,CAAC,IAAI,EAAE,CAAC;QAC5B,gCAAgC;QAChC,YAAY,GAAG,QAAQ,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAChD,CAAC;SAAM,IAAI,WAAW,EAAE,CAAC;QACvB,yDAAyD;QACzD,YAAY,GAAG,WAAW,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9D,CAAC;SAAM,CAAC;QACN,oEAAoE;QACpE,YAAY,GAAG,IAAI,CAAC;IACtB,CAAC;IAED,4EAA4E;IAC5E,MAAM,YAAY,GAAG,WAAW,CAAC,QAAQ,IAAI,WAAW,CAAC;IAEzD,yBAAyB;IACzB,IAAI,MAA4B,CAAC;IACjC,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,MAAM,CAAC,SAAS,CAC7B,IAAI,GAAG,CAAC,SAAS,CAAC,EAClB,QAAQ,EACR,SAAS,EAAE,6CAA6C;QACxD,MAAM,CAAC,IAAI,EAAE,CAAC,+BAA+B;SAC9C,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,SAAS,CAAC,aAAa,CAC3B,WAAW,EACX,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CACvD,CAAC;IACJ,CAAC;IAED,wDAAwD;IACxD,MAAM,YAAY,GAAG,MAAM,CAAC,sBAAsB,EAAE,CAAC;IACrD,MAAM,aAAa,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,YAAY,CAAC,CAAC;IAC5E,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IAEnC,iDAAiD;IACjD,MAAM,UAAU,GAAG;QACjB,YAAY,EAAE,WAAW;QACzB,KAAK;QACL,cAAc,EAAE,aAAa;QAC7B,qBAAqB,EAAE,MAAM,EAAE,+CAA+C;QAC9E,KAAK;KACN,CAAC;IAEF,MAAM,gBAAgB,GAAG,MAAM,CAAC,qBAAqB,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAE1E,8CAA8C;IAC9C,IAAI,QAAgB,CAAC;IACrB,IAAI,aAAiC,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC;YAC/B,IAAI,EAAE,YAAY;YAClB,YAAY;YACZ,gBAAgB,EAAE,gBAAgB,CAAC,QAAQ,EAAE;YAC7C,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE,MAAM,EAAE,sCAAsC;SACxD,CAAC,CAAC;QACH,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC;QACvB,uFAAuF;QACvF,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,IAAK,MAAkC,CAAC,KAA2B,CAAC;IAC1G,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,SAAS,CAAC,aAAa,CAC3B,UAAU,EACV,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CACvD,CAAC;IACJ,CAAC;IAED,iDAAiD;IACjD,IAAI,aAAa,KAAK,KAAK,EAAE,CAAC;QAC5B,MAAM,SAAS,CAAC,aAAa,CAC3B,kBAAkB,EAClB,iDAAiD,CAClD,CAAC;IACJ,CAAC;IAED,mCAAmC;IACnC,IAAI,aAAwE,CAAC;IAC7E,IAAI,CAAC;QACH,4EAA4E;QAC5E,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;QACzC,WAAW,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAC/C,WAAW,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAE7C,aAAa,GAAG,MAAM,MAAM,CAAC,sBAAsB,CAAC,MAAM,EAAE,WAAW,EAAE;YACvE,gBAAgB,EAAE,YAAY;YAC9B,aAAa,EAAE,KAAK;SACrB,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,MAAM,CAAC,0BAA0B,EAAE,CAAC;YACvD,MAAM,SAAS,CAAC,aAAa,CAAC,eAAe,EAAE,KAAK,CAAC,iBAAiB,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC;QACzF,CAAC;QACD,IAAI,KAAK,YAAY,MAAM,CAAC,iBAAiB,EAAE,CAAC;YAC9C,MAAM,SAAS,CAAC,aAAa,CAAC,gBAAgB,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QACjE,CAAC;QACD,MAAM,SAAS,CAAC,aAAa,CAC3B,gBAAgB,EAChB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CACvD,CAAC;IACJ,CAAC;IAED,sCAAsC;IACtC,MAAM,MAAM,GAAiB;QAC3B,WAAW,EAAE,aAAa,CAAC,YAAY;QACvC,YAAY,EAAE,aAAa,CAAC,aAAa;QACzC,OAAO,EAAE,aAAa,CAAC,QAAQ;QAC/B,SAAS,EAAE,aAAa,CAAC,UAAU;YACjC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,aAAa,CAAC,UAAU;YAC1D,CAAC,CAAC,SAAS;KACd,CAAC;IAEF,MAAM,UAAU,CAAC,MAAM,CAAC,CAAC;IAEzB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,MAOxC;IACC,IAAI,MAAM,CAAC,gBAAgB,KAAK,MAAM,EAAE,CAAC;QACvC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,gBAAgB,IAAI,CAAC,MAAM,CAAC,mBAAmB,EAAE,CAAC;QAC5D,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,SAAS,EAAE,MAAM,CAAC,gBAAgB;QAClC,QAAQ,EAAE,MAAM,CAAC,mBAAmB;QACpC,KAAK,EAAE,MAAM,CAAC,eAAe;QAC7B,WAAW,EAAE,MAAM,CAAC,sBAAsB;QAC1C,SAAS,EAAE,MAAM,CAAC,oBAAoB;KACvC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,56 @@
1
+ import * as client from 'openid-client';
2
+ import type { StoredTokens } from './token-store.js';
3
+ /**
4
+ * Refresh tokens 60 seconds before actual expiry
5
+ * This buffer ensures tokens are refreshed before they become invalid
6
+ */
7
+ export declare const TOKEN_EXPIRY_BUFFER = 60;
8
+ /**
9
+ * Token refresher with mutex for concurrent access safety
10
+ *
11
+ * When an MCP server handles multiple simultaneous requests, each may try
12
+ * to refresh the token if it's expiring soon. Without coordination, this
13
+ * causes "invalid_grant" errors. This class ensures only one refresh
14
+ * happens at a time and all callers get the fresh token.
15
+ */
16
+ export declare class TokenRefresher {
17
+ private issuerUrl;
18
+ private clientId;
19
+ private refreshPromise;
20
+ private cachedConfig;
21
+ constructor(issuerUrl: string, clientId: string);
22
+ /**
23
+ * Get or create cached OIDC issuer configuration
24
+ */
25
+ getIssuerConfig(): Promise<client.Configuration>;
26
+ /**
27
+ * Check if token is valid (not expired or expiring soon)
28
+ * Returns false if token will expire within TOKEN_EXPIRY_BUFFER seconds
29
+ */
30
+ isTokenValid(tokens: StoredTokens): boolean;
31
+ /**
32
+ * Ensure we have a valid token, refreshing if necessary
33
+ *
34
+ * Uses a mutex pattern: if a refresh is already in progress,
35
+ * all callers wait for that same promise rather than starting
36
+ * parallel refresh requests.
37
+ */
38
+ ensureValidToken(): Promise<StoredTokens>;
39
+ /**
40
+ * Perform the actual token refresh
41
+ */
42
+ private doRefresh;
43
+ /**
44
+ * Clear cached configuration (useful for testing)
45
+ */
46
+ clearCache(): void;
47
+ }
48
+ /**
49
+ * Factory function to create a TokenRefresher instance
50
+ */
51
+ export declare function createTokenRefresher(issuerUrl: string, clientId: string): TokenRefresher;
52
+ /**
53
+ * Convenience function to ensure valid token with a new refresher
54
+ * For simple use cases that don't need to maintain refresher state
55
+ */
56
+ export declare function ensureValidToken(issuerUrl: string, clientId: string): Promise<StoredTokens>;