@imenam/mcp-messenger 1.0.32 → 1.0.33
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/.env.example +12 -22
- package/README.md +825 -772
- package/dist/src/cli/reset-password.d.ts +12 -0
- package/dist/src/cli/reset-password.js +29 -0
- package/dist/src/cli/setup-mcp.d.ts +5 -1
- package/dist/src/cli/setup-mcp.js +15 -37
- package/dist/src/cli/watch-handoff.d.ts +3 -3
- package/dist/src/cli/watch-handoff.js +2 -2
- package/dist/src/cli/watch.d.ts +1 -1
- package/dist/src/cli/watch.js +3 -3
- package/dist/src/client/messenger-client.d.ts +3 -3
- package/dist/src/client/messenger-client.js +5 -6
- package/dist/src/config.d.ts +13 -8
- package/dist/src/config.js +32 -43
- package/dist/src/core/access-store.d.ts +124 -0
- package/dist/src/core/access-store.js +307 -0
- package/dist/src/core/credentials.d.ts +28 -0
- package/dist/src/core/credentials.js +75 -0
- package/dist/src/core/errors.d.ts +5 -0
- package/dist/src/core/errors.js +9 -0
- package/dist/src/core/store.d.ts +5 -5
- package/dist/src/core/store.js +8 -9
- package/dist/src/index.js +32 -17
- package/dist/src/mcp/start.js +26 -26
- package/dist/src/mcp/tools.js +2 -2
- package/dist/src/server/app.d.ts +10 -1
- package/dist/src/server/app.js +247 -69
- package/dist/src/server/console-session.d.ts +31 -21
- package/dist/src/server/console-session.js +46 -63
- package/dist/src/server/start.d.ts +3 -0
- package/dist/src/server/start.js +10 -13
- package/dist/src/server/ui/app.js +1380 -38
- package/dist/src/server/ui/index.html +192 -9
- package/dist/src/server/ui/styles.css +1144 -0
- package/package.json +2 -3
|
@@ -1,72 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generateSetupCode, safeEqual } from "../core/credentials.js";
|
|
2
2
|
/**
|
|
3
3
|
* Sessions de la console web.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* La console s'ouvre avec un nom d'utilisateur et un mot de passe, et reçoit en retour un
|
|
6
|
+
* identifiant de session opaque dans un cookie `HttpOnly`. Le serveur n'en garde que
|
|
7
|
+
* l'empreinte, en base : une session survit à un redémarrage, et se ferme depuis la page
|
|
8
|
+
* « Mon compte ».
|
|
9
9
|
*
|
|
10
|
-
* Le cookie
|
|
11
|
-
*
|
|
12
|
-
* `/ui` : l'API des agents reste sur le Bearer, un site tiers ne peut donc pas faire
|
|
13
|
-
* agir un agent à travers le navigateur de l'administrateur.
|
|
10
|
+
* Le cookie n'est honoré que sur `/admin` et `/ui` : l'API des agents reste sur la clé
|
|
11
|
+
* API, un site tiers ne peut donc pas faire agir un agent à travers le navigateur.
|
|
14
12
|
*/
|
|
15
13
|
export const CONSOLE_COOKIE = "messenger_console";
|
|
16
|
-
/** Durée
|
|
17
|
-
const SESSION_TTL_MS = 12 * 60 * 60 * 1000;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
now;
|
|
21
|
-
sessions = new Map();
|
|
22
|
-
constructor(ttlMs = SESSION_TTL_MS, now = Date.now) {
|
|
23
|
-
this.ttlMs = ttlMs;
|
|
24
|
-
this.now = now;
|
|
25
|
-
}
|
|
26
|
-
/** Ouvre une session et rend son identifiant, seul contenu du cookie. */
|
|
27
|
-
create() {
|
|
28
|
-
this.prune();
|
|
29
|
-
const id = randomBytes(32).toString("base64url");
|
|
30
|
-
this.sessions.set(id, { expiresAt: this.now() + this.ttlMs });
|
|
31
|
-
return id;
|
|
32
|
-
}
|
|
33
|
-
/** Vrai si l'identifiant désigne une session encore valide. */
|
|
34
|
-
has(id) {
|
|
35
|
-
if (!id)
|
|
36
|
-
return false;
|
|
37
|
-
// Parcours à temps constant : la Map compare ses clés en temps variable, ce qui
|
|
38
|
-
// laisserait deviner un identifiant octet par octet.
|
|
39
|
-
let match = null;
|
|
40
|
-
const presented = Buffer.from(id);
|
|
41
|
-
for (const [key, session] of this.sessions) {
|
|
42
|
-
const candidate = Buffer.from(key);
|
|
43
|
-
if (candidate.length === presented.length && timingSafeEqual(candidate, presented))
|
|
44
|
-
match = session;
|
|
45
|
-
}
|
|
46
|
-
if (!match)
|
|
47
|
-
return false;
|
|
48
|
-
if (match.expiresAt <= this.now()) {
|
|
49
|
-
this.sessions.delete(id);
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
revoke(id) {
|
|
55
|
-
if (id)
|
|
56
|
-
this.sessions.delete(id);
|
|
57
|
-
}
|
|
58
|
-
get size() {
|
|
59
|
-
this.prune();
|
|
60
|
-
return this.sessions.size;
|
|
61
|
-
}
|
|
62
|
-
prune() {
|
|
63
|
-
const now = this.now();
|
|
64
|
-
for (const [id, session] of this.sessions) {
|
|
65
|
-
if (session.expiresAt <= now)
|
|
66
|
-
this.sessions.delete(id);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
14
|
+
/** Durée d'une session ordinaire : assez pour une journée de supervision, pas plus. */
|
|
15
|
+
export const SESSION_TTL_MS = 12 * 60 * 60 * 1000;
|
|
16
|
+
/** Durée d'une session ouverte avec « rester connecté ». */
|
|
17
|
+
export const PERSISTENT_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
70
18
|
/** Identifiant de session porté par la requête, s'il y en a un. */
|
|
71
19
|
export function consoleSessionId(req) {
|
|
72
20
|
const header = req.headers.cookie;
|
|
@@ -83,3 +31,38 @@ export function consoleSessionId(req) {
|
|
|
83
31
|
}
|
|
84
32
|
return null;
|
|
85
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Code d'installation : il garde la création du premier compte.
|
|
36
|
+
*
|
|
37
|
+
* Tant qu'aucun compte n'existe, la console propose de le créer, et le premier visiteur
|
|
38
|
+
* venu le pourrait. Le code, écrit dans le journal du serveur au démarrage, prouve qu'on a
|
|
39
|
+
* accès à la machine. Il ne vit qu'en mémoire, change à chaque démarrage, et ne sert qu'une
|
|
40
|
+
* fois : dès qu'un compte existe, il n'y a plus rien à garder.
|
|
41
|
+
*/
|
|
42
|
+
export class SetupGate {
|
|
43
|
+
access;
|
|
44
|
+
code;
|
|
45
|
+
constructor(access) {
|
|
46
|
+
this.access = access;
|
|
47
|
+
this.code = access.hasUsers() ? null : generateSetupCode();
|
|
48
|
+
}
|
|
49
|
+
/** Le code à recopier, tant qu'aucun compte n'existe. */
|
|
50
|
+
get pendingCode() {
|
|
51
|
+
return this.access.hasUsers() ? null : this.code;
|
|
52
|
+
}
|
|
53
|
+
get required() {
|
|
54
|
+
return this.pendingCode !== null;
|
|
55
|
+
}
|
|
56
|
+
/** Vrai si le code présenté est le bon. Les tirets et la casse ne comptent pas. */
|
|
57
|
+
accepts(presented) {
|
|
58
|
+
const code = this.pendingCode;
|
|
59
|
+
if (!code)
|
|
60
|
+
return false;
|
|
61
|
+
const normalize = (value) => value.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
62
|
+
return safeEqual(normalize(presented), normalize(code));
|
|
63
|
+
}
|
|
64
|
+
/** Le code a servi : il ne vaut plus rien. */
|
|
65
|
+
consume() {
|
|
66
|
+
this.code = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { Server } from "node:http";
|
|
2
|
+
import { SetupGate } from "./console-session.js";
|
|
2
3
|
import { type ServerConfig } from "../config.js";
|
|
3
4
|
import { MessageStore } from "../core/store.js";
|
|
4
5
|
export interface RunningServer {
|
|
5
6
|
server: Server;
|
|
6
7
|
store: MessageStore;
|
|
7
8
|
config: ServerConfig;
|
|
9
|
+
/** Garde de la création du premier compte : son code est écrit dans le journal. */
|
|
10
|
+
setup: SetupGate;
|
|
8
11
|
url: string;
|
|
9
12
|
close: () => Promise<void>;
|
|
10
13
|
}
|
package/dist/src/server/start.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createApp } from "./app.js";
|
|
2
|
+
import { SetupGate } from "./console-session.js";
|
|
2
3
|
import { loadServerConfig } from "../config.js";
|
|
3
4
|
import { MessageStore } from "../core/store.js";
|
|
4
5
|
import { logger } from "../logger.js";
|
|
@@ -6,7 +7,8 @@ import { APP_VERSION } from "../version.js";
|
|
|
6
7
|
/** Démarre la boîte aux lettres. `port: 0` laisse l'OS choisir (utilisé par les tests). */
|
|
7
8
|
export async function startServer(config = loadServerConfig()) {
|
|
8
9
|
const store = new MessageStore(config.dbPath);
|
|
9
|
-
const
|
|
10
|
+
const setup = new SetupGate(store.access);
|
|
11
|
+
const app = createApp({ store, config, setup });
|
|
10
12
|
const server = await new Promise((resolve, reject) => {
|
|
11
13
|
const httpServer = app.listen(config.port, config.host, () => resolve(httpServer));
|
|
12
14
|
httpServer.once("error", reject);
|
|
@@ -31,26 +33,21 @@ export async function startServer(config = loadServerConfig()) {
|
|
|
31
33
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
32
34
|
store.close();
|
|
33
35
|
};
|
|
34
|
-
return { server, store, config, url, close };
|
|
36
|
+
return { server, store, config, setup, url, close };
|
|
35
37
|
}
|
|
36
38
|
/** Point d'entrée du mode serveur (`mcp-messenger --server`). */
|
|
37
39
|
export async function runServerMode() {
|
|
38
40
|
const config = loadServerConfig();
|
|
39
41
|
const running = await startServer(config);
|
|
40
|
-
const authMode = config.agentTokens.size > 0
|
|
41
|
-
? `per-agent tokens (${config.agentTokens.size})${config.token ? " + shared token" : ""}`
|
|
42
|
-
: "shared token";
|
|
43
42
|
logger.info(`mcp-messenger v${APP_VERSION} listening on ${running.url} (loopback only)`);
|
|
44
43
|
logger.info(`database: ${config.dbPath}`);
|
|
45
|
-
logger.info(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
logger.warn(
|
|
51
|
-
logger.warn("set MESSENGER_AGENT_TOKENS (one token per agent) to compartmentalize");
|
|
44
|
+
logger.info("auth: API keys on every agent request, console account on the web console");
|
|
45
|
+
logger.info(`web console: ${running.url}/ui/`);
|
|
46
|
+
const code = running.setup.pendingCode;
|
|
47
|
+
if (code) {
|
|
48
|
+
logger.warn("no console account yet — open the web console to create it");
|
|
49
|
+
logger.warn(`setup code: ${code}`);
|
|
52
50
|
}
|
|
53
|
-
logger.info(`web console: ${running.url}/ui/ (asks for MESSENGER_ADMIN_TOKEN once)`);
|
|
54
51
|
let shuttingDown = false;
|
|
55
52
|
const shutdown = (signal) => {
|
|
56
53
|
if (shuttingDown)
|