@imenam/mcp-messenger 1.0.31 → 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.
Files changed (36) hide show
  1. package/.env.example +12 -22
  2. package/README.md +825 -764
  3. package/dist/src/cli/reset-password.d.ts +12 -0
  4. package/dist/src/cli/reset-password.js +29 -0
  5. package/dist/src/cli/setup-mcp.d.ts +5 -1
  6. package/dist/src/cli/setup-mcp.js +15 -37
  7. package/dist/src/cli/watch-handoff.d.ts +3 -3
  8. package/dist/src/cli/watch-handoff.js +2 -2
  9. package/dist/src/cli/watch.d.ts +1 -1
  10. package/dist/src/cli/watch.js +3 -3
  11. package/dist/src/client/messenger-client.d.ts +3 -3
  12. package/dist/src/client/messenger-client.js +5 -6
  13. package/dist/src/config.d.ts +13 -8
  14. package/dist/src/config.js +32 -43
  15. package/dist/src/core/access-store.d.ts +124 -0
  16. package/dist/src/core/access-store.js +307 -0
  17. package/dist/src/core/credentials.d.ts +28 -0
  18. package/dist/src/core/credentials.js +75 -0
  19. package/dist/src/core/errors.d.ts +5 -0
  20. package/dist/src/core/errors.js +9 -0
  21. package/dist/src/core/store.d.ts +10 -5
  22. package/dist/src/core/store.js +18 -9
  23. package/dist/src/core/types.d.ts +1 -1
  24. package/dist/src/index.js +32 -17
  25. package/dist/src/mcp/start.js +26 -26
  26. package/dist/src/mcp/tools.js +2 -2
  27. package/dist/src/server/app.d.ts +10 -1
  28. package/dist/src/server/app.js +259 -69
  29. package/dist/src/server/console-session.d.ts +31 -21
  30. package/dist/src/server/console-session.js +46 -63
  31. package/dist/src/server/start.d.ts +3 -0
  32. package/dist/src/server/start.js +10 -13
  33. package/dist/src/server/ui/app.js +3997 -2464
  34. package/dist/src/server/ui/index.html +467 -284
  35. package/dist/src/server/ui/styles.css +1263 -18
  36. package/package.json +2 -3
@@ -0,0 +1,307 @@
1
+ import { API_KEY_PREFIX, digestSecret } from "./credentials.js";
2
+ import { newId } from "./ids.js";
3
+ import { MessengerError } from "./errors.js";
4
+ /** Une date de dernier usage n'est réécrite qu'au-delà de ce délai : pas une écriture par requête. */
5
+ const TOUCH_INTERVAL_MS = 60_000;
6
+ const DAY_MS = 24 * 60 * 60 * 1000;
7
+ const KEY_COLUMNS = `k.id, k.name, k.prefix, k.suffix, k.agent_name, k.created_at, k.created_by, k.expires_at,
8
+ k.lifetime_days, k.revoked_at, k.last_used_at, k.last_used_ip,
9
+ (SELECT COUNT(*) FROM mailbox_keys mk JOIN agents a ON a.id = mk.mailbox WHERE mk.api_key_id = k.id) AS mailboxes`;
10
+ export class AccessStore {
11
+ db;
12
+ now;
13
+ constructor(db, now = () => Date.now()) {
14
+ this.db = db;
15
+ this.now = now;
16
+ this.db.exec(`
17
+ CREATE TABLE IF NOT EXISTS users (
18
+ id TEXT PRIMARY KEY,
19
+ username TEXT NOT NULL UNIQUE COLLATE NOCASE,
20
+ password_hash TEXT NOT NULL,
21
+ created_at TEXT NOT NULL,
22
+ updated_at TEXT
23
+ );
24
+
25
+ CREATE TABLE IF NOT EXISTS console_sessions (
26
+ id TEXT PRIMARY KEY,
27
+ secret_hash TEXT NOT NULL UNIQUE,
28
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
29
+ created_at TEXT NOT NULL,
30
+ last_seen_at TEXT NOT NULL,
31
+ expires_at TEXT NOT NULL,
32
+ persistent INTEGER NOT NULL DEFAULT 0,
33
+ user_agent TEXT,
34
+ ip TEXT
35
+ );
36
+
37
+ CREATE TABLE IF NOT EXISTS api_keys (
38
+ id TEXT PRIMARY KEY,
39
+ name TEXT NOT NULL,
40
+ secret_hash TEXT NOT NULL UNIQUE,
41
+ prefix TEXT NOT NULL,
42
+ suffix TEXT NOT NULL,
43
+ agent_name TEXT,
44
+ created_at TEXT NOT NULL,
45
+ created_by TEXT,
46
+ expires_at TEXT,
47
+ lifetime_days INTEGER,
48
+ revoked_at TEXT,
49
+ last_used_at TEXT,
50
+ last_used_ip TEXT
51
+ );
52
+
53
+ CREATE TABLE IF NOT EXISTS mailbox_keys (
54
+ mailbox TEXT PRIMARY KEY,
55
+ api_key_id TEXT NOT NULL,
56
+ opened_at TEXT NOT NULL
57
+ );
58
+
59
+ CREATE INDEX IF NOT EXISTS idx_mailbox_keys_key ON mailbox_keys (api_key_id);
60
+ CREATE INDEX IF NOT EXISTS idx_console_sessions_user ON console_sessions (user_id);
61
+ `);
62
+ }
63
+ iso(offsetMs = 0) {
64
+ return new Date(this.now() + offsetMs).toISOString();
65
+ }
66
+ // --- Comptes --------------------------------------------------------------
67
+ hasUsers() {
68
+ return Boolean(this.db.prepare("SELECT 1 FROM users LIMIT 1").get());
69
+ }
70
+ createUser(username, passwordHash) {
71
+ const id = newId("usr");
72
+ try {
73
+ this.db
74
+ .prepare("INSERT INTO users (id, username, password_hash, created_at) VALUES (?, ?, ?, ?)")
75
+ .run(id, username, passwordHash, this.iso());
76
+ }
77
+ catch (error) {
78
+ if (isUniqueViolation(error))
79
+ throw new MessengerError(`username already taken: ${username}`, 409);
80
+ throw error;
81
+ }
82
+ return this.getUser(id);
83
+ }
84
+ getUser(id) {
85
+ return (this.db.prepare("SELECT id, username, created_at, updated_at FROM users WHERE id = ?").get(id) ?? null);
86
+ }
87
+ findUserByName(username) {
88
+ return this.db.prepare("SELECT * FROM users WHERE username = ?").get(username) ?? null;
89
+ }
90
+ listUsers() {
91
+ return this.db.prepare("SELECT id, username, created_at, updated_at FROM users ORDER BY created_at").all();
92
+ }
93
+ getPasswordHash(id) {
94
+ const row = this.db.prepare("SELECT password_hash FROM users WHERE id = ?").get(id);
95
+ return row?.password_hash ?? null;
96
+ }
97
+ renameUser(id, username) {
98
+ try {
99
+ const result = this.db
100
+ .prepare("UPDATE users SET username = ?, updated_at = ? WHERE id = ?")
101
+ .run(username, this.iso(), id);
102
+ if (result.changes === 0)
103
+ throw new MessengerError(`user not found: ${id}`, 404);
104
+ }
105
+ catch (error) {
106
+ if (isUniqueViolation(error))
107
+ throw new MessengerError(`username already taken: ${username}`, 409);
108
+ throw error;
109
+ }
110
+ return this.getUser(id);
111
+ }
112
+ setPassword(id, passwordHash) {
113
+ const result = this.db
114
+ .prepare("UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?")
115
+ .run(passwordHash, this.iso(), id);
116
+ if (result.changes === 0)
117
+ throw new MessengerError(`user not found: ${id}`, 404);
118
+ }
119
+ // --- Sessions de la console -----------------------------------------------
120
+ createSession(input) {
121
+ this.pruneSessions();
122
+ const id = newId("ses");
123
+ const now = this.iso();
124
+ this.db
125
+ .prepare(`INSERT INTO console_sessions (id, secret_hash, user_id, created_at, last_seen_at, expires_at, persistent, user_agent, ip)
126
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
127
+ .run(id, digestSecret(input.secret), input.userId, now, now, this.iso(input.ttlMs), input.persistent ? 1 : 0, input.userAgent?.slice(0, 300) ?? null, input.ip);
128
+ return this.getSession(id);
129
+ }
130
+ /** Session encore valide désignée par le contenu du cookie, avec son compte. */
131
+ findSession(secret, seen = { ip: null }) {
132
+ if (!secret)
133
+ return null;
134
+ const row = this.db
135
+ .prepare("SELECT * FROM console_sessions WHERE secret_hash = ? AND expires_at > ?")
136
+ .get(digestSecret(secret), this.iso());
137
+ if (!row)
138
+ return null;
139
+ const user = this.getUser(row.user_id);
140
+ if (!user)
141
+ return null;
142
+ if (this.now() - Date.parse(row.last_seen_at) > TOUCH_INTERVAL_MS) {
143
+ const now = this.iso();
144
+ this.db
145
+ .prepare("UPDATE console_sessions SET last_seen_at = ?, ip = COALESCE(?, ip) WHERE id = ?")
146
+ .run(now, seen.ip, row.id);
147
+ row.last_seen_at = now;
148
+ if (seen.ip)
149
+ row.ip = seen.ip;
150
+ }
151
+ return { session: toSession(row), user };
152
+ }
153
+ getSession(id) {
154
+ const row = this.db.prepare("SELECT * FROM console_sessions WHERE id = ?").get(id);
155
+ return row ? toSession(row) : null;
156
+ }
157
+ listSessions(userId) {
158
+ this.pruneSessions();
159
+ return this.db
160
+ .prepare("SELECT * FROM console_sessions WHERE user_id = ? ORDER BY last_seen_at DESC")
161
+ .all(userId).map(toSession);
162
+ }
163
+ revokeSession(userId, id) {
164
+ const result = this.db.prepare("DELETE FROM console_sessions WHERE id = ? AND user_id = ?").run(id, userId);
165
+ if (result.changes === 0)
166
+ throw new MessengerError(`session not found: ${id}`, 404);
167
+ }
168
+ revokeSessionBySecret(secret) {
169
+ if (secret)
170
+ this.db.prepare("DELETE FROM console_sessions WHERE secret_hash = ?").run(digestSecret(secret));
171
+ }
172
+ /** Ferme les sessions du compte sauf `keepId` (null : toutes). Rend le nombre fermé. */
173
+ revokeOtherSessions(userId, keepId) {
174
+ return this.db
175
+ .prepare("DELETE FROM console_sessions WHERE user_id = ? AND id != ?")
176
+ .run(userId, keepId ?? "").changes;
177
+ }
178
+ pruneSessions() {
179
+ this.db.prepare("DELETE FROM console_sessions WHERE expires_at <= ?").run(this.iso());
180
+ }
181
+ // --- Clés API -------------------------------------------------------------
182
+ createApiKey(input) {
183
+ const id = newId("key");
184
+ const visible = input.secret.startsWith(API_KEY_PREFIX) ? API_KEY_PREFIX.length : 0;
185
+ this.db
186
+ .prepare(`INSERT INTO api_keys (id, name, secret_hash, prefix, suffix, agent_name, created_at, created_by, expires_at, lifetime_days)
187
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
188
+ .run(id, input.name, digestSecret(input.secret), input.secret.slice(0, visible + 4), input.secret.slice(-4), input.agentName, this.iso(), input.createdBy, input.lifetimeDays === null ? null : this.iso(input.lifetimeDays * DAY_MS), input.lifetimeDays);
189
+ return this.getApiKey(id);
190
+ }
191
+ listApiKeys() {
192
+ return this.db.prepare(`SELECT ${KEY_COLUMNS} FROM api_keys k ORDER BY k.created_at DESC`).all().map((row) => this.toApiKey(row));
193
+ }
194
+ getApiKey(id) {
195
+ const row = this.db.prepare(`SELECT ${KEY_COLUMNS} FROM api_keys k WHERE k.id = ?`).get(id);
196
+ return row ? this.toApiKey(row) : null;
197
+ }
198
+ requireKey(id) {
199
+ const key = this.getApiKey(id);
200
+ if (!key)
201
+ throw new MessengerError(`API key not found: ${id}`, 404);
202
+ return key;
203
+ }
204
+ renameApiKey(id, name) {
205
+ this.requireKey(id);
206
+ this.db.prepare("UPDATE api_keys SET name = ? WHERE id = ?").run(name, id);
207
+ return this.getApiKey(id);
208
+ }
209
+ /**
210
+ * Repousse l'expiration de `days` jours, comptés depuis l'échéance encore à venir ou,
211
+ * pour une clé déjà expirée, depuis maintenant. `null` retire toute échéance.
212
+ */
213
+ extendApiKey(id, days) {
214
+ const key = this.requireKey(id);
215
+ if (key.status === "revoked")
216
+ throw new MessengerError("a revoked key cannot be extended", 409);
217
+ const base = key.expires_at ? Math.max(this.now(), Date.parse(key.expires_at)) : this.now();
218
+ const expiresAt = days === null ? null : new Date(base + days * DAY_MS).toISOString();
219
+ this.db.prepare("UPDATE api_keys SET expires_at = ? WHERE id = ?").run(expiresAt, id);
220
+ return this.getApiKey(id);
221
+ }
222
+ revokeApiKey(id) {
223
+ const key = this.requireKey(id);
224
+ if (key.status === "revoked")
225
+ throw new MessengerError("this key is already revoked", 409);
226
+ this.db.prepare("UPDATE api_keys SET revoked_at = ? WHERE id = ?").run(this.iso(), id);
227
+ return this.getApiKey(id);
228
+ }
229
+ /**
230
+ * Remplace une clé : la nouvelle reprend le nom, l'agent et la durée de validité de
231
+ * l'ancienne ; l'ancienne reste acceptée `graceMs` encore, au plus jusqu'à son échéance.
232
+ */
233
+ rotateApiKey(id, secret, createdBy, graceMs) {
234
+ const old = this.requireKey(id);
235
+ if (old.status !== "active")
236
+ throw new MessengerError(`only an active key can be replaced (this one is ${old.status})`, 409);
237
+ return this.db.transaction(() => {
238
+ const key = this.createApiKey({
239
+ name: old.name,
240
+ agentName: old.agent_name,
241
+ lifetimeDays: old.lifetime_days,
242
+ createdBy,
243
+ secret,
244
+ });
245
+ const grace = this.iso(graceMs);
246
+ const expiresAt = old.expires_at && old.expires_at < grace ? old.expires_at : grace;
247
+ this.db.prepare("UPDATE api_keys SET expires_at = ? WHERE id = ?").run(expiresAt, id);
248
+ return { key, replaced: this.getApiKey(id) };
249
+ })();
250
+ }
251
+ /** Clé active désignée par le secret présenté, ou null. Note son dernier usage. */
252
+ authenticateApiKey(secret, ip) {
253
+ if (!secret)
254
+ return null;
255
+ const row = this.db
256
+ .prepare(`SELECT ${KEY_COLUMNS} FROM api_keys k WHERE k.secret_hash = ?`)
257
+ .get(digestSecret(secret));
258
+ if (!row)
259
+ return null;
260
+ const key = this.toApiKey(row);
261
+ if (key.status !== "active")
262
+ return null;
263
+ if (!key.last_used_at || this.now() - Date.parse(key.last_used_at) > TOUCH_INTERVAL_MS || key.last_used_ip !== ip) {
264
+ const now = this.iso();
265
+ this.db.prepare("UPDATE api_keys SET last_used_at = ?, last_used_ip = ? WHERE id = ?").run(now, ip, key.id);
266
+ key.last_used_at = now;
267
+ key.last_used_ip = ip;
268
+ }
269
+ return key;
270
+ }
271
+ /** Retient la clé qui a ouvert ou repris une boîte : la dernière fait foi. */
272
+ recordMailboxKey(mailbox, apiKeyId) {
273
+ this.db
274
+ .prepare(`INSERT INTO mailbox_keys (mailbox, api_key_id, opened_at) VALUES (?, ?, ?)
275
+ ON CONFLICT (mailbox) DO UPDATE SET api_key_id = excluded.api_key_id, opened_at = excluded.opened_at
276
+ WHERE mailbox_keys.api_key_id != excluded.api_key_id`)
277
+ .run(mailbox, apiKeyId, this.iso());
278
+ }
279
+ mailboxesForKey(id) {
280
+ this.requireKey(id);
281
+ const rows = this.db
282
+ .prepare(`SELECT a.id, a.agent_name, a.display_name,
283
+ MAX(COALESCE(a.last_seen_at, ''),
284
+ COALESCE((SELECT MAX(d.created_at) FROM deliveries d WHERE d.agent_id = a.id), '')) AS last_activity_at
285
+ FROM mailbox_keys mk
286
+ JOIN agents a ON a.id = mk.mailbox
287
+ WHERE mk.api_key_id = ?
288
+ ORDER BY last_activity_at DESC, a.id`)
289
+ .all(id);
290
+ return rows.map((row) => ({ ...row, last_activity_at: row.last_activity_at || null }));
291
+ }
292
+ toApiKey(row) {
293
+ const status = row.revoked_at
294
+ ? "revoked"
295
+ : row.expires_at && Date.parse(row.expires_at) <= this.now()
296
+ ? "expired"
297
+ : "active";
298
+ return { ...row, status };
299
+ }
300
+ }
301
+ function toSession(row) {
302
+ const { secret_hash: _secret, ...rest } = row;
303
+ return { ...rest, persistent: Boolean(row.persistent) };
304
+ }
305
+ function isUniqueViolation(error) {
306
+ return typeof error === "object" && error !== null && error.code === "SQLITE_CONSTRAINT_UNIQUE";
307
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Secrets de l'authentification : mots de passe, clés API, sessions, code d'installation.
3
+ *
4
+ * Rien de ce qui est fabriqué ici n'est stocké en clair. Un mot de passe est haché par
5
+ * scrypt, lent et salé, parce qu'il est choisi par un humain et donc devinable. Une clé ou
6
+ * un identifiant de session sort de 256 bits d'aléa : une empreinte SHA-256 suffit à le
7
+ * retrouver sans le conserver, et se calcule à chaque requête sans coût.
8
+ */
9
+ /** Préfixe des clés API : il les distingue des adresses (`box_`) et des messages (`msg_`). */
10
+ export declare const API_KEY_PREFIX = "mk_";
11
+ /** Longueur minimale d'un mot de passe. */
12
+ export declare const PASSWORD_MIN_LENGTH = 12;
13
+ /** Empreinte d'un mot de passe : `scrypt$N$r$p$sel$hash`, sel et hash en base64url. */
14
+ export declare function hashPassword(password: string): Promise<string>;
15
+ /** Vrai si le mot de passe correspond à l'empreinte. Toute empreinte illisible est un refus. */
16
+ export declare function verifyPassword(password: string, stored: string): Promise<boolean>;
17
+ /** Empreinte d'un secret à haute entropie (clé API, identifiant de session). */
18
+ export declare function digestSecret(secret: string): string;
19
+ /** Clé API neuve : le préfixe, puis 256 bits d'aléa. */
20
+ export declare function generateApiKey(): string;
21
+ /** Identifiant de session de la console, seul contenu de son cookie. */
22
+ export declare function generateSessionSecret(): string;
23
+ /** Code d'installation : trois groupes de quatre caractères, à recopier depuis le journal. */
24
+ export declare function generateSetupCode(): string;
25
+ /** Mot de passe temporaire donné par `--reset-password`. */
26
+ export declare function generatePassword(): string;
27
+ /** Comparaison à temps constant de deux chaînes. */
28
+ export declare function safeEqual(a: string, b: string): boolean;
@@ -0,0 +1,75 @@
1
+ import { createHash, randomBytes, randomInt, scrypt, timingSafeEqual } from "node:crypto";
2
+ /**
3
+ * Secrets de l'authentification : mots de passe, clés API, sessions, code d'installation.
4
+ *
5
+ * Rien de ce qui est fabriqué ici n'est stocké en clair. Un mot de passe est haché par
6
+ * scrypt, lent et salé, parce qu'il est choisi par un humain et donc devinable. Une clé ou
7
+ * un identifiant de session sort de 256 bits d'aléa : une empreinte SHA-256 suffit à le
8
+ * retrouver sans le conserver, et se calcule à chaque requête sans coût.
9
+ */
10
+ /** Préfixe des clés API : il les distingue des adresses (`box_`) et des messages (`msg_`). */
11
+ export const API_KEY_PREFIX = "mk_";
12
+ /** Longueur minimale d'un mot de passe. */
13
+ export const PASSWORD_MIN_LENGTH = 12;
14
+ const SCRYPT = { N: 16384, r: 8, p: 1, keylen: 64 };
15
+ function scryptAsync(password, salt, keylen, options) {
16
+ return new Promise((resolve, reject) => {
17
+ scrypt(password, salt, keylen, options, (error, derived) => (error ? reject(error) : resolve(derived)));
18
+ });
19
+ }
20
+ /** Empreinte d'un mot de passe : `scrypt$N$r$p$sel$hash`, sel et hash en base64url. */
21
+ export async function hashPassword(password) {
22
+ const salt = randomBytes(16);
23
+ const derived = await scryptAsync(password, salt, SCRYPT.keylen, { N: SCRYPT.N, r: SCRYPT.r, p: SCRYPT.p });
24
+ return ["scrypt", SCRYPT.N, SCRYPT.r, SCRYPT.p, salt.toString("base64url"), derived.toString("base64url")].join("$");
25
+ }
26
+ /** Vrai si le mot de passe correspond à l'empreinte. Toute empreinte illisible est un refus. */
27
+ export async function verifyPassword(password, stored) {
28
+ const [scheme, n, r, p, salt, hash] = stored.split("$");
29
+ if (scheme !== "scrypt" || !salt || !hash)
30
+ return false;
31
+ const expected = Buffer.from(hash, "base64url");
32
+ const derived = await scryptAsync(password, Buffer.from(salt, "base64url"), expected.length, {
33
+ N: Number(n),
34
+ r: Number(r),
35
+ p: Number(p),
36
+ });
37
+ return derived.length === expected.length && timingSafeEqual(derived, expected);
38
+ }
39
+ /** Empreinte d'un secret à haute entropie (clé API, identifiant de session). */
40
+ export function digestSecret(secret) {
41
+ return createHash("sha256").update(secret).digest("hex");
42
+ }
43
+ /** Clé API neuve : le préfixe, puis 256 bits d'aléa. */
44
+ export function generateApiKey() {
45
+ return `${API_KEY_PREFIX}${randomBytes(32).toString("base64url")}`;
46
+ }
47
+ /** Identifiant de session de la console, seul contenu de son cookie. */
48
+ export function generateSessionSecret() {
49
+ return randomBytes(32).toString("base64url");
50
+ }
51
+ /** Lettres et chiffres sans les caractères qui se confondent à la lecture (0/O, 1/I/L). */
52
+ const SETUP_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
53
+ /** Code d'installation : trois groupes de quatre caractères, à recopier depuis le journal. */
54
+ export function generateSetupCode() {
55
+ const groups = [];
56
+ for (let g = 0; g < 3; g += 1) {
57
+ let group = "";
58
+ for (let i = 0; i < 4; i += 1)
59
+ group += SETUP_ALPHABET[randomInt(SETUP_ALPHABET.length)];
60
+ groups.push(group);
61
+ }
62
+ return groups.join("-");
63
+ }
64
+ /** Mot de passe temporaire donné par `--reset-password`. */
65
+ export function generatePassword() {
66
+ return randomBytes(18).toString("base64url");
67
+ }
68
+ /** Comparaison à temps constant de deux chaînes. */
69
+ export function safeEqual(a, b) {
70
+ const bufA = Buffer.from(a);
71
+ const bufB = Buffer.from(b);
72
+ if (bufA.length !== bufB.length)
73
+ return false;
74
+ return timingSafeEqual(bufA, bufB);
75
+ }
@@ -0,0 +1,5 @@
1
+ /** Erreur métier : traduite en 4xx par la couche HTTP, en message d'outil côté MCP. */
2
+ export declare class MessengerError extends Error {
3
+ readonly status: number;
4
+ constructor(message: string, status?: number);
5
+ }
@@ -0,0 +1,9 @@
1
+ /** Erreur métier : traduite en 4xx par la couche HTTP, en message d'outil côté MCP. */
2
+ export class MessengerError extends Error {
3
+ status;
4
+ constructor(message, status = 400) {
5
+ super(message);
6
+ this.status = status;
7
+ this.name = "MessengerError";
8
+ }
9
+ }
@@ -1,10 +1,8 @@
1
1
  import { EventEmitter } from "node:events";
2
+ import { AccessStore } from "./access-store.js";
3
+ import { MessengerError } from "./errors.js";
2
4
  import { type AdminThreadSummary, type Agent, type AgentDirectoryEntry, type AttachmentInfo, type DeliveryRole, type FolderInfo, type LinkedMailbox, type ListMessagesOptions, type ListThreadsOptions, type MailboxLink, type MailboxStats, type Message, type MessageSummary, type Priority, type SendMessageInput, type SendMessageResult, type ThreadInfo, type ThreadSummary } from "./types.js";
3
- /** Erreur métier : traduite en 4xx par la couche HTTP, en message d'outil côté MCP. */
4
- export declare class MessengerError extends Error {
5
- readonly status: number;
6
- constructor(message: string, status?: number);
7
- }
5
+ export { MessengerError };
8
6
  /** Contenu d'une pièce jointe, avec ses métadonnées. */
9
7
  export interface AttachmentContent extends AttachmentInfo {
10
8
  message_id: string;
@@ -42,6 +40,8 @@ export interface StoreEvents {
42
40
  */
43
41
  export declare class MessageStore extends EventEmitter<StoreEvents> {
44
42
  private readonly db;
43
+ /** Comptes, sessions de la console et clés API, dans la même base. */
44
+ readonly access: AccessStore;
45
45
  constructor(dbPath: string);
46
46
  private migrate;
47
47
  close(): void;
@@ -51,6 +51,11 @@ export declare class MessageStore extends EventEmitter<StoreEvents> {
51
51
  * de l'agent sans toucher au nom de la boîte, et `rename_mailbox` fait l'inverse.
52
52
  */
53
53
  ensureAgent(id: string, opts?: EnsureAgentOptions): Agent;
54
+ /**
55
+ * Nomme une boîte depuis la console. Le nom remplace celui en place, quel qu'en soit
56
+ * l'auteur ; null l'efface. La boîte doit exister, et sa date de passage ne bouge pas.
57
+ */
58
+ setMailboxName(id: string, name: string | null): Agent;
54
59
  touchAgent(id: string): void;
55
60
  getAgent(id: string): Agent | null;
56
61
  /**
@@ -3,17 +3,11 @@ import { EventEmitter } from "node:events";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { MAX_ATTACHMENTS, MAX_ATTACHMENTS_TOTAL_BYTES, MAX_ATTACHMENT_BYTES, formatBytes } from "./attachments.js";
6
+ import { AccessStore } from "./access-store.js";
7
+ import { MessengerError } from "./errors.js";
6
8
  import { newId } from "./ids.js";
7
9
  import { DELIVERY_ROLES, SYSTEM_FOLDERS, } from "./types.js";
8
- /** Erreur métier : traduite en 4xx par la couche HTTP, en message d'outil côté MCP. */
9
- export class MessengerError extends Error {
10
- status;
11
- constructor(message, status = 400) {
12
- super(message);
13
- this.status = status;
14
- this.name = "MessengerError";
15
- }
16
- }
10
+ export { MessengerError };
17
11
  const PREVIEW_LENGTH = 160;
18
12
  const MAX_LIMIT = 100;
19
13
  const FOLDER_NAME_RE = /^[a-z0-9][a-z0-9 _-]{0,39}$/;
@@ -57,6 +51,8 @@ const ADMIN_DELIVERY = `'to' AS role,
57
51
  */
58
52
  export class MessageStore extends EventEmitter {
59
53
  db;
54
+ /** Comptes, sessions de la console et clés API, dans la même base. */
55
+ access;
60
56
  constructor(dbPath) {
61
57
  super();
62
58
  if (dbPath !== ":memory:") {
@@ -67,6 +63,7 @@ export class MessageStore extends EventEmitter {
67
63
  if (dbPath !== ":memory:")
68
64
  this.db.pragma("journal_mode = WAL");
69
65
  this.migrate();
66
+ this.access = new AccessStore(this.db);
70
67
  }
71
68
  migrate() {
72
69
  this.db.exec(`
@@ -205,6 +202,16 @@ export class MessageStore extends EventEmitter {
205
202
  }
206
203
  return this.getAgent(id);
207
204
  }
205
+ /**
206
+ * Nomme une boîte depuis la console. Le nom remplace celui en place, quel qu'en soit
207
+ * l'auteur ; null l'efface. La boîte doit exister, et sa date de passage ne bouge pas.
208
+ */
209
+ setMailboxName(id, name) {
210
+ const result = this.db.prepare("UPDATE agents SET display_name = ? WHERE id = ?").run(name, id);
211
+ if (result.changes === 0)
212
+ throw new MessengerError(`mailbox not found: ${id}`, 404);
213
+ return this.getAgent(id);
214
+ }
208
215
  touchAgent(id) {
209
216
  this.db.prepare("UPDATE agents SET last_seen_at = ? WHERE id = ?").run(new Date().toISOString(), id);
210
217
  }
@@ -713,10 +720,12 @@ export class MessageStore extends EventEmitter {
713
720
  const dropFolders = this.db.prepare("DELETE FROM folders WHERE agent_id = ?");
714
721
  const dropAgent = this.db.prepare("DELETE FROM agents WHERE id = ?");
715
722
  const dropLinks = this.db.prepare("DELETE FROM links WHERE mailbox_a = ? OR mailbox_b = ?");
723
+ const dropKey = this.db.prepare("DELETE FROM mailbox_keys WHERE mailbox = ?");
716
724
  for (const id of ids) {
717
725
  deliveries += dropDeliveries.run(id).changes;
718
726
  dropFolders.run(id);
719
727
  dropLinks.run(id, id);
728
+ dropKey.run(id);
720
729
  mailboxes += dropAgent.run(id).changes;
721
730
  }
722
731
  this.dropOrphanMessages();
@@ -11,7 +11,7 @@ export type DeliveryRole = (typeof DELIVERY_ROLES)[number];
11
11
  * - l'agent (`agent_name`, `agent_description`) : d'où il vient, typiquement le projet.
12
12
  * Posé par la configuration à l'enregistrement, stable pour toute la vie de la boîte.
13
13
  * - la boîte (`display_name`, `description`) : de quoi cette session s'occupe. Vide au
14
- * départ, posé par l'agent lui-même (`rename_mailbox`), et libre de changer.
14
+ * départ, posé par l'agent (`rename_mailbox`) ou depuis la console, et libre de changer.
15
15
  * Le nom de l'agent seul ne distingue pas deux sessions du même projet ; le nom de la boîte
16
16
  * seul ne dit pas où l'agent travaille. La console et l'annuaire montrent les deux.
17
17
  */
package/dist/src/index.js CHANGED
@@ -11,18 +11,28 @@ Usage:
11
11
  mcp-messenger --watch <mailbox>
12
12
  Print one line per incoming message, forever. Meant to be
13
13
  run by the agent's host as a background watcher, not by hand.
14
- mcp-messenger --claude-setup-mcp [--force]
14
+ mcp-messenger --claude-setup-mcp [--server-url <url>] [--api-key <key>] [--force]
15
15
  Write the messenger entry into ./.mcp.json.
16
- mcp-messenger --generate-token
17
- Print a fresh random token for MESSENGER_TOKEN.
16
+ mcp-messenger --reset-password
17
+ Give the web console account a new random password and
18
+ close all its sessions. Run it on the server machine.
18
19
  mcp-messenger --version Print the version.
19
20
  mcp-messenger --help Print this help.
20
21
 
21
- MCP mode requires MESSENGER_SERVER_URL and MESSENGER_TOKEN; each session gets its own
22
- mailbox unless MESSENGER_AGENT_ID pins a fixed address.
23
- Server mode requires MESSENGER_TOKEN (or MESSENGER_AGENT_TOKENS) and reads MESSENGER_PORT
24
- and MESSENGER_DB_PATH. It always listens on 127.0.0.1.
22
+ MCP mode requires MESSENGER_SERVER_URL and MESSENGER_API_KEY (an API key created in the
23
+ web console); each session gets its own mailbox unless MESSENGER_AGENT_ID pins a fixed
24
+ address.
25
+ Server mode reads MESSENGER_PORT and MESSENGER_DB_PATH and always listens on 127.0.0.1.
26
+ At the first start it prints a setup code: open /ui/ to create the console account.
25
27
  See .env.example for the full list.`;
28
+ /** Valeur qui suit une option (`--api-key mk_…`), ou null si l'option est absente ou vide. */
29
+ function optionValue(args, name) {
30
+ const index = args.indexOf(name);
31
+ if (index === -1)
32
+ return null;
33
+ const value = args[index + 1];
34
+ return value && !value.startsWith("--") ? value : null;
35
+ }
26
36
  async function main() {
27
37
  const args = process.argv.slice(2);
28
38
  if (args.includes("--help") || args.includes("-h")) {
@@ -33,17 +43,17 @@ async function main() {
33
43
  process.stdout.write(`${APP_VERSION}\n`);
34
44
  return;
35
45
  }
36
- // Le jeton part seul sur stdout : la commande reste utilisable dans un pipe
37
- // (`mcp-messenger --generate-token > token.txt`), les conseils vont sur stderr.
38
- if (args.includes("--generate-token")) {
39
- const { generateToken } = await import("./cli/generate-token.js");
40
- process.stdout.write(`${generateToken()}\n`);
46
+ // Le mot de passe part seul sur stdout : la commande reste utilisable dans un pipe, les
47
+ // explications vont sur stderr.
48
+ if (args.includes("--reset-password")) {
49
+ const { resetPassword } = await import("./cli/reset-password.js");
50
+ const result = await resetPassword();
51
+ process.stdout.write(`${result.password}\n`);
41
52
  process.stderr.write([
42
53
  "",
43
- "Ce jeton doit être identique aux trois endroits suivants :",
44
- " 1. le serveur : MESSENGER_TOKEN",
45
- " 2. chaque agent : MESSENGER_TOKEN dans son .mcp.json",
46
- " 3. la console web : http://127.0.0.1:3600/ui/ (à saisir dans le formulaire)",
54
+ `Nouveau mot de passe du compte « ${result.username} » ci-dessus.`,
55
+ `${result.closedSessions} session(s) de la console fermée(s).`,
56
+ "Connectez-vous avec lui, puis changez-le depuis Mon compte.",
47
57
  "",
48
58
  ].join("\n"));
49
59
  return;
@@ -52,7 +62,12 @@ async function main() {
52
62
  // l'utilisateur, elle n'ouvre ni base ni serveur.
53
63
  if (args.includes("--claude-setup-mcp")) {
54
64
  const { runSetupMcp } = await import("./cli/setup-mcp.js");
55
- process.exit(runSetupMcp({ cwd: process.cwd(), force: args.includes("--force") }));
65
+ process.exit(runSetupMcp({
66
+ cwd: process.cwd(),
67
+ force: args.includes("--force"),
68
+ serverUrl: optionValue(args, "--server-url"),
69
+ apiKey: optionValue(args, "--api-key"),
70
+ }));
56
71
  }
57
72
  // `--watch` prend l'adresse en argument positionnel : c'est la seule chose que l'agent
58
73
  // recopie, le reste (URL, jeton) vient du relais déposé par `enable_notifications`.