@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
@@ -25,15 +25,19 @@ const APP_KEYCLOAK_REALM_FILE = "docker/keycloak/import/realm.json";
25
25
  const APP_KEYCLOAK_THEMES_DIR = "docker/keycloak/themes";
26
26
  const APP_KEYCLOAK_THEME_DIR = `${APP_KEYCLOAK_THEMES_DIR}/${KEYCLOAK_THEME_NAME}`;
27
27
  /** Fichier de configuration de sécurité d'une application générée. */
28
- const APP_SECURITY_CONFIG_FILE = "nodefony/config/security.ts";
29
28
  /**
30
- * Ressource de l'API partenaire d'une application neuve en développement :
31
- * ce que rend `partnerResource()` du gabarit `complete` sans `NF_JWT_ISSUER`
32
- * ni `NF_PORT_HTTPS` — le port HTTPS par défaut, comme les URL de retour du
33
- * realm. Deux paquets, une valeur : le test de contribution compare le
34
- * gabarit rendu à cette constante.
29
+ * Le fichier où la démo de `create app --demo` (module privé `modules/demo`)
30
+ * déclare sa zone à jeton — la surcharge `module-security` qu'elle apporte.
35
31
  */
36
- const APP_PARTNER_DEV_RESOURCE = "https://localhost:5152/api/partner";
32
+ const DEMO_SECURITY_CONFIG_FILE = "modules/demo/nodefony/config/security.ts";
33
+ /**
34
+ * Ressource de l'API partenaire de la démo en développement : le chemin de sa
35
+ * zone (`/demo/api/partner`) rendu absolu contre l'émetteur de développement
36
+ * — le port HTTPS par défaut, comme les URL de retour du realm. Deux paquets,
37
+ * une valeur : le test de contribution la confronte à une application
38
+ * `--demo` réellement rendue.
39
+ */
40
+ const APP_PARTNER_DEV_RESOURCE = "https://localhost:5152/demo/api/partner";
37
41
  /** Les types de thème que le realm sait déclarer, et la clé qui les porte. */
38
42
  const THEME_TYPES = [
39
43
  "login",
@@ -124,6 +128,12 @@ function devMachineSecret(appName) {
124
128
  * rôle client `admin` (`roleMapping: { admin: "ROLE_ADMIN" }`), et les
125
129
  * audiences des zones `external-jwt` que la configuration rendue déclare.
126
130
  *
131
+ * Le client « machine » (`<app>-machine`, `client_credentials`) n'existe que
132
+ * s'il a une audience à recevoir : une application neuve n'ouvre aucune zone à
133
+ * jeton tiers (#746), et un client que rien n'accepte serait un secret de plus
134
+ * à garder pour rien. La démo, qui apporte l'API entre programmes, le fait
135
+ * naître en déclarant sa ressource.
136
+ *
127
137
  * @param appName - nom de l'application (= realm = identifiant de client)
128
138
  * @param themes - types de thème à déclarer
129
139
  * @param ids - `sub` des comptes de démonstration, FIXÉS dans le fichier :
@@ -150,7 +160,7 @@ function appDevRealmInput(appName, themes, ids, audiences = []) {
150
160
  description: "Administrateur de l'application — traduit en ROLE_ADMIN par la table roleMapping du fournisseur keycloak."
151
161
  }],
152
162
  realmRoles: [],
153
- machine: {
163
+ machine: audiences.length === 0 ? null : {
154
164
  clientId: `${appName}-machine`,
155
165
  secret: devMachineSecret(appName),
156
166
  name: `${appName} — appelant machine (compte de service)`,
@@ -191,17 +201,24 @@ function mountsKeycloak(context) {
191
201
  ].some((file) => context.read(file)?.includes("./docker/keycloak/") === true);
192
202
  }
193
203
  /**
194
- * Les audiences que la configuration RENDUE de l'application déclare. Le
195
- * gabarit `complete` ouvre la zone `partner` (`jwt` + `external-jwt`) : sans
196
- * son audience dans le realm, le jeton du client machine y est refusé dès le
197
- * premier essai, et `security:keycloak:realm --check` trouve le realm né en
198
- * désaccord avec la configuration.
204
+ * Les audiences que la configuration RENDUE de l'application déclare. Une
205
+ * application neuve n'en déclare aucune ; la démo ouvre la zone
206
+ * `demo-partner` (`jwt` + `external-jwt`) dans son propre module : sans son
207
+ * audience dans le realm, le jeton du client machine y serait refusé dès le
208
+ * premier essai, et `security:keycloak:realm --check` trouverait le realm né
209
+ * en désaccord avec la configuration.
210
+ *
211
+ * Le fichier de zone de la démo doit écrire le CHEMIN de la ressource : un
212
+ * module qui l'aurait changé n'a pas l'audience de cette constante, et
213
+ * mieux vaut ne rien inscrire qu'une audience que la zone refuserait.
199
214
  *
200
215
  * @param context - contexte de contribution
201
216
  * @returns les ressources à inscrire dans les jetons du realm
202
217
  */
203
218
  function renderedAudiences(context) {
204
- return context.read(APP_SECURITY_CONFIG_FILE)?.includes("partnerResource(") === true ? [APP_PARTNER_DEV_RESOURCE] : [];
219
+ const demo = context.read(DEMO_SECURITY_CONFIG_FILE);
220
+ const resourcePath = new URL(APP_PARTNER_DEV_RESOURCE).pathname;
221
+ return demo?.includes(`"${resourcePath}"`) === true ? [APP_PARTNER_DEV_RESOURCE] : [];
205
222
  }
206
223
  /**
207
224
  * Recopie les thèmes livrés dans l'application — `nodefony` et ses thèmes
@@ -1,5 +1,5 @@
1
- import { KeycloakRealmError } from "./keycloakRealmInput.js";
2
1
  import { KeycloakAdminError } from "./keycloakAdmin.js";
2
+ import { KeycloakRealmError } from "./keycloakRealmInput.js";
3
3
  import { Command } from "nodefony";
4
4
  import path from "node:path";
5
5
  // nodefony/keycloak/security-keycloak-realm.ts
@@ -1,11 +1,11 @@
1
- import { AuthenticationError } from "../errors/AuthenticationError.js";
2
1
  import { defineSecurityConfig } from "../config/defineModuleConfig.js";
3
- import { UserToken } from "../src/token/UserToken.js";
4
- import peekIssuer from "../src/authenticator/peekIssuer.js";
2
+ import { RemoteJwtVerifier } from "../src/token/RemoteJwtVerifier.js";
5
3
  import { JwtAuthenticator } from "../src/authenticator/JwtAuthenticator.js";
6
- import { ExternalSubjectResolver, externalSubjectOptions } from "../src/authenticator/ExternalSubjectResolver.js";
4
+ import peekIssuer from "../src/authenticator/peekIssuer.js";
7
5
  import { resolveJwtRuntime } from "../src/token/jwtRuntime.js";
8
- import { RemoteJwtVerifier } from "../src/token/RemoteJwtVerifier.js";
6
+ import { UserToken } from "../src/token/UserToken.js";
7
+ import { ExternalSubjectResolver, externalSubjectOptions } from "../src/authenticator/ExternalSubjectResolver.js";
8
+ import { AuthenticationError } from "../errors/AuthenticationError.js";
9
9
  import { ACCESS_ACCOUNT_RESOLVER, ACCESS_TOKEN_VERIFIER, Service } from "nodefony";
10
10
  // nodefony/service/accessTokenVerifier.ts
11
11
  const serviceName = ACCESS_TOKEN_VERIFIER;
@@ -67,7 +67,7 @@ var AccessTokenVerifierService = class extends Service {
67
67
  } catch {
68
68
  return;
69
69
  }
70
- const runtime = config.jwt.enabled ? resolveJwtRuntime(config.jwt) : null;
70
+ const runtime = config.jwt.enabled ? resolveJwtRuntime(config.jwt, config.areas) : null;
71
71
  const local = runtime === null ? null : new JwtAuthenticator(this.container, runtime);
72
72
  const { issuers, ...tuning } = config.resourceServer;
73
73
  let remote = null;
@@ -1,10 +1,10 @@
1
- import { ScopeGrantPolicy, refusedScopes } from "../src/token/scopeGrants.js";
1
+ import { TOKEN_FACETS } from "../src/token/tokenFilters.js";
2
2
  import { defineSecurityConfig } from "../config/defineModuleConfig.js";
3
- import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY, credentialsVersionOf } from "../src/accountBinding.js";
3
+ import { ApiKeyError } from "../errors/ApiKeyError.js";
4
4
  import { generateApiKey } from "../src/apikey/apiKeyFormat.js";
5
+ import { ScopeGrantPolicy, refusedScopes } from "../src/token/scopeGrants.js";
5
6
  import { recordAudit } from "../src/audit/recordAudit.js";
6
- import { TOKEN_FACETS } from "../src/token/tokenFilters.js";
7
- import { ApiKeyError } from "../errors/ApiKeyError.js";
7
+ import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY, credentialsVersionOf } from "../src/accountBinding.js";
8
8
  import { Service, countFacets } from "nodefony";
9
9
  import { randomUUID } from "node:crypto";
10
10
  // nodefony/service/apiKeys.ts
@@ -97,6 +97,9 @@ var ApiKeyService = class extends Service {
97
97
  * @param roles - rôles du porteur tels que son compte les porte ; omis =
98
98
  * aucun rôle, donc tout scope réservé est refusé (fail-closed).
99
99
  * @throws ApiKeyError 400 — nom vide/trop long, scope hors catalogue, expiry invalide.
100
+ * @throws ApiKeyError 400 — `tenantId` rempli : le périmètre d'un locataire n'est
101
+ * pas encore appliqué (aucun lecteur ne cloisonne sur lui) — l'accepter rendrait
102
+ * la clé trompeuse, et l'appliquer un jour lui retirerait des accès (rupture).
100
103
  * @throws ApiKeyError 403 — scope réservé à des rôles que le porteur n'a pas.
101
104
  * @throws ApiKeyError 409 — plafond `maxPerSubject` atteint.
102
105
  * @throws ApiKeyError 403 — aucun compte sous cet identifiant (la clé est liée
@@ -104,6 +107,7 @@ var ApiKeyService = class extends Service {
104
107
  * @throws ApiKeyError 503 — store ou annuaire `users` indisponible.
105
108
  */
106
109
  async createForSubject(subjectId, subjectType, opts, roles = []) {
110
+ if (opts.tenantId != null) throw new ApiKeyError("apiKeys: tenantId n'est pas encore pris en charge — le multi-tenant n'existe pas, et une clé rattachée à un locataire verrait tout. Laisser tenantId absent ou null.", 400);
107
111
  const store = this.#resolveStore();
108
112
  const name = this.#normalizeName(opts.name);
109
113
  const scopes = this.#normalizeScopes(opts.scopes);
@@ -1,5 +1,6 @@
1
1
  import { defineSecurityConfig } from "../config/defineModuleConfig.js";
2
2
  import { getAuditStoreFactory, listAuditStores } from "../src/audit/auditStoreRegistry.js";
3
+ import { AuditRefusalAggregator } from "../src/audit/AuditRefusalAggregator.js";
3
4
  import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Service, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore, storeConnectivity } from "nodefony";
4
5
  import { randomBytes } from "node:crypto";
5
6
  // nodefony/service/auditService.ts
@@ -30,11 +31,21 @@ var AuditService = class extends Service {
30
31
  #idPrefix = "";
31
32
  #seq = 0;
32
33
  #gc = null;
34
+ /** Agrégation des refus — `null` si désactivée par la config. */
35
+ #aggregator = null;
36
+ /** Balayage des fenêtres échues — armé au premier refus suivi, coupé à vide. */
37
+ #sweep = null;
38
+ #windowMs = 0;
33
39
  constructor(module) {
34
40
  super(serviceName, module.container, module.notificationsCenter, module.options);
35
41
  this.module = module;
36
- this.kernel?.once("onBoot", () => this.#build());
37
- this.kernel?.once("onTerminate", () => this.#shutdown());
42
+ this.kernel?.once("onBoot", () => {
43
+ try {
44
+ this.#build();
45
+ } finally {
46
+ this.kernel?.once("onTerminate", () => this.#shutdown());
47
+ }
48
+ });
38
49
  }
39
50
  #build() {
40
51
  let config;
@@ -91,9 +102,17 @@ var AuditService = class extends Service {
91
102
  onError: (e) => this.log(e, "WARNING")
92
103
  });
93
104
  this.#gc.start();
105
+ const aggregate = config.audit.aggregate;
106
+ if (aggregate.enabled) {
107
+ this.#windowMs = aggregate.windowMs;
108
+ this.#aggregator = new AuditRefusalAggregator(aggregate, (draft) => this.#write(draft, Date.now()));
109
+ }
94
110
  this.log(`audit service ready — store "${config.audit.store}", rétention ${config.audit.retentionDays}j`, "DEBUG");
95
111
  }
96
112
  #shutdown() {
113
+ this.#stopSweep();
114
+ this.#aggregator?.flush();
115
+ this.#aggregator = null;
97
116
  this.#gc?.stop();
98
117
  this.#gc = null;
99
118
  this.#listeners = null;
@@ -102,10 +121,21 @@ var AuditService = class extends Service {
102
121
  }
103
122
  record(draft) {
104
123
  if (!this.#enabled || this.#store === null) return;
124
+ const now = Date.now();
125
+ if (draft.outcome !== "success" && this.#aggregator !== null) {
126
+ const written = this.#aggregator.admit(draft, now);
127
+ if (this.#sweep === null) this.#startSweep();
128
+ if (!written) return;
129
+ }
130
+ this.#write(draft, now);
131
+ }
132
+ /** Persiste (fire-and-forget) et diffuse un événement horodaté `ts`. */
133
+ #write(draft, ts) {
134
+ if (this.#store === null) return;
105
135
  const event = {
106
136
  ...draft,
107
137
  id: `${this.#idPrefix}-${(this.#seq++).toString(36)}`,
108
- ts: Date.now()
138
+ ts
109
139
  };
110
140
  this.#store.append(event).catch((error) => this.log(error, "ERROR"));
111
141
  if (this.#listeners !== null) for (const listener of this.#listeners) try {
@@ -114,6 +144,21 @@ var AuditService = class extends Service {
114
144
  this.log(error, "ERROR");
115
145
  }
116
146
  }
147
+ /** Arme le balayage des fenêtres échues (une fois par fenêtre, `unref`). */
148
+ #startSweep() {
149
+ this.#sweep = setInterval(() => {
150
+ const aggregator = this.#aggregator;
151
+ if (aggregator === null) return this.#stopSweep();
152
+ aggregator.sweep(Date.now());
153
+ if (aggregator.size === 0) this.#stopSweep();
154
+ }, this.#windowMs);
155
+ this.#sweep.unref();
156
+ }
157
+ #stopSweep() {
158
+ if (this.#sweep === null) return;
159
+ clearInterval(this.#sweep);
160
+ this.#sweep = null;
161
+ }
117
162
  subscribe(listener) {
118
163
  this.#listeners ??= [];
119
164
  this.#listeners.push(listener);
@@ -1,12 +1,12 @@
1
- import { securityConfigSchema } from "../config/config.js";
2
- import { InsecureTransportError } from "../errors/InsecureTransportError.js";
3
1
  import { AuthenticationError } from "../errors/AuthenticationError.js";
4
2
  import { ThrottledError } from "../errors/ThrottledError.js";
5
- import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY, credentialsVersionOf } from "../src/accountBinding.js";
3
+ import { InsecureTransportError } from "../errors/InsecureTransportError.js";
6
4
  import { resolveBoundSessionIdentity, resolveSessionIdentity } from "../src/sessionIdentity.js";
5
+ import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY, credentialsVersionOf } from "../src/accountBinding.js";
7
6
  import { recordAudit } from "../src/audit/recordAudit.js";
8
7
  import { readAuditContext } from "../src/audit/readAuditContext.js";
9
8
  import { amrForFactor } from "../src/sessionAuthentication.js";
9
+ import { securityConfigSchema } from "../config/config.js";
10
10
  import { LOGIN_PAGE_SKIN_SPECS, MFA_METHOD_TOTP, SESSION_AMR_KEY, SESSION_AUTH_AT_KEY, Service } from "nodefony";
11
11
  import { requestUser } from "@nodefony/user";
12
12
  // nodefony/service/authFlow.ts
@@ -284,8 +284,8 @@ var AuthFlow = class extends Service {
284
284
  * (mêmes contrôles que le `SessionAuthenticator` — source unique).
285
285
  *
286
286
  * @returns la projection publique, ou `null` (pas de session, session
287
- * orpheline, compte verrouillé/désactivé) — le handler répond `200`
288
- * `{ user: null }`.
287
+ * orpheline, compte verrouillé/désactivé — la session périmée est alors
288
+ * détruite, cookie compris) — le handler répond `200` `{ user: null }`.
289
289
  */
290
290
  async me(context) {
291
291
  const identifier = context.session?.user;
@@ -295,7 +295,10 @@ var AuthFlow = class extends Service {
295
295
  try {
296
296
  return toSafeUser(await resolveBoundSessionIdentity(this.#resolveUsers(), identifier, context.session?.getMetaBag(ACCOUNT_ID_KEY), context.session?.getMetaBag(CREDENTIALS_VERSION_KEY)));
297
297
  } catch (error) {
298
- if (error instanceof AuthenticationError) return null;
298
+ if (error instanceof AuthenticationError) {
299
+ await context.session?.destroy(true);
300
+ return null;
301
+ }
299
302
  throw error;
300
303
  }
301
304
  }
@@ -1,6 +1,6 @@
1
- import { recordAudit } from "../src/audit/recordAudit.js";
2
1
  import "../contracts/IAccessVoter.js";
3
2
  import { listVoterFactories } from "../src/voter/voterRegistry.js";
3
+ import { recordAudit } from "../src/audit/recordAudit.js";
4
4
  import { Service, logColor } from "nodefony";
5
5
  // nodefony/service/authorization.ts
6
6
  const serviceName = "authorization";