imagemcp-cli 1.0.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 +32 -0
- package/bin/imagemcp.js +121 -0
- package/package.json +28 -0
- package/src/auth.js +250 -0
- package/src/config.js +169 -0
- package/src/index.js +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# ImageMCP CLI & Auth SDK (`imagemcp`)
|
|
2
|
+
|
|
3
|
+
The official CLI and authentication SDK for [ImageMCP Server](https://api.imagemcpserver.com).
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### 1. Browser-based Authentication
|
|
8
|
+
```bash
|
|
9
|
+
npx imagemcp login
|
|
10
|
+
```
|
|
11
|
+
Opens your browser, lets you select or generate an API token, and securely stores an encrypted access token in `~/.imagemcp/config.json`.
|
|
12
|
+
|
|
13
|
+
### 2. Check Auth Status
|
|
14
|
+
```bash
|
|
15
|
+
npx imagemcp status
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### 3. Log Out
|
|
19
|
+
```bash
|
|
20
|
+
npx imagemcp logout
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Agent Skill Usage
|
|
24
|
+
When installed via skills CLI:
|
|
25
|
+
```bash
|
|
26
|
+
npx skills add web5lab/imagemcpserver
|
|
27
|
+
```
|
|
28
|
+
If not logged in, any invocation prompts:
|
|
29
|
+
```
|
|
30
|
+
ImageMCPServer isn't connected yet. Run npx imagemcp login to connect your account.
|
|
31
|
+
```
|
|
32
|
+
Once `npx imagemcp login` is completed, skills automatically resolve and use the saved token.
|
package/bin/imagemcp.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ImageMCP CLI - Browser-based authentication & token manager
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { loginFlow, verifyToken } = require('../src/auth');
|
|
8
|
+
const { loadConfig, clearConfig, GLOBAL_CONFIG_FILE } = require('../src/config');
|
|
9
|
+
|
|
10
|
+
const isTTY = Boolean(process.stderr.isTTY);
|
|
11
|
+
const c = {
|
|
12
|
+
reset: isTTY ? '\x1b[0m' : '',
|
|
13
|
+
bold: isTTY ? '\x1b[1m' : '',
|
|
14
|
+
green: isTTY ? '\x1b[32m' : '',
|
|
15
|
+
yellow: isTTY ? '\x1b[33m' : '',
|
|
16
|
+
cyan: isTTY ? '\x1b[36m' : '',
|
|
17
|
+
red: isTTY ? '\x1b[31m' : '',
|
|
18
|
+
dim: isTTY ? '\x1b[2m' : ''
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
async function handleLogin() {
|
|
22
|
+
const cfg = loadConfig();
|
|
23
|
+
if (cfg.apiKey) {
|
|
24
|
+
try {
|
|
25
|
+
const user = await verifyToken(cfg.apiKey, cfg.apiUrl);
|
|
26
|
+
console.log(`${c.bold}${c.green}✓ Already logged in as ${user.name} (${user.email})${c.reset}`);
|
|
27
|
+
console.log(`Stored token in ${cfg.savedTo || GLOBAL_CONFIG_FILE}\n`);
|
|
28
|
+
console.log(`Re-authenticating...`);
|
|
29
|
+
} catch (_) {
|
|
30
|
+
// Stored token invalid/expired, proceed to login
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const result = await loginFlow();
|
|
36
|
+
console.log(`${c.bold}${c.green}✓ Successfully authenticated as ${result.user.name} (${result.user.email})!${c.reset}`);
|
|
37
|
+
console.log(`${c.dim}Encrypted access token stored in ${result.savedTo}${c.reset}`);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error(`${c.bold}${c.red}✗ Login failed: ${err.message}${c.reset}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function handleLogout() {
|
|
45
|
+
const cleared = clearConfig();
|
|
46
|
+
if (cleared) {
|
|
47
|
+
console.log(`${c.bold}${c.green}✓ Successfully logged out from ImageMCP Server.${c.reset}`);
|
|
48
|
+
console.log(`Cleared config at ${GLOBAL_CONFIG_FILE}`);
|
|
49
|
+
} else {
|
|
50
|
+
console.log(`${c.yellow}No active login session found.${c.reset}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function handleStatus() {
|
|
55
|
+
const cfg = loadConfig();
|
|
56
|
+
if (!cfg.apiKey) {
|
|
57
|
+
console.log(`${c.bold}${c.yellow}ImageMCPServer isn't connected yet. Run npx imagemcp login to connect your account.${c.reset}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const user = await verifyToken(cfg.apiKey, cfg.apiUrl);
|
|
63
|
+
console.log(`${c.bold}${c.cyan}ImageMCP CLI Status:${c.reset}`);
|
|
64
|
+
console.log(` User: ${c.bold}${user.name}${c.reset} (${user.email})`);
|
|
65
|
+
console.log(` Plan: ${user.plan || 'free'}`);
|
|
66
|
+
console.log(` Credits: ${user.credits ?? 0}`);
|
|
67
|
+
console.log(` API URL: ${cfg.apiUrl}`);
|
|
68
|
+
console.log(` Saved To: ${cfg.savedTo || GLOBAL_CONFIG_FILE}`);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error(`${c.bold}${c.red}Invalid or expired session. Run npx imagemcp login to re-authenticate.${c.reset}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function printHelp() {
|
|
76
|
+
console.log(`
|
|
77
|
+
${c.bold}${c.cyan}ImageMCP CLI${c.reset} - Authentication & Token Management
|
|
78
|
+
|
|
79
|
+
${c.bold}USAGE:${c.reset}
|
|
80
|
+
npx imagemcp <command>
|
|
81
|
+
|
|
82
|
+
${c.bold}COMMANDS:${c.reset}
|
|
83
|
+
${c.green}login${c.reset} Log into ImageMCPServer via browser & save encrypted token
|
|
84
|
+
${c.green}status${c.reset} (whoami) Check current authentication status and user credits
|
|
85
|
+
${c.green}logout${c.reset} Remove stored authentication credentials from this machine
|
|
86
|
+
${c.green}help${c.reset} Display help details
|
|
87
|
+
`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function main() {
|
|
91
|
+
const args = process.argv.slice(2);
|
|
92
|
+
const command = args[0];
|
|
93
|
+
|
|
94
|
+
switch (command) {
|
|
95
|
+
case 'login':
|
|
96
|
+
await handleLogin();
|
|
97
|
+
break;
|
|
98
|
+
case 'logout':
|
|
99
|
+
handleLogout();
|
|
100
|
+
break;
|
|
101
|
+
case 'status':
|
|
102
|
+
case 'whoami':
|
|
103
|
+
await handleStatus();
|
|
104
|
+
break;
|
|
105
|
+
case 'help':
|
|
106
|
+
case '--help':
|
|
107
|
+
case '-h':
|
|
108
|
+
printHelp();
|
|
109
|
+
break;
|
|
110
|
+
default:
|
|
111
|
+
if (!command) {
|
|
112
|
+
printHelp();
|
|
113
|
+
} else {
|
|
114
|
+
console.error(`${c.red}Unknown command: ${command}${c.reset}`);
|
|
115
|
+
printHelp();
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "imagemcp-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ImageMCP CLI & Authentication SDK — Seamless browser login, token encryption, and multi-model AI image generation.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"imagemcp": "bin/imagemcp.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"login": "node bin/imagemcp.js login",
|
|
11
|
+
"status": "node bin/imagemcp.js status",
|
|
12
|
+
"logout": "node bin/imagemcp.js logout"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"imagemcp",
|
|
16
|
+
"mcp",
|
|
17
|
+
"auth",
|
|
18
|
+
"cli",
|
|
19
|
+
"ai",
|
|
20
|
+
"image-generation",
|
|
21
|
+
"skills"
|
|
22
|
+
],
|
|
23
|
+
"author": "web5lab",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const url = require('url');
|
|
3
|
+
const { exec } = require('child_process');
|
|
4
|
+
const { saveConfig, loadConfig } = require('./config');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Open URL in default system web browser across OS platforms
|
|
8
|
+
*/
|
|
9
|
+
function openBrowser(targetUrl) {
|
|
10
|
+
const platform = process.platform;
|
|
11
|
+
let command = '';
|
|
12
|
+
|
|
13
|
+
if (platform === 'darwin') {
|
|
14
|
+
command = `open "${targetUrl}"`;
|
|
15
|
+
} else if (platform === 'win32') {
|
|
16
|
+
command = `start "" "${targetUrl}"`;
|
|
17
|
+
} else {
|
|
18
|
+
command = `xdg-open "${targetUrl}"`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
exec(command, (err) => {
|
|
22
|
+
if (err) {
|
|
23
|
+
console.error(`\x1b[33mNote: Could not open browser automatically. Please open this link manually:\x1b[0m`);
|
|
24
|
+
console.error(` \x1b[36m${targetUrl}\x1b[0m\n`);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Verify token with ImageMCP Server backend
|
|
31
|
+
*/
|
|
32
|
+
async function verifyToken(token, apiUrl = 'https://api.imagemcpserver.com') {
|
|
33
|
+
const backendUrl = apiUrl.replace(/\/+$/, '');
|
|
34
|
+
const res = await fetch(`${backendUrl}/auth/user-data`, {
|
|
35
|
+
headers: {
|
|
36
|
+
'Authorization': `Bearer ${token}`,
|
|
37
|
+
'Content-Type': 'application/json'
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const errorData = await res.json().catch(() => ({}));
|
|
43
|
+
throw new Error(errorData.message || `Authentication failed with status ${res.status}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const data = await res.json();
|
|
47
|
+
if (!data.user) {
|
|
48
|
+
throw new Error('User profile data not returned by backend');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return data.user;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* HTML Response template shown in browser after login callback
|
|
56
|
+
*/
|
|
57
|
+
function getSuccessHtml(userName, userEmail) {
|
|
58
|
+
return `<!DOCTYPE html>
|
|
59
|
+
<html lang="en">
|
|
60
|
+
<head>
|
|
61
|
+
<meta charset="UTF-8">
|
|
62
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
63
|
+
<title>ImageMCP CLI Authentication Successful</title>
|
|
64
|
+
<style>
|
|
65
|
+
body {
|
|
66
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
67
|
+
background-color: #0f172a;
|
|
68
|
+
color: #f8fafc;
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
justify-content: center;
|
|
72
|
+
height: 100vh;
|
|
73
|
+
margin: 0;
|
|
74
|
+
}
|
|
75
|
+
.card {
|
|
76
|
+
background: #1e293b;
|
|
77
|
+
border: 1px solid #334155;
|
|
78
|
+
border-radius: 16px;
|
|
79
|
+
padding: 40px;
|
|
80
|
+
text-align: center;
|
|
81
|
+
max-width: 440px;
|
|
82
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
|
|
83
|
+
}
|
|
84
|
+
.icon {
|
|
85
|
+
width: 64px;
|
|
86
|
+
height: 64px;
|
|
87
|
+
background: #10b981;
|
|
88
|
+
color: #ffffff;
|
|
89
|
+
border-radius: 50%;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
font-size: 32px;
|
|
94
|
+
margin: 0 auto 24px;
|
|
95
|
+
}
|
|
96
|
+
h1 { font-size: 24px; margin: 0 0 12px; font-weight: 700; color: #ffffff; }
|
|
97
|
+
p { color: #94a3b8; font-size: 15px; margin: 0 0 24px; line-height: 1.5; }
|
|
98
|
+
.user-badge {
|
|
99
|
+
background: #334155;
|
|
100
|
+
border-radius: 8px;
|
|
101
|
+
padding: 12px 16px;
|
|
102
|
+
font-size: 14px;
|
|
103
|
+
color: #38bdf8;
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
margin-bottom: 24px;
|
|
106
|
+
word-break: break-all;
|
|
107
|
+
}
|
|
108
|
+
.footer { font-size: 13px; color: #64748b; }
|
|
109
|
+
</style>
|
|
110
|
+
</head>
|
|
111
|
+
<body>
|
|
112
|
+
<div class="card">
|
|
113
|
+
<div class="icon">✓</div>
|
|
114
|
+
<h1>CLI Connected!</h1>
|
|
115
|
+
<p>ImageMCP Server CLI has been successfully authenticated on this machine.</p>
|
|
116
|
+
<div class="user-badge">${userName || 'Authenticated User'} (${userEmail || 'ImageMCP Account'})</div>
|
|
117
|
+
<div class="footer">You can safely close this browser window and return to your terminal.</div>
|
|
118
|
+
</div>
|
|
119
|
+
</body>
|
|
120
|
+
</html>`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getErrorHtml(errorMessage) {
|
|
124
|
+
return `<!DOCTYPE html>
|
|
125
|
+
<html lang="en">
|
|
126
|
+
<head>
|
|
127
|
+
<meta charset="UTF-8">
|
|
128
|
+
<title>Authentication Failed - ImageMCP</title>
|
|
129
|
+
<style>
|
|
130
|
+
body { font-family: sans-serif; background: #0f172a; color: #f8fafc; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
|
131
|
+
.card { background: #1e293b; border: 1px solid #ef4444; border-radius: 16px; padding: 40px; text-align: center; max-width: 440px; }
|
|
132
|
+
.icon { font-size: 36px; margin-bottom: 16px; color: #ef4444; }
|
|
133
|
+
h1 { margin: 0 0 12px; color: #f87171; }
|
|
134
|
+
p { color: #cbd5e1; }
|
|
135
|
+
</style>
|
|
136
|
+
</head>
|
|
137
|
+
<body>
|
|
138
|
+
<div class="card">
|
|
139
|
+
<div class="icon">✕</div>
|
|
140
|
+
<h1>Authentication Failed</h1>
|
|
141
|
+
<p>${errorMessage || 'An error occurred during authentication.'}</p>
|
|
142
|
+
</div>
|
|
143
|
+
</body>
|
|
144
|
+
</html>`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Execute interactive browser login flow
|
|
149
|
+
*/
|
|
150
|
+
function loginFlow(options = {}) {
|
|
151
|
+
const currentConfig = loadConfig();
|
|
152
|
+
const apiUrl = options.apiUrl || currentConfig.apiUrl || 'https://api.imagemcpserver.com';
|
|
153
|
+
const webUrl = options.webUrl || process.env.IMAGEMCP_WEB_URL || 'http://localhost:5173';
|
|
154
|
+
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
const server = http.createServer(async (req, res) => {
|
|
157
|
+
const parsedUrl = url.parse(req.url, true);
|
|
158
|
+
|
|
159
|
+
// Handle CORS for options request
|
|
160
|
+
if (req.method === 'OPTIONS') {
|
|
161
|
+
res.writeHead(204, {
|
|
162
|
+
'Access-Control-Allow-Origin': '*',
|
|
163
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
164
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
|
165
|
+
});
|
|
166
|
+
return res.end();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (parsedUrl.pathname === '/callback') {
|
|
170
|
+
const token = parsedUrl.query.token;
|
|
171
|
+
|
|
172
|
+
if (!token) {
|
|
173
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
174
|
+
res.end(getErrorHtml('No authentication token received in callback.'));
|
|
175
|
+
server.close();
|
|
176
|
+
return reject(new Error('No authentication token received'));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
// Verify token against ImageMCP backend
|
|
181
|
+
const user = await verifyToken(token, apiUrl);
|
|
182
|
+
|
|
183
|
+
// Save encrypted token & user data to ~/.imagemcp/config.json
|
|
184
|
+
const savedFile = saveConfig({
|
|
185
|
+
apiKey: token,
|
|
186
|
+
apiUrl,
|
|
187
|
+
user: {
|
|
188
|
+
id: user._id,
|
|
189
|
+
name: user.name,
|
|
190
|
+
email: user.email,
|
|
191
|
+
plan: user.plan || 'free',
|
|
192
|
+
credits: user.credits ?? 0
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
res.writeHead(200, {
|
|
197
|
+
'Content-Type': 'text/html',
|
|
198
|
+
'Access-Control-Allow-Origin': '*'
|
|
199
|
+
});
|
|
200
|
+
res.end(getSuccessHtml(user.name, user.email));
|
|
201
|
+
|
|
202
|
+
// Close server and resolve CLI login process
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
server.close();
|
|
205
|
+
resolve({
|
|
206
|
+
success: true,
|
|
207
|
+
token,
|
|
208
|
+
user,
|
|
209
|
+
savedTo: savedFile
|
|
210
|
+
});
|
|
211
|
+
}, 500);
|
|
212
|
+
|
|
213
|
+
} catch (err) {
|
|
214
|
+
res.writeHead(401, { 'Content-Type': 'text/html' });
|
|
215
|
+
res.end(getErrorHtml(`Failed to verify token: ${err.message}`));
|
|
216
|
+
server.close();
|
|
217
|
+
reject(err);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
221
|
+
res.end('Not found');
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Listen on random available port or fallback port
|
|
226
|
+
server.listen(0, '127.0.0.1', () => {
|
|
227
|
+
const address = server.address();
|
|
228
|
+
const port = address.port;
|
|
229
|
+
|
|
230
|
+
const authUrl = `${webUrl.replace(/\/+$/, '')}/cli-login?port=${port}&apiUrl=${encodeURIComponent(apiUrl)}`;
|
|
231
|
+
|
|
232
|
+
console.log(`\n\x1b[1m\x1b[36m--- ImageMCP CLI Authentication ---\x1b[0m\n`);
|
|
233
|
+
console.log(`Opening browser to authenticate with ImageMCPServer...`);
|
|
234
|
+
console.log(`If browser does not open automatically, visit:\n \x1b[34m${authUrl}\x1b[0m\n`);
|
|
235
|
+
console.log(`Waiting for authentication...\n`);
|
|
236
|
+
|
|
237
|
+
openBrowser(authUrl);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
server.on('error', (err) => {
|
|
241
|
+
reject(new Error(`Local authentication server failed: ${err.message}`));
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = {
|
|
247
|
+
openBrowser,
|
|
248
|
+
verifyToken,
|
|
249
|
+
loginFlow
|
|
250
|
+
};
|
package/src/config.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
// Configuration directory & file paths
|
|
7
|
+
const GLOBAL_CONFIG_DIR = path.join(os.homedir(), '.imagemcp');
|
|
8
|
+
const GLOBAL_CONFIG_FILE = path.join(GLOBAL_CONFIG_DIR, 'config.json');
|
|
9
|
+
const LOCAL_CONFIG_DIR = path.join(process.cwd(), '.imagemcp');
|
|
10
|
+
const LOCAL_CONFIG_FILE = path.join(LOCAL_CONFIG_DIR, 'config.json');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_API_URL = process.env.IMAGEMCP_API_URL || 'https://api.imagemcpserver.com';
|
|
13
|
+
|
|
14
|
+
// Derive machine-specific key for AES-256-GCM encryption
|
|
15
|
+
function getMasterKey() {
|
|
16
|
+
const secretInfo = `${os.hostname()}-${os.userInfo().username}-imagemcp-secure-salt-v1`;
|
|
17
|
+
return crypto.createHash('sha256').update(secretInfo).digest();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Encrypt string data using AES-256-GCM
|
|
22
|
+
*/
|
|
23
|
+
function encrypt(text) {
|
|
24
|
+
if (!text) return null;
|
|
25
|
+
try {
|
|
26
|
+
const iv = crypto.randomBytes(12);
|
|
27
|
+
const key = getMasterKey();
|
|
28
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
29
|
+
|
|
30
|
+
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
31
|
+
encrypted += cipher.final('hex');
|
|
32
|
+
const authTag = cipher.getAuthTag().toString('hex');
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
encrypted,
|
|
36
|
+
iv: iv.toString('hex'),
|
|
37
|
+
authTag
|
|
38
|
+
};
|
|
39
|
+
} catch (err) {
|
|
40
|
+
// Fallback to unencrypted in case of crypto error
|
|
41
|
+
return { raw: text };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decrypt data structure produced by encrypt()
|
|
47
|
+
*/
|
|
48
|
+
function decrypt(payload) {
|
|
49
|
+
if (!payload) return '';
|
|
50
|
+
if (typeof payload === 'string') return payload; // Legacy / plain text support
|
|
51
|
+
if (payload.raw) return payload.raw;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const key = getMasterKey();
|
|
55
|
+
const iv = Buffer.from(payload.iv, 'hex');
|
|
56
|
+
const authTag = Buffer.from(payload.authTag, 'hex');
|
|
57
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
58
|
+
decipher.setAuthTag(authTag);
|
|
59
|
+
|
|
60
|
+
let decrypted = decipher.update(payload.encrypted, 'hex', 'utf8');
|
|
61
|
+
decrypted += decipher.final('utf8');
|
|
62
|
+
return decrypted;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
// If decryption fails, check if plain text token exists
|
|
65
|
+
return payload.encrypted || '';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Load active config from env, local file, or global ~/.imagemcp/config.json
|
|
71
|
+
*/
|
|
72
|
+
function loadConfig() {
|
|
73
|
+
let config = {
|
|
74
|
+
apiKey: '',
|
|
75
|
+
apiUrl: DEFAULT_API_URL,
|
|
76
|
+
user: null,
|
|
77
|
+
savedTo: null
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// 1. Environment variable override
|
|
81
|
+
if (process.env.IMAGEMCP_API_KEY || process.env.IMAGEMCP_KEY || process.env.IMAGEMCP_TOKEN) {
|
|
82
|
+
config.apiKey = process.env.IMAGEMCP_API_KEY || process.env.IMAGEMCP_KEY || process.env.IMAGEMCP_TOKEN;
|
|
83
|
+
}
|
|
84
|
+
if (process.env.IMAGEMCP_API_URL) {
|
|
85
|
+
config.apiUrl = process.env.IMAGEMCP_API_URL;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. Local config file (.imagemcp/config.json)
|
|
89
|
+
if (!config.apiKey && fs.existsSync(LOCAL_CONFIG_FILE)) {
|
|
90
|
+
try {
|
|
91
|
+
const data = JSON.parse(fs.readFileSync(LOCAL_CONFIG_FILE, 'utf8'));
|
|
92
|
+
if (data.token) config.apiKey = decrypt(data.token);
|
|
93
|
+
if (data.apiUrl) config.apiUrl = data.apiUrl;
|
|
94
|
+
if (data.user) config.user = data.user;
|
|
95
|
+
config.savedTo = LOCAL_CONFIG_FILE;
|
|
96
|
+
} catch (_) {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 3. Global config file (~/.imagemcp/config.json)
|
|
100
|
+
if (!config.apiKey && fs.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
101
|
+
try {
|
|
102
|
+
const data = JSON.parse(fs.readFileSync(GLOBAL_CONFIG_FILE, 'utf8'));
|
|
103
|
+
if (data.token) config.apiKey = decrypt(data.token);
|
|
104
|
+
if (data.apiUrl) config.apiUrl = data.apiUrl;
|
|
105
|
+
if (data.user) config.user = data.user;
|
|
106
|
+
config.savedTo = GLOBAL_CONFIG_FILE;
|
|
107
|
+
} catch (_) {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
config.apiUrl = (config.apiUrl || DEFAULT_API_URL).replace(/\/+$/, '');
|
|
111
|
+
return config;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Save configuration to global ~/.imagemcp/config.json or local .imagemcp/config.json
|
|
116
|
+
*/
|
|
117
|
+
function saveConfig({ apiKey, apiUrl, user }, isLocal = false) {
|
|
118
|
+
const configFile = isLocal ? LOCAL_CONFIG_FILE : GLOBAL_CONFIG_FILE;
|
|
119
|
+
const configDir = isLocal ? LOCAL_CONFIG_DIR : GLOBAL_CONFIG_DIR;
|
|
120
|
+
|
|
121
|
+
if (!fs.existsSync(configDir)) {
|
|
122
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let existing = {};
|
|
126
|
+
if (fs.existsSync(configFile)) {
|
|
127
|
+
try {
|
|
128
|
+
existing = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
129
|
+
} catch (_) {}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const encryptedToken = encrypt(apiKey);
|
|
133
|
+
|
|
134
|
+
const updated = {
|
|
135
|
+
...existing,
|
|
136
|
+
token: encryptedToken,
|
|
137
|
+
apiUrl: apiUrl || existing.apiUrl || DEFAULT_API_URL,
|
|
138
|
+
user: user || existing.user || null,
|
|
139
|
+
updatedAt: new Date().toISOString()
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
fs.writeFileSync(configFile, JSON.stringify(updated, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
143
|
+
return configFile;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Clear configuration / token
|
|
148
|
+
*/
|
|
149
|
+
function clearConfig(isLocal = false) {
|
|
150
|
+
const configFile = isLocal ? LOCAL_CONFIG_FILE : GLOBAL_CONFIG_FILE;
|
|
151
|
+
if (fs.existsSync(configFile)) {
|
|
152
|
+
fs.unlinkSync(configFile);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
GLOBAL_CONFIG_DIR,
|
|
160
|
+
GLOBAL_CONFIG_FILE,
|
|
161
|
+
LOCAL_CONFIG_DIR,
|
|
162
|
+
LOCAL_CONFIG_FILE,
|
|
163
|
+
DEFAULT_API_URL,
|
|
164
|
+
loadConfig,
|
|
165
|
+
saveConfig,
|
|
166
|
+
clearConfig,
|
|
167
|
+
encrypt,
|
|
168
|
+
decrypt
|
|
169
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const config = require('./config');
|
|
2
|
+
const auth = require('./auth');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get active API token or null if unauthenticated
|
|
6
|
+
*/
|
|
7
|
+
function getToken() {
|
|
8
|
+
const cfg = config.loadConfig();
|
|
9
|
+
return cfg.apiKey || null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check authentication or throw helpful error message
|
|
14
|
+
*/
|
|
15
|
+
function requireAuth(options = {}) {
|
|
16
|
+
const cfg = config.loadConfig();
|
|
17
|
+
if (!cfg.apiKey) {
|
|
18
|
+
const message = options.customMessage || "ImageMCPServer isn't connected yet. Run npx imagemcp login to connect your account.";
|
|
19
|
+
const err = new Error(message);
|
|
20
|
+
err.code = 'UNAUTHENTICATED';
|
|
21
|
+
err.hint = "Run 'npx imagemcp login' in your terminal to log in via browser.";
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
return cfg;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
...config,
|
|
29
|
+
...auth,
|
|
30
|
+
getToken,
|
|
31
|
+
requireAuth
|
|
32
|
+
};
|