@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
@@ -2,7 +2,7 @@ import { KeycloakRealmError } from "./keycloakRealmInput.js";
2
2
  import { KeycloakAdminError } from "./keycloakAdmin.js";
3
3
  import { Command } from "nodefony";
4
4
  import path from "node:path";
5
- //#region nodefony/keycloak/security-keycloak-realm.ts
5
+ // nodefony/keycloak/security-keycloak-realm.ts
6
6
  const options = {
7
7
  helpGroup: "COMPTES ET SECRETS",
8
8
  showBanner: false,
@@ -47,7 +47,10 @@ const EXIT_CONFIG = 78;
47
47
  */
48
48
  var SecurityKeycloakRealm = class extends Command {
49
49
  constructor(cli) {
50
- super("security:keycloak:realm", "écrit le realm Keycloak déduit de la configuration", cli, options);
50
+ super("security:keycloak:realm", "écrit le realm Keycloak déduit de la configuration", cli, {
51
+ ...options,
52
+ examples: ["nodefony security:keycloak:realm --check", "nodefony security:keycloak:realm --write"]
53
+ });
51
54
  this.addOption("-w, --write [fichier]", "écrit le realm fusionné dans le fichier d'import");
52
55
  this.addOption("-c, --check [fichier]", "sort en 1 si le fichier d'import n'est plus à jour");
53
56
  this.addOption("-a, --apply", "pose le thème de connexion sur le realm existant (développement)");
@@ -124,5 +127,4 @@ var SecurityKeycloakRealm = class extends Command {
124
127
  }
125
128
  }
126
129
  };
127
- //#endregion
128
130
  export { SecurityKeycloakRealm as default };
@@ -1,5 +1,5 @@
1
1
  import { contributeKeycloak } from "../keycloak/scaffold.js";
2
- //#region nodefony/scaffold/contribute.ts
2
+ // nodefony/scaffold/contribute.ts
3
3
  /**
4
4
  * Le point de contribution de `@nodefony/security` : ce que le paquet livre à
5
5
  * une application, déclaré dans son `package.json` (`nodefony.contribute`) et
@@ -16,5 +16,4 @@ import { contributeKeycloak } from "../keycloak/scaffold.js";
16
16
  function contribute(context) {
17
17
  contributeKeycloak(context);
18
18
  }
19
- //#endregion
20
19
  export { contribute };
@@ -1,25 +1,52 @@
1
+ import { AuthenticationError } from "../errors/AuthenticationError.js";
1
2
  import { defineSecurityConfig } from "../config/defineModuleConfig.js";
3
+ import { UserToken } from "../src/token/UserToken.js";
4
+ import peekIssuer from "../src/authenticator/peekIssuer.js";
5
+ import { JwtAuthenticator } from "../src/authenticator/JwtAuthenticator.js";
6
+ import { ExternalSubjectResolver, externalSubjectOptions } from "../src/authenticator/ExternalSubjectResolver.js";
7
+ import { resolveJwtRuntime } from "../src/token/jwtRuntime.js";
2
8
  import { RemoteJwtVerifier } from "../src/token/RemoteJwtVerifier.js";
3
- import { ACCESS_TOKEN_VERIFIER, Service, canonicalIssuer } from "nodefony";
4
- //#region nodefony/service/accessTokenVerifier.ts
9
+ import { ACCESS_ACCOUNT_RESOLVER, ACCESS_TOKEN_VERIFIER, Service } from "nodefony";
10
+ // nodefony/service/accessTokenVerifier.ts
5
11
  const serviceName = ACCESS_TOKEN_VERIFIER;
6
12
  /**
7
- * Pose (ou non) le vérificateur de jetons d'accès TIERS dans le conteneur.
13
+ * Pose (ou non) le vérificateur de jetons d'accès dans le conteneur — celui
14
+ * qu'une ressource protégée (porte MCP, zone `external-jwt`) interroge.
8
15
  *
9
- * Ce service est une **décision de câblage**, pas de la cryptographie : toute la
10
- * mécanique vit dans {@link RemoteJwtVerifier}, et la doctrine de refus dans le
11
- * cœur (`nodefony/src/oauth/`). Ici, on lit la configuration et on tranche une
12
- * seule question — cette application accepte-t-elle des jetons émis ailleurs ?
16
+ * Ce service est une **décision de câblage**, pas de la cryptographie : la
17
+ * vérification d'un jeton TIERS vit dans {@link RemoteJwtVerifier}, celle d'un
18
+ * jeton de CETTE application dans {@link JwtAuthenticator}, et la doctrine de
19
+ * refus dans le cœur (`nodefony/src/oauth/`). Ici, on tranche deux questions :
20
+ * l'application signe-t-elle ses jetons, et en accepte-t-elle d'ailleurs ?
13
21
  *
14
- * **Aucun émetteur déclaré = rien n'est posé**, et c'est le comportement voulu :
15
- * une porte protégée qui ne trouve pas de vérificateur refuse de servir en le
16
- * disant (503 + CRITIC), là où un vérificateur présent mais vide refuserait
22
+ * ⭐ **Un jeton signé par cette application est TOUJOURS vérifiable**, dès que
23
+ * sa capacité JWT est active (`security.jwt.enabled`, le défaut) — sans rien
24
+ * déclarer dans `resourceServer.issuers`. Une application qui émet un jeton
25
+ * pour sa propre porte (`nodefony security:mcp:token --resource …`) doit pouvoir
26
+ * le lire : sans cette règle, la porte refusait un jeton parfaitement valide,
27
+ * et l'application était un serveur d'autorisation dont aucune ressource ne
28
+ * reconnaissait la signature. Rien de nouveau ne devient valide : l'audience
29
+ * reste exigée, et seules celles de `jwt.audiences` sont émises.
30
+ *
31
+ * 🔴 **Il est vérifié par les règles de l'authentificateur `jwt`, pas par son
32
+ * jeu de clés seul** : révocation ciblée et en masse, compte encore actif. Lu
33
+ * comme un jeton tiers, un jeton RÉVOQUÉ — ou celui d'un compte désactivé —
34
+ * ouvrait encore la porte jusqu'à son expiration.
35
+ *
36
+ * L'aiguillage se fait sur l'émetteur revendiqué (`peekIssuer`), comme dans le
37
+ * pare-feu : l'émetteur de l'application va à la vérification locale, tout
38
+ * autre à la liste fermée des émetteurs tiers. Un émetteur inconnu est refusé
39
+ * sans la moindre requête sortante.
40
+ *
41
+ * **Rien à vérifier = rien n'est posé** (capacité JWT coupée, aucun émetteur
42
+ * tiers) : une porte protégée qui ne trouve pas de vérificateur refuse alors de
43
+ * servir en le disant (503 + CRITIC), là où un vérificateur vide refuserait
17
44
  * chaque jeton un par un — même résultat pour l'appelant, diagnostic beaucoup
18
45
  * plus difficile pour l'exploitant.
19
46
  *
20
- * Le coût est nul quand la capacité n'est pas utilisée : rien n'est instancié,
21
- * aucune requête n'est faite au démarrage. La découverte des clés n'a lieu qu'au
22
- * PREMIER jeton réellement présenté, et une seule fois par émetteur.
47
+ * Le coût est nul tant qu'aucun jeton n'est présenté : aucune requête au
48
+ * démarrage, clés locales lues au premier jeton, découverte d'un émetteur tiers
49
+ * au premier jeton qui s'en réclame.
23
50
  */
24
51
  var AccessTokenVerifierService = class extends Service {
25
52
  module;
@@ -29,7 +56,7 @@ var AccessTokenVerifierService = class extends Service {
29
56
  this.module = module;
30
57
  this.kernel?.once("onBoot", () => this.#build());
31
58
  }
32
- /** Le vérificateur, ou `null` si aucun émetteur n'est déclaré. */
59
+ /** Le vérificateur posé, ou `null` si rien n'est vérifiable. */
33
60
  get verifier() {
34
61
  return this.#verifier;
35
62
  }
@@ -40,23 +67,13 @@ var AccessTokenVerifierService = class extends Service {
40
67
  } catch {
41
68
  return;
42
69
  }
70
+ const runtime = config.jwt.enabled ? resolveJwtRuntime(config.jwt) : null;
71
+ const local = runtime === null ? null : new JwtAuthenticator(this.container, runtime);
43
72
  const { issuers, ...tuning } = config.resourceServer;
44
- if (issuers.length === 0) {
45
- this.log("aucun émetteur de confiance déclaré (security.resourceServer.issuers) — les jetons émis par un serveur d'autorisation tiers ne sont pas vérifiables ; une porte protégée refusera de servir.", "DEBUG");
46
- return;
47
- }
48
- const localIssuer = config.jwt.issuer;
49
- const armed = issuers.map((trusted) => localIssuer && canonicalIssuer(trusted.issuer) === canonicalIssuer(localIssuer) ? {
50
- ...trusted,
51
- localJwks: async () => {
52
- const keystore = this.container?.get("jwtKeystore");
53
- if (!keystore) throw new Error(`l'émetteur « ${trusted.issuer} » est cette application, mais sa capacité JWT n'est pas armée (security.jwt) — aucune clé locale à présenter`);
54
- return keystore.getPublicJWKS();
55
- }
56
- } : trusted);
57
- try {
58
- this.#verifier = new RemoteJwtVerifier({
59
- issuers: armed,
73
+ let remote = null;
74
+ if (issuers.length > 0) try {
75
+ remote = new RemoteJwtVerifier({
76
+ issuers,
60
77
  timeoutMs: tuning.timeoutMs,
61
78
  cooldownMs: tuning.cooldownMs,
62
79
  cacheMaxAgeMs: tuning.cacheMaxAgeMs,
@@ -67,11 +84,61 @@ var AccessTokenVerifierService = class extends Service {
67
84
  this.log(`vérificateur de jetons non armé — ${error.message}`, "CRITIC");
68
85
  return;
69
86
  }
70
- const verify = (token, audience) => this.#verifier.verify(token, audience);
87
+ if (local === null && remote === null) {
88
+ this.log("rien à vérifier — capacité JWT coupée (security.jwt.enabled) et aucun émetteur tiers (security.resourceServer.issuers) : une porte protégée refusera de servir.", "DEBUG");
89
+ return;
90
+ }
91
+ const localIssuer = runtime?.issuer ?? null;
92
+ const verify = (token, audience) => {
93
+ if (local !== null && peekIssuer(token) === localIssuer) return local.verifyForResource(token, audience);
94
+ return remote === null ? Promise.resolve(null) : remote.verify(token, audience);
95
+ };
96
+ this.#verifier = verify;
71
97
  this.container?.set(serviceName, verify);
72
- const locaux = armed.filter((i) => "localJwks" in i).length;
73
- this.log(`vérificateur de jetons armé — ${issuers.length} émetteur(s) de confiance : ` + issuers.map((i) => i.issuer).join(", ") + (locaux > 0 ? ` (dont ${locaux} servi par les clés LOCALES, sans requête)` : ""), "INFO");
98
+ this.container?.set(ACCESS_ACCOUNT_RESOLVER, accountResolver(local, issuers.length > 0 ? new ExternalSubjectResolver(this.container, externalSubjectOptions(config)) : null));
99
+ const trusted = [...localIssuer === null ? [] : [`${localIssuer} (cette application)`], ...issuers.map((i) => i.issuer)];
100
+ this.log(`vérificateur de jetons armé — ${trusted.length} émetteur(s) de confiance : ` + trusted.join(", "), "INFO");
74
101
  }
75
102
  };
76
- //#endregion
103
+ /**
104
+ * Le résolveur de compte posé à côté du vérificateur : il retrouve, pour un
105
+ * principal que celui-ci vient d'établir, le compte de l'application qui agit.
106
+ *
107
+ * Aiguillé par l'émetteur, comme le vérificateur : un jeton maison rejoue la
108
+ * règle de la zone `jwt` (`JwtAuthenticator.resolveAccount`), un jeton tiers
109
+ * celle de la zone `external-jwt` (`ExternalSubjectResolver`). Aucune n'est
110
+ * recopiée ici. Un refus rend `null` ; une panne (annuaire non câblé,
111
+ * émetteur absent de la table de rattachement) traverse.
112
+ *
113
+ * Le jeton rendu est un `UserToken` de type `"mcp"` — hors des types humains
114
+ * que le voteur de scopes laisse passer : les scopes du jeton bornent les
115
+ * attributs `api:action`, et les rôles restent ceux du compte, comme dans une
116
+ * zone `jwt`.
117
+ */
118
+ function accountResolver(local, external) {
119
+ return async (principal) => {
120
+ let user = null;
121
+ if (local !== null) user = await local.resolveAccount(principal);
122
+ if (user === null && external !== null && principal.subject) {
123
+ if (!external.recognizes(principal.issuer)) return null;
124
+ try {
125
+ user = await external.resolve(principal.issuer, principal.subject, principal.claims);
126
+ } catch (error) {
127
+ if (error instanceof AuthenticationError) return null;
128
+ throw error;
129
+ }
130
+ }
131
+ if (user === null) return null;
132
+ const token = new UserToken("mcp").promote(user);
133
+ token.setAttribute("scopes", [...principal.scopes]);
134
+ token.setAttribute("issuer", principal.issuer);
135
+ token.setAttribute("subject", principal.subject);
136
+ if (principal.tokenId !== void 0) token.setAttribute("jti", principal.tokenId);
137
+ if (principal.claims !== void 0) token.setAttribute("claims", principal.claims);
138
+ return {
139
+ token,
140
+ user
141
+ };
142
+ };
143
+ }
77
144
  export { AccessTokenVerifierService, AccessTokenVerifierService as default };
@@ -1,11 +1,13 @@
1
+ import { ScopeGrantPolicy, refusedScopes } from "../src/token/scopeGrants.js";
1
2
  import { defineSecurityConfig } from "../config/defineModuleConfig.js";
3
+ import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY, credentialsVersionOf } from "../src/accountBinding.js";
2
4
  import { generateApiKey } from "../src/apikey/apiKeyFormat.js";
3
5
  import { recordAudit } from "../src/audit/recordAudit.js";
4
6
  import { TOKEN_FACETS } from "../src/token/tokenFilters.js";
5
7
  import { ApiKeyError } from "../errors/ApiKeyError.js";
6
8
  import { Service, countFacets } from "nodefony";
7
9
  import { randomUUID } from "node:crypto";
8
- //#region nodefony/service/apiKeys.ts
10
+ // nodefony/service/apiKeys.ts
9
11
  const serviceName = "apiKeys";
10
12
  const MAX_NAME_LEN = 100;
11
13
  const MS_PER_DAY = 864e5;
@@ -32,6 +34,8 @@ var ApiKeyService = class extends Service {
32
34
  #defaultExpiryDays = 90;
33
35
  #maxPerSubject = 100;
34
36
  #allowedScopes = null;
37
+ /** `grantableScopes` compilé au boot ; `null` tant que la config n'est pas lue. */
38
+ #scopeGrants = null;
35
39
  constructor(module) {
36
40
  super(serviceName, module.container, module.notificationsCenter, module.options);
37
41
  this.module = module;
@@ -54,6 +58,7 @@ var ApiKeyService = class extends Service {
54
58
  this.#defaultExpiryDays = ak.defaultExpiryDays;
55
59
  this.#maxPerSubject = ak.maxPerSubject;
56
60
  this.#allowedScopes = ak.allowedScopes;
61
+ this.#scopeGrants = new ScopeGrantPolicy(config.grantableScopes);
57
62
  this.log(`api keys ready — prefix "${ak.prefix}_", max ${ak.maxPerSubject}/porteur`, "DEBUG");
58
63
  }
59
64
  /** `true` si les clés API sont activées en config. */
@@ -83,17 +88,31 @@ var ApiKeyService = class extends Service {
83
88
  * Émet une nouvelle clé API pour un porteur — renvoie sa vue publique **+ le
84
89
  * token en clair** (affiché une seule fois).
85
90
  *
91
+ * Un scope que le porteur ne peut pas OBTENIR — réservé par
92
+ * `grantableScopes` à des rôles qu'il n'a pas, ou `admin:*` sans le rôle
93
+ * d'administration — refuse la création entière : une clé émise avec moins
94
+ * que demandé surprendrait sans qu'aucune réponse ne le dise. Même juge que
95
+ * l'émission des jetons ({@link refusedScopes}).
96
+ *
97
+ * @param roles - rôles du porteur tels que son compte les porte ; omis =
98
+ * aucun rôle, donc tout scope réservé est refusé (fail-closed).
86
99
  * @throws ApiKeyError 400 — nom vide/trop long, scope hors catalogue, expiry invalide.
100
+ * @throws ApiKeyError 403 — scope réservé à des rôles que le porteur n'a pas.
87
101
  * @throws ApiKeyError 409 — plafond `maxPerSubject` atteint.
88
- * @throws ApiKeyError 503 — store indisponible.
102
+ * @throws ApiKeyError 403 — aucun compte sous cet identifiant (la clé est liée
103
+ * à l'`id` immuable du compte, lu dans l'annuaire).
104
+ * @throws ApiKeyError 503 — store ou annuaire `users` indisponible.
89
105
  */
90
- async createForSubject(subjectId, subjectType, opts) {
106
+ async createForSubject(subjectId, subjectType, opts, roles = []) {
91
107
  const store = this.#resolveStore();
92
108
  const name = this.#normalizeName(opts.name);
93
109
  const scopes = this.#normalizeScopes(opts.scopes);
110
+ const refusals = refusedScopes(scopes, roles, this.#scopeGrants ?? new ScopeGrantPolicy(), this.container?.get("roleHierarchy") ?? null);
111
+ if (refusals.length > 0) throw new ApiKeyError(`scope not grantable to this account: ${refusals.map((r) => r.scope).join(", ")}`, 403);
94
112
  const now = Date.now();
95
113
  const expiresAt = this.#resolveExpiry(opts.expiresInDays, now);
96
114
  if ((await store.findBySubject(subjectId)).filter((r) => r.kind === "pat" && this.#isActive(r, now)).length >= this.#maxPerSubject) throw new ApiKeyError(`API key limit reached (${this.#maxPerSubject})`, 409);
115
+ const holder = await this.#resolveHolder(subjectId);
97
116
  const generated = generateApiKey(this.#prefix);
98
117
  const record = {
99
118
  id: randomUUID(),
@@ -119,7 +138,10 @@ var ApiKeyService = class extends Service {
119
138
  lastUsedUserAgent: null,
120
139
  revokedAt: null,
121
140
  revokedReason: null,
122
- metadata: {}
141
+ metadata: {
142
+ [ACCOUNT_ID_KEY]: holder.id,
143
+ [CREDENTIALS_VERSION_KEY]: credentialsVersionOf(holder)
144
+ }
123
145
  };
124
146
  await store.put(record);
125
147
  this.log(`api key created — id=${record.id} subject=${subjectId} scopes=[${scopes.join(",")}]`, "INFO");
@@ -253,6 +275,25 @@ var ApiKeyService = class extends Service {
253
275
  });
254
276
  return true;
255
277
  }
278
+ /**
279
+ * Le compte qui reçoit la clé — son identifiant IMMUABLE et sa version
280
+ * d'identifiants sont la liaison qu'elle portera, contrôlée à chaque
281
+ * présentation (`isBoundToAccount`). Lu dans
282
+ * l'annuaire, jamais reçu de l'appelant : la clé appartient au compte qui
283
+ * existe MAINTENANT sous cet identifiant.
284
+ *
285
+ * @throws ApiKeyError 503 — annuaire `users` absent (câblage).
286
+ * @throws ApiKeyError 403 — aucun compte sous cet identifiant.
287
+ */
288
+ async #resolveHolder(subjectId) {
289
+ const users = this.get("users");
290
+ if (!users) throw new ApiKeyError("user directory unavailable", 503);
291
+ try {
292
+ return await users.loadUserByIdentifier(subjectId);
293
+ } catch {
294
+ throw new ApiKeyError("API key holder unknown", 403);
295
+ }
296
+ }
256
297
  #resolveStore() {
257
298
  if (this.#store === null) {
258
299
  const store = this.get("tokenStore");
@@ -306,5 +347,4 @@ var ApiKeyService = class extends Service {
306
347
  };
307
348
  }
308
349
  };
309
- //#endregion
310
350
  export { ApiKeyService, ApiKeyService as default };
@@ -1,8 +1,8 @@
1
1
  import { defineSecurityConfig } from "../config/defineModuleConfig.js";
2
2
  import { getAuditStoreFactory, listAuditStores } from "../src/audit/auditStoreRegistry.js";
3
- import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Service, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore, runNeedsExternalServices } from "nodefony";
3
+ import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Service, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore, storeConnectivity } from "nodefony";
4
4
  import { randomBytes } from "node:crypto";
5
- //#region nodefony/service/auditService.ts
5
+ // nodefony/service/auditService.ts
6
6
  const serviceName = "auditService";
7
7
  const GC_INTERVAL_MS = 36e5;
8
8
  /**
@@ -50,7 +50,7 @@ var AuditService = class extends Service {
50
50
  let storeName = config.audit.store;
51
51
  let reason = `store explicitement configuré ("${storeName}")`;
52
52
  if (storeName === AUTO_STORE) {
53
- const auto = resolveAutoStore("durable", this.kernel?.infra ?? EMPTY_INFRA, listAuditStores(), "memory", runNeedsExternalServices(this.kernel));
53
+ const auto = resolveAutoStore("durable", this.kernel?.infra ?? EMPTY_INFRA, listAuditStores(), "memory", storeConnectivity(this.kernel));
54
54
  storeName = auto.store;
55
55
  reason = auto.reason;
56
56
  this.log(`audit.store "auto" → "${storeName}" (${auto.reason})`, "INFO");
@@ -62,7 +62,7 @@ var AuditService = class extends Service {
62
62
  this.log(`${msg} — audit désactivé (journal de sécurité non collecté)`, "CRITIC");
63
63
  return;
64
64
  }
65
- if (storeName === "memory" && this.kernel?.environment === "production") this.log("audit.store \"memory\" en PRODUCTION — journal de sécurité volatil et per-pod : perdu au redémarrage, invisible des autres pods, rétention de conformité impossible. " + durableStoreRemedy(runNeedsExternalServices(this.kernel)), "WARNING");
65
+ if (storeName === "memory" && this.kernel?.environment === "production") this.log("audit.store \"memory\" en PRODUCTION — journal de sécurité volatil et per-pod : perdu au redémarrage, invisible des autres pods, rétention de conformité impossible. " + durableStoreRemedy(storeConnectivity(this.kernel)), "WARNING");
66
66
  this.#enabled = true;
67
67
  this.#idPrefix = randomBytes(4).toString("hex");
68
68
  this.#store = factory({
@@ -142,5 +142,4 @@ var AuditService = class extends Service {
142
142
  return this.#enabled;
143
143
  }
144
144
  };
145
- //#endregion
146
145
  export { AuditService as default };
@@ -2,13 +2,14 @@ import { securityConfigSchema } from "../config/config.js";
2
2
  import { InsecureTransportError } from "../errors/InsecureTransportError.js";
3
3
  import { AuthenticationError } from "../errors/AuthenticationError.js";
4
4
  import { ThrottledError } from "../errors/ThrottledError.js";
5
- import { resolveSessionIdentity } from "../src/sessionIdentity.js";
5
+ import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY, credentialsVersionOf } from "../src/accountBinding.js";
6
+ import { resolveBoundSessionIdentity, resolveSessionIdentity } from "../src/sessionIdentity.js";
6
7
  import { recordAudit } from "../src/audit/recordAudit.js";
7
8
  import { readAuditContext } from "../src/audit/readAuditContext.js";
8
9
  import { amrForFactor } from "../src/sessionAuthentication.js";
9
10
  import { LOGIN_PAGE_SKIN_SPECS, MFA_METHOD_TOTP, SESSION_AMR_KEY, SESSION_AUTH_AT_KEY, Service } from "nodefony";
10
11
  import { requestUser } from "@nodefony/user";
11
- //#region nodefony/service/authFlow.ts
12
+ // nodefony/service/authFlow.ts
12
13
  const MFA_METHODS = Object.freeze([MFA_METHOD_TOTP]);
13
14
  const serviceName = "authFlow";
14
15
  const INVALID_CREDENTIALS = "Invalid credentials";
@@ -127,7 +128,7 @@ var AuthFlow = class extends Service {
127
128
  methods: MFA_METHODS
128
129
  };
129
130
  }
130
- await this.#openSession(context, user.identifier, "password");
131
+ await this.#openSession(context, user, "password");
131
132
  recordAudit(this.container, {
132
133
  category: "auth",
133
134
  action: "login.success",
@@ -157,7 +158,7 @@ var AuthFlow = class extends Service {
157
158
  async establishSessionFor(context, identifier, reason = "federated") {
158
159
  if (typeof identifier !== "string" || identifier.length === 0) throw new AuthenticationError(INVALID_CREDENTIALS);
159
160
  const user = await resolveSessionIdentity(this.#resolveUsers(), identifier);
160
- await this.#openSession(context, user.identifier, reason);
161
+ await this.#openSession(context, user, reason);
161
162
  recordAudit(this.container, {
162
163
  category: "auth",
163
164
  action: "login.success",
@@ -292,7 +293,7 @@ var AuthFlow = class extends Service {
292
293
  const current = requestUser(identifier);
293
294
  if (current !== null) return toSafeUser(current);
294
295
  try {
295
- return toSafeUser(await resolveSessionIdentity(this.#resolveUsers(), identifier));
296
+ return toSafeUser(await resolveBoundSessionIdentity(this.#resolveUsers(), identifier, context.session?.getMetaBag(ACCOUNT_ID_KEY), context.session?.getMetaBag(CREDENTIALS_VERSION_KEY)));
296
297
  } catch (error) {
297
298
  if (error instanceof AuthenticationError) return null;
298
299
  throw error;
@@ -350,7 +351,7 @@ var AuthFlow = class extends Service {
350
351
  }
351
352
  return this.#loginPage;
352
353
  }
353
- async #openSession(context, identifier, factor) {
354
+ async #openSession(context, user, factor) {
354
355
  const session = context.session ?? await this.#resolveSessions().start(context);
355
356
  if (session === null) {
356
357
  this.log("login: session service returned no session", "ERROR");
@@ -370,7 +371,9 @@ var AuthFlow = class extends Service {
370
371
  } catch {}
371
372
  session.setMetaBag(SESSION_AUTH_AT_KEY, Date.now());
372
373
  session.setMetaBag(SESSION_AMR_KEY, amrForFactor(factor));
373
- await session.save(identifier);
374
+ session.setMetaBag(ACCOUNT_ID_KEY, user.id);
375
+ session.setMetaBag(CREDENTIALS_VERSION_KEY, credentialsVersionOf(user));
376
+ await session.save(user.identifier);
374
377
  }
375
378
  #resolveSessions() {
376
379
  if (this.#sessions === null) {
@@ -428,5 +431,4 @@ function toSafeUser(user) {
428
431
  roles: [...user.roles]
429
432
  };
430
433
  }
431
- //#endregion
432
434
  export { AuthFlow, AuthFlow as default };
@@ -2,7 +2,7 @@ import { recordAudit } from "../src/audit/recordAudit.js";
2
2
  import "../contracts/IAccessVoter.js";
3
3
  import { listVoterFactories } from "../src/voter/voterRegistry.js";
4
4
  import { Service, logColor } from "nodefony";
5
- //#region nodefony/service/authorization.ts
5
+ // nodefony/service/authorization.ts
6
6
  const serviceName = "authorization";
7
7
  /**
8
8
  * Service d'autorisation Nodefony (niveau C, P6 J6) — décide d'un accès via un
@@ -90,5 +90,4 @@ function describeSubject(subject) {
90
90
  if (subject === null || subject === void 0) return String(subject);
91
91
  return subject.constructor?.name ?? typeof subject;
92
92
  }
93
- //#endregion
94
93
  export { Authorization, Authorization as default };
@@ -1,4 +1,4 @@
1
- //#region nodefony/service/cors.ts
1
+ // nodefony/service/cors.ts
2
2
  /**
3
3
  * Politique CORS de Nodefony — Same-Origin Policy assouplie côté serveur
4
4
  * (Fetch Standard / W3C CORS protocol). Logique PURE et synchrone : décide les
@@ -77,5 +77,4 @@ var Cors = class {
77
77
  return h;
78
78
  }
79
79
  };
80
- //#endregion
81
80
  export { Cors, Cors as default };
@@ -1,5 +1,5 @@
1
1
  import { CsrfError } from "../errors/CsrfError.js";
2
- //#region nodefony/service/csrf.ts
2
+ // nodefony/service/csrf.ts
3
3
  /**
4
4
  * Méthodes HTTP « sûres » (RFC 9110 §9.2.1) — sans effet de bord par contrat,
5
5
  * donc hors du vecteur CSRF. Tout le reste (POST/PUT/PATCH/DELETE…) est traité
@@ -93,5 +93,4 @@ var Csrf = class {
93
93
  }
94
94
  }
95
95
  };
96
- //#endregion
97
96
  export { Csrf, Csrf as default };
@@ -5,6 +5,7 @@ import { LoginThrottler } from "../src/throttle/LoginThrottler.js";
5
5
  import { InsecureTransportError } from "../errors/InsecureTransportError.js";
6
6
  import { CredentialTransportPolicy, shouldEnforceCredentialTransport } from "../src/transport/CredentialTransportPolicy.js";
7
7
  import { RoleHierarchyWalker } from "../src/RoleHierarchyWalker.js";
8
+ import { rolesWithin } from "../src/rolesWithin.js";
8
9
  import { mergeCspFragments } from "../src/csp.js";
9
10
  import { CsrfTokenManager } from "../src/csrfToken.js";
10
11
  import { CsrfError } from "../errors/CsrfError.js";
@@ -22,10 +23,10 @@ import { recordAudit } from "../src/audit/recordAudit.js";
22
23
  import { readAuditContext } from "../src/audit/readAuditContext.js";
23
24
  import { SECURITY_AUDIT_CHANNEL, createAuditBridge } from "../src/audit/auditBridge.js";
24
25
  import { findZonesOpenToSignup } from "../src/openToSignup.js";
25
- import { RequestContext, Service, logColor } from "nodefony";
26
+ import { ROLES_WITHIN, RequestContext, Service, logColor } from "nodefony";
26
27
  import { randomBytes } from "node:crypto";
27
28
  import { encoderFromConfig } from "@nodefony/user";
28
- //#region nodefony/service/firewall.ts
29
+ // nodefony/service/firewall.ts
29
30
  const serviceName = "firewall";
30
31
  /**
31
32
  * Réponse partagée de {@link Firewall.publishedProtectedResources} quand il n'y
@@ -108,6 +109,7 @@ var Firewall = class extends Service {
108
109
  }
109
110
  this.#roleHierarchy = new RoleHierarchyWalker(this.#config.roleHierarchy);
110
111
  this.container?.set("roleHierarchy", this.#roleHierarchy);
112
+ this.container?.set(ROLES_WITHIN, rolesWithin(() => this.container?.get("users") ?? null, this.#roleHierarchy));
111
113
  this.#provisionSharedServices(this.#config);
112
114
  if (this.#config.csrf.enabled) {
113
115
  this.#csrf = new Csrf(this.#config.csrf, [...this.#config.csrf.trustedOrigins, ...this.#config.cors.origins]);
@@ -144,7 +146,7 @@ var Firewall = class extends Service {
144
146
  pattern: area.pattern,
145
147
  host: area.host
146
148
  } : { pattern: area.pattern };
147
- realtime.useAuthenticator(matcher, new FirewallRealtimeAuthenticator(() => this.container?.get("tokenStore") ?? null));
149
+ realtime.useAuthenticator(matcher, new FirewallRealtimeAuthenticator(() => this.container?.get("tokenStore") ?? null, () => this.container?.get("users") ?? null));
148
150
  wired = true;
149
151
  }
150
152
  const configRules = (this.#config?.realtimeChannels ?? []).map((r) => ({
@@ -797,5 +799,4 @@ var Firewall = class extends Service {
797
799
  return super.log(pci, severity, msgid, msg);
798
800
  }
799
801
  };
800
- //#endregion
801
802
  export { Firewall, Firewall as default };
@@ -6,7 +6,7 @@ import { KeycloakAdminClient, keycloakAdminTarget } from "../keycloak/keycloakAd
6
6
  import { Service } from "nodefony";
7
7
  import path from "node:path";
8
8
  import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
9
- //#region nodefony/service/keycloak.ts
9
+ // nodefony/service/keycloak.ts
10
10
  const serviceName = "keycloak";
11
11
  /** Où le compose d'une application importe ses realms (chemin natif). */
12
12
  const IMPORT_DIR = path.join("docker", "keycloak", "import");
@@ -205,5 +205,4 @@ var KeycloakService = class extends Service {
205
205
  return this.admin(options.provider).applyLoginTheme(options.skin ?? this.config().loginPage.skin);
206
206
  }
207
207
  };
208
- //#endregion
209
208
  export { KeycloakService, KeycloakService as default };
@@ -7,7 +7,7 @@ import { generateCodeVerifier, generateState } from "../src/oauth/oauth2Client.j
7
7
  import { getOAuthProviderFactory, listOAuthProviders, oauthProviderRequiresIssuer } from "../src/oauth/oauthProviderRegistry.js";
8
8
  import { diagnoseOAuthProvider } from "../src/oauth/providerDiagnosis.js";
9
9
  import { BootConfigurationError, LOGIN_PAGE_PATH, Service, canonicalIssuer } from "nodefony";
10
- //#region nodefony/service/oauth2.ts
10
+ // nodefony/service/oauth2.ts
11
11
  const serviceName = "oauth2";
12
12
  /**
13
13
  * Clé de session de l'indice de déconnexion. Lue et écrite par CE service
@@ -133,6 +133,31 @@ function checkProviderIssuer(name, issuer) {
133
133
  }
134
134
  }
135
135
  /**
136
+ * Annonce qu'un annuaire accorde des rôles de PLATEFORME, à la sévérité de
137
+ * l'environnement — ou `null` quand il n'y a rien à dire.
138
+ *
139
+ * L'ouverture est ÉCRITE (`allowPlatformRoles`), donc voulue : l'annonce ne
140
+ * la conteste pas, elle la rappelle. Le risque — un annuaire compromis
141
+ * administre l'instance — n'est réel qu'en production : là, `WARNING` ;
142
+ * ailleurs le même fait est dit en `INFO`, sans sonner une alarme à chaque
143
+ * démarrage d'un décor de développement.
144
+ *
145
+ * @param name - nom du fournisseur (`oauth2.providers.<name>`).
146
+ * @param allowed - `allowPlatformRoles` tel que déclaré.
147
+ * @param granted - rôles de l'application que la table accorde.
148
+ * @param production - l'application tourne en production.
149
+ * @returns le message et sa sévérité, ou `null`.
150
+ */
151
+ function platformRolesNotice(name, allowed, granted, production) {
152
+ if (allowed !== true) return null;
153
+ const platform = [...new Set(granted.filter(isPlatformRole))];
154
+ if (platform.length === 0) return null;
155
+ return {
156
+ message: `oauth2 "${name}" : l'annuaire accorde des rôles de PLATEFORME (${platform.join(", ")}) — son administrateur administre cette instance (allowPlatformRoles)`,
157
+ severity: production ? "WARNING" : "INFO"
158
+ };
159
+ }
160
+ /**
136
161
  * Claims du jeton d'ACCÈS, quand c'est un JWT — `null` sinon (jeton opaque).
137
162
  *
138
163
  * Keycloak pose les rôles dans le jeton d'accès, et seulement là par défaut
@@ -209,10 +234,8 @@ var OAuth2Service = class extends Service {
209
234
  checkProviderIssuer(name, provider?.issuer);
210
235
  const mapping = provider === void 0 ? null : compileProviderRoleMapping(provider);
211
236
  if (mapping !== null) {
212
- if (provider?.allowPlatformRoles === true) {
213
- const platform = Object.values(mapping.table).filter(isPlatformRole);
214
- if (platform.length > 0) this.log(`oauth2 "${name}" : l'annuaire accorde des rôles de PLATEFORME (${[...new Set(platform)].join(", ")}) — son administrateur administre cette instance (allowPlatformRoles)`, "WARNING");
215
- }
237
+ const notice = platformRolesNotice(name, provider?.allowPlatformRoles, Object.values(mapping.table), this.kernel?.environment === "production");
238
+ if (notice !== null) this.log(notice.message, notice.severity);
216
239
  this.#roleMappings ??= Object.create(null);
217
240
  this.#roleMappings[name] = mapping;
218
241
  }
@@ -590,5 +613,4 @@ var OAuth2Service = class extends Service {
590
613
  return this.#config;
591
614
  }
592
615
  };
593
- //#endregion
594
- export { OAuth2Service, OAuth2Service as default, checkProviderIssuer, oauthDisplayLabel };
616
+ export { OAuth2Service, OAuth2Service as default, checkProviderIssuer, oauthDisplayLabel, platformRolesNotice };
@@ -1,5 +1,5 @@
1
1
  import { mergeCspFragments } from "../src/csp.js";
2
- //#region nodefony/service/securityHeaders.ts
2
+ // nodefony/service/securityHeaders.ts
3
3
  /** Marqueur substitué par le nonce CSP de la requête (cf `Context.cspNonce`). */
4
4
  const NONCE_PLACEHOLDER = "{{nonce}}";
5
5
  /**
@@ -87,5 +87,4 @@ var SecurityHeaders = class {
87
87
  return this.#cspParts !== null ? merged.split(NONCE_PLACEHOLDER).join(nonce) : merged.replace(/\s*'nonce-\{\{nonce\}\}'/g, "");
88
88
  }
89
89
  };
90
- //#endregion
91
90
  export { SecurityHeaders, SecurityHeaders as default };