@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,8 +1,10 @@
1
1
  import { AuthenticationError } from "../../errors/AuthenticationError.js";
2
+ import { loadBoundAccount } from "../accountBinding.js";
2
3
  import { UserToken } from "../token/UserToken.js";
3
4
  import { bearerToken } from "./bearer.js";
4
5
  import peekIssuer from "./peekIssuer.js";
5
- //#region nodefony/src/authenticator/JwtAuthenticator.ts
6
+ import { canonicalIssuer } from "nodefony";
7
+ // nodefony/src/authenticator/JwtAuthenticator.ts
6
8
  const COMPACT_JWS = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/;
7
9
  const INVALID_TOKEN = "Invalid token";
8
10
  /**
@@ -82,6 +84,57 @@ var JwtAuthenticator = class {
82
84
  async authenticate(token) {
83
85
  const raw = token.getCredentials();
84
86
  if (typeof raw !== "string" || raw.length === 0) throw new AuthenticationError(INVALID_TOKEN);
87
+ const { user, payload } = await this.#verify(raw, this.#runtime.audiences);
88
+ return this.#promote(token, user, payload);
89
+ }
90
+ /**
91
+ * Vérifie un jeton émis par CETTE application pour une ressource protégée
92
+ * nommée par l'appelant — la porte d'une ressource (RFC 8707), pas une zone.
93
+ *
94
+ * Les MÊMES règles que {@link authenticate}, appliquées par la même fonction :
95
+ * signature par le keyset local, `iss`, `typ: at+jwt`, révocation ciblée
96
+ * (`jti`) et en masse (`invalidBefore`), compte encore actif. Seule
97
+ * l'audience diffère : c'est celle de la ressource qui appelle, et elle est
98
+ * exigée. Un jeton révoqué, ou celui d'un compte désactivé, n'ouvre donc pas
99
+ * plus une porte MCP qu'une route — ce que la vérification par le seul jeu de
100
+ * clés publiques laissait passer jusqu'à l'expiration.
101
+ *
102
+ * @param raw - le jeton présenté, brut
103
+ * @param audience - l'URI canonique de la ressource qui le reçoit
104
+ * @returns le principal établi, ou `null` pour tout refus (contrat
105
+ * `IAccessTokenVerifier` : un refus n'est jamais une exception)
106
+ * @throws Error si le câblage manque (keystore, store, annuaire) — la porte
107
+ * le traduit en `503`, jamais en jeton accepté
108
+ */
109
+ async verifyForResource(raw, audience) {
110
+ let verified;
111
+ try {
112
+ verified = await this.#verify(raw, audience);
113
+ } catch (error) {
114
+ if (error instanceof AuthenticationError) return null;
115
+ throw error;
116
+ }
117
+ const { payload } = verified;
118
+ const scope = payload.scope;
119
+ return {
120
+ issuer: this.#resourceIssuer(),
121
+ subject: payload.sub,
122
+ scopes: typeof scope === "string" && scope.length > 0 ? scope.split(" ") : [],
123
+ expiresAt: payload.exp,
124
+ issuedAt: payload.iat,
125
+ tokenId: payload.jti,
126
+ claims: payload
127
+ };
128
+ }
129
+ /**
130
+ * La vérification entière d'un jeton maison — UNE implémentation, pour la
131
+ * zone ({@link authenticate}) comme pour la ressource
132
+ * ({@link verifyForResource}).
133
+ *
134
+ * @throws AuthenticationError pour tout refus (message uniforme)
135
+ * @throws Error si le câblage manque
136
+ */
137
+ async #verify(raw, audience) {
85
138
  const jose = this.#jose ??= await import("jose");
86
139
  const getKey = await this.#ensureGetKey(jose);
87
140
  let payload;
@@ -89,7 +142,7 @@ var JwtAuthenticator = class {
89
142
  payload = (await jose.jwtVerify(raw, getKey, {
90
143
  algorithms: ["EdDSA"],
91
144
  issuer: this.#runtime.issuer,
92
- audience: this.#runtime.audiences,
145
+ audience,
93
146
  typ: "at+jwt"
94
147
  })).payload;
95
148
  } catch {
@@ -104,8 +157,31 @@ var JwtAuthenticator = class {
104
157
  if (invalidBefore !== null) {
105
158
  if ((typeof payload.iat === "number" ? payload.iat * 1e3 : 0) < invalidBefore) throw new AuthenticationError(INVALID_TOKEN);
106
159
  }
107
- const user = await this.#resolveUserOrReject(sub);
108
- return this.#promote(token, user, payload);
160
+ const user = await loadBoundAccount(this.#resolveUserProvider(), sub, payload["uid"], payload["cv"]);
161
+ if (user === null) throw new AuthenticationError(INVALID_TOKEN);
162
+ return {
163
+ user,
164
+ payload
165
+ };
166
+ }
167
+ /**
168
+ * Recharge le compte que désigne un principal établi par
169
+ * {@link verifyForResource} — pour une ressource protégée qui fait agir un
170
+ * outil au nom du propriétaire du jeton.
171
+ *
172
+ * La MÊME règle que la zone ({@link loadBoundAccount}), rejouée sur les
173
+ * revendications du principal : le vérificateur ne garde pas le compte qu'il
174
+ * a lu, et une seconde lecture de l'annuaire est le coût accepté d'un outil
175
+ * qui la demande — les autres ne la paient pas.
176
+ *
177
+ * @param principal - principal d'un jeton émis par CETTE application
178
+ * @returns le compte, ou `null` s'il n'est pas (ou plus) celui du jeton
179
+ * @throws Error si l'annuaire des comptes n'est pas câblé
180
+ */
181
+ resolveAccount(principal) {
182
+ const subject = principal.subject;
183
+ if (principal.issuer !== this.#resourceIssuer() || typeof subject !== "string" || subject.length === 0) return Promise.resolve(null);
184
+ return loadBoundAccount(this.#resolveUserProvider(), subject, principal.claims?.["uid"], principal.claims?.["cv"]);
109
185
  }
110
186
  /** Slot audit (J4b). */
111
187
  onSuccess(_context, _token) {
@@ -119,6 +195,17 @@ var JwtAuthenticator = class {
119
195
  challenge() {
120
196
  return "Bearer";
121
197
  }
198
+ /**
199
+ * L'émetteur que portent les principaux de {@link verifyForResource} — et
200
+ * donc celui qu'un principal doit porter pour être un jeton maison.
201
+ */
202
+ #resourceIssuer() {
203
+ try {
204
+ return canonicalIssuer(this.#runtime.issuer);
205
+ } catch {
206
+ return this.#runtime.issuer;
207
+ }
208
+ }
122
209
  /** Construit (une fois) le résolveur de clé par `kid` depuis le JWKS public. */
123
210
  async #ensureGetKey(jose) {
124
211
  if (this.#getKey !== null) return this.#getKey;
@@ -137,17 +224,6 @@ var JwtAuthenticator = class {
137
224
  ut.setAttribute("issuer", payload.iss);
138
225
  return ut;
139
226
  }
140
- async #resolveUserOrReject(sub) {
141
- const provider = this.#resolveUserProvider();
142
- let user;
143
- try {
144
- user = await provider.loadUserByIdentifier(sub);
145
- } catch {
146
- throw new AuthenticationError(INVALID_TOKEN);
147
- }
148
- if (!user.isActive() || user.isLocked()) throw new AuthenticationError(INVALID_TOKEN);
149
- return user;
150
- }
151
227
  #resolveKeystore() {
152
228
  if (this.#keystore === null) {
153
229
  const ks = this.#container.get("jwtKeystore");
@@ -173,5 +249,4 @@ var JwtAuthenticator = class {
173
249
  return this.#userProvider;
174
250
  }
175
251
  };
176
- //#endregion
177
252
  export { JwtAuthenticator, JwtAuthenticator as default };
@@ -1,7 +1,8 @@
1
1
  import { AuthenticationError } from "../../errors/AuthenticationError.js";
2
- import { resolveSessionIdentity } from "../sessionIdentity.js";
2
+ import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY } from "../accountBinding.js";
3
+ import { resolveBoundSessionIdentity } from "../sessionIdentity.js";
3
4
  import { UserToken } from "../token/UserToken.js";
4
- //#region nodefony/src/authenticator/SessionAuthenticator.ts
5
+ // nodefony/src/authenticator/SessionAuthenticator.ts
5
6
  /**
6
7
  * Authentification par **session serveur** (cookie opaque, modèle BFF) — la
7
8
  * preuve des requêtes qui SUIVENT le login (`AuthFlow.login`, qui a déjà posé
@@ -59,7 +60,11 @@ var SessionAuthenticator = class {
59
60
  }
60
61
  /** Extrait l'identifiant du blob de session (jamais de secret en jeu). */
61
62
  createToken(context) {
62
- const credentials = { identifier: context.session?.user ?? "" };
63
+ const credentials = {
64
+ identifier: context.session?.user ?? "",
65
+ accountId: context.session?.getMetaBag(ACCOUNT_ID_KEY),
66
+ credentialsVersion: context.session?.getMetaBag(CREDENTIALS_VERSION_KEY)
67
+ };
63
68
  return Promise.resolve(new UserToken(this.name, credentials));
64
69
  }
65
70
  /**
@@ -74,7 +79,7 @@ var SessionAuthenticator = class {
74
79
  const credentials = token.getCredentials();
75
80
  if (!credentials?.identifier) throw new AuthenticationError("Invalid session");
76
81
  const provider = this.#provider ??= this.#resolveProvider();
77
- const user = await resolveSessionIdentity(provider, credentials.identifier);
82
+ const user = await resolveBoundSessionIdentity(provider, credentials.identifier, credentials.accountId, credentials.credentialsVersion);
78
83
  return token.promote(user);
79
84
  }
80
85
  /**
@@ -90,5 +95,4 @@ var SessionAuthenticator = class {
90
95
  return Promise.resolve();
91
96
  }
92
97
  };
93
- //#endregion
94
98
  export { SessionAuthenticator, SessionAuthenticator as default };
@@ -1,7 +1,7 @@
1
1
  import { AuthenticationError } from "../../errors/AuthenticationError.js";
2
2
  import { ThrottledError } from "../../errors/ThrottledError.js";
3
3
  import { UserToken } from "../token/UserToken.js";
4
- //#region nodefony/src/authenticator/UserPasswordAuthenticator.ts
4
+ // nodefony/src/authenticator/UserPasswordAuthenticator.ts
5
5
  const BASIC_SCHEME = /^basic\s+/i;
6
6
  const INVALID_CREDENTIALS = "Invalid credentials";
7
7
  /**
@@ -101,5 +101,4 @@ var UserPasswordAuthenticator = class {
101
101
  return "Basic realm=\"nodefony\", charset=\"UTF-8\"";
102
102
  }
103
103
  };
104
- //#endregion
105
104
  export { UserPasswordAuthenticator, UserPasswordAuthenticator as default };
@@ -1,12 +1,12 @@
1
- import { compileProviderRoleMapping } from "../oauth/providerRoles.js";
2
1
  import { AnonymousAuthenticator } from "./AnonymousAuthenticator.js";
3
2
  import { SessionAuthenticator } from "./SessionAuthenticator.js";
4
3
  import { UserPasswordAuthenticator } from "./UserPasswordAuthenticator.js";
5
4
  import { JwtAuthenticator } from "./JwtAuthenticator.js";
6
5
  import { ApiKeyAuthenticator } from "./ApiKeyAuthenticator.js";
6
+ import { externalSubjectOptions } from "./ExternalSubjectResolver.js";
7
7
  import { ExternalJwtAuthenticator } from "./ExternalJwtAuthenticator.js";
8
8
  import { resolveJwtRuntime } from "../token/jwtRuntime.js";
9
- //#region nodefony/src/authenticator/authenticatorRegistry.ts
9
+ // nodefony/src/authenticator/authenticatorRegistry.ts
10
10
  const factories = /* @__PURE__ */ new Map();
11
11
  /**
12
12
  * Enregistre (ou remplace) la fabrique d'un authenticator. Appelé par les
@@ -45,20 +45,7 @@ registerAuthenticatorFactory("jwt", ({ container, config }) => {
45
45
  return new JwtAuthenticator(container, resolveJwtRuntime(config.jwt));
46
46
  });
47
47
  registerAuthenticatorFactory("external-jwt", ({ container, config }) => {
48
- const rs = config.resourceServer;
49
- return new ExternalJwtAuthenticator(container, {
50
- issuers: rs.issuers.map((i) => ({
51
- issuer: i.issuer,
52
- subjectMapping: i.subjectMapping
53
- })),
54
- subjectPolicy: rs.subjectPolicy,
55
- ephemeralRoles: rs.ephemeralRoles,
56
- oauthProviders: Object.entries(config.oauth2.providers).flatMap(([name, p]) => p.issuer ? [{
57
- name,
58
- issuer: p.issuer,
59
- roleMapping: compileProviderRoleMapping(p)
60
- }] : [])
61
- });
48
+ return new ExternalJwtAuthenticator(container, externalSubjectOptions(config));
62
49
  });
63
50
  registerAuthenticatorFactory("apikey", ({ container, config }) => {
64
51
  return new ApiKeyAuthenticator(container, {
@@ -66,5 +53,4 @@ registerAuthenticatorFactory("apikey", ({ container, config }) => {
66
53
  lastUsedThrottleS: config.apiKeys.lastUsedThrottleS
67
54
  });
68
55
  });
69
- //#endregion
70
56
  export { getAuthenticatorFactory, listAuthenticatorFactories, registerAuthenticatorFactory };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/authenticator/externalSubject.ts
1
+ // nodefony/src/authenticator/externalSubject.ts
2
2
  /**
3
3
  * Séparateur entre l'émetteur et le sujet.
4
4
  *
@@ -32,5 +32,4 @@ function localIdentifierFor(issuer, subject, mapping) {
32
32
  if (mapping === "subject") return subject;
33
33
  return `${issuer}${SEPARATOR}${subject}`;
34
34
  }
35
- //#endregion
36
35
  export { localIdentifierFor };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/authenticator/peekIssuer.ts
1
+ // nodefony/src/authenticator/peekIssuer.ts
2
2
  /**
3
3
  * Lecture NON VÉRIFIÉE de l'émetteur d'un JWS compact.
4
4
  *
@@ -52,5 +52,4 @@ function peekIssuer(raw) {
52
52
  return null;
53
53
  }
54
54
  }
55
- //#endregion
56
55
  export { peekIssuer as default, peekIssuer };
@@ -1,5 +1,5 @@
1
1
  import { isFrameworkZone } from "nodefony";
2
- //#region nodefony/src/boot/publicRoutesNotice.ts
2
+ // nodefony/src/boot/publicRoutesNotice.ts
3
3
  /**
4
4
  * Le point d'attention « routes métier publiques » du bilan de démarrage.
5
5
  *
@@ -23,5 +23,4 @@ function publicRoutesNotice(zones) {
23
23
  fix: "use(\"@nodefony/security\", { areas: { main: { pattern: \"^/api\", authenticators: [\"session\"] } } })"
24
24
  };
25
25
  }
26
- //#endregion
27
26
  export { publicRoutesNotice };
@@ -1,6 +1,6 @@
1
1
  import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from "node:crypto";
2
2
  import { Buffer } from "node:buffer";
3
- //#region nodefony/src/crypto/secretCipher.ts
3
+ // nodefony/src/crypto/secretCipher.ts
4
4
  /**
5
5
  * Chiffrement **réversible** de secrets applicatifs au repos — AES-256-GCM
6
6
  * (authentifié). Brique générique partagée par tous les domaines qui doivent
@@ -75,5 +75,4 @@ function decryptSecret(blob, key) {
75
75
  decipher.setAuthTag(tag);
76
76
  return Buffer.concat([decipher.update(ct), decipher.final()]);
77
77
  }
78
- //#endregion
79
78
  export { decryptSecret, deriveKey, encryptSecret, generateEphemeralKey };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/csp.ts
1
+ // nodefony/src/csp.ts
2
2
  /**
3
3
  * Parse une chaîne CSP en directives ordonnées `[nom, sources[]]`. L'ordre des
4
4
  * directives et des sources est préservé (déterminisme : header stable, tests
@@ -49,5 +49,4 @@ function mergeCspFragments(base, fragments) {
49
49
  }
50
50
  return serializeCsp(directives);
51
51
  }
52
- //#endregion
53
52
  export { mergeCspFragments, parseCsp, serializeCsp };
@@ -1,5 +1,5 @@
1
1
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
- //#region nodefony/src/csrfToken.ts
2
+ // nodefony/src/csrfToken.ts
3
3
  /**
4
4
  * Jeton synchronizer CSRF — modèle **double-submit signé** (OWASP CSRF Prevention
5
5
  * Cheat Sheet, « Signed Double-Submit Cookie »). Le token est
@@ -61,5 +61,4 @@ var CsrfTokenManager = class CsrfTokenManager {
61
61
  return timingSafeEqual(ba, bb);
62
62
  }
63
63
  };
64
- //#endregion
65
64
  export { CsrfTokenManager, CsrfTokenManager as default };
@@ -1,5 +1,5 @@
1
1
  import { loginPagePath, safeRedirectPath } from "nodefony";
2
- //#region nodefony/src/entryPoint.ts
2
+ // nodefony/src/entryPoint.ts
3
3
  /**
4
4
  * Dit si une requête refusée est la NAVIGATION d'un navigateur — le seul cas
5
5
  * où conduire à la page de connexion aide, au lieu de casser un client.
@@ -35,5 +35,4 @@ function entryPointLocation(entryPoint, request) {
35
35
  if (request.path.split("?", 1)[0] === entryPoint.split("?", 1)[0]) return null;
36
36
  return loginPagePath(safeRedirectPath(request.path), entryPoint);
37
37
  }
38
- //#endregion
39
38
  export { entryPointLocation, isBrowserNavigation };
@@ -1,7 +1,7 @@
1
1
  import { SsrfError } from "../../errors/SsrfError.js";
2
2
  import { BlockList, isIP } from "node:net";
3
3
  import { lookup } from "node:dns/promises";
4
- //#region nodefony/src/net/ssrfGuard.ts
4
+ // nodefony/src/net/ssrfGuard.ts
5
5
  /**
6
6
  * Protection **SSRF** (Server-Side Request Forgery) générique pour les URL
7
7
  * sortantes — endpoints webhook, fetch applicatif, futurs agents/RAG.
@@ -126,5 +126,4 @@ async function assertPublicUrl(rawUrl, options = {}) {
126
126
  addresses
127
127
  };
128
128
  }
129
- //#endregion
130
129
  export { assertPublicUrl, isBlockedAddress };
@@ -0,0 +1,57 @@
1
+ // nodefony/src/net/transportFailure.ts
2
+ /**
3
+ * Codes TLS d'une chaîne de certificats qu'aucune autorité CONNUE ne signe.
4
+ *
5
+ * Leur remède est le même : faire reconnaître l'autorité. Un certificat expiré
6
+ * (`CERT_HAS_EXPIRED`) ou émis pour un autre nom (`ERR_TLS_CERT_ALTNAME_INVALID`)
7
+ * n'en fait pas partie : ajouter une autorité n'y changerait rien.
8
+ */
9
+ const UNTRUSTED_AUTHORITY_CODES = /* @__PURE__ */ new Set([
10
+ "SELF_SIGNED_CERT_IN_CHAIN",
11
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
12
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
13
+ "UNABLE_TO_GET_ISSUER_CERT",
14
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
15
+ "CERT_UNTRUSTED"
16
+ ]);
17
+ /** Le code porté par la `cause` d'un échec de `fetch`, s'il y en a un. */
18
+ function causeCode(error) {
19
+ if (!(error instanceof Error)) return null;
20
+ const cause = error.cause;
21
+ if (!(cause instanceof Error)) return null;
22
+ const code = cause.code;
23
+ return typeof code === "string" ? code : null;
24
+ }
25
+ /**
26
+ * Rend un échec de transport LISIBLE : `fetch` (undici) ne dit que
27
+ * `fetch failed` et range la vraie raison dans `cause` — certificat refusé,
28
+ * connexion refusée, nom introuvable.
29
+ *
30
+ * @param error - ce que `fetch` (ou une bibliothèque qui l'appelle) a levé
31
+ * @returns `message (CODE : raison)`, ou le message seul faute de cause
32
+ */
33
+ function describeTransportFailure(error) {
34
+ if (!(error instanceof Error)) return String(error);
35
+ const cause = error.cause;
36
+ if (!(cause instanceof Error)) return error.message;
37
+ const code = causeCode(error);
38
+ return code !== null ? `${error.message} (${code} : ${cause.message})` : `${error.message} (${cause.message})`;
39
+ }
40
+ /**
41
+ * Le remède d'un certificat signé par une autorité que le processus ne
42
+ * reconnaît pas — `null` pour tout autre échec.
43
+ *
44
+ * En développement, Nodefony reconnaît d'office l'autorité qu'il a générée ; en
45
+ * production jamais, et c'est voulu. Sans ce remède, le journal ne montrait que
46
+ * `SELF_SIGNED_CERT_IN_CHAIN` : la variable qui le règle n'était écrite que dans
47
+ * un commentaire du `.env`, fichier qu'un serveur de production n'a pas.
48
+ *
49
+ * @param error - ce que `fetch` (ou une bibliothèque qui l'appelle) a levé
50
+ * @returns une phrase d'action, ou `null`
51
+ */
52
+ function untrustedAuthorityHint(error) {
53
+ const code = causeCode(error);
54
+ if (code === null || !UNTRUSTED_AUTHORITY_CODES.has(code)) return null;
55
+ return "le certificat de ce serveur est signé par une autorité que l'application ne reconnaît pas : déclare-la dans NF_TLS_CA_FILE (fichier PEM, `http.certificates.extraCaFiles`)";
56
+ }
57
+ export { describeTransportFailure, untrustedAuthorityHint };
@@ -1,6 +1,6 @@
1
1
  import { postTokenRequest } from "./oauth2Client.js";
2
2
  import { discoverAuthorizationServer } from "./metadata.js";
3
- //#region nodefony/src/oauth/clientCredentials.ts
3
+ // nodefony/src/oauth/clientCredentials.ts
4
4
  /**
5
5
  * Obtient un jeton d'accès pour un programme, par le flux `client_credentials`
6
6
  * (RFC 6749 §4.4), auprès d'un émetteur découvert (RFC 8414).
@@ -36,5 +36,4 @@ async function requestClientCredentialsToken(request) {
36
36
  timeoutMs: request.timeoutMs
37
37
  }, body);
38
38
  }
39
- //#endregion
40
39
  export { requestClientCredentialsToken };
@@ -1,4 +1,5 @@
1
- //#region nodefony/src/oauth/httpJson.ts
1
+ import { isPlainObject } from "nodefony";
2
+ // nodefony/src/oauth/httpJson.ts
2
3
  /**
3
4
  * Lecture BORNÉE d'un corps JSON — la brique de transport commune aux trois
4
5
  * appels sortants du social login (découverte, point de jeton, API d'un
@@ -57,8 +58,7 @@ async function readJsonBounded(response, maxBytes, subject) {
57
58
  */
58
59
  async function readJsonObjectBounded(response, maxBytes, subject) {
59
60
  const payload = await readJsonBounded(response, maxBytes, subject);
60
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) throw new Error(`${subject} → corps inattendu (objet JSON attendu)`);
61
+ if (!isPlainObject(payload)) throw new Error(`${subject} → corps inattendu (objet JSON attendu)`);
61
62
  return payload;
62
63
  }
63
- //#endregion
64
64
  export { readJsonBounded, readJsonObjectBounded };
@@ -1,6 +1,7 @@
1
+ import { describeTransportFailure, untrustedAuthorityHint } from "../net/transportFailure.js";
1
2
  import { readJsonObjectBounded } from "./httpJson.js";
2
3
  import { canonicalIssuer, isBackChannelTransportAllowed, metadataCandidateUrls, validateIssuerMetadata } from "nodefony";
3
- //#region nodefony/src/oauth/metadata.ts
4
+ // nodefony/src/oauth/metadata.ts
4
5
  /**
5
6
  * **Découverte des métadonnées d'un serveur d'autorisation** (RFC 8414) — la face
6
7
  * CLIENTE de la règle que le cœur porte déjà.
@@ -22,19 +23,6 @@ import { canonicalIssuer, isBackChannelTransportAllowed, metadataCandidateUrls,
22
23
  const DISCOVERY_TIMEOUT_MS = 1e4;
23
24
  /** Au-delà, le document n'est plus un document de métadonnées — on refuse de lire. */
24
25
  const MAX_METADATA_BYTES = 1048576;
25
- /**
26
- * Rend un échec de transport LISIBLE : `fetch` (undici) ne dit que
27
- * `fetch failed` et range la vraie raison dans `cause` — certificat refusé,
28
- * connexion refusée, nom introuvable. Sans elle, « métadonnées introuvables »
29
- * envoie chercher un émetteur absent quand c'est la confiance TLS qui manque.
30
- */
31
- function describeTransportFailure(error) {
32
- if (!(error instanceof Error)) return String(error);
33
- const cause = error.cause;
34
- if (!(cause instanceof Error)) return error.message;
35
- const code = cause.code;
36
- return typeof code === "string" ? `${error.message} (${code} : ${cause.message})` : `${error.message} (${cause.message})`;
37
- }
38
26
  async function fetchMetadataDocument(url, options) {
39
27
  const response = await (options.fetch ?? globalThis.fetch)(url, {
40
28
  headers: {
@@ -86,6 +74,7 @@ function optionalEndpoint(document, field, issuer) {
86
74
  async function discoverAuthorizationServer(rawIssuer, options = {}) {
87
75
  const issuer = canonicalIssuer(rawIssuer);
88
76
  const failures = [];
77
+ let hint = null;
89
78
  const discoveryUrl = options.discoveryUrl;
90
79
  for (const candidate of metadataCandidateUrls(issuer, discoveryUrl)) {
91
80
  let document;
@@ -93,6 +82,7 @@ async function discoverAuthorizationServer(rawIssuer, options = {}) {
93
82
  document = await fetchMetadataDocument(candidate, options);
94
83
  } catch (error) {
95
84
  failures.push(describeTransportFailure(error));
85
+ hint ??= untrustedAuthorityHint(error);
96
86
  continue;
97
87
  }
98
88
  const identity = validateIssuerMetadata(document, issuer, discoveryUrl);
@@ -109,7 +99,6 @@ async function discoverAuthorizationServer(rawIssuer, options = {}) {
109
99
  idTokenSigningAlgValuesSupported: Array.isArray(idTokenAlgs) ? idTokenAlgs.filter((a) => typeof a === "string") : null
110
100
  };
111
101
  }
112
- throw new Error(`métadonnées introuvables pour « ${issuer} » — ${failures.join(" ; ")}`);
102
+ throw new Error(`métadonnées introuvables pour « ${issuer} » — ${failures.join(" ; ")}` + (hint === null ? "" : ` — ${hint}`));
113
103
  }
114
- //#endregion
115
104
  export { discoverAuthorizationServer };
@@ -1,6 +1,6 @@
1
1
  import { readJsonObjectBounded } from "./httpJson.js";
2
2
  import { createHash, randomBytes } from "node:crypto";
3
- //#region nodefony/src/oauth/oauth2Client.ts
3
+ // nodefony/src/oauth/oauth2Client.ts
4
4
  /**
5
5
  * Client **OAuth 2.0 / Authorization Code** minimal — la face cliente du protocole
6
6
  * dont Nodefony écrit déjà la face serveur (émetteur de jetons, métadonnées,
@@ -289,5 +289,4 @@ function basicCredentials(client) {
289
289
  const secret = formUrlencode(client.clientSecret);
290
290
  return Buffer.from(`${id}:${secret}`, "utf8").toString("base64");
291
291
  }
292
- //#endregion
293
292
  export { OAuth2Client, OAuth2RequestError, OAuth2Tokens, createCodeChallenge, generateCodeVerifier, generateState, postTokenRequest };
@@ -1,6 +1,6 @@
1
1
  import { createDiscoveredOidcProvider } from "./providers/oidc.js";
2
2
  import { createGithubProvider } from "./providers/github.js";
3
- //#region nodefony/src/oauth/oauthProviderRegistry.ts
3
+ // nodefony/src/oauth/oauthProviderRegistry.ts
4
4
  const factories = /* @__PURE__ */ new Map();
5
5
  /**
6
6
  * Enregistre (ou remplace) la fabrique d'un fournisseur OAuth. Appelée par les
@@ -55,5 +55,4 @@ registerOAuthProvider("google", (ctx) => createDiscoveredOidcProvider("google",
55
55
  registerOAuthProvider("keycloak", async (ctx) => withKeycloakThemeHint(await createDiscoveredOidcProvider("keycloak", ctx)), { requiresIssuer: true });
56
56
  registerOAuthProvider("oidc", (ctx) => createDiscoveredOidcProvider("oidc", ctx), { requiresIssuer: true });
57
57
  registerOAuthProvider("github", createGithubProvider);
58
- //#endregion
59
58
  export { KEYCLOAK_THEME_PARAMETER, getOAuthProviderFactory, listOAuthProviders, oauthProviderRequiresIssuer, registerOAuthProvider, withKeycloakThemeHint };
@@ -1,6 +1,7 @@
1
+ import { describeTransportFailure, untrustedAuthorityHint } from "../net/transportFailure.js";
1
2
  import { OAuth2RequestError, generateCodeVerifier, generateState } from "./oauth2Client.js";
2
3
  import { IssuerMismatchError } from "nodefony";
3
- //#region nodefony/src/oauth/providerDiagnosis.ts
4
+ // nodefony/src/oauth/providerDiagnosis.ts
4
5
  /** Le code présenté au point de jeton : il n'a jamais été émis, il sera refusé. */
5
6
  const PROBE_CODE = "nodefony-doctor-probe";
6
7
  const AUTHORIZATION_TIMEOUT_MS = 1e4;
@@ -10,10 +11,9 @@ const AUTHORIZATION_TIMEOUT_MS = 1e4;
10
11
  */
11
12
  const MAX_ERROR_PAGE_BYTES = 65536;
12
13
  function errorMessage(error) {
13
- if (!(error instanceof Error)) return String(error);
14
- const cause = error.cause;
15
- if (cause instanceof Error) return `${error.message} (${cause.message})`;
16
- return error.message;
14
+ const hint = untrustedAuthorityHint(error);
15
+ const text = describeTransportFailure(error);
16
+ return hint === null ? text : `${text} — ${hint}`;
17
17
  }
18
18
  /** Les premiers octets d'une page, en texte — le reste est abandonné. */
19
19
  async function readHead(response, maxBytes) {
@@ -279,5 +279,4 @@ async function diagnoseOAuthProvider(input) {
279
279
  checks
280
280
  };
281
281
  }
282
- //#endregion
283
282
  export { diagnoseOAuthProvider };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/oauth/providerRoles.ts
1
+ // nodefony/src/oauth/providerRoles.ts
2
2
  /** Source par défaut : les rôles du CLIENT — ceux que l'annuaire attribue à CETTE application. */
3
3
  const DEFAULT_ROLE_SOURCES = ["client"];
4
4
  /**
@@ -75,5 +75,4 @@ function strings(value) {
75
75
  if (!Array.isArray(value)) return [];
76
76
  return value.filter((v) => typeof v === "string");
77
77
  }
78
- //#endregion
79
78
  export { DEFAULT_ROLE_SOURCES, PLATFORM_ROLE_PREFIX, compileProviderRoleMapping, isPlatformRole, mapProviderRoles };
@@ -1,6 +1,7 @@
1
- import { readJsonBounded } from "../httpJson.js";
1
+ import { readJsonBounded, readJsonObjectBounded } from "../httpJson.js";
2
2
  import { OAuth2Client, OAuth2RequestError } from "../oauth2Client.js";
3
- //#region nodefony/src/oauth/providers/github.ts
3
+ import { isPlainObject } from "nodefony";
4
+ // nodefony/src/oauth/providers/github.ts
4
5
  /** Scopes minimaux : profil public + emails (l'email primaire peut être privé). */
5
6
  const DEFAULT_SCOPES = ["read:user", "user:email"];
6
7
  const API = "https://api.github.com";
@@ -25,7 +26,17 @@ async function ghGet(url, accessToken) {
25
26
  signal: AbortSignal.timeout(API_TIMEOUT_MS)
26
27
  });
27
28
  if (!res.ok) throw new Error(`GitHub API ${url} → ${res.status}`);
28
- return readJsonBounded(res, MAX_PROFILE_BYTES, `GitHub API ${url}`);
29
+ return res;
30
+ }
31
+ /**
32
+ * Les adresses que rend `GET /user/emails` — un tableau, sinon une erreur qui
33
+ * nomme l'appel ; une entrée qui n'est pas un objet est écartée.
34
+ */
35
+ async function ghEmails(accessToken) {
36
+ const url = `${API}/user/emails`;
37
+ const list = await readJsonBounded(await ghGet(url, accessToken), MAX_PROFILE_BYTES, `GitHub API ${url}`);
38
+ if (!Array.isArray(list)) throw new Error(`GitHub API ${url} → corps inattendu (tableau JSON attendu)`);
39
+ return list.filter(isPlainObject);
29
40
  }
30
41
  /**
31
42
  * Codes d'erreur PROPRES à GitHub, rendus dans leur équivalent RFC 6749 §5.2
@@ -86,7 +97,7 @@ function createGithubProvider(ctx) {
86
97
  },
87
98
  async fetchProfile(tokens) {
88
99
  const accessToken = tokens.accessToken();
89
- const user = await ghGet(`${API}/user`, accessToken);
100
+ const user = await readJsonObjectBounded(await ghGet(`${API}/user`, accessToken), MAX_PROFILE_BYTES, `GitHub API ${API}/user`);
90
101
  const id = user.id;
91
102
  if (typeof id !== "number" && typeof id !== "string") throw new Error("GitHub /user sans identifiant.");
92
103
  const login = typeof user.login === "string" ? user.login : null;
@@ -94,7 +105,7 @@ function createGithubProvider(ctx) {
94
105
  let email = typeof user.email === "string" ? user.email : null;
95
106
  let emailVerified = email !== null;
96
107
  if (email === null) {
97
- const list = await ghGet(`${API}/user/emails`, accessToken);
108
+ const list = await ghEmails(accessToken);
98
109
  const primary = list.find((e) => e.primary === true) ?? list.at(0) ?? null;
99
110
  if (primary && typeof primary.email === "string") {
100
111
  email = primary.email;
@@ -112,5 +123,4 @@ function createGithubProvider(ctx) {
112
123
  }
113
124
  };
114
125
  }
115
- //#endregion
116
126
  export { createGithubProvider };