@nodefony/user 10.0.0-alpha.5 → 10.0.0-alpha.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 CHANGED
@@ -112,7 +112,7 @@ Module de l'espace de travail `nodefony-core`. Dépendances de pair : `nodefony`
112
112
  `BcryptEncoder`).
113
113
 
114
114
  ```bash
115
- npm run build --workspace=src/packages/@nodefony/user
115
+ npm install @nodefony/user@alpha
116
116
  ```
117
117
 
118
118
  ## Licence
package/dist/index.js CHANGED
@@ -8,9 +8,12 @@ import { USER_DEFAULT_ORDER, USER_SORTABLE_FIELDS, USER_SORTABLE_FIELDS_COMMON,
8
8
  import { USER_COLUMNS, USER_TABLE_NAME, assertUserContract, attachExtraColumns, missingUserColumns } from "./nodefony/src/userContract.js";
9
9
  import { InMemoryUserRepository } from "./nodefony/src/InMemoryUserRepository.js";
10
10
  import { listUserStores, registerUserStore } from "./nodefony/src/userStoreRegistry.js";
11
+ import { truncatedPasswordHash } from "./nodefony/src/password/passwordHash.js";
12
+ import { DEFAULT_PASSWORD_POLICY, PasswordPolicy } from "./nodefony/src/password/passwordPolicy.js";
11
13
  import { UserNotFoundError } from "./nodefony/errors/UserNotFoundError.js";
12
14
  import { WeakPasswordError } from "./nodefony/errors/WeakPasswordError.js";
13
15
  import { mergeProfileIntoMetadata, profileFromClaims, projectProfile, validateProfilePatch } from "./nodefony/src/userProfile.js";
14
16
  import { UserService } from "./nodefony/service/UserService.js";
17
+ import { describeSeedFailure } from "./nodefony/src/password/seedFailure.js";
15
18
  import { USER_REVOKED_EVENT, createUserAdminApi, registerUserAdminApi, toUserSummary } from "./nodefony/src/admin/UserAdminApi.js";
16
- export { AnonymousUser, Argon2idEncoder, BaseUser, BcryptEncoder, InMemoryUserRepository, MigratingEncoder, ROLE_ANONYMOUS, USER_COLUMNS, USER_DEFAULT_ORDER, USER_REVOKED_EVENT, USER_SORTABLE_FIELDS, USER_SORTABLE_FIELDS_COMMON, USER_SORTABLE_FIELDS_IN_MEMORY, USER_TABLE_NAME, UserNotFoundError, UserService, WeakPasswordError, anonymousUser, assertUserContract, attachExtraColumns, createUserAdminApi, encoderFromConfig, listUserStores, mergeProfileIntoMetadata, missingUserColumns, profileFromClaims, projectProfile, registerUserAdminApi, registerUserStore, toUserSummary, validateProfilePatch };
19
+ export { AnonymousUser, Argon2idEncoder, BaseUser, BcryptEncoder, DEFAULT_PASSWORD_POLICY, InMemoryUserRepository, MigratingEncoder, PasswordPolicy, ROLE_ANONYMOUS, USER_COLUMNS, USER_DEFAULT_ORDER, USER_REVOKED_EVENT, USER_SORTABLE_FIELDS, USER_SORTABLE_FIELDS_COMMON, USER_SORTABLE_FIELDS_IN_MEMORY, USER_TABLE_NAME, UserNotFoundError, UserService, WeakPasswordError, anonymousUser, assertUserContract, attachExtraColumns, createUserAdminApi, describeSeedFailure, encoderFromConfig, listUserStores, mergeProfileIntoMetadata, missingUserColumns, profileFromClaims, projectProfile, registerUserAdminApi, registerUserStore, toUserSummary, truncatedPasswordHash, validateProfilePatch };
@@ -4,12 +4,20 @@ import { nodefonyError } from "nodefony";
4
4
  * Mot de passe refusé par la politique (connu-compromis / interdit) — `code = 400`.
5
5
  *
6
6
  * Levée par `UserService.createUser`/`changePassword` quand le
7
- * {@link IPasswordBlocklist} branché rejette le candidat. Le message reste
8
- * générique : ni la source de la liste ni le mot de passe ne sont exposés.
7
+ * {@link IPasswordBlocklist} branché rejette le candidat. Le message NOMME la
8
+ * règle enfreinte quand la politique sait la dire, et rien d'autre : ni la
9
+ * source de la liste, ni le mot de passe — il finirait dans un journal.
9
10
  */
10
11
  var WeakPasswordError = class extends nodefonyError {
11
- constructor() {
12
- super("Password rejected by policy", 400);
12
+ /** La règle enfreinte, telle que la politique l'a nommée (`null` si muette). */
13
+ violation;
14
+ /**
15
+ * @param violation - règle enfreinte, en français. Omise, le message reste
16
+ * générique — c'est le cas d'une implémentation qui ne rend qu'un booléen.
17
+ */
18
+ constructor(violation) {
19
+ super(violation != null && violation.length > 0 ? `Mot de passe refusé : ${violation}` : "Mot de passe refusé par la politique de l'application", 400);
20
+ this.violation = violation ?? null;
13
21
  }
14
22
  };
15
23
  //#endregion
@@ -1,4 +1,5 @@
1
1
  import { USER_FACETS } from "../src/userFilters.js";
2
+ import { PasswordPolicy } from "../src/password/passwordPolicy.js";
2
3
  import { UserNotFoundError } from "../errors/UserNotFoundError.js";
3
4
  import { WeakPasswordError } from "../errors/WeakPasswordError.js";
4
5
  import { profileFromClaims } from "../src/userProfile.js";
@@ -28,12 +29,16 @@ var UserService = class extends AbstractCrudService {
28
29
  encoder;
29
30
  #dummyHash = null;
30
31
  /**
31
- * Liste de blocage des mots de passe compromis (NIST SP 800-63B §5.1.1.2) —
32
- * hook opt-in consulté à la création/changement (jamais au login). `null`
33
- * par défaut : le framework fournit le point d'extension, l'application
34
- * branche sa source (top-10k, fichier, API k-anonymity).
32
+ * Politique de mot de passe (NIST SP 800-63B §5.1.1.2) — consultée à la
33
+ * création et au changement, jamais au login.
34
+ *
35
+ * **Posée PAR DÉFAUT**, et c'est le point : un défaut `null` ne se voit pas,
36
+ * et personne ne l'écrasait — `security:user:add compta --password abc`
37
+ * réussissait. L'application REMPLACE cet objet pour durcir ou pour brancher
38
+ * sa propre source ; la mettre à `null` désactive tout contrôle, et c'est
39
+ * alors un geste explicite, pas un oubli.
35
40
  */
36
- passwordBlocklist = null;
41
+ passwordBlocklist = new PasswordPolicy();
37
42
  /**
38
43
  * @param repository - source de persistance des utilisateurs (credential inclus).
39
44
  * @param encoder - encodeur de mot de passe (hash/verify/needsRehash).
@@ -51,7 +56,7 @@ var UserService = class extends AbstractCrudService {
51
56
  * @returns l'utilisateur persisté (id généré, hash stocké).
52
57
  */
53
58
  async createUser(input) {
54
- if (input.plainPassword != null) await this.#assertNotBlocked(input.plainPassword);
59
+ if (input.plainPassword != null) await this.#assertNotBlocked(input.plainPassword, input.identifier);
55
60
  const password = input.plainPassword != null ? await this.encoder.hash(input.plainPassword) : null;
56
61
  return this.create({
57
62
  identifier: input.identifier,
@@ -142,7 +147,9 @@ var UserService = class extends AbstractCrudService {
142
147
  * @returns l'utilisateur mis à jour, ou `null`.
143
148
  */
144
149
  async changePassword(id, plainPassword) {
145
- await this.#assertNotBlocked(plainPassword);
150
+ const target = await this.findById(id);
151
+ if (target === null) return null;
152
+ await this.#assertNotBlocked(plainPassword, target.identifier);
146
153
  const password = await this.encoder.hash(plainPassword);
147
154
  const updated = await this.repository.updateOne({ id }, { password });
148
155
  if (updated !== null) this.fire("onPasswordChanged", updated);
@@ -261,9 +268,27 @@ var UserService = class extends AbstractCrudService {
261
268
  if (Object.keys(oauthProfile).length > 0) data.metadata = { profile: oauthProfile };
262
269
  return this.create(data);
263
270
  }
264
- async #assertNotBlocked(plain) {
265
- if (this.passwordBlocklist === null) return;
266
- if (await this.passwordBlocklist.isBlocked(plain)) throw new WeakPasswordError();
271
+ /**
272
+ * Refuse un candidat que la politique écarte — et NOMME la règle enfreinte
273
+ * quand la politique sait la dire (`violation`, membre optionnel du contrat).
274
+ *
275
+ * `null` = aucune politique branchée : l'application l'a retiré sciemment.
276
+ *
277
+ * @param plain - mot de passe candidat.
278
+ * @param identifier - identifiant du compte visé, quand l'appelant le connaît :
279
+ * sans lui, la règle « le mot de passe répète l'identifiant » ne peut pas
280
+ * mordre, et les deux portes d'écriture n'appliqueraient pas la même loi.
281
+ */
282
+ async #assertNotBlocked(plain, identifier) {
283
+ const policy = this.passwordBlocklist;
284
+ if (policy === null) return;
285
+ const subject = identifier != null ? { identifier } : void 0;
286
+ if (policy.violation !== void 0) {
287
+ const violation = await policy.violation(plain, subject);
288
+ if (violation !== null) throw new WeakPasswordError(violation);
289
+ return;
290
+ }
291
+ if (await policy.isBlocked(plain, subject)) throw new WeakPasswordError();
267
292
  }
268
293
  fail(identifier, reason) {
269
294
  this.fire("onAuthenticationFailure", identifier, reason);
@@ -1,5 +1,6 @@
1
1
  import { listUserStores } from "../userStoreRegistry.js";
2
2
  import { USER_FACETS, USER_FILTERS, USER_STATS_FILTERS } from "../userFilters.js";
3
+ import { DEFAULT_PASSWORD_POLICY } from "../password/passwordPolicy.js";
3
4
  import { WeakPasswordError } from "../../errors/WeakPasswordError.js";
4
5
  import { mergeProfileIntoMetadata, projectProfile, validateProfilePatch } from "../userProfile.js";
5
6
  import { parseFilters, parsePageQuery } from "nodefony";
@@ -9,8 +10,15 @@ import { parseFilters, parsePageQuery } from "nodefony";
9
10
  * Les garde-fous anti-lockout protègent **ce** rôle (jamais déchoir le dernier).
10
11
  */
11
12
  const ADMIN_ROLE = "ROLE_NODEFONY_ADMIN";
12
- /** Longueur minimale d'un mot de passe self-service (OWASP ASVS V2.1.1 — plancher). */
13
- const MIN_PASSWORD_LENGTH = 8;
13
+ /**
14
+ * Longueur minimale d'un mot de passe self-service.
15
+ *
16
+ * DÉRIVÉE de la politique, jamais écrite ici : un plancher recopié devient un
17
+ * plancher qui diverge, et c'est alors la porte la plus laxiste qui fait loi.
18
+ * Ce contrôle ne remplace pas la politique — il rend seulement un 400 lisible
19
+ * AVANT de toucher au service (le refus complet, lui, vient de `changePassword`).
20
+ */
21
+ const MIN_PASSWORD_LENGTH = DEFAULT_PASSWORD_POLICY.minLength;
14
22
  /** Convertit un timestamp (`Date`/string ISO/number) en epoch ms — `null` si invalide. */
15
23
  function toEpoch(value) {
16
24
  if (value === void 0 || value === null) return null;
@@ -0,0 +1,24 @@
1
+ //#region nodefony/src/password/commonPasswordHashes.ts
2
+ /**
3
+ * Empreintes des mots de passe les plus courants — **fichier GÉNÉRÉ, ne pas éditer**.
4
+ *
5
+ * Regénérer :
6
+ * `node scripts/generate-password-blocklist.mjs <fichier-source>`
7
+ *
8
+ * Contenu : les 4 premiers octets du SHA-1 de chaque mot de passe, triés, en
9
+ * base64 d'un `Uint32Array` gros-boutien. Le clair n'est PAS embarqué — ni
10
+ * lisible, ni reconstructible depuis ce fichier.
11
+ *
12
+ * Sources :
13
+ * - `xato-net-10-million-passwords-10000.txt` — 9999 entrées, SHA-256 `c63d5e4ccc31344d662583cc39ca4bd5…`
14
+ *
15
+ * Corpus : SecLists (`danielmiessler/SecLists`), **licence MIT** — attribution
16
+ * portée ici, telle que la licence l'exige. Le corpus lui-même n'est pas
17
+ * redistribué : seules ces empreintes le sont.
18
+ */
19
+ /** 9999 empreintes triées (39996 octets décodés). */
20
+ const COMMON_PASSWORD_HASHES_BASE64 = "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";
21
+ /** Nombre d'empreintes — sert de contrôle au décodage. */
22
+ const COMMON_PASSWORD_HASH_COUNT = 9999;
23
+ //#endregion
24
+ export { COMMON_PASSWORD_HASHES_BASE64, COMMON_PASSWORD_HASH_COUNT };
@@ -0,0 +1,25 @@
1
+ import { createHash } from "node:crypto";
2
+ //#region nodefony/src/password/passwordHash.ts
3
+ /**
4
+ * Empreinte tronquée d'un mot de passe, pour la seule comparaison à une liste.
5
+ *
6
+ * ⚠️ Ce n'est PAS un hachage de stockage : SHA-1 tronqué n'est ni lent ni salé,
7
+ * et ne doit jamais toucher un mot de passe persisté — c'est le rôle de
8
+ * `IPasswordEncoder` (bcrypt/argon). Ici, l'enjeu est inverse : comparer 40 Ko
9
+ * d'empreintes sans embarquer un seul mot de passe en clair.
10
+ *
11
+ * La règle vit ici plutôt que dans le générateur ET dans la politique, parce que
12
+ * deux copies d'une même troncature divergeraient en silence : la liste
13
+ * deviendrait muette, et le contrôle passerait pour vert.
14
+ */
15
+ /**
16
+ * Les 4 premiers octets du SHA-1, en entier non signé.
17
+ *
18
+ * @param plain - mot de passe candidat.
19
+ * @returns l'empreinte tronquée, comparable aux entrées de la liste.
20
+ */
21
+ function truncatedPasswordHash(plain) {
22
+ return createHash("sha1").update(plain, "utf8").digest().readUInt32BE(0);
23
+ }
24
+ //#endregion
25
+ export { truncatedPasswordHash };
@@ -0,0 +1,188 @@
1
+ import { truncatedPasswordHash } from "./passwordHash.js";
2
+ import { readFileSync } from "node:fs";
3
+ //#region nodefony/src/password/passwordPolicy.ts
4
+ /**
5
+ * **Politique de mot de passe par défaut** — refuse ce qui est manifestement
6
+ * faible, et DIT laquelle de ses règles a mordu.
7
+ *
8
+ * ## Pourquoi des règles AVANT une liste
9
+ *
10
+ * Une liste, même de dix mille entrées, ne voit pas `aaaaaaaaaa`, `azertyuiop`
11
+ * ni un mot de passe égal au nom du compte. Les règles algorithmiques coûtent
12
+ * zéro octet, attrapent ces familles entières, et s'appliquent donc d'abord. La
13
+ * liste ne sert qu'à ce qu'elles ratent — un mot réel et courant, assez long
14
+ * pour passer la longueur minimale (`password123`, `iloveyou2`).
15
+ *
16
+ * ## Pourquoi la liste est LAZY
17
+ *
18
+ * Les empreintes pèsent 40 Ko de tas décodé, et 53 Ko de source. Le module qui
19
+ * les porte est donc chargé par un `import()` DYNAMIQUE, au premier contrôle
20
+ * réellement atteint — lors d'une création ou d'un changement de mot de passe,
21
+ * jamais au démarrage, jamais dans le chemin d'une requête. Une application qui
22
+ * ne crée aucun compte ne paie ni le décodage, ni même la lecture du fichier.
23
+ *
24
+ * ## Ce que cette politique ne fait PAS
25
+ *
26
+ * Elle ne juge ni la composition (majuscules, chiffres, caractères spéciaux) ni
27
+ * la rotation : le NIST (SP 800-63B §5.1.1.2) a retiré ces exigences, qui
28
+ * poussent aux mots de passe prévisibles (`Password1!`) sans rien ajouter à
29
+ * l'entropie réelle. Ce qui est mesuré ici, c'est la PRÉVISIBILITÉ.
30
+ *
31
+ * Une application durcit en composant sa propre implémentation
32
+ * d'`IPasswordBlocklist` ; elle ne peut pas désactiver ce contrôle en silence —
33
+ * le service en pose une par défaut, et la retirer est un geste explicite.
34
+ */
35
+ /** Défauts de la politique — source unique, relue par les tests. */
36
+ const DEFAULT_PASSWORD_POLICY = {
37
+ minLength: 10,
38
+ blocklist: [],
39
+ blocklistFile: null,
40
+ checkCommonPasswords: true
41
+ };
42
+ /**
43
+ * Suites de touches et de caractères d'où sortent les mots de passe « au clavier ».
44
+ *
45
+ * Les trois dispositions courantes y figurent : un `azerty` français et un
46
+ * `qwerty` anglais donnent des suites DIFFÉRENTES, et ne garder que l'une ferait
47
+ * un contrôle qui mord chez l'un et pas chez l'autre.
48
+ */
49
+ const KEYBOARD_RUNS = [
50
+ "abcdefghijklmnopqrstuvwxyz",
51
+ "0123456789",
52
+ "azertyuiop",
53
+ "qwertyuiop",
54
+ "qwertzuiop",
55
+ "asdfghjklm",
56
+ "qsdfghjklm",
57
+ "wxcvbn",
58
+ "zxcvbnm",
59
+ "yxcvbnm"
60
+ ];
61
+ /** Longueur minimale d'une suite pour que la coïncidence cesse d'en être une. */
62
+ const MIN_RUN_LENGTH = 4;
63
+ /** Vrai si `value` contient une tranche d'au moins 4 touches consécutives. */
64
+ function containsKeyboardRun(value) {
65
+ const lower = value.toLowerCase();
66
+ for (const run of KEYBOARD_RUNS) {
67
+ const reversed = [...run].reverse().join("");
68
+ for (const sequence of [run, reversed]) for (let start = 0; start + MIN_RUN_LENGTH <= sequence.length; start += 1) if (lower.includes(sequence.slice(start, start + MIN_RUN_LENGTH))) return true;
69
+ }
70
+ return false;
71
+ }
72
+ /**
73
+ * Vrai si `value` n'est qu'un motif court répété (`aaaa`, `abab`, `123123`).
74
+ *
75
+ * On s'arrête à un motif de 4 caractères : au-delà, « répéter » n'est plus un
76
+ * indice de faiblesse (`correct-horse-correct-horse` est long et imprévisible).
77
+ */
78
+ function isRepeatedPattern(value) {
79
+ const lower = value.toLowerCase();
80
+ for (let size = 1; size <= 4; size += 1) {
81
+ if (lower.length < size * 2 || lower.length % size !== 0) continue;
82
+ if (lower.slice(0, size).repeat(lower.length / size) === lower) return true;
83
+ }
84
+ return false;
85
+ }
86
+ /** Partie « nom » d'un identifiant : `marie.dupont@exemple.fr` → `marie.dupont`. */
87
+ function localPart(identifier) {
88
+ const at = identifier.indexOf("@");
89
+ return at > 0 ? identifier.slice(0, at) : identifier;
90
+ }
91
+ /** Longueur en deçà de laquelle une inclusion n'est plus significative. */
92
+ const MIN_IDENTIFIER_FRAGMENT = 4;
93
+ /**
94
+ * Politique par défaut : six règles, de la moins chère à la plus chère.
95
+ *
96
+ * Implémente {@link IPasswordBlocklist} — donc remplaçable par l'application,
97
+ * et composable (une implémentation maison peut l'appeler avant sa propre source).
98
+ */
99
+ var PasswordPolicy = class {
100
+ options;
101
+ /** Empreintes décodées — `null` tant qu'aucun contrôle n'en a eu besoin. */
102
+ #commonHashes = null;
103
+ /** Valeurs refusées en propre, repliées en minuscules — lazy, comme le reste. */
104
+ #denied = null;
105
+ /**
106
+ * @param options - réglages ; chaque clé absente prend le défaut sain.
107
+ */
108
+ constructor(options = {}) {
109
+ this.options = {
110
+ ...DEFAULT_PASSWORD_POLICY,
111
+ ...options
112
+ };
113
+ }
114
+ /**
115
+ * La règle enfreinte, ou `null` si le mot de passe est accepté.
116
+ *
117
+ * Le texte rendu nomme la règle SANS jamais citer le mot de passe : il finit
118
+ * dans un message d'erreur, donc potentiellement dans un journal.
119
+ *
120
+ * @param plain - mot de passe candidat.
121
+ * @param subject - ce qu'on sait du compte (son identifiant).
122
+ * @returns la règle enfreinte, en français, ou `null`.
123
+ */
124
+ async violation(plain, subject = {}) {
125
+ if (plain.length < this.options.minLength) return `trop court — ${this.options.minLength} caractères au minimum`;
126
+ const identifier = subject.identifier;
127
+ if (identifier != null && identifier.length > 0) {
128
+ const lower = plain.toLowerCase();
129
+ const name = localPart(identifier).toLowerCase();
130
+ if (lower === identifier.toLowerCase() || name.length >= MIN_IDENTIFIER_FRAGMENT && lower.includes(name)) return "contient l'identifiant du compte";
131
+ }
132
+ if (isRepeatedPattern(plain)) return "répète un même motif de bout en bout";
133
+ if (containsKeyboardRun(plain)) return "contient une suite de touches ou de chiffres";
134
+ if (this.#deniedValues().has(plain.toLowerCase())) return "figure dans la liste interdite de cette application";
135
+ if (this.options.checkCommonPasswords && await this.#isCommon(plain)) return "figure parmi les mots de passe les plus courants";
136
+ return null;
137
+ }
138
+ /**
139
+ * Le mot de passe doit-il être refusé ?
140
+ *
141
+ * Même règle, même code que {@link violation} — une seule implémentation, deux
142
+ * portes : le contrat ne demande qu'un booléen, l'appelant qui veut expliquer
143
+ * prend l'autre.
144
+ *
145
+ * @param plain - mot de passe candidat.
146
+ * @param subject - ce qu'on sait du compte.
147
+ */
148
+ async isBlocked(plain, subject = {}) {
149
+ return await this.violation(plain, subject) !== null;
150
+ }
151
+ /** Valeurs refusées en propre — config + fichier, lus une seule fois. */
152
+ #deniedValues() {
153
+ if (this.#denied !== null) return this.#denied;
154
+ const denied = /* @__PURE__ */ new Set();
155
+ for (const value of this.options.blocklist) if (value.length > 0) denied.add(value.toLowerCase());
156
+ const file = this.options.blocklistFile;
157
+ if (file != null && file.length > 0) for (const line of readFileSync(file, "utf8").split("\n")) {
158
+ const value = line.replace(/\r$/, "");
159
+ if (value.length > 0) denied.add(value.toLowerCase());
160
+ }
161
+ this.#denied = denied;
162
+ return denied;
163
+ }
164
+ /** Recherche binaire dans les empreintes triées (chargées au premier appel). */
165
+ async #isCommon(plain) {
166
+ if (this.#commonHashes === null) {
167
+ const { COMMON_PASSWORD_HASHES_BASE64, COMMON_PASSWORD_HASH_COUNT } = await import("./commonPasswordHashes.js");
168
+ const bytes = Buffer.from(COMMON_PASSWORD_HASHES_BASE64, "base64");
169
+ const hashes = new Uint32Array(COMMON_PASSWORD_HASH_COUNT);
170
+ for (let index = 0; index < hashes.length; index += 1) hashes[index] = bytes.readUInt32BE(index * 4);
171
+ this.#commonHashes = hashes;
172
+ }
173
+ const needle = truncatedPasswordHash(plain);
174
+ const hashes = this.#commonHashes;
175
+ let low = 0;
176
+ let high = hashes.length - 1;
177
+ while (low <= high) {
178
+ const middle = low + high >>> 1;
179
+ const value = hashes[middle];
180
+ if (value === needle) return true;
181
+ if (value < needle) low = middle + 1;
182
+ else high = middle - 1;
183
+ }
184
+ return false;
185
+ }
186
+ };
187
+ //#endregion
188
+ export { DEFAULT_PASSWORD_POLICY, PasswordPolicy };
@@ -0,0 +1,35 @@
1
+ //#region nodefony/src/password/seedFailure.ts
2
+ /**
3
+ * Rédige le message d'un semis raté : QUEL compte, QUELLE règle, QUEL geste.
4
+ *
5
+ * Les trois sont indissociables. Un message qui nomme la règle sans le compte
6
+ * laisse chercher lequel ; un message qui nomme le compte sans le geste laisse
7
+ * l'exploitant devant un constat. Et la dernière phrase dit ce que le lecteur se
8
+ * demande immédiatement : l'application tourne-t-elle encore ?
9
+ *
10
+ * @param cause - ce que la création du compte a levé.
11
+ * @param context - ce que l'appelant est seul à savoir.
12
+ * @returns le message à journaliser, sans saut de ligne (il part en `ERROR`).
13
+ */
14
+ function describeSeedFailure(cause, context) {
15
+ const { identifier, envVar, fromEnv, admin = false } = context;
16
+ return `Le compte "${identifier}" n'a PAS été semé : ${seedFailureReason(cause)}. ${fromEnv ? `Corrige ${envVar}` : `Ce mot de passe est le défaut écrit dans le code de l'application — pose ${envVar} (\`.env.local\`, gestionnaire de secrets)`}, ou crée le compte à la main : \`npx nodefony security:user:add ${identifier}${admin ? " --admin" : ""}\`. Le démarrage continue — l'application tourne, sans ce compte.`;
17
+ }
18
+ /**
19
+ * La cause, en clair — la règle de la politique quand il y en a une.
20
+ *
21
+ * La règle se lit sur la PROPRIÉTÉ `violation`, jamais par `instanceof` : une
22
+ * application qui se retrouve avec deux copies de `@nodefony/user` (hissage npm,
23
+ * lien local, monorepo) verrait le test de classe échouer et perdrait la seule
24
+ * information utile — au pire endroit, celui où l'on explique un échec.
25
+ *
26
+ * @param cause - ce qui a été levé.
27
+ * @returns la règle enfreinte, ou le message de l'erreur.
28
+ */
29
+ function seedFailureReason(cause) {
30
+ const violation = cause?.violation;
31
+ if (typeof violation === "string" && violation.length > 0) return `mot de passe refusé (${violation})`;
32
+ return cause instanceof Error ? cause.message : String(cause);
33
+ }
34
+ //#endregion
35
+ export { describeSeedFailure };
@@ -15,7 +15,7 @@
15
15
  * @remarks P5.5–5.9 livrés (contracts, base users, `UserService` + encoders,
16
16
  * adapters Drizzle/Mongoose). `IRole`/`IPermission` différés à P6.8.
17
17
  */
18
- export type { IUser, IPasswordAuthenticatedUser, ISocialProvider, IPasswordBlocklist, IPasswordEncoder, IPasswordVerifier, IUserProvider, IUserRepository, IUserListQuery, IOAuthProfile, IOAuthProvisionPolicy, IOAuthUserProvisioner, } from "./nodefony/contracts/index.js";
18
+ export type { IUser, IPasswordAuthenticatedUser, ISocialProvider, IPasswordBlocklist, IPasswordSubjectHint, IPasswordEncoder, IPasswordVerifier, IUserProvider, IUserRepository, IUserListQuery, IOAuthProfile, IOAuthProvisionPolicy, IOAuthUserProvisioner, } from "./nodefony/contracts/index.js";
19
19
  export { BaseUser } from "./nodefony/src/BaseUser.js";
20
20
  export type { IBaseUserOptions } from "./nodefony/src/BaseUser.js";
21
21
  export { AnonymousUser, anonymousUser, ROLE_ANONYMOUS, } from "./nodefony/src/AnonymousUser.js";
@@ -32,6 +32,11 @@ export type { IUserColumn, IUserRow, UserColumnType, UserColumnOrigin, } from ".
32
32
  export { registerUserStore, listUserStores, } from "./nodefony/src/userStoreRegistry.js";
33
33
  export { UserService } from "./nodefony/service/UserService.js";
34
34
  export type { ICreateUserInput, AuthFailureReason, } from "./nodefony/service/UserService.js";
35
+ export { PasswordPolicy, DEFAULT_PASSWORD_POLICY, } from "./nodefony/src/password/passwordPolicy.js";
36
+ export type { IPasswordPolicyOptions } from "./nodefony/src/password/passwordPolicy.js";
37
+ export { truncatedPasswordHash } from "./nodefony/src/password/passwordHash.js";
38
+ export { describeSeedFailure } from "./nodefony/src/password/seedFailure.js";
39
+ export type { ISeedFailureContext } from "./nodefony/src/password/seedFailure.js";
35
40
  export { UserNotFoundError } from "./nodefony/errors/UserNotFoundError.js";
36
41
  export { WeakPasswordError } from "./nodefony/errors/WeakPasswordError.js";
37
42
  export { createUserAdminApi, registerUserAdminApi, toUserSummary, USER_REVOKED_EVENT, } from "./nodefony/src/admin/UserAdminApi.js";
@@ -4,17 +4,37 @@
4
4
  * au login (le hash stocké ne permet plus de juger le clair, et refuser un
5
5
  * login existant verrouillerait l'utilisateur).
6
6
  *
7
- * Le cœur ne fournit QUE le point d'extension : la source de vérité (top-10k
8
- * embarqué, fichier d'exploitation, API k-anonymity type HaveIBeenPwned) est un
9
- * choix de déploiement, pas du ressort du framework. Brancher une implémentation
10
- * via `UserService.passwordBlocklist`.
7
+ * Le framework en pose une PAR DÉFAUT (`PasswordPolicy` : longueur, identifiant
8
+ * répété, suites de touches, mots de passe les plus courants) une application
9
+ * la remplace pour durcir ou pour brancher sa propre source (fichier
10
+ * d'exploitation, API k-anonymity type HaveIBeenPwned), jamais pour la
11
+ * désactiver par oubli. Le point de branchement est `UserService.passwordBlocklist`.
11
12
  */
12
13
  export interface IPasswordBlocklist {
13
14
  /**
14
15
  * Le mot de passe en clair est-il connu-compromis / interdit ?
15
16
  *
16
17
  * @param plain - mot de passe candidat (jamais journalisé par l'implémentation).
18
+ * @param subject - ce qu'on sait du compte visé, quand l'appelant le sait : un
19
+ * mot de passe ne doit pas répéter l'identifiant. Optionnel — une
20
+ * implémentation qui n'en a pas besoin l'ignore.
17
21
  * @returns `true` si le mot de passe doit être refusé.
18
22
  */
19
- isBlocked(plain: string): Promise<boolean>;
23
+ isBlocked(plain: string, subject?: IPasswordSubjectHint): Promise<boolean>;
24
+ /**
25
+ * La règle enfreinte, en français, ou `null` si le mot de passe est accepté.
26
+ *
27
+ * Optionnel, et c'est voulu : le contrat minimal reste un booléen. Quand une
28
+ * implémentation le fournit, le refus peut NOMMER sa cause — un « mot de passe
29
+ * refusé » sans motif envoie l'utilisateur deviner.
30
+ *
31
+ * @param plain - mot de passe candidat.
32
+ * @param subject - ce qu'on sait du compte visé.
33
+ */
34
+ violation?(plain: string, subject?: IPasswordSubjectHint): Promise<string | null>;
35
+ }
36
+ /** Ce qu'un appelant peut dire du compte visé — tout est optionnel. */
37
+ export interface IPasswordSubjectHint {
38
+ /** Identifiant du compte (courriel, login). */
39
+ identifier?: string;
20
40
  }
@@ -1,5 +1,5 @@
1
1
  export type { IUser, IPasswordAuthenticatedUser, ISocialProvider, } from "./IUser.js";
2
- export type { IPasswordBlocklist } from "./IPasswordBlocklist.js";
2
+ export type { IPasswordBlocklist, IPasswordSubjectHint, } from "./IPasswordBlocklist.js";
3
3
  export type { IPasswordEncoder } from "./IPasswordEncoder.js";
4
4
  export type { IPasswordVerifier } from "./IPasswordVerifier.js";
5
5
  export type { IUserProvider } from "./IUserProvider.js";
@@ -3,10 +3,17 @@ import { nodefonyError } from "nodefony";
3
3
  * Mot de passe refusé par la politique (connu-compromis / interdit) — `code = 400`.
4
4
  *
5
5
  * Levée par `UserService.createUser`/`changePassword` quand le
6
- * {@link IPasswordBlocklist} branché rejette le candidat. Le message reste
7
- * générique : ni la source de la liste ni le mot de passe ne sont exposés.
6
+ * {@link IPasswordBlocklist} branché rejette le candidat. Le message NOMME la
7
+ * règle enfreinte quand la politique sait la dire, et rien d'autre : ni la
8
+ * source de la liste, ni le mot de passe — il finirait dans un journal.
8
9
  */
9
10
  export declare class WeakPasswordError extends nodefonyError {
10
- constructor();
11
+ /** La règle enfreinte, telle que la politique l'a nommée (`null` si muette). */
12
+ readonly violation: string | null;
13
+ /**
14
+ * @param violation - règle enfreinte, en français. Omise, le message reste
15
+ * générique — c'est le cas d'une implémentation qui ne rend qu'un booléen.
16
+ */
17
+ constructor(violation?: string | null);
11
18
  }
12
19
  export default WeakPasswordError;
@@ -46,10 +46,14 @@ export declare class UserService extends AbstractCrudService<IPasswordAuthentica
46
46
  #private;
47
47
  protected readonly encoder: IPasswordEncoder;
48
48
  /**
49
- * Liste de blocage des mots de passe compromis (NIST SP 800-63B §5.1.1.2) —
50
- * hook opt-in consulté à la création/changement (jamais au login). `null`
51
- * par défaut : le framework fournit le point d'extension, l'application
52
- * branche sa source (top-10k, fichier, API k-anonymity).
49
+ * Politique de mot de passe (NIST SP 800-63B §5.1.1.2) — consultée à la
50
+ * création et au changement, jamais au login.
51
+ *
52
+ * **Posée PAR DÉFAUT**, et c'est le point : un défaut `null` ne se voit pas,
53
+ * et personne ne l'écrasait — `security:user:add compta --password abc`
54
+ * réussissait. L'application REMPLACE cet objet pour durcir ou pour brancher
55
+ * sa propre source ; la mettre à `null` désactive tout contrôle, et c'est
56
+ * alors un geste explicite, pas un oubli.
53
57
  */
54
58
  passwordBlocklist: IPasswordBlocklist | null;
55
59
  /**
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Empreintes des mots de passe les plus courants — **fichier GÉNÉRÉ, ne pas éditer**.
3
+ *
4
+ * Regénérer :
5
+ * `node scripts/generate-password-blocklist.mjs <fichier-source>`
6
+ *
7
+ * Contenu : les 4 premiers octets du SHA-1 de chaque mot de passe, triés, en
8
+ * base64 d'un `Uint32Array` gros-boutien. Le clair n'est PAS embarqué — ni
9
+ * lisible, ni reconstructible depuis ce fichier.
10
+ *
11
+ * Sources :
12
+ * - `xato-net-10-million-passwords-10000.txt` — 9999 entrées, SHA-256 `c63d5e4ccc31344d662583cc39ca4bd5…`
13
+ *
14
+ * Corpus : SecLists (`danielmiessler/SecLists`), **licence MIT** — attribution
15
+ * portée ici, telle que la licence l'exige. Le corpus lui-même n'est pas
16
+ * redistribué : seules ces empreintes le sont.
17
+ */
18
+ /** 9999 empreintes triées (39996 octets décodés). */
19
+ export declare const COMMON_PASSWORD_HASHES_BASE64: string;
20
+ /** Nombre d'empreintes — sert de contrôle au décodage. */
21
+ export declare const COMMON_PASSWORD_HASH_COUNT = 9999;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Les 4 premiers octets du SHA-1, en entier non signé.
3
+ *
4
+ * @param plain - mot de passe candidat.
5
+ * @returns l'empreinte tronquée, comparable aux entrées de la liste.
6
+ */
7
+ export declare function truncatedPasswordHash(plain: string): number;
@@ -0,0 +1,60 @@
1
+ import type { IPasswordBlocklist, IPasswordSubjectHint } from "../../contracts/IPasswordBlocklist.js";
2
+ /** Réglages d'une politique — tout est optionnel, les défauts sont sains. */
3
+ export interface IPasswordPolicyOptions {
4
+ /**
5
+ * Longueur minimale acceptée. Défaut : 10.
6
+ *
7
+ * Le plancher du NIST est 8 ; on retient 10 parce que le contrôle de liste le
8
+ * permet sans exiger de composition — et parce qu'en deçà, les empreintes des
9
+ * mots de passe courants couvrent déjà presque tout l'espace saisi.
10
+ */
11
+ minLength?: number;
12
+ /** Mots de passe refusés en propre (noms métier, du produit, du client). */
13
+ blocklist?: readonly string[];
14
+ /**
15
+ * Fichier de mots de passe refusés, une valeur par ligne (UTF-8). Lu au
16
+ * PREMIER contrôle, comme les empreintes — un fichier absent ou illisible est
17
+ * une erreur FRANCHE, jamais un contrôle qui se tait.
18
+ */
19
+ blocklistFile?: string | null;
20
+ /** Consulter les empreintes embarquées. Défaut : `true`. */
21
+ checkCommonPasswords?: boolean;
22
+ }
23
+ /** Défauts de la politique — source unique, relue par les tests. */
24
+ export declare const DEFAULT_PASSWORD_POLICY: Required<IPasswordPolicyOptions>;
25
+ /**
26
+ * Politique par défaut : six règles, de la moins chère à la plus chère.
27
+ *
28
+ * Implémente {@link IPasswordBlocklist} — donc remplaçable par l'application,
29
+ * et composable (une implémentation maison peut l'appeler avant sa propre source).
30
+ */
31
+ export declare class PasswordPolicy implements IPasswordBlocklist {
32
+ #private;
33
+ readonly options: Required<IPasswordPolicyOptions>;
34
+ /**
35
+ * @param options - réglages ; chaque clé absente prend le défaut sain.
36
+ */
37
+ constructor(options?: IPasswordPolicyOptions);
38
+ /**
39
+ * La règle enfreinte, ou `null` si le mot de passe est accepté.
40
+ *
41
+ * Le texte rendu nomme la règle SANS jamais citer le mot de passe : il finit
42
+ * dans un message d'erreur, donc potentiellement dans un journal.
43
+ *
44
+ * @param plain - mot de passe candidat.
45
+ * @param subject - ce qu'on sait du compte (son identifiant).
46
+ * @returns la règle enfreinte, en français, ou `null`.
47
+ */
48
+ violation(plain: string, subject?: IPasswordSubjectHint): Promise<string | null>;
49
+ /**
50
+ * Le mot de passe doit-il être refusé ?
51
+ *
52
+ * Même règle, même code que {@link violation} — une seule implémentation, deux
53
+ * portes : le contrat ne demande qu'un booléen, l'appelant qui veut expliquer
54
+ * prend l'autre.
55
+ *
56
+ * @param plain - mot de passe candidat.
57
+ * @param subject - ce qu'on sait du compte.
58
+ */
59
+ isBlocked(plain: string, subject?: IPasswordSubjectHint): Promise<boolean>;
60
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Le message d'un **semis de compte qui a échoué** — celui que l'erreur, seule,
3
+ * ne peut pas écrire.
4
+ *
5
+ * Le problème est un partage d'information, pas un défaut de code.
6
+ * {@link WeakPasswordError} nomme la règle enfreinte et **rien d'autre** : ni le
7
+ * compte visé, ni la valeur refusée, parce qu'un message d'erreur finit dans un
8
+ * journal. Le seul à savoir QUI était semé, et D'OÙ venait le mot de passe, est
9
+ * l'appelant. Tant que les deux moitiés restent séparées, l'exploitant lit
10
+ * « Mot de passe refusé : contient l'identifiant du compte » sans savoir quel
11
+ * compte, ni quoi corriger — vécu : une application générée ne démarrait plus,
12
+ * et la seule trace utile était une ligne perdue dans un journal détaché.
13
+ *
14
+ * Ce module réunit les deux moitiés, une fois pour tout le monde : le gabarit
15
+ * d'application, le dépôt de développement du framework et toute application
16
+ * qui sème ses propres comptes appellent la MÊME fonction. Recopier ce message
17
+ * dans chaque semis le ferait diverger en silence — et c'est le message, pas le
18
+ * code, qui fait la différence entre dix secondes et une demi-heure.
19
+ */
20
+ /** Ce que l'appelant sait du semis, et que l'erreur ignore. */
21
+ export interface ISeedFailureContext {
22
+ /** Identifiant du compte qu'on tentait de semer (`"admin"`). */
23
+ identifier: string;
24
+ /**
25
+ * Variable d'environnement qui porte le mot de passe (`"NF_ADMIN_PASSWORD"`).
26
+ */
27
+ envVar: string;
28
+ /**
29
+ * Le mot de passe venait-il de {@link envVar} ?
30
+ *
31
+ * `false` = il vient d'un défaut écrit dans le code de l'application. Envoyer
32
+ * corriger une variable que personne n'a posée ferait chercher là où il n'y a
33
+ * rien : le remède n'est alors pas le même geste.
34
+ */
35
+ fromEnv: boolean;
36
+ /** Le compte visait-il un rôle d'administration ? Décide du `--admin` du remède. */
37
+ admin?: boolean;
38
+ }
39
+ /**
40
+ * Rédige le message d'un semis raté : QUEL compte, QUELLE règle, QUEL geste.
41
+ *
42
+ * Les trois sont indissociables. Un message qui nomme la règle sans le compte
43
+ * laisse chercher lequel ; un message qui nomme le compte sans le geste laisse
44
+ * l'exploitant devant un constat. Et la dernière phrase dit ce que le lecteur se
45
+ * demande immédiatement : l'application tourne-t-elle encore ?
46
+ *
47
+ * @param cause - ce que la création du compte a levé.
48
+ * @param context - ce que l'appelant est seul à savoir.
49
+ * @returns le message à journaliser, sans saut de ligne (il part en `ERROR`).
50
+ */
51
+ export declare function describeSeedFailure(cause: unknown, context: ISeedFailureContext): string;
package/docs/index.md CHANGED
@@ -24,7 +24,7 @@ status: stable
24
24
  updated: 2026-07-19
25
25
  source: "src/packages/@nodefony/user/docs/index.md"
26
26
  coverageModule: user
27
- coverageFiles: UserService.ts,BaseUser.ts,AnonymousUser.ts,InMemoryUserRepository.ts,userProfile.ts,UserAdminApi.ts,Argon2idEncoder.ts,BcryptEncoder.ts,MigratingEncoder.ts,encoderFromConfig.ts
27
+ coverageFiles: UserService.ts,passwordPolicy.ts,BaseUser.ts,AnonymousUser.ts,InMemoryUserRepository.ts,userProfile.ts,UserAdminApi.ts,Argon2idEncoder.ts,BcryptEncoder.ts,MigratingEncoder.ts,encoderFromConfig.ts
28
28
  ---
29
29
 
30
30
  # @nodefony/user — l'identité, socle de toute la sécurité
@@ -464,7 +464,7 @@ lieu au seul moment où le mot de passe en clair existe côté serveur, un login
464
464
  ### L'ordre des vérifications
465
465
 
466
466
  `locked` → `disabled` → `no_password` → `bad_credentials`. Cet ordre est fixé
467
- (`UserService.ts:212`), mais il n'est pas observable de l'extérieur : la valeur de retour est `null`
467
+ (`UserService.ts:270`), mais il n'est pas observable de l'extérieur : la valeur de retour est `null`
468
468
  dans tous les cas, et la raison précise part dans l'événement `onAuthenticationFailure` — donc dans
469
469
  l'audit serveur, jamais dans la réponse.
470
470
 
@@ -536,7 +536,7 @@ ajoute quatre accès que le `Criteria` générique ne sait pas exprimer.
536
536
  | `loadUserByIdentifier()` | `IUserProvider` — **lève** `UserNotFoundError` si absent | `UserService.ts:301` |
537
537
  | `loadUserByOAuth()` | `IUserProvider` — lit un lien social, ne crée jamais | `UserService.ts:317` |
538
538
  | `refreshUser()` | recharge depuis la source (rôles frais, révocation immédiate) | `UserService.ts:331` |
539
- | `provisionOAuthUser()` | Shadow User : lit, ou crée si la politique l'autorise | `UserService.ts:306` |
539
+ | `provisionOAuthUser()` | Shadow User : lit, ou crée si la politique l'autorise | `UserService.ts:363` |
540
540
  | `passwordBlocklist` | champ opt-in — branche ta liste de mots de passe compromis | `UserService.ts:83` |
541
541
 
542
542
  **La distinction à retenir** : `loadUserByOAuth()` **lit** (et lève si le lien est inconnu) ;
@@ -670,7 +670,7 @@ flowchart TD
670
670
 
671
671
  Le compte local est le **Shadow User** : ton application garde sa propre ligne, avec ses propres
672
672
  rôles, son propre état actif/verrouillé. Le fournisseur n'est qu'une façon de prouver qu'on est bien
673
- la personne rattachée à cette ligne (`UserService.ts:306`).
673
+ la personne rattachée à cette ligne (`UserService.ts:363`).
674
674
 
675
675
  ### Trois invariants, et pourquoi ils existent
676
676
 
@@ -749,23 +749,93 @@ d'identité est un échec explicite, pas une valeur.
749
749
  Si tu veux seulement valider un couple identifiant/mot de passe contre un système externe, le contrat
750
750
  plus étroit `IPasswordVerifier` suffit (`IPasswordVerifier.ts:15`).
751
751
 
752
- ### Sa liste de mots de passe compromis
752
+ ### La politique de mot de passe — posée par défaut, et remplaçable
753
753
 
754
- Le NIST (SP 800-63B §5.1.1.2) recommande de refuser les mots de passe connus des fuites. Le framework
755
- fournit le **point d'extension**, pas la liste : la source (top 10 000 embarqué, fichier
756
- d'exploitation, API k-anonymity) est une décision de déploiement.
754
+ Le NIST (SP 800-63B §5.1.1.2) recommande de refuser les mots de passe prévisibles et de NE PAS
755
+ exiger de composition (majuscules, chiffres, caractères spéciaux), qui produit `Password1!` sans
756
+ ajouter d'entropie réelle. Nodefony suit cette lecture : ce qui est mesuré, c'est la
757
+ **prévisibilité**.
758
+
759
+ **La politique est posée d'office** sur tout `UserService` : rien à brancher, et c'est le point —
760
+ un point d'extension que personne ne remplit ne protège personne. Elle est consultée à la
761
+ **création** et au **changement**, jamais au login : là, le clair n'est plus jugeable, et refuser
762
+ une connexion existante enfermerait l'utilisateur dehors.
763
+
764
+ Six règles, dans cet ordre — les gratuites d'abord, la liste en dernier :
765
+
766
+ | # | Ce qui est refusé | Exemple refusé |
767
+ | --- | -------------------------------------------------------- | ---------------------------- |
768
+ | 1 | plus court que `minLength` (défaut **10**) | `abc`, `motdepas` |
769
+ | 2 | contient l'identifiant du compte | `marie.dupont-2026` |
770
+ | 3 | répète un même motif de bout en bout | `aaaaaaaaaa`, `abababababab` |
771
+ | 4 | contient une suite de touches ou de chiffres | `azertyuiop`, `0123456789` |
772
+ | 5 | figure dans la liste de l'application | ce que tu y mets |
773
+ | 6 | figure parmi les ~10 000 mots de passe les plus courants | `basketball`, `password123` |
774
+
775
+ Le refus **nomme la règle** : `Mot de passe refusé : figure parmi les mots de passe les plus
776
+ courants`. C'est ce que rendent `security:user:add` et `security:user:password` (code de sortie non
777
+ nul), et ce que la console d'administration renvoie en 400.
778
+
779
+ #### Régler la politique
757
780
 
758
781
  ```ts ignore
782
+ import { PasswordPolicy } from "@nodefony/user";
783
+
784
+ users.passwordBlocklist = new PasswordPolicy({
785
+ minLength: 14, // durcir
786
+ blocklist: ["MaSociete2026", "NomDuProduit"], // quelques mots métier
787
+ blocklistFile: "var/mots-de-passe-interdits.txt", // une politique maintenue à part
788
+ });
789
+ ```
790
+
791
+ | Option | Défaut | Ce qu'elle fait |
792
+ | ---------------------- | ------ | ----------------------------------------------------------- |
793
+ | `minLength` | `10` | longueur minimale |
794
+ | `blocklist` | `[]` | valeurs refusées, comparaison insensible à la casse |
795
+ | `blocklistFile` | `null` | fichier UTF-8, une valeur par ligne, lu au premier contrôle |
796
+ | `checkCommonPasswords` | `true` | consulter la liste embarquée |
797
+
798
+ Un **fichier introuvable fait échouer le contrôle**, il ne le rend pas muet : une politique qu'on
799
+ croit en place et qui n'a rien lu est pire qu'une politique absente.
800
+
801
+ #### Brancher sa propre source
802
+
803
+ `IPasswordBlocklist` reste le point d'extension — pour un annuaire d'entreprise, une base maison, ou
804
+ une API k-anonymity (type Have I Been Pwned, qui n'envoie que cinq caractères du hash). Elle
805
+ s'appelle en plus de la politique, pas à sa place :
806
+
807
+ ```ts ignore
808
+ const politique = new PasswordPolicy();
759
809
  users.passwordBlocklist = {
760
- async isBlocked(plain) {
761
- return TOP_10K.has(plain.toLowerCase());
762
- },
810
+ isBlocked: async (plain, subject) =>
811
+ (await politique.isBlocked(plain, subject)) ||
812
+ (await maSource.connu(plain)),
813
+ violation: async (plain, subject) =>
814
+ (await politique.violation(plain, subject)) ??
815
+ ((await maSource.connu(plain))
816
+ ? "figure dans notre annuaire de fuites"
817
+ : null),
763
818
  };
764
819
  ```
765
820
 
766
- Consultée à la **création** et au **changement**, jamais au login (`UserService.ts:359`) : au login,
767
- le clair n'est plus jugeable contre une politique, et refuser une connexion existante enfermerait
768
- l'utilisateur dehors. Un refus lève `WeakPasswordError` (400), avec un message générique.
821
+ Mettre `users.passwordBlocklist = null` désactive tout contrôle. C'est permis une application qui
822
+ juge les mots de passe en amont n'a pas à payer deux fois mais c'est alors un **geste explicite**,
823
+ plus un défaut qu'on ne voit pas.
824
+
825
+ #### La liste embarquée, et ce qu'elle coûte
826
+
827
+ ~10 000 empreintes de 4 octets (`Uint32Array` trié, recherche binaire) : **40 Ko**, et le module qui
828
+ les porte est chargé par un `import()` dynamique au premier contrôle — une application qui ne crée
829
+ aucun compte ne le lit jamais. Le clair n'est pas embarqué : seules les empreintes le sont, et elles
830
+ ne sont pas réversibles. La troncature rend une collision possible (~2,3 × 10⁻⁶ par test) ; elle
831
+ **refuse** alors un mot de passe sain, ce qui tombe du bon côté.
832
+
833
+ Le corpus vient de [SecLists](https://github.com/danielmiessler/SecLists) (licence MIT, attribution
834
+ portée dans l'artefact). Regénérer l'artefact :
835
+
836
+ ```bash
837
+ node scripts/generate-password-blocklist.mjs <fichier-source>
838
+ ```
769
839
 
770
840
  ### Son propre provisionnement OAuth
771
841
 
@@ -939,7 +1009,7 @@ Une erreur d'administration ne doit pas fermer la porte définitivement. Cinq ga
939
1009
  | supprimer ou désactiver le dernier admin actif | 409 |
940
1010
 
941
1011
  Le comptage passe par `countActiveAdmins()`, un `COUNT` natif au store — jamais un chargement complet
942
- en mémoire (`UserService.ts:154`).
1012
+ en mémoire (`UserService.ts:174`).
943
1013
 
944
1014
  ### Cascade de révocation
945
1015
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/user",
3
- "version": "10.0.0-alpha.5",
3
+ "version": "10.0.0-alpha.7",
4
4
  "description": "Contrats utilisateur de Nodefony, indépendants de l'ORM : interface IUser, utilisateurs de base, encodeurs de mot de passe et UserService — consommable sans @nodefony/security",
5
5
  "contributors": [],
6
6
  "type": "module",
@@ -40,8 +40,8 @@
40
40
  "peerDependencies": {
41
41
  "@node-rs/argon2": "^2.0.2",
42
42
  "@node-rs/bcrypt": "^1.10.0",
43
- "@nodefony/orm-core": "^10.0.0-alpha.5",
44
- "nodefony": "^10.0.0-alpha.5"
43
+ "@nodefony/orm-core": "^10.0.0-alpha.7",
44
+ "nodefony": "^10.0.0-alpha.7"
45
45
  },
46
46
  "peerDependenciesMeta": {
47
47
  "@node-rs/bcrypt": {
@@ -54,12 +54,12 @@
54
54
  "devDependencies": {
55
55
  "@node-rs/argon2": "^2.2.1",
56
56
  "@node-rs/bcrypt": "^1.10.8",
57
- "@nodefony/orm-core": "^10.0.0-alpha.5",
58
- "@types/node": "26.5.1",
59
- "@vitest/coverage-v8": "5.0.0",
60
- "nodefony": "^10.0.0-alpha.5",
57
+ "@nodefony/orm-core": "^10.0.0-alpha.7",
58
+ "@types/node": "26.6.1",
59
+ "@vitest/coverage-v8": "5.0.1",
60
+ "nodefony": "^10.0.0-alpha.7",
61
61
  "rimraf": "6.1.3",
62
- "vitest": "5.0.0"
62
+ "vitest": "5.0.1"
63
63
  },
64
64
  "repository": {
65
65
  "type": "git",