@nodefony/user 10.0.0-alpha.1
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/LICENSE +544 -0
- package/README.md +120 -0
- package/dist/index.js +16 -0
- package/dist/nodefony/contracts/IOAuthUserProvisioner.js +1 -0
- package/dist/nodefony/contracts/IPasswordBlocklist.js +1 -0
- package/dist/nodefony/contracts/IPasswordEncoder.js +1 -0
- package/dist/nodefony/contracts/IPasswordVerifier.js +1 -0
- package/dist/nodefony/contracts/IUser.js +1 -0
- package/dist/nodefony/contracts/IUserProfile.js +1 -0
- package/dist/nodefony/contracts/IUserProvider.js +1 -0
- package/dist/nodefony/contracts/IUserRepository.js +1 -0
- package/dist/nodefony/contracts/index.js +1 -0
- package/dist/nodefony/errors/UserNotFoundError.js +19 -0
- package/dist/nodefony/errors/WeakPasswordError.js +16 -0
- package/dist/nodefony/service/UserService.js +280 -0
- package/dist/nodefony/src/AnonymousUser.js +37 -0
- package/dist/nodefony/src/BaseUser.js +121 -0
- package/dist/nodefony/src/InMemoryUserRepository.js +230 -0
- package/dist/nodefony/src/admin/UserAdminApi.js +588 -0
- package/dist/nodefony/src/encoders/Argon2idEncoder.js +107 -0
- package/dist/nodefony/src/encoders/BcryptEncoder.js +75 -0
- package/dist/nodefony/src/encoders/MigratingEncoder.js +88 -0
- package/dist/nodefony/src/encoders/encoderFromConfig.js +37 -0
- package/dist/nodefony/src/userContract.js +247 -0
- package/dist/nodefony/src/userFilters.js +66 -0
- package/dist/nodefony/src/userProfile.js +195 -0
- package/dist/nodefony/src/userSort.js +53 -0
- package/dist/nodefony/src/userStoreRegistry.js +41 -0
- package/dist/types/index.d.ts +40 -0
- package/dist/types/nodefony/contracts/IOAuthUserProvisioner.d.ts +69 -0
- package/dist/types/nodefony/contracts/IPasswordBlocklist.d.ts +20 -0
- package/dist/types/nodefony/contracts/IPasswordEncoder.d.ts +49 -0
- package/dist/types/nodefony/contracts/IPasswordVerifier.d.ts +26 -0
- package/dist/types/nodefony/contracts/IUser.d.ts +68 -0
- package/dist/types/nodefony/contracts/IUserProfile.d.ts +29 -0
- package/dist/types/nodefony/contracts/IUserProvider.d.ts +44 -0
- package/dist/types/nodefony/contracts/IUserRepository.d.ts +129 -0
- package/dist/types/nodefony/contracts/index.d.ts +7 -0
- package/dist/types/nodefony/errors/UserNotFoundError.d.ts +15 -0
- package/dist/types/nodefony/errors/WeakPasswordError.d.ts +12 -0
- package/dist/types/nodefony/service/UserService.d.ts +179 -0
- package/dist/types/nodefony/src/AnonymousUser.d.ts +27 -0
- package/dist/types/nodefony/src/BaseUser.d.ts +98 -0
- package/dist/types/nodefony/src/InMemoryUserRepository.d.ts +73 -0
- package/dist/types/nodefony/src/admin/UserAdminApi.d.ts +119 -0
- package/dist/types/nodefony/src/encoders/Argon2idEncoder.d.ts +83 -0
- package/dist/types/nodefony/src/encoders/BcryptEncoder.d.ts +55 -0
- package/dist/types/nodefony/src/encoders/MigratingEncoder.d.ts +68 -0
- package/dist/types/nodefony/src/encoders/encoderFromConfig.d.ts +36 -0
- package/dist/types/nodefony/src/userContract.d.ts +198 -0
- package/dist/types/nodefony/src/userFilters.d.ts +80 -0
- package/dist/types/nodefony/src/userProfile.d.ts +56 -0
- package/dist/types/nodefony/src/userSort.d.ts +41 -0
- package/dist/types/nodefony/src/userStoreRegistry.d.ts +15 -0
- package/docs/ajouter-des-champs.md +189 -0
- package/docs/index.md +1113 -0
- package/package.json +90 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { IPasswordEncoder } from "../../contracts/IPasswordEncoder.js";
|
|
2
|
+
/**
|
|
3
|
+
* Paramètres de coût Argon2id (RFC 9106).
|
|
4
|
+
*
|
|
5
|
+
* Les défauts suivent le minimum OWASP 2026 (m=19 MiB, t=2, p=1) — déjà imposé
|
|
6
|
+
* par le schéma Zod de `@nodefony/security` (`encoders.memoryKiB.min(19456)`).
|
|
7
|
+
* L'encodeur ne valide ici que les bornes TECHNIQUES de l'algorithme : la
|
|
8
|
+
* politique de sécurité vit dans la config (et permet aux tests d'utiliser des
|
|
9
|
+
* coûts bas, rapides).
|
|
10
|
+
*/
|
|
11
|
+
export interface Argon2idOptions {
|
|
12
|
+
/** Mémoire par hash en KiB (défaut 19456 = 19 MiB, minimum OWASP). */
|
|
13
|
+
memoryKiB?: number;
|
|
14
|
+
/** Nombre de passes sur la mémoire (défaut 3 — voir DEFAULT_TIME_COST). */
|
|
15
|
+
timeCost?: number;
|
|
16
|
+
/** Nombre de threads/lanes (défaut 1 — chaque lane alloue `memoryKiB`). */
|
|
17
|
+
parallelism?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Encodeur de mot de passe **Argon2id** (RFC 9106) — recommandation NIST/OWASP 2026.
|
|
21
|
+
*
|
|
22
|
+
* Fonction de dérivation à MÉMOIRE DURE : chaque vérification exige `memoryKiB`
|
|
23
|
+
* de RAM en plus du CPU, ce qui ruine les attaques massivement parallèles
|
|
24
|
+
* (GPU/ASIC ont des milliers de cœurs mais pas 19 MiB de mémoire dédiée par
|
|
25
|
+
* cœur). Le variant `id` est hybride : résistant aux canaux auxiliaires
|
|
26
|
+
* (première moitié indépendante du mot de passe) ET aux compromis temps-mémoire.
|
|
27
|
+
*
|
|
28
|
+
* Délègue à `@node-rs/argon2` (binding NAPI Rust, exécuté hors thread principal,
|
|
29
|
+
* **peerDependency optionnelle**) : le binaire natif est importé DYNAMIQUEMENT
|
|
30
|
+
* au premier `hash`/`verify` — instancier l'encodeur ne charge rien.
|
|
31
|
+
*/
|
|
32
|
+
export declare class Argon2idEncoder implements IPasswordEncoder {
|
|
33
|
+
/** Mémoire par hash (KiB) utilisée pour produire les nouveaux hashs. */
|
|
34
|
+
readonly memoryKiB: number;
|
|
35
|
+
/** Passes sur la mémoire utilisées pour produire les nouveaux hashs. */
|
|
36
|
+
readonly timeCost: number;
|
|
37
|
+
/** Lanes parallèles utilisées pour produire les nouveaux hashs. */
|
|
38
|
+
readonly parallelism: number;
|
|
39
|
+
/**
|
|
40
|
+
* @param options - coûts Argon2id ; défauts = minimum OWASP (19 MiB, t=2, p=1).
|
|
41
|
+
* @throws {RangeError} si un paramètre viole les bornes techniques de
|
|
42
|
+
* l'algorithme (entiers, `t ≥ 1`, `1 ≤ p ≤ 255`, `m ≥ 8×p`).
|
|
43
|
+
*/
|
|
44
|
+
constructor(options?: Argon2idOptions);
|
|
45
|
+
/**
|
|
46
|
+
* Reconnaît un hash argon2 (tous variants `$argon2d|i|id$`) — parsing pur, sync.
|
|
47
|
+
*
|
|
48
|
+
* Tous les variants sont supportés à la VÉRIFICATION (le binding parse le
|
|
49
|
+
* format PHC) ; {@link needsRehash} se charge de moderniser `d`/`i` vers `id`.
|
|
50
|
+
*
|
|
51
|
+
* @param hash - hash stocké à inspecter.
|
|
52
|
+
* @returns `true` si le hash est au format argon2.
|
|
53
|
+
*/
|
|
54
|
+
supports(hash: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Hache un mot de passe en clair (sel généré, paramètres inclus dans la sortie PHC).
|
|
57
|
+
*
|
|
58
|
+
* @param plain - mot de passe en clair.
|
|
59
|
+
* @returns le hash argon2id à persister.
|
|
60
|
+
*/
|
|
61
|
+
hash(plain: string): Promise<string>;
|
|
62
|
+
/**
|
|
63
|
+
* Vérifie un mot de passe en clair contre un hash stocké (temps constant interne).
|
|
64
|
+
*
|
|
65
|
+
* Les paramètres de vérification sont LUS DANS le hash PHC (pas ceux de
|
|
66
|
+
* l'encodeur) : un hash produit avec d'anciens coûts reste vérifiable.
|
|
67
|
+
*
|
|
68
|
+
* @param plain - mot de passe fourni à la connexion.
|
|
69
|
+
* @param hash - hash argon2 stocké.
|
|
70
|
+
* @returns `true` si la correspondance est valide.
|
|
71
|
+
*/
|
|
72
|
+
verify(plain: string, hash: string): Promise<boolean>;
|
|
73
|
+
/**
|
|
74
|
+
* Indique si un hash doit être recalculé : format non argon2, variant non
|
|
75
|
+
* `id`, version antérieure à 0x13, ou coûts stockés INFÉRIEURS aux coûts
|
|
76
|
+
* courants (politique affaiblie). Des coûts supérieurs ne déclenchent pas de
|
|
77
|
+
* re-hash (jamais de downgrade silencieux).
|
|
78
|
+
*
|
|
79
|
+
* @param hash - hash stocké à inspecter.
|
|
80
|
+
* @returns `true` si un re-hash est recommandé au prochain login réussi.
|
|
81
|
+
*/
|
|
82
|
+
needsRehash(hash: string): boolean;
|
|
83
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { IPasswordEncoder } from "../../contracts/IPasswordEncoder.js";
|
|
2
|
+
/**
|
|
3
|
+
* Encodeur de mot de passe **bcrypt** — implémentation de référence d'{@link IPasswordEncoder}.
|
|
4
|
+
*
|
|
5
|
+
* Délègue à `@node-rs/bcrypt` (binding NAPI Rust) : hachage/vérification réellement
|
|
6
|
+
* asynchrones (exécutés hors thread principal), ne bloquent pas la boucle
|
|
7
|
+
* d'événements même au coût 12. `@node-rs/bcrypt` est une **peerDependency
|
|
8
|
+
* optionnelle** : seules les applications qui authentifient par mot de passe local
|
|
9
|
+
* la tirent ; un consommateur qui n'importe que `IUser`/`BaseUser` ne charge jamais
|
|
10
|
+
* ce module ni le binaire natif.
|
|
11
|
+
*
|
|
12
|
+
* @remarks Le binding natif est importé DYNAMIQUEMENT au premier `hash`/`verify`
|
|
13
|
+
* (instancier l'encodeur ne charge rien — la peerDep optionnelle n'est requise
|
|
14
|
+
* qu'au premier usage réel). Le coût est paramétrable au constructeur pour
|
|
15
|
+
* permettre un re-hash progressif via {@link BcryptEncoder.needsRehash}.
|
|
16
|
+
*/
|
|
17
|
+
export declare class BcryptEncoder implements IPasswordEncoder {
|
|
18
|
+
/** Coût bcrypt utilisé pour produire les nouveaux hashs. */
|
|
19
|
+
readonly rounds: number;
|
|
20
|
+
/**
|
|
21
|
+
* @param rounds - coût bcrypt (log2 des itérations). Entier dans `[4, 31]`.
|
|
22
|
+
* @throws {RangeError} si `rounds` est hors de l'intervalle valide.
|
|
23
|
+
*/
|
|
24
|
+
constructor(rounds?: number);
|
|
25
|
+
/**
|
|
26
|
+
* Reconnaît un hash bcrypt (`$2a$`/`$2b$`/`$2y$` + coût) — parsing pur, sync.
|
|
27
|
+
*
|
|
28
|
+
* @param hash - hash stocké à inspecter.
|
|
29
|
+
* @returns `true` si le hash est au format bcrypt.
|
|
30
|
+
*/
|
|
31
|
+
supports(hash: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Hache un mot de passe en clair (sel généré et inclus dans la sortie).
|
|
34
|
+
*
|
|
35
|
+
* @param plain - mot de passe en clair.
|
|
36
|
+
* @returns le hash bcrypt à persister.
|
|
37
|
+
*/
|
|
38
|
+
hash(plain: string): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* Vérifie un mot de passe en clair contre un hash stocké (temps constant interne).
|
|
41
|
+
*
|
|
42
|
+
* @param plain - mot de passe fourni à la connexion.
|
|
43
|
+
* @param hash - hash bcrypt stocké.
|
|
44
|
+
* @returns `true` si la correspondance est valide.
|
|
45
|
+
*/
|
|
46
|
+
verify(plain: string, hash: string): Promise<boolean>;
|
|
47
|
+
/**
|
|
48
|
+
* Indique si un hash doit être recalculé (coût stocké inférieur au coût courant,
|
|
49
|
+
* ou format non bcrypt).
|
|
50
|
+
*
|
|
51
|
+
* @param hash - hash stocké à inspecter.
|
|
52
|
+
* @returns `true` si un re-hash est recommandé au prochain login réussi.
|
|
53
|
+
*/
|
|
54
|
+
needsRehash(hash: string): boolean;
|
|
55
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { IPasswordEncoder } from "../../contracts/IPasswordEncoder.js";
|
|
2
|
+
/**
|
|
3
|
+
* Encodeur composite de **migration d'algorithme** — accepte les hashs legacy à
|
|
4
|
+
* la vérification, ne produit que des hashs au format courant.
|
|
5
|
+
*
|
|
6
|
+
* Une base existante (ex. bcrypt) ne peut pas être convertie hors-ligne : les
|
|
7
|
+
* mots de passe n'existent qu'au moment du login. Ce composite fait la
|
|
8
|
+
* transition sans rien casser :
|
|
9
|
+
*
|
|
10
|
+
* - {@link hash} → toujours l'encodeur PRINCIPAL (le format cible) ;
|
|
11
|
+
* - {@link verify} → routé vers le premier encodeur qui {@link IPasswordEncoder.supports | reconnaît}
|
|
12
|
+
* le hash stocké (principal d'abord, puis legacy dans l'ordre) ;
|
|
13
|
+
* - {@link needsRehash} → `true` dès que le hash n'est pas au format principal
|
|
14
|
+
* → `UserService.authenticate` re-hashe au prochain login réussi (seul moment
|
|
15
|
+
* où le clair existe) = migration transparente, comptes legacy compris.
|
|
16
|
+
*
|
|
17
|
+
* Exemple — migration bcrypt → argon2id :
|
|
18
|
+
* ```ts
|
|
19
|
+
* new MigratingEncoder(new Argon2idEncoder(), [new BcryptEncoder()]);
|
|
20
|
+
* ```
|
|
21
|
+
* Le jour où plus aucun hash bcrypt ne subsiste, retirer le legacy.
|
|
22
|
+
*/
|
|
23
|
+
export declare class MigratingEncoder implements IPasswordEncoder {
|
|
24
|
+
#private;
|
|
25
|
+
/** Encodeur cible — produit tous les nouveaux hashs. */
|
|
26
|
+
readonly primary: IPasswordEncoder;
|
|
27
|
+
/** Encodeurs acceptés en lecture seule pendant la migration. */
|
|
28
|
+
readonly legacy: readonly IPasswordEncoder[];
|
|
29
|
+
/**
|
|
30
|
+
* @param primary - encodeur cible (format des nouveaux hashs).
|
|
31
|
+
* @param legacy - encodeurs des formats encore présents en base (vérification seule).
|
|
32
|
+
*/
|
|
33
|
+
constructor(primary: IPasswordEncoder, legacy: readonly IPasswordEncoder[]);
|
|
34
|
+
/**
|
|
35
|
+
* Reconnaît un hash si l'UN des encodeurs (principal ou legacy) le reconnaît.
|
|
36
|
+
*
|
|
37
|
+
* @param hash - hash stocké à inspecter.
|
|
38
|
+
* @returns `true` si un membre du composite sait vérifier ce hash.
|
|
39
|
+
*/
|
|
40
|
+
supports(hash: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Hache avec l'encodeur PRINCIPAL uniquement (jamais un format legacy).
|
|
43
|
+
*
|
|
44
|
+
* @param plain - mot de passe en clair.
|
|
45
|
+
* @returns le hash au format cible.
|
|
46
|
+
*/
|
|
47
|
+
hash(plain: string): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Vérifie en routant vers l'encodeur dont le format correspond au hash stocké.
|
|
50
|
+
*
|
|
51
|
+
* Hash d'un format inconnu de tous → `false` (jamais d'erreur : un credential
|
|
52
|
+
* invérifiable est un credential invalide — l'égalisation de temps des chemins
|
|
53
|
+
* d'échec reste portée par `UserService`).
|
|
54
|
+
*
|
|
55
|
+
* @param plain - mot de passe fourni à la connexion.
|
|
56
|
+
* @param hash - hash stocké (format courant ou legacy).
|
|
57
|
+
* @returns `true` si la correspondance est valide.
|
|
58
|
+
*/
|
|
59
|
+
verify(plain: string, hash: string): Promise<boolean>;
|
|
60
|
+
/**
|
|
61
|
+
* Recommande un re-hash si le hash n'est pas au format principal (migration),
|
|
62
|
+
* ou si le principal lui-même le juge obsolète (coûts augmentés).
|
|
63
|
+
*
|
|
64
|
+
* @param hash - hash stocké à inspecter.
|
|
65
|
+
* @returns `true` si un re-hash est recommandé au prochain login réussi.
|
|
66
|
+
*/
|
|
67
|
+
needsRehash(hash: string): boolean;
|
|
68
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { IPasswordEncoder } from "../../contracts/IPasswordEncoder.js";
|
|
2
|
+
/**
|
|
3
|
+
* Spécification déclarative d'UN encodeur de mot de passe — miroir structurel
|
|
4
|
+
* de la section `encoders` du schéma Zod de `@nodefony/security` (qui valide
|
|
5
|
+
* bornes et défauts), SANS en dépendre : ce module reste la couche basse,
|
|
6
|
+
* l'inversion de dépendance est préservée (security consomme user, jamais
|
|
7
|
+
* l'inverse).
|
|
8
|
+
*/
|
|
9
|
+
export interface IEncoderSpec {
|
|
10
|
+
/** Algorithme : `argon2id` (RFC 9106, défaut) ou `bcrypt` (legacy). */
|
|
11
|
+
type: "argon2id" | "bcrypt";
|
|
12
|
+
/** Argon2id : mémoire par hash (KiB). */
|
|
13
|
+
memoryKiB?: number;
|
|
14
|
+
/** Argon2id : passes d'itération. */
|
|
15
|
+
timeCost?: number;
|
|
16
|
+
/** Argon2id : lanes parallèles. */
|
|
17
|
+
parallelism?: number;
|
|
18
|
+
/** bcrypt : coût (ignoré par argon2id). */
|
|
19
|
+
rounds?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Traduit une liste ORDONNÉE de specs d'encodeurs en encodeur exécutable —
|
|
23
|
+
* le pont entre la section `encoders` de la config sécurité et le
|
|
24
|
+
* {@link UserService}.
|
|
25
|
+
*
|
|
26
|
+
* Sémantique de l'ordre : la **première** spec est l'encodeur PRINCIPAL
|
|
27
|
+
* (produit tous les nouveaux hashs), les suivantes sont les formats LEGACY
|
|
28
|
+
* acceptés en lecture seule — un login réussi sur un hash legacy est re-haché
|
|
29
|
+
* au format principal ({@link MigratingEncoder}, migration transparente).
|
|
30
|
+
*
|
|
31
|
+
* @param specs - specs ordonnées (typiquement `Object.values(config.encoders)`,
|
|
32
|
+
* l'ordre d'insertion du record fait foi).
|
|
33
|
+
* @returns l'encodeur seul (1 spec), un {@link MigratingEncoder} (N specs), ou
|
|
34
|
+
* l'Argon2id aux défauts OWASP si la liste est vide (défaut sûr).
|
|
35
|
+
*/
|
|
36
|
+
export declare function encoderFromConfig(specs: readonly IEncoderSpec[]): IPasswordEncoder;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { ISocialProvider } from "../contracts/IUser.js";
|
|
2
|
+
/**
|
|
3
|
+
* Type **logique** d'une colonne de l'utilisateur — ce que la donnée EST, jamais
|
|
4
|
+
* comment un moteur la range.
|
|
5
|
+
*
|
|
6
|
+
* Chaque adaptateur traduit ce vocabulaire dans le sien (`text`/`json`/`bool`
|
|
7
|
+
* du colKit SQL, `String`/`Array`/`Object` d'un schéma Mongoose). Le contrat
|
|
8
|
+
* reste donc lisible par un développeur qui n'a choisi aucune base, et un
|
|
9
|
+
* troisième adaptateur ne demande pas de le rouvrir.
|
|
10
|
+
*/
|
|
11
|
+
export type UserColumnType = "uuid" | "string" | "string[]" | "boolean" | "object" | "object[]" | "date";
|
|
12
|
+
/**
|
|
13
|
+
* D'où vient la valeur d'une colonne — ce qui décide si un adaptateur doit la
|
|
14
|
+
* DÉCLARER ou peut la laisser au moteur.
|
|
15
|
+
*
|
|
16
|
+
* - `identity` : la clé primaire. Déclarée en SQL (`text` + UUID applicatif),
|
|
17
|
+
* fournie par le moteur en document (`_id` + virtuel `id`).
|
|
18
|
+
* - `column` : une colonne ordinaire. **Tout adaptateur doit la déclarer** —
|
|
19
|
+
* c'est sur elle que porte le test de correspondance.
|
|
20
|
+
* - `audit` : un horodatage. Déclaré en SQL, délégué au moteur en document
|
|
21
|
+
* (`timestamps: true`).
|
|
22
|
+
*/
|
|
23
|
+
export type UserColumnOrigin = "identity" | "column" | "audit";
|
|
24
|
+
/**
|
|
25
|
+
* Une colonne du contrat utilisateur : son nom, ce qu'elle contient, et **qui la
|
|
26
|
+
* lit**.
|
|
27
|
+
*
|
|
28
|
+
* `readers` n'est pas de la documentation d'agrément : c'est ce qui permet à un
|
|
29
|
+
* refus de démarrage de nommer le lecteur en même temps que la colonne absente
|
|
30
|
+
* (« `socialProviders` manque — `findBySocialProvider` la lit »), au lieu de
|
|
31
|
+
* laisser chercher qui casse. N'y figurent donc que les lecteurs qui touchent la
|
|
32
|
+
* BASE — requête, filtre, tri, projection du dépôt — pas tout code qui manipule
|
|
33
|
+
* un utilisateur déjà chargé.
|
|
34
|
+
*/
|
|
35
|
+
export interface IUserColumn {
|
|
36
|
+
/** Nom de la colonne, identique sur tous les moteurs (figé en v1). */
|
|
37
|
+
readonly name: string;
|
|
38
|
+
/** Ce que la colonne contient, indépendamment du moteur. */
|
|
39
|
+
readonly type: UserColumnType;
|
|
40
|
+
/** `true` si la colonne accepte l'absence de valeur. */
|
|
41
|
+
readonly nullable: boolean;
|
|
42
|
+
/** `true` si la valeur doit être unique dans l'annuaire (implique un index). */
|
|
43
|
+
readonly unique?: boolean;
|
|
44
|
+
/** Qui fournit la valeur — cf {@link UserColumnOrigin}. */
|
|
45
|
+
readonly origin: UserColumnOrigin;
|
|
46
|
+
/**
|
|
47
|
+
* Fabrique du défaut, appelée à CHAQUE insertion.
|
|
48
|
+
*
|
|
49
|
+
* C'est une fabrique et non une valeur parce qu'un défaut structuré (`[]`,
|
|
50
|
+
* `{}`) partagé par référence serait le MÊME objet pour tous les utilisateurs :
|
|
51
|
+
* une modification en mémoire les contaminerait tous.
|
|
52
|
+
*/
|
|
53
|
+
readonly makeDefault?: () => unknown;
|
|
54
|
+
/**
|
|
55
|
+
* `true` si la valeur est régénérée à CHAQUE écriture (horodatage de
|
|
56
|
+
* modification). Déclaré ici plutôt que déduit du nom de la colonne : un
|
|
57
|
+
* adaptateur qui reconnaîtrait `"updatedAt"` rendrait la règle invisible, et
|
|
58
|
+
* fausse le jour où une seconde colonne se comporte pareil.
|
|
59
|
+
*/
|
|
60
|
+
readonly refreshedOnWrite?: boolean;
|
|
61
|
+
/** Les lecteurs qui cassent si la colonne manque — nommés dans les refus. */
|
|
62
|
+
readonly readers: readonly string[];
|
|
63
|
+
/** Rôle de la colonne, en une phrase. */
|
|
64
|
+
readonly description: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* **Le contrat de colonnes de l'utilisateur** — la seule description de ce que
|
|
68
|
+
* le framework LIT sur un utilisateur persisté.
|
|
69
|
+
*
|
|
70
|
+
* Il existe parce que cette liste était écrite en cinq endroits : la table SQL,
|
|
71
|
+
* le schéma document, la documentation publiée, et bientôt un générateur
|
|
72
|
+
* d'entité et un contrôle de démarrage. Cinq copies d'une même règle divergent
|
|
73
|
+
* en silence, et l'écart ne se voit alors que sur l'installation d'un tiers —
|
|
74
|
+
* même raison, même remède que {@link USER_SORTABLE_FIELDS} et `USER_FILTERS`.
|
|
75
|
+
*
|
|
76
|
+
* Les adaptateurs le DÉRIVENT (`userTable` en SQL, `userSchema` en document) au
|
|
77
|
+
* lieu de le recopier ; un test par adaptateur refuse toute colonne du contrat
|
|
78
|
+
* qui n'aurait pas de correspondance dans la définition produite.
|
|
79
|
+
*
|
|
80
|
+
* ⚠️ **Ces noms sont figés en v1.** Le SQL natif du listing les écrit en dur, et
|
|
81
|
+
* surtout : desserrer un nom est additif, le resserrer est une rupture. Ajouter
|
|
82
|
+
* une colonne ici engage donc toute application qui possède déjà sa table.
|
|
83
|
+
*/
|
|
84
|
+
/**
|
|
85
|
+
* Le nom de la TABLE (ou collection) qui porte les utilisateurs.
|
|
86
|
+
*
|
|
87
|
+
* Il fait partie du contrat au même titre que les noms de colonnes, et pour la
|
|
88
|
+
* même raison : le SQL natif du framework l'écrit en dur — recherche par compte
|
|
89
|
+
* externe, listing. Une divergence ne LÈVE PAS, elle rend des résultats vides,
|
|
90
|
+
* si bien qu'une connexion par fournisseur externe crée un compte de plus à
|
|
91
|
+
* chaque passage.
|
|
92
|
+
*
|
|
93
|
+
* Il vit ici plutôt que dans un adaptateur parce que trois lieux le lisent — la
|
|
94
|
+
* spec de table SQL, les requêtes natives, et le générateur d'entité, qui écrit
|
|
95
|
+
* l'entité DANS l'application. Trois copies divergent en silence ; le générateur
|
|
96
|
+
* en portait déjà une, et elle disait `users`.
|
|
97
|
+
*
|
|
98
|
+
* ⚠️ **Figé en v1**, comme les noms de colonnes : le changer romprait toute
|
|
99
|
+
* application qui possède déjà sa table.
|
|
100
|
+
*/
|
|
101
|
+
export declare const USER_TABLE_NAME = "User";
|
|
102
|
+
export declare const USER_COLUMNS: readonly IUserColumn[];
|
|
103
|
+
/**
|
|
104
|
+
* Forme plate d'un utilisateur tel qu'un dépôt le rend, avant reconstruction en
|
|
105
|
+
* `BaseUser` — pendant TypeScript de {@link USER_COLUMNS}.
|
|
106
|
+
*
|
|
107
|
+
* Déclarée ici, et non dans chaque adaptateur, pour la raison qui fonde tout ce
|
|
108
|
+
* fichier : deux copies de la même forme divergent sans que rien ne le dise.
|
|
109
|
+
* Les colonnes JSON arrivent déjà désérialisées, et les dates en `Date` — c'est
|
|
110
|
+
* le travail du pilote, pas celui du dépôt.
|
|
111
|
+
*/
|
|
112
|
+
export interface IUserRow {
|
|
113
|
+
id: string;
|
|
114
|
+
identifier: string;
|
|
115
|
+
password: string | null;
|
|
116
|
+
roles: string[];
|
|
117
|
+
enabled: boolean;
|
|
118
|
+
locked: boolean;
|
|
119
|
+
currentRole: string | null;
|
|
120
|
+
socialProviders: ISocialProvider[];
|
|
121
|
+
metadata: Record<string, unknown>;
|
|
122
|
+
createdAt: Date;
|
|
123
|
+
updatedAt: Date;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Reporte sur un utilisateur reconstruit toutes les colonnes que `BaseUser` ne
|
|
127
|
+
* porte pas — horodatages de la ligne, et **champs métier ajoutés par
|
|
128
|
+
* l'application**.
|
|
129
|
+
*
|
|
130
|
+
* Sans ce report, l'écriture passe et la lecture perd, **sans une erreur** : le
|
|
131
|
+
* développeur voit sa donnée en base et un objet vide dans son code. C'est
|
|
132
|
+
* l'asymétrie qui est le défaut, et elle ne se corrige qu'ici — les trois dépôts
|
|
133
|
+
* appellent cette fonction plutôt que d'en recopier chacun sa version, sinon
|
|
134
|
+
* l'un d'eux divergerait en silence (c'était déjà le cas : le dépôt SQL
|
|
135
|
+
* reportait les horodatages, le dépôt document non).
|
|
136
|
+
*
|
|
137
|
+
* ⚠️ Cette fonction est sur le chemin de CHAQUE requête portant une session
|
|
138
|
+
* authentifiée : elle n'alloue rien, ne construit aucun tableau intermédiaire,
|
|
139
|
+
* et ne parcourt que les clés propres de la ligne.
|
|
140
|
+
*
|
|
141
|
+
* @param user - l'utilisateur reconstruit, muté sur place.
|
|
142
|
+
* @param row - la ligne rendue par le moteur.
|
|
143
|
+
* @param skip - clés de plomberie propres au moteur (`_id`, `__v`…), à ne pas
|
|
144
|
+
* reporter. Le contrat ne peut pas les connaître : c'est au dépôt de les dire.
|
|
145
|
+
* @returns le même objet `user`, pour permettre `return attachExtraColumns(...)`.
|
|
146
|
+
*/
|
|
147
|
+
export declare function attachExtraColumns<T extends object>(user: T, row: Readonly<Record<string, unknown>>, skip?: ReadonlySet<string>): T;
|
|
148
|
+
/**
|
|
149
|
+
* Les colonnes du contrat qu'une définition d'entité ne rend PAS.
|
|
150
|
+
*
|
|
151
|
+
* @param present - noms des colonnes que l'entité expose réellement.
|
|
152
|
+
* @param expected - le sous-ensemble du contrat à exiger (défaut : tout).
|
|
153
|
+
* @returns les colonnes manquantes, dans l'ordre du contrat (vide si complet).
|
|
154
|
+
*/
|
|
155
|
+
export declare function missingUserColumns(present: Iterable<string>, expected?: readonly IUserColumn[]): readonly IUserColumn[];
|
|
156
|
+
/**
|
|
157
|
+
* Refuse une entité utilisateur incomplète, en nommant chaque colonne absente
|
|
158
|
+
* ET ce qui la lit.
|
|
159
|
+
*
|
|
160
|
+
* Le refus existe parce que le manque est SILENCIEUX : une application qui
|
|
161
|
+
* possède sa table `User` peut en retirer une colonne, la migration s'applique,
|
|
162
|
+
* le démarrage réussit, et la commande qui liste les comptes affiche des
|
|
163
|
+
* utilisateurs. Le défaut n'éclate qu'au premier accès à la colonne absente —
|
|
164
|
+
* une authentification, un filtre par rôle — peut-être des semaines plus tard,
|
|
165
|
+
* dans un chemin peu fréquenté. Échouer au démarrage déplace la découverte au
|
|
166
|
+
* seul moment où elle ne coûte rien.
|
|
167
|
+
*
|
|
168
|
+
* Nommer le LECTEUR, et pas seulement la colonne, est ce qui rend le message
|
|
169
|
+
* actionnable : « `roles` manque » n'apprend rien à qui a retiré la colonne
|
|
170
|
+
* exprès ; « `roles`, lue par le filtre `?role=` et par le compte des
|
|
171
|
+
* administrateurs actifs » dit ce qui cassera.
|
|
172
|
+
*
|
|
173
|
+
* @param present - noms des colonnes que l'entité expose réellement.
|
|
174
|
+
* @param origin - d'où vient l'entité examinée, cité tel quel dans le refus
|
|
175
|
+
* (ex. `l'entité « User » de l'application, sur le connecteur « default »`).
|
|
176
|
+
* @param expected - le sous-ensemble du contrat à exiger. Il existe parce que
|
|
177
|
+
* tous les stockages ne portent pas les mêmes colonnes EN PROPRE : un schéma
|
|
178
|
+
* document laisse la clé au moteur (`_id` + virtuel) et les horodatages à son
|
|
179
|
+
* option `timestamps`. Exiger le contrat entier là-bas refuserait une entité
|
|
180
|
+
* parfaitement correcte — et un refus faux apprend à passer outre les refus.
|
|
181
|
+
* L'appelant le DÉRIVE de ce qu'il produit lui-même, il ne le recopie pas.
|
|
182
|
+
* @throws BootConfigurationError si une colonne attendue manque.
|
|
183
|
+
*
|
|
184
|
+
* **Pourquoi une `BootConfigurationError` et pas une `Error`.** Ce refus est
|
|
185
|
+
* appelé pendant le boot d'un module, et le kernel y applique une politique de
|
|
186
|
+
* résilience : un hook de boot qui lève est journalisé en WARNING, le module est
|
|
187
|
+
* écarté, et l'application DÉMARRE — « BOOT dégradé », code de sortie nul.
|
|
188
|
+
* Mesuré : une application générée dont l'entité `User` avait perdu six colonnes
|
|
189
|
+
* du contrat démarrait, servait ses routes, et `orm:migrate:status` la déclarait
|
|
190
|
+
* « à jour ». Le refus était écrit, parfaitement rédigé, et sans effet.
|
|
191
|
+
*
|
|
192
|
+
* Le fail-soft est le bon comportement pour une panne TRANSITOIRE (une base qui
|
|
193
|
+
* ne répond pas encore) ; il est le mauvais pour une erreur de PROGRAMMATION,
|
|
194
|
+
* qui ne se répare pas en continuant. `BootConfigurationError` est exactement la
|
|
195
|
+
* frontière que le cœur a posée entre les deux — son propre TSDoc nomme le cas
|
|
196
|
+
* « une entité non portée sur le dialecte demandé ».
|
|
197
|
+
*/
|
|
198
|
+
export declare function assertUserContract(present: Iterable<string>, origin: string, expected?: readonly IUserColumn[]): void;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { FacetCount, FacetCounts } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* **Le vocabulaire de filtre des utilisateurs**, en noms PUBLICS — ceux qu'un
|
|
4
|
+
* administrateur écrit dans l'URL (`?role=ROLE_ADMIN&enabled=false`).
|
|
5
|
+
*
|
|
6
|
+
* Frère de `USER_SORTABLE_FIELDS` (`userSort.ts`), et posé au même endroit pour
|
|
7
|
+
* la même raison : le vocabulaire appartient au module propriétaire du contrat,
|
|
8
|
+
* la mécanique de lecture au cœur (`parseFilters`).
|
|
9
|
+
*
|
|
10
|
+
* `role` reste une chaîne LIBRE : la hiérarchie de rôles est celle de
|
|
11
|
+
* l'application (`ROLE_*` de tenant) autant que de la plateforme
|
|
12
|
+
* (`ROLE_NODEFONY_*`), et aucune allowlist du framework ne peut la connaître.
|
|
13
|
+
* `enabled` est un booléen strict — `?enabled=1` est désormais refusé plutôt que
|
|
14
|
+
* lu comme `false`, ce que faisait la comparaison `enabled === "true"`.
|
|
15
|
+
*/
|
|
16
|
+
export declare const USER_FILTERS: {
|
|
17
|
+
/** Rôle plat devant figurer dans `roles` (containment natif). */
|
|
18
|
+
readonly role: "string";
|
|
19
|
+
/** `true` = actifs seulement, `false` = inactifs seulement, absent = les deux. */
|
|
20
|
+
readonly enabled: "boolean";
|
|
21
|
+
/** `true` = verrouillés seulement (défense anti-force brute), absent = les deux. */
|
|
22
|
+
readonly locked: "boolean";
|
|
23
|
+
/** `true` = liés à au moins un fournisseur externe (OAuth), absent = les deux. */
|
|
24
|
+
readonly hasSocial: "boolean";
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* **Les facettes des utilisateurs** — les questions fermées posées à l'annuaire
|
|
28
|
+
* ENTIER pour les cartes de tête.
|
|
29
|
+
*
|
|
30
|
+
* Les populations se **recoupent** délibérément : un compte peut être désactivé
|
|
31
|
+
* ET verrouillé, un administrateur peut avoir un lien social. Aucune facette
|
|
32
|
+
* n'est donc déduite d'une autre — « désactivés » et « verrouillés » sont deux
|
|
33
|
+
* questions distinctes, et les confondre en un seul « inactifs » masquait
|
|
34
|
+
* lequel des deux mécanismes bloque réellement un compte.
|
|
35
|
+
*
|
|
36
|
+
* `admins` n'y figure pas : le rôle d'administration est une valeur de
|
|
37
|
+
* configuration (`ROLE_NODEFONY_ADMIN` par défaut, surchargeable), pas une
|
|
38
|
+
* constante du vocabulaire. Le service la lit et compose la facette lui-même.
|
|
39
|
+
*/
|
|
40
|
+
export declare const USER_FACETS: {
|
|
41
|
+
/** Tous les comptes de l'annuaire. */
|
|
42
|
+
readonly total: {};
|
|
43
|
+
/** Comptes utilisables : activés et non verrouillés. */
|
|
44
|
+
readonly active: {
|
|
45
|
+
readonly enabled: true;
|
|
46
|
+
readonly locked: false;
|
|
47
|
+
};
|
|
48
|
+
/** Comptes désactivés par décision d'administration. */
|
|
49
|
+
readonly disabled: {
|
|
50
|
+
readonly enabled: false;
|
|
51
|
+
};
|
|
52
|
+
/** Comptes verrouillés par la défense anti-force brute. */
|
|
53
|
+
readonly locked: {
|
|
54
|
+
readonly locked: true;
|
|
55
|
+
};
|
|
56
|
+
/** Comptes liés à au moins un fournisseur d'identité externe. */
|
|
57
|
+
readonly social: {
|
|
58
|
+
readonly hasSocial: true;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Les compteurs rendus par `GET /nodefony/user/api/users/stats` — les facettes
|
|
63
|
+
* déclarées, plus `admins` que le service compose depuis le rôle configuré.
|
|
64
|
+
*/
|
|
65
|
+
export type IUserCounts = FacetCounts<typeof USER_FACETS> & {
|
|
66
|
+
/** Comptes portant le rôle d'administration de la plateforme. */
|
|
67
|
+
admins: FacetCount;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Ce que l'endpoint de COMPTEURS accepte de filtrer — `USER_FILTERS` **moins**
|
|
71
|
+
* les champs que les facettes décomposent (`enabled`, `locked`, `hasSocial`).
|
|
72
|
+
*
|
|
73
|
+
* Les demander ici rendrait une réponse contradictoire : le total suivrait le
|
|
74
|
+
* filtre pendant que chaque facette l'écraserait par le sien. `role` reste :
|
|
75
|
+
* il découpe une AUTRE dimension, et « combien de `ROLE_SUPPORT`, et dans quel
|
|
76
|
+
* état ? » est une question cohérente.
|
|
77
|
+
*/
|
|
78
|
+
export declare const USER_STATS_FILTERS: {
|
|
79
|
+
readonly role: "string";
|
|
80
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { IUserProfile } from "../contracts/IUserProfile.js";
|
|
2
|
+
/**
|
|
3
|
+
* Logique PURE du profil utilisateur (claims OIDC stockés dans
|
|
4
|
+
* `metadata.profile`) — validation d'un patch client, projection en DTO par
|
|
5
|
+
* allowlist, fusion dans la `metadata` existante. Aucun I/O, aucun service :
|
|
6
|
+
* testable isolément (cœur de la garantie anti-fuite + anti-injection de clé).
|
|
7
|
+
*/
|
|
8
|
+
/** Clés de profil reconnues — allowlist STRICTE (toute autre clé est ignorée). */
|
|
9
|
+
export declare const PROFILE_FIELDS: readonly ["givenName", "familyName", "displayName", "email", "locale", "picture"];
|
|
10
|
+
export type ProfileField = (typeof PROFILE_FIELDS)[number];
|
|
11
|
+
/**
|
|
12
|
+
* Valide + normalise un patch de profil reçu d'un client. Allowlist stricte :
|
|
13
|
+
* seules les clés {@link PROFILE_FIELDS} sont lues (anti-injection d'une clé
|
|
14
|
+
* `metadata` arbitraire). Chaque valeur est `trim`ée ; une chaîne vide ou `null`
|
|
15
|
+
* = **effacement** du champ (matérialisé `""`, retiré au merge).
|
|
16
|
+
*
|
|
17
|
+
* @param input - corps client (`request.body.profile` admin, ou `request.body`
|
|
18
|
+
* self).
|
|
19
|
+
* @returns `{ ok: true, value }` (patch normalisé) ou `{ ok: false, error }` si
|
|
20
|
+
* une valeur est mal typée, trop longue, ou syntaxiquement invalide.
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateProfilePatch(input: unknown): {
|
|
23
|
+
ok: true;
|
|
24
|
+
value: Partial<Record<ProfileField, string>>;
|
|
25
|
+
} | {
|
|
26
|
+
ok: false;
|
|
27
|
+
error: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Projette la `metadata` brute d'une entité vers un {@link IUserProfile} — lecture
|
|
31
|
+
* par **allowlist** (seules les clés connues, typées string non vide). Garantit
|
|
32
|
+
* qu'aucune autre clé de `metadata` (applicative/sensible) n'entre dans le DTO.
|
|
33
|
+
*
|
|
34
|
+
* @param metadata - `entity.metadata` (inconnu : l'entité ORM, pas le contrat).
|
|
35
|
+
*/
|
|
36
|
+
export declare function projectProfile(metadata: unknown): IUserProfile;
|
|
37
|
+
/**
|
|
38
|
+
* Fusionne un patch validé dans la `metadata` existante en **préservant les autres
|
|
39
|
+
* clés**. Les champs de profil vidés (`""`) sont RETIRÉS (pas de clé fantôme).
|
|
40
|
+
* Rend la nouvelle `metadata` complète, prête pour `updateOne({ metadata })`.
|
|
41
|
+
*
|
|
42
|
+
* @param metadata - `metadata` actuelle de l'entité (préservée hors `profile`).
|
|
43
|
+
* @param patch - patch validé par {@link validateProfilePatch}.
|
|
44
|
+
*/
|
|
45
|
+
export declare function mergeProfileIntoMetadata(metadata: unknown, patch: Partial<Record<ProfileField, string>>): Record<string, unknown>;
|
|
46
|
+
/**
|
|
47
|
+
* Pré-remplit un profil depuis des **claims OIDC/OAuth** (provisioning JIT d'un
|
|
48
|
+
* Shadow User social). Mappe les claims standard (`given_name`/`family_name`/
|
|
49
|
+
* `name`/`email`/`picture`/`locale`, + `avatar_url` GitHub) vers
|
|
50
|
+
* {@link IUserProfile}, **best-effort par champ** : chaque valeur est validée
|
|
51
|
+
* isolément (bornes/format) et un claim invalide est ignoré sans bloquer les
|
|
52
|
+
* autres ni le login. Les claims viennent d'un tiers → lecture défensive.
|
|
53
|
+
*
|
|
54
|
+
* @param claims - claims/payload du fournisseur (ex. `{...raw, name, email}`).
|
|
55
|
+
*/
|
|
56
|
+
export declare function profileFromClaims(claims: Record<string, unknown>): Partial<Record<ProfileField, string>>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { IPageQuery } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* **Le vocabulaire de tri des utilisateurs** — source unique, en noms publics.
|
|
4
|
+
*
|
|
5
|
+
* Il existe parce que cette liste était écrite DEUX fois, et qu'elle avait déjà
|
|
6
|
+
* divergé : l'adapter SQL autorisait `id`, l'adapter Mongo non. Un `?order=id`
|
|
7
|
+
* triait donc en base SQL et se faisait ignorer en base Mongo, sans erreur ni
|
|
8
|
+
* trace — le genre d'écart qui ne se voit qu'en production, sur l'installation
|
|
9
|
+
* d'un tiers.
|
|
10
|
+
*
|
|
11
|
+
* Chaque store la consomme au lieu de la recopier ; ceux qui **concatènent** le
|
|
12
|
+
* nom dans une requête (SQL) continuent de filtrer avec, en défense en
|
|
13
|
+
* profondeur — mais ils filtrent alors sur la même liste que tout le monde.
|
|
14
|
+
*/
|
|
15
|
+
export declare const USER_SORTABLE_FIELDS: readonly ["identifier", "enabled", "createdAt", "updatedAt", "id"];
|
|
16
|
+
/**
|
|
17
|
+
* Sous-ensemble réellement triable par l'annuaire **en mémoire**.
|
|
18
|
+
*
|
|
19
|
+
* `BaseUser` ne porte ni `createdAt` ni `updatedAt` — ce sont des colonnes des
|
|
20
|
+
* schémas persistants, pas des attributs du modèle. Déclarer le vocabulaire
|
|
21
|
+
* complet ici reviendrait à annoncer un tri que le store ne peut pas rendre :
|
|
22
|
+
* la page sortirait dans un ordre arbitraire, sans erreur, et personne ne le
|
|
23
|
+
* verrait. La capacité réduite est donc ANNONCÉE, pas simulée — même doctrine
|
|
24
|
+
* que le store de sessions Redis, qui ne déclare aucun tri du tout.
|
|
25
|
+
*
|
|
26
|
+
* Ce que cela donne à l'usage : `?order=createdAt` trie sur une base SQL ou
|
|
27
|
+
* Mongo, et rend **400** sur le backend mémoire. Un refus explicite vaut mieux
|
|
28
|
+
* qu'un ordre inventé.
|
|
29
|
+
*/
|
|
30
|
+
export declare const USER_SORTABLE_FIELDS_IN_MEMORY: readonly ["identifier", "enabled", "id"];
|
|
31
|
+
/**
|
|
32
|
+
* Socle garanti par **tous** les backends d'utilisateurs. C'est ce sur quoi une
|
|
33
|
+
* interface peut compter sans savoir quelle base est branchée.
|
|
34
|
+
*/
|
|
35
|
+
export declare const USER_SORTABLE_FIELDS_COMMON: readonly ["identifier", "enabled", "id"];
|
|
36
|
+
/**
|
|
37
|
+
* Ordre appliqué quand le client n'en demande aucun : par identifiant croissant
|
|
38
|
+
* — le seul champ toujours présent et unique, donc le seul qui rende une
|
|
39
|
+
* pagination offset déterministe sans départage supplémentaire.
|
|
40
|
+
*/
|
|
41
|
+
export declare const USER_DEFAULT_ORDER: NonNullable<IPageQuery["order"]>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Déclare un backend de persistance utilisateur disponible (idempotent).
|
|
3
|
+
*
|
|
4
|
+
* @param name - nom court du backend (`"memory"`, `"drizzle"`, `"mongoose"`).
|
|
5
|
+
*/
|
|
6
|
+
export declare function registerUserStore(name: string): void;
|
|
7
|
+
/**
|
|
8
|
+
* Liste des backends utilisateur disponibles — alimente le champ `available` du
|
|
9
|
+
* statut user (écran Studio « Stores »). `"memory"` est mis EN TÊTE (baseline
|
|
10
|
+
* builtin, toujours disponible = le repli qui marche partout), les adapters ORM
|
|
11
|
+
* suivent triés. L'ordre est celui affiché dans la colonne « Backends dispo ».
|
|
12
|
+
*
|
|
13
|
+
* @returns noms des backends enregistrés (vide si aucun — jamais `null`).
|
|
14
|
+
*/
|
|
15
|
+
export declare function listUserStores(): string[];
|