@nodefony/ai 10.0.0-beta.3 → 10.0.0-beta.4
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 +14 -6
- package/dist/_virtual/{_@oxc-project_runtime@0.154.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +21 -14
- package/dist/nodefony/command/StdioCommand.js +75 -0
- package/dist/nodefony/config/config.js +141 -8
- package/dist/nodefony/config/configSchema.js +1 -2
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/mcp/caller.js +38 -0
- package/dist/nodefony/mcp/gate.js +628 -0
- package/dist/nodefony/mcp/guard.js +82 -0
- package/dist/nodefony/mcp/progress.js +74 -0
- package/dist/nodefony/mcp/protocol.js +76 -0
- package/dist/nodefony/mcp/server.js +302 -0
- package/dist/nodefony/mcp/sessionTicket.js +89 -0
- package/dist/nodefony/mcp/stdio.js +64 -0
- package/dist/nodefony/mcp/stream.js +80 -0
- package/dist/nodefony/mcp/ticketSession.js +177 -0
- package/dist/nodefony/mcp/tools.js +485 -0
- package/dist/nodefony/src/errors/AiError.js +1 -2
- package/dist/types/index.d.ts +25 -0
- package/dist/types/nodefony/command/StdioCommand.d.ts +28 -0
- package/dist/types/nodefony/config/config.d.ts +28 -32
- package/dist/types/nodefony/mcp/caller.d.ts +54 -0
- package/dist/types/nodefony/mcp/gate.d.ts +235 -0
- package/dist/types/nodefony/mcp/guard.d.ts +89 -0
- package/dist/types/nodefony/mcp/progress.d.ts +48 -0
- package/dist/types/nodefony/mcp/protocol.d.ts +107 -0
- package/dist/types/nodefony/mcp/server.d.ts +156 -0
- package/dist/types/nodefony/mcp/sessionTicket.d.ts +60 -0
- package/dist/types/nodefony/mcp/stdio.d.ts +38 -0
- package/dist/types/nodefony/mcp/stream.d.ts +76 -0
- package/dist/types/nodefony/mcp/ticketSession.d.ts +174 -0
- package/dist/types/nodefony/mcp/tools.d.ts +232 -0
- package/docs/index.md +56 -0
- package/docs/mcp.md +754 -0
- package/package.json +13 -11
- package/dist/_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js +0 -6
- package/dist/nodefony/interfaces/IAiService.js +0 -1
- package/dist/nodefony/interfaces/index.js +0 -1
- package/dist/nodefony/service/AiService.js +0 -96
- package/dist/types/nodefony/interfaces/IAiService.d.ts +0 -15
- package/dist/types/nodefony/interfaces/index.d.ts +0 -1
- package/dist/types/nodefony/service/AiService.d.ts +0 -80
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { type IJsonRpcMessage, type IMcpHttpReply, type JsonRpcId } from "./protocol.js";
|
|
2
|
+
import { type IJsonRpcNotification } from "nodefony";
|
|
3
|
+
import { type IMcpTool, type IMcpCaller } from "./tools.js";
|
|
4
|
+
/**
|
|
5
|
+
* Cœur du serveur MCP : un message JSON-RPC entre, une réponse HTTP sort.
|
|
6
|
+
*
|
|
7
|
+
* **Fonction pure** — elle ne touche ni au socket, ni au conteneur, ni à
|
|
8
|
+
* l'horloge. C'est ce qui permet d'éprouver le protocole entier (statuts
|
|
9
|
+
* compris) sans démarrer de serveur, et c'est aussi ce qui rend le transport
|
|
10
|
+
* interchangeable : le jour où un transport `stdio` serait nécessaire pour
|
|
11
|
+
* répondre application éteinte, il appellerait cette même fonction.
|
|
12
|
+
*
|
|
13
|
+
* ## Ce que la révision 2026-07-28 change, et pourquoi c'est ce qui rend cette
|
|
14
|
+
* porte viable
|
|
15
|
+
*
|
|
16
|
+
* Les sessions de niveau protocole ont disparu. Rien n'est retenu entre deux
|
|
17
|
+
* appels : chaque `POST` porte tout ce qu'il faut pour être servi. Un
|
|
18
|
+
* redémarrage du serveur de développement — celui que le superviseur déclenche
|
|
19
|
+
* à chaque fichier sauvegardé — ne casse donc aucun état, et la réponse
|
|
20
|
+
* suivante vient du code qui vient d'être rechargé. Aucun cache à invalider,
|
|
21
|
+
* jamais : la fraîcheur est une propriété du protocole, pas une discipline.
|
|
22
|
+
*/
|
|
23
|
+
/** Ce que ce serveur sait faire, annoncé à l'initialisation. */
|
|
24
|
+
export interface IServerInfo {
|
|
25
|
+
name: string;
|
|
26
|
+
version: string;
|
|
27
|
+
}
|
|
28
|
+
/** Tout ce dont le traitement d'un message a besoin. */
|
|
29
|
+
export interface IMcpServerContext {
|
|
30
|
+
/**
|
|
31
|
+
* Outils SERVIS, déjà ramassés et filtrés (`collectMcpTools`).
|
|
32
|
+
*
|
|
33
|
+
* ⭐ Ce sont des outils exécutables, pas des noms : c'est ce qui rend le
|
|
34
|
+
* protocole indépendant du catalogue. Tant que cette fonction recevait une
|
|
35
|
+
* allowlist de clés, elle devait connaître le catalogue pour la résoudre —
|
|
36
|
+
* et un serveur MCP d'un autre paquet aurait dû redéclarer le sien.
|
|
37
|
+
*/
|
|
38
|
+
tools: readonly IMcpTool[];
|
|
39
|
+
/** Identité annoncée au client. */
|
|
40
|
+
serverInfo: IServerInfo;
|
|
41
|
+
/**
|
|
42
|
+
* Appelant établi par la porte, transmis aux handlers.
|
|
43
|
+
*
|
|
44
|
+
* Le protocole ne s'en sert PAS pour décider : la décision a déjà été prise à
|
|
45
|
+
* la collecte, et `tools` ne contient que ce qui lui revient. Il le transporte
|
|
46
|
+
* pour qu'un outil puisse borner ce qu'il REND à son sujet.
|
|
47
|
+
*/
|
|
48
|
+
caller?: IMcpCaller;
|
|
49
|
+
/**
|
|
50
|
+
* Combien d'outils la collecte a RETENUS pour cet appelant.
|
|
51
|
+
*
|
|
52
|
+
* Sert uniquement à l'annonce de `server/discover` — un nombre, jamais un
|
|
53
|
+
* nom. Absent ou `0` = rien à signaler.
|
|
54
|
+
*/
|
|
55
|
+
withheldCount?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Cette porte sait-elle RECEVOIR une autorisation (porte HTTP protégée, qui
|
|
58
|
+
* publie son document RFC 9728 et défie par `401`) ? Absent ou `false` : les
|
|
59
|
+
* outils retenus ne s'ouvrent pas ici, et l'annonce ne renvoie à aucun
|
|
60
|
+
* document ni défi qui n'existent pas.
|
|
61
|
+
*/
|
|
62
|
+
authorizable?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Ce que la porte sait faire PENDANT un appel — absent, l'appel est servi
|
|
65
|
+
* comme une lecture courte : jamais annulé, sans progression.
|
|
66
|
+
*/
|
|
67
|
+
transport?: IMcpTransport;
|
|
68
|
+
/**
|
|
69
|
+
* Les appels en cours de CET appelant, que `notifications/cancelled` abat —
|
|
70
|
+
* absent, seule la fermeture du flux annule un appel.
|
|
71
|
+
*/
|
|
72
|
+
cancellation?: IMcpCancellation;
|
|
73
|
+
/**
|
|
74
|
+
* Révisions du protocole que CETTE porte accepte, la préférée en tête.
|
|
75
|
+
* Absent = toutes celles que le moteur sait servir ({@link MCP_SUPPORTED_VERSIONS}).
|
|
76
|
+
*/
|
|
77
|
+
versions?: readonly string[];
|
|
78
|
+
/**
|
|
79
|
+
* Ce que l'agent lit pour savoir QUAND se servir de cette porte — la phrase
|
|
80
|
+
* d'ouverture des `instructions`. Absent = une phrase neutre.
|
|
81
|
+
*/
|
|
82
|
+
instructions?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Indices de cache des listes (`ttlMs`, `cacheScope`), servis aux clients de
|
|
85
|
+
* l'ère moderne seulement. Absent = `0` / `"private"`.
|
|
86
|
+
*/
|
|
87
|
+
cache?: IMcpCacheHints;
|
|
88
|
+
}
|
|
89
|
+
/** Indices de cache d'un résultat MCP (spec `server/utilities/caching`). */
|
|
90
|
+
export interface IMcpCacheHints {
|
|
91
|
+
/** Durée de fraîcheur en millisecondes, `>= 0` (MUST). */
|
|
92
|
+
ttlMs: number;
|
|
93
|
+
/** `private` dès que le résultat dépend de l'appelant. */
|
|
94
|
+
cacheScope: "public" | "private";
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Le transport d'UNE requête, vu du protocole.
|
|
98
|
+
*
|
|
99
|
+
* Le protocole décide QUOI émettre (une progression liée à la requête) ; la
|
|
100
|
+
* porte décide COMMENT — et c'est à elle d'ouvrir son flux au premier envoi.
|
|
101
|
+
* Une porte qui ne sait pas tenir de flux ne fournit pas `notify` : la
|
|
102
|
+
* progression devient muette, la réponse reste du JSON.
|
|
103
|
+
*/
|
|
104
|
+
export interface IMcpTransport {
|
|
105
|
+
/** Abattu quand le client abandonne la requête. */
|
|
106
|
+
signal?: AbortSignal;
|
|
107
|
+
/** Émet une notification liée à la requête, AVANT la réponse finale. */
|
|
108
|
+
notify?: (notification: IJsonRpcNotification) => void;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Les appels en cours d'UN appelant, annulables par `notifications/cancelled`.
|
|
112
|
+
*
|
|
113
|
+
* Deux voies d'annulation coexistent. La révision `2026-07-28` fait de la
|
|
114
|
+
* fermeture du flux LA seule annulation en Streamable HTTP ; la révision
|
|
115
|
+
* `2025-11-25`, celle du SDK officiel, envoie à la place une notification dans
|
|
116
|
+
* un AUTRE `POST`, sans rien fermer. Cette table sert la seconde voie.
|
|
117
|
+
*
|
|
118
|
+
* La porte la fournit déjà LIÉE à l'appelant qu'elle a reconnu : l'identifiant
|
|
119
|
+
* JSON-RPC est choisi par le CLIENT, deux appelants peuvent donc envoyer le
|
|
120
|
+
* même, et une annulation ne doit jamais atteindre l'appel d'un autre.
|
|
121
|
+
*/
|
|
122
|
+
export interface IMcpCancellation {
|
|
123
|
+
/**
|
|
124
|
+
* Inscrit l'appel `id` le temps de son exécution.
|
|
125
|
+
*
|
|
126
|
+
* @param id - identifiant JSON-RPC de l'appel
|
|
127
|
+
* @returns le signal que la notification abat, et `close()`, à appeler
|
|
128
|
+
* quand l'appel est fini (il le désinscrit)
|
|
129
|
+
*/
|
|
130
|
+
open(id: JsonRpcId): {
|
|
131
|
+
readonly signal: AbortSignal;
|
|
132
|
+
close(): void;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Abat l'appel `id` s'il est en cours ; inconnu ou déjà fini, ne fait rien
|
|
136
|
+
* (la spec permet d'ignorer une telle annulation).
|
|
137
|
+
*
|
|
138
|
+
* @param id - identifiant JSON-RPC de l'appel visé
|
|
139
|
+
* @param reason - motif donné par le client, à journaliser seulement
|
|
140
|
+
*/
|
|
141
|
+
cancel(id: JsonRpcId, reason: string | undefined): void;
|
|
142
|
+
}
|
|
143
|
+
/** En-têtes HTTP dont le protocole se sert. */
|
|
144
|
+
export interface IMcpHeaders {
|
|
145
|
+
/** `MCP-Protocol-Version`, absent chez un client de l'ère legacy. */
|
|
146
|
+
protocolVersion?: string | undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Traite UN message JSON-RPC.
|
|
150
|
+
*
|
|
151
|
+
* @param message - corps du `POST`, déjà parsé
|
|
152
|
+
* @param context - outils autorisés et briques qui répondent
|
|
153
|
+
* @param headers - en-têtes du transport (`MCP-Protocol-Version`)
|
|
154
|
+
* @returns statut HTTP et corps à écrire (corps `null` = `202` sans contenu)
|
|
155
|
+
*/
|
|
156
|
+
export declare function handleMcpMessage(message: IJsonRpcMessage | null, context: IMcpServerContext, headers?: IMcpHeaders): Promise<IMcpHttpReply>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Durée de vie d'un ticket : le temps d'une requête de connexion en boucle locale. */
|
|
2
|
+
export declare const SESSION_TICKET_TTL_MS = 10000;
|
|
3
|
+
/**
|
|
4
|
+
* Tickets en attente au plus. Un outil en émet UN et le rachète aussitôt :
|
|
5
|
+
* en atteindre huit signale un usage anormal, refusé plutôt qu'accumulé.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SESSION_TICKET_MAX_PENDING = 8;
|
|
8
|
+
/** Longueur d'un ticket : 32 octets d'aléa en base64url, sans remplissage. */
|
|
9
|
+
export declare const SESSION_TICKET_LENGTH = 43;
|
|
10
|
+
/** En-tête qui porte le ticket vers la route de connexion par ticket. */
|
|
11
|
+
export declare const SESSION_TICKET_HEADER = "x-nodefony-session-ticket";
|
|
12
|
+
/** Réglages du magasin — injectés par les tests, défauts en production. */
|
|
13
|
+
export interface ISessionTicketStoreOptions {
|
|
14
|
+
/** Horloge, en millisecondes. */
|
|
15
|
+
readonly now?: () => number;
|
|
16
|
+
/** Empreinte d'un ticket (instrumentée pour compter les hachages). */
|
|
17
|
+
readonly fingerprint?: (ticket: string) => string;
|
|
18
|
+
}
|
|
19
|
+
/** Empreinte sous laquelle un ticket est rangé : le ticket brut n'est jamais gardé. */
|
|
20
|
+
export declare function fingerprintSessionTicket(ticket: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Tickets à usage unique qui autorisent l'ouverture d'UNE session pour une
|
|
23
|
+
* identité donnée — celle au nom de laquelle un outil MCP tire de vraies
|
|
24
|
+
* requêtes HTTP vers l'application.
|
|
25
|
+
*
|
|
26
|
+
* Le ticket ne quitte jamais le process : l'outil qui l'émet est celui qui le
|
|
27
|
+
* présente, en boucle locale, à la route de connexion de sa porte
|
|
28
|
+
* (`defineTicketSessionGate`). L'identité est fixée à l'émission — la
|
|
29
|
+
* requête de rachat n'en porte aucune, donc ne peut pas en choisir une autre.
|
|
30
|
+
*
|
|
31
|
+
* Aucune minuterie : les tickets expirés sont purgés à chaque émission et à
|
|
32
|
+
* chaque rachat, sur au plus {@link SESSION_TICKET_MAX_PENDING} entrées.
|
|
33
|
+
*/
|
|
34
|
+
export declare class SessionTicketStore {
|
|
35
|
+
#private;
|
|
36
|
+
/**
|
|
37
|
+
* @param options - horloge et empreinte, injectées par les tests.
|
|
38
|
+
*/
|
|
39
|
+
constructor(options?: ISessionTicketStoreOptions);
|
|
40
|
+
/** Nombre de tickets émis, ni rachetés ni expirés. */
|
|
41
|
+
get pending(): number;
|
|
42
|
+
/**
|
|
43
|
+
* Émet un ticket pour `identifier`, valable {@link SESSION_TICKET_TTL_MS} ms.
|
|
44
|
+
*
|
|
45
|
+
* @param identifier - identifiant du compte pour lequel la session s'ouvrira.
|
|
46
|
+
* @returns le ticket, à présenter dans {@link SESSION_TICKET_HEADER}.
|
|
47
|
+
* @throws Error - identifiant vide, ou plafond de tickets en attente atteint.
|
|
48
|
+
*/
|
|
49
|
+
issue(identifier: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Rachète un ticket : le retire, et rend l'identité pour laquelle il a été émis.
|
|
52
|
+
*
|
|
53
|
+
* La forme est contrôlée AVANT l'empreinte : une entrée hostile (1 Mo, autre
|
|
54
|
+
* type) ne coûte pas un hachage.
|
|
55
|
+
*
|
|
56
|
+
* @param ticket - valeur reçue, non fiable.
|
|
57
|
+
* @returns l'identifiant, ou `null` (absent, malformé, inconnu, expiré, déjà racheté).
|
|
58
|
+
*/
|
|
59
|
+
redeem(ticket: unknown): string | null;
|
|
60
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
import type { IMcpGate, IMcpGateRuntime } from "./gate.js";
|
|
3
|
+
/**
|
|
4
|
+
* Le transport stdio de MCP : le client lance le serveur et lui parle par
|
|
5
|
+
* l'entrée et la sortie standard, un message JSON-RPC par ligne.
|
|
6
|
+
*
|
|
7
|
+
* Toute la règle vit dans la porte ({@link IMcpGate.respondStdio}) : ce
|
|
8
|
+
* fichier n'apporte que la lecture ligne à ligne et l'écriture des réponses.
|
|
9
|
+
*/
|
|
10
|
+
/** Ce que sert une boucle stdio. */
|
|
11
|
+
export interface IMcpStdioOptions {
|
|
12
|
+
/** L'entrée du protocole — `process.stdin` en pratique. */
|
|
13
|
+
input: Readable;
|
|
14
|
+
/**
|
|
15
|
+
* Écrit UNE ligne sur la sortie du protocole, saut de ligne compris. La
|
|
16
|
+
* sortie standard EST le protocole : rien d'autre ne doit y écrire.
|
|
17
|
+
*/
|
|
18
|
+
write: (line: string) => void;
|
|
19
|
+
/** Modules, identité, journal — transmis à la porte. */
|
|
20
|
+
runtime?: IMcpGateRuntime | undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Sert une porte MCP sur un flux de lignes, jusqu'à la fin de l'entrée.
|
|
24
|
+
*
|
|
25
|
+
* Chaque ligne est servie SANS attendre la précédente : une
|
|
26
|
+
* `notifications/cancelled` doit pouvoir arriver pendant l'appel qu'elle
|
|
27
|
+
* vise. Les réponses partent donc dans l'ordre où elles se terminent — chacune
|
|
28
|
+
* porte son identifiant, c'est ce que JSON-RPC prévoit. Une ligne vide est
|
|
29
|
+
* ignorée ; une ligne illisible rend `-32700` sans arrêter la boucle.
|
|
30
|
+
*
|
|
31
|
+
* L'entrée fermée, le client est parti : les appels encore en cours sont
|
|
32
|
+
* abattus (leur signal), puis la promesse se résout quand tous ont rendu.
|
|
33
|
+
*
|
|
34
|
+
* @param gate - la porte à servir.
|
|
35
|
+
* @param options - entrée, écriture d'une ligne, environnement de la porte.
|
|
36
|
+
* @returns une promesse résolue à la fin de l'entrée, appels en cours clos.
|
|
37
|
+
*/
|
|
38
|
+
export declare function serveMcpStdio(gate: IMcpGate, options: IMcpStdioOptions): Promise<void>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { IMcpTransport } from "./server.js";
|
|
2
|
+
/**
|
|
3
|
+
* Le flux de réponse d'une requête MCP, ouvert PARESSEUSEMENT — brique
|
|
4
|
+
* générique de la boîte à outils, indépendante du transport.
|
|
5
|
+
*
|
|
6
|
+
* Toute porte (HTTP de développement, porte d'une application, futur `stdio`)
|
|
7
|
+
* a la même décision à prendre : répondre en un objet, ou en flux quand
|
|
8
|
+
* l'appel a quelque chose à dire en route. La règle vit ICI, une fois : le flux
|
|
9
|
+
* ne s'ouvre qu'au PREMIER envoi, les envois s'enchaînent dans l'ordre, et la
|
|
10
|
+
* réponse finale clôt le flux. Un appel qui ne dit rien en route n'ouvre rien —
|
|
11
|
+
* la porte répond alors comme avant.
|
|
12
|
+
*
|
|
13
|
+
* Exception : un appel encore en cours au bout du délai de grâce
|
|
14
|
+
* ({@link MCP_STREAM_GRACE_MS}) ouvre le flux sans rien y écrire. La norme
|
|
15
|
+
* oblige le client à accepter une réponse SSE (`transports/streamable-http`),
|
|
16
|
+
* avec ou sans `progressToken` ; or une réponse JSON qui tarde tombe sous le
|
|
17
|
+
* délai d'inactivité de la requête (408), quand un flux ouvert le désarme et
|
|
18
|
+
* bat de lui-même. Un outil court n'y voit rien : il a répondu avant.
|
|
19
|
+
*
|
|
20
|
+
* La porte ne fournit que ce qu'elle seule sait faire : OUVRIR son flux
|
|
21
|
+
* (`renderSse()` pour une route HTTP).
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Délai de grâce, en ms : au-delà, un appel sans réponse ouvre le flux. Très en
|
|
25
|
+
* deçà du délai d'inactivité d'une requête (30 s par défaut), assez long pour
|
|
26
|
+
* qu'un outil ordinaire réponde en JSON comme avant.
|
|
27
|
+
*/
|
|
28
|
+
export declare const MCP_STREAM_GRACE_MS = 5000;
|
|
29
|
+
/** Ce qu'une porte sait ouvrir : un puits d'événements, ordonné. */
|
|
30
|
+
export interface IMcpEventSink {
|
|
31
|
+
/** Le flux est-il fermé (client parti, ou clos) ? */
|
|
32
|
+
readonly closed: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Envoie un message JSON-RPC ; peut lever à l'appel ou rendre une promesse
|
|
35
|
+
* de contre-pression.
|
|
36
|
+
*/
|
|
37
|
+
send(data: unknown): Promise<void> | undefined;
|
|
38
|
+
}
|
|
39
|
+
/** Le flux de réponse d'UNE requête, vu de la porte. */
|
|
40
|
+
export interface IMcpResponseStream<S extends IMcpEventSink> {
|
|
41
|
+
/** À donner au protocole (`IMcpServerContext.transport`). */
|
|
42
|
+
readonly transport: IMcpTransport;
|
|
43
|
+
/**
|
|
44
|
+
* Attend les envois en cours puis, si un flux a été ouvert, y écrit la
|
|
45
|
+
* réponse finale.
|
|
46
|
+
*
|
|
47
|
+
* @param body - la réponse JSON-RPC finale.
|
|
48
|
+
* @returns le flux ouvert (à clore par la porte), ou `null` : rien n'a été
|
|
49
|
+
* envoyé en route, la porte répond en un objet.
|
|
50
|
+
*/
|
|
51
|
+
finish(body: unknown): Promise<S | null>;
|
|
52
|
+
}
|
|
53
|
+
/** Options de {@link createMcpResponseStream}. */
|
|
54
|
+
export interface IMcpResponseStreamOptions<S extends IMcpEventSink> {
|
|
55
|
+
/** Abattu quand le client abandonne la requête. */
|
|
56
|
+
signal?: AbortSignal | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Ouvre le flux de la porte ; absent quand le client n'en accepte pas — la
|
|
59
|
+
* progression devient alors muette, jamais refusée.
|
|
60
|
+
*/
|
|
61
|
+
open?: (() => S | Promise<S>) | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Ouvre le flux si l'appel n'a pas répondu dans ce délai (ms) ; absent ou 0 :
|
|
64
|
+
* le flux ne s'ouvre qu'au premier envoi.
|
|
65
|
+
*/
|
|
66
|
+
graceMs?: number | undefined;
|
|
67
|
+
/** Un envoi perdu (client parti, événement refusé) : à journaliser. */
|
|
68
|
+
onError?: ((error: unknown, final: boolean) => void) | undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Fabrique le flux de réponse paresseux d'une requête MCP.
|
|
72
|
+
*
|
|
73
|
+
* @param options - signal d'abandon, ouverture du flux, journal des pertes.
|
|
74
|
+
* @returns le transport à donner au protocole, et `finish()`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function createMcpResponseStream<S extends IMcpEventSink>(options: IMcpResponseStreamOptions<S>): IMcpResponseStream<S>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Controller } from "@nodefony/framework";
|
|
3
|
+
import type { ContextType } from "@nodefony/http";
|
|
4
|
+
import { SessionTicketStore } from "./sessionTicket.js";
|
|
5
|
+
/** Message unique de tout refus : la cause fine ne sort jamais vers l'appelant. */
|
|
6
|
+
export declare const TICKET_SESSION_REFUSED = "ticket de session refus\u00E9";
|
|
7
|
+
/** Ce que la route reçoit, tel que la requête le présente. */
|
|
8
|
+
export interface ITicketSessionRequest {
|
|
9
|
+
/** Valeur de l'en-tête du ticket, non fiable. */
|
|
10
|
+
readonly ticket: unknown;
|
|
11
|
+
/** Adresse distante de la connexion. */
|
|
12
|
+
readonly remoteAddress: string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* La requête a traversé un relais (`isRelayedRequest`) : sur une porte
|
|
15
|
+
* locale, l'adresse ne prouve alors plus la localité.
|
|
16
|
+
*/
|
|
17
|
+
readonly relayed?: boolean | undefined;
|
|
18
|
+
}
|
|
19
|
+
/** Compte pour lequel la session vient d'être ouverte. */
|
|
20
|
+
export interface ITicketSessionUser {
|
|
21
|
+
readonly identifier: string;
|
|
22
|
+
readonly roles?: readonly string[] | undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Ce dont la décision a besoin, fourni par la porte. */
|
|
25
|
+
export interface ITicketSessionDeps {
|
|
26
|
+
/** Magasin des tickets, `null` si aucun n'a jamais été émis. */
|
|
27
|
+
readonly tickets: SessionTicketStore | null;
|
|
28
|
+
/**
|
|
29
|
+
* Ouvre la session pour `identifier` par le flux de session de l'application,
|
|
30
|
+
* `null` quand aucun n'est installé. Lève si le compte est inconnu ou refusé.
|
|
31
|
+
*/
|
|
32
|
+
readonly establish: ((identifier: string) => Promise<ITicketSessionUser>) | null;
|
|
33
|
+
/** Pseudonyme public de la session ouverte, `null` si aucune ne l'est. */
|
|
34
|
+
readonly sessionRef: () => string | null;
|
|
35
|
+
/**
|
|
36
|
+
* Refuser tout appel qui ne vient pas de la machine locale. Absent = `true` :
|
|
37
|
+
* le ticket ne quitte jamais le process, la boucle locale est son seul chemin.
|
|
38
|
+
*/
|
|
39
|
+
readonly localOnly?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** Verdict de la route : la session ouverte, ou un refus. */
|
|
42
|
+
export type TicketSessionVerdict = {
|
|
43
|
+
readonly status: 200;
|
|
44
|
+
readonly identifier: string;
|
|
45
|
+
readonly roles: readonly string[];
|
|
46
|
+
readonly ref: string;
|
|
47
|
+
} | {
|
|
48
|
+
readonly status: 403 | 503;
|
|
49
|
+
readonly message: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Décide l'ouverture d'une session par ticket.
|
|
53
|
+
*
|
|
54
|
+
* Ordre délibéré : la localité, puis la présence d'un flux de session, et
|
|
55
|
+
* seulement ensuite le rachat du ticket — un appel qui ne peut pas aboutir ne
|
|
56
|
+
* consomme pas le ticket. L'identité vient du TICKET, jamais de la requête.
|
|
57
|
+
*
|
|
58
|
+
* @param request - le ticket présenté et l'adresse de l'appelant.
|
|
59
|
+
* @param deps - magasin de tickets, flux de session, exigence de localité.
|
|
60
|
+
* @returns `200` avec le compte et le pseudonyme de session, `403` uniforme, ou
|
|
61
|
+
* `503` quand l'application n'a pas de flux de session.
|
|
62
|
+
*/
|
|
63
|
+
export declare function openTicketSession(request: ITicketSessionRequest, deps: ITicketSessionDeps): Promise<TicketSessionVerdict>;
|
|
64
|
+
/**
|
|
65
|
+
* Vue minimale du flux de session (`authFlow`, fourni par `@nodefony/security`).
|
|
66
|
+
* Ce module n'en dépend pas : le service se résout par NOM.
|
|
67
|
+
*/
|
|
68
|
+
export interface ISessionFlowView {
|
|
69
|
+
establishSessionFor(context: ContextType, identifier: string, reason?: string): Promise<unknown>;
|
|
70
|
+
}
|
|
71
|
+
/** Vue minimale du service de sessions (`sessions`, `@nodefony/http`). */
|
|
72
|
+
export interface ISessionServiceView {
|
|
73
|
+
/** Pseudonyme public d'une session, non réversible. */
|
|
74
|
+
sessionRef(id: string): string;
|
|
75
|
+
/** Détruit la session désignée par son pseudonyme. */
|
|
76
|
+
destroyByRef(ref: string, actor?: string | null): Promise<boolean>;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Le service résolu est-il un flux de session utilisable ?
|
|
80
|
+
*
|
|
81
|
+
* @param value - service lu dans le conteneur.
|
|
82
|
+
* @returns `true` s'il ouvre une session pour une identité prouvée.
|
|
83
|
+
*/
|
|
84
|
+
export declare function isSessionFlowView(value: unknown): value is ISessionFlowView;
|
|
85
|
+
/**
|
|
86
|
+
* Le service résolu est-il un service de sessions utilisable ?
|
|
87
|
+
*
|
|
88
|
+
* @param value - service lu dans le conteneur.
|
|
89
|
+
* @returns `true` s'il dérive un pseudonyme et détruit une session par lui.
|
|
90
|
+
*/
|
|
91
|
+
export declare function isSessionServiceView(value: unknown): value is ISessionServiceView;
|
|
92
|
+
/**
|
|
93
|
+
* Lit le compte rendu par le flux de session, sans lui faire confiance.
|
|
94
|
+
*
|
|
95
|
+
* @param value - retour de `establishSessionFor`.
|
|
96
|
+
* @param identifier - identité du ticket, retenue si le retour n'en porte pas.
|
|
97
|
+
* @returns l'identifiant et les rôles chaîne du compte.
|
|
98
|
+
*/
|
|
99
|
+
export declare function toTicketSessionUser(value: unknown, identifier: string): ITicketSessionUser;
|
|
100
|
+
/**
|
|
101
|
+
* Lit, côté outil, la réponse de la route de connexion par ticket — sans lui
|
|
102
|
+
* faire confiance. Vit à côté de {@link openTicketSession}, qui en produit la
|
|
103
|
+
* forme : deux copies du format divergeraient.
|
|
104
|
+
*
|
|
105
|
+
* @param body - corps de la réponse, `null` si aucun.
|
|
106
|
+
* @returns le compte et le pseudonyme de session, ou le message du refus.
|
|
107
|
+
*/
|
|
108
|
+
export declare function readTicketSessionVerdict(body: string | null): {
|
|
109
|
+
identifier: string;
|
|
110
|
+
roles: readonly string[];
|
|
111
|
+
ref: string;
|
|
112
|
+
} | {
|
|
113
|
+
message: string;
|
|
114
|
+
};
|
|
115
|
+
/** Définition d'une porte de session par ticket — validée et figée au boot. */
|
|
116
|
+
export declare const ticketSessionGateSchema: z.ZodObject<{
|
|
117
|
+
factor: z.ZodString;
|
|
118
|
+
localOnly: z.ZodDefault<z.ZodBoolean>;
|
|
119
|
+
}, z.core.$strict>;
|
|
120
|
+
/** Ce qu'on passe à {@link defineTicketSessionGate}. */
|
|
121
|
+
export type ITicketSessionGateDefinition = z.input<typeof ticketSessionGateSchema>;
|
|
122
|
+
/** Réglages effectifs d'une porte de session par ticket. */
|
|
123
|
+
export type ITicketSessionGateSettings = z.output<typeof ticketSessionGateSchema>;
|
|
124
|
+
/** Une porte de session par ticket définie — à servir depuis une action de controller. */
|
|
125
|
+
export interface ITicketSessionGate {
|
|
126
|
+
/** Les réglages effectifs, validés et figés. */
|
|
127
|
+
readonly settings: ITicketSessionGateSettings;
|
|
128
|
+
/** Tickets émis, ni rachetés ni expirés. */
|
|
129
|
+
readonly pending: number;
|
|
130
|
+
/**
|
|
131
|
+
* Émet un ticket à usage unique pour `identifier`.
|
|
132
|
+
*
|
|
133
|
+
* @param identifier - compte au nom duquel la session s'ouvrira.
|
|
134
|
+
* @returns le ticket, à présenter dans {@link SESSION_TICKET_HEADER}.
|
|
135
|
+
* @throws Error - identifiant vide, ou plafond de tickets en attente atteint.
|
|
136
|
+
*/
|
|
137
|
+
issue(identifier: string): string;
|
|
138
|
+
/**
|
|
139
|
+
* Sert la requête courante d'un controller : rachète le ticket et ouvre la
|
|
140
|
+
* session par le flux de l'application (`authFlow`, résolu par NOM).
|
|
141
|
+
*
|
|
142
|
+
* @param controller - le controller qui déclare la route (`POST`, chemin littéral).
|
|
143
|
+
* @returns ce que l'action doit rendre : le verdict en JSON, à son statut.
|
|
144
|
+
*/
|
|
145
|
+
serve(controller: Controller): Promise<unknown>;
|
|
146
|
+
/**
|
|
147
|
+
* Détruit une session ouverte par ticket, au nom du facteur de la porte.
|
|
148
|
+
*
|
|
149
|
+
* @param sessions - le service `sessions` lu dans le conteneur.
|
|
150
|
+
* @param ref - pseudonyme rendu par la route.
|
|
151
|
+
* @returns `true` si une session a été détruite.
|
|
152
|
+
*/
|
|
153
|
+
close(sessions: unknown, ref: string): Promise<boolean>;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Définit une porte de session par ticket : ce qui permet à un outil MCP de
|
|
157
|
+
* tirer de vraies requêtes HTTP vers l'application AU NOM d'un compte.
|
|
158
|
+
*
|
|
159
|
+
* Une session ne s'ouvre que par le flux de session de l'application
|
|
160
|
+
* (`authFlow.establishSessionFor`) : il revérifie le compte (actif, non
|
|
161
|
+
* verrouillé), régénère l'identifiant et écrit l'audit. Ce flux exige une
|
|
162
|
+
* requête en cours ; l'outil en envoie donc une, en boucle locale, à la route
|
|
163
|
+
* que le controller déclare, et récupère le cookie que la réponse pose.
|
|
164
|
+
* Fabriquer la session sans requête aurait recopié la règle de connexion.
|
|
165
|
+
*
|
|
166
|
+
* Comme {@link defineMcpGate}, la porte ne monte AUCUNE route : le controller
|
|
167
|
+
* qui en a besoin la déclare, chemin écrit en littéral, et appelle `serve`.
|
|
168
|
+
* Le magasin de tickets n'est alloué qu'au premier ticket émis.
|
|
169
|
+
*
|
|
170
|
+
* @param definition - facteur d'audit et exigence de localité.
|
|
171
|
+
* @returns la porte, figée.
|
|
172
|
+
* @throws BootConfigurationError - définition hors schéma.
|
|
173
|
+
*/
|
|
174
|
+
export declare function defineTicketSessionGate(definition: ITicketSessionGateDefinition): ITicketSessionGate;
|