@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.
@@ -1,72 +1,20 @@
1
- import { randomBytes, timingSafeEqual } from "node:crypto";
1
+ import { generateSetupCode, safeEqual } from "../core/credentials.js";
2
2
  /**
3
3
  * Sessions de la console web.
4
4
  *
5
- * Le navigateur ne peut pas poser d'en-tête sur une navigation, et faire transiter le
6
- * jeton dans l'URL le dépose dans l'historique et les journaux d'accès du proxy. La
7
- * console s'ouvre donc sans jeton, le demande une fois dans un formulaire, et reçoit en
8
- * retour un identifiant de session opaque dans un cookie `HttpOnly`.
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 ne porte jamais le jeton lui-même : sa fuite ne donne qu'une session, à durée
11
- * limitée, que le serveur oublie au redémarrage. Il n'est honoré que sur `/admin` et
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 de vie d'une session : assez pour une journée de supervision, pas plus. */
17
- const SESSION_TTL_MS = 12 * 60 * 60 * 1000;
18
- export class ConsoleSessions {
19
- ttlMs;
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
  }
@@ -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 app = createApp({ store, config });
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(`auth: ${authMode} — required on every request`);
46
- // Le jeton partagé prouve qu'on fait partie de la flotte, pas qu'on est telle boîte :
47
- // `X-Agent-Id` n'est alors adossé à rien. Le dire ici, sinon la ligne au-dessus se lit
48
- // comme une garantie de cloisonnement qu'elle ne donne pas.
49
- if (config.agentTokens.size === 0) {
50
- logger.warn("shared token: no isolation between mailboxes — any holder can act as any address");
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)