create-walle 0.3.0 → 0.3.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.
package/package.json
CHANGED
|
@@ -107,6 +107,26 @@ const server = http.createServer((req, res) => {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// Slack OAuth callback (browser redirect — no auth token, must be before auth check)
|
|
111
|
+
if (url.pathname === '/api/slack/callback' && req.method === 'GET') {
|
|
112
|
+
try {
|
|
113
|
+
const slackMcp = require('../wall-e/tools/slack-mcp');
|
|
114
|
+
const code = url.searchParams.get('code');
|
|
115
|
+
const state = url.searchParams.get('state');
|
|
116
|
+
slackMcp.handleOAuthCallback(code, state).then(result => {
|
|
117
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
118
|
+
res.end(result.html);
|
|
119
|
+
}).catch(err => {
|
|
120
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
121
|
+
res.end('<html><body>OAuth error: ' + err.message + '</body></html>');
|
|
122
|
+
});
|
|
123
|
+
} catch (e) {
|
|
124
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
125
|
+
res.end('<html><body>Slack module not available: ' + e.message + '</body></html>');
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
110
130
|
// API routes
|
|
111
131
|
if (url.pathname.startsWith('/api/')) {
|
|
112
132
|
if (!isLocalhost(req)) {
|
|
@@ -213,13 +213,8 @@ function handleWalleApi(req, res, url) {
|
|
|
213
213
|
jsonResponse(res, { ok: true, already: true });
|
|
214
214
|
return true;
|
|
215
215
|
}
|
|
216
|
-
// Start OAuth —
|
|
217
|
-
slackMcp.authenticate()
|
|
218
|
-
console.log('[wall-e] Slack OAuth completed');
|
|
219
|
-
}).catch(err => {
|
|
220
|
-
console.error('[wall-e] Slack OAuth failed:', err.message);
|
|
221
|
-
});
|
|
222
|
-
// Tell client the flow started (browser will open)
|
|
216
|
+
// Start OAuth — opens browser, callback handled by CTM server route
|
|
217
|
+
slackMcp.authenticate();
|
|
223
218
|
jsonResponse(res, { ok: true, pending: true });
|
|
224
219
|
} catch (e) {
|
|
225
220
|
jsonResponse(res, { error: e.message }, 500);
|
|
@@ -6,9 +6,15 @@ const path = require('path');
|
|
|
6
6
|
|
|
7
7
|
const SLACK_MCP_URL = 'https://mcp.slack.com/mcp';
|
|
8
8
|
const CLIENT_ID = '1601185624273.8899143856786';
|
|
9
|
-
const CALLBACK_PORT = 3118; // Must match the registered redirect_uri for this client ID
|
|
10
9
|
const TOKEN_FILE = path.join(process.env.HOME, '.claude', 'wall-e-slack-token.json');
|
|
11
10
|
|
|
11
|
+
// OAuth callback is handled by CTM server at /api/slack/callback
|
|
12
|
+
// so it works on whatever port CTM is running on (no separate server needed).
|
|
13
|
+
function getCallbackUrl() {
|
|
14
|
+
const port = process.env.CTM_PORT || '3456';
|
|
15
|
+
return `http://localhost:${port}/api/slack/callback`;
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
// ── Token persistence ──
|
|
13
19
|
|
|
14
20
|
function loadToken() {
|
|
@@ -37,120 +43,109 @@ function generatePKCE() {
|
|
|
37
43
|
return { verifier, challenge };
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
// Pending OAuth state (set during authenticate, consumed by handleOAuthCallback)
|
|
47
|
+
let _pendingOAuth = null;
|
|
48
|
+
|
|
40
49
|
/**
|
|
41
|
-
* Start OAuth flow: opens browser,
|
|
42
|
-
* Returns the
|
|
50
|
+
* Start OAuth flow: opens browser, callback handled by CTM server.
|
|
51
|
+
* Returns immediately — the token is saved when CTM receives the callback.
|
|
43
52
|
*/
|
|
44
|
-
|
|
53
|
+
function authenticate() {
|
|
45
54
|
// Check if we already have a valid token
|
|
46
55
|
const existing = loadToken();
|
|
47
56
|
if (existing && existing.access_token) {
|
|
48
|
-
// TODO: check expiry if available
|
|
49
57
|
return existing.access_token;
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
const { verifier, challenge } = generatePKCE();
|
|
53
61
|
const state = crypto.randomBytes(16).toString('hex');
|
|
62
|
+
const redirectUri = getCallbackUrl();
|
|
54
63
|
|
|
55
|
-
|
|
56
|
-
const timeout = setTimeout(() => {
|
|
57
|
-
server.close();
|
|
58
|
-
reject(new Error('OAuth flow timed out (120s). Please try again.'));
|
|
59
|
-
}, 120000);
|
|
60
|
-
|
|
61
|
-
const server = http.createServer(async (req, res) => {
|
|
62
|
-
const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
|
|
63
|
-
if (url.pathname !== '/callback') {
|
|
64
|
-
res.writeHead(404);
|
|
65
|
-
res.end('Not found');
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const code = url.searchParams.get('code');
|
|
70
|
-
const returnedState = url.searchParams.get('state');
|
|
71
|
-
|
|
72
|
-
if (returnedState !== state) {
|
|
73
|
-
res.writeHead(400);
|
|
74
|
-
res.end('State mismatch. Please try again.');
|
|
75
|
-
clearTimeout(timeout);
|
|
76
|
-
server.close();
|
|
77
|
-
reject(new Error('OAuth state mismatch'));
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!code) {
|
|
82
|
-
res.writeHead(400);
|
|
83
|
-
res.end('No authorization code received.');
|
|
84
|
-
clearTimeout(timeout);
|
|
85
|
-
server.close();
|
|
86
|
-
reject(new Error('No authorization code'));
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
// Exchange code for token (MCP uses v2.user.access endpoint)
|
|
92
|
-
const tokenResp = await fetch('https://slack.com/api/oauth.v2.user.access', {
|
|
93
|
-
method: 'POST',
|
|
94
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
95
|
-
body: new URLSearchParams({
|
|
96
|
-
client_id: CLIENT_ID,
|
|
97
|
-
code,
|
|
98
|
-
code_verifier: verifier,
|
|
99
|
-
redirect_uri: `http://localhost:${CALLBACK_PORT}/callback`,
|
|
100
|
-
}),
|
|
101
|
-
});
|
|
102
|
-
const tokenData = await tokenResp.json();
|
|
103
|
-
|
|
104
|
-
if (!tokenData.ok) {
|
|
105
|
-
throw new Error(`Slack OAuth error: ${tokenData.error}`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Save token (v2.user.access returns tokens at top level, not nested under authed_user)
|
|
109
|
-
const tokenInfo = {
|
|
110
|
-
access_token: tokenData.access_token || tokenData.authed_user?.access_token,
|
|
111
|
-
refresh_token: tokenData.refresh_token,
|
|
112
|
-
team_id: tokenData.team?.id || tokenData.team_id,
|
|
113
|
-
team_name: tokenData.team?.name,
|
|
114
|
-
user_id: tokenData.user_id || tokenData.authed_user?.id,
|
|
115
|
-
scope: tokenData.scope || tokenData.authed_user?.scope,
|
|
116
|
-
obtained_at: new Date().toISOString(),
|
|
117
|
-
};
|
|
118
|
-
saveToken(tokenInfo);
|
|
119
|
-
|
|
120
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
121
|
-
res.end('<html><body style="font-family:system-ui;text-align:center;padding:40px"><h2>WALL-E connected to Slack!</h2><p>You can close this tab.</p></body></html>');
|
|
122
|
-
|
|
123
|
-
clearTimeout(timeout);
|
|
124
|
-
server.close();
|
|
125
|
-
resolve(tokenInfo.access_token);
|
|
126
|
-
} catch (err) {
|
|
127
|
-
res.writeHead(500);
|
|
128
|
-
res.end('Token exchange failed: ' + err.message);
|
|
129
|
-
clearTimeout(timeout);
|
|
130
|
-
server.close();
|
|
131
|
-
reject(err);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
64
|
+
_pendingOAuth = { verifier, state, redirectUri, createdAt: Date.now() };
|
|
134
65
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const scopes = 'search:read.public,search:read.private,search:read.mpim,search:read.im,search:read.files,search:read.users,chat:write,channels:history,groups:history,mpim:history,im:history,canvases:read,users:read,users:read.email';
|
|
138
|
-
const authUrl = `https://slack.com/oauth/v2_user/authorize?client_id=${CLIENT_ID}&scope=${encodeURIComponent(scopes)}&redirect_uri=${encodeURIComponent(`http://localhost:${CALLBACK_PORT}/callback`)}&state=${state}&code_challenge=${challenge}&code_challenge_method=S256`;
|
|
139
|
-
console.log('[slack-mcp] Opening browser for Slack OAuth...');
|
|
140
|
-
console.log('[slack-mcp] Auth URL:', authUrl);
|
|
141
|
-
|
|
142
|
-
// Open browser
|
|
143
|
-
const { execFile } = require('child_process');
|
|
144
|
-
execFile('open', [authUrl], (err) => {
|
|
145
|
-
if (err) console.error('[slack-mcp] Failed to open browser:', err.message);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
66
|
+
const scopes = 'search:read.public,search:read.private,search:read.mpim,search:read.im,search:read.files,search:read.users,chat:write,channels:history,groups:history,mpim:history,im:history,canvases:read,users:read,users:read.email';
|
|
67
|
+
const authUrl = `https://slack.com/oauth/v2_user/authorize?client_id=${CLIENT_ID}&scope=${encodeURIComponent(scopes)}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}&code_challenge=${challenge}&code_challenge_method=S256`;
|
|
148
68
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
69
|
+
console.log('[slack-mcp] Opening browser for Slack OAuth...');
|
|
70
|
+
console.log('[slack-mcp] Callback URL:', redirectUri);
|
|
71
|
+
|
|
72
|
+
const { execFile } = require('child_process');
|
|
73
|
+
execFile('open', [authUrl], (err) => {
|
|
74
|
+
if (err) console.error('[slack-mcp] Failed to open browser:', err.message);
|
|
153
75
|
});
|
|
76
|
+
|
|
77
|
+
return null; // Token not yet available — will be set by callback
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Handle the OAuth callback from Slack (called by CTM server route).
|
|
82
|
+
* Returns { ok, html } or { error, html }.
|
|
83
|
+
*/
|
|
84
|
+
async function handleOAuthCallback(code, returnedState) {
|
|
85
|
+
if (!_pendingOAuth) {
|
|
86
|
+
return { error: 'No pending OAuth flow. Click "Connect" again.', html: errorPage('No pending OAuth flow. Go back and click Connect again.') };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Expire after 10 minutes
|
|
90
|
+
if (Date.now() - _pendingOAuth.createdAt > 600000) {
|
|
91
|
+
_pendingOAuth = null;
|
|
92
|
+
return { error: 'OAuth flow expired.', html: errorPage('OAuth flow expired. Go back and click Connect again.') };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (returnedState !== _pendingOAuth.state) {
|
|
96
|
+
_pendingOAuth = null;
|
|
97
|
+
return { error: 'State mismatch.', html: errorPage('OAuth state mismatch. Please try again.') };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!code) {
|
|
101
|
+
_pendingOAuth = null;
|
|
102
|
+
return { error: 'No code.', html: errorPage('No authorization code received from Slack.') };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const tokenResp = await fetch('https://slack.com/api/oauth.v2.user.access', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
109
|
+
body: new URLSearchParams({
|
|
110
|
+
client_id: CLIENT_ID,
|
|
111
|
+
code,
|
|
112
|
+
code_verifier: _pendingOAuth.verifier,
|
|
113
|
+
redirect_uri: _pendingOAuth.redirectUri,
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
const tokenData = await tokenResp.json();
|
|
117
|
+
|
|
118
|
+
_pendingOAuth = null;
|
|
119
|
+
|
|
120
|
+
if (!tokenData.ok) {
|
|
121
|
+
return { error: tokenData.error, html: errorPage('Slack OAuth error: ' + tokenData.error) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const tokenInfo = {
|
|
125
|
+
access_token: tokenData.access_token || tokenData.authed_user?.access_token,
|
|
126
|
+
refresh_token: tokenData.refresh_token,
|
|
127
|
+
team_id: tokenData.team?.id || tokenData.team_id,
|
|
128
|
+
team_name: tokenData.team?.name,
|
|
129
|
+
user_id: tokenData.user_id || tokenData.authed_user?.id,
|
|
130
|
+
scope: tokenData.scope || tokenData.authed_user?.scope,
|
|
131
|
+
obtained_at: new Date().toISOString(),
|
|
132
|
+
};
|
|
133
|
+
saveToken(tokenInfo);
|
|
134
|
+
|
|
135
|
+
console.log('[slack-mcp] OAuth completed — team:', tokenInfo.team_name);
|
|
136
|
+
return { ok: true, html: successPage() };
|
|
137
|
+
} catch (err) {
|
|
138
|
+
_pendingOAuth = null;
|
|
139
|
+
return { error: err.message, html: errorPage('Token exchange failed: ' + err.message) };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function successPage() {
|
|
144
|
+
return '<html><body style="font-family:system-ui;text-align:center;padding:60px;background:#0d1117;color:#e6edf3"><h2 style="color:#3fb950">Wall-E connected to Slack!</h2><p style="color:#8b949e">You can close this tab and return to the setup page.</p></body></html>';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function errorPage(msg) {
|
|
148
|
+
return '<html><body style="font-family:system-ui;text-align:center;padding:60px;background:#0d1117;color:#e6edf3"><h2 style="color:#f85149">Slack Connection Failed</h2><p style="color:#8b949e">' + msg + '</p></body></html>';
|
|
154
149
|
}
|
|
155
150
|
|
|
156
151
|
// ── MCP HTTP client ──
|
|
@@ -287,4 +282,4 @@ if (require.main === module) {
|
|
|
287
282
|
}
|
|
288
283
|
}
|
|
289
284
|
|
|
290
|
-
module.exports = { authenticate, callSlackMcp, listSlackTools, isAuthenticated, loadToken, clearToken };
|
|
285
|
+
module.exports = { authenticate, handleOAuthCallback, callSlackMcp, listSlackTools, isAuthenticated, loadToken, clearToken };
|