@nodefony/security 10.0.0-beta.5 → 10.0.0-beta.7

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 (104) hide show
  1. package/README.md +24 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/index.js +63 -61
  4. package/dist/nodefony/command/security-mcp-token.js +43 -5
  5. package/dist/nodefony/config/config.js +30 -11
  6. package/dist/nodefony/config/defineModuleConfig.js +52 -1
  7. package/dist/nodefony/contracts/IWebhookPayload.js +1 -0
  8. package/dist/nodefony/errors/FetchPublicError.js +18 -0
  9. package/dist/nodefony/errors/SsrfError.js +9 -1
  10. package/dist/nodefony/errors/StaleSessionError.js +20 -0
  11. package/dist/nodefony/errors/index.js +7 -5
  12. package/dist/nodefony/keycloak/keycloakAdmin.js +3 -3
  13. package/dist/nodefony/keycloak/scaffold.js +31 -14
  14. package/dist/nodefony/keycloak/security-keycloak-realm.js +1 -1
  15. package/dist/nodefony/service/accessTokenVerifier.js +6 -6
  16. package/dist/nodefony/service/apiKeys.js +8 -4
  17. package/dist/nodefony/service/auditService.js +48 -3
  18. package/dist/nodefony/service/authFlow.js +9 -6
  19. package/dist/nodefony/service/authorization.js +1 -1
  20. package/dist/nodefony/service/firewall.js +281 -44
  21. package/dist/nodefony/service/keycloak.js +1 -1
  22. package/dist/nodefony/service/oauth2.js +8 -4
  23. package/dist/nodefony/service/securityHeaders.js +340 -47
  24. package/dist/nodefony/service/tokenService.js +16 -11
  25. package/dist/nodefony/service/totp.js +10 -5
  26. package/dist/nodefony/service/webAuthn.js +46 -7
  27. package/dist/nodefony/service/webhooks.js +11 -4
  28. package/dist/nodefony/src/admin/SecurityAdminApi.js +19 -0
  29. package/dist/nodefony/src/audit/AuditRefusalAggregator.js +139 -0
  30. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +1 -1
  31. package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +31 -7
  32. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +62 -20
  33. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +29 -6
  34. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +22 -9
  35. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +2 -2
  36. package/dist/nodefony/src/csp.js +265 -10
  37. package/dist/nodefony/src/cspReports.js +307 -0
  38. package/dist/nodefony/src/net/fetchPublic.js +244 -0
  39. package/dist/nodefony/src/net/ssrfGuard.js +2 -2
  40. package/dist/nodefony/src/oauth/clientCredentials.js +1 -1
  41. package/dist/nodefony/src/oauth/metadata.js +1 -1
  42. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +17 -4
  43. package/dist/nodefony/src/oauth/providers/github.js +1 -1
  44. package/dist/nodefony/src/realtime/UserRealtimeToken.js +59 -2
  45. package/dist/nodefony/src/realtime/frameAuthorizer.js +136 -44
  46. package/dist/nodefony/src/token/RemoteJwtVerifier.js +8 -2
  47. package/dist/nodefony/src/token/jwtRuntime.js +22 -2
  48. package/dist/nodefony/src/token/tokenTimeClaims.js +34 -0
  49. package/dist/nodefony/src/totp/totpOperations.js +1 -1
  50. package/dist/nodefony/src/voter/RoleVoter.js +1 -1
  51. package/dist/nodefony/src/webhook/WebhookDispatcher.js +71 -17
  52. package/dist/nodefony/src/webhook/webhookDelivery.js +37 -86
  53. package/dist/nodefony/testing/index.js +26 -36
  54. package/dist/types/index.d.ts +5 -1
  55. package/dist/types/nodefony/command/security-mcp-token.d.ts +12 -0
  56. package/dist/types/nodefony/config/config.d.ts +8 -0
  57. package/dist/types/nodefony/contracts/IApiKey.d.ts +8 -2
  58. package/dist/types/nodefony/contracts/IFirewall.d.ts +15 -2
  59. package/dist/types/nodefony/contracts/ITokenStore.d.ts +7 -3
  60. package/dist/types/nodefony/contracts/IWebhookEndpoint.d.ts +13 -3
  61. package/dist/types/nodefony/contracts/IWebhookPayload.d.ts +24 -0
  62. package/dist/types/nodefony/errors/FetchPublicError.d.ts +15 -0
  63. package/dist/types/nodefony/errors/SsrfError.d.ts +8 -1
  64. package/dist/types/nodefony/errors/StaleSessionError.d.ts +17 -0
  65. package/dist/types/nodefony/errors/index.d.ts +3 -0
  66. package/dist/types/nodefony/keycloak/scaffold.d.ts +12 -6
  67. package/dist/types/nodefony/service/apiKeys.d.ts +3 -0
  68. package/dist/types/nodefony/service/authFlow.d.ts +2 -2
  69. package/dist/types/nodefony/service/firewall.d.ts +28 -7
  70. package/dist/types/nodefony/service/securityHeaders.d.ts +82 -11
  71. package/dist/types/nodefony/service/webAuthn.d.ts +13 -0
  72. package/dist/types/nodefony/service/webhooks.d.ts +7 -1
  73. package/dist/types/nodefony/src/admin/SecurityAdminApi.d.ts +3 -0
  74. package/dist/types/nodefony/src/audit/AuditRefusalAggregator.d.ts +55 -0
  75. package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +11 -0
  76. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +17 -3
  77. package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +11 -4
  78. package/dist/types/nodefony/src/csp.d.ts +142 -0
  79. package/dist/types/nodefony/src/cspReports.d.ts +73 -0
  80. package/dist/types/nodefony/src/net/fetchPublic.d.ts +73 -0
  81. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +15 -0
  82. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +35 -1
  83. package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +10 -2
  84. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +42 -18
  85. package/dist/types/nodefony/src/token/jwtRuntime.d.ts +21 -2
  86. package/dist/types/nodefony/src/token/tokenTimeClaims.d.ts +32 -0
  87. package/dist/types/nodefony/src/webhook/WebhookDispatcher.d.ts +36 -1
  88. package/dist/types/nodefony/src/webhook/webhookDelivery.d.ts +5 -2
  89. package/dist/types/nodefony/testing/index.d.ts +6 -0
  90. package/docs/api-keys.md +3 -1
  91. package/docs/audit.md +112 -28
  92. package/docs/authenticators.md +6 -4
  93. package/docs/csrf.md +3 -2
  94. package/docs/external-jwt.md +5 -1
  95. package/docs/firewall.md +3 -2
  96. package/docs/headers.md +427 -71
  97. package/docs/keycloak.md +4 -4
  98. package/docs/login-page.md +4 -3
  99. package/docs/obtenir-un-jeton.md +17 -10
  100. package/docs/tokens.md +1 -1
  101. package/docs/totp.md +2 -2
  102. package/docs/webauthn.md +2 -2
  103. package/docs/webhooks.md +63 -18
  104. package/package.json +17 -17
@@ -13,6 +13,72 @@
13
13
  */
14
14
  /** Fragment additif d'un module : directive CSP → sources à ajouter. */
15
15
  export type CspFragment = Record<string, readonly string[]>;
16
+ /**
17
+ * Dit si une source CSP ouvre la directive à n'importe quel contenu.
18
+ *
19
+ * Trop large : `*`, un schéma réseau nu (`https:`, `wss:`…), un hôte joker seul
20
+ * (`https://*`, `*:443`). Restent acceptés : une origine exacte, un joker de
21
+ * sous-domaine borné (`https://*.example.com`), un mot-clé (`'self'`) et les
22
+ * schémas locaux (`blob:`, `data:`) — SAUF dans une directive de scripts :
23
+ * `data:` y exécute le code porté par l'URL, `blob:` celui qu'un script a
24
+ * fabriqué, et sans nonce c'est une injection de script.
25
+ *
26
+ * @param source - un jeton de source CSP.
27
+ * @param directive - la directive qui porte la source ; sans elle, les schémas
28
+ * locaux sont acceptés.
29
+ * @returns `true` si la source autorise n'importe quel hôte ou contenu.
30
+ */
31
+ export declare function isOverbroadSource(source: string, directive?: string): boolean;
32
+ /**
33
+ * Retire d'un fragment de MODULE ses sources trop larges (cf {@link isOverbroadSource}).
34
+ *
35
+ * Moindre privilège : une contribution GLOBALE (`Firewall.registerCspOrigins`)
36
+ * ne déclare que SON origine, et n'ouvre pas la politique de toute l'application.
37
+ * Retirer plutôt que lever : la sécurité ne bloque pas le démarrage. Ne
38
+ * s'applique PAS à ce que l'application écrit elle-même (sa config, un `@Csp` de
39
+ * route) : un choix délibéré de son auteur n'est jamais contourné en silence.
40
+ *
41
+ * @param fragment - directives déclarées par un module.
42
+ * @returns le fragment sans les sources trop larges, et les paires retirées.
43
+ */
44
+ export declare function withoutOverbroadSources(fragment: CspFragment): {
45
+ fragment: CspFragment;
46
+ dropped: Array<[string, string]>;
47
+ };
48
+ /**
49
+ * Relève les sources trop larges d'un jeu de directives (cf {@link isOverbroadSource}).
50
+ *
51
+ * @param directives - paires `[directive, sources]` (`parseCsp(csp)` ou `Object.entries(fragment)`).
52
+ * @returns les paires `[directive, source]` trop larges, dans l'ordre de lecture.
53
+ */
54
+ export declare function findOverbroadSources(directives: Iterable<readonly [string, readonly string[]]>): Array<[string, string]>;
55
+ /**
56
+ * Relève les sources de scripts d'un fragment que `'strict-dynamic'` rend inertes.
57
+ *
58
+ * Sous `'strict-dynamic'` dans `script-src`, un navigateur CSP3 ignore les hôtes
59
+ * et les schémas : un script n'est admis que signé, ou chargé par un script
60
+ * signé (W3C CSP3 § « strict-dynamic »). La source reste utile à un navigateur
61
+ * CSP2 ; on la garde, mais son auteur doit savoir qu'elle n'ouvre rien.
62
+ *
63
+ * @param fragment - directives déclarées par un module.
64
+ * @param policy - politique à laquelle le fragment s'ajoute.
65
+ * @returns les paires `[directive, source]` inertes ; vide sans `'strict-dynamic'`.
66
+ */
67
+ export declare function findInertScriptSources(fragment: CspFragment, policy: string): Array<[string, string]>;
68
+ /**
69
+ * Refuse un fragment CSP qui sortirait de la grammaire CSP3 — seule garde contre
70
+ * l'injection de directives par une valeur de configuration.
71
+ *
72
+ * Sans elle, une source comme `https://a.example; frame-src *` ajoute une
73
+ * directive que personne n'a demandée, et `https://a.example, frame-src *` coupe
74
+ * l'en-tête en deux politiques. Appelée là où un fragment ENTRE (enregistrement
75
+ * d'un module, fusion d'une route `@Csp`), jamais par requête.
76
+ *
77
+ * @param fragment - directives additionnelles (`directive → sources`).
78
+ * @param origin - qui déclare le fragment, pour nommer la faute.
79
+ * @throws BootConfigurationError si un nom ou une source sort de la grammaire.
80
+ */
81
+ export declare function assertCspFragment(fragment: CspFragment, origin: string): void;
16
82
  /**
17
83
  * Parse une chaîne CSP en directives ordonnées `[nom, sources[]]`. L'ordre des
18
84
  * directives et des sources est préservé (déterminisme : header stable, tests
@@ -32,8 +98,84 @@ export declare function serializeCsp(directives: Array<[string, string[]]>): str
32
98
  * origines (jamais par requête). Le résultat repart dans `SecurityHeaders`, qui
33
99
  * re-split autour de `{{nonce}}` au boot → 1 `join` par requête (hot-path inchangé).
34
100
  *
101
+ * Chaque fragment passe {@link assertCspFragment} : une source hors grammaire
102
+ * lève au lieu d'injecter une directive. Aucune source n'est retirée ici : le
103
+ * filtre des sources trop larges ne vise que les contributions globales des
104
+ * modules ({@link withoutOverbroadSources}, appliqué à l'enregistrement).
105
+ *
35
106
  * @param base - CSP de configuration (peut contenir `'nonce-{{nonce}}'`).
36
107
  * @param fragments - fragments additifs (un par module enregistré).
37
108
  * @returns la chaîne CSP fusionnée.
109
+ * @throws BootConfigurationError si un fragment sort de la grammaire CSP3.
38
110
  */
39
111
  export declare function mergeCspFragments(base: string, fragments: Iterable<CspFragment>): string;
112
+ /** Directive réservée aux réponses servies en TLS (ADR-0024 §C6). */
113
+ export declare const UPGRADE_INSECURE_REQUESTS = "upgrade-insecure-requests";
114
+ /**
115
+ * Retire une directive d'une politique, sans toucher au reste.
116
+ *
117
+ * Sert à composer, au démarrage, la variante EN CLAIR d'une politique qui porte
118
+ * `upgrade-insecure-requests` : sur une page servie en `http`, la directive
119
+ * réécrirait chaque sous-ressource en `https`, que le serveur ne sert pas.
120
+ *
121
+ * @param csp - politique (peut contenir `'nonce-{{nonce}}'`).
122
+ * @param name - nom de la directive, insensible à la casse.
123
+ * @returns la politique sans la directive ; la MÊME chaîne si elle n'y est pas.
124
+ */
125
+ export declare function withoutDirective(csp: string, name: string): string;
126
+ /**
127
+ * Traduit la valeur de `X-Frame-Options` en source de `frame-ancestors`.
128
+ *
129
+ * Un seul réglage, `http.securityHeaders.frameOptions`, pilote les DEUX en-têtes :
130
+ * un navigateur CSP ignore `X-Frame-Options` dès que `frame-ancestors` est
131
+ * présent (W3C CSP3 § « `frame-ancestors` »), et l'inverse vaut pour un ancien
132
+ * navigateur. Deux réglages distincts finiraient par dire deux choses.
133
+ *
134
+ * @param frameOptions - valeur émise de `X-Frame-Options` (`null` = non émis).
135
+ * @returns `'none'` pour `DENY`, `'self'` pour `SAMEORIGIN`, `null` sinon
136
+ * (en-tête coupé, ou valeur que `frame-ancestors` ne sait pas traduire).
137
+ */
138
+ export declare function frameAncestorsFor(frameOptions: string | null): string | null;
139
+ /**
140
+ * Ajoute à une politique la directive `frame-ancestors` dérivée de
141
+ * `X-Frame-Options` (cf {@link frameAncestorsFor}).
142
+ *
143
+ * L'application garde la main : une politique qui déclare DÉJÀ
144
+ * `frame-ancestors` est rendue telle quelle — c'est le geste qui autorise
145
+ * l'encadrement par une origine tierce. Une politique vide le reste (pas de CSP
146
+ * voulue). Appelée à la composition de la politique de base, jamais par requête.
147
+ *
148
+ * @param csp - politique de base (config `security.headers.csp`).
149
+ * @param frameOptions - valeur émise de `X-Frame-Options` (`null` = non émis).
150
+ * @returns la politique, complétée de `frame-ancestors` si besoin.
151
+ */
152
+ export declare function withFrameAncestors(csp: string, frameOptions: string | null): string;
153
+ /**
154
+ * Demande à une politique ses rapports de violation : `report-uri` (lu par
155
+ * tous les navigateurs, en http comme en https) ET `report-to` (Reporting API,
156
+ * que le navigateur n'honore que vers une origine sûre — https ou localhost).
157
+ * ADR-0024 §C7.
158
+ *
159
+ * L'application garde la main : une politique qui déclare DÉJÀ `report-uri` ou
160
+ * `report-to` est rendue telle quelle — elle a son collecteur. Une politique
161
+ * vide le reste.
162
+ *
163
+ * @param csp - politique de base.
164
+ * @param url - où poster les rapports (chemin absolu ou URL https).
165
+ * @param group - nom du groupe `report-to`, déclaré par `Reporting-Endpoints`.
166
+ * @returns la politique, et `added` vrai si les directives ont été posées ici —
167
+ * c'est alors à l'appelant d'émettre `Reporting-Endpoints`.
168
+ */
169
+ export declare function withViolationReports(csp: string, url: string, group: string): {
170
+ csp: string;
171
+ added: boolean;
172
+ };
173
+ /**
174
+ * Valide l'URL d'un collecteur de rapports écrite par l'application : un
175
+ * chemin absolu (`/csp`) ou une URL `https:` / `http:`, sans rien qu'un en-tête
176
+ * ou une directive CSP ne puisse porter (espace, `;`, `,`, guillemet).
177
+ *
178
+ * @param url - valeur de `headers.cspReport`.
179
+ * @returns la cause du refus, ou `null` si l'URL est utilisable.
180
+ */
181
+ export declare function reportUrlProblem(url: string): string | null;
@@ -0,0 +1,73 @@
1
+ import type { ICspReportsSnapshot } from "nodefony";
2
+ /** Sévérité d'une ligne du journal, au vocabulaire du syslog Nodefony. */
3
+ type LogSeverity = "WARNING" | "NOTICE";
4
+ /** Options d'un {@link CspReportCollector}. */
5
+ export interface ICspReportCollectorOptions {
6
+ /** URL que la politique désigne (rendue par la lecture). */
7
+ endpoint: string;
8
+ /** Violations distinctes gardées au plus. */
9
+ capacity?: number;
10
+ /** Rapports acceptés par adresse et par minute. */
11
+ perMinute?: number;
12
+ /** Adresses suivies au plus par le quota (borne mémoire). */
13
+ maxTrackedClients?: number;
14
+ /** Violations NEUVES journalisées au plus par minute, toutes adresses confondues. */
15
+ logPerMinute?: number;
16
+ /** Journal ; une violation neuve y part une fois, jamais à chaque répétition. */
17
+ log?: (message: string, severity: LogSeverity) => void;
18
+ /** Horloge injectable (ms), pour les tests. */
19
+ now?: () => number;
20
+ }
21
+ /** Ce que devient un rapport reçu — l'émetteur n'en saura rien (204 toujours). */
22
+ export type CspReportOutcome = "stored" | "duplicate" | "limited" | "unsupported" | "malformed";
23
+ /**
24
+ * Reçoit, expurge, dédoublonne et garde EN MÉMOIRE les rapports de violation
25
+ * CSP (ADR-0024 §C7). La porte qui les livre est publique — le navigateur
26
+ * n'envoie aucun identifiant — donc tout ce qui entre est traité comme écrit
27
+ * par un attaquant :
28
+ *
29
+ * - **quota par adresse** (fenêtre d'une minute, le limiteur de `@nodefony/http`) ;
30
+ * - **deux types de contenu seulement** : `application/csp-report` (`report-uri`)
31
+ * et `application/reports+json` (`report-to`) ;
32
+ * - **URL expurgées** de leur requête, de leur fragment et de leurs identifiants
33
+ * (la norme ne retire pas la requête : elle peut porter un jeton) ; une URL
34
+ * qui n'est pas HTTP(S) se réduit à son schéma ;
35
+ * - **l'extrait de code n'est jamais gardé** — c'est une chaîne de l'attaquant ;
36
+ * - **mémoire bornée** : au plus `capacity` violations distinctes, la moins
37
+ * récemment vue évincée d'abord ; une répétition incrémente un compteur ;
38
+ * - **journal dédoublonné et borné** : une violation neuve y part une fois, et
39
+ * pas plus de `logPerMinute` lignes par minute.
40
+ *
41
+ * Rien n'est écrit sur disque : le journal centralisé voit déjà ce qui compte.
42
+ */
43
+ export declare class CspReportCollector {
44
+ #private;
45
+ /** URL que la politique désigne. */
46
+ readonly endpoint: string;
47
+ /** Violations distinctes gardées au plus. */
48
+ readonly capacity: number;
49
+ /**
50
+ * @param options - URL désignée, bornes, journal, horloge.
51
+ */
52
+ constructor(options: ICspReportCollectorOptions);
53
+ /**
54
+ * Traite un corps de rapport déjà lu (et déjà borné par l'appelant).
55
+ *
56
+ * @param contentType - type de contenu de la requête, paramètres compris ou non.
57
+ * @param body - le corps, octets ou texte.
58
+ * @param client - adresse cliente résolue (proxy de confiance compris) ;
59
+ * `null` = inconnue, non comptée (jamais une clé commune à tous).
60
+ * @param userAgent - en-tête `User-Agent`, gardé tronqué.
61
+ * @returns ce que le rapport est devenu — pour le journal et les tests ;
62
+ * l'émetteur reçoit 204 quoi qu'il arrive.
63
+ */
64
+ receive(contentType: string | null | undefined, body: Uint8Array | string, client: string | null, userAgent?: string | null): CspReportOutcome;
65
+ /**
66
+ * Photo des violations gardées, la plus récemment vue d'abord — ce que rend
67
+ * le plan d'administration.
68
+ *
69
+ * @returns une copie : la modifier ne touche pas le collecteur.
70
+ */
71
+ snapshot(): ICspReportsSnapshot;
72
+ }
73
+ export {};
@@ -0,0 +1,73 @@
1
+ import { type IncomingHttpHeaders } from "node:http";
2
+ import { type DnsResolver } from "./ssrfGuard.js";
3
+ /**
4
+ * Requête sortante vers un site TIERS, sûre par construction — la primitive que
5
+ * tout code de Nodefony (webhooks, aperçu de lien, agents, RAG) appelle quand
6
+ * l'adresse vient d'un humain.
7
+ *
8
+ * Guide pour un agent : n'écris jamais `fetch(urlUtilisateur)`. Appelle
9
+ * {@link fetchPublic} ; la règle de cible vit dans `assertPublicUrl`
10
+ * (`ssrfGuard.ts`) et n'est jamais recopiée ici.
11
+ */
12
+ /** Options de {@link fetchPublic}. */
13
+ export interface IFetchPublicOptions {
14
+ /** Méthode HTTP. Défaut : `GET` (`POST` si un corps est fourni). */
15
+ readonly method?: string;
16
+ /** En-têtes ajoutés. Ni `Cookie` ni `Authorization` ne sont posés d'office. */
17
+ readonly headers?: Readonly<Record<string, string>>;
18
+ /** Corps de la requête (texte UTF-8 ou octets). */
19
+ readonly body?: string | Uint8Array;
20
+ /** Redirections suivies au plus. Défaut : `0` (un 3xx est rendu tel quel). */
21
+ readonly maxRedirects?: number;
22
+ /** Octets du corps de réponse lus au plus. Défaut : 1 Mio. */
23
+ readonly maxBytes?: number;
24
+ /** Délai TOTAL (résolution, connexions, redirections, lecture), en ms. Défaut : 10 000. */
25
+ readonly timeoutMs?: number;
26
+ /** Autorise `http:` en plus de `https:`. Défaut : `false`. */
27
+ readonly allowHttp?: boolean;
28
+ /** Autorise les cibles non publiques (dev/test seulement). Défaut : `false`. */
29
+ readonly allowPrivate?: boolean;
30
+ /** Résolveur DNS injectable (tests). Défaut : `node:dns`. */
31
+ readonly resolver?: DnsResolver;
32
+ /**
33
+ * IP DÉJÀ vérifiées par `assertPublicUrl` pour l'adresse de départ : la
34
+ * connexion y est épinglée sans nouvelle résolution. Pour un appelant qui a
35
+ * contrôlé l'adresse plus tôt (webhooks) ; les redirections, elles, sont
36
+ * toujours revérifiées.
37
+ */
38
+ readonly vettedAddresses?: readonly string[];
39
+ }
40
+ /** Réponse de {@link fetchPublic}. */
41
+ export interface IFetchPublicResult {
42
+ /** Code de statut HTTP de la réponse finale. */
43
+ readonly status: number;
44
+ /** En-têtes de la réponse finale. */
45
+ readonly headers: IncomingHttpHeaders;
46
+ /** Corps reçu, NON décodé (`Content-Encoding` n'est jamais appliqué). */
47
+ readonly body: Uint8Array;
48
+ /** `true` si `maxBytes` a coupé la lecture (la connexion a alors été détruite). */
49
+ readonly truncated: boolean;
50
+ /** Adresse de la réponse finale (après redirections). */
51
+ readonly url: URL;
52
+ /** Nombre de redirections suivies. */
53
+ readonly redirects: number;
54
+ }
55
+ /**
56
+ * Lit une URL fournie par un humain sans jamais atteindre le réseau interne.
57
+ *
58
+ * À CHAQUE saut (adresse de départ comme redirections) : protocole, absence
59
+ * d'identifiants et IP sont jugés par `assertPublicUrl`, puis la connexion est
60
+ * ÉPINGLÉE sur l'IP vérifiée (aucune seconde résolution : un rebinding DNS ne
61
+ * mène nulle part). Le délai est TOTAL — un serveur qui goutte un octet par
62
+ * seconde est coupé — et la lecture s'arrête à `maxBytes` en détruisant la
63
+ * connexion. Aucun cookie, aucun `Authorization` n'est ajouté ni conservé à
64
+ * travers une redirection, et le corps n'est jamais décodé (pas de bombe de
65
+ * décompression).
66
+ *
67
+ * @param rawUrl - adresse à lire.
68
+ * @param options - méthode, corps, bornes et politique de cible.
69
+ * @returns la réponse finale, corps borné et non décodé.
70
+ * @throws SsrfError si la cible, ou une redirection, est interdite.
71
+ * @throws FetchPublicError si le délai total, le nombre de redirections ou le réseau échoue.
72
+ */
73
+ export declare function fetchPublic(rawUrl: string, options?: IFetchPublicOptions): Promise<IFetchPublicResult>;
@@ -75,6 +75,13 @@ export interface IOAuthProviderRegistration {
75
75
  * émetteur. Absent : l'émetteur est facultatif (connu d'avance, ou inutile).
76
76
  */
77
77
  readonly requiresIssuer?: boolean;
78
+ /**
79
+ * Sources d'où le fournisseur sert les images du profil (avatars) —
80
+ * déclarées dans `img-src` de la politique de contenu quand, et seulement
81
+ * quand, le fournisseur est configuré. Origines exactes ; un joker de
82
+ * sous-domaine (`https://*.googleusercontent.com`) au plus.
83
+ */
84
+ readonly imageSources?: readonly string[];
78
85
  }
79
86
  /**
80
87
  * Enregistre (ou remplace) la fabrique d'un fournisseur OAuth. Appelée par les
@@ -85,6 +92,14 @@ export interface IOAuthProviderRegistration {
85
92
  * @param registration - exigences de configuration, vérifiées au démarrage.
86
93
  */
87
94
  export declare function registerOAuthProvider(name: string, factory: OAuthProviderFactory, registration?: IOAuthProviderRegistration): void;
95
+ /**
96
+ * Sources d'images (avatars) des fournisseurs nommés, sans doublon — ce que
97
+ * `img-src` doit autoriser pour afficher leurs photos de profil.
98
+ *
99
+ * @param names - fournisseurs configurés ; un nom inconnu ne rend rien
100
+ * @returns les sources déclarées par leurs enregistrements
101
+ */
102
+ export declare function oauthProviderImageSources(names: Iterable<string>): string[];
88
103
  /** Fabrique d'un fournisseur par nom, ou `undefined` si inconnu. */
89
104
  export declare function getOAuthProviderFactory(name: string): OAuthProviderFactory | undefined;
90
105
  /** `true` si le fournisseur nommé a déclaré exiger un émetteur en configuration. */
@@ -1,5 +1,17 @@
1
+ import type { ISubscriberIdentity, TokenStatus } from "nodefony";
1
2
  import type { IUser } from "@nodefony/user";
2
3
  import type { IRealtimeToken } from "./realtimeContracts.js";
4
+ /**
5
+ * Ce que l'identité d'admission tient du jeton d'ORIGINE et que l'utilisateur ne dit pas : son émetteur, et l'existence d'un compte local relisible.
6
+ */
7
+ export interface IRealtimeAdmissionSource {
8
+ /** `iss` du jeton vérifié ; `null` sans jeton (session, clé d'API). */
9
+ readonly issuer: string | null;
10
+ /** `false` pour un appelant machine sans compte local (`<émetteur>#<sujet>`). */
11
+ readonly account: boolean;
12
+ }
13
+ /** Une session ou une clé d'API : aucun émetteur, un compte local — figé et partagé. */
14
+ export declare const LOCAL_ACCOUNT: IRealtimeAdmissionSource;
3
15
  /**
4
16
  * Adaptateur `IUser` → `IRealtimeToken` (jeton realtime).
5
17
  *
@@ -32,8 +44,12 @@ export declare class UserRealtimeToken implements IRealtimeToken {
32
44
  * @param type - mode d'authentification réel (défaut `"session"`).
33
45
  * @param scopes - scopes délégués du jeton d'origine (défaut aucun).
34
46
  * @param confirmAccount - relecture du compte, payée sur `api.request` seul.
47
+ * @param status - la CAUSE de l'état, pour un jeton porteur (expiré ou révoqué) ;
48
+ * `null` pour une session, qui n'a qu'un verdict.
49
+ * @param admission - émetteur et compte local du jeton d'origine (défaut :
50
+ * aucun émetteur, compte local — une session).
35
51
  */
36
- constructor(user: IUser, revalidate?: ((nowMs?: number) => Promise<boolean>) | null, type?: string, scopes?: readonly string[], confirmAccount?: (() => Promise<boolean>) | null);
52
+ constructor(user: IUser, revalidate?: ((nowMs?: number) => Promise<boolean>) | null, type?: string, scopes?: readonly string[], confirmAccount?: (() => Promise<boolean>) | null, status?: ((nowMs?: number) => Promise<TokenStatus>) | null, admission?: IRealtimeAdmissionSource);
37
53
  /**
38
54
  * Zero Trust : l'identité qui a ouvert cette socket est-elle TOUJOURS vivante ?
39
55
  *
@@ -47,6 +63,14 @@ export declare class UserRealtimeToken implements IRealtimeToken {
47
63
  * sans dépendre de l'horloge de la machine.
48
64
  */
49
65
  isValid(nowMs?: number): Promise<boolean>;
66
+ /**
67
+ * La cause de l'état de l'identité — `expired` ferme la socket en 4006, `revoked`
68
+ * en 4001 (ADR-0021 §S11). Sans cause câblée (session), dérivée de
69
+ * {@link isValid} : `false` vaut `revoked`.
70
+ *
71
+ * @param nowMs - horloge injectable (epoch ms) ; par défaut l'heure courante.
72
+ */
73
+ status(nowMs?: number): Promise<TokenStatus>;
50
74
  /**
51
75
  * Le compte qui a ouvert la socket existe-t-il toujours, actif, et sous le
52
76
  * même `id` immuable ? `true` si aucune confirmation n'est câblée.
@@ -63,6 +87,16 @@ export declare class UserRealtimeToken implements IRealtimeToken {
63
87
  * « tous les droits », elle veut dire « aucun scope » — donc refus.
64
88
  */
65
89
  getScopes(): string[];
90
+ /**
91
+ * Identité d'admission gelée de la connexion — la donnée plate que reçoit la règle `allow` d'un canal (ADR-0021 §S10).
92
+ *
93
+ * Construite une fois, au premier appel, puis rendue par RÉFÉRENCE : les rôles
94
+ * y sont copiés et gelés une seule fois (là où {@link getRoles} recopie à
95
+ * chaque appel), si bien que ni le `welcome` ni l'admission ne recopient
96
+ * plus rien. Gelée : une règle `allow` ne peut pas s'ajouter un rôle.
97
+ * `id` est l'identifiant immuable du compte, jamais son login.
98
+ */
99
+ getIdentity(): ISubscriberIdentity;
66
100
  getAttribute<T = unknown>(key: string): T | undefined;
67
101
  }
68
102
  export default UserRealtimeToken;
@@ -1,4 +1,4 @@
1
- import type { FrameAuthorizer, IChannelPolicy, IRealtimeToken } from "./realtimeContracts.js";
1
+ import type { FrameAuthorizer, IChannelPolicies, IChannelPolicy, IRealtimeToken } from "./realtimeContracts.js";
2
2
  /**
3
3
  * Surface MINIMALE du firewall consommée par le verrou de frame : matcher une
4
4
  * zone par pathname ET vérifier un rôle (hiérarchie comprise). `Firewall` la
@@ -21,6 +21,12 @@ export interface IFrameAuthorizerFirewall {
21
21
  */
22
22
  export interface IChannelPolicyResolver {
23
23
  resolveChannelPolicy?(channel: string): IChannelPolicy | null;
24
+ /**
25
+ * Lecture ET écriture distincte d'un nom, en une résolution (ADR-0022 §6,
26
+ * décision 1 quater). Préférée quand elle existe ; un hub d'une version
27
+ * antérieure ne l'expose pas, et ne porte alors aucune écriture distincte.
28
+ */
29
+ resolveChannelPolicies?(channel: string): IChannelPolicies | null;
24
30
  }
25
31
  /**
26
32
  * Règle de canal **système** (plateforme) — un préfixe de namespace réservé et
@@ -42,7 +48,9 @@ export interface ISystemChannelRule {
42
48
  * @param surface - cible refusée : pont socket (`api.request`) ou canal
43
49
  * (subscribe/inbound).
44
50
  * @param target - pathname (api.request) ou nom de canal refusé.
45
- * @param reason - raison machine stable (`zone_protected` | `channel_policy`).
51
+ * @param reason - raison machine stable (`zone_protected` | `channel_policy`
52
+ * | `channel_write_policy` — l'écriture distincte du canal a refusé, la
53
+ * lecture était satisfaite).
46
54
  * @param token - jeton WS de l'acteur (lu pour `getUserIdentifier()`).
47
55
  */
48
56
  export type FrameDenyReporter = (surface: "api.request" | "channel", target: string, reason: string, token: IRealtimeToken) => void;
@@ -1,3 +1,4 @@
1
+ import type { IChannelPolicies, IChannelPolicy, ISubscriberIdentity, TokenStatus } from "nodefony";
1
2
  /**
2
3
  * Miroirs STRUCTURELS des contrats `@nodefony/realtime` — **zéro import** du
3
4
  * module realtime (ni runtime ni type-only).
@@ -32,12 +33,27 @@ export interface IRealtimeToken {
32
33
  * (absent = valide). Appelé par le pont `api.request` avant l'action data plane.
33
34
  */
34
35
  isValid?(nowMs?: number): boolean | Promise<boolean>;
36
+ /**
37
+ * La CAUSE de l'état de l'identité, quand elle est connaissable : `active`,
38
+ * `expired` (jeton porteur arrivé à `exp`) ou `revoked` (révoqué l'emporte sur
39
+ * expiré). Le hub en tire le code de fermeture d'une socket ouverte : `expired` →
40
+ * `TOKEN_EXPIRED_CLOSE_CODE` (4006), sinon `IDENTITY_DEAD_CLOSE_CODE` (4001) —
41
+ * ADR-0021 §S11. Optionnel : absent, `isValid` décide et son `false` vaut 4001
42
+ * (une session ne sait pas dire si elle a expiré ou été détruite).
43
+ */
44
+ status?(nowMs?: number): TokenStatus | Promise<TokenStatus>;
35
45
  /**
36
46
  * Le compte de la socket existe-t-il toujours, actif, sous le même `id` ?
37
47
  * (miroir de `IRealtimeToken.confirmAccount` realtime). Optionnel (absent =
38
48
  * confirmé). Relit l'annuaire : payé sur `api.request` seul, jamais au tick.
39
49
  */
40
50
  confirmAccount?(): Promise<boolean>;
51
+ /**
52
+ * Identité d'admission gelée de la connexion (miroir de
53
+ * `IRealtimeToken.getIdentity` realtime) — ce que reçoit la règle `allow`
54
+ * d'un canal. Absente, la connexion n'entre sur aucun canal à `allow`.
55
+ */
56
+ getIdentity?(): ISubscriberIdentity;
41
57
  }
42
58
  /**
43
59
  * Données de handshake WS (miroir de `IRealtimeHandshake`) — DTO neutre passé à
@@ -73,28 +89,30 @@ export interface IRealtimeAuthenticatorMatcher {
73
89
  /**
74
90
  * Verrou de frame (miroir de `FrameAuthorizer`) — SYNC : `true` = frame
75
91
  * autorisée. Lit le token déjà résolu au handshake (0 lecture base par frame).
92
+ *
93
+ * `judgesWrite` : le verrou juge l'écriture distincte d'un canal (ADR-0022 §6,
94
+ * décision 1 quater) ; un hub refuse de démarrer face à un verrou qui ne le
95
+ * déclare pas dès qu'une telle écriture est inscrite.
96
+ *
97
+ * `explain` : la phrase qui dit pourquoi une trame a été refusée, ou `null` —
98
+ * lue par le hub après un refus seulement (chemin froid).
76
99
  */
77
- export type FrameAuthorizer = (frame: unknown, token: IRealtimeToken) => boolean;
100
+ export type FrameAuthorizer = ((frame: unknown, token: IRealtimeToken) => boolean) & {
101
+ readonly judgesWrite?: true;
102
+ readonly explain?: (frame: unknown, token: IRealtimeToken) => string | null;
103
+ };
78
104
  /**
79
- * Politique d'autorisation d'un **canal** (miroir de `ChannelPolicy` realtime) —
80
- * exigences à satisfaire pour `subscribe`/inbound. Toutes les contraintes posées
81
- * sont cumulatives (ET) ; un champ absent = pas de contrainte sur cet axe. Une
82
- * politique entièrement vide = canal libre (équivaut à `null`).
105
+ * Politique d'autorisation d'un canal — déclaration UNIQUE au cœur (`nodefony`), que le
106
+ * hub realtime transporte et que ce module juge. Plus de miroir structurel : la
107
+ * copie dérivait en silence, chacune passant ses propres tests.
83
108
  *
84
- * Résolue 1× au `subscribe` (cold path : un client s'abonne rarement), comparée
85
- * au token déjà chargé au handshake → 0 lecture base. Deux origines : déclaration
86
- * **métier** (`@RealtimeChannel(name, opts)`, portée par le hub realtime) et
87
- * **plateforme** (`defineSecurityConfig().realtimeChannels` + namespaces système
88
- * réservés, portée par security — cf {@link buildFrameAuthorizer}).
109
+ * Résolue au `subscribe`/inbound (chemin froid), comparée au jeton déjà chargé
110
+ * à la poignée de main → 0 lecture base. Deux origines : déclaration MÉTIER
111
+ * (`@RealtimeChannel(nom, politique)`, portée par le hub) et PLATEFORME
112
+ * (`defineSecurityConfig().realtimeChannels` + namespaces système réservés —
113
+ * cf {@link buildFrameAuthorizer}).
89
114
  */
90
- export interface IChannelPolicy {
91
- /** Exige une connexion authentifiée (token non anonyme). */
92
- readonly authenticated?: boolean | undefined;
93
- /** Un de ces rôles suffit (évalué AVEC la hiérarchie de rôles du firewall). */
94
- readonly roles?: readonly string[] | undefined;
95
- /** Un de ces scopes suffit (axe API : JWT/clé API ; session BFF n'en porte pas). */
96
- readonly scopes?: readonly string[] | undefined;
97
- }
115
+ export type { IChannelPolicy, IChannelPolicies };
98
116
  /**
99
117
  * Surface consommée du `realtimeService` (miroir partiel de `RealtimeService`) —
100
118
  * seulement les seams que security câble/lit au boot et au dispatch. Résolu par
@@ -123,6 +141,12 @@ export interface IRealtimeService {
123
141
  * hub d'une version antérieure ne l'expose pas → traité comme `undefined`.
124
142
  */
125
143
  resolveChannelPolicy?(channel: string): IChannelPolicy | null;
144
+ /**
145
+ * Seam #1b — lecture ET écriture distincte d'un nom, en une résolution
146
+ * (ADR-0022 §6, décision 1 quater). Optionnel : un hub d'une version
147
+ * antérieure ne l'expose pas, et ne porte alors aucune écriture distincte.
148
+ */
149
+ resolveChannelPolicies?(channel: string): IChannelPolicies | null;
126
150
  /**
127
151
  * Namespaces de canaux **réservés à la plateforme**, tels que le hub les connaît
128
152
  * (`syslog:`, `orm:`, `kernel:`…). Le firewall y accroche ses politiques au lieu
@@ -9,7 +9,11 @@ import type { ISecurityConfig } from "../../config/defineModuleConfig.js";
9
9
  export interface IJwtRuntime {
10
10
  /** Émetteur (`iss`). */
11
11
  readonly issuer: string;
12
- /** Audiences acceptées au verify ; la première sert d'`aud` à l'émission. */
12
+ /**
13
+ * Liste blanche des audiences ÉMETTABLES (RFC 8707) ; la première est
14
+ * l'audience par défaut. Ce n'est pas ce qu'une zone accepte : une zone `jwt`
15
+ * n'accepte que SA ressource, ou l'audience par défaut si elle n'en a pas.
16
+ */
13
17
  readonly audiences: string[];
14
18
  /** TTL access token (s). */
15
19
  readonly accessTtlS: number;
@@ -24,8 +28,23 @@ export interface IJwtRuntime {
24
28
  * Dérive les paramètres effectifs depuis la config sécurité. `issuer` omis →
25
29
  * `"nodefony"` (DEVRAIT être surchargé en prod) ; `audiences` vide → l'app est sa
26
30
  * propre audience (`[issuer]`).
31
+ *
32
+ * La ressource d'une ZONE (`areas.<zone>.resource`) entre aussi dans la liste,
33
+ * APRÈS les audiences déclarées : c'est une ressource que ce serveur protège, et
34
+ * la redéclarer dans `jwt.audiences` était une seconde écriture de la même
35
+ * valeur — celle qu'on oublie, et dont le seul symptôme est un `invalid_target`
36
+ * à l'émission. Ajoutée en fin de liste, elle ne change jamais l'audience par
37
+ * défaut ({@link defaultAudience}).
38
+ *
39
+ * Elle n'ouvre rien de plus à la VÉRIFICATION : chaque zone, `jwt` comme
40
+ * `external-jwt`, exige SA ressource (l'audience par défaut pour une zone `jwt`
41
+ * qui n'en déclare pas) — un jeton émis pour une zone n'en ouvre pas une autre.
42
+ *
43
+ * @param jwt - section `jwt` de la config validée
44
+ * @param areas - zones du pare-feu ; omises, seules les audiences déclarées comptent
45
+ * @returns les paramètres partagés par l'émetteur et les vérificateurs
27
46
  */
28
- export declare function resolveJwtRuntime(jwt: ISecurityConfig["jwt"]): IJwtRuntime;
47
+ export declare function resolveJwtRuntime(jwt: ISecurityConfig["jwt"], areas?: ISecurityConfig["areas"]): IJwtRuntime;
29
48
  /**
30
49
  * Audience inscrite dans un jeton quand aucune ressource n'est demandée : la
31
50
  * première de la liste blanche. La liste n'est jamais vide après
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Bornes temporelles qu'un jeton d'accès JWT DOIT porter : `exp` et `iat`
3
+ * (RFC 9068 §2.2, l'un et l'autre REQUIRED).
4
+ *
5
+ * UNE règle pour les deux chemins de vérification — jeton de l'application
6
+ * (`JwtAuthenticator`) et jeton d'un émetteur tiers (`RemoteJwtVerifier`). Sans
7
+ * `exp`, une connexion ouverte avec le jeton n'a plus de borne propre : le
8
+ * revalidateur d'une socket ne sait alors la fermer que par le store de
9
+ * révocation, et une clé volée suffit à signer un accès éternel.
10
+ */
11
+ export declare const REQUIRED_TIME_CLAIMS: readonly string[];
12
+ /**
13
+ * Tolérance d'horloge entre l'émetteur et ce serveur, en secondes : appliquée à
14
+ * `exp`/`nbf` par jose, et à `iat` par {@link isIssuedInFuture}.
15
+ */
16
+ export declare const CLOCK_TOLERANCE_S = 5;
17
+ /**
18
+ * Le jeton se dit-il émis dans le futur, au-delà de la tolérance d'horloge ?
19
+ *
20
+ * jose ne contrôle `iat` que lorsqu'un âge maximal est imposé (`maxTokenAge`),
21
+ * ce que Nodefony ne fait pas. Or un `iat` daté du futur rend inopérante la
22
+ * révocation en masse (`invalidBefore`), qui compare `iat` à un seuil passé :
23
+ * un jeton forgé à `iat = maintenant + 10 ans` ne serait jamais « émis avant ».
24
+ *
25
+ * @param payload - claims VÉRIFIÉS du jeton (signature déjà contrôlée)
26
+ * @param nowS - instant présent, en secondes epoch
27
+ * @param toleranceS - écart d'horloge admis, en secondes
28
+ * @returns `true` si `iat` dépasse `nowS + toleranceS`
29
+ */
30
+ export declare function isIssuedInFuture(payload: {
31
+ readonly iat?: unknown;
32
+ }, nowS: number, toleranceS: number): boolean;
@@ -2,8 +2,36 @@ import type { IAuditEvent } from "../../contracts/IAuditEvent.js";
2
2
  import type { IWebhookEndpoint } from "../../contracts/IWebhookEndpoint.js";
3
3
  import type { IWebhookDeliveryPolicy } from "../../service/webhooks.js";
4
4
  import type { IDeliveryResult } from "./webhookDelivery.js";
5
- /** Une souscription matche-t-elle une action d'audit ? `*` = toutes, `x.*` = préfixe. */
5
+ /**
6
+ * Rend les motifs compilés d'une liste d'abonnement, compilés à sa première lecture puis relus du cache.
7
+ *
8
+ * Deux appels sur le MÊME tableau rendent la même liste : c'est ce qui garantit
9
+ * qu'un abonnement n'est jamais recompilé par événement. Un motif trop large
10
+ * (refusé par `compileChannelPattern`) n'y figure pas.
11
+ *
12
+ * @param patterns - les motifs de l'abonnement (`IWebhookEndpoint.events`).
13
+ * @returns les expressions compilées, une par motif valide.
14
+ */
15
+ export declare function compiledPatterns(patterns: readonly string[]): readonly RegExp[];
16
+ /**
17
+ * Dit si un abonnement webhook désigne une action d'audit, selon la grammaire commune aux canaux et aux webhooks (ADR-0023 W1).
18
+ *
19
+ * `*` vaut n'importe quelle suite de caractères : `*` désigne tout, `login.*` les
20
+ * actions qui commencent par `login.`, `commande:*` aussi bien que `*:créée`
21
+ * (`compileChannelPattern`, cœur). Chaque liste n'est compilée qu'une fois.
22
+ *
23
+ * @param patterns - les motifs de l'abonnement (`IWebhookEndpoint.events`).
24
+ * @param action - le nom de l'action d'audit.
25
+ * @returns `true` si un motif au moins désigne l'action.
26
+ */
6
27
  export declare function matchesSubscription(patterns: readonly string[], action: string): boolean;
28
+ /**
29
+ * Refuse une liste d'abonnement dont un motif porte trop de `*`, en le nommant.
30
+ *
31
+ * @param patterns - les motifs proposés à l'enregistrement ou à la mise à jour.
32
+ * @throws Error si un motif dépasse `MAX_CHANNEL_PATTERN_WILDCARDS` étoiles.
33
+ */
34
+ export declare function assertSubscriptionPatterns(patterns: readonly string[]): void;
7
35
  /**
8
36
  * Classe le résultat d'une livraison : 2xx = succès ; réseau/timeout/429/408/5xx =
9
37
  * réessayable ; 3xx/4xx (config cliente erronée) = échec définitif (pas de retry).
@@ -29,6 +57,13 @@ export interface IWebhookDispatcherDeps {
29
57
  endpointCount(): number;
30
58
  /** Snapshot des endpoints (n'est lu que si `endpointCount() > 0`). */
31
59
  getSnapshot(): IWebhookEndpoint[];
60
+ /**
61
+ * L'abonnement tel qu'il est MAINTENANT, ou `null` s'il a été supprimé — relu à
62
+ * chaque tentative. Un réessai part jusqu'à plusieurs minutes après le premier
63
+ * envoi : sans cette relecture, il partirait vers une URL retirée, signé d'un
64
+ * secret révoqué, pour un abonnement supprimé ou désactivé.
65
+ */
66
+ currentEndpoint(id: string): IWebhookEndpoint | null;
32
67
  /** Secret de signature en clair (`whsec_…`) d'un endpoint. */
33
68
  secretOf(endpoint: IWebhookEndpoint): string;
34
69
  /** Politique de livraison (tolérance/retries/timeout/concurrence/file). */