@imenam/self-register-proxy 1.0.37
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/LICENSE +15 -0
- package/README.md +510 -0
- package/config.js +31 -0
- package/logger.js +88 -0
- package/meta-config.js +336 -0
- package/package.json +71 -0
- package/proxy-client.js +203 -0
- package/proxy.js +169 -0
- package/routes/dashboard.js +152 -0
- package/routes/metaConfig.js +180 -0
- package/routes/registration.js +227 -0
- package/routes/terminal.js +68 -0
- package/services/auth.js +151 -0
- package/services/healthCheck.js +95 -0
- package/services/portManager.js +82 -0
- package/services/proxyRouter.js +99 -0
- package/services/registry.js +41 -0
- package/terminal-server.js +154 -0
- package/views/dashboard.js +688 -0
- package/views/login.js +130 -0
- package/views/metaConfigs.js +643 -0
- package/views/terminal.html +1029 -0
- package/views/terminal.js +30 -0
package/proxy.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
require('dotenv').config({ path: path.join(__dirname, '.env') });
|
|
5
|
+
|
|
6
|
+
const express = require('express');
|
|
7
|
+
const helmet = require('helmet');
|
|
8
|
+
const rateLimit = require('express-rate-limit');
|
|
9
|
+
const { setupLogging } = require('./logger');
|
|
10
|
+
|
|
11
|
+
setupLogging('PROXY');
|
|
12
|
+
|
|
13
|
+
// Config — doit être importé après dotenv
|
|
14
|
+
const {
|
|
15
|
+
PROXY_PORT,
|
|
16
|
+
PROXY_NAME,
|
|
17
|
+
INTERNAL_PORT_START,
|
|
18
|
+
INTERNAL_PORT_END,
|
|
19
|
+
HEALTH_CHECK_INTERVAL,
|
|
20
|
+
HEALTH_CHECK_GRACE_PERIOD,
|
|
21
|
+
ADMIN_PASSWORD,
|
|
22
|
+
TERMINAL_CWD,
|
|
23
|
+
TERMINAL_COMMAND,
|
|
24
|
+
} = require('./config');
|
|
25
|
+
|
|
26
|
+
if (ADMIN_PASSWORD && !process.env.PROXY_SESSION_SECRET) {
|
|
27
|
+
console.error('FATAL: PROXY_SESSION_SECRET must be set when ADMIN_PASSWORD is configured.');
|
|
28
|
+
console.error('Generate one with: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Services
|
|
33
|
+
const { startHealthCheckPolling } = require('./services/healthCheck');
|
|
34
|
+
const { routingMiddleware } = require('./services/proxyRouter');
|
|
35
|
+
const { parseCookies, verifySignedToken, isValidSession } = require('./services/auth');
|
|
36
|
+
|
|
37
|
+
// Routes
|
|
38
|
+
const registrationRoutes = require('./routes/registration');
|
|
39
|
+
const metaConfigRoutes = require('./routes/metaConfig');
|
|
40
|
+
const terminalRoutes = require('./routes/terminal');
|
|
41
|
+
const dashboardRoutes = require('./routes/dashboard');
|
|
42
|
+
|
|
43
|
+
const app = express();
|
|
44
|
+
|
|
45
|
+
app.use(helmet({
|
|
46
|
+
contentSecurityPolicy: {
|
|
47
|
+
directives: {
|
|
48
|
+
defaultSrc: ["'self'"],
|
|
49
|
+
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.tailwindcss.com", "https://cdn.jsdelivr.net"],
|
|
50
|
+
scriptSrcAttr: ["'unsafe-inline'"],
|
|
51
|
+
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
|
|
52
|
+
connectSrc: ["'self'", "ws:", "wss:"],
|
|
53
|
+
imgSrc: ["'self'", "data:", "https://i.ytimg.com"],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
hsts: ADMIN_PASSWORD ? { maxAge: 31536000, includeSubDomains: true } : false,
|
|
57
|
+
referrerPolicy: { policy: 'same-origin' },
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
const isLocalRequest = (req) => {
|
|
61
|
+
const ip = req.ip || req.connection?.remoteAddress || '';
|
|
62
|
+
return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const loginLimiter = rateLimit({
|
|
66
|
+
windowMs: 15 * 60 * 1000,
|
|
67
|
+
max: 10,
|
|
68
|
+
message: { success: false, error: 'Too many login attempts, try again later' },
|
|
69
|
+
standardHeaders: true,
|
|
70
|
+
legacyHeaders: false,
|
|
71
|
+
skip: isLocalRequest,
|
|
72
|
+
});
|
|
73
|
+
app.use('/proxy/login', loginLimiter);
|
|
74
|
+
|
|
75
|
+
const globalLimiter = rateLimit({
|
|
76
|
+
windowMs: 60 * 1000,
|
|
77
|
+
max: 300,
|
|
78
|
+
standardHeaders: true,
|
|
79
|
+
legacyHeaders: false,
|
|
80
|
+
skip: isLocalRequest,
|
|
81
|
+
});
|
|
82
|
+
app.use(globalLimiter);
|
|
83
|
+
|
|
84
|
+
app.use(express.json({ limit: '1mb' }));
|
|
85
|
+
app.use(express.urlencoded({ limit: '1mb', extended: false }));
|
|
86
|
+
|
|
87
|
+
// Routes (ordre important : spécifiques avant le catch-all)
|
|
88
|
+
app.use(dashboardRoutes);
|
|
89
|
+
app.use(registrationRoutes);
|
|
90
|
+
app.use(metaConfigRoutes);
|
|
91
|
+
app.use(terminalRoutes);
|
|
92
|
+
|
|
93
|
+
// Middleware proxy (catch-all, doit être en dernier)
|
|
94
|
+
app.use(routingMiddleware);
|
|
95
|
+
|
|
96
|
+
// ============================================
|
|
97
|
+
// DÉMARRAGE DU SERVEUR
|
|
98
|
+
// ============================================
|
|
99
|
+
|
|
100
|
+
const server = app.listen(PROXY_PORT, () => {
|
|
101
|
+
console.log('='.repeat(50));
|
|
102
|
+
console.log(`🚀 ${PROXY_NAME}`);
|
|
103
|
+
console.log('='.repeat(50));
|
|
104
|
+
console.log(`Listening on port ${PROXY_PORT}`);
|
|
105
|
+
console.log(`Internal ports range: ${INTERNAL_PORT_START}-${INTERNAL_PORT_END}`);
|
|
106
|
+
console.log(`Health check interval: ${HEALTH_CHECK_INTERVAL}ms`);
|
|
107
|
+
console.log(`Health check grace period: ${HEALTH_CHECK_GRACE_PERIOD}ms`);
|
|
108
|
+
console.log('');
|
|
109
|
+
|
|
110
|
+
if (!ADMIN_PASSWORD) {
|
|
111
|
+
console.log('⚠️ WARNING: ADMIN_PASSWORD is not set — web interface is unprotected');
|
|
112
|
+
} else {
|
|
113
|
+
console.log('🔒 Web interface protected by admin password');
|
|
114
|
+
console.log('🔒 Session secret configured');
|
|
115
|
+
}
|
|
116
|
+
console.log('🔒 API routes restricted to localhost only');
|
|
117
|
+
console.log('');
|
|
118
|
+
|
|
119
|
+
console.log('API Endpoints:');
|
|
120
|
+
console.log(' POST /proxy/register-auto - Register a new route (auto port)');
|
|
121
|
+
console.log(' POST /proxy/register-manual - Register a new route (manual port)');
|
|
122
|
+
console.log(' DELETE /proxy/unregister - Remove a route');
|
|
123
|
+
console.log(' GET /proxy/routes - List all routes');
|
|
124
|
+
console.log(' GET /proxy/health - Health check');
|
|
125
|
+
console.log(' GET /proxy/login - Admin login page');
|
|
126
|
+
console.log(' GET /proxy/terminal - Agent terminal');
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log('Note: Registered servers must implement GET /proxy/health');
|
|
129
|
+
console.log(' returning status 200 to stay registered.');
|
|
130
|
+
console.log('='.repeat(50));
|
|
131
|
+
|
|
132
|
+
// Terminal en ligne
|
|
133
|
+
const { TerminalServer } = require('./terminal-server');
|
|
134
|
+
new TerminalServer(server, {
|
|
135
|
+
path: '/terminal-ws',
|
|
136
|
+
command: TERMINAL_COMMAND,
|
|
137
|
+
ptyOptions: {
|
|
138
|
+
cwd: TERMINAL_CWD,
|
|
139
|
+
},
|
|
140
|
+
wsOptions: {
|
|
141
|
+
verifyClient: (info) => {
|
|
142
|
+
if (ADMIN_PASSWORD) {
|
|
143
|
+
const origin = info.origin || info.req.headers.origin;
|
|
144
|
+
const host = info.req.headers.host;
|
|
145
|
+
if (origin) {
|
|
146
|
+
try {
|
|
147
|
+
const originHost = new URL(origin).host;
|
|
148
|
+
if (originHost !== host) return false;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const cookies = parseCookies(info.req);
|
|
155
|
+
const signedValue = cookies['proxy_session'];
|
|
156
|
+
if (!signedValue) return false;
|
|
157
|
+
const token = verifySignedToken(signedValue);
|
|
158
|
+
return token ? isValidSession(token) : false;
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
console.log(`🖥️ Agent terminal at /proxy/terminal (cwd: ${TERMINAL_CWD}, command: ${TERMINAL_COMMAND || 'none'})`);
|
|
165
|
+
|
|
166
|
+
startHealthCheckPolling();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
module.exports = { app };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const { Router } = require('express');
|
|
2
|
+
const { exec } = require('child_process');
|
|
3
|
+
const { requireAuth } = require('../services/auth');
|
|
4
|
+
const { safeEqual, createSession, setSessionCookie, destroySession, clearSessionCookie, getSessionFromCookie } = require('../services/auth');
|
|
5
|
+
const { registry } = require('../services/registry');
|
|
6
|
+
const { renderLogin } = require('../views/login');
|
|
7
|
+
const { renderDashboard } = require('../views/dashboard');
|
|
8
|
+
const { PROXY_NAME, PROXY_PORT, ADMIN_PASSWORD, INTERNAL_PORT_START, INTERNAL_PORT_END, OPENCODE_CLEAN_UP, TERMINAL_CWD } = require('../config');
|
|
9
|
+
|
|
10
|
+
const router = Router();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* GET /proxy/health
|
|
14
|
+
* Health check du proxy — info minimale sans auth, complète avec auth
|
|
15
|
+
*/
|
|
16
|
+
router.get('/proxy/health', (req, res) => {
|
|
17
|
+
if (!ADMIN_PASSWORD || getSessionFromCookie(req)) {
|
|
18
|
+
return res.json({
|
|
19
|
+
name: PROXY_NAME,
|
|
20
|
+
status: 'healthy',
|
|
21
|
+
uptime: process.uptime(),
|
|
22
|
+
registeredRoutes: registry.routes.size,
|
|
23
|
+
usedPorts: registry.usedPorts.size
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
res.json({ status: 'healthy' });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* GET /proxy/login
|
|
31
|
+
* Page de connexion administrateur
|
|
32
|
+
*/
|
|
33
|
+
router.get('/proxy/login', (req, res) => {
|
|
34
|
+
const error = req.query.error === '1';
|
|
35
|
+
res.type('html').send(renderLogin(PROXY_NAME, error));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* POST /proxy/login
|
|
40
|
+
* Traitement du formulaire de connexion
|
|
41
|
+
*/
|
|
42
|
+
router.post('/proxy/login', (req, res) => {
|
|
43
|
+
const { password } = req.body;
|
|
44
|
+
|
|
45
|
+
if (ADMIN_PASSWORD && safeEqual(password, ADMIN_PASSWORD)) {
|
|
46
|
+
const token = createSession();
|
|
47
|
+
setSessionCookie(res, token);
|
|
48
|
+
return res.redirect('/');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
res.redirect('/proxy/login?error=1');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* POST /proxy/logout
|
|
56
|
+
* Déconnexion — détruit la session et redirige vers /proxy/login
|
|
57
|
+
*/
|
|
58
|
+
router.post('/proxy/logout', (req, res) => {
|
|
59
|
+
const sessionToken = getSessionFromCookie(req);
|
|
60
|
+
if (sessionToken) {
|
|
61
|
+
destroySession(sessionToken);
|
|
62
|
+
}
|
|
63
|
+
clearSessionCookie(res);
|
|
64
|
+
res.redirect('/proxy/login');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* GET /
|
|
69
|
+
* Dashboard — retourne HTML ou JSON selon l'en-tête Accept
|
|
70
|
+
*/
|
|
71
|
+
router.get('/', requireAuth, (req, res) => {
|
|
72
|
+
const routes = [];
|
|
73
|
+
registry.routes.forEach((value, path) => {
|
|
74
|
+
routes.push({
|
|
75
|
+
path,
|
|
76
|
+
port: value.port,
|
|
77
|
+
target: value.target || 'http://localhost',
|
|
78
|
+
name: value.name,
|
|
79
|
+
healthCheck: value.healthCheck,
|
|
80
|
+
registeredAt: value.registeredAt,
|
|
81
|
+
fullTarget: `${value.target || 'http://localhost'}:${value.port}`
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
routes.sort((a, b) => a.name.localeCompare(b.name));
|
|
86
|
+
|
|
87
|
+
const acceptHeader = req.get('Accept') || '';
|
|
88
|
+
const wantsJson = acceptHeader.includes('application/json') ||
|
|
89
|
+
(acceptHeader.includes('*/*') && !acceptHeader.includes('text/html'));
|
|
90
|
+
|
|
91
|
+
if (wantsJson) {
|
|
92
|
+
return res.json({
|
|
93
|
+
name: PROXY_NAME,
|
|
94
|
+
status: 'healthy',
|
|
95
|
+
uptime: process.uptime(),
|
|
96
|
+
count: routes.length,
|
|
97
|
+
routes,
|
|
98
|
+
availablePorts: (INTERNAL_PORT_END - INTERNAL_PORT_START) - registry.usedPorts.size
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const availablePorts = (INTERNAL_PORT_END - INTERNAL_PORT_START) - registry.usedPorts.size;
|
|
103
|
+
const html = renderDashboard(routes, availablePorts, PROXY_NAME, !!ADMIN_PASSWORD, OPENCODE_CLEAN_UP);
|
|
104
|
+
res.type('html').send(html);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* POST /proxy/cleanup
|
|
109
|
+
* Exécute la commande de nettoyage des processus (OPENCODE_CLEAN_UP)
|
|
110
|
+
*/
|
|
111
|
+
router.post('/proxy/cleanup', requireAuth, (req, res) => {
|
|
112
|
+
if (!OPENCODE_CLEAN_UP) {
|
|
113
|
+
return res.status(400).json({ error: 'OPENCODE_CLEAN_UP non configuré' });
|
|
114
|
+
}
|
|
115
|
+
console.log(`[CLEANUP] CWD: ${TERMINAL_CWD}`);
|
|
116
|
+
console.log(`[CLEANUP] Executing: ${OPENCODE_CLEAN_UP}`);
|
|
117
|
+
exec(OPENCODE_CLEAN_UP, { shell: true, cwd: TERMINAL_CWD }, (err, stdout, stderr) => {
|
|
118
|
+
if (stdout) console.log(`[CLEANUP] stdout: ${stdout.trim()}`);
|
|
119
|
+
if (stderr) console.log(`[CLEANUP] stderr: ${stderr.trim()}`);
|
|
120
|
+
if (err) {
|
|
121
|
+
console.log(`[CLEANUP] Error: code=${err.code}, signal=${err.signal}, message=${err.message}`);
|
|
122
|
+
} else {
|
|
123
|
+
console.log('[CLEANUP] Done (exit code: 0)');
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
res.json({ ok: true });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* POST /proxy/kill-port
|
|
131
|
+
* Tue le process écoutant sur un port donné (cross-platform)
|
|
132
|
+
*/
|
|
133
|
+
router.post('/proxy/kill-port', requireAuth, (req, res) => {
|
|
134
|
+
const port = parseInt(req.body.port, 10);
|
|
135
|
+
if (!port || port < 1 || port > 65535) {
|
|
136
|
+
return res.status(400).json({ error: 'Port invalide' });
|
|
137
|
+
}
|
|
138
|
+
const cmd = process.platform === 'win32'
|
|
139
|
+
? `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne 0 } | Select-Object -ExpandProperty OwningProcess -Unique | ForEach-Object { Stop-Process -Id $_ -Force -ErrorAction SilentlyContinue }`
|
|
140
|
+
: `fuser -k ${port}/tcp`;
|
|
141
|
+
const options = process.platform === 'win32'
|
|
142
|
+
? { shell: 'powershell.exe' }
|
|
143
|
+
: { shell: true };
|
|
144
|
+
exec(cmd, options, (err, stdout, stderr) => {
|
|
145
|
+
if (stdout) console.log(`[KILL-PORT] stdout: ${stdout.trim()}`);
|
|
146
|
+
if (stderr) console.log(`[KILL-PORT] stderr: ${stderr.trim()}`);
|
|
147
|
+
if (err) console.log(`[KILL-PORT] Error: ${err.message}`);
|
|
148
|
+
});
|
|
149
|
+
res.json({ ok: true });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
module.exports = router;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const { Router } = require('express');
|
|
2
|
+
const metaConfig = require('../meta-config');
|
|
3
|
+
const { requireAuth } = require('../services/auth');
|
|
4
|
+
const { registry } = require('../services/registry');
|
|
5
|
+
const { renderMetaConfigs } = require('../views/metaConfigs');
|
|
6
|
+
const { PROXY_NAME } = require('../config');
|
|
7
|
+
|
|
8
|
+
const router = Router();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GET /proxy/mcp-configurations
|
|
12
|
+
* Découvre les configurations disponibles auprès de tous les MCPs enregistrés
|
|
13
|
+
*/
|
|
14
|
+
router.get('/proxy/mcp-configurations', requireAuth, async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const results = await metaConfig.discoverConfigurations(registry.routes);
|
|
17
|
+
const compatible = results.filter(r => r.configurations !== null);
|
|
18
|
+
const incompatible = results.filter(r => r.configurations === null);
|
|
19
|
+
|
|
20
|
+
res.json({ success: true, compatible: compatible.length, incompatible: incompatible.length, results });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('[META-CONFIG] Discovery error:', error.message);
|
|
23
|
+
res.status(500).json({ success: false, error: error.message });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* GET /proxy/meta-configs
|
|
29
|
+
* Liste toutes les méta-configurations sauvegardées
|
|
30
|
+
*/
|
|
31
|
+
router.get('/proxy/meta-configs', requireAuth, (req, res) => {
|
|
32
|
+
const data = metaConfig.getAll();
|
|
33
|
+
res.json({ success: true, metaConfigs: data.metaConfigs, activeMetaConfig: data.activeMetaConfig });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* POST /proxy/meta-configs
|
|
38
|
+
* Crée une nouvelle méta-configuration
|
|
39
|
+
*/
|
|
40
|
+
router.post('/proxy/meta-configs', requireAuth, (req, res) => {
|
|
41
|
+
const { id, name, description, mappings } = req.body;
|
|
42
|
+
|
|
43
|
+
if (!id || !name) {
|
|
44
|
+
return res.status(400).json({ success: false, error: 'id and name are required' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const created = metaConfig.create(id, name, description, mappings);
|
|
49
|
+
console.log(`[META-CONFIG] Created: ${id}`);
|
|
50
|
+
res.json({ success: true, metaConfig: { id, ...created } });
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const status = error.message.includes('already exists') ? 409 : 500;
|
|
53
|
+
res.status(status).json({ success: false, error: error.message });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* PUT /proxy/meta-configs/:id
|
|
59
|
+
* Met à jour une méta-configuration existante
|
|
60
|
+
*/
|
|
61
|
+
router.put('/proxy/meta-configs/:id', requireAuth, (req, res) => {
|
|
62
|
+
const { id } = req.params;
|
|
63
|
+
const { name, description, mappings } = req.body;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const updated = metaConfig.update(id, { name, description, mappings });
|
|
67
|
+
console.log(`[META-CONFIG] Updated: ${id}`);
|
|
68
|
+
res.json({ success: true, metaConfig: { id, ...updated } });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const status = error.message.includes('not found') ? 404 : 500;
|
|
71
|
+
res.status(status).json({ success: false, error: error.message });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* DELETE /proxy/meta-configs/:id
|
|
77
|
+
* Supprime une méta-configuration
|
|
78
|
+
*/
|
|
79
|
+
router.delete('/proxy/meta-configs/:id', requireAuth, (req, res) => {
|
|
80
|
+
const { id } = req.params;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
metaConfig.remove(id);
|
|
84
|
+
console.log(`[META-CONFIG] Deleted: ${id}`);
|
|
85
|
+
res.json({ success: true, deleted: id });
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const status = error.message.includes('not found') ? 404 : 500;
|
|
88
|
+
res.status(status).json({ success: false, error: error.message });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* POST /proxy/meta-configs/:id/apply
|
|
94
|
+
* Applique une méta-configuration
|
|
95
|
+
*/
|
|
96
|
+
router.post('/proxy/meta-configs/:id/apply', requireAuth, async (req, res) => {
|
|
97
|
+
const { id } = req.params;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const result = await metaConfig.applyMetaConfig(id, registry.routes);
|
|
101
|
+
console.log(`[META-CONFIG] Applied "${id}": ${result.success ? 'all OK' : 'some failures'}`);
|
|
102
|
+
res.json({ success: result.success, warnings: result.warnings, metaConfigId: id, results: result.results });
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const status = error.message.includes('not found') ? 404 : 500;
|
|
105
|
+
res.status(status).json({ success: false, error: error.message });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* GET /proxy/meta-configs/active/sync
|
|
111
|
+
* Vérifie si les configurations actives des MCPs correspondent à la méta-config active
|
|
112
|
+
*/
|
|
113
|
+
router.get('/proxy/meta-configs/active/sync', requireAuth, async (req, res) => {
|
|
114
|
+
const activeId = metaConfig.getActive();
|
|
115
|
+
console.log(`[META-CONFIG SYNC] activeId = ${JSON.stringify(activeId)}`);
|
|
116
|
+
|
|
117
|
+
if (!activeId) {
|
|
118
|
+
return res.json({ synced: true, activeMetaConfigId: null });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const activeCfg = metaConfig.getById(activeId);
|
|
122
|
+
console.log(`[META-CONFIG SYNC] mappings = ${JSON.stringify(activeCfg?.mappings)}`);
|
|
123
|
+
if (!activeCfg) {
|
|
124
|
+
return res.json({ synced: true, activeMetaConfigId: null });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const results = await metaConfig.discoverConfigurations(registry.routes);
|
|
129
|
+
console.log(`[META-CONFIG SYNC] discoverConfigurations results:`);
|
|
130
|
+
results.forEach(r => {
|
|
131
|
+
if (r.configurations) {
|
|
132
|
+
const active = r.configurations.find(c => c.active);
|
|
133
|
+
console.log(` ${r.path} → configs: [${r.configurations.map(c => `${c.id}${c.active ? '*' : ''}`).join(', ')}] — active: ${JSON.stringify(active?.id ?? null)}`);
|
|
134
|
+
} else {
|
|
135
|
+
console.log(` ${r.path} → error: ${r.error}`);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const conflicts = [];
|
|
140
|
+
|
|
141
|
+
for (const [mcpPath, expectedConfigId] of Object.entries(activeCfg.mappings)) {
|
|
142
|
+
const mcpResult = results.find(r => r.path === mcpPath);
|
|
143
|
+
if (!mcpResult) {
|
|
144
|
+
console.log(` [META-CONFIG SYNC] ${mcpPath} → MCP non trouvé dans les résultats, ignoré`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!mcpResult.configurations) {
|
|
148
|
+
console.log(` [META-CONFIG SYNC] ${mcpPath} → pas de configurations (erreur: ${mcpResult.error}), ignoré`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const activeConfig = mcpResult.configurations.find(c => c.active);
|
|
153
|
+
const actualConfigId = activeConfig ? activeConfig.id : null;
|
|
154
|
+
|
|
155
|
+
console.log(` [META-CONFIG SYNC] ${mcpPath} → attendu: ${JSON.stringify(expectedConfigId)} | actuel: ${JSON.stringify(actualConfigId)} | match: ${actualConfigId === expectedConfigId}`);
|
|
156
|
+
|
|
157
|
+
if (actualConfigId !== expectedConfigId) {
|
|
158
|
+
conflicts.push({ path: mcpPath, expected: expectedConfigId, actual: actualConfigId });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(`[META-CONFIG SYNC] synced=${conflicts.length === 0}, conflicts=${JSON.stringify(conflicts)}`);
|
|
163
|
+
res.json({ synced: conflicts.length === 0, activeMetaConfigId: activeId, conflicts });
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error('[META-CONFIG SYNC] Erreur:', error.message);
|
|
166
|
+
res.status(500).json({ success: false, error: error.message });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* GET /proxy/meta-configs-ui
|
|
172
|
+
* Interface de gestion des méta-configurations
|
|
173
|
+
*/
|
|
174
|
+
router.get('/proxy/meta-configs-ui', requireAuth, (req, res) => {
|
|
175
|
+
const data = metaConfig.getAll();
|
|
176
|
+
const html = renderMetaConfigs(PROXY_NAME, data.metaConfigs, data.activeMetaConfig);
|
|
177
|
+
res.type('html').send(html);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
module.exports = router;
|