@nodefony/security 10.0.0-beta.3 → 10.0.0-beta.5

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 (165) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/index.js +6 -8
  3. package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +18 -11
  4. package/dist/nodefony/command/security-oauth-doctor.js +5 -3
  5. package/dist/nodefony/command/security-secrets.js +10 -4
  6. package/dist/nodefony/command/security-user-add.js +9 -3
  7. package/dist/nodefony/command/security-user-delete.js +5 -3
  8. package/dist/nodefony/command/security-user-list.js +5 -3
  9. package/dist/nodefony/command/security-user-password.js +5 -3
  10. package/dist/nodefony/config/config.js +20 -4
  11. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  12. package/dist/nodefony/contracts/IAccessVoter.js +1 -2
  13. package/dist/nodefony/errors/AccessDeniedError.js +1 -2
  14. package/dist/nodefony/errors/ApiKeyError.js +3 -2
  15. package/dist/nodefony/errors/AuthenticationError.js +1 -2
  16. package/dist/nodefony/errors/CsrfError.js +1 -2
  17. package/dist/nodefony/errors/InsecureTransportError.js +1 -2
  18. package/dist/nodefony/errors/InvalidTargetError.js +1 -2
  19. package/dist/nodefony/errors/SsrfError.js +1 -2
  20. package/dist/nodefony/errors/ThrottledError.js +1 -2
  21. package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
  22. package/dist/nodefony/errors/WebAuthnError.js +1 -2
  23. package/dist/nodefony/keycloak/keycloakAdmin.js +1 -2
  24. package/dist/nodefony/keycloak/keycloakRealm.js +1 -2
  25. package/dist/nodefony/keycloak/keycloakRealmInput.js +1 -2
  26. package/dist/nodefony/keycloak/scaffold.js +3 -3
  27. package/dist/nodefony/keycloak/security-keycloak-realm.js +5 -3
  28. package/dist/nodefony/scaffold/contribute.js +1 -2
  29. package/dist/nodefony/service/accessTokenVerifier.js +101 -34
  30. package/dist/nodefony/service/apiKeys.js +45 -5
  31. package/dist/nodefony/service/auditService.js +4 -5
  32. package/dist/nodefony/service/authFlow.js +10 -8
  33. package/dist/nodefony/service/authorization.js +1 -2
  34. package/dist/nodefony/service/cors.js +1 -2
  35. package/dist/nodefony/service/csrf.js +1 -2
  36. package/dist/nodefony/service/firewall.js +5 -4
  37. package/dist/nodefony/service/keycloak.js +1 -2
  38. package/dist/nodefony/service/oauth2.js +29 -7
  39. package/dist/nodefony/service/securityHeaders.js +1 -2
  40. package/dist/nodefony/service/tokenService.js +38 -22
  41. package/dist/nodefony/service/totp.js +4 -5
  42. package/dist/nodefony/service/webAuthn.js +4 -5
  43. package/dist/nodefony/service/webhooks.js +4 -5
  44. package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
  45. package/dist/nodefony/src/SecuredArea.js +1 -2
  46. package/dist/nodefony/src/accountBinding.js +96 -0
  47. package/dist/nodefony/src/admin/SecurityAdminApi.js +1 -2
  48. package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
  49. package/dist/nodefony/src/admin/adminAudit.js +1 -2
  50. package/dist/nodefony/src/admin/userRevocationCascade.js +79 -15
  51. package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
  52. package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
  53. package/dist/nodefony/src/audit/auditBridge.js +1 -2
  54. package/dist/nodefony/src/audit/auditFilters.js +1 -2
  55. package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
  56. package/dist/nodefony/src/audit/readAuditContext.js +1 -2
  57. package/dist/nodefony/src/audit/recordAudit.js +1 -2
  58. package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
  59. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
  60. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +8 -113
  61. package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
  62. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +42 -4
  63. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +91 -16
  64. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
  65. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +1 -2
  66. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +3 -17
  67. package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
  68. package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
  69. package/dist/nodefony/src/boot/publicRoutesNotice.js +1 -2
  70. package/dist/nodefony/src/crypto/secretCipher.js +1 -2
  71. package/dist/nodefony/src/csp.js +1 -2
  72. package/dist/nodefony/src/csrfToken.js +1 -2
  73. package/dist/nodefony/src/entryPoint.js +1 -2
  74. package/dist/nodefony/src/net/ssrfGuard.js +1 -2
  75. package/dist/nodefony/src/net/transportFailure.js +57 -0
  76. package/dist/nodefony/src/oauth/clientCredentials.js +1 -2
  77. package/dist/nodefony/src/oauth/httpJson.js +3 -3
  78. package/dist/nodefony/src/oauth/metadata.js +5 -16
  79. package/dist/nodefony/src/oauth/oauth2Client.js +1 -2
  80. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +1 -2
  81. package/dist/nodefony/src/oauth/providerDiagnosis.js +5 -6
  82. package/dist/nodefony/src/oauth/providerRoles.js +1 -2
  83. package/dist/nodefony/src/oauth/providers/github.js +16 -6
  84. package/dist/nodefony/src/oauth/providers/oidc.js +9 -6
  85. package/dist/nodefony/src/openToSignup.js +1 -2
  86. package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
  87. package/dist/nodefony/src/realtime/frameAuthorizer.js +2 -3
  88. package/dist/nodefony/src/rolesWithin.js +57 -0
  89. package/dist/nodefony/src/sessionAuthentication.js +1 -2
  90. package/dist/nodefony/src/sessionIdentity.js +29 -3
  91. package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
  92. package/dist/nodefony/src/token/AnonymousToken.js +1 -2
  93. package/dist/nodefony/src/token/JwtKeystore.js +14 -5
  94. package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
  95. package/dist/nodefony/src/token/RemoteJwtVerifier.js +6 -3
  96. package/dist/nodefony/src/token/UserToken.js +1 -2
  97. package/dist/nodefony/src/token/jwtRuntime.js +1 -2
  98. package/dist/nodefony/src/token/scopeGrants.js +109 -0
  99. package/dist/nodefony/src/token/secretFile.js +1 -2
  100. package/dist/nodefony/src/token/tokenCriteria.js +1 -2
  101. package/dist/nodefony/src/token/tokenFilters.js +1 -2
  102. package/dist/nodefony/src/token/tokenSort.js +1 -2
  103. package/dist/nodefony/src/token/tokenStatus.js +1 -2
  104. package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
  105. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
  106. package/dist/nodefony/src/totp/totpCipher.js +1 -2
  107. package/dist/nodefony/src/totp/totpCrypto.js +1 -2
  108. package/dist/nodefony/src/totp/totpOperations.js +1 -2
  109. package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
  110. package/dist/nodefony/src/transport/CredentialTransportPolicy.js +1 -2
  111. package/dist/nodefony/src/voter/RoleVoter.js +1 -2
  112. package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
  113. package/dist/nodefony/src/voter/voterRegistry.js +1 -2
  114. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
  115. package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
  116. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
  117. package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
  118. package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
  119. package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
  120. package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
  121. package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
  122. package/dist/nodefony/src/webhook/webhookSort.js +1 -2
  123. package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
  124. package/dist/nodefony/testing/index.js +108 -0
  125. package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +7 -6
  126. package/dist/types/nodefony/config/config.d.ts +1 -0
  127. package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
  128. package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
  129. package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
  130. package/dist/types/nodefony/service/oauth2.d.ts +20 -0
  131. package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
  132. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +23 -5
  133. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -44
  134. package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
  135. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +4 -1
  136. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
  137. package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
  138. package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
  139. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
  140. package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +2 -2
  141. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
  142. package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
  143. package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
  144. package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
  145. package/dist/types/nodefony/testing/index.d.ts +51 -0
  146. package/docs/api-keys.md +181 -174
  147. package/docs/audit.md +140 -140
  148. package/docs/authenticators.md +101 -101
  149. package/docs/authorization.md +116 -69
  150. package/docs/cors.md +64 -64
  151. package/docs/csrf.md +74 -71
  152. package/docs/external-jwt.md +22 -12
  153. package/docs/firewall.md +85 -82
  154. package/docs/headers.md +81 -81
  155. package/docs/index.md +4 -4
  156. package/docs/keycloak.md +95 -78
  157. package/docs/login-page.md +23 -23
  158. package/docs/oauth2.md +157 -146
  159. package/docs/obtenir-un-jeton.md +116 -26
  160. package/docs/tokens.md +128 -122
  161. package/docs/totp.md +136 -136
  162. package/docs/webauthn.md +153 -153
  163. package/docs/webhooks.md +136 -124
  164. package/package.json +19 -13
  165. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
@@ -1,7 +1,8 @@
1
1
  import { RemoteJwtVerifier } from "../../token/RemoteJwtVerifier.js";
2
2
  import { OAuth2Client } from "../oauth2Client.js";
3
3
  import { discoverAuthorizationServer } from "../metadata.js";
4
- //#region nodefony/src/oauth/providers/oidc.ts
4
+ import { isPlainObject } from "nodefony";
5
+ // nodefony/src/oauth/providers/oidc.ts
5
6
  const DEFAULT_OIDC_SCOPES = [
6
7
  "openid",
7
8
  "profile",
@@ -35,7 +36,7 @@ function readLogoutTokenClaims(issuer, payload) {
35
36
  const sid = typeof payload.sid === "string" ? payload.sid : null;
36
37
  if ((sub === null || sub === "") && (sid === null || sid === "")) return null;
37
38
  const events = payload.events;
38
- if (typeof events !== "object" || events === null || Array.isArray(events)) return null;
39
+ if (!isPlainObject(events)) return null;
39
40
  const event = events[BACKCHANNEL_LOGOUT_EVENT];
40
41
  if (typeof event !== "object" || event === null || Array.isArray(event)) return null;
41
42
  if ("nonce" in payload) return null;
@@ -70,14 +71,17 @@ async function decodeIdTokenClaims(idToken) {
70
71
  * écarts : un jeton d'un autre émetteur (point 2), délivré à une autre
71
72
  * application (point 3), ou périmé (point 9), n'a rien à faire ici.
72
73
  *
74
+ * @returns le `sub` du jeton, éprouvé non vide.
73
75
  * @throws Error - un claim obligatoire est absent, discordant ou périmé.
74
76
  */
75
77
  function assertIdTokenClaims(claims, opts) {
76
- if (typeof claims.sub !== "string" || claims.sub.length === 0) throw new Error(`${opts.name}: ID token sans claim 'sub'.`);
78
+ const sub = claims.sub;
79
+ if (typeof sub !== "string" || sub.length === 0) throw new Error(`${opts.name}: ID token sans claim 'sub'.`);
77
80
  if (claims.iss !== opts.issuer) throw new Error(`${opts.name}: ID token émis par « ${String(claims.iss)} », attendu « ${opts.issuer} ».`);
78
81
  const aud = claims.aud;
79
82
  if (!(aud === opts.clientId || Array.isArray(aud) && aud.includes(opts.clientId))) throw new Error(`${opts.name}: ID token délivré à une autre application.`);
80
83
  if (typeof claims.exp !== "number" || claims.exp * 1e3 <= Date.now()) throw new Error(`${opts.name}: ID token périmé ou sans 'exp'.`);
84
+ return sub;
81
85
  }
82
86
  /**
83
87
  * Fabrique un {@link IOAuthProvider} **générique OIDC** — couvre TOUT fournisseur
@@ -113,8 +117,8 @@ function createOidcProvider(opts) {
113
117
  },
114
118
  async fetchProfile(tokens) {
115
119
  const claims = await opts.decodeIdToken(tokens.idToken());
116
- assertIdTokenClaims(claims, opts);
117
- const sub = claims.sub;
120
+ if (!isPlainObject(claims)) throw new Error(`${opts.name}: ID token sans charge utile objet.`);
121
+ const sub = assertIdTokenClaims(claims, opts);
118
122
  return {
119
123
  provider: opts.name,
120
124
  providerId: sub,
@@ -197,5 +201,4 @@ async function createDiscoveredOidcProvider(name, ctx, options = {}) {
197
201
  })
198
202
  });
199
203
  }
200
- //#endregion
201
204
  export { createDiscoveredOidcProvider, createOidcProvider, readLogoutTokenClaims };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/openToSignup.ts
1
+ // nodefony/src/openToSignup.ts
2
2
  /**
3
3
  * Les zones protégées qui n'exigent aucun rôle alors que la connexion OAuth
4
4
  * crée les comptes à la volée — c'est-à-dire ouvertes à tout compte que le
@@ -43,5 +43,4 @@ function findZonesOpenToSignup(config) {
43
43
  }
44
44
  return found;
45
45
  }
46
- //#endregion
47
46
  export { findZonesOpenToSignup };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/realtime/UserRealtimeToken.ts
1
+ // nodefony/src/realtime/UserRealtimeToken.ts
2
2
  /** Aucun scope — figé et partagé (0 allocation pour le cas humain, le plus courant). */
3
3
  const EMPTY_SCOPES = Object.freeze([]);
4
4
  /**
@@ -41,12 +41,20 @@ var UserRealtimeToken = class {
41
41
  */
42
42
  #scopes;
43
43
  /**
44
+ * Confirmation du COMPTE (annuaire relu), fournie par le
45
+ * {@link FirewallRealtimeAuthenticator} pour les modes dont l'application
46
+ * émet elle-même l'artefact. `null` = rien à confirmer.
47
+ */
48
+ #confirmAccount;
49
+ /**
44
50
  * @param user - identité authentifiée résolue par le firewall.
45
51
  * @param revalidate - preuve de vie de l'identité, adaptée à son mode.
46
52
  * @param type - mode d'authentification réel (défaut `"session"`).
47
53
  * @param scopes - scopes délégués du jeton d'origine (défaut aucun).
54
+ * @param confirmAccount - relecture du compte, payée sur `api.request` seul.
48
55
  */
49
- constructor(user, revalidate = null, type = "session", scopes = EMPTY_SCOPES) {
56
+ constructor(user, revalidate = null, type = "session", scopes = EMPTY_SCOPES, confirmAccount = null) {
57
+ this.#confirmAccount = confirmAccount;
50
58
  this.#user = user;
51
59
  this.#revalidate = revalidate;
52
60
  this.type = type;
@@ -67,6 +75,13 @@ var UserRealtimeToken = class {
67
75
  isValid(nowMs) {
68
76
  return this.#revalidate ? this.#revalidate(nowMs) : Promise.resolve(true);
69
77
  }
78
+ /**
79
+ * Le compte qui a ouvert la socket existe-t-il toujours, actif, et sous le
80
+ * même `id` immuable ? `true` si aucune confirmation n'est câblée.
81
+ */
82
+ confirmAccount() {
83
+ return this.#confirmAccount ? this.#confirmAccount() : Promise.resolve(true);
84
+ }
70
85
  getUserIdentifier() {
71
86
  return this.#user.identifier;
72
87
  }
@@ -90,5 +105,4 @@ var UserRealtimeToken = class {
90
105
  return key === "user" ? this.#user : void 0;
91
106
  }
92
107
  };
93
- //#endregion
94
108
  export { UserRealtimeToken, UserRealtimeToken as default };
@@ -1,5 +1,5 @@
1
1
  import { NODEFONY_CHANNEL_NAMESPACE, PLATFORM_CHANNELS, PLATFORM_INBOUND, startsWithCI } from "nodefony";
2
- //#region nodefony/src/realtime/frameAuthorizer.ts
2
+ // nodefony/src/realtime/frameAuthorizer.ts
3
3
  /**
4
4
  * Politique système par défaut des canaux d'**introspection serveur** : réservés
5
5
  * à l'exploitant de l'INSTANCE, pas à l'administrateur d'une application.
@@ -254,7 +254,7 @@ function authorizeChannel(channel, token, firewall, resolver, systemRules, onDen
254
254
  * (api.request) ou la politique du canal (subscribe/inbound).
255
255
  *
256
256
  * Trois surfaces gardées :
257
- * - `api.request {path}` (pont API souverain) : re-match de zone HTTP → zone
257
+ * - `api.request {path}` (pont socket) : re-match de zone HTTP → zone
258
258
  * protégée + anonyme = refus (autorisation de ZONE, identique à `GET {path}` ;
259
259
  * la méthode/action est gardée en aval par le router + `@IsGranted`).
260
260
  * - `subscribe {channel}` : politique de canal (système plancher + déclaration
@@ -289,5 +289,4 @@ function buildFrameAuthorizer(firewall, options) {
289
289
  return true;
290
290
  };
291
291
  }
292
- //#endregion
293
292
  export { DEFAULT_SYSTEM_PREFIXES, DEFAULT_SYSTEM_RULES, RESERVED_FLOOR_PREFIXES, SECURITY_CHANNEL_POLICY, SYSTEM_CHANNEL_POLICY, UPLINK_CHANNEL_POLICY, buildFrameAuthorizer, buildFrameAuthorizer as default, buildSystemRules };
@@ -0,0 +1,57 @@
1
+ // nodefony/src/rolesWithin.ts
2
+ /**
3
+ * Lit un compte de l'annuaire, sans faire confiance au fournisseur : une
4
+ * lecture qui lève et une lecture qui rend `null` disent toutes deux
5
+ * « introuvable ».
6
+ */
7
+ async function loadAccount(users, identifier) {
8
+ try {
9
+ return await users.loadUserByIdentifier(identifier) ?? null;
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+ /** Rôles chaîne d'un compte lu, sans lui faire confiance. */
15
+ function rolesOf(user) {
16
+ return Array.isArray(user.roles) ? user.roles.filter((r) => typeof r === "string") : [];
17
+ }
18
+ /**
19
+ * La règle « jouer un autre compte » : les rôles de `target`, hiérarchie
20
+ * comprise, sont-ils TOUS atteints par ceux de `owner` ? Jouer un compte ne
21
+ * monte jamais en droits.
22
+ *
23
+ * Fermée en cas de doute : un annuaire absent, un compte introuvable — y
24
+ * compris un sujet externe éphémère, qui n'a pas de compte local — rendent un
25
+ * refus, jamais un accord. L'annuaire est résolu à l'appel : le service `users`
26
+ * peut être posé après le pare-feu.
27
+ *
28
+ * @param resolveUsers - rend l'annuaire des comptes (`users`), ou `null`
29
+ * @param hierarchy - la hiérarchie de rôles du pare-feu
30
+ * @returns la règle, à poser au conteneur sous `ROLES_WITHIN`
31
+ */
32
+ function rolesWithin(resolveUsers, hierarchy) {
33
+ return async (owner, target) => {
34
+ const users = resolveUsers();
35
+ if (users === null) return {
36
+ ok: false,
37
+ reason: "annuaire des comptes indisponible (@nodefony/user)"
38
+ };
39
+ const ownerUser = await loadAccount(users, owner);
40
+ if (ownerUser === null) return {
41
+ ok: false,
42
+ reason: `\`${owner}\` n'a pas de compte dans l'annuaire de l'application (un sujet externe éphémère ne peut jouer aucun compte)`
43
+ };
44
+ const targetUser = await loadAccount(users, target);
45
+ if (targetUser === null) return {
46
+ ok: false,
47
+ reason: `compte \`${target}\` introuvable`
48
+ };
49
+ const granted = hierarchy.reachableRoles(rolesOf(ownerUser));
50
+ const extra = [...hierarchy.reachableRoles(rolesOf(targetUser))].filter((role) => !granted.has(role));
51
+ return extra.length === 0 ? { ok: true } : {
52
+ ok: false,
53
+ reason: `\`${target}\` a des rôles que \`${owner}\` n'a pas : ${extra.sort().join(", ")}`
54
+ };
55
+ };
56
+ }
57
+ export { rolesWithin };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/sessionAuthentication.ts
1
+ // nodefony/src/sessionAuthentication.ts
2
2
  /**
3
3
  * Facteurs d'authentification de Nodefony → méthodes RFC 8176, posées dans la
4
4
  * session à son ouverture. Les clés et leur lecteur sont au cœur
@@ -22,5 +22,4 @@ const NO_LOCAL_FACTOR = Object.freeze([]);
22
22
  function amrForFactor(factor) {
23
23
  return Object.hasOwn(FACTOR_AMR, factor) ? FACTOR_AMR[factor] ?? NO_LOCAL_FACTOR : NO_LOCAL_FACTOR;
24
24
  }
25
- //#endregion
26
25
  export { amrForFactor };
@@ -1,6 +1,7 @@
1
1
  import { AuthenticationError } from "../errors/AuthenticationError.js";
2
+ import { isBoundToAccount } from "./accountBinding.js";
2
3
  import { UserNotFoundError } from "@nodefony/user";
3
- //#region nodefony/src/sessionIdentity.ts
4
+ // nodefony/src/sessionIdentity.ts
4
5
  const INVALID_SESSION = "Invalid session";
5
6
  /**
6
7
  * Résout l'identité portée par une session (l'identifiant stocké dans le blob)
@@ -31,5 +32,30 @@ async function resolveSessionIdentity(provider, identifier) {
31
32
  if (user.isLocked() || !user.isActive()) throw new AuthenticationError(INVALID_SESSION);
32
33
  return user;
33
34
  }
34
- //#endregion
35
- export { resolveSessionIdentity };
35
+ /**
36
+ * Résout l'identité d'une session DÉJÀ ouverte, en exigeant qu'elle ait été
37
+ * ouverte pour CE compte — {@link resolveSessionIdentity} plus la liaison à
38
+ * l'identifiant immuable (`isBoundToAccount`).
39
+ *
40
+ * La session range son principal sous l'`identifier`, qu'un compte recréé
41
+ * (suppression hors framework, restauration, import) peut reprendre : sans la
42
+ * liaison, la session de l'ancien titulaire ouvrirait le compte du nouveau.
43
+ * Une session sans liaison est refusée au même titre.
44
+ *
45
+ * @param provider - source d'identité (`UserService` via le container).
46
+ * @param identifier - identifiant fonctionnel stocké en session.
47
+ * @param boundAccountId - identifiant immuable inscrit à l'ouverture
48
+ * (`metaBag`, clé `ACCOUNT_ID_KEY`).
49
+ * @param boundCredentialsVersion - version des identifiants inscrite à
50
+ * l'ouverture (`metaBag`, clé `CREDENTIALS_VERSION_KEY`).
51
+ * @returns l'utilisateur actif, titulaire de la session.
52
+ * @throws AuthenticationError (401, message uniforme) — identifiant inconnu,
53
+ * compte verrouillé ou désactivé, session ouverte pour un autre compte ou
54
+ * avant le dernier changement de mot de passe.
55
+ */
56
+ async function resolveBoundSessionIdentity(provider, identifier, boundAccountId, boundCredentialsVersion) {
57
+ const user = await resolveSessionIdentity(provider, identifier);
58
+ if (!isBoundToAccount(user, boundAccountId, boundCredentialsVersion)) throw new AuthenticationError(INVALID_SESSION);
59
+ return user;
60
+ }
61
+ export { resolveBoundSessionIdentity, resolveSessionIdentity };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/throttle/LoginThrottler.ts
1
+ // nodefony/src/throttle/LoginThrottler.ts
2
2
  const DEFAULT_FREE_ATTEMPTS = 3;
3
3
  const DEFAULT_BASE_DELAY_S = 1;
4
4
  const DEFAULT_CAP_DELAY_S = 900;
@@ -93,5 +93,4 @@ var LoginThrottler = class {
93
93
  }
94
94
  }
95
95
  };
96
- //#endregion
97
96
  export { LoginThrottler, LoginThrottler as default };
@@ -1,5 +1,5 @@
1
1
  import { anonymousUser } from "@nodefony/user";
2
- //#region nodefony/src/token/AnonymousToken.ts
2
+ // nodefony/src/token/AnonymousToken.ts
3
3
  /**
4
4
  * Token du visiteur non authentifié — Zero Trust : un visiteur EST un utilisateur
5
5
  * anonyme (jamais `null`).
@@ -36,5 +36,4 @@ var AnonymousToken = class {
36
36
  (this.#attributes ??= /* @__PURE__ */ new Map()).set(key, value);
37
37
  }
38
38
  };
39
- //#endregion
40
39
  export { AnonymousToken, AnonymousToken as default };
@@ -1,6 +1,7 @@
1
1
  import { createSecretExclusive, messageNonRestreint, modeNonRestreintAsync, readIfPresent } from "./secretFile.js";
2
+ import { isPlainObject } from "nodefony";
2
3
  import { join } from "node:path";
3
- //#region nodefony/src/token/JwtKeystore.ts
4
+ // nodefony/src/token/JwtKeystore.ts
4
5
  /**
5
6
  * Dossiers que le `Dockerfile` généré EFFACE à l'étage de construction.
6
7
  *
@@ -54,9 +55,18 @@ function parseKeySet(json) {
54
55
  } catch {
55
56
  throw new Error("JwtKeystore: keyset JSON invalide");
56
57
  }
57
- const keyset = parsed;
58
- if (!keyset || !Array.isArray(keyset.keys) || keyset.keys.length === 0 || keyset.keys.some((k) => typeof k.kid !== "string")) throw new Error("JwtKeystore: keyset malformé (champ `keys` non vide avec `kid` requis)");
59
- return keyset;
58
+ if (!isStoredKeySet(parsed)) throw new Error("JwtKeystore: keyset malformé (champ `keys` non vide avec `kid` requis)");
59
+ return parsed;
60
+ }
61
+ /**
62
+ * Forme minimale d'un jeu de clés relu : un objet dont `keys` est un tableau non
63
+ * vide d'objets portant chacun un `kid` textuel. Le reste de chaque JWK est
64
+ * éprouvé par `jose` à l'import de la clé.
65
+ */
66
+ function isStoredKeySet(value) {
67
+ if (!isPlainObject(value)) return false;
68
+ const keys = value.keys;
69
+ return Array.isArray(keys) && keys.length > 0 && keys.every((k) => isPlainObject(k) && typeof k.kid === "string");
60
70
  }
61
71
  /** Génère une nouvelle paire Ed25519 (extractable pour persistance JWK). */
62
72
  async function buildKeySet(jose) {
@@ -225,5 +235,4 @@ var JwtKeystore = class {
225
235
  this.#log(`JWT keystore: ${messageNonRestreint(file, mode)} La clé PRIVÉE y réside : provisionnez-la plutôt hors-bande via jwt.keystore.keySetJson (recommandé en production).`, "WARNING");
226
236
  }
227
237
  };
228
- //#endregion
229
238
  export { JwtKeystore, JwtKeystore as default, generateKeySet, keystoreLeaksIntoImage, parseKeySet };
@@ -1,7 +1,7 @@
1
1
  import { TOKEN_DEFAULT_ORDER, TOKEN_SORTABLE_FIELDS } from "./tokenSort.js";
2
2
  import { matchesTokenStatus } from "./tokenStatus.js";
3
3
  import { assertPageQuery, compareByOrder, pickOrder } from "nodefony";
4
- //#region nodefony/src/token/MemoryTokenStore.ts
4
+ // nodefony/src/token/MemoryTokenStore.ts
5
5
  /**
6
6
  * Filtre un record contre une requête de listing — prédicat de RÉFÉRENCE du
7
7
  * contrat, réutilisé par les implémentations qui évaluent en mémoire.
@@ -245,5 +245,4 @@ var MemoryTokenStore = class {
245
245
  for (const [jti, expiresAt] of this.#deniedJti) if (expiresAt <= now) this.#deniedJti.delete(jti);
246
246
  }
247
247
  };
248
- //#endregion
249
248
  export { MemoryTokenStore, matchesTokenQuery };
@@ -1,5 +1,6 @@
1
+ import { describeTransportFailure, untrustedAuthorityHint } from "../net/transportFailure.js";
1
2
  import { canonicalIssuer, extractScopes, metadataCandidateUrls, parseDiscoveryUrl, validateIssuerMetadata } from "nodefony";
2
- //#region nodefony/src/token/RemoteJwtVerifier.ts
3
+ // nodefony/src/token/RemoteJwtVerifier.ts
3
4
  /**
4
5
  * Codes d'erreur `jose` qui désignent un **jeton fautif** — la liste est
5
6
  * BLANCHE, et c'est le point important.
@@ -163,7 +164,10 @@ var RemoteJwtVerifier = class {
163
164
  };
164
165
  } catch (error) {
165
166
  const code = error.code;
166
- if (!code || !TOKEN_FAULT_CODES.has(code)) throw new Error(`vérification impossible : l'émetteur « ${trusted.issuer} » n'a pas fourni de jeu de clés utilisable (${code ?? error.message}).`, { cause: error });
167
+ if (!code || !TOKEN_FAULT_CODES.has(code)) {
168
+ const hint = untrustedAuthorityHint(error);
169
+ throw new Error(`vérification impossible : l'émetteur « ${trusted.issuer} » n'a pas fourni de jeu de clés utilisable (${code ?? describeTransportFailure(error)}).` + (hint === null ? "" : ` ${hint}.`), { cause: error });
170
+ }
167
171
  this.#audit(`jeton refusé (${trusted.issuer}) : ${code}`);
168
172
  return null;
169
173
  }
@@ -254,5 +258,4 @@ function assertAsymmetric(issuer, algorithms) {
254
258
  if (algorithms.length === 0) throw new Error(`émetteur « ${issuer} » : aucun algorithme accepté — la liste est la garde principale (RFC 8725 §3.1), elle ne peut pas être vide.`);
255
259
  for (const alg of algorithms) if (alg.startsWith("HS") || alg.toLowerCase() === "none") throw new Error(`émetteur « ${issuer} » : algorithme « ${alg} » refusé. Les clés proviennent d'un jeu PUBLIC : un algorithme à secret partagé y transformerait la clé publique en secret de signature.`);
256
260
  }
257
- //#endregion
258
261
  export { RemoteJwtVerifier, RemoteJwtVerifier as default };
@@ -1,5 +1,5 @@
1
1
  import { anonymousUser } from "@nodefony/user";
2
- //#region nodefony/src/token/UserToken.ts
2
+ // nodefony/src/token/UserToken.ts
3
3
  /**
4
4
  * Jeton porteur d'un utilisateur réel — produit par les authenticators à
5
5
  * credential (`userpassword`, puis `session`/`jwt`...).
@@ -63,5 +63,4 @@ var UserToken = class {
63
63
  (this.#attributes ??= /* @__PURE__ */ new Map()).set(key, value);
64
64
  }
65
65
  };
66
- //#endregion
67
66
  export { UserToken, UserToken as default };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/token/jwtRuntime.ts
1
+ // nodefony/src/token/jwtRuntime.ts
2
2
  /**
3
3
  * Dérive les paramètres effectifs depuis la config sécurité. `issuer` omis →
4
4
  * `"nodefony"` (DEVRAIT être surchargé en prod) ; `audiences` vide → l'app est sa
@@ -27,5 +27,4 @@ function resolveJwtRuntime(jwt) {
27
27
  function defaultAudience(runtime) {
28
28
  return runtime.audiences[0] ?? runtime.issuer;
29
29
  }
30
- //#endregion
31
30
  export { defaultAudience, resolveJwtRuntime };
@@ -0,0 +1,109 @@
1
+ import { ADMIN_SCOPE_READ, ADMIN_SCOPE_WRITE, refusedAdminScopes } from "nodefony";
2
+ // nodefony/src/token/scopeGrants.ts
3
+ const SCOPE_TOKEN = /^[\x21\x23-\x5B\x5D-\x7E]+$/u;
4
+ const WILDCARD_SUFFIX = ":*";
5
+ /**
6
+ * Clés que `grantableScopes` refuse : le plan d'administration est jugé par le
7
+ * framework, et une déclaration ici serait soit sans effet, soit un
8
+ * assouplissement — deux pièges que rien ne signalerait.
9
+ */
10
+ const RESERVED_SCOPE_GRANT_KEYS = [
11
+ ADMIN_SCOPE_READ,
12
+ ADMIN_SCOPE_WRITE,
13
+ "admin:*"
14
+ ];
15
+ /**
16
+ * Dit pourquoi une clé de `grantableScopes` est invalide, ou `null` si elle
17
+ * est acceptable.
18
+ *
19
+ * Une clé est un `scope-token` (RFC 6749 §3.3) ; un `*` n'y est admis qu'en
20
+ * dernière position, juste après `:` (une famille `préfixe:*`).
21
+ *
22
+ * @param key - la clé déclarée.
23
+ * @returns le motif du refus, ou `null`.
24
+ */
25
+ function scopeGrantKeyProblem(key) {
26
+ if (!SCOPE_TOKEN.test(key)) return "n'est pas un scope valide (RFC 6749 §3.3 : ni espace, ni guillemet, ni barre oblique inverse)";
27
+ const star = key.indexOf("*");
28
+ if (star !== -1 && (star !== key.length - 1 || !key.endsWith(WILDCARD_SUFFIX) || key.length === 2)) return "porte un `*` ailleurs qu'en fin de famille (`préfixe:*`)";
29
+ if (RESERVED_SCOPE_GRANT_KEYS.includes(key)) return "est réservé au plan d'administration du framework (les scopes `admin:read`/`admin:write` sont accordés au seul rôle ROLE_NODEFONY_ADMIN)";
30
+ return null;
31
+ }
32
+ /**
33
+ * Politique d'attribution des scopes d'application, compilée une fois au boot.
34
+ *
35
+ * Résolution : une clé EXACTE l'emporte sur une famille, et la famille la plus
36
+ * longue sur une plus courte (`shop:admin:*` > `shop:*`). Le résultat ne dépend
37
+ * donc pas de l'ordre des clés. Un scope qu'aucune clé ne couvre reste accordé
38
+ * à tout porteur (comportement historique).
39
+ */
40
+ var ScopeGrantPolicy = class {
41
+ #exact = /* @__PURE__ */ new Map();
42
+ #families = [];
43
+ /**
44
+ * @param grants - la section `grantableScopes`, déjà validée par le schéma.
45
+ */
46
+ constructor(grants = {}) {
47
+ for (const [key, roles] of Object.entries(grants)) if (key.endsWith(WILDCARD_SUFFIX)) this.#families.push([key.slice(0, -1), roles]);
48
+ else this.#exact.set(key, roles);
49
+ this.#families.sort((a, b) => b[0].length - a[0].length);
50
+ }
51
+ /** `true` si aucune clé n'est déclarée — tout scope d'application est accordé. */
52
+ get isEmpty() {
53
+ return this.#exact.size === 0 && this.#families.length === 0;
54
+ }
55
+ /**
56
+ * Les rôles dont UN suffit pour obtenir ce scope.
57
+ *
58
+ * @param scope - un scope demandé.
59
+ * @returns les rôles de la clé la plus spécifique, ou `null` si aucune clé ne
60
+ * le couvre (scope libre).
61
+ */
62
+ rolesFor(scope) {
63
+ const exact = this.#exact.get(scope);
64
+ if (exact !== void 0) return exact;
65
+ for (const [prefix, roles] of this.#families) if (scope.startsWith(prefix)) return roles;
66
+ return null;
67
+ }
68
+ };
69
+ /**
70
+ * Les scopes demandés que ce porteur ne peut pas obtenir, avec la règle qui
71
+ * refuse chacun.
72
+ *
73
+ * Seul juge des DEUX familles, appelé par chaque émetteur de l'application
74
+ * (jeton d'accès, clé API) : la règle d'administration du framework
75
+ * ({@link refusedAdminScopes}, jugée sur les rôles PLATS du compte, comme le
76
+ * plan d'administration) et la politique de l'application
77
+ * ({@link ScopeGrantPolicy}, jugée hiérarchie de rôles comprise, comme un
78
+ * `@IsGranted`).
79
+ *
80
+ * @param requested - scopes demandés (RFC 6749 §3.3).
81
+ * @param roles - rôles du porteur, tels que son compte les porte.
82
+ * @param policy - politique compilée de `grantableScopes`.
83
+ * @param hierarchy - hiérarchie de rôles ; absente, seuls les rôles plats comptent.
84
+ * @returns les refus, dans l'ordre de la demande — vide si tout est accordé.
85
+ */
86
+ function refusedScopes(requested, roles, policy, hierarchy) {
87
+ const admin = refusedAdminScopes(requested, roles);
88
+ if (admin.length === 0 && policy.isEmpty) return [];
89
+ let reachable = null;
90
+ const out = [];
91
+ for (const scope of requested) {
92
+ if (admin.includes(scope)) {
93
+ out.push({
94
+ scope,
95
+ rule: "admin"
96
+ });
97
+ continue;
98
+ }
99
+ const allowed = policy.rolesFor(scope);
100
+ if (allowed === null) continue;
101
+ reachable ??= hierarchy ? hierarchy.reachableRoles(roles) : new Set(roles);
102
+ if (!allowed.some((role) => reachable?.has(role))) out.push({
103
+ scope,
104
+ rule: "application"
105
+ });
106
+ }
107
+ return out;
108
+ }
109
+ export { RESERVED_SCOPE_GRANT_KEYS, ScopeGrantPolicy, refusedScopes, scopeGrantKeyProblem };
@@ -3,7 +3,7 @@ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, write
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { link, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
5
5
  import process from "node:process";
6
- //#region nodefony/src/token/secretFile.ts
6
+ // nodefony/src/token/secretFile.ts
7
7
  /**
8
8
  * Écrire un SECRET sur disque — la seule implémentation du dépôt.
9
9
  *
@@ -187,5 +187,4 @@ function writeSecretSync(file, content) {
187
187
  chmodSync(file, 384);
188
188
  } catch {}
189
189
  }
190
- //#endregion
191
190
  export { MODE_SECRET, createSecretExclusive, messageNonRestreint, modeNonRestreint, modeNonRestreintAsync, readIfPresent, readIfPresentSync, writeSecret, writeSecretSync };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/token/tokenCriteria.ts
1
+ // nodefony/src/token/tokenCriteria.ts
2
2
  /**
3
3
  * Fragment de critère **portable** exprimant l'état de vie d'un jeton — la
4
4
  * traduction que les adapters SQL et Mongo partagent au lieu de la réécrire.
@@ -32,5 +32,4 @@ function tokenStatusCriteria(status, now) {
32
32
  default: return {};
33
33
  }
34
34
  }
35
- //#endregion
36
35
  export { tokenStatusCriteria };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/token/tokenFilters.ts
1
+ // nodefony/src/token/tokenFilters.ts
2
2
  /**
3
3
  * **Le vocabulaire de filtre des jetons**, en noms PUBLICS — ceux qu'un client
4
4
  * écrit dans l'URL (`?status=revoked`), jamais des noms de colonne.
@@ -68,5 +68,4 @@ const TOKEN_FACETS = {
68
68
  * Un test verrouille l'accord entre cette liste et {@link TOKEN_FACETS}.
69
69
  */
70
70
  const TOKEN_STATS_FILTERS = { subjectId: "string" };
71
- //#endregion
72
71
  export { TOKEN_FACETS, TOKEN_FILTERS, TOKEN_STATS_FILTERS };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/token/tokenSort.ts
1
+ // nodefony/src/token/tokenSort.ts
2
2
  /**
3
3
  * **Le vocabulaire de tri des jetons**, en noms PUBLICS — ceux qu'un client écrit
4
4
  * dans l'URL (`?order=createdAt:DESC`), jamais des noms de colonne.
@@ -36,5 +36,4 @@ const TOKEN_SORTABLE_FIELDS = [
36
36
  * ligne entre deux pages).
37
37
  */
38
38
  const TOKEN_DEFAULT_ORDER = [["createdAt", "DESC"], ["id", "DESC"]];
39
- //#endregion
40
39
  export { TOKEN_DEFAULT_ORDER, TOKEN_SORTABLE_FIELDS };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/token/tokenStatus.ts
1
+ // nodefony/src/token/tokenStatus.ts
2
2
  /**
3
3
  * **La** définition de l'état d'un jeton — un seul exemplaire, pour les backends
4
4
  * qui évaluent en mémoire (store mémoire, filtrage inline d'un batch `SCAN`).
@@ -31,5 +31,4 @@ function tokenStatusOf(token, now) {
31
31
  function matchesTokenStatus(token, status, now) {
32
32
  return status === void 0 || tokenStatusOf(token, now) === status;
33
33
  }
34
- //#endregion
35
34
  export { matchesTokenStatus, tokenStatusOf };
@@ -1,5 +1,5 @@
1
1
  import { MemoryTokenStore } from "./MemoryTokenStore.js";
2
- //#region nodefony/src/token/tokenStoreRegistry.ts
2
+ // nodefony/src/token/tokenStoreRegistry.ts
3
3
  const factories = /* @__PURE__ */ new Map();
4
4
  /**
5
5
  * Enregistre (ou remplace) la fabrique d'un store de jetons. Appelée par le
@@ -21,5 +21,4 @@ registerTokenStore("memory", (ctx) => {
21
21
  const days = ctx?.config?.tokenStore?.retentionRevokedDays;
22
22
  return typeof days === "number" ? new MemoryTokenStore(Date.now, days * 864e5) : new MemoryTokenStore();
23
23
  });
24
- //#endregion
25
24
  export { getTokenStoreFactory, listTokenStores, registerTokenStore };
@@ -1,5 +1,5 @@
1
1
  import { assertPageQuery } from "nodefony";
2
- //#region nodefony/src/totp/MemoryTotpSecretStore.ts
2
+ // nodefony/src/totp/MemoryTotpSecretStore.ts
3
3
  /**
4
4
  * Projette un secret en vue d'introspection — **le seul endroit** où l'on
5
5
  * décide ce qui sort d'un store TOTP en mémoire. `secretEnc` et les condensats
@@ -105,5 +105,4 @@ var MemoryTotpSecretStore = class {
105
105
  for (const secret of snapshot.secrets) this.#byUser.set(secret.userId, secret);
106
106
  }
107
107
  };
108
- //#endregion
109
108
  export { MemoryTotpSecretStore, MemoryTotpSecretStore as default, matchesTotpQuery, toTotpEnrollment };
@@ -1,6 +1,6 @@
1
1
  import { decryptSecret, deriveKey, encryptSecret, generateEphemeralKey } from "../crypto/secretCipher.js";
2
2
  import { Buffer } from "node:buffer";
3
- //#region nodefony/src/totp/totpCipher.ts
3
+ // nodefony/src/totp/totpCipher.ts
4
4
  /**
5
5
  * Chiffrement réversible du secret TOTP au repos — façade de domaine au-dessus
6
6
  * de la brique générique {@link ../crypto/secretCipher}.
@@ -26,5 +26,4 @@ const TOTP_DERIVATION = {
26
26
  function deriveTotpKey(material) {
27
27
  return deriveKey(material, TOTP_DERIVATION);
28
28
  }
29
- //#endregion
30
29
  export { decryptSecret, deriveTotpKey, encryptSecret, generateEphemeralKey };
@@ -1,5 +1,5 @@
1
1
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
- //#region nodefony/src/totp/totpCrypto.ts
2
+ // nodefony/src/totp/totpCrypto.ts
3
3
  /** Paramètres TOTP — défauts interopérables (Google Authenticator). */
4
4
  const TOTP_DEFAULTS = {
5
5
  /** Taille du secret en octets (RFC 4226 R6 : 160 bits recommandés). */
@@ -222,5 +222,4 @@ function matchRecoveryCode(presented, hashes) {
222
222
  }
223
223
  return found;
224
224
  }
225
- //#endregion
226
225
  export { TOTP_DEFAULTS, base32Decode, base32Encode, buildOtpauthUri, generateRecoveryCodes, generateTotpSecret, hashRecoveryCode, hotp, matchRecoveryCode, totpCode, totpCounter, unbiasedIndices, verifyTotp };
@@ -1,7 +1,7 @@
1
1
  import { decryptSecret, encryptSecret } from "../crypto/secretCipher.js";
2
2
  import "./totpCipher.js";
3
3
  import { base32Encode, buildOtpauthUri, generateRecoveryCodes, generateTotpSecret, hashRecoveryCode, matchRecoveryCode, verifyTotp } from "./totpCrypto.js";
4
- //#region nodefony/src/totp/totpOperations.ts
4
+ // nodefony/src/totp/totpOperations.ts
5
5
  /**
6
6
  * Démarre l'enrôlement : génère un secret aléatoire, le **chiffre** au repos et
7
7
  * l'enregistre en attente de confirmation (`confirmedAt: null`). Retourne le
@@ -125,5 +125,4 @@ async function totpStatus(deps, userId) {
125
125
  recoveryCodesRemaining: record.recoveryCodes.length
126
126
  };
127
127
  }
128
- //#endregion
129
128
  export { beginTotpEnrollment, confirmTotpEnrollment, disableTotp, totpStatus, verifyTotpLogin };
@@ -1,5 +1,5 @@
1
1
  import { MemoryTotpSecretStore } from "./MemoryTotpSecretStore.js";
2
- //#region nodefony/src/totp/totpSecretStoreRegistry.ts
2
+ // nodefony/src/totp/totpSecretStoreRegistry.ts
3
3
  const factories = /* @__PURE__ */ new Map();
4
4
  /** Enregistre (ou remplace) la fabrique d'un store de secrets TOTP. */
5
5
  function registerTotpStore(name, factory) {
@@ -14,5 +14,4 @@ function listTotpStores() {
14
14
  return [...factories.keys()];
15
15
  }
16
16
  registerTotpStore("memory", () => new MemoryTotpSecretStore());
17
- //#endregion
18
17
  export { getTotpStoreFactory, listTotpStores, registerTotpStore };