@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.
- package/README.md +24 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/index.js +63 -61
- package/dist/nodefony/command/security-mcp-token.js +43 -5
- package/dist/nodefony/config/config.js +30 -11
- package/dist/nodefony/config/defineModuleConfig.js +52 -1
- package/dist/nodefony/contracts/IWebhookPayload.js +1 -0
- package/dist/nodefony/errors/FetchPublicError.js +18 -0
- package/dist/nodefony/errors/SsrfError.js +9 -1
- package/dist/nodefony/errors/StaleSessionError.js +20 -0
- package/dist/nodefony/errors/index.js +7 -5
- package/dist/nodefony/keycloak/keycloakAdmin.js +3 -3
- package/dist/nodefony/keycloak/scaffold.js +31 -14
- package/dist/nodefony/keycloak/security-keycloak-realm.js +1 -1
- package/dist/nodefony/service/accessTokenVerifier.js +6 -6
- package/dist/nodefony/service/apiKeys.js +8 -4
- package/dist/nodefony/service/auditService.js +48 -3
- package/dist/nodefony/service/authFlow.js +9 -6
- package/dist/nodefony/service/authorization.js +1 -1
- package/dist/nodefony/service/firewall.js +281 -44
- package/dist/nodefony/service/keycloak.js +1 -1
- package/dist/nodefony/service/oauth2.js +8 -4
- package/dist/nodefony/service/securityHeaders.js +340 -47
- package/dist/nodefony/service/tokenService.js +16 -11
- package/dist/nodefony/service/totp.js +10 -5
- package/dist/nodefony/service/webAuthn.js +46 -7
- package/dist/nodefony/service/webhooks.js +11 -4
- package/dist/nodefony/src/admin/SecurityAdminApi.js +19 -0
- package/dist/nodefony/src/audit/AuditRefusalAggregator.js +139 -0
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +1 -1
- package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +31 -7
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +62 -20
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +29 -6
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +22 -9
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +2 -2
- package/dist/nodefony/src/csp.js +265 -10
- package/dist/nodefony/src/cspReports.js +307 -0
- package/dist/nodefony/src/net/fetchPublic.js +244 -0
- package/dist/nodefony/src/net/ssrfGuard.js +2 -2
- package/dist/nodefony/src/oauth/clientCredentials.js +1 -1
- package/dist/nodefony/src/oauth/metadata.js +1 -1
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +17 -4
- package/dist/nodefony/src/oauth/providers/github.js +1 -1
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +59 -2
- package/dist/nodefony/src/realtime/frameAuthorizer.js +136 -44
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +8 -2
- package/dist/nodefony/src/token/jwtRuntime.js +22 -2
- package/dist/nodefony/src/token/tokenTimeClaims.js +34 -0
- package/dist/nodefony/src/totp/totpOperations.js +1 -1
- package/dist/nodefony/src/voter/RoleVoter.js +1 -1
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +71 -17
- package/dist/nodefony/src/webhook/webhookDelivery.js +37 -86
- package/dist/nodefony/testing/index.js +26 -36
- package/dist/types/index.d.ts +5 -1
- package/dist/types/nodefony/command/security-mcp-token.d.ts +12 -0
- package/dist/types/nodefony/config/config.d.ts +8 -0
- package/dist/types/nodefony/contracts/IApiKey.d.ts +8 -2
- package/dist/types/nodefony/contracts/IFirewall.d.ts +15 -2
- package/dist/types/nodefony/contracts/ITokenStore.d.ts +7 -3
- package/dist/types/nodefony/contracts/IWebhookEndpoint.d.ts +13 -3
- package/dist/types/nodefony/contracts/IWebhookPayload.d.ts +24 -0
- package/dist/types/nodefony/errors/FetchPublicError.d.ts +15 -0
- package/dist/types/nodefony/errors/SsrfError.d.ts +8 -1
- package/dist/types/nodefony/errors/StaleSessionError.d.ts +17 -0
- package/dist/types/nodefony/errors/index.d.ts +3 -0
- package/dist/types/nodefony/keycloak/scaffold.d.ts +12 -6
- package/dist/types/nodefony/service/apiKeys.d.ts +3 -0
- package/dist/types/nodefony/service/authFlow.d.ts +2 -2
- package/dist/types/nodefony/service/firewall.d.ts +28 -7
- package/dist/types/nodefony/service/securityHeaders.d.ts +82 -11
- package/dist/types/nodefony/service/webAuthn.d.ts +13 -0
- package/dist/types/nodefony/service/webhooks.d.ts +7 -1
- package/dist/types/nodefony/src/admin/SecurityAdminApi.d.ts +3 -0
- package/dist/types/nodefony/src/audit/AuditRefusalAggregator.d.ts +55 -0
- package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +11 -0
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +17 -3
- package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +11 -4
- package/dist/types/nodefony/src/csp.d.ts +142 -0
- package/dist/types/nodefony/src/cspReports.d.ts +73 -0
- package/dist/types/nodefony/src/net/fetchPublic.d.ts +73 -0
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +15 -0
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +35 -1
- package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +10 -2
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +42 -18
- package/dist/types/nodefony/src/token/jwtRuntime.d.ts +21 -2
- package/dist/types/nodefony/src/token/tokenTimeClaims.d.ts +32 -0
- package/dist/types/nodefony/src/webhook/WebhookDispatcher.d.ts +36 -1
- package/dist/types/nodefony/src/webhook/webhookDelivery.d.ts +5 -2
- package/dist/types/nodefony/testing/index.d.ts +6 -0
- package/docs/api-keys.md +3 -1
- package/docs/audit.md +112 -28
- package/docs/authenticators.md +6 -4
- package/docs/csrf.md +3 -2
- package/docs/external-jwt.md +5 -1
- package/docs/firewall.md +3 -2
- package/docs/headers.md +427 -71
- package/docs/keycloak.md +4 -4
- package/docs/login-page.md +4 -3
- package/docs/obtenir-un-jeton.md +17 -10
- package/docs/tokens.md +1 -1
- package/docs/totp.md +2 -2
- package/docs/webauthn.md +2 -2
- package/docs/webhooks.md +63 -18
- package/package.json +17 -17
|
@@ -3,12 +3,32 @@
|
|
|
3
3
|
* Dérive les paramètres effectifs depuis la config sécurité. `issuer` omis →
|
|
4
4
|
* `"nodefony"` (DEVRAIT être surchargé en prod) ; `audiences` vide → l'app est sa
|
|
5
5
|
* propre audience (`[issuer]`).
|
|
6
|
+
*
|
|
7
|
+
* La ressource d'une ZONE (`areas.<zone>.resource`) entre aussi dans la liste,
|
|
8
|
+
* APRÈS les audiences déclarées : c'est une ressource que ce serveur protège, et
|
|
9
|
+
* la redéclarer dans `jwt.audiences` était une seconde écriture de la même
|
|
10
|
+
* valeur — celle qu'on oublie, et dont le seul symptôme est un `invalid_target`
|
|
11
|
+
* à l'émission. Ajoutée en fin de liste, elle ne change jamais l'audience par
|
|
12
|
+
* défaut ({@link defaultAudience}).
|
|
13
|
+
*
|
|
14
|
+
* Elle n'ouvre rien de plus à la VÉRIFICATION : chaque zone, `jwt` comme
|
|
15
|
+
* `external-jwt`, exige SA ressource (l'audience par défaut pour une zone `jwt`
|
|
16
|
+
* qui n'en déclare pas) — un jeton émis pour une zone n'en ouvre pas une autre.
|
|
17
|
+
*
|
|
18
|
+
* @param jwt - section `jwt` de la config validée
|
|
19
|
+
* @param areas - zones du pare-feu ; omises, seules les audiences déclarées comptent
|
|
20
|
+
* @returns les paramètres partagés par l'émetteur et les vérificateurs
|
|
6
21
|
*/
|
|
7
|
-
function resolveJwtRuntime(jwt) {
|
|
22
|
+
function resolveJwtRuntime(jwt, areas) {
|
|
8
23
|
const issuer = jwt.issuer && jwt.issuer.length > 0 ? jwt.issuer : "nodefony";
|
|
24
|
+
const audiences = jwt.audiences.length > 0 ? [...jwt.audiences] : [issuer];
|
|
25
|
+
if (areas !== void 0) for (const name in areas) {
|
|
26
|
+
const resource = areas[name]?.resource;
|
|
27
|
+
if (resource !== void 0 && !audiences.includes(resource)) audiences.push(resource);
|
|
28
|
+
}
|
|
9
29
|
return {
|
|
10
30
|
issuer,
|
|
11
|
-
audiences
|
|
31
|
+
audiences,
|
|
12
32
|
accessTtlS: jwt.accessTtlS,
|
|
13
33
|
refreshTtlS: jwt.refreshTtlS,
|
|
14
34
|
rotateRefresh: jwt.rotateRefresh,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// nodefony/src/token/tokenTimeClaims.ts
|
|
2
|
+
/**
|
|
3
|
+
* Bornes temporelles qu'un jeton d'accès JWT DOIT porter : `exp` et `iat`
|
|
4
|
+
* (RFC 9068 §2.2, l'un et l'autre REQUIRED).
|
|
5
|
+
*
|
|
6
|
+
* UNE règle pour les deux chemins de vérification — jeton de l'application
|
|
7
|
+
* (`JwtAuthenticator`) et jeton d'un émetteur tiers (`RemoteJwtVerifier`). Sans
|
|
8
|
+
* `exp`, une connexion ouverte avec le jeton n'a plus de borne propre : le
|
|
9
|
+
* revalidateur d'une socket ne sait alors la fermer que par le store de
|
|
10
|
+
* révocation, et une clé volée suffit à signer un accès éternel.
|
|
11
|
+
*/
|
|
12
|
+
const REQUIRED_TIME_CLAIMS = Object.freeze(["exp", "iat"]);
|
|
13
|
+
/**
|
|
14
|
+
* Tolérance d'horloge entre l'émetteur et ce serveur, en secondes : appliquée à
|
|
15
|
+
* `exp`/`nbf` par jose, et à `iat` par {@link isIssuedInFuture}.
|
|
16
|
+
*/
|
|
17
|
+
const CLOCK_TOLERANCE_S = 5;
|
|
18
|
+
/**
|
|
19
|
+
* Le jeton se dit-il émis dans le futur, au-delà de la tolérance d'horloge ?
|
|
20
|
+
*
|
|
21
|
+
* jose ne contrôle `iat` que lorsqu'un âge maximal est imposé (`maxTokenAge`),
|
|
22
|
+
* ce que Nodefony ne fait pas. Or un `iat` daté du futur rend inopérante la
|
|
23
|
+
* révocation en masse (`invalidBefore`), qui compare `iat` à un seuil passé :
|
|
24
|
+
* un jeton forgé à `iat = maintenant + 10 ans` ne serait jamais « émis avant ».
|
|
25
|
+
*
|
|
26
|
+
* @param payload - claims VÉRIFIÉS du jeton (signature déjà contrôlée)
|
|
27
|
+
* @param nowS - instant présent, en secondes epoch
|
|
28
|
+
* @param toleranceS - écart d'horloge admis, en secondes
|
|
29
|
+
* @returns `true` si `iat` dépasse `nowS + toleranceS`
|
|
30
|
+
*/
|
|
31
|
+
function isIssuedInFuture(payload, nowS, toleranceS) {
|
|
32
|
+
return typeof payload.iat === "number" && payload.iat > nowS + toleranceS;
|
|
33
|
+
}
|
|
34
|
+
export { CLOCK_TOLERANCE_S, REQUIRED_TIME_CLAIMS, isIssuedInFuture };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { decryptSecret, encryptSecret } from "../crypto/secretCipher.js";
|
|
2
|
-
import "./totpCipher.js";
|
|
3
2
|
import { base32Encode, buildOtpauthUri, generateRecoveryCodes, generateTotpSecret, hashRecoveryCode, matchRecoveryCode, verifyTotp } from "./totpCrypto.js";
|
|
3
|
+
import "./totpCipher.js";
|
|
4
4
|
// nodefony/src/totp/totpOperations.ts
|
|
5
5
|
/**
|
|
6
6
|
* Démarre l'enrôlement : génère un secret aléatoire, le **chiffre** au repos et
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { RoleHierarchyWalker } from "../RoleHierarchyWalker.js";
|
|
2
1
|
import "../../contracts/IAccessVoter.js";
|
|
2
|
+
import { RoleHierarchyWalker } from "../RoleHierarchyWalker.js";
|
|
3
3
|
// nodefony/src/voter/RoleVoter.ts
|
|
4
4
|
/**
|
|
5
5
|
* Voter built-in **niveau A** — résout les attributs `ROLE_*` via la hiérarchie
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { webhookSignatureHeaders } from "./webhookSignature.js";
|
|
2
|
+
import { MAX_CHANNEL_PATTERN_WILDCARDS, compileChannelPattern, countWildcards } from "nodefony";
|
|
2
3
|
// nodefony/src/webhook/WebhookDispatcher.ts
|
|
3
4
|
/**
|
|
4
5
|
* Dispatcher de webhooks sortants — abonné au journal d'audit, il filtre les
|
|
@@ -20,15 +21,60 @@ import { webhookSignatureHeaders } from "./webhookSignature.js";
|
|
|
20
21
|
*/
|
|
21
22
|
const MAX_BACKOFF_MS = 3e5;
|
|
22
23
|
const BASE_BACKOFF_MS = 5e3;
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Motifs compilés par liste d'abonnement — une fois par tableau `events`, jamais par
|
|
26
|
+
* événement. La clé est le tableau lui-même : un abonnement modifié porte un tableau
|
|
27
|
+
* neuf, donc se recompile, et l'ancien part avec lui au ramasse-miettes.
|
|
28
|
+
*/
|
|
29
|
+
const compiledSubscriptions = /* @__PURE__ */ new WeakMap();
|
|
30
|
+
/**
|
|
31
|
+
* Rend les motifs compilés d'une liste d'abonnement, compilés à sa première lecture puis relus du cache.
|
|
32
|
+
*
|
|
33
|
+
* Deux appels sur le MÊME tableau rendent la même liste : c'est ce qui garantit
|
|
34
|
+
* qu'un abonnement n'est jamais recompilé par événement. Un motif trop large
|
|
35
|
+
* (refusé par `compileChannelPattern`) n'y figure pas.
|
|
36
|
+
*
|
|
37
|
+
* @param patterns - les motifs de l'abonnement (`IWebhookEndpoint.events`).
|
|
38
|
+
* @returns les expressions compilées, une par motif valide.
|
|
39
|
+
*/
|
|
40
|
+
function compiledPatterns(patterns) {
|
|
41
|
+
let compiled = compiledSubscriptions.get(patterns);
|
|
42
|
+
if (compiled === void 0) {
|
|
43
|
+
const list = [];
|
|
44
|
+
for (const p of patterns) {
|
|
45
|
+
const re = compileChannelPattern(p);
|
|
46
|
+
if (re !== null) list.push(re);
|
|
47
|
+
}
|
|
48
|
+
compiled = list;
|
|
49
|
+
compiledSubscriptions.set(patterns, compiled);
|
|
28
50
|
}
|
|
51
|
+
return compiled;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Dit si un abonnement webhook désigne une action d'audit, selon la grammaire commune aux canaux et aux webhooks (ADR-0023 W1).
|
|
55
|
+
*
|
|
56
|
+
* `*` vaut n'importe quelle suite de caractères : `*` désigne tout, `login.*` les
|
|
57
|
+
* actions qui commencent par `login.`, `commande:*` aussi bien que `*:créée`
|
|
58
|
+
* (`compileChannelPattern`, cœur). Chaque liste n'est compilée qu'une fois.
|
|
59
|
+
*
|
|
60
|
+
* @param patterns - les motifs de l'abonnement (`IWebhookEndpoint.events`).
|
|
61
|
+
* @param action - le nom de l'action d'audit.
|
|
62
|
+
* @returns `true` si un motif au moins désigne l'action.
|
|
63
|
+
*/
|
|
64
|
+
function matchesSubscription(patterns, action) {
|
|
65
|
+
for (const re of compiledPatterns(patterns)) if (re.test(action)) return true;
|
|
29
66
|
return false;
|
|
30
67
|
}
|
|
31
68
|
/**
|
|
69
|
+
* Refuse une liste d'abonnement dont un motif porte trop de `*`, en le nommant.
|
|
70
|
+
*
|
|
71
|
+
* @param patterns - les motifs proposés à l'enregistrement ou à la mise à jour.
|
|
72
|
+
* @throws Error si un motif dépasse `MAX_CHANNEL_PATTERN_WILDCARDS` étoiles.
|
|
73
|
+
*/
|
|
74
|
+
function assertSubscriptionPatterns(patterns) {
|
|
75
|
+
for (const p of patterns) if (countWildcards(p) > MAX_CHANNEL_PATTERN_WILDCARDS) throw new Error(`webhooks: le motif « ${p} » porte ${countWildcards(p)} caractères « * » (maximum ${MAX_CHANNEL_PATTERN_WILDCARDS}) — un motif en demande un, deux au pire.`);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
32
78
|
* Classe le résultat d'une livraison : 2xx = succès ; réseau/timeout/429/408/5xx =
|
|
33
79
|
* réessayable ; 3xx/4xx (config cliente erronée) = échec définitif (pas de retry).
|
|
34
80
|
*/
|
|
@@ -71,7 +117,8 @@ var WebhookDispatcher = class {
|
|
|
71
117
|
this.#enqueue({
|
|
72
118
|
ep,
|
|
73
119
|
event,
|
|
74
|
-
attempt: 0
|
|
120
|
+
attempt: 0,
|
|
121
|
+
messageId: null
|
|
75
122
|
});
|
|
76
123
|
}
|
|
77
124
|
}
|
|
@@ -112,17 +159,20 @@ var WebhookDispatcher = class {
|
|
|
112
159
|
}
|
|
113
160
|
/** Signe + livre une tentative ; gère succès / retry / échec définitif. */
|
|
114
161
|
async #process(job) {
|
|
115
|
-
const {
|
|
162
|
+
const { event, attempt } = job;
|
|
163
|
+
const ep = this.#deps.currentEndpoint(job.ep.id);
|
|
164
|
+
if (ep === null || !ep.enabled || !matchesSubscription(ep.events, event.action)) return;
|
|
116
165
|
try {
|
|
117
|
-
const id = this.#deps.newMessageId();
|
|
166
|
+
const id = job.messageId ?? this.#deps.newMessageId();
|
|
118
167
|
const nowMs = this.#deps.now();
|
|
119
168
|
const tsS = Math.floor(nowMs / 1e3);
|
|
120
|
-
const
|
|
169
|
+
const payload = {
|
|
121
170
|
id,
|
|
122
171
|
timestamp: new Date(nowMs).toISOString(),
|
|
123
172
|
type: event.action,
|
|
124
173
|
data: event
|
|
125
|
-
}
|
|
174
|
+
};
|
|
175
|
+
const body = JSON.stringify(payload);
|
|
126
176
|
const headers = {
|
|
127
177
|
"content-type": "application/json",
|
|
128
178
|
"user-agent": "Nodefony-Webhooks/1.0",
|
|
@@ -132,7 +182,7 @@ var WebhookDispatcher = class {
|
|
|
132
182
|
try {
|
|
133
183
|
addresses = await this.#deps.resolveTarget(ep.url);
|
|
134
184
|
} catch (e) {
|
|
135
|
-
const error = `ssrf: ${e.message}`;
|
|
185
|
+
const error = `ssrf: ${e instanceof Error ? e.message : String(e)}`;
|
|
136
186
|
await this.#deps.markDelivery(ep.id, {
|
|
137
187
|
ok: false,
|
|
138
188
|
status: null,
|
|
@@ -157,7 +207,7 @@ var WebhookDispatcher = class {
|
|
|
157
207
|
allowHttp: this.#deps.policy.allowHttp
|
|
158
208
|
});
|
|
159
209
|
if (classifyDelivery(result) === "retry" && attempt < this.#deps.policy.maxRetries) {
|
|
160
|
-
this.#scheduleRetry(job);
|
|
210
|
+
this.#scheduleRetry(job, id);
|
|
161
211
|
return;
|
|
162
212
|
}
|
|
163
213
|
await this.#deps.markDelivery(ep.id, result);
|
|
@@ -173,11 +223,14 @@ var WebhookDispatcher = class {
|
|
|
173
223
|
responseBody: result.responseBody ?? null
|
|
174
224
|
});
|
|
175
225
|
} catch (e) {
|
|
176
|
-
this.#deps.log(`webhook dispatch ${ep.id}: ${e.message}`);
|
|
226
|
+
this.#deps.log(`webhook dispatch ${ep.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
177
227
|
}
|
|
178
228
|
}
|
|
179
|
-
/**
|
|
180
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Replanifie la livraison après backoff (repasse par la file bornée), sous le
|
|
231
|
+
* MÊME `webhook-id` que la tentative échouée.
|
|
232
|
+
*/
|
|
233
|
+
#scheduleRetry(job, messageId) {
|
|
181
234
|
if (this.#stopped) return;
|
|
182
235
|
this.#timers ??= /* @__PURE__ */ new Set();
|
|
183
236
|
const cancel = this.#deps.schedule(() => {
|
|
@@ -185,7 +238,8 @@ var WebhookDispatcher = class {
|
|
|
185
238
|
this.#enqueue({
|
|
186
239
|
ep: job.ep,
|
|
187
240
|
event: job.event,
|
|
188
|
-
attempt: job.attempt + 1
|
|
241
|
+
attempt: job.attempt + 1,
|
|
242
|
+
messageId
|
|
189
243
|
});
|
|
190
244
|
}, backoffMs(job.attempt));
|
|
191
245
|
this.#timers.add(cancel);
|
|
@@ -205,4 +259,4 @@ var WebhookDispatcher = class {
|
|
|
205
259
|
this.#queue = null;
|
|
206
260
|
}
|
|
207
261
|
};
|
|
208
|
-
export { WebhookDispatcher, backoffMs, classifyDelivery, matchesSubscription };
|
|
262
|
+
export { WebhookDispatcher, assertSubscriptionPatterns, backoffMs, classifyDelivery, compiledPatterns, matchesSubscription };
|
|
@@ -1,101 +1,52 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { request } from "node:https";
|
|
4
|
-
import { request as request$1 } from "node:http";
|
|
1
|
+
import { fetchPublic } from "../net/fetchPublic.js";
|
|
2
|
+
import { FetchPublicError } from "../../errors/FetchPublicError.js";
|
|
5
3
|
// nodefony/src/webhook/webhookDelivery.ts
|
|
6
4
|
/**
|
|
7
|
-
* Émetteur HTTP d'une livraison webhook —
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 1. **Pas de suivi de redirection** : `node:http(s)` ne suit JAMAIS les 3xx
|
|
11
|
-
* (contrairement à `fetch`) → un `302 → 169.254.169.254` ne contourne pas le
|
|
12
|
-
* contrôle SSRF ; le 3xx est rendu tel quel (échec de config).
|
|
13
|
-
* 2. **Pin de l'IP validée** (anti DNS-rebinding) : `lookup` force la connexion TCP
|
|
14
|
-
* vers l'IP déjà contrôlée par `assertPublicUrl`, tout en gardant le hostname
|
|
15
|
-
* pour le SNI/TLS et l'en-tête `Host` → pas de re-résolution entre contrôle et
|
|
16
|
-
* connexion.
|
|
17
|
-
* 3. **Timeout dur** : la requête est détruite au-delà du délai.
|
|
5
|
+
* Émetteur HTTP d'une livraison webhook — délègue à {@link fetchPublic}, la
|
|
6
|
+
* primitive unique des requêtes sortantes : pas de suivi de redirection, IP
|
|
7
|
+
* vérifiée épinglée (anti DNS-rebinding), délai total, lecture bornée.
|
|
18
8
|
*/
|
|
19
9
|
/** Taille max du corps de réponse capturé (observabilité, anti-mémoire). */
|
|
20
10
|
const RESPONSE_BODY_CAP = 2048;
|
|
21
11
|
/**
|
|
22
|
-
* POST le corps signé vers l'URL,
|
|
23
|
-
*
|
|
12
|
+
* POST le corps signé vers l'URL, par {@link fetchPublic} : IP vérifiée épinglée,
|
|
13
|
+
* aucune redirection suivie, délai TOTAL (un destinataire qui goutte un octet
|
|
14
|
+
* ne retient pas la livraison) et lecture du corps coupée à
|
|
15
|
+
* {@link RESPONSE_BODY_CAP} (connexion détruite, pas drainée). Ne lève jamais :
|
|
16
|
+
* tout échec est encodé dans {@link IDeliveryResult}.
|
|
24
17
|
*/
|
|
25
|
-
function deliverWebhook(url, body, headers, opts) {
|
|
26
|
-
let u;
|
|
18
|
+
async function deliverWebhook(url, body, headers, opts) {
|
|
27
19
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
const vetted = opts.addresses !== void 0 && opts.addresses.length > 0;
|
|
21
|
+
const r = await fetchPublic(url, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers,
|
|
24
|
+
body,
|
|
25
|
+
maxBytes: RESPONSE_BODY_CAP,
|
|
26
|
+
timeoutMs: opts.timeoutMs,
|
|
27
|
+
allowHttp: opts.allowHttp === true,
|
|
28
|
+
allowPrivate: !vetted,
|
|
29
|
+
...vetted ? { vettedAddresses: opts.addresses } : {}
|
|
30
|
+
});
|
|
31
|
+
const ok = r.status >= 200 && r.status < 300;
|
|
32
|
+
const text = r.body.length > 0 ? new TextDecoder().decode(r.body) : "";
|
|
33
|
+
return {
|
|
34
|
+
ok,
|
|
35
|
+
status: r.status,
|
|
36
|
+
error: ok ? null : `HTTP ${r.status}`,
|
|
37
|
+
responseBody: text.length > 0 ? text : null
|
|
38
|
+
};
|
|
39
|
+
} catch (e) {
|
|
40
|
+
if (e instanceof FetchPublicError && e.reason === "timeout") return {
|
|
31
41
|
ok: false,
|
|
32
42
|
status: null,
|
|
33
|
-
error: "
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
const isHttps = u.protocol === "https:";
|
|
37
|
-
if (!isHttps && !(u.protocol === "http:" && opts.allowHttp)) return Promise.resolve({
|
|
38
|
-
ok: false,
|
|
39
|
-
status: null,
|
|
40
|
-
error: `protocole ${u.protocol} non autorisé`
|
|
41
|
-
});
|
|
42
|
-
const pinned = opts.addresses && opts.addresses.length > 0 ? opts.addresses[0] : null;
|
|
43
|
-
const lookup = pinned ? (_hostname, options, callback) => {
|
|
44
|
-
const family = isIP(pinned) === 6 ? 6 : 4;
|
|
45
|
-
if (options.all) callback(null, [{
|
|
46
|
-
address: pinned,
|
|
47
|
-
family
|
|
48
|
-
}], family);
|
|
49
|
-
else callback(null, pinned, family);
|
|
50
|
-
} : void 0;
|
|
51
|
-
const payload = Buffer.from(body, "utf8");
|
|
52
|
-
const requestFn = isHttps ? request : request$1;
|
|
53
|
-
return new Promise((resolve) => {
|
|
54
|
-
let settled = false;
|
|
55
|
-
const done = (r) => {
|
|
56
|
-
if (!settled) {
|
|
57
|
-
settled = true;
|
|
58
|
-
resolve(r);
|
|
59
|
-
}
|
|
43
|
+
error: "timeout"
|
|
60
44
|
};
|
|
61
|
-
|
|
62
|
-
method: "POST",
|
|
63
|
-
headers: {
|
|
64
|
-
...headers,
|
|
65
|
-
"content-length": String(payload.length)
|
|
66
|
-
},
|
|
67
|
-
lookup
|
|
68
|
-
}, (res) => {
|
|
69
|
-
const status = res.statusCode ?? 0;
|
|
70
|
-
const ok = status >= 200 && status < 300;
|
|
71
|
-
let resBody = "";
|
|
72
|
-
res.on("data", (c) => {
|
|
73
|
-
if (resBody.length < RESPONSE_BODY_CAP) {
|
|
74
|
-
resBody += c.toString("utf8");
|
|
75
|
-
if (resBody.length > RESPONSE_BODY_CAP) resBody = resBody.slice(0, RESPONSE_BODY_CAP);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
res.on("end", () => done({
|
|
79
|
-
ok,
|
|
80
|
-
status,
|
|
81
|
-
error: ok ? null : `HTTP ${status}`,
|
|
82
|
-
responseBody: resBody.length > 0 ? resBody : null
|
|
83
|
-
}));
|
|
84
|
-
res.on("error", (e) => done({
|
|
85
|
-
ok: false,
|
|
86
|
-
status,
|
|
87
|
-
error: e.message,
|
|
88
|
-
responseBody: resBody.length > 0 ? resBody : null
|
|
89
|
-
}));
|
|
90
|
-
});
|
|
91
|
-
req.setTimeout(opts.timeoutMs, () => req.destroy(/* @__PURE__ */ new Error("timeout")));
|
|
92
|
-
req.on("error", (e) => done({
|
|
45
|
+
return {
|
|
93
46
|
ok: false,
|
|
94
47
|
status: null,
|
|
95
|
-
error: e.message
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
req.end();
|
|
99
|
-
});
|
|
48
|
+
error: e instanceof Error ? e.message : String(e)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
100
51
|
}
|
|
101
52
|
export { deliverWebhook };
|
|
@@ -2,40 +2,6 @@ import { RoleHierarchyWalker } from "../src/RoleHierarchyWalker.js";
|
|
|
2
2
|
import { DEFAULT_SYSTEM_RULES, buildFrameAuthorizer } from "../src/realtime/frameAuthorizer.js";
|
|
3
3
|
// nodefony/testing/index.ts
|
|
4
4
|
/**
|
|
5
|
-
* `@nodefony/security/testing` — **identité d'essai et VRAI verrou de frame**,
|
|
6
|
-
* pour éprouver un canal ou une action temps réel protégés.
|
|
7
|
-
*
|
|
8
|
-
* Le harnais de `@nodefony/realtime/testing` accepte une identité et un verrou,
|
|
9
|
-
* mais n'en fournit aucun : `@nodefony/realtime` ne connaît pas la sécurité.
|
|
10
|
-
* Faute de mieux, une application écrivait un jeton factice et un verrou qui ne
|
|
11
|
-
* regardait que `isAuthenticated()` — son test passait, et n'éprouvait PAS la
|
|
12
|
-
* politique de rôles du canal. Un canal mal protégé y était vert. Deux agents
|
|
13
|
-
* sur deux l'ont fait, en recopiant le jeton que montrait le test généré.
|
|
14
|
-
*
|
|
15
|
-
* Ce module rend les deux pièces, et le verrou rendu est celui du pare-feu —
|
|
16
|
-
* {@link buildFrameAuthorizer}, la même fabrique que le boot branche sur le hub,
|
|
17
|
-
* avec la même hiérarchie de rôles et les mêmes canaux système. Seuls manquent
|
|
18
|
-
* le journal d'audit (un refus en test n'est pas une transition de sécurité) et
|
|
19
|
-
* les ZONES HTTP : une frame `api.request` y passe comme hors de toute zone, et
|
|
20
|
-
* la garde d'une route s'éprouve par HTTP, pas ici.
|
|
21
|
-
*
|
|
22
|
-
* ```ts
|
|
23
|
-
* import { getRealtimeHub } from "@nodefony/realtime";
|
|
24
|
-
* import { createRealtimeHarness } from "@nodefony/realtime/testing";
|
|
25
|
-
* import {
|
|
26
|
-
* createTestFrameAuthorizer,
|
|
27
|
-
* createTestIdentity,
|
|
28
|
-
* } from "@nodefony/security/testing";
|
|
29
|
-
*
|
|
30
|
-
* const h = createRealtimeHarness((ctx) => new ChatController(ctx), {
|
|
31
|
-
* identity: createTestIdentity({ identifier: "alice", roles: ["ROLE_USER"] }),
|
|
32
|
-
* frameAuthorizer: createTestFrameAuthorizer(getRealtimeHub()),
|
|
33
|
-
* });
|
|
34
|
-
* ```
|
|
35
|
-
*
|
|
36
|
-
* @packageDocumentation
|
|
37
|
-
*/
|
|
38
|
-
/**
|
|
39
5
|
* Une identité de connexion AUTHENTIFIÉE, telle que le pare-feu l'aurait posée
|
|
40
6
|
* au handshake.
|
|
41
7
|
*
|
|
@@ -48,7 +14,7 @@ import { DEFAULT_SYSTEM_RULES, buildFrameAuthorizer } from "../src/realtime/fram
|
|
|
48
14
|
function createTestIdentity(options = {}) {
|
|
49
15
|
const identifier = options.identifier ?? "alice";
|
|
50
16
|
const roles = options.roles ?? ["ROLE_USER"];
|
|
51
|
-
return new TestToken(identifier, roles, options.scopes ?? [], options.user ?? {
|
|
17
|
+
return new TestToken(identifier, options.id ?? identifier, roles, options.scopes ?? [], options.user ?? {
|
|
52
18
|
identifier,
|
|
53
19
|
roles: [...roles]
|
|
54
20
|
});
|
|
@@ -56,16 +22,40 @@ function createTestIdentity(options = {}) {
|
|
|
56
22
|
/** Le jeton que rend {@link createTestIdentity}. */
|
|
57
23
|
var TestToken = class {
|
|
58
24
|
identifier;
|
|
25
|
+
id;
|
|
59
26
|
roles;
|
|
60
27
|
scopes;
|
|
61
28
|
user;
|
|
62
29
|
type = "session";
|
|
63
|
-
|
|
30
|
+
#identity = null;
|
|
31
|
+
constructor(identifier, id, roles, scopes, user) {
|
|
64
32
|
this.identifier = identifier;
|
|
33
|
+
this.id = id;
|
|
65
34
|
this.roles = roles;
|
|
66
35
|
this.scopes = scopes;
|
|
67
36
|
this.user = user;
|
|
68
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Identité d'admission gelée — ce que reçoit la règle `allow` d'un canal.
|
|
40
|
+
*
|
|
41
|
+
* Même forme que celle du jeton du pare-feu (`UserRealtimeToken.getIdentity`) :
|
|
42
|
+
* sans elle, tout canal à `allow` refusait la connexion d'essai, et une
|
|
43
|
+
* application ne pouvait éprouver un salon privé qu'en écrivant son propre jeton.
|
|
44
|
+
*/
|
|
45
|
+
getIdentity() {
|
|
46
|
+
if (this.#identity !== null) return this.#identity;
|
|
47
|
+
this.#identity = Object.freeze({
|
|
48
|
+
type: this.type,
|
|
49
|
+
authenticated: true,
|
|
50
|
+
id: this.id,
|
|
51
|
+
userIdentifier: this.identifier,
|
|
52
|
+
roles: Object.freeze([...this.roles]),
|
|
53
|
+
scopes: Object.freeze([...this.scopes]),
|
|
54
|
+
issuer: null,
|
|
55
|
+
account: true
|
|
56
|
+
});
|
|
57
|
+
return this.#identity;
|
|
58
|
+
}
|
|
69
59
|
getUserIdentifier() {
|
|
70
60
|
return this.identifier;
|
|
71
61
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -168,9 +168,12 @@ export { WEBHOOK_SORTABLE_FIELDS, WEBHOOK_DEFAULT_ORDER, } from "./nodefony/src/
|
|
|
168
168
|
export { registerWebhookStore, getWebhookStoreFactory, listWebhookStores, } from "./nodefony/src/webhook/webhookStoreRegistry.js";
|
|
169
169
|
export type { WebhookStoreFactory, IWebhookStoreFactoryContext, } from "./nodefony/src/webhook/webhookStoreRegistry.js";
|
|
170
170
|
export type { IWebhookEndpoint, WebhookEndpointUpdate, WebhookEndpointSummary, } from "./nodefony/contracts/IWebhookEndpoint.js";
|
|
171
|
+
export type { IWebhookPayload } from "./nodefony/contracts/IWebhookPayload.js";
|
|
171
172
|
export type { IWebhookListQuery, IWebhookStore, } from "./nodefony/contracts/IWebhookStore.js";
|
|
172
173
|
export { assertPublicUrl, isBlockedAddress, } from "./nodefony/src/net/ssrfGuard.js";
|
|
173
174
|
export type { IAssertPublicUrlOptions, IPublicUrlResult, DnsResolver, } from "./nodefony/src/net/ssrfGuard.js";
|
|
175
|
+
export { fetchPublic } from "./nodefony/src/net/fetchPublic.js";
|
|
176
|
+
export type { IFetchPublicOptions, IFetchPublicResult, } from "./nodefony/src/net/fetchPublic.js";
|
|
174
177
|
export { deriveKey, encryptSecret, decryptSecret, generateEphemeralKey, } from "./nodefony/src/crypto/secretCipher.js";
|
|
175
178
|
export type { IKeyDerivation } from "./nodefony/src/crypto/secretCipher.js";
|
|
176
179
|
export { defineSecurityConfig, securityConfigJsonSchema, } from "./nodefony/config/defineModuleConfig.js";
|
|
@@ -179,6 +182,7 @@ export { LoginThrottler } from "./nodefony/src/throttle/LoginThrottler.js";
|
|
|
179
182
|
export type { ILoginThrottleOptions } from "./nodefony/src/throttle/LoginThrottler.js";
|
|
180
183
|
export { CredentialTransportPolicy, shouldEnforceCredentialTransport, } from "./nodefony/src/transport/CredentialTransportPolicy.js";
|
|
181
184
|
export type { ICredentialTransportContext, ICredentialTransportPolicyOptions, } from "./nodefony/src/transport/CredentialTransportPolicy.js";
|
|
182
|
-
export { AuthenticationError, AccessDeniedError, ThrottledError, UnverifiableTokenError, InvalidTargetError, CsrfError, SsrfError, InsecureTransportError, } from "./nodefony/errors/index.js";
|
|
185
|
+
export { AuthenticationError, AccessDeniedError, ThrottledError, UnverifiableTokenError, InvalidTargetError, CsrfError, SsrfError, FetchPublicError, InsecureTransportError, } from "./nodefony/errors/index.js";
|
|
186
|
+
export type { FetchPublicFailure } from "./nodefony/errors/index.js";
|
|
183
187
|
export { VoterVote } from "./nodefony/contracts/index.js";
|
|
184
188
|
export type { IToken, IAuthenticator, ISecuredArea, IFirewall, IAccessVoter, IAuthorizationService, IAccessTokenRecord, ITokenStore, ITokenListQuery, ITokenUsage, IResourcePermission, TokenRevokeReason, TokenStatus, IJwtKeystore, IJwtSigningKey, } from "./nodefony/contracts/index.js";
|
|
@@ -19,6 +19,18 @@ export declare function ttlSeconds(raw: string | undefined): number | undefined
|
|
|
19
19
|
* @returns les secondes, `undefined` pour le défaut de configuration, une `Error` sinon
|
|
20
20
|
*/
|
|
21
21
|
export declare function tokenTtlSeconds(raw: string | undefined, write: boolean): number | undefined | Error;
|
|
22
|
+
/**
|
|
23
|
+
* Le jeton vise-t-il une AUTRE porte que celle de l'outillage ?
|
|
24
|
+
*
|
|
25
|
+
* `NF_MCP_TOKEN` et `--write` servent la porte par défaut, et elle seule : un
|
|
26
|
+
* jeton d'une autre audience (RFC 8707) y prendrait sa place, et cette porte le
|
|
27
|
+
* refuserait — ses outils réservés disparaîtraient sans un mot.
|
|
28
|
+
*
|
|
29
|
+
* @param resource - la valeur de `--resource`, ou rien
|
|
30
|
+
* @param defaultResource - la porte par défaut de l'application
|
|
31
|
+
* @returns `true` quand `--resource` désigne une autre porte
|
|
32
|
+
*/
|
|
33
|
+
export declare function targetsOtherGate(resource: string | undefined, defaultResource: string): boolean;
|
|
22
34
|
/**
|
|
23
35
|
* `nodefony security:mcp:token` — émet un jeton d'accès pour une porte de cette
|
|
24
36
|
* application (la porte MCP par défaut).
|
|
@@ -85,7 +85,9 @@ export declare const securityConfigSchema: z.ZodObject<{
|
|
|
85
85
|
hsts: z.ZodDefault<z.ZodBoolean>;
|
|
86
86
|
hstsMaxAgeS: z.ZodDefault<z.ZodNumber>;
|
|
87
87
|
csp: z.ZodDefault<z.ZodString>;
|
|
88
|
+
cspSources: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>>;
|
|
88
89
|
cspNonces: z.ZodDefault<z.ZodBoolean>;
|
|
90
|
+
cspReport: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>;
|
|
89
91
|
frameguard: z.ZodDefault<z.ZodEnum<{
|
|
90
92
|
deny: "deny";
|
|
91
93
|
sameorigin: "sameorigin";
|
|
@@ -338,6 +340,12 @@ export declare const securityConfigSchema: z.ZodObject<{
|
|
|
338
340
|
immutable: z.ZodDefault<z.ZodBoolean>;
|
|
339
341
|
stream: z.ZodDefault<z.ZodBoolean>;
|
|
340
342
|
retentionDays: z.ZodDefault<z.ZodNumber>;
|
|
343
|
+
aggregate: z.ZodDefault<z.ZodObject<{
|
|
344
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
345
|
+
windowMs: z.ZodDefault<z.ZodNumber>;
|
|
346
|
+
burst: z.ZodDefault<z.ZodNumber>;
|
|
347
|
+
maxKeys: z.ZodDefault<z.ZodNumber>;
|
|
348
|
+
}, z.core.$strict>>;
|
|
341
349
|
}, z.core.$strict>>;
|
|
342
350
|
studio: z.ZodDefault<z.ZodObject<{
|
|
343
351
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -16,7 +16,10 @@ export interface IApiKeyView {
|
|
|
16
16
|
subjectId: string;
|
|
17
17
|
/** Discriminant du porteur. */
|
|
18
18
|
subjectType: "user" | "service";
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Périmètre d'un locataire — réservé, toujours `null` tant que le multi-tenant
|
|
21
|
+
* n'existe pas (`createForSubject` refuse une valeur).
|
|
22
|
+
*/
|
|
20
23
|
tenantId: string | null;
|
|
21
24
|
/** Création (epoch ms). */
|
|
22
25
|
createdAt: number;
|
|
@@ -70,6 +73,9 @@ export interface ICreateApiKeyOptions {
|
|
|
70
73
|
scopes?: string[];
|
|
71
74
|
/** Durée de vie en jours ; `null` = sans expiration ; omis = défaut config. */
|
|
72
75
|
expiresInDays?: number | null;
|
|
73
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Périmètre d'un locataire — réservé, **refusé s'il est rempli** (`ApiKeyError` 400)
|
|
78
|
+
* tant que le multi-tenant n'existe pas. Absent ou `null` : clé sans périmètre.
|
|
79
|
+
*/
|
|
74
80
|
tenantId?: string | null;
|
|
75
81
|
}
|
|
@@ -28,13 +28,26 @@ export interface IFirewall extends IFirewallGate {
|
|
|
28
28
|
handleCors(context: ContextType): number | undefined;
|
|
29
29
|
/**
|
|
30
30
|
* Pose les en-têtes de sécurité applicatifs (CSP, Referrer-Policy, COOP/COEP/
|
|
31
|
-
* CORP, Permissions-Policy) sur la réponse
|
|
31
|
+
* CORP, Permissions-Policy) sur la réponse, selon son type (ADR-0024 §C11).
|
|
32
|
+
* No-op si désactivés / hors HTTP.
|
|
33
|
+
*
|
|
34
|
+
* @param contentType - `Content-Type` de la réponse ; `null` = sans contenu ;
|
|
35
|
+
* omis = type inconnu (politique complète).
|
|
36
|
+
* @param servedAsFile - corps servi tel quel (fichier statique, relais), qui
|
|
37
|
+
* ne peut pas porter de nonce : un document y reçoit la politique sans
|
|
38
|
+
* nonce ni `'strict-dynamic'` (ADR-0024 §C1).
|
|
32
39
|
*/
|
|
33
|
-
applySecurityHeaders(context: ContextType): void;
|
|
40
|
+
applySecurityHeaders(context: ContextType, contentType?: string | null, servedAsFile?: boolean): void;
|
|
34
41
|
/**
|
|
35
42
|
* Déclare des directives CSP additionnelles pour un module (ex. `@nodefony/frontend`
|
|
36
43
|
* en dev : origines Vite + `'unsafe-eval'`). Fusionnées dans le CSP de base et
|
|
37
44
|
* recomposées (re-split nonce) au (dé)enregistrement — jamais par requête.
|
|
45
|
+
*
|
|
46
|
+
* Ne lève jamais : une contribution MALFORMÉE est ignorée en entier (journal
|
|
47
|
+
* ERROR), une source TROP LARGE (`*`, `https:`, `https://*`) est retirée
|
|
48
|
+
* (journal WARNING). La politique ne devient jamais plus permissive que demandé,
|
|
49
|
+
* et l'application démarre. Ce que l'application écrit elle-même (sa config,
|
|
50
|
+
* ses `@Csp`) ne passe pas par ce filtre.
|
|
38
51
|
*/
|
|
39
52
|
registerCspOrigins(moduleName: string, fragment: CspFragment): void;
|
|
40
53
|
/** Retire les directives CSP d'un module (ex. `stopDev` du frontend). */
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* 3. **seuil par porteur** (`invalidBefore`) — révocation EN MASSE (tout access
|
|
15
15
|
* auto-porté émis avant un instant T est rejeté → « déconnexion globale » / ban).
|
|
16
16
|
*/
|
|
17
|
-
import type { IPage, IPageQuery, ISortableSource } from "nodefony";
|
|
17
|
+
import type { IPage, IPageQuery, ISortableSource, TokenStatus } from "nodefony";
|
|
18
18
|
/** Raison de révocation d'un jeton — tracée pour l'audit de sécurité. */
|
|
19
19
|
export type TokenRevokeReason = "logout" | "rotated" | "reuse_detected" | "manual" | "compromised" | "subject_disabled" | "expired_cleanup";
|
|
20
20
|
/**
|
|
@@ -153,8 +153,12 @@ export interface ITokenListQuery extends IPageQuery {
|
|
|
153
153
|
*/
|
|
154
154
|
status?: TokenStatus;
|
|
155
155
|
}
|
|
156
|
-
/**
|
|
157
|
-
|
|
156
|
+
/**
|
|
157
|
+
* État de vie d'un jeton — partition exhaustive, évaluée révoqué → expiré → actif.
|
|
158
|
+
* Défini au cœur (le temps réel le lit aussi, sans dépendre de la sécurité) et
|
|
159
|
+
* réexporté ici sous le même nom.
|
|
160
|
+
*/
|
|
161
|
+
export type { TokenStatus } from "nodefony";
|
|
158
162
|
/**
|
|
159
163
|
* Contrat du store de jetons — implémentations enregistrées via
|
|
160
164
|
* `registerTokenStore`, sélectionnées par config. Toutes les opérations sont
|
|
@@ -15,15 +15,25 @@ export interface IWebhookEndpoint {
|
|
|
15
15
|
/** Secret de signature **chiffré** au repos (blob `gcm1.…`). Jamais en clair. */
|
|
16
16
|
readonly secretEnc: string;
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Motifs des actions d'audit souscrites, dans la grammaire commune aux canaux et
|
|
19
|
+
* aux webhooks (ADR-0023 W1) : `*` vaut n'importe quelle suite de caractères —
|
|
20
|
+
* `"*"` désigne tout, `"login.*"` les actions qui commencent par `login.`,
|
|
21
|
+
* `"commande:*"` comme `"*:créée"` ; le reste est littéral, trois `*` au plus
|
|
22
|
+
* (refusé à l'enregistrement au-delà). La livraison ne part que si un motif désigne
|
|
23
|
+
* l'action. Sans `tenantId`, l'abonnement a le périmètre de la PLATEFORME : il reçoit
|
|
24
|
+
* aussi les événements qu'une version ultérieure rattachera à un locataire
|
|
25
|
+
* (ADR-0023 W2).
|
|
20
26
|
*/
|
|
21
27
|
readonly events: readonly string[];
|
|
22
28
|
/** Endpoint actif ? (désactivé = aucune livraison). */
|
|
23
29
|
readonly enabled: boolean;
|
|
24
30
|
/** Libellé humain optionnel (console admin). */
|
|
25
31
|
readonly description: string | null;
|
|
26
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Périmètre d'un locataire — réservé, toujours `null` tant que le multi-tenant
|
|
34
|
+
* n'existe pas (`register` refuse une valeur). Un abonnement sans périmètre ne
|
|
35
|
+
* reçoit que les événements sans périmètre (ADR-0023 W2).
|
|
36
|
+
*/
|
|
27
37
|
readonly tenantId: string | null;
|
|
28
38
|
/** Identité de l'admin créateur (soft ref, traçabilité). */
|
|
29
39
|
readonly createdBy: string | null;
|