imagemcp-cli 1.0.1 → 1.0.3

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imagemcp-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "ImageMCP CLI & Authentication SDK — Seamless browser login, token encryption, and multi-model AI image generation.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/auth.js CHANGED
@@ -1,5 +1,3 @@
1
- const http = require('http');
2
- const url = require('url');
3
1
  const { exec } = require('child_process');
4
2
  const { saveConfig, loadConfig } = require('./config');
5
3
 
@@ -52,194 +50,83 @@ async function verifyToken(token, apiUrl = 'https://api.imagemcpserver.com') {
52
50
  }
53
51
 
54
52
  /**
55
- * HTML Response template shown in browser after login callback
53
+ * Execute Cloud Device-Code browser login flow with 5-minute timeout
56
54
  */
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;
55
+ async function loginFlow(options = {}) {
56
+ const currentConfig = loadConfig();
57
+ const apiUrl = (options.apiUrl || currentConfig.apiUrl || 'https://api.imagemcpserver.com').replace(/\/+$/, '');
58
+
59
+ // 1. Initialize CLI Authentication session via Backend API
60
+ let sessionData;
61
+ try {
62
+ const initRes = await fetch(`${apiUrl}/auth/cli-session/init`, {
63
+ method: 'POST',
64
+ headers: { 'Content-Type': 'application/json' }
65
+ });
66
+
67
+ if (!initRes.ok) {
68
+ throw new Error(`Server returned status ${initRes.status}`);
107
69
  }
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
- }
70
+ sessionData = await initRes.json();
71
+ } catch (err) {
72
+ throw new Error(`Failed to initiate authentication session with ${apiUrl}: ${err.message}`);
73
+ }
122
74
 
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
- }
75
+ const { code, userUrl } = sessionData;
76
+ const timeoutMs = 5 * 60 * 1000; // 5 Minutes (300 seconds) timeout
77
+ const startTime = Date.now();
146
78
 
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 || 'https://imagemcpserver.com';
79
+ console.log(`\n\x1b[1m\x1b[36m--- ImageMCP CLI Authentication ---\x1b[0m\n`);
80
+ console.log(`Opening browser to log in and approve CLI session...`);
81
+ console.log(`URL: \x1b[34m${userUrl}\x1b[0m`);
82
+ console.log(`Code: \x1b[1m\x1b[33m${code}\x1b[0m\n`);
83
+ console.log(`Waiting for approval in browser (expires in 5 minutes)...\n`);
154
84
 
85
+ openBrowser(userUrl);
86
+
87
+ // 2. Poll backend every 2 seconds for user approval
155
88
  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();
89
+ const pollInterval = setInterval(async () => {
90
+ const elapsed = Date.now() - startTime;
91
+
92
+ // 5-Minute Auto-Timeout Check
93
+ if (elapsed >= timeoutMs) {
94
+ clearInterval(pollInterval);
95
+ return reject(new Error('Authentication timed out after 5 minutes. Please run npx imagemcp login again.'));
167
96
  }
168
97
 
169
- if (parsedUrl.pathname === '/callback') {
170
- const token = parsedUrl.query.token;
98
+ try {
99
+ const statusRes = await fetch(`${apiUrl}/auth/cli-session/${code}/status`);
100
+ if (!statusRes.ok) return;
101
+
102
+ const data = await statusRes.json();
171
103
 
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'));
104
+ if (data.status === 'expired') {
105
+ clearInterval(pollInterval);
106
+ return reject(new Error('Authentication session expired after 5 minutes. Please run npx imagemcp login again.'));
177
107
  }
178
108
 
179
- try {
180
- // Verify token against ImageMCP backend
181
- const user = await verifyToken(token, apiUrl);
109
+ if (data.status === 'approved' && data.token) {
110
+ clearInterval(pollInterval);
182
111
 
183
- // Save encrypted token & user data to ~/.imagemcp/config.json
112
+ // Save encrypted token & user metadata to ~/.imagemcp/config.json
184
113
  const savedFile = saveConfig({
185
- apiKey: token,
114
+ apiKey: data.token,
186
115
  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
- }
116
+ user: data.user || null
194
117
  });
195
118
 
196
- res.writeHead(200, {
197
- 'Content-Type': 'text/html',
198
- 'Access-Control-Allow-Origin': '*'
119
+ resolve({
120
+ success: true,
121
+ token: data.token,
122
+ user: data.user,
123
+ savedTo: savedFile
199
124
  });
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
125
  }
219
- } else {
220
- res.writeHead(404, { 'Content-Type': 'text/plain' });
221
- res.end('Not found');
126
+ } catch (_) {
127
+ // Ignore temporary network errors during polling loop
222
128
  }
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
- });
129
+ }, 2000);
243
130
  });
244
131
  }
245
132
 
package/src/config.js CHANGED
@@ -89,7 +89,8 @@ function loadConfig() {
89
89
  if (!config.apiKey && fs.existsSync(LOCAL_CONFIG_FILE)) {
90
90
  try {
91
91
  const data = JSON.parse(fs.readFileSync(LOCAL_CONFIG_FILE, 'utf8'));
92
- if (data.token) config.apiKey = decrypt(data.token);
92
+ const rawToken = data.token || data.apiKey;
93
+ if (rawToken) config.apiKey = typeof rawToken === 'string' ? rawToken : decrypt(rawToken);
93
94
  if (data.apiUrl) config.apiUrl = data.apiUrl;
94
95
  if (data.user) config.user = data.user;
95
96
  config.savedTo = LOCAL_CONFIG_FILE;
@@ -100,7 +101,8 @@ function loadConfig() {
100
101
  if (!config.apiKey && fs.existsSync(GLOBAL_CONFIG_FILE)) {
101
102
  try {
102
103
  const data = JSON.parse(fs.readFileSync(GLOBAL_CONFIG_FILE, 'utf8'));
103
- if (data.token) config.apiKey = decrypt(data.token);
104
+ const rawToken = data.token || data.apiKey;
105
+ if (rawToken) config.apiKey = typeof rawToken === 'string' ? rawToken : decrypt(rawToken);
104
106
  if (data.apiUrl) config.apiUrl = data.apiUrl;
105
107
  if (data.user) config.user = data.user;
106
108
  config.savedTo = GLOBAL_CONFIG_FILE;
@@ -129,11 +131,12 @@ function saveConfig({ apiKey, apiUrl, user }, isLocal = false) {
129
131
  } catch (_) {}
130
132
  }
131
133
 
132
- const encryptedToken = encrypt(apiKey);
134
+ const tokenString = typeof apiKey === 'string' ? apiKey : (apiKey?.raw || apiKey?.token || '');
133
135
 
134
136
  const updated = {
135
137
  ...existing,
136
- token: encryptedToken,
138
+ token: tokenString,
139
+ apiKey: tokenString,
137
140
  apiUrl: apiUrl || existing.apiUrl || DEFAULT_API_URL,
138
141
  user: user || existing.user || null,
139
142
  updatedAt: new Date().toISOString()