@wonderwhy-er/desktop-commander 0.2.17 ā 0.2.18-alpha.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.
- package/README.md +1 -4
- package/dist/http-server-auto-tunnel.d.ts +1 -0
- package/dist/http-server-auto-tunnel.js +667 -0
- package/dist/http-server-named-tunnel.d.ts +2 -0
- package/dist/http-server-named-tunnel.js +167 -0
- package/dist/http-server-tunnel.d.ts +2 -0
- package/dist/http-server-tunnel.js +111 -0
- package/dist/http-server.d.ts +2 -0
- package/dist/http-server.js +270 -0
- package/dist/index.js +4 -0
- package/dist/oauth/auth-middleware.d.ts +20 -0
- package/dist/oauth/auth-middleware.js +62 -0
- package/dist/oauth/index.d.ts +3 -0
- package/dist/oauth/index.js +3 -0
- package/dist/oauth/oauth-manager.d.ts +80 -0
- package/dist/oauth/oauth-manager.js +179 -0
- package/dist/oauth/oauth-routes.d.ts +3 -0
- package/dist/oauth/oauth-routes.js +377 -0
- package/dist/server.js +32 -7
- package/dist/setup-claude-server.js +29 -5
- package/dist/terminal-manager.d.ts +1 -1
- package/dist/terminal-manager.js +56 -1
- package/dist/tools/config.js +2 -0
- package/dist/tools/feedback.js +2 -2
- package/dist/tools/improved-process-tools.js +179 -58
- package/dist/tools/schemas.d.ts +9 -0
- package/dist/tools/schemas.js +3 -0
- package/dist/types.d.ts +19 -0
- package/dist/utils/feature-flags.d.ts +43 -0
- package/dist/utils/feature-flags.js +147 -0
- package/dist/utils/toolHistory.js +3 -8
- package/dist/utils/usageTracker.d.ts +4 -0
- package/dist/utils/usageTracker.js +63 -37
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
|
@@ -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,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
|
+
}
|