@imenam/mcp-messenger 1.0.17 → 1.0.19
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/README.md +53 -8
- package/dist/src/core/store.d.ts +36 -0
- package/dist/src/core/store.js +79 -1
- package/dist/src/server/app.js +68 -1
- package/dist/src/server/ui/app.js +1333 -992
- package/dist/src/server/ui/index.html +90 -1
- package/dist/src/server/ui/styles.css +1645 -1409
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -377,7 +377,7 @@ configuration partagent alors la même boîte. Utile pour un service unique et s
|
|
|
377
377
|
|
|
378
378
|
## Console web
|
|
379
379
|
|
|
380
|
-
`http://127.0.0.1:3600/ui/` affiche
|
|
380
|
+
`http://127.0.0.1:3600/ui/` affiche **tous les échanges** de la flotte,
|
|
381
381
|
organisés autour du fil — l'unité d'une correspondance, celle qui porte le sujet, les deux
|
|
382
382
|
correspondants et l'état de lecture :
|
|
383
383
|
|
|
@@ -386,7 +386,8 @@ correspondants et l'état de lecture :
|
|
|
386
386
|
chevron sur ses boîtes ; une boîte que son agent n'a pas nommée s'affiche à son adresse,
|
|
387
387
|
la seule chose qui la distingue alors de ses sœurs. Cliquer l'agent filtre les fils sur
|
|
388
388
|
**toutes ses sessions**, cliquer une boîte sur **cette seule session**. La colonne se
|
|
389
|
-
replie sur les seuls avatars d'agents quand la place manque
|
|
389
|
+
replie sur les seuls avatars d'agents quand la place manque, son bouton de repli restant
|
|
390
|
+
en place : c'est lui qui la rouvre.
|
|
390
391
|
|
|
391
392
|
**Le classement se fait en deux temps : d'abord ce qui a servi, ensuite ce qui est
|
|
392
393
|
vide ; puis, dans chacun des deux, la dernière activité, la plus récente en tête.** Une
|
|
@@ -394,8 +395,9 @@ correspondants et l'état de lecture :
|
|
|
394
395
|
une par session ouverte, et beaucoup n'échangeront jamais rien. Ces boîtes-là sont
|
|
395
396
|
**masquées par défaut**, derrière un bouton en fin de dépli qui dit combien il y en a ;
|
|
396
397
|
les masquer ne cache jamais de courrier, une boîte sans réception n'ayant par
|
|
397
|
-
construction aucun non-lu. Un agent dont aucune session n'a rien échangé
|
|
398
|
-
derrière
|
|
398
|
+
construction aucun non-lu. **Un agent dont aucune session n'a rien échangé est masqué de
|
|
399
|
+
la même façon**, derrière un bouton en pied de colonne : même geste, un cran plus haut.
|
|
400
|
+
Chaque boîte affiche la date qui la classe, et deux dates
|
|
399
401
|
identiques sont départagées par l'adresse pour que l'ordre ne bouge pas d'un
|
|
400
402
|
rafraîchissement à l'autre ;
|
|
401
403
|
- **la liste des fils**, du plus récemment actif au plus ancien, chacun résumé par son
|
|
@@ -416,6 +418,10 @@ correspondants et l'état de lecture :
|
|
|
416
418
|
dossier, état). Tout ce qui se recopie a son bouton de copie ;
|
|
417
419
|
- une recherche unique, qui couvre le nom d'un fil, sa description et le texte de ses
|
|
418
420
|
messages ;
|
|
421
|
+
- **des panneaux redimensionnables** : chaque ligne de séparation se saisit à la souris,
|
|
422
|
+
se règle aussi aux flèches du clavier une fois qu'elle a le focus, et rend sa largeur
|
|
423
|
+
d'origine sur un double-clic. Les largeurs choisies sont retenues par le navigateur d'une
|
|
424
|
+
visite à l'autre ;
|
|
419
425
|
- un rafraîchissement automatique toutes les 5 secondes, débrayable.
|
|
420
426
|
|
|
421
427
|
**Les adresses ne sont pas de l'information de lecture.** La console résout partout
|
|
@@ -439,7 +445,37 @@ navigateur de l'administrateur. Les routes `/admin` acceptent toujours le jeton
|
|
|
439
445
|
`Authorization: Bearer` pour les scripts. Le formulaire est limité à 10 tentatives par
|
|
440
446
|
quart d'heure et par IP.
|
|
441
447
|
|
|
442
|
-
La console n'
|
|
448
|
+
La console n'envoie ni ne modifie aucun message. Les seules écritures qu'elle commande
|
|
449
|
+
sont des suppressions, derrière l'icône de corbeille de la barre du haut. **Deux
|
|
450
|
+
nettoyages y cohabitent, avec chacun son bouton, parce qu'ils ne visent pas la même
|
|
451
|
+
chose :**
|
|
452
|
+
|
|
453
|
+
- **les boîtes vides** : rien n'y est jamais passé, ni envoi ni réception. Elles n'ont pas
|
|
454
|
+
d'âge, une boîte vide est vide qu'elle date d'un mois ou d'une minute. C'est le
|
|
455
|
+
nettoyage à passer sur un serveur qui a vu défiler beaucoup de sessions muettes. **Un
|
|
456
|
+
agent dont toutes les boîtes disparaissent quitte l'annuaire avec elles**, faute de
|
|
457
|
+
boîte à regrouper ;
|
|
458
|
+
- **les boîtes inactives depuis une date** : elles ont servi, mais plus rien ne les a
|
|
459
|
+
touchées depuis. **Une boîte créée après cette date est épargnée, même vide** — une
|
|
460
|
+
session qui vient de démarrer et n'a pas encore parlé n'est pas une boîte à l'abandon.
|
|
461
|
+
|
|
462
|
+
L'un comme l'autre se font en deux temps, parce qu'ils ne se rattrapent pas :
|
|
463
|
+
|
|
464
|
+
1. **L'aperçu** liste les boîtes concernées, chacune avec son nom, son adresse, son nombre
|
|
465
|
+
de messages et sa dernière activité. Chaque ligne se décoche.
|
|
466
|
+
2. **La confirmation** demande deux clics, et n'envoie que les adresses affichées, avec le
|
|
467
|
+
critère qui les a désignées. Le serveur, lui, reconfronte chacune à ce critère avant de
|
|
468
|
+
supprimer : une boîte redevenue active, ou qui vient de recevoir son premier message,
|
|
469
|
+
fait échouer le nettoyage entier, nommément. Changer de critère, changer la date ou
|
|
470
|
+
décocher une ligne annule une confirmation déjà donnée.
|
|
471
|
+
|
|
472
|
+
**Ce qui disparaît.** Un message a deux copies, une par correspondant : supprimer une
|
|
473
|
+
boîte n'emporte que sa moitié. Le message lui-même ne s'efface qu'une fois sa dernière
|
|
474
|
+
copie partie, autrement dit quand plus personne ne pourrait le lire ; un fil vidé de ses
|
|
475
|
+
messages s'en va avec. Ce que l'autre côté a reçu reste intact, expéditeur compris.
|
|
476
|
+
|
|
477
|
+
Le registre local des boîtes n'a pas à être nettoyé après une purge : une entrée qui
|
|
478
|
+
désigne une boîte disparue n'est jamais reprise, et s'oublie d'elle-même.
|
|
443
479
|
|
|
444
480
|
---
|
|
445
481
|
|
|
@@ -506,6 +542,9 @@ la console (cookie), qui elle n'ouvre rien d'autre.
|
|
|
506
542
|
| `GET` | `/admin/overview` · `/admin/messages[/:id]` | Supervision, lecture seule |
|
|
507
543
|
| `GET` | `/admin/threads` | Fils du serveur entier (`agent` — une ou plusieurs adresses séparées par des virgules —, `search`, `unread_only`, `priority`, `limit`, `offset`) |
|
|
508
544
|
| `GET` | `/admin/threads/:id` | Fil complet, corps inclus |
|
|
545
|
+
| `GET` | `/admin/mailboxes/stale` | Boîtes sans activité depuis `before`, avec ce que chacune contient |
|
|
546
|
+
| `GET` | `/admin/mailboxes/empty` | Boîtes par lesquelles rien n'est jamais passé, sans critère de date |
|
|
547
|
+
| `POST` | `/admin/mailboxes/purge` | Supprime les boîtes nommées (`mailboxes`, plus `before` **ou** `empty: true`) — refusé si l'une d'elles ne répond plus au critère |
|
|
509
548
|
| `POST` | `/ui/login` · `/ui/logout` | Session de la console (`{ "token": … }` → cookie `HttpOnly`) |
|
|
510
549
|
|
|
511
550
|
---
|
|
@@ -525,8 +564,12 @@ la console (cookie), qui elle n'ouvre rien d'autre.
|
|
|
525
564
|
dans les logs).
|
|
526
565
|
- **Isolation des boîtes** garantie côté serveur : les identifiants de messages fournis par
|
|
527
566
|
un agent sont systématiquement filtrés sur ses propres livraisons.
|
|
528
|
-
- La console web n'insère jamais de HTML fourni par un agent
|
|
529
|
-
|
|
567
|
+
- La console web n'insère jamais de HTML fourni par un agent : le corps des messages est
|
|
568
|
+
rendu en markdown par construction de nœuds, jamais par assignation de balisage, et la
|
|
569
|
+
CSP interdit tout script tiers.
|
|
570
|
+
- La console ne peut ni écrire ni modifier un message. Ses seules commandes destructrices
|
|
571
|
+
sont les deux nettoyages de boîtes, réservés au jeton d'administration, confirmés deux
|
|
572
|
+
fois et revérifiés par le serveur avant exécution.
|
|
530
573
|
- Derrière un reverse proxy TLS, exposer le serveur en `https://` et donner cette URL aux
|
|
531
574
|
agents via `MESSENGER_SERVER_URL`.
|
|
532
575
|
|
|
@@ -567,7 +610,9 @@ test/ # store, config, API HTTP, outils MCP, binaire réel
|
|
|
567
610
|
|
|
568
611
|
### Tests
|
|
569
612
|
|
|
570
|
-
|
|
613
|
+
253 tests couvrant : les deux nettoyages de boîtes commandés depuis la console — vides,
|
|
614
|
+
ou inactives depuis une date — avec leur aperçu, leur double confirmation et le refus
|
|
615
|
+
nominatif d'une boîte qui ne répond plus au critère, la reprise de la boîte d'une session interrompue (clé de session de
|
|
571
616
|
l'hôte, boîte orpheline, boîte d'une session vivante jamais reprise), le rendu markdown des messages et l'aplatissement des aperçus (dont
|
|
572
617
|
l'innocuité du rendu : ni HTML interprété, ni lien vers un autre schéma qu'`http`, `https`
|
|
573
618
|
ou `mailto`), le stockage et l'isolation des boîtes, la validation de configuration,
|
package/dist/src/core/store.d.ts
CHANGED
|
@@ -87,6 +87,42 @@ export declare class MessageStore extends EventEmitter<StoreEvents> {
|
|
|
87
87
|
stats(agentId: string): MailboxStats;
|
|
88
88
|
/** Purge les livraisons restées en corbeille au-delà de `days`. Renvoie le nombre supprimé. */
|
|
89
89
|
purgeTrash(days: number): number;
|
|
90
|
+
/**
|
|
91
|
+
* Boîtes dont plus rien ne bouge depuis `before`, avec ce que leur suppression coûterait.
|
|
92
|
+
*
|
|
93
|
+
* L'inactivité se mesure au plus récent de trois moments : la dernière connexion, la
|
|
94
|
+
* dernière livraison, et la création. Le dernier compte autant que les autres — une
|
|
95
|
+
* boîte allouée il y a une minute et qui n'a rien fait n'est pas une boîte à l'abandon,
|
|
96
|
+
* c'est une session qui n'a pas encore parlé.
|
|
97
|
+
*/
|
|
98
|
+
staleMailboxes(before: string): (AgentDirectoryEntry & {
|
|
99
|
+
messages: number;
|
|
100
|
+
})[];
|
|
101
|
+
/**
|
|
102
|
+
* Boîtes par lesquelles rien n'est jamais passé, ni envoi ni réception.
|
|
103
|
+
*
|
|
104
|
+
* Elles n'ont pas d'âge : une boîte vide est vide, qu'elle date d'un mois ou d'une
|
|
105
|
+
* minute. C'est ce qui la distingue d'une boîte inactive, et ce qui rend ce nettoyage
|
|
106
|
+
* différent de la purge par date — la boîte d'une session qui vient d'ouvrir et n'a pas
|
|
107
|
+
* encore parlé en fait donc partie.
|
|
108
|
+
*/
|
|
109
|
+
emptyMailboxes(): (AgentDirectoryEntry & {
|
|
110
|
+
messages: number;
|
|
111
|
+
})[];
|
|
112
|
+
/**
|
|
113
|
+
* Supprime ces boîtes et tout ce qui n'appartenait qu'à elles.
|
|
114
|
+
*
|
|
115
|
+
* Un message a deux copies, une par correspondant : effacer une boîte n'efface donc que
|
|
116
|
+
* sa moitié. Le message lui-même ne disparaît qu'une fois sa dernière copie partie —
|
|
117
|
+
* autrement dit quand plus personne ne peut le lire. Ce que l'autre côté a reçu reste
|
|
118
|
+
* intact, avec l'adresse de l'expéditeur telle qu'elle a été livrée.
|
|
119
|
+
*/
|
|
120
|
+
deleteMailboxes(ids: string[]): {
|
|
121
|
+
mailboxes: number;
|
|
122
|
+
deliveries: number;
|
|
123
|
+
messages: number;
|
|
124
|
+
threads: number;
|
|
125
|
+
};
|
|
90
126
|
/** Vue globale : tous les messages du serveur, indépendamment des boîtes. */
|
|
91
127
|
listAllMessages(opts?: {
|
|
92
128
|
search?: string;
|
package/dist/src/core/store.js
CHANGED
|
@@ -581,7 +581,85 @@ export class MessageStore extends EventEmitter {
|
|
|
581
581
|
this.dropOrphanMessages();
|
|
582
582
|
return removed;
|
|
583
583
|
}
|
|
584
|
-
|
|
584
|
+
/**
|
|
585
|
+
* Boîtes dont plus rien ne bouge depuis `before`, avec ce que leur suppression coûterait.
|
|
586
|
+
*
|
|
587
|
+
* L'inactivité se mesure au plus récent de trois moments : la dernière connexion, la
|
|
588
|
+
* dernière livraison, et la création. Le dernier compte autant que les autres — une
|
|
589
|
+
* boîte allouée il y a une minute et qui n'a rien fait n'est pas une boîte à l'abandon,
|
|
590
|
+
* c'est une session qui n'a pas encore parlé.
|
|
591
|
+
*/
|
|
592
|
+
staleMailboxes(before) {
|
|
593
|
+
const rows = this.db
|
|
594
|
+
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at,
|
|
595
|
+
(SELECT COUNT(*) FROM deliveries d WHERE d.agent_id = a.id) AS messages
|
|
596
|
+
FROM agents a
|
|
597
|
+
WHERE MAX(${LAST_ACTIVITY}, a.created_at) < ?
|
|
598
|
+
ORDER BY MAX(${LAST_ACTIVITY}, a.created_at) ASC`)
|
|
599
|
+
.all(before);
|
|
600
|
+
return rows.map((row) => ({
|
|
601
|
+
...row,
|
|
602
|
+
registered: Boolean(row.registered),
|
|
603
|
+
last_activity_at: row.last_activity_at || null,
|
|
604
|
+
}));
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Boîtes par lesquelles rien n'est jamais passé, ni envoi ni réception.
|
|
608
|
+
*
|
|
609
|
+
* Elles n'ont pas d'âge : une boîte vide est vide, qu'elle date d'un mois ou d'une
|
|
610
|
+
* minute. C'est ce qui la distingue d'une boîte inactive, et ce qui rend ce nettoyage
|
|
611
|
+
* différent de la purge par date — la boîte d'une session qui vient d'ouvrir et n'a pas
|
|
612
|
+
* encore parlé en fait donc partie.
|
|
613
|
+
*/
|
|
614
|
+
emptyMailboxes() {
|
|
615
|
+
const rows = this.db
|
|
616
|
+
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at, 0 AS messages
|
|
617
|
+
FROM agents a
|
|
618
|
+
WHERE NOT EXISTS (SELECT 1 FROM deliveries d WHERE d.agent_id = a.id)
|
|
619
|
+
ORDER BY a.created_at ASC`)
|
|
620
|
+
.all();
|
|
621
|
+
return rows.map((row) => ({
|
|
622
|
+
...row,
|
|
623
|
+
registered: Boolean(row.registered),
|
|
624
|
+
last_activity_at: row.last_activity_at || null,
|
|
625
|
+
}));
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Supprime ces boîtes et tout ce qui n'appartenait qu'à elles.
|
|
629
|
+
*
|
|
630
|
+
* Un message a deux copies, une par correspondant : effacer une boîte n'efface donc que
|
|
631
|
+
* sa moitié. Le message lui-même ne disparaît qu'une fois sa dernière copie partie —
|
|
632
|
+
* autrement dit quand plus personne ne peut le lire. Ce que l'autre côté a reçu reste
|
|
633
|
+
* intact, avec l'adresse de l'expéditeur telle qu'elle a été livrée.
|
|
634
|
+
*/
|
|
635
|
+
deleteMailboxes(ids) {
|
|
636
|
+
if (ids.length === 0)
|
|
637
|
+
return { mailboxes: 0, deliveries: 0, messages: 0, threads: 0 };
|
|
638
|
+
return this.db.transaction(() => {
|
|
639
|
+
const messagesBefore = this.db.prepare("SELECT COUNT(*) AS n FROM messages").get().n;
|
|
640
|
+
const threadsBefore = this.db.prepare("SELECT COUNT(*) AS n FROM threads").get().n;
|
|
641
|
+
let mailboxes = 0;
|
|
642
|
+
let deliveries = 0;
|
|
643
|
+
const dropDeliveries = this.db.prepare("DELETE FROM deliveries WHERE agent_id = ?");
|
|
644
|
+
const dropFolders = this.db.prepare("DELETE FROM folders WHERE agent_id = ?");
|
|
645
|
+
const dropAgent = this.db.prepare("DELETE FROM agents WHERE id = ?");
|
|
646
|
+
for (const id of ids) {
|
|
647
|
+
deliveries += dropDeliveries.run(id).changes;
|
|
648
|
+
dropFolders.run(id);
|
|
649
|
+
mailboxes += dropAgent.run(id).changes;
|
|
650
|
+
}
|
|
651
|
+
this.dropOrphanMessages();
|
|
652
|
+
const messagesAfter = this.db.prepare("SELECT COUNT(*) AS n FROM messages").get().n;
|
|
653
|
+
const threadsAfter = this.db.prepare("SELECT COUNT(*) AS n FROM threads").get().n;
|
|
654
|
+
return {
|
|
655
|
+
mailboxes,
|
|
656
|
+
deliveries,
|
|
657
|
+
messages: messagesBefore - messagesAfter,
|
|
658
|
+
threads: threadsBefore - threadsAfter,
|
|
659
|
+
};
|
|
660
|
+
})();
|
|
661
|
+
}
|
|
662
|
+
// --- Supervision (interface web, lecture seule sauf purge) ---------------
|
|
585
663
|
/** Vue globale : tous les messages du serveur, indépendamment des boîtes. */
|
|
586
664
|
listAllMessages(opts = {}) {
|
|
587
665
|
const limit = clamp(opts.limit ?? 50, 1, MAX_LIMIT);
|
package/dist/src/server/app.js
CHANGED
|
@@ -89,6 +89,35 @@ const adminThreadsSchema = z.object({
|
|
|
89
89
|
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
90
90
|
offset: z.coerce.number().int().min(0).optional(),
|
|
91
91
|
});
|
|
92
|
+
/**
|
|
93
|
+
* Date de coupure d'une purge.
|
|
94
|
+
*
|
|
95
|
+
* Un jour (`2026-08-01`) autant qu'un instant (`2026-08-01T09:00:00.000Z`) : la console
|
|
96
|
+
* envoie ce que le champ de date du navigateur produit, un jour nu, qui est déjà un
|
|
97
|
+
* horodatage ISO valide et se compare tel quel aux dates stockées.
|
|
98
|
+
*/
|
|
99
|
+
const isoDate = z
|
|
100
|
+
.string()
|
|
101
|
+
.trim()
|
|
102
|
+
.regex(/^\d{4}-\d{2}-\d{2}(T[\d:.]+Z?)?$/, "expected a date such as 2026-08-01")
|
|
103
|
+
.refine((value) => !Number.isNaN(Date.parse(value)), "not a real date");
|
|
104
|
+
const staleSchema = z.object({ before: isoDate });
|
|
105
|
+
/**
|
|
106
|
+
* 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.
|
|
110
|
+
*/
|
|
111
|
+
const purgeSchema = z
|
|
112
|
+
.object({
|
|
113
|
+
before: isoDate.optional(),
|
|
114
|
+
empty: z.literal(true).optional(),
|
|
115
|
+
/** Les adresses vues dans l'aperçu : on ne supprime jamais autre chose que ce qui a été montré. */
|
|
116
|
+
mailboxes: z.array(z.string().trim().min(1)).min(1).max(500),
|
|
117
|
+
})
|
|
118
|
+
.refine((input) => (input.before === undefined) !== (input.empty === undefined), {
|
|
119
|
+
message: "provide either `before` (inactive since that date) or `empty: true`, not both",
|
|
120
|
+
});
|
|
92
121
|
const adminListSchema = z.object({
|
|
93
122
|
search: z.string().trim().min(1).optional(),
|
|
94
123
|
agent: z.string().trim().min(1).optional(),
|
|
@@ -157,7 +186,7 @@ export function createApp({ store, config }) {
|
|
|
157
186
|
app.get("/", (_req, res) => {
|
|
158
187
|
res.redirect("/ui/");
|
|
159
188
|
});
|
|
160
|
-
// --- Supervision (lecture
|
|
189
|
+
// --- Supervision (lecture, sauf les nettoyages de boîtes) ----------------
|
|
161
190
|
// Montée avant la porte d'entrée : son contrôle est au moins aussi strict (session de
|
|
162
191
|
// console ou jeton admin), et la session par cookie n'est reconnue nulle part ailleurs.
|
|
163
192
|
const admin = express.Router();
|
|
@@ -197,6 +226,44 @@ export function createApp({ store, config }) {
|
|
|
197
226
|
throw new MessengerError(`thread not found: ${threadId}`, 404);
|
|
198
227
|
res.json({ thread_id: threadId, thread: store.getThreadInfo(threadId), messages });
|
|
199
228
|
});
|
|
229
|
+
/**
|
|
230
|
+
* Boîtes sans activité depuis une date : ce que la purge emporterait.
|
|
231
|
+
*
|
|
232
|
+
* Toujours consultée avant de supprimer — la console montre la liste, et c'est sur cette
|
|
233
|
+
* liste que porte la suppression. Une purge ne se décide pas sur un compte.
|
|
234
|
+
*/
|
|
235
|
+
admin.get("/mailboxes/stale", (req, res) => {
|
|
236
|
+
const { before } = staleSchema.parse(req.query);
|
|
237
|
+
res.json({ before, mailboxes: store.staleMailboxes(before) });
|
|
238
|
+
});
|
|
239
|
+
/**
|
|
240
|
+
* Boîtes par lesquelles rien n'est jamais passé. Sans date : une boîte vide l'est
|
|
241
|
+
* indépendamment de son âge, et un agent dont toutes les boîtes sont vides disparaît
|
|
242
|
+
* de l'annuaire du même coup, faute de boîte à regrouper.
|
|
243
|
+
*/
|
|
244
|
+
admin.get("/mailboxes/empty", (_req, res) => {
|
|
245
|
+
res.json({ mailboxes: store.emptyMailboxes() });
|
|
246
|
+
});
|
|
247
|
+
/**
|
|
248
|
+
* La seule route de la console qui écrit, et elle détruit.
|
|
249
|
+
*
|
|
250
|
+
* Deux garde-fous, parce qu'un nettoyage ne se rattrape pas : le critère est redemandé
|
|
251
|
+
* avec les adresses, et le serveur refuse toute adresse qui n'y répond plus au moment
|
|
252
|
+
* où il l'examine. Une boîte redevenue vivante, ou qui vient de recevoir son premier
|
|
253
|
+
* message, est donc épargnée — et le refus est nominatif, il porte sur le lot entier.
|
|
254
|
+
*/
|
|
255
|
+
admin.post("/mailboxes/purge", (req, res) => {
|
|
256
|
+
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));
|
|
259
|
+
const changed = input.mailboxes.filter((id) => !eligible.has(id));
|
|
260
|
+
if (changed.length > 0) {
|
|
261
|
+
throw new MessengerError(`these mailboxes are no longer ${reason}, nothing was deleted: ${changed.join(", ")}`, 409);
|
|
262
|
+
}
|
|
263
|
+
const removed = store.deleteMailboxes(input.mailboxes);
|
|
264
|
+
logger.warn(`admin purge: ${removed.mailboxes} mailbox(es) deleted, ${reason}`);
|
|
265
|
+
res.json({ before: input.before ?? null, empty: input.empty ?? false, ...removed });
|
|
266
|
+
});
|
|
200
267
|
app.use("/admin", admin);
|
|
201
268
|
// Porte d'entrée de tout le reste : aucune requête n'est servie sans jeton valide. Le
|
|
202
269
|
// contrôle par route (identité de l'agent) vient ensuite.
|