@imenam/mcp-messenger 1.0.26 → 1.0.28

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.
@@ -102,21 +102,24 @@ const isoDate = z
102
102
  .regex(/^\d{4}-\d{2}-\d{2}(T[\d:.]+Z?)?$/, "expected a date such as 2026-08-01")
103
103
  .refine((value) => !Number.isNaN(Date.parse(value)), "not a real date");
104
104
  const staleSchema = z.object({ before: isoDate });
105
+ const createdBeforeSchema = z.object({ created_before: isoDate });
105
106
  /**
106
107
  * Une suppression désigne ses boîtes, et rappelle à quel titre elles sont supprimables :
107
- * `before`, elles n'ont plus d'activité depuis cette date ; `empty`, rien n'y est jamais
108
- * passé. Les deux critères s'excluent — le serveur revérifie celui qu'on lui donne, il
109
- * doit donc savoir lequel c'est.
108
+ * `before`, elles n'ont plus d'activité depuis cette date ; `created_before`, elles ont
109
+ * été ouvertes avant cette date ; `empty`, rien n'y est jamais passé. Les trois critères
110
+ * s'excluent — le serveur revérifie celui qu'on lui donne, il doit donc savoir lequel
111
+ * c'est, et les deux critères datés ne désignent pas les mêmes boîtes.
110
112
  */
111
113
  const purgeSchema = z
112
114
  .object({
113
115
  before: isoDate.optional(),
116
+ created_before: isoDate.optional(),
114
117
  empty: z.literal(true).optional(),
115
118
  /** Les adresses vues dans l'aperçu : on ne supprime jamais autre chose que ce qui a été montré. */
116
119
  mailboxes: z.array(z.string().trim().min(1)).min(1).max(500),
117
120
  })
118
- .refine((input) => (input.before === undefined) !== (input.empty === undefined), {
119
- message: "provide either `before` (inactive since that date) or `empty: true`, not both",
121
+ .refine((input) => [input.before, input.created_before, input.empty].filter((value) => value !== undefined).length === 1, {
122
+ message: "provide exactly one of `before` (inactive since that date), `created_before` (created before that date) or `empty: true`",
120
123
  });
121
124
  const adminListSchema = z.object({
122
125
  search: z.string().trim().min(1).optional(),
@@ -130,7 +133,10 @@ export function createApp({ store, config }) {
130
133
  // Présence des veilleurs : état de connexion, donc en mémoire du processus.
131
134
  const watchers = new WatcherRegistry();
132
135
  app.disable("x-powered-by");
133
- app.set("trust proxy", false);
136
+ // Le serveur n'écoute qu'en loopback : ce qui l'atteint de l'extérieur passe par un
137
+ // reverse proxy. Sans ce réglage, toutes les requêtes se présentent comme `127.0.0.1`
138
+ // et le quota par adresse n'en distingue aucune.
139
+ app.set("trust proxy", config.trustProxy);
134
140
  app.use(helmet({
135
141
  contentSecurityPolicy: {
136
142
  directives: {
@@ -160,7 +166,9 @@ export function createApp({ store, config }) {
160
166
  const cookieOptions = (req) => ({
161
167
  httpOnly: true,
162
168
  sameSite: "strict",
163
- // `req.secure` n'est vrai derrière un proxy TLS que si `trust proxy` est configuré.
169
+ // Vrai dès que le proxy annonce `X-Forwarded-Proto: https`, ce que `trust proxy` (posé
170
+ // plus haut depuis la configuration) autorise Express à lire. Reste faux en clair sur
171
+ // la machine, où le cookie doit justement circuler.
164
172
  secure: req.secure,
165
173
  path: "/",
166
174
  });
@@ -236,6 +244,16 @@ export function createApp({ store, config }) {
236
244
  const { before } = staleSchema.parse(req.query);
237
245
  res.json({ before, mailboxes: store.staleMailboxes(before) });
238
246
  });
247
+ /**
248
+ * Boîtes ouvertes avant une date : ce que la purge par ancienneté emporterait.
249
+ *
250
+ * Même aperçu obligatoire que les autres nettoyages, mais un critère différent — ici
251
+ * l'activité ne protège rien, seule compte la date d'ouverture de la boîte.
252
+ */
253
+ admin.get("/mailboxes/created-before", (req, res) => {
254
+ const { created_before: createdBefore } = createdBeforeSchema.parse(req.query);
255
+ res.json({ created_before: createdBefore, mailboxes: store.mailboxesCreatedBefore(createdBefore) });
256
+ });
239
257
  /**
240
258
  * Boîtes par lesquelles rien n'est jamais passé. Sans date : une boîte vide l'est
241
259
  * indépendamment de son âge, et un agent dont toutes les boîtes sont vides disparaît
@@ -254,15 +272,29 @@ export function createApp({ store, config }) {
254
272
  */
255
273
  admin.post("/mailboxes/purge", (req, res) => {
256
274
  const input = purgeSchema.parse(req.body ?? {});
257
- const reason = input.empty ? "empty" : `inactive since ${input.before}`;
258
- const eligible = new Set((input.empty ? store.emptyMailboxes() : store.staleMailboxes(input.before)).map((mailbox) => mailbox.id));
275
+ const reason = input.empty
276
+ ? "empty"
277
+ : input.created_before
278
+ ? `older than ${input.created_before}`
279
+ : `inactive since ${input.before}`;
280
+ const candidates = input.empty
281
+ ? store.emptyMailboxes()
282
+ : input.created_before
283
+ ? store.mailboxesCreatedBefore(input.created_before)
284
+ : store.staleMailboxes(input.before);
285
+ const eligible = new Set(candidates.map((mailbox) => mailbox.id));
259
286
  const changed = input.mailboxes.filter((id) => !eligible.has(id));
260
287
  if (changed.length > 0) {
261
288
  throw new MessengerError(`these mailboxes are no longer ${reason}, nothing was deleted: ${changed.join(", ")}`, 409);
262
289
  }
263
290
  const removed = store.deleteMailboxes(input.mailboxes);
264
291
  logger.warn(`admin purge: ${removed.mailboxes} mailbox(es) deleted, ${reason}`);
265
- res.json({ before: input.before ?? null, empty: input.empty ?? false, ...removed });
292
+ res.json({
293
+ before: input.before ?? null,
294
+ created_before: input.created_before ?? null,
295
+ empty: input.empty ?? false,
296
+ ...removed,
297
+ });
266
298
  });
267
299
  /**
268
300
  * Suppression d'une boîte désignée à la main, quel que soit son état.
@@ -502,6 +534,10 @@ function authenticateAgent(config) {
502
534
  res.status(401).json({ error: "invalid or missing bearer token" });
503
535
  return;
504
536
  }
537
+ // Le jeton partagé atteste l'appartenance à la flotte, pas une adresse : `X-Agent-Id`
538
+ // n'est adossé à rien ici, et rien ne cloisonne les boîtes entre elles. C'est le mode
539
+ // « cercle de confiance », annoncé comme tel au démarrage et dans le README ;
540
+ // MESSENGER_AGENT_TOKENS est ce qui lie une adresse à un jeton.
505
541
  }
506
542
  req.agentId = agentId;
507
543
  next();
@@ -43,6 +43,13 @@ export async function runServerMode() {
43
43
  logger.info(`mcp-messenger v${APP_VERSION} listening on ${running.url} (loopback only)`);
44
44
  logger.info(`database: ${config.dbPath}`);
45
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");
52
+ }
46
53
  logger.info(`web console: ${running.url}/ui/ (asks for MESSENGER_ADMIN_TOKEN once)`);
47
54
  let shuttingDown = false;
48
55
  const shutdown = (signal) => {
@@ -101,7 +101,9 @@ const el = {
101
101
  purgeClose: document.getElementById("purge-close"),
102
102
  purgeCancel: document.getElementById("purge-cancel"),
103
103
  purgeBefore: document.getElementById("purge-before"),
104
+ purgeCreatedBefore: document.getElementById("purge-created-before"),
104
105
  purgePreview: document.getElementById("purge-preview"),
106
+ purgePreviewCreated: document.getElementById("purge-preview-created"),
105
107
  purgePreviewEmpty: document.getElementById("purge-preview-empty"),
106
108
  purgeResult: document.getElementById("purge-result"),
107
109
  purgeConfirm: document.getElementById("purge-confirm"),
@@ -1303,8 +1305,11 @@ const purge = { mode: null, before: null, mailboxes: [], selected: new Set(), ar
1303
1305
 
1304
1306
  function openPurge() {
1305
1307
  const month = new Date(Date.now() - 30 * 86_400_000);
1308
+ const today = new Date().toISOString().slice(0, 10);
1306
1309
  el.purgeBefore.value = month.toISOString().slice(0, 10);
1307
- el.purgeBefore.max = new Date().toISOString().slice(0, 10);
1310
+ el.purgeBefore.max = today;
1311
+ el.purgeCreatedBefore.value = month.toISOString().slice(0, 10);
1312
+ el.purgeCreatedBefore.max = today;
1308
1313
  resetPurge();
1309
1314
  el.purge.showModal();
1310
1315
  }
@@ -1356,6 +1361,13 @@ async function previewStale() {
1356
1361
  await previewPurge("stale", () => api("/admin/mailboxes/stale", { before }), before);
1357
1362
  }
1358
1363
 
1364
+ /** Boîtes ouvertes avant la date choisie, qu'elles servent encore ou non. */
1365
+ async function previewCreated() {
1366
+ const before = el.purgeCreatedBefore.value;
1367
+ if (!before) return;
1368
+ await previewPurge("created", () => api("/admin/mailboxes/created-before", { created_before: before }), before);
1369
+ }
1370
+
1359
1371
  async function previewPurge(mode, load, before = null) {
1360
1372
  resetPurge();
1361
1373
  try {
@@ -1373,10 +1385,15 @@ async function previewPurge(mode, load, before = null) {
1373
1385
  function renderPurgePreview() {
1374
1386
  const container = document.createDocumentFragment();
1375
1387
  const count = purge.mailboxes.length;
1376
- // Le critère est rappelé sous les yeux de qui va confirmer : les trois suppressions
1377
- // remplissent la même liste, et elles ne portent pas sur les mêmes boîtes.
1378
- const since = ` sans activité depuis le ${formatDay(purge.before)}`;
1379
- const criterion = purge.mode === "empty" ? "vide" : purge.mode === "one" ? "" : since;
1388
+ // Le critère est rappelé sous les yeux de qui va confirmer : les quatre suppressions
1389
+ // remplissent la même liste, et elles ne portent pas sur les mêmes boîtes. Les deux
1390
+ // critères datés se distinguent par leur seule formulation : « depuis » regarde
1391
+ // l'activité, « créée avant » regarde l'ouverture de la boîte.
1392
+ const dated =
1393
+ purge.mode === "created"
1394
+ ? ` créée avant le ${formatDay(purge.before)}`
1395
+ : ` sans activité depuis le ${formatDay(purge.before)}`;
1396
+ const criterion = purge.mode === "empty" ? "vide" : purge.mode === "one" ? "" : dated;
1380
1397
 
1381
1398
  if (count === 0) {
1382
1399
  container.appendChild(node("p", "purge-empty", `Aucune boîte ${criterion.trim()}.`));
@@ -1392,7 +1409,9 @@ function renderPurgePreview() {
1392
1409
  ? "Cette boîte, et tout ce que personne d'autre ne pourrait plus lire :"
1393
1410
  : purge.mode === "empty"
1394
1411
  ? `${plural(count, "boîte vide", "boîtes vides")}.`
1395
- : `${plural(count, "boîte")}${since}.`
1412
+ : purge.mode === "created"
1413
+ ? `${plural(count, "boîte créée", "boîtes créées")} avant le ${formatDay(purge.before)}.`
1414
+ : `${plural(count, "boîte")}${dated}.`
1396
1415
  )
1397
1416
  );
1398
1417
 
@@ -1457,7 +1476,11 @@ async function confirmPurge() {
1457
1476
  ? await remove(`/admin/mailboxes/${encodeURIComponent(deleted[0])}`)
1458
1477
  : await post("/admin/mailboxes/purge", {
1459
1478
  // Le critère repart avec les adresses : c'est lui que le serveur revérifie.
1460
- ...(purge.mode === "empty" ? { empty: true } : { before: purge.before }),
1479
+ ...(purge.mode === "empty"
1480
+ ? { empty: true }
1481
+ : purge.mode === "created"
1482
+ ? { created_before: purge.before }
1483
+ : { before: purge.before }),
1461
1484
  mailboxes: deleted,
1462
1485
  });
1463
1486
  resetPurge();
@@ -1488,7 +1511,9 @@ el.purgeOpen.addEventListener("click", openPurge);
1488
1511
  el.purgeClose.addEventListener("click", () => el.purge.close());
1489
1512
  el.purgeCancel.addEventListener("click", () => el.purge.close());
1490
1513
  el.purgeBefore.addEventListener("change", resetPurge);
1514
+ el.purgeCreatedBefore.addEventListener("change", resetPurge);
1491
1515
  el.purgePreview.addEventListener("click", previewStale);
1516
+ el.purgePreviewCreated.addEventListener("click", previewCreated);
1492
1517
  el.purgePreviewEmpty.addEventListener("click", previewEmpty);
1493
1518
  el.purgeConfirm.addEventListener("click", confirmPurge);
1494
1519
 
@@ -70,7 +70,7 @@
70
70
  </button>
71
71
  </div>
72
72
  <p class="modal-sub">
73
- Deux nettoyages, deux critères. La suppression est définitive dans les deux cas, et emporte les messages que
73
+ Trois nettoyages, trois critères. La suppression est définitive dans tous les cas, et emporte les messages que
74
74
  plus personne ne pourrait lire ; ce que le correspondant a reçu de son côté reste intact. Un agent dont toutes
75
75
  les boîtes disparaissent quitte l'annuaire avec elles.
76
76
  </p>
@@ -94,6 +94,16 @@
94
94
  <button type="button" class="btn" id="purge-preview">Prévisualiser</button>
95
95
  </div>
96
96
 
97
+ <!-- Troisième critère : l'âge de la boîte, que son activité ne rachète pas. -->
98
+ <div class="purge-controls">
99
+ <label class="field">
100
+ <span class="field-label">Boîtes créées avant le</span>
101
+ <span class="field-input"><input type="date" id="purge-created-before" /></span>
102
+ <span class="purge-hint">Sur la seule date d'ouverture : une boîte encore active en fait partie.</span>
103
+ </label>
104
+ <button type="button" class="btn" id="purge-preview-created">Prévisualiser</button>
105
+ </div>
106
+
97
107
  <div class="purge-result" id="purge-result"></div>
98
108
 
99
109
  <div class="modal-foot">
@@ -1 +1 @@
1
- export declare const APP_VERSION = "1.0.26";
1
+ export declare const APP_VERSION = "1.0.28";
@@ -1,2 +1,2 @@
1
1
  // Généré par scripts/generate-version.js — ne pas modifier à la main.
2
- export const APP_VERSION = "1.0.26";
2
+ export const APP_VERSION = "1.0.28";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/mcp-messenger",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "main": "dist/src/index.js",
5
5
  "scripts": {
6
6
  "test": "npm run build && vitest run",