@wonderwhy-er/desktop-commander 0.2.17 → 0.2.18-alpha.1

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.
@@ -0,0 +1,80 @@
1
+ interface JWTPayload {
2
+ sub: string;
3
+ client_id: string;
4
+ scope: string;
5
+ [key: string]: any;
6
+ }
7
+ interface TokenValidation {
8
+ valid: boolean;
9
+ username?: string;
10
+ client_id?: string;
11
+ scope?: string;
12
+ error?: string;
13
+ }
14
+ interface ClientData {
15
+ client_id: string;
16
+ client_name: string;
17
+ redirect_uris: string[];
18
+ grant_types: string[];
19
+ response_types: string[];
20
+ }
21
+ interface AuthCodeData {
22
+ username: string;
23
+ client_id: string;
24
+ redirect_uri: string;
25
+ code_challenge?: string;
26
+ code_challenge_method?: string;
27
+ scope: string;
28
+ expiresAt: number;
29
+ }
30
+ export declare class OAuthManager {
31
+ private users;
32
+ private codes;
33
+ private clients;
34
+ private privateKey;
35
+ private publicKey;
36
+ private baseUrl;
37
+ constructor(baseUrl: string);
38
+ /**
39
+ * Pre-register well-known OAuth clients with fixed IDs
40
+ * This prevents issues when server restarts and clients still have cached IDs
41
+ */
42
+ private preRegisterWellKnownClients;
43
+ /**
44
+ * Create a JWT token with the given payload
45
+ */
46
+ createJWT(payload: JWTPayload): string;
47
+ /**
48
+ * Validate a JWT token
49
+ */
50
+ validateToken(token: string): TokenValidation;
51
+ /**
52
+ * Register a new OAuth client
53
+ */
54
+ registerClient(redirect_uris: string[], client_name?: string): ClientData;
55
+ /**
56
+ * Get a client by ID
57
+ */
58
+ getClient(clientId: string): ClientData | undefined;
59
+ /**
60
+ * List all registered client IDs (for debugging)
61
+ */
62
+ listClients(): string;
63
+ /**
64
+ * Validate user credentials
65
+ */
66
+ validateUser(username: string, password: string): boolean;
67
+ /**
68
+ * Create an authorization code
69
+ */
70
+ createAuthCode(data: Omit<AuthCodeData, 'expiresAt'>): string;
71
+ /**
72
+ * Validate and consume an authorization code
73
+ */
74
+ validateAuthCode(code: string, client_id: string, redirect_uri: string, code_verifier?: string): {
75
+ valid: boolean;
76
+ data?: AuthCodeData;
77
+ error?: string;
78
+ };
79
+ }
80
+ export {};
@@ -0,0 +1,179 @@
1
+ import crypto from 'crypto';
2
+ export class OAuthManager {
3
+ constructor(baseUrl) {
4
+ this.users = new Map([['admin', 'password123']]);
5
+ this.codes = new Map();
6
+ this.clients = new Map();
7
+ this.baseUrl = baseUrl;
8
+ // Generate RSA keys for JWT signing
9
+ const keys = crypto.generateKeyPairSync('rsa', {
10
+ modulusLength: 2048,
11
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
12
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
13
+ });
14
+ this.privateKey = keys.privateKey;
15
+ this.publicKey = keys.publicKey;
16
+ console.log('šŸ” OAuth Manager: JWT Keys generated');
17
+ // Pre-register well-known clients for common AI tools
18
+ // This survives server restarts as they use fixed IDs
19
+ this.preRegisterWellKnownClients();
20
+ }
21
+ /**
22
+ * Pre-register well-known OAuth clients with fixed IDs
23
+ * This prevents issues when server restarts and clients still have cached IDs
24
+ */
25
+ preRegisterWellKnownClients() {
26
+ // ChatGPT client
27
+ const chatgptClient = {
28
+ client_id: 'chatgpt-fixed-client-id',
29
+ client_name: 'ChatGPT',
30
+ redirect_uris: [
31
+ 'https://chatgpt.com/connector_platform_oauth_redirect',
32
+ 'https://chat.openai.com/connector_platform_oauth_redirect'
33
+ ],
34
+ grant_types: ['authorization_code'],
35
+ response_types: ['code']
36
+ };
37
+ this.clients.set(chatgptClient.client_id, chatgptClient);
38
+ console.log(`šŸ” Pre-registered: ${chatgptClient.client_name} (${chatgptClient.client_id})`);
39
+ // Claude client
40
+ const claudeClient = {
41
+ client_id: 'claude-fixed-client-id',
42
+ client_name: 'Claude',
43
+ redirect_uris: [
44
+ 'https://claude.ai/oauth/callback',
45
+ 'http://localhost:3000/callback'
46
+ ],
47
+ grant_types: ['authorization_code'],
48
+ response_types: ['code']
49
+ };
50
+ this.clients.set(claudeClient.client_id, claudeClient);
51
+ console.log(`šŸ” Pre-registered: ${claudeClient.client_name} (${claudeClient.client_id})`);
52
+ }
53
+ /**
54
+ * Create a JWT token with the given payload
55
+ */
56
+ createJWT(payload) {
57
+ const header = { alg: 'RS256', typ: 'JWT', kid: 'key-1' };
58
+ const now = Math.floor(Date.now() / 1000);
59
+ const claims = {
60
+ ...payload,
61
+ iat: now,
62
+ exp: now + 3600, // 1 hour expiration
63
+ iss: this.baseUrl,
64
+ aud: this.baseUrl
65
+ };
66
+ const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url');
67
+ const encodedPayload = Buffer.from(JSON.stringify(claims)).toString('base64url');
68
+ const signature = crypto.sign('sha256', Buffer.from(`${encodedHeader}.${encodedPayload}`), this.privateKey);
69
+ return `${encodedHeader}.${encodedPayload}.${signature.toString('base64url')}`;
70
+ }
71
+ /**
72
+ * Validate a JWT token
73
+ */
74
+ validateToken(token) {
75
+ try {
76
+ const parts = token.split('.');
77
+ if (parts.length !== 3) {
78
+ return { valid: false, error: 'Invalid token format' };
79
+ }
80
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
81
+ // Check expiration
82
+ if (payload.exp < Math.floor(Date.now() / 1000)) {
83
+ return { valid: false, error: 'Token expired' };
84
+ }
85
+ // Verify signature
86
+ const signature = parts[2];
87
+ const data = `${parts[0]}.${parts[1]}`;
88
+ const valid = crypto.verify('sha256', Buffer.from(data), this.publicKey, Buffer.from(signature, 'base64url'));
89
+ if (!valid) {
90
+ return { valid: false, error: 'Invalid signature' };
91
+ }
92
+ return {
93
+ valid: true,
94
+ username: payload.sub,
95
+ client_id: payload.client_id,
96
+ scope: payload.scope
97
+ };
98
+ }
99
+ catch (err) {
100
+ return { valid: false, error: err instanceof Error ? err.message : 'Unknown error' };
101
+ }
102
+ }
103
+ /**
104
+ * Register a new OAuth client
105
+ */
106
+ registerClient(redirect_uris, client_name) {
107
+ const clientId = crypto.randomUUID();
108
+ const client = {
109
+ client_id: clientId,
110
+ client_name: client_name || 'MCP Client',
111
+ redirect_uris,
112
+ grant_types: ['authorization_code'],
113
+ response_types: ['code']
114
+ };
115
+ this.clients.set(clientId, client);
116
+ console.log(`šŸ” OAuth Manager: Registered client ${clientId} (${client.client_name})`);
117
+ return client;
118
+ }
119
+ /**
120
+ * Get a client by ID
121
+ */
122
+ getClient(clientId) {
123
+ return this.clients.get(clientId);
124
+ }
125
+ /**
126
+ * List all registered client IDs (for debugging)
127
+ */
128
+ listClients() {
129
+ const ids = Array.from(this.clients.keys());
130
+ return ids.length > 0 ? ids.join(', ') : 'none';
131
+ }
132
+ /**
133
+ * Validate user credentials
134
+ */
135
+ validateUser(username, password) {
136
+ return this.users.get(username) === password;
137
+ }
138
+ /**
139
+ * Create an authorization code
140
+ */
141
+ createAuthCode(data) {
142
+ const code = crypto.randomBytes(32).toString('base64url');
143
+ this.codes.set(code, {
144
+ ...data,
145
+ expiresAt: Date.now() + 600000 // 10 minutes
146
+ });
147
+ console.log(`šŸ” OAuth Manager: Created auth code for user ${data.username}`);
148
+ return code;
149
+ }
150
+ /**
151
+ * Validate and consume an authorization code
152
+ */
153
+ validateAuthCode(code, client_id, redirect_uri, code_verifier) {
154
+ const codeData = this.codes.get(code);
155
+ if (!codeData) {
156
+ return { valid: false, error: 'Invalid or expired code' };
157
+ }
158
+ if (codeData.expiresAt < Date.now()) {
159
+ this.codes.delete(code);
160
+ return { valid: false, error: 'Code expired' };
161
+ }
162
+ if (codeData.client_id !== client_id || codeData.redirect_uri !== redirect_uri) {
163
+ return { valid: false, error: 'Client ID or redirect URI mismatch' };
164
+ }
165
+ // PKCE verification
166
+ if (codeData.code_challenge) {
167
+ if (!code_verifier) {
168
+ return { valid: false, error: 'code_verifier required' };
169
+ }
170
+ const hash = crypto.createHash('sha256').update(code_verifier).digest('base64url');
171
+ if (hash !== codeData.code_challenge) {
172
+ return { valid: false, error: 'Invalid code_verifier' };
173
+ }
174
+ }
175
+ // Delete code after use (one-time use)
176
+ this.codes.delete(code);
177
+ return { valid: true, data: codeData };
178
+ }
179
+ }
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import { OAuthManager } from './oauth-manager.js';
3
+ export declare function createOAuthRoutes(oauthManager: OAuthManager, baseUrl: string): Router;
@@ -0,0 +1,377 @@
1
+ import { Router } from 'express';
2
+ export function createOAuthRoutes(oauthManager, baseUrl) {
3
+ const router = Router();
4
+ // ==================== DISCOVERY ENDPOINTS ====================
5
+ /**
6
+ * OAuth Authorization Server Metadata
7
+ * https://tools.ietf.org/html/rfc8414
8
+ */
9
+ router.get('/.well-known/oauth-authorization-server', (req, res) => {
10
+ res.json({
11
+ issuer: baseUrl,
12
+ authorization_endpoint: `${baseUrl}/authorize`,
13
+ token_endpoint: `${baseUrl}/token`,
14
+ registration_endpoint: `${baseUrl}/register`,
15
+ response_types_supported: ['code'],
16
+ grant_types_supported: ['authorization_code'],
17
+ code_challenge_methods_supported: ['S256'],
18
+ token_endpoint_auth_methods_supported: ['none'],
19
+ scopes_supported: ['mcp:tools']
20
+ });
21
+ });
22
+ /**
23
+ * MCP-specific OAuth Authorization Server Metadata
24
+ * ChatGPT requires this endpoint with pre-registered client_id
25
+ */
26
+ router.get('/.well-known/oauth-authorization-server/mcp', (req, res) => {
27
+ res.json({
28
+ issuer: baseUrl,
29
+ authorization_endpoint: `${baseUrl}/authorize`,
30
+ token_endpoint: `${baseUrl}/token`,
31
+ registration_endpoint: `${baseUrl}/register`,
32
+ response_types_supported: ['code'],
33
+ grant_types_supported: ['authorization_code'],
34
+ code_challenge_methods_supported: ['S256'],
35
+ token_endpoint_auth_methods_supported: ['none'],
36
+ scopes_supported: ['mcp:tools'],
37
+ // MCP-specific: Pre-registered client for ChatGPT
38
+ mcp: {
39
+ client_id: 'chatgpt-fixed-client-id',
40
+ redirect_uri: `${baseUrl}/callback`
41
+ }
42
+ });
43
+ });
44
+ /**
45
+ * OAuth Protected Resource Metadata
46
+ * https://tools.ietf.org/html/rfc8707
47
+ */
48
+ router.get('/.well-known/oauth-protected-resource', (req, res) => {
49
+ res.json({
50
+ resource: baseUrl,
51
+ authorization_servers: [baseUrl],
52
+ scopes_supported: ['mcp:tools'],
53
+ bearer_methods_supported: ['header']
54
+ });
55
+ });
56
+ /**
57
+ * MCP-specific OAuth Protected Resource Metadata
58
+ * ChatGPT queries this variant
59
+ */
60
+ router.get('/.well-known/oauth-protected-resource/mcp', (req, res) => {
61
+ res.json({
62
+ resource: baseUrl,
63
+ authorization_servers: [baseUrl],
64
+ scopes_supported: ['mcp:tools'],
65
+ bearer_methods_supported: ['header']
66
+ });
67
+ });
68
+ // ==================== CLIENT REGISTRATION ====================
69
+ /**
70
+ * Dynamic Client Registration
71
+ * https://tools.ietf.org/html/rfc7591
72
+ */
73
+ router.post('/register', (req, res) => {
74
+ const { redirect_uris, client_name } = req.body;
75
+ console.log(`\nšŸ” CLIENT REGISTRATION`);
76
+ console.log(` Client Name: ${client_name || 'Unnamed'}`);
77
+ console.log(` Redirect URIs:`, redirect_uris);
78
+ if (!redirect_uris || !Array.isArray(redirect_uris) || redirect_uris.length === 0) {
79
+ return res.status(400).json({
80
+ error: 'invalid_redirect_uri',
81
+ error_description: 'redirect_uris must be a non-empty array'
82
+ });
83
+ }
84
+ // Check if this looks like ChatGPT
85
+ const isChatGPT = redirect_uris.some(uri => uri.includes('chatgpt.com') || uri.includes('openai.com'));
86
+ // Check if this looks like Claude
87
+ const isClaude = redirect_uris.some(uri => uri.includes('claude.ai') || uri.includes('anthropic.com'));
88
+ let client;
89
+ if (isChatGPT) {
90
+ // Return pre-registered ChatGPT client
91
+ client = oauthManager.getClient('chatgpt-fixed-client-id');
92
+ console.log(`āœ… Using pre-registered ChatGPT client`);
93
+ // Add any new redirect URIs they're using
94
+ redirect_uris.forEach(uri => {
95
+ if (!client.redirect_uris.includes(uri)) {
96
+ client.redirect_uris.push(uri);
97
+ console.log(` šŸ“ Added redirect_uri: ${uri}`);
98
+ }
99
+ });
100
+ }
101
+ else if (isClaude) {
102
+ // Return pre-registered Claude client
103
+ client = oauthManager.getClient('claude-fixed-client-id');
104
+ console.log(`āœ… Using pre-registered Claude client`);
105
+ // Add any new redirect URIs they're using
106
+ redirect_uris.forEach(uri => {
107
+ if (!client.redirect_uris.includes(uri)) {
108
+ client.redirect_uris.push(uri);
109
+ console.log(` šŸ“ Added redirect_uri: ${uri}`);
110
+ }
111
+ });
112
+ }
113
+ else {
114
+ // Register new client for other tools
115
+ client = oauthManager.registerClient(redirect_uris, client_name);
116
+ console.log(`āœ… Registered new client: ${client.client_id}`);
117
+ }
118
+ res.status(201).json({
119
+ client_id: client.client_id,
120
+ client_name: client.client_name,
121
+ redirect_uris: client.redirect_uris,
122
+ grant_types: client.grant_types,
123
+ response_types: client.response_types,
124
+ token_endpoint_auth_method: 'none'
125
+ });
126
+ });
127
+ // ==================== AUTHORIZATION ====================
128
+ /**
129
+ * Authorization Endpoint (GET) - Display login page
130
+ */
131
+ router.get('/authorize', (req, res) => {
132
+ const { client_id, redirect_uri, response_type, state, code_challenge, code_challenge_method, scope } = req.query;
133
+ console.log(`\nšŸ” AUTHORIZATION REQUEST`);
134
+ console.log(` Client ID: ${client_id}`);
135
+ console.log(` Redirect URI: ${redirect_uri}`);
136
+ console.log(` Response Type: ${response_type}`);
137
+ console.log(` State: ${state}`);
138
+ console.log(` Code Challenge: ${code_challenge ? 'present' : 'missing'}`);
139
+ console.log(` Scope: ${scope}`);
140
+ // Validate client
141
+ const client = oauthManager.getClient(client_id);
142
+ if (!client) {
143
+ console.log(`āŒ Client ${client_id} not found. Was /register called?`);
144
+ console.log(` Registered clients: ${oauthManager.listClients()}`);
145
+ return res.status(400).send(`
146
+ <html>
147
+ <head><title>Error</title></head>
148
+ <body style="font-family: system-ui; max-width: 400px; margin: 50px auto; padding: 20px;">
149
+ <h2>āŒ Invalid Client</h2>
150
+ <p>The client_id <code>${client_id}</code> is not recognized.</p>
151
+ <p><small>The client must register at <code>/register</code> first.</small></p>
152
+ </body>
153
+ </html>
154
+ `);
155
+ }
156
+ console.log(`āœ… Client validated: ${client.client_name}`);
157
+ // Validate redirect_uri
158
+ if (!client.redirect_uris.includes(redirect_uri)) {
159
+ console.log(`āŒ Invalid redirect_uri: ${redirect_uri}`);
160
+ console.log(` Registered URIs:`, client.redirect_uris);
161
+ return res.status(400).send(`
162
+ <html>
163
+ <head><title>Error</title></head>
164
+ <body style="font-family: system-ui; max-width: 400px; margin: 50px auto; padding: 20px;">
165
+ <h2>āŒ Invalid Redirect URI</h2>
166
+ <p>The redirect_uri <code>${redirect_uri}</code> is not registered for this client.</p>
167
+ <p><strong>Registered URIs for ${client.client_name}:</strong></p>
168
+ <ul>
169
+ ${client.redirect_uris.map(uri => `<li><code>${uri}</code></li>`).join('')}
170
+ </ul>
171
+ </body>
172
+ </html>
173
+ `);
174
+ }
175
+ // Display login page
176
+ res.send(`
177
+ <html>
178
+ <head>
179
+ <title>Desktop Commander - Login</title>
180
+ <style>
181
+ body {
182
+ font-family: system-ui, -apple-system, sans-serif;
183
+ max-width: 400px;
184
+ margin: 50px auto;
185
+ padding: 20px;
186
+ background: #f5f5f5;
187
+ }
188
+ .container {
189
+ background: white;
190
+ padding: 30px;
191
+ border-radius: 10px;
192
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
193
+ }
194
+ h2 {
195
+ margin-top: 0;
196
+ color: #333;
197
+ }
198
+ .info {
199
+ background: #f0f0f0;
200
+ padding: 15px;
201
+ margin: 20px 0;
202
+ border-radius: 5px;
203
+ font-size: 14px;
204
+ }
205
+ input {
206
+ width: 100%;
207
+ padding: 12px;
208
+ margin: 10px 0;
209
+ box-sizing: border-box;
210
+ border: 1px solid #ddd;
211
+ border-radius: 5px;
212
+ font-size: 16px;
213
+ }
214
+ button {
215
+ width: 100%;
216
+ padding: 14px;
217
+ background: #0066cc;
218
+ color: white;
219
+ border: none;
220
+ border-radius: 5px;
221
+ cursor: pointer;
222
+ font-size: 16px;
223
+ font-weight: 600;
224
+ }
225
+ button:hover {
226
+ background: #0052a3;
227
+ }
228
+ .demo-creds {
229
+ text-align: center;
230
+ color: #666;
231
+ margin-top: 20px;
232
+ font-size: 14px;
233
+ }
234
+ </style>
235
+ </head>
236
+ <body>
237
+ <div class="container">
238
+ <h2>šŸ” Login to Desktop Commander</h2>
239
+ <div class="info">
240
+ <strong>Client:</strong> ${client.client_name}
241
+ </div>
242
+ <form method="POST" action="/authorize">
243
+ <input type="hidden" name="client_id" value="${client_id}">
244
+ <input type="hidden" name="redirect_uri" value="${redirect_uri}">
245
+ <input type="hidden" name="response_type" value="${response_type}">
246
+ <input type="hidden" name="state" value="${state || ''}">
247
+ <input type="hidden" name="code_challenge" value="${code_challenge || ''}">
248
+ <input type="hidden" name="code_challenge_method" value="${code_challenge_method || ''}">
249
+ <input type="hidden" name="scope" value="${scope || 'mcp:tools'}">
250
+
251
+ <input type="text" name="username" placeholder="Username" required autofocus>
252
+ <input type="password" name="password" placeholder="Password" required>
253
+
254
+ <button type="submit">Login & Authorize</button>
255
+ </form>
256
+ <div class="demo-creds">
257
+ Demo credentials:<br>
258
+ <strong>admin</strong> / <strong>password123</strong>
259
+ </div>
260
+ </div>
261
+ </body>
262
+ </html>
263
+ `);
264
+ });
265
+ /**
266
+ * Authorization Endpoint (POST) - Process login
267
+ */
268
+ router.post('/authorize', (req, res) => {
269
+ const { username, password, client_id, redirect_uri, state, code_challenge, code_challenge_method, scope } = req.body;
270
+ console.log(`\nšŸ” PROCESSING LOGIN`);
271
+ console.log(` Username: ${username}`);
272
+ console.log(` Client ID: ${client_id}`);
273
+ // Validate credentials
274
+ if (!oauthManager.validateUser(username, password)) {
275
+ return res.send(`
276
+ <html>
277
+ <head><title>Login Failed</title></head>
278
+ <body style="font-family: system-ui; max-width: 400px; margin: 50px auto; padding: 20px;">
279
+ <h2>āŒ Invalid Credentials</h2>
280
+ <p>The username or password is incorrect.</p>
281
+ <a href="/authorize?client_id=${client_id}&redirect_uri=${encodeURIComponent(redirect_uri)}&response_type=code&state=${state || ''}&code_challenge=${code_challenge || ''}&code_challenge_method=${code_challenge_method || ''}&scope=${scope || 'mcp:tools'}">Try Again</a>
282
+ </body>
283
+ </html>
284
+ `);
285
+ }
286
+ // Create authorization code
287
+ const code = oauthManager.createAuthCode({
288
+ username,
289
+ client_id,
290
+ redirect_uri,
291
+ code_challenge,
292
+ code_challenge_method,
293
+ scope: scope || 'mcp:tools'
294
+ });
295
+ console.log(`āœ… User ${username} authorized, redirecting...`);
296
+ // Redirect back to client with authorization code
297
+ const redirectUrl = new URL(redirect_uri);
298
+ redirectUrl.searchParams.set('code', code);
299
+ if (state) {
300
+ redirectUrl.searchParams.set('state', state);
301
+ }
302
+ res.redirect(redirectUrl.toString());
303
+ });
304
+ // ==================== TOKEN ENDPOINT ====================
305
+ /**
306
+ * Callback endpoint - for compatibility with MCP discovery
307
+ * This redirects to the client's actual redirect_uri with the code
308
+ */
309
+ router.get('/callback', (req, res) => {
310
+ const { code, state, error, error_description } = req.query;
311
+ console.log(`\nšŸ”„ CALLBACK`);
312
+ console.log(` Code: ${code ? 'present' : 'missing'}`);
313
+ console.log(` State: ${state}`);
314
+ if (error) {
315
+ return res.send(`
316
+ <html>
317
+ <head><title>OAuth Error</title></head>
318
+ <body style="font-family: system-ui; max-width: 400px; margin: 50px auto; padding: 20px;">
319
+ <h2>āŒ OAuth Error</h2>
320
+ <p><strong>Error:</strong> ${error}</p>
321
+ <p><strong>Description:</strong> ${error_description || 'No description provided'}</p>
322
+ </body>
323
+ </html>
324
+ `);
325
+ }
326
+ // This endpoint shouldn't really be called directly
327
+ // It's here for compatibility with the MCP discovery document
328
+ res.send(`
329
+ <html>
330
+ <head><title>OAuth Callback</title></head>
331
+ <body style="font-family: system-ui; max-width: 400px; margin: 50px auto; padding: 20px;">
332
+ <h2>āœ… Authorization Successful</h2>
333
+ <p>You can close this window and return to the application.</p>
334
+ </body>
335
+ </html>
336
+ `);
337
+ });
338
+ /**
339
+ * Token Endpoint - Exchange authorization code for access token
340
+ */
341
+ router.post('/token', (req, res) => {
342
+ const { grant_type, code, redirect_uri, client_id, code_verifier } = req.body;
343
+ console.log(`\nšŸŽ« TOKEN REQUEST`);
344
+ console.log(` Grant Type: ${grant_type}`);
345
+ console.log(` Client ID: ${client_id}`);
346
+ // Validate grant type
347
+ if (grant_type !== 'authorization_code') {
348
+ return res.status(400).json({
349
+ error: 'unsupported_grant_type',
350
+ error_description: 'Only authorization_code grant type is supported'
351
+ });
352
+ }
353
+ // Validate authorization code
354
+ const validation = oauthManager.validateAuthCode(code, client_id, redirect_uri, code_verifier);
355
+ if (!validation.valid) {
356
+ console.log(`āŒ Token request failed: ${validation.error}`);
357
+ return res.status(400).json({
358
+ error: 'invalid_grant',
359
+ error_description: validation.error
360
+ });
361
+ }
362
+ // Create access token
363
+ const accessToken = oauthManager.createJWT({
364
+ sub: validation.data.username,
365
+ client_id: validation.data.client_id,
366
+ scope: validation.data.scope
367
+ });
368
+ console.log(`āœ… Issued token for ${validation.data.username}`);
369
+ res.json({
370
+ access_token: accessToken,
371
+ token_type: 'Bearer',
372
+ expires_in: 3600,
373
+ scope: validation.data.scope
374
+ });
375
+ });
376
+ return router;
377
+ }