@kourouma/express-authkit 0.0.0-stage → 0.1.0-rc.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +21 -0
  3. package/README.md +218 -2
  4. package/SECURITY.md +30 -0
  5. package/bin/express-authkit.js +30 -0
  6. package/package.json +93 -4
  7. package/src/adapters/express/bearer-token.js +35 -0
  8. package/src/adapters/express/http-errors.js +89 -0
  9. package/src/adapters/express/index.js +405 -0
  10. package/src/config/assertions.js +80 -0
  11. package/src/config/auth-kit-options.js +61 -0
  12. package/src/config/defaults.js +66 -0
  13. package/src/config/jwt-options.js +86 -0
  14. package/src/config/password-options.js +38 -0
  15. package/src/config/refresh-token-options.js +49 -0
  16. package/src/config/secret-strength.js +112 -0
  17. package/src/core/authentication-service.js +278 -0
  18. package/src/core/authorization.js +156 -0
  19. package/src/core/contracts/assert-contract.js +26 -0
  20. package/src/core/contracts/auth-principal.js +36 -0
  21. package/src/core/contracts/authentication-service.js +44 -0
  22. package/src/core/contracts/password-provider.js +27 -0
  23. package/src/core/contracts/refresh-token-store.js +80 -0
  24. package/src/core/contracts/token-provider.js +45 -0
  25. package/src/core/contracts/user-repository.js +157 -0
  26. package/src/core/errors.js +258 -0
  27. package/src/core/input-limits.js +26 -0
  28. package/src/core/messages.js +239 -0
  29. package/src/core/refresh-token-service.js +149 -0
  30. package/src/factory/create-auth-kit.js +90 -0
  31. package/src/index.js +61 -0
  32. package/src/providers/argon2-password-provider.js +68 -0
  33. package/src/providers/in-memory-refresh-token-store.js +51 -0
  34. package/src/providers/jwt-token-provider.js +112 -0
  35. package/types/adapters/express/bearer-token.d.ts +11 -0
  36. package/types/adapters/express/http-errors.d.ts +30 -0
  37. package/types/adapters/express/index.d.ts +57 -0
  38. package/types/config/assertions.d.ts +40 -0
  39. package/types/config/auth-kit-options.d.ts +41 -0
  40. package/types/config/defaults.d.ts +55 -0
  41. package/types/config/jwt-options.d.ts +42 -0
  42. package/types/config/password-options.d.ts +23 -0
  43. package/types/config/refresh-token-options.d.ts +21 -0
  44. package/types/config/secret-strength.d.ts +7 -0
  45. package/types/core/authentication-service.d.ts +37 -0
  46. package/types/core/authorization.d.ts +40 -0
  47. package/types/core/contracts/assert-contract.d.ts +13 -0
  48. package/types/core/contracts/auth-principal.d.ts +38 -0
  49. package/types/core/contracts/authentication-service.d.ts +102 -0
  50. package/types/core/contracts/password-provider.d.ts +32 -0
  51. package/types/core/contracts/refresh-token-store.d.ts +105 -0
  52. package/types/core/contracts/token-provider.d.ts +79 -0
  53. package/types/core/contracts/user-repository.d.ts +112 -0
  54. package/types/core/errors.d.ts +213 -0
  55. package/types/core/input-limits.d.ts +20 -0
  56. package/types/core/messages.d.ts +141 -0
  57. package/types/core/refresh-token-service.d.ts +31 -0
  58. package/types/factory/create-auth-kit.d.ts +52 -0
  59. package/types/index.d.ts +60 -0
  60. package/types/providers/argon2-password-provider.d.ts +13 -0
  61. package/types/providers/in-memory-refresh-token-store.d.ts +11 -0
  62. package/types/providers/jwt-token-provider.d.ts +13 -0
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Identité validée d’un utilisateur authentifié.
3
+ *
4
+ * Le principal ne contient que des données non sensibles issues d’un jeton
5
+ * vérifié. Il ne contient jamais le hash du mot de passe ni le jeton brut.
6
+ * Il est figé pour empêcher les middlewares suivants de le modifier.
7
+ *
8
+ * Les rôles et permissions viennent du repository, relu à chaque requête :
9
+ * ils ne sont jamais écrits dans le jeton, et un changement de rôle
10
+ * s’applique immédiatement.
11
+ *
12
+ * @typedef {Readonly<{
13
+ * userId: string,
14
+ * tokenId: string,
15
+ * issuedAt: number,
16
+ * expiresAt: number,
17
+ * roles: readonly string[],
18
+ * permissions: readonly string[]
19
+ * }>} AuthPrincipal
20
+ */
21
+
22
+ /**
23
+ * @param {import("./token-provider.js").VerifiedTokenClaims | import("./token-provider.js").IssuedToken} claims
24
+ * @param {{ id: string, roles: readonly string[], permissions: readonly string[] }} user
25
+ * @returns {AuthPrincipal}
26
+ */
27
+ export function createAuthPrincipal(claims, user) {
28
+ return Object.freeze({
29
+ userId: user.id,
30
+ tokenId: claims.tokenId,
31
+ issuedAt: claims.issuedAt,
32
+ expiresAt: claims.expiresAt,
33
+ roles: user.roles,
34
+ permissions: user.permissions
35
+ });
36
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Identifiants transmis par un client.
3
+ *
4
+ * @typedef {object} LoginCredentials
5
+ * @property {string} identifier Identifiant de connexion (e-mail, nom d’utilisateur…).
6
+ * @property {string} password Mot de passe en clair. Il n’est ni journalisé ni stocké.
7
+ */
8
+
9
+ /**
10
+ * Résultat d’une connexion réussie, envoyable tel quel dans une réponse HTTP.
11
+ *
12
+ * @typedef {object} LoginResult
13
+ * @property {string} accessToken Jeton d’accès JWT signé.
14
+ * @property {"Bearer"} tokenType Toujours `"Bearer"`.
15
+ * @property {number} expiresIn Durée de vie du jeton, en secondes.
16
+ * @property {import("./auth-principal.js").AuthPrincipal} principal Identité authentifiée.
17
+ * @property {string} [refreshToken] Jeton de rafraîchissement (présent si `refreshTokens` est activé).
18
+ * @property {number} [refreshExpiresIn] Durée de vie du jeton de rafraîchissement, en secondes.
19
+ */
20
+
21
+ /**
22
+ * Forme acceptée pour un identifiant d’utilisateur : texte, entier, BigInt ou ObjectId MongoDB.
23
+ *
24
+ * @typedef {string | number | bigint | { toHexString(): string }} UserIdInput
25
+ */
26
+
27
+ /**
28
+ * Cas d’usage d’authentification indépendants du framework.
29
+ *
30
+ * @typedef {object} AuthenticationService
31
+ * @property {(credentials: LoginCredentials) => Promise<LoginResult>} login
32
+ * Authentifie un utilisateur. Renvoie `InvalidCredentialsError` pour un
33
+ * utilisateur inconnu, un mauvais mot de passe ou un compte désactivé.
34
+ * @property {(refreshToken: string) => Promise<LoginResult>} refresh
35
+ * Échange un jeton de rafraîchissement contre une nouvelle paire de jetons (rotation).
36
+ * @property {(refreshToken: string) => Promise<void>} logout
37
+ * Révoque la session liée au jeton de rafraîchissement.
38
+ * @property {(userId: UserIdInput) => Promise<void>} revokeAllSessions
39
+ * Révoque toutes les sessions d’un utilisateur.
40
+ * @property {(token: string) => Promise<import("./auth-principal.js").AuthPrincipal>} verifyAccessToken
41
+ * Vérifie un jeton et contrôle que son utilisateur existe et est toujours actif.
42
+ */
43
+
44
+ export {};
@@ -0,0 +1,27 @@
1
+ import { assertImplements } from "./assert-contract.js";
2
+
3
+ /**
4
+ * Contrat de hachage des mots de passe.
5
+ *
6
+ * Les implémentations doivent employer un algorithme éprouvé (le fournisseur
7
+ * par défaut utilise Argon2id) et ne jamais programmer la cryptographie elles-mêmes.
8
+ *
9
+ * @typedef {object} PasswordProvider
10
+ * @property {(password: string) => Promise<string>} hash
11
+ * Renvoie un hash autonome (algorithme, paramètres et sel encodés dans la chaîne).
12
+ * @property {(password: string, hash: string) => Promise<boolean>} verify
13
+ * Renvoie `true` si le mot de passe correspond. La comparaison doit être en
14
+ * temps constant et lever `InvalidPasswordHashError` si `hash` est illisible.
15
+ */
16
+
17
+ /** @type {readonly string[]} */
18
+ export const PASSWORD_PROVIDER_METHODS = Object.freeze(["hash", "verify"]);
19
+
20
+ /**
21
+ * @param {unknown} provider
22
+ * @returns {asserts provider is PasswordProvider}
23
+ * @throws {import("../errors.js").ContractViolationError}
24
+ */
25
+ export function assertPasswordProvider(provider) {
26
+ assertImplements(provider, "PasswordProvider", PASSWORD_PROVIDER_METHODS);
27
+ }
@@ -0,0 +1,80 @@
1
+ import { ContractViolationError } from "../errors.js";
2
+ import { DEVELOPER_MESSAGES } from "../messages.js";
3
+ import { assertImplements } from "./assert-contract.js";
4
+
5
+ /**
6
+ * Jeton de rafraîchissement tel qu’il est enregistré. Le jeton lui-même n’est
7
+ * jamais stocké : seule son empreinte SHA-256 l’est. Une fuite de la base ne
8
+ * donne donc aucun jeton utilisable.
9
+ *
10
+ * @typedef {object} RefreshTokenRecord
11
+ * @property {string} tokenHash Empreinte SHA-256 du jeton (64 caractères hexadécimaux). Clé unique.
12
+ * @property {string} userId Identifiant de l’utilisateur.
13
+ * @property {string} familyId Identifiant de la « famille » : tous les jetons issus d’une même connexion.
14
+ * @property {number} expiresAt Date d’expiration, en secondes depuis l’époque Unix.
15
+ */
16
+
17
+ /**
18
+ * Résultat de {@link RefreshTokenStore.consume}.
19
+ *
20
+ * @typedef {object} ConsumedRefreshToken
21
+ * @property {string} userId
22
+ * @property {string} familyId
23
+ * @property {number} expiresAt Secondes depuis l’époque Unix.
24
+ * @property {boolean} alreadyUsed `true` si le jeton avait déjà été consommé avant cet appel.
25
+ */
26
+
27
+ /**
28
+ * Stockage des jetons de rafraîchissement, implémenté par l’application.
29
+ *
30
+ * @typedef {object} RefreshTokenStore
31
+ * @property {(record: RefreshTokenRecord) => Promise<void>} save
32
+ * Enregistre un nouveau jeton.
33
+ * @property {(tokenHash: string) => Promise<ConsumedRefreshToken | null | undefined>} consume
34
+ * Marque le jeton comme utilisé et renvoie son état précédent, ou `null` s’il
35
+ * est inconnu. Doit être **atomique** : si deux requêtes consomment le même
36
+ * jeton en même temps, une seule doit obtenir `alreadyUsed: false`.
37
+ * Les jetons utilisés doivent être conservés jusqu’à leur expiration, pour
38
+ * détecter une réutilisation.
39
+ * @property {(familyId: string) => Promise<void>} revokeFamily
40
+ * Supprime (ou invalide) tous les jetons d’une famille.
41
+ * @property {(userId: string) => Promise<void>} revokeUser
42
+ * Supprime (ou invalide) tous les jetons d’un utilisateur.
43
+ */
44
+
45
+ /** @type {readonly string[]} */
46
+ export const REFRESH_TOKEN_STORE_METHODS = Object.freeze(["save", "consume", "revokeFamily", "revokeUser"]);
47
+
48
+ /**
49
+ * @param {unknown} store
50
+ * @returns {asserts store is RefreshTokenStore}
51
+ * @throws {ContractViolationError}
52
+ */
53
+ export function assertRefreshTokenStore(store) {
54
+ assertImplements(store, "RefreshTokenStore", REFRESH_TOKEN_STORE_METHODS);
55
+ }
56
+
57
+ /**
58
+ * Valide à l’exécution la valeur renvoyée par `consume()`.
59
+ *
60
+ * @param {unknown} result
61
+ * @returns {ConsumedRefreshToken | null}
62
+ * @throws {ContractViolationError}
63
+ */
64
+ export function normalizeConsumedRefreshToken(result) {
65
+ if (result === null || result === undefined) {
66
+ return null;
67
+ }
68
+ const { userId, familyId, expiresAt, alreadyUsed } = /** @type {Record<string, unknown>} */ (result);
69
+ const isValid =
70
+ typeof userId === "string" &&
71
+ userId.length > 0 &&
72
+ typeof familyId === "string" &&
73
+ familyId.length > 0 &&
74
+ Number.isFinite(expiresAt) &&
75
+ typeof alreadyUsed === "boolean";
76
+ if (!isValid) {
77
+ throw new ContractViolationError(DEVELOPER_MESSAGES.refreshStoreConsumeInvalid);
78
+ }
79
+ return { userId, familyId, expiresAt: /** @type {number} */ (expiresAt), alreadyUsed };
80
+ }
@@ -0,0 +1,45 @@
1
+ import { assertImplements } from "./assert-contract.js";
2
+
3
+ /**
4
+ * Résultat de l’émission d’un jeton d’accès.
5
+ *
6
+ * @typedef {object} IssuedToken
7
+ * @property {string} token Jeton sérialisé.
8
+ * @property {string} tokenId Identifiant unique du jeton (`jti`).
9
+ * @property {number} issuedAt Date d’émission, en secondes depuis l’époque Unix.
10
+ * @property {number} expiresAt Date d’expiration, en secondes depuis l’époque Unix.
11
+ */
12
+
13
+ /**
14
+ * Revendications extraites d’un jeton d’accès vérifié.
15
+ *
16
+ * @typedef {object} VerifiedTokenClaims
17
+ * @property {string} subject Identifiant utilisateur (`sub`).
18
+ * @property {string} tokenId Identifiant du jeton (`jti`).
19
+ * @property {number} issuedAt `iat`, en secondes depuis l’époque Unix.
20
+ * @property {number} expiresAt `exp`, en secondes depuis l’époque Unix.
21
+ */
22
+
23
+ /**
24
+ * Contrat de gestion des jetons d’accès.
25
+ *
26
+ * `verify()` doit contrôler la signature, les algorithmes autorisés, l’expiration,
27
+ * l’émetteur, l’audience et le sujet. Elle lève `TokenExpiredError` pour un jeton
28
+ * expiré et `InvalidTokenError` pour tout autre rejet.
29
+ *
30
+ * @typedef {object} TokenProvider
31
+ * @property {(input: { subject: string }) => Promise<IssuedToken>} issue
32
+ * @property {(token: string) => Promise<VerifiedTokenClaims>} verify
33
+ */
34
+
35
+ /** @type {readonly string[]} */
36
+ export const TOKEN_PROVIDER_METHODS = Object.freeze(["issue", "verify"]);
37
+
38
+ /**
39
+ * @param {unknown} provider
40
+ * @returns {asserts provider is TokenProvider}
41
+ * @throws {import("../errors.js").ContractViolationError}
42
+ */
43
+ export function assertTokenProvider(provider) {
44
+ assertImplements(provider, "TokenProvider", TOKEN_PROVIDER_METHODS);
45
+ }
@@ -0,0 +1,157 @@
1
+ import { ContractViolationError } from "../errors.js";
2
+ import { MAX_AUTHORITIES, MAX_AUTHORITY_LENGTH } from "../input-limits.js";
3
+ import { DEVELOPER_MESSAGES, repositoryRecordAuthorities } from "../messages.js";
4
+ import { assertImplements } from "./assert-contract.js";
5
+
6
+ /**
7
+ * Données utilisateur minimales attendues par le moteur. Le dépôt transforme
8
+ * son modèle (ligne SQL, document Mongo, réponse d’API…) dans ce format.
9
+ *
10
+ * Les propriétés supplémentaires sont autorisées et ignorées. Elles ne sont
11
+ * jamais copiées dans les jetons ni dans le principal authentifié.
12
+ *
13
+ * @typedef {object} AuthUserRecord
14
+ * @property {string | number | bigint | { toHexString(): string }} id Identifiant stable, unique et non secret (chaîne, entier ou ObjectId MongoDB) ; converti en chaîne, il devient la revendication JWT `sub`.
15
+ * @property {string} passwordHash Hash Argon2id produit par `auth.password.hash()`.
16
+ * @property {boolean} isActive Doit valoir exactement `true` pour autoriser la connexion.
17
+ * @property {string[]} [roles] Rôles de l’utilisateur (par exemple `["admin"]`). Facultatif.
18
+ * @property {string[]} [permissions] Permissions de l’utilisateur (par exemple `["orders:write"]`). Facultatif.
19
+ */
20
+
21
+ /**
22
+ * Enregistrement validé et normalisé, utilisé par le moteur.
23
+ *
24
+ * @typedef {{
25
+ * id: string,
26
+ * passwordHash: string,
27
+ * isActive: boolean,
28
+ * roles: readonly string[],
29
+ * permissions: readonly string[]
30
+ * }} NormalizedUserRecord
31
+ */
32
+
33
+ /**
34
+ * Contrat de persistance implémenté par l’application.
35
+ *
36
+ * Les deux méthodes doivent renvoyer `null` (ou `undefined`) lorsqu’aucun
37
+ * utilisateur ne correspond. Elles ne doivent pas lever d’erreur dans ce cas ;
38
+ * les erreurs sont réservées aux problèmes d’infrastructure.
39
+ *
40
+ * @typedef {object} UserRepository
41
+ * @property {(identifier: string) => Promise<AuthUserRecord | null | undefined>} findByIdentifier
42
+ * Cherche un utilisateur par identifiant de connexion (e-mail, nom…). La
43
+ * normalisation (minuscules, espaces) relève de la responsabilité du dépôt.
44
+ * @property {(id: string) => Promise<AuthUserRecord | null | undefined>} findById
45
+ * Cherche un utilisateur par l’identifiant contenu dans la revendication `sub`.
46
+ */
47
+
48
+ /** @type {readonly string[]} */
49
+ export const USER_REPOSITORY_METHODS = Object.freeze(["findByIdentifier", "findById"]);
50
+
51
+ /**
52
+ * Vérifie qu’un dépôt injecté respecte le contrat {@link UserRepository}.
53
+ *
54
+ * @param {unknown} repository
55
+ * @returns {asserts repository is UserRepository}
56
+ * @throws {ContractViolationError}
57
+ */
58
+ export function assertUserRepository(repository) {
59
+ assertImplements(repository, "UserRepository", USER_REPOSITORY_METHODS);
60
+ }
61
+
62
+ /**
63
+ * Valide à l’exécution une donnée renvoyée par le dépôt.
64
+ *
65
+ * Une donnée mal formée est une erreur de programmation. Elle lève donc
66
+ * {@link ContractViolationError} au lieu d’être traitée comme un échec de
67
+ * connexion, ce qui masquerait le problème.
68
+ *
69
+ * @param {unknown} record
70
+ * @returns {NormalizedUserRecord | null} Enregistrement avec un `id` toujours en texte.
71
+ * @throws {ContractViolationError}
72
+ */
73
+ export function normalizeUserRecord(record) {
74
+ if (record === null || record === undefined) {
75
+ return null;
76
+ }
77
+ if (typeof record !== "object") {
78
+ throw new ContractViolationError(DEVELOPER_MESSAGES.repositoryRecordNotObject);
79
+ }
80
+
81
+ const { passwordHash, isActive, roles, permissions } = /** @type {Record<string, unknown>} */ (record);
82
+ const id = normalizeUserId(/** @type {Record<string, unknown>} */ (record).id);
83
+ if (typeof passwordHash !== "string" || passwordHash.length === 0) {
84
+ throw new ContractViolationError(DEVELOPER_MESSAGES.repositoryRecordPasswordHash);
85
+ }
86
+ if (typeof isActive !== "boolean") {
87
+ throw new ContractViolationError(DEVELOPER_MESSAGES.repositoryRecordIsActive);
88
+ }
89
+
90
+ return {
91
+ id,
92
+ passwordHash,
93
+ isActive,
94
+ roles: normalizeAuthorities(roles, "roles"),
95
+ permissions: normalizeAuthorities(permissions, "permissions")
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Valide une liste de rôles ou de permissions. Absente, elle vaut une liste
101
+ * vide : un utilisateur sans rôle n’obtient aucun accès protégé par rôle.
102
+ *
103
+ * @param {unknown} value
104
+ * @param {string} field
105
+ * @returns {readonly string[]} Liste figée, sans doublons.
106
+ * @throws {ContractViolationError}
107
+ */
108
+ function normalizeAuthorities(value, field) {
109
+ if (value === undefined || value === null) {
110
+ return Object.freeze([]);
111
+ }
112
+ const isValid =
113
+ Array.isArray(value) &&
114
+ value.length <= MAX_AUTHORITIES &&
115
+ value.every((item) => typeof item === "string" && item.length > 0 && item.length <= MAX_AUTHORITY_LENGTH);
116
+ if (!isValid) {
117
+ throw new ContractViolationError(repositoryRecordAuthorities(field, MAX_AUTHORITIES, MAX_AUTHORITY_LENGTH));
118
+ }
119
+ return Object.freeze([...new Set(/** @type {string[]} */ (value))]);
120
+ }
121
+
122
+ /**
123
+ * Accepte les formes d’identifiant les plus courantes et les convertit en
124
+ * chaîne : texte, entier auto-incrémenté (SQL), BigInt et ObjectId MongoDB.
125
+ * La conversion est sans perte, ce qui évite aux applications de l’écrire.
126
+ *
127
+ * @param {unknown} id
128
+ * @returns {string | null} L’identifiant en texte, ou `null` si sa forme n’est pas prise en charge.
129
+ */
130
+ export function toUserIdString(id) {
131
+ if (typeof id === "string" && id.length > 0) {
132
+ return id;
133
+ }
134
+ if ((typeof id === "number" && Number.isSafeInteger(id)) || typeof id === "bigint") {
135
+ return String(id);
136
+ }
137
+ if (id !== null && typeof id === "object") {
138
+ const { toHexString } = /** @type {{ toHexString?: unknown }} */ (id);
139
+ if (typeof toHexString === "function") {
140
+ return String(toHexString.call(id));
141
+ }
142
+ }
143
+ return null;
144
+ }
145
+
146
+ /**
147
+ * @param {unknown} id
148
+ * @returns {string}
149
+ * @throws {ContractViolationError}
150
+ */
151
+ function normalizeUserId(id) {
152
+ const normalized = toUserIdString(id);
153
+ if (normalized === null) {
154
+ throw new ContractViolationError(DEVELOPER_MESSAGES.repositoryRecordId);
155
+ }
156
+ return normalized;
157
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Types d’erreurs structurées pour Express AuthKit.
3
+ *
4
+ * Chaque erreur expose un `code` stable, utilisable par les applications.
5
+ * Ces codes font partie de l’API publique. Le cœur n’associe jamais les
6
+ * erreurs à des statuts HTTP : c’est le rôle des adaptateurs.
7
+ *
8
+ * @module core/errors
9
+ */
10
+
11
+ import { DEVELOPER_MESSAGES, PUBLIC_MESSAGES, dependencyFailed } from "./messages.js";
12
+
13
+ /**
14
+ * Codes d’erreur stables exposés par la bibliothèque.
15
+ *
16
+ * @readonly
17
+ * @enum {string}
18
+ */
19
+ export const AUTH_ERROR_CODES = Object.freeze({
20
+ /** Échec de connexion, volontairement générique pour éviter l’énumération des comptes. */
21
+ INVALID_CREDENTIALS: "INVALID_CREDENTIALS",
22
+ /** Donnée invalide (mauvais type, vide ou trop longue). */
23
+ INVALID_INPUT: "INVALID_INPUT",
24
+ /** Aucun jeton d’accès n’a été fourni. */
25
+ TOKEN_MISSING: "TOKEN_MISSING",
26
+ /** Le jeton est mal formé, altéré, signé avec une autre clé ou contient des revendications invalides. */
27
+ TOKEN_INVALID: "TOKEN_INVALID",
28
+ /** La signature du jeton est valide, mais le jeton a expiré. */
29
+ TOKEN_EXPIRED: "TOKEN_EXPIRED",
30
+ /** Le moteur est configuré avec des options manquantes ou non sûres. */
31
+ INVALID_CONFIGURATION: "INVALID_CONFIGURATION",
32
+ /** Une dépendance injectée ne respecte pas son contrat. */
33
+ CONTRACT_VIOLATION: "CONTRACT_VIOLATION",
34
+ /** Le fournisseur de mots de passe ne peut pas lire un hash stocké. */
35
+ INVALID_PASSWORD_HASH: "INVALID_PASSWORD_HASH",
36
+ /** L’utilisateur est authentifié mais n’a pas les rôles ou permissions requis. */
37
+ ACCESS_DENIED: "ACCESS_DENIED",
38
+ /** Le jeton de rafraîchissement est inconnu, expiré, déjà utilisé ou révoqué. */
39
+ REFRESH_TOKEN_INVALID: "REFRESH_TOKEN_INVALID",
40
+ /** Une dépendance injectée (repository, provider) a échoué de façon inattendue. */
41
+ DEPENDENCY_FAILURE: "DEPENDENCY_FAILURE"
42
+ });
43
+
44
+ /**
45
+ * Raisons internes associées aux échecs d’authentification.
46
+ *
47
+ * Elles servent uniquement à l’audit côté serveur et ne doivent jamais être
48
+ * envoyées au client, car elles révéleraient l’existence d’un compte.
49
+ *
50
+ * @readonly
51
+ * @enum {string}
52
+ */
53
+ export const AUTH_FAILURE_REASONS = Object.freeze({
54
+ USER_NOT_FOUND: "USER_NOT_FOUND",
55
+ INVALID_PASSWORD: "INVALID_PASSWORD",
56
+ ACCOUNT_DISABLED: "ACCOUNT_DISABLED",
57
+ /** Jeton de rafraîchissement absent du stockage (jamais émis, ou révoqué). */
58
+ REFRESH_TOKEN_UNKNOWN: "REFRESH_TOKEN_UNKNOWN",
59
+ /** Jeton de rafraîchissement expiré. */
60
+ REFRESH_TOKEN_EXPIRED: "REFRESH_TOKEN_EXPIRED",
61
+ /** Jeton de rafraîchissement déjà utilisé : vol probable, toute la famille est révoquée. */
62
+ REFRESH_TOKEN_REUSED: "REFRESH_TOKEN_REUSED"
63
+ });
64
+
65
+ /**
66
+ * Classe de base de toutes les erreurs levées par Express AuthKit.
67
+ */
68
+ export class AuthKitError extends Error {
69
+ /**
70
+ * @param {string} code Code d’erreur stable (voir {@link AUTH_ERROR_CODES}).
71
+ * @param {string} message Message pouvant être exposé aux clients de l’API.
72
+ * @param {{ cause?: unknown }} [options]
73
+ */
74
+ constructor(code, message, options = {}) {
75
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
76
+ this.name = new.target.name;
77
+ /** @type {string} */
78
+ this.code = code;
79
+ }
80
+
81
+ /**
82
+ * Représentation sûre pour les journaux et `JSON.stringify()` : ni pile
83
+ * d’appels, ni cause, ni raison interne.
84
+ *
85
+ * @returns {{ name: string, code: string, message: string }}
86
+ */
87
+ toJSON() {
88
+ return { name: this.name, code: this.code, message: this.message };
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Échec de connexion. Le message public ne précise jamais la cause.
94
+ */
95
+ export class InvalidCredentialsError extends AuthKitError {
96
+ /**
97
+ * @param {string} [reason] Raison d’audit interne (voir {@link AUTH_FAILURE_REASONS}).
98
+ */
99
+ constructor(reason) {
100
+ super(AUTH_ERROR_CODES.INVALID_CREDENTIALS, PUBLIC_MESSAGES.invalidCredentials);
101
+ // Non énumérable pour éviter une fuite accidentelle dans une réponse HTTP.
102
+ Object.defineProperty(this, "reason", { value: reason, enumerable: false });
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Donnée structurellement invalide reçue à une frontière de confiance.
108
+ */
109
+ export class InvalidInputError extends AuthKitError {
110
+ /**
111
+ * @param {string} message
112
+ */
113
+ constructor(message) {
114
+ super(AUTH_ERROR_CODES.INVALID_INPUT, message);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Jeton d’accès absent de la requête.
120
+ */
121
+ export class TokenMissingError extends AuthKitError {
122
+ constructor() {
123
+ super(AUTH_ERROR_CODES.TOKEN_MISSING, PUBLIC_MESSAGES.tokenMissing);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Jeton d’accès rejeté (mal formé, altéré, mauvais émetteur ou audience, utilisateur inconnu ou désactivé…).
129
+ */
130
+ export class InvalidTokenError extends AuthKitError {
131
+ /**
132
+ * @param {{ cause?: unknown, reason?: string }} [options]
133
+ */
134
+ constructor(options = {}) {
135
+ super(AUTH_ERROR_CODES.TOKEN_INVALID, PUBLIC_MESSAGES.tokenInvalid, { cause: options.cause });
136
+ Object.defineProperty(this, "reason", { value: options.reason, enumerable: false });
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Jeton d’accès dont la signature est valide, mais qui a expiré.
142
+ */
143
+ export class TokenExpiredError extends AuthKitError {
144
+ /**
145
+ * @param {{ cause?: unknown }} [options]
146
+ */
147
+ constructor(options = {}) {
148
+ super(AUTH_ERROR_CODES.TOKEN_EXPIRED, PUBLIC_MESSAGES.tokenExpired, options);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Utilisateur authentifié, mais sans les droits demandés (rôle, permission ou
154
+ * règle personnalisée). Le message ne dit pas quel droit manque.
155
+ */
156
+ export class AccessDeniedError extends AuthKitError {
157
+ constructor() {
158
+ super(AUTH_ERROR_CODES.ACCESS_DENIED, PUBLIC_MESSAGES.accessDenied);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Jeton de rafraîchissement refusé. Le message public est toujours le même ;
164
+ * la cause précise est dans `reason` (non énumérable), pour les journaux.
165
+ */
166
+ export class InvalidRefreshTokenError extends AuthKitError {
167
+ /**
168
+ * @param {string} [reason] Raison d’audit interne (voir {@link AUTH_FAILURE_REASONS}).
169
+ */
170
+ constructor(reason) {
171
+ super(AUTH_ERROR_CODES.REFRESH_TOKEN_INVALID, PUBLIC_MESSAGES.refreshTokenInvalid);
172
+ Object.defineProperty(this, "reason", { value: reason, enumerable: false });
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Configuration manquante, mal formée ou non sûre, levée à l’initialisation.
178
+ */
179
+ export class ConfigurationError extends AuthKitError {
180
+ /**
181
+ * @param {string} message
182
+ */
183
+ constructor(message) {
184
+ super(AUTH_ERROR_CODES.INVALID_CONFIGURATION, message);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Dépendance injectée qui n’implémente pas son contrat.
190
+ */
191
+ export class ContractViolationError extends AuthKitError {
192
+ /**
193
+ * @param {string} message
194
+ */
195
+ constructor(message) {
196
+ super(AUTH_ERROR_CODES.CONTRACT_VIOLATION, message);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Hash de mot de passe stocké que le fournisseur ne peut pas lire. Cela vient
202
+ * généralement d’une migration de données (par exemple bcrypt avec Argon2).
203
+ */
204
+ export class InvalidPasswordHashError extends AuthKitError {
205
+ /**
206
+ * @param {{ cause?: unknown }} [options]
207
+ */
208
+ constructor(options = {}) {
209
+ super(AUTH_ERROR_CODES.INVALID_PASSWORD_HASH, DEVELOPER_MESSAGES.unsupportedPasswordHash, options);
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Échec inattendu d’une dépendance injectée : base de données indisponible,
215
+ * service distant en panne, bug dans un provider personnalisé…
216
+ *
217
+ * L’erreur d’origine est conservée dans `cause` pour le diagnostic. Elle n’est
218
+ * jamais exposée aux clients HTTP par l’adaptateur Express.
219
+ */
220
+ export class DependencyFailureError extends AuthKitError {
221
+ /**
222
+ * @param {string} dependency Nom de l’appel qui a échoué, par exemple « UserRepository.findById ».
223
+ * @param {unknown} cause Erreur d’origine.
224
+ */
225
+ constructor(dependency, cause) {
226
+ super(AUTH_ERROR_CODES.DEPENDENCY_FAILURE, dependencyFailed(dependency), { cause });
227
+ /** @type {string} */
228
+ this.dependency = dependency;
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Exécute l’appel à une dépendance injectée. Les erreurs de la bibliothèque
234
+ * (jeton expiré, hash illisible…) sont transmises telles quelles ; toute autre
235
+ * erreur est enveloppée dans une {@link DependencyFailureError}.
236
+ *
237
+ * @template T
238
+ * @param {string} dependency
239
+ * @param {() => Promise<T> | T} call
240
+ * @returns {Promise<T>}
241
+ */
242
+ export async function callDependency(dependency, call) {
243
+ try {
244
+ return await call();
245
+ } catch (error) {
246
+ throw isAuthKitError(error) ? error : new DependencyFailureError(dependency, error);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Garde de type pour les erreurs levées par Express AuthKit.
252
+ *
253
+ * @param {unknown} error
254
+ * @returns {error is AuthKitError}
255
+ */
256
+ export function isAuthKitError(error) {
257
+ return error instanceof AuthKitError;
258
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Limites appliquées aux données non fiables avant tout traitement coûteux.
3
+ *
4
+ * Elles évitent l’épuisement des ressources par des valeurs trop longues tout
5
+ * en restant largement au-dessus des besoins légitimes.
6
+ *
7
+ * @module core/input-limits
8
+ */
9
+
10
+ /** Longueur maximale d’un e-mail selon la RFC 5321, suffisante aussi pour un nom d’utilisateur. */
11
+ export const MAX_IDENTIFIER_LENGTH = 320;
12
+
13
+ /** Suffisante pour les phrases de passe, tout en limitant le coût du hachage. */
14
+ export const MAX_PASSWORD_LENGTH = 1024;
15
+
16
+ /** Les jetons d’accès produits par cette bibliothèque font quelques centaines de caractères. */
17
+ export const MAX_TOKEN_LENGTH = 8192;
18
+
19
+ /** Les jetons de rafraîchissement émis font 43 caractères ; la marge couvre d’autres formats. */
20
+ export const MAX_REFRESH_TOKEN_LENGTH = 512;
21
+
22
+ /** Nombre maximal de rôles (ou de permissions) par utilisateur : borne le coût des vérifications. */
23
+ export const MAX_AUTHORITIES = 256;
24
+
25
+ /** Longueur maximale d’un nom de rôle ou de permission (par exemple « orders:write »). */
26
+ export const MAX_AUTHORITY_LENGTH = 128;