@nodefony/security 10.0.0-alpha.8 → 10.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
  3. package/dist/index.js +2 -2
  4. package/dist/nodefony/command/security-token.js +4 -4
  5. package/dist/nodefony/command/security-user-add.js +2 -18
  6. package/dist/nodefony/command/security-user-delete.js +2 -2
  7. package/dist/nodefony/command/security-user-list.js +1 -1
  8. package/dist/nodefony/command/security-user-password.js +2 -2
  9. package/dist/nodefony/config/config.js +2 -2
  10. package/dist/nodefony/service/apiKeys.js +2 -2
  11. package/dist/nodefony/service/auditService.js +6 -5
  12. package/dist/nodefony/service/authFlow.js +2 -2
  13. package/dist/nodefony/service/authorization.js +3 -4
  14. package/dist/nodefony/service/firewall.js +21 -9
  15. package/dist/nodefony/service/oauth2.js +5 -3
  16. package/dist/nodefony/service/securityHeaders.js +1 -1
  17. package/dist/nodefony/service/tokenService.js +30 -20
  18. package/dist/nodefony/service/totp.js +5 -3
  19. package/dist/nodefony/service/webAuthn.js +41 -33
  20. package/dist/nodefony/service/webhooks.js +55 -44
  21. package/dist/nodefony/src/admin/SecurityAdminApi.js +2 -1
  22. package/dist/nodefony/src/admin/WebhookAdminApi.js +1 -0
  23. package/dist/nodefony/src/admin/userRevocationCascade.js +1 -1
  24. package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -1
  25. package/dist/nodefony/src/audit/MemoryAuditStore.js +4 -8
  26. package/dist/nodefony/src/audit/auditBridge.js +9 -5
  27. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -3
  28. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +2 -2
  29. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +2 -2
  30. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +2 -2
  31. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +5 -3
  32. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +2 -2
  33. package/dist/nodefony/src/csp.js +4 -5
  34. package/dist/nodefony/src/oauth/providers/github.js +1 -1
  35. package/dist/nodefony/src/realtime/frameAuthorizer.js +6 -6
  36. package/dist/nodefony/src/token/JwtKeystore.js +7 -6
  37. package/dist/nodefony/src/token/MemoryTokenStore.js +19 -6
  38. package/dist/nodefony/src/token/jwtRuntime.js +13 -1
  39. package/dist/nodefony/src/token/tokenCriteria.js +1 -0
  40. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +15 -3
  41. package/dist/nodefony/src/totp/totpCrypto.js +9 -9
  42. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +24 -6
  43. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +18 -10
  44. package/dist/nodefony/src/webhook/WebhookDispatcher.js +4 -3
  45. package/dist/types/nodefony/command/security-user-add.d.ts +0 -16
  46. package/dist/types/nodefony/contracts/IAuditEvent.d.ts +8 -4
  47. package/dist/types/nodefony/contracts/IFirewall.d.ts +2 -2
  48. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +5 -5
  49. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +4 -4
  50. package/dist/types/nodefony/contracts/ITokenStore.d.ts +3 -3
  51. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +4 -4
  52. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +1 -1
  53. package/dist/types/nodefony/errors/InvalidTargetError.d.ts +1 -1
  54. package/dist/types/nodefony/errors/UnverifiableTokenError.d.ts +1 -1
  55. package/dist/types/nodefony/service/firewall.d.ts +3 -2
  56. package/dist/types/nodefony/service/securityHeaders.d.ts +5 -5
  57. package/dist/types/nodefony/src/SecuredArea.d.ts +2 -2
  58. package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +5 -3
  59. package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +2 -2
  60. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +2 -2
  61. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +3 -3
  62. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +2 -2
  63. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +2 -2
  64. package/dist/types/nodefony/src/token/jwtRuntime.d.ts +10 -0
  65. package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +0 -9
  66. package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +0 -8
  67. package/docs/api-keys.md +67 -65
  68. package/docs/audit.md +87 -82
  69. package/docs/authenticators.md +61 -49
  70. package/docs/authorization.md +21 -21
  71. package/docs/cors.md +30 -30
  72. package/docs/csrf.md +28 -28
  73. package/docs/external-jwt.md +21 -13
  74. package/docs/firewall.md +32 -32
  75. package/docs/headers.md +29 -29
  76. package/docs/index.md +3 -1
  77. package/docs/oauth2.md +61 -61
  78. package/docs/obtenir-un-jeton.md +2 -2
  79. package/docs/tokens.md +56 -56
  80. package/docs/totp.md +73 -72
  81. package/docs/webauthn.md +85 -85
  82. package/docs/webhooks.md +50 -47
  83. package/package.json +19 -16
@@ -2,7 +2,7 @@ import { AuthenticationError } from "../errors/AuthenticationError.js";
2
2
  import { defineSecurityConfig } from "../config/defineModuleConfig.js";
3
3
  import { WebAuthnError } from "../errors/WebAuthnError.js";
4
4
  import { getWebAuthnStoreFactory, listWebAuthnStores } from "../src/webauthn/webAuthnCredentialStoreRegistry.js";
5
- import { AUTO_STORE, EMPTY_INFRA, Service, deriveStoreBackend, durableStoreRemedy, readStoreLocation, resolveAutoStore, runNeedsExternalServices } from "nodefony";
5
+ import { AUTO_STORE, EMPTY_INFRA, Service, deriveStoreBackend, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore, runNeedsExternalServices } from "nodefony";
6
6
  import { Buffer } from "node:buffer";
7
7
  //#region nodefony/service/webAuthn.ts
8
8
  const serviceName = "webauthn";
@@ -102,7 +102,8 @@ var WebAuthnService = class extends Service {
102
102
  available: listWebAuthnStores(),
103
103
  reason,
104
104
  configPath: "security.passkeys.store",
105
- location: readStoreLocation(this.#store)
105
+ location: readStoreLocation(this.#store),
106
+ connector: readStoreConnector(this.#store)
106
107
  });
107
108
  if (config.passkeys.attestation !== "none") this.log(`passkeys.attestation "${config.passkeys.attestation}" — l'attestation est demandée au navigateur mais N'EST PAS VÉRIFIÉE : ni métadonnées FIDO (MDS), ni certificats racines fabricant, et l'AAGUID n'est pas conservé. Ce réglage ne tient donc pas un AAL3 régulé. Repasser à "none" si la garantie n'est pas requise.`, "WARNING");
108
109
  if (config.passkeys.residentKey === "discouraged") this.log("passkeys.residentKey \"discouraged\" — les credentials enrôlés ne sont pas découvrables, or le login par passkey est usernameless (aucun ciblage depuis la requête, anti-énumération). Ces porteurs ne pourront pas se connecter : repasser à \"preferred\" ou \"required\".", "WARNING");
@@ -134,10 +135,10 @@ var WebAuthnService = class extends Service {
134
135
  * pour empêcher un double enregistrement sur le même authenticator (§7.1).
135
136
  */
136
137
  async generateRegistrationOptions(user) {
137
- this.#ensureReady();
138
+ const { store, config } = this.#ensureReady();
138
139
  const lib = await this.#ensureLib();
139
- const pk = this.#config.passkeys;
140
- const existing = await this.#store.findByUser(user.id);
140
+ const pk = config.passkeys;
141
+ const existing = await store.findByUser(user.id);
141
142
  return lib.generateRegistrationOptions({
142
143
  rpName: this.#rpName,
143
144
  rpID: this.#rpID,
@@ -168,23 +169,23 @@ var WebAuthnService = class extends Service {
168
169
  * @throws WebAuthnError (409) — plafond `passkeys.maxPerUser` atteint.
169
170
  */
170
171
  async verifyRegistration(response, expectedChallenge, userId, requestOrigin) {
171
- this.#ensureReady();
172
+ const { store, config } = this.#ensureReady();
172
173
  const lib = await this.#ensureLib();
173
- const pk = this.#config.passkeys;
174
+ const pk = config.passkeys;
174
175
  let verification;
175
176
  try {
176
177
  verification = await lib.verifyRegistrationResponse({
177
178
  response,
178
179
  expectedChallenge,
179
- expectedOrigin: this.#expectedOrigin(requestOrigin),
180
+ expectedOrigin: this.#expectedOrigin(pk.origins, requestOrigin),
180
181
  expectedRPID: this.#rpID,
181
182
  requireUserVerification: pk.userVerification === "required"
182
183
  });
183
184
  } catch {
184
185
  throw new AuthenticationError("WebAuthn registration failed");
185
186
  }
186
- if (!verification.verified || !verification.registrationInfo) throw new AuthenticationError("WebAuthn registration failed");
187
- if (await this.#store.countByUser(userId) >= pk.maxPerUser) throw new WebAuthnError(`passkey limit reached (${pk.maxPerUser})`, 409);
187
+ if (!verification.verified) throw new AuthenticationError("WebAuthn registration failed");
188
+ if (await store.countByUser(userId) >= pk.maxPerUser) throw new WebAuthnError(`passkey limit reached (${pk.maxPerUser})`, 409);
188
189
  const info = verification.registrationInfo;
189
190
  const credential = {
190
191
  id: info.credential.id,
@@ -198,7 +199,7 @@ var WebAuthnService = class extends Service {
198
199
  createdAt: Date.now(),
199
200
  lastUsedAt: null
200
201
  };
201
- await this.#store.save(credential);
202
+ await store.save(credential);
202
203
  return credential;
203
204
  }
204
205
  /**
@@ -215,17 +216,17 @@ var WebAuthnService = class extends Service {
215
216
  * `WebAuthnController.loginOptions()` ne cible que depuis la session.
216
217
  */
217
218
  async generateAuthenticationOptions(userId) {
218
- this.#ensureReady();
219
+ const { store, config } = this.#ensureReady();
219
220
  const lib = await this.#ensureLib();
220
- const pk = this.#config.passkeys;
221
+ const pk = config.passkeys;
221
222
  let allowCredentials;
222
- if (userId) allowCredentials = (await this.#store.findByUser(userId)).map((c) => ({
223
+ if (userId) allowCredentials = (await store.findByUser(userId)).map((c) => ({
223
224
  id: c.id,
224
225
  transports: [...c.transports]
225
226
  }));
226
227
  return lib.generateAuthenticationOptions({
227
228
  rpID: this.#rpID,
228
- allowCredentials,
229
+ ...allowCredentials !== void 0 && { allowCredentials },
229
230
  userVerification: pk.userVerification,
230
231
  timeout: pk.timeoutMs
231
232
  });
@@ -239,17 +240,17 @@ var WebAuthnService = class extends Service {
239
240
  * @throws AuthenticationError (401) — credential inconnu ou vérification échouée.
240
241
  */
241
242
  async verifyAuthentication(response, expectedChallenge, requestOrigin) {
242
- this.#ensureReady();
243
+ const { store, config } = this.#ensureReady();
243
244
  const lib = await this.#ensureLib();
244
- const pk = this.#config.passkeys;
245
- const stored = await this.#store.findById(response.id);
245
+ const pk = config.passkeys;
246
+ const stored = await store.findById(response.id);
246
247
  if (!stored) throw new AuthenticationError("WebAuthn authentication failed");
247
248
  let verification;
248
249
  try {
249
250
  verification = await lib.verifyAuthenticationResponse({
250
251
  response,
251
252
  expectedChallenge,
252
- expectedOrigin: this.#expectedOrigin(requestOrigin),
253
+ expectedOrigin: this.#expectedOrigin(pk.origins, requestOrigin),
253
254
  expectedRPID: this.#rpID,
254
255
  credential: {
255
256
  id: stored.id,
@@ -264,7 +265,7 @@ var WebAuthnService = class extends Service {
264
265
  }
265
266
  if (!verification.verified) throw new AuthenticationError("WebAuthn authentication failed");
266
267
  const info = verification.authenticationInfo;
267
- await this.#store.update(stored.id, {
268
+ await store.update(stored.id, {
268
269
  signCount: info.newCounter,
269
270
  backupState: info.credentialBackedUp,
270
271
  uvInitialized: stored.uvInitialized || info.userVerified,
@@ -277,8 +278,7 @@ var WebAuthnService = class extends Service {
277
278
  }
278
279
  /** Liste les credentials d'un utilisateur (UX « mes appareils »). */
279
280
  listUserCredentials(userId) {
280
- this.#ensureReady();
281
- return this.#store.findByUser(userId);
281
+ return this.#ensureReady().store.findByUser(userId);
282
282
  }
283
283
  /**
284
284
  * Page de passkeys pour le data plane admin (vue TRANSVERSE : « quels appareils
@@ -289,21 +289,18 @@ var WebAuthnService = class extends Service {
289
289
  * du store exclut la clé publique.
290
290
  */
291
291
  listCredentialsPage(query) {
292
- this.#ensureReady();
293
- return this.#store.listPage(query);
292
+ return this.#ensureReady().store.listPage(query);
294
293
  }
295
294
  /**
296
295
  * Nombre de passkeys correspondant aux filtres, ou `-1` si le backend ne sait
297
296
  * pas compter à coût raisonnable (Redis).
298
297
  */
299
298
  countCredentials(query) {
300
- this.#ensureReady();
301
- return this.#store.countCredentials(query);
299
+ return this.#ensureReady().store.countCredentials(query);
302
300
  }
303
301
  /** Révoque un credential (retrait d'un appareil). */
304
302
  removeCredential(credentialId) {
305
- this.#ensureReady();
306
- return this.#store.delete(credentialId);
303
+ return this.#ensureReady().store.delete(credentialId);
307
304
  }
308
305
  /**
309
306
  * Supprime un credential **du propriétaire** (self-service, anti-IDOR) : la
@@ -312,10 +309,10 @@ var WebAuthnService = class extends Service {
312
309
  * credential d'autrui).
313
310
  */
314
311
  async removeUserCredential(userId, credentialId) {
315
- this.#ensureReady();
316
- const cred = await this.#store.findById(credentialId);
312
+ const { store } = this.#ensureReady();
313
+ const cred = await store.findById(credentialId);
317
314
  if (!cred || cred.userId !== userId) return false;
318
- await this.#store.delete(credentialId);
315
+ await store.delete(credentialId);
319
316
  return true;
320
317
  }
321
318
  /**
@@ -323,9 +320,11 @@ var WebAuthnService = class extends Service {
323
320
  * priorité (prod). À défaut : l'origine de la requête est acceptée **seulement
324
321
  * si son hostname == rpID** (dev : `localhost:port` quel que soit le port, sans
325
322
  * jamais ouvrir à un domaine tiers). Dernier recours : `https://{rpID}`.
323
+ *
324
+ * @param origins - liste blanche `passkeys.origins` de la config prête.
325
+ * @param requestOrigin - origine HTTP de la requête, si connue.
326
326
  */
327
- #expectedOrigin(requestOrigin) {
328
- const origins = this.#config.passkeys.origins;
327
+ #expectedOrigin(origins, requestOrigin) {
329
328
  if (origins.length > 0) return [...origins];
330
329
  if (requestOrigin) try {
331
330
  if (new URL(requestOrigin).hostname === this.#rpID) return requestOrigin;
@@ -335,8 +334,17 @@ var WebAuthnService = class extends Service {
335
334
  async #ensureLib() {
336
335
  return this.#lib ??= await import("@simplewebauthn/server");
337
336
  }
337
+ /**
338
+ * Garde d'initialisation : lève si le service n'est pas prêt, sinon rend le
339
+ * store et la config typés non nuls (ni l'un ni l'autre ne sont réassignés
340
+ * après le boot — les capturer ici équivaut à les relire plus tard).
341
+ */
338
342
  #ensureReady() {
339
343
  if (!this.#ready || !this.#store || !this.#config) throw new Error("WebAuthnService: non initialisé (passkeys désactivés ou boot échoué)");
344
+ return {
345
+ store: this.#store,
346
+ config: this.#config
347
+ };
340
348
  }
341
349
  };
342
350
  //#endregion
@@ -6,7 +6,7 @@ import { deriveWebhookKey } from "../src/webhook/webhookCipher.js";
6
6
  import { assertPublicUrl } from "../src/net/ssrfGuard.js";
7
7
  import { WebhookDispatcher } from "../src/webhook/WebhookDispatcher.js";
8
8
  import { deliverWebhook } from "../src/webhook/webhookDelivery.js";
9
- import { AUTO_STORE, EMPTY_INFRA, Service, countFacets, deriveStoreBackend, durableStoreRemedy, readStoreLocation, resolveAutoStore, runNeedsExternalServices } from "nodefony";
9
+ import { AUTO_STORE, EMPTY_INFRA, Service, countFacets, deriveStoreBackend, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore, runNeedsExternalServices } from "nodefony";
10
10
  import { randomBytes } from "node:crypto";
11
11
  import { Buffer } from "node:buffer";
12
12
  import { schemaMismatchOf } from "@nodefony/http";
@@ -104,7 +104,7 @@ var WebhookService = class extends Service {
104
104
  return;
105
105
  }
106
106
  this.#audit = audit;
107
- this.#dispatcher = new WebhookDispatcher({
107
+ const dispatcher = new WebhookDispatcher({
108
108
  endpointCount: () => this.endpointCount(),
109
109
  getSnapshot: () => this.getSnapshot(),
110
110
  secretOf: (ep) => this.decryptEndpointSecret(ep).toString("utf8"),
@@ -122,7 +122,8 @@ var WebhookService = class extends Service {
122
122
  },
123
123
  log: (m) => this.log(m, "ERROR")
124
124
  });
125
- this.#unsubscribe = audit.subscribe((e) => this.#dispatcher.onAuditEvent(e));
125
+ this.#dispatcher = dispatcher;
126
+ this.#unsubscribe = audit.subscribe((e) => dispatcher.onAuditEvent(e));
126
127
  this.log("webhooks dispatcher abonné à l'audit", "DEBUG");
127
128
  }
128
129
  /** Arrêt propre : désabonnement audit + annulation des retries en vol. */
@@ -146,7 +147,8 @@ var WebhookService = class extends Service {
146
147
  available: listWebhookStores(),
147
148
  reason: "adapter posé au container (infra database déclarée)",
148
149
  configPath: "security.webhooks.store",
149
- location: readStoreLocation(existing)
150
+ location: readStoreLocation(existing),
151
+ connector: readStoreConnector(existing)
150
152
  });
151
153
  return existing;
152
154
  }
@@ -179,7 +181,8 @@ var WebhookService = class extends Service {
179
181
  available: listWebhookStores(),
180
182
  reason,
181
183
  configPath: "security.webhooks.store",
182
- location: readStoreLocation(store)
184
+ location: readStoreLocation(store),
185
+ connector: readStoreConnector(store)
183
186
  });
184
187
  return store;
185
188
  }
@@ -260,14 +263,14 @@ var WebhookService = class extends Service {
260
263
  * @throws SsrfError si l'URL est invalide / cible non publique.
261
264
  */
262
265
  async register(input) {
263
- this.#assertReady();
264
- await this.#assertSafeUrl(input.url);
266
+ const { store, key, config } = this.#assertReady();
267
+ await this.#assertSafeUrl(config, input.url);
265
268
  const secret = generateSecret();
266
269
  const now = Date.now();
267
270
  const endpoint = {
268
271
  id: generateId(),
269
272
  url: input.url,
270
- secretEnc: encryptSecret(Buffer.from(secret, "utf8"), this.#key),
273
+ secretEnc: encryptSecret(Buffer.from(secret, "utf8"), key),
271
274
  events: [...input.events],
272
275
  enabled: input.enabled ?? true,
273
276
  description: input.description ?? null,
@@ -281,7 +284,7 @@ var WebhookService = class extends Service {
281
284
  failureCount: 0,
282
285
  metadata: input.metadata ?? {}
283
286
  };
284
- await this.#store.save(endpoint);
287
+ await store.save(endpoint);
285
288
  this.#endpoints?.set(endpoint.id, endpoint);
286
289
  return {
287
290
  endpoint: toSummary(endpoint),
@@ -296,8 +299,7 @@ var WebhookService = class extends Service {
296
299
  * @returns la page, chaque endpoint réduit à sa vue publique.
297
300
  */
298
301
  async listPage(query) {
299
- this.#assertReady();
300
- const page = await this.#store.listPage(query);
302
+ const page = await this.#assertReady().store.listPage(query);
301
303
  return {
302
304
  ...page,
303
305
  items: page.items.map(toSummary)
@@ -310,8 +312,7 @@ var WebhookService = class extends Service {
310
312
  * @returns le compte exact, ou `-1` si le backend ne sait pas compter.
311
313
  */
312
314
  async countEndpoints(query) {
313
- this.#assertReady();
314
- return this.#store.countEndpoints(query);
315
+ return this.#assertReady().store.countEndpoints(query);
315
316
  }
316
317
  /**
317
318
  * Les compteurs de tête de la console — posés sur la collection ENTIÈRE, pas
@@ -324,16 +325,15 @@ var WebhookService = class extends Service {
324
325
  * @param query - filtres à appliquer avant comptage (sans fenêtre).
325
326
  */
326
327
  async countWebhookFacets(query) {
327
- this.#assertReady();
328
- return countFacets(WEBHOOK_FACETS, (facet) => this.#store.countEndpoints({
328
+ const { store } = this.#assertReady();
329
+ return countFacets(WEBHOOK_FACETS, (facet) => store.countEndpoints({
329
330
  ...query,
330
331
  ...facet
331
332
  }));
332
333
  }
333
334
  /** Un endpoint par id (vue publique), ou `null`. */
334
335
  async getEndpoint(id) {
335
- this.#assertReady();
336
- const found = await this.#store.findById(id);
336
+ const found = await this.#assertReady().store.findById(id);
337
337
  return found ? toSummary(found) : null;
338
338
  }
339
339
  /**
@@ -342,15 +342,15 @@ var WebhookService = class extends Service {
342
342
  * ou `null` si absent.
343
343
  */
344
344
  async update(id, patch) {
345
- this.#assertReady();
346
- const current = await this.#store.findById(id);
345
+ const { store, config } = this.#assertReady();
346
+ const current = await store.findById(id);
347
347
  if (!current) return null;
348
- if (patch.url !== void 0 && patch.url !== current.url) await this.#assertSafeUrl(patch.url);
348
+ if (patch.url !== void 0 && patch.url !== current.url) await this.#assertSafeUrl(config, patch.url);
349
349
  const applied = {
350
350
  ...patch,
351
351
  updatedAt: Date.now()
352
352
  };
353
- await this.#store.update(id, applied);
353
+ await store.update(id, applied);
354
354
  const next = {
355
355
  ...current,
356
356
  ...applied
@@ -367,15 +367,15 @@ var WebhookService = class extends Service {
367
367
  * L'ancien cesse immédiatement d'être valide. `null` si l'endpoint est absent.
368
368
  */
369
369
  async rotateSecret(id) {
370
- this.#assertReady();
371
- const current = await this.#store.findById(id);
370
+ const { store, key } = this.#assertReady();
371
+ const current = await store.findById(id);
372
372
  if (!current) return null;
373
373
  const secret = generateSecret();
374
374
  const patch = {
375
- secretEnc: encryptSecret(Buffer.from(secret, "utf8"), this.#key),
375
+ secretEnc: encryptSecret(Buffer.from(secret, "utf8"), key),
376
376
  updatedAt: Date.now()
377
377
  };
378
- await this.#store.update(id, patch);
378
+ await store.update(id, patch);
379
379
  const next = {
380
380
  ...current,
381
381
  ...patch
@@ -391,16 +391,16 @@ var WebhookService = class extends Service {
391
391
  * par l'appelant). `null` si absent.
392
392
  */
393
393
  async revealSecret(id) {
394
- this.#assertReady();
395
- const current = await this.#store.findById(id);
394
+ const { store, key } = this.#assertReady();
395
+ const current = await store.findById(id);
396
396
  if (!current) return null;
397
- return decryptSecret(current.secretEnc, this.#key).toString("utf8");
397
+ return decryptSecret(current.secretEnc, key).toString("utf8");
398
398
  }
399
399
  /** Supprime un endpoint. Retourne `false` si absent. */
400
400
  async delete(id) {
401
- this.#assertReady();
402
- if (!await this.#store.findById(id)) return false;
403
- await this.#store.delete(id);
401
+ const { store } = this.#assertReady();
402
+ if (!await store.findById(id)) return false;
403
+ await store.delete(id);
404
404
  this.#endpoints?.delete(id);
405
405
  this.#deliveries?.delete(id);
406
406
  return true;
@@ -420,7 +420,7 @@ var WebhookService = class extends Service {
420
420
  * sur l'issue FINALE d'une livraison.
421
421
  */
422
422
  #recordDelivery(id, rec) {
423
- if (this.#deliveries === null) this.#deliveries = /* @__PURE__ */ new Map();
423
+ this.#deliveries ??= /* @__PURE__ */ new Map();
424
424
  let ring = this.#deliveries.get(id);
425
425
  if (ring === void 0) {
426
426
  ring = [];
@@ -459,13 +459,11 @@ var WebhookService = class extends Service {
459
459
  }
460
460
  /** Déchiffre le secret de signature d'un endpoint (pour signer une livraison). */
461
461
  decryptEndpointSecret(endpoint) {
462
- this.#assertReady();
463
- return decryptSecret(endpoint.secretEnc, this.#key);
462
+ return decryptSecret(endpoint.secretEnc, this.#assertReady().key);
464
463
  }
465
464
  /** Politique de livraison (tolérance/retries/timeout…) issue de la config. */
466
465
  getDeliveryPolicy() {
467
- this.#assertReady();
468
- const w = this.#config.webhooks;
466
+ const w = this.#assertReady().config.webhooks;
469
467
  return {
470
468
  timestampToleranceS: w.timestampToleranceS,
471
469
  maxRetries: w.maxRetries,
@@ -483,12 +481,14 @@ var WebhookService = class extends Service {
483
481
  * l'endpoint au-delà du seuil (façon GitHub). Le succès remet le compteur à 0.
484
482
  */
485
483
  async markDelivery(id, result) {
486
- if (!this.#ready || !this.#store) return;
487
- const current = await this.#store.findById(id);
484
+ const config = this.#config;
485
+ const store = this.#store;
486
+ if (!this.#ready || !store || !config) return;
487
+ const current = await store.findById(id);
488
488
  if (!current) return;
489
489
  const failureCount = result.ok ? 0 : current.failureCount + 1;
490
490
  const now = Date.now();
491
- const threshold = this.#config.webhooks.autoDisableThreshold;
491
+ const threshold = config.webhooks.autoDisableThreshold;
492
492
  const disable = !result.ok && threshold > 0 && failureCount >= threshold;
493
493
  const patch = {
494
494
  lastDeliveryAt: now,
@@ -509,17 +509,28 @@ var WebhookService = class extends Service {
509
509
  reason: "max_failures"
510
510
  });
511
511
  }
512
- await this.#store.update(id, patch);
512
+ await store.update(id, patch);
513
513
  this.#endpoints?.set(id, {
514
514
  ...current,
515
515
  ...patch
516
516
  });
517
517
  }
518
+ /**
519
+ * Garde d'initialisation : lève si le service n'est pas prêt, sinon rend le
520
+ * store, la clé de chiffrement et la config typés non nuls. Les quatre champs
521
+ * sont posés ensemble au boot et jamais réassignés — tester aussi `#config`
522
+ * ne change donc rien au verdict.
523
+ */
518
524
  #assertReady() {
519
- if (!this.#ready || !this.#store || !this.#key) throw new Error("webhooks indisponibles (désactivés ou mal configurés)");
525
+ if (!this.#ready || !this.#store || !this.#key || !this.#config) throw new Error("webhooks indisponibles (désactivés ou mal configurés)");
526
+ return {
527
+ store: this.#store,
528
+ key: this.#key,
529
+ config: this.#config
530
+ };
520
531
  }
521
- async #assertSafeUrl(url) {
522
- const w = this.#config.webhooks;
532
+ async #assertSafeUrl(config, url) {
533
+ const w = config.webhooks;
523
534
  await assertPublicUrl(url, {
524
535
  allowPrivate: !w.denyPrivateIps,
525
536
  allowHttp: w.allowHttp
@@ -527,7 +538,7 @@ var WebhookService = class extends Service {
527
538
  }
528
539
  /** Re-contrôle SSRF avant livraison → IP validées à pinner (anti-rebinding). */
529
540
  async #resolveTarget(url) {
530
- const w = this.#config.webhooks;
541
+ const w = this.#assertReady().config.webhooks;
531
542
  const { addresses } = await assertPublicUrl(url, {
532
543
  allowPrivate: !w.denyPrivateIps,
533
544
  allowHttp: w.allowHttp
@@ -21,6 +21,7 @@ function tokenStoreDriver(className) {
21
21
  case "DrizzleTokenStore":
22
22
  case "MongooseTokenStore": return "orm";
23
23
  case "RedisTokenStore": return "redis";
24
+ case void 0:
24
25
  default: return null;
25
26
  }
26
27
  }
@@ -65,7 +66,7 @@ function parseTokenListQuery(query, sortable) {
65
66
  function one(query, key) {
66
67
  const value = query[key];
67
68
  if (value === void 0) return void 0;
68
- return Array.isArray(value) ? value[0] : value;
69
+ return Array.isArray(value) ? value.at(0) : value;
69
70
  }
70
71
  /** Taille de page du journal d'audit quand l'appelant n'en demande pas. */
71
72
  const AUDIT_DEFAULT_LIMIT = 100;
@@ -17,6 +17,7 @@ function webhookStoreDriver(className) {
17
17
  case "MemoryWebhookStore": return "memory";
18
18
  case "DrizzleWebhookStore":
19
19
  case "MongooseWebhookStore": return "orm";
20
+ case void 0:
20
21
  default: return null;
21
22
  }
22
23
  }
@@ -19,7 +19,7 @@ async function cascadeUserRevocation(container, event, now = Date.now()) {
19
19
  await container.get("sessions")?.destroyByUser?.(identifier);
20
20
  } catch {}
21
21
  try {
22
- await container.get("tokenStore")?.revokeAllForSubject?.(identifier, now);
22
+ await container.get("tokenStore")?.revokeAllForSubject(identifier, now);
23
23
  } catch {}
24
24
  }
25
25
  /**
@@ -41,7 +41,7 @@ const CRC_TABLE = (() => {
41
41
  /** CRC32 (IEEE) d'une chaîne ASCII — entier non signé 32 bits. */
42
42
  function crc32(input) {
43
43
  let crc = 4294967295;
44
- for (let i = 0; i < input.length; i++) crc = CRC_TABLE[(crc ^ input.charCodeAt(i)) & 255] ^ crc >>> 8;
44
+ for (let i = 0; i < input.length; i++) crc = (CRC_TABLE[(crc ^ input.charCodeAt(i)) & 255] ?? 0) ^ crc >>> 8;
45
45
  return (crc ^ 4294967295) >>> 0;
46
46
  }
47
47
  /** Chunk de checksum (6 car. base64url) du payload `prefix_pubid_secret`. */
@@ -1,7 +1,6 @@
1
1
  import { assertPageQuery } from "nodefony";
2
2
  //#region nodefony/src/audit/MemoryAuditStore.ts
3
3
  const DEFAULT_MAX_ENTRIES = 1e4;
4
- const DEFAULT_LIMIT = 100;
5
4
  const MAX_LIMIT = 500;
6
5
  /**
7
6
  * Sépare les deux composantes du curseur. Le curseur est **composite**
@@ -44,12 +43,9 @@ var MemoryAuditStore = class {
44
43
  }
45
44
  listPage(query) {
46
45
  assertPageQuery(query, "cursor");
47
- const limit = Math.min(Math.max(1, query.limit ?? DEFAULT_LIMIT), MAX_LIMIT);
46
+ const limit = Math.min(Math.max(1, query.limit), MAX_LIMIT);
48
47
  const matched = [];
49
- for (let i = 0; i < this.#events.length; i++) {
50
- const event = this.#events[i];
51
- if (this.#matches(event, query)) matched.push(event);
52
- }
48
+ for (const event of this.#events) if (this.#matches(event, query)) matched.push(event);
53
49
  const total = matched.length;
54
50
  matched.sort((a, b) => a.ts !== b.ts ? b.ts - a.ts : a.id < b.id ? 1 : -1);
55
51
  let page = matched;
@@ -60,7 +56,7 @@ var MemoryAuditStore = class {
60
56
  }
61
57
  const hasNext = page.length > limit;
62
58
  const items = hasNext ? page.slice(0, limit) : page;
63
- const last = items[items.length - 1];
59
+ const last = items.at(-1);
64
60
  return Promise.resolve({
65
61
  items,
66
62
  limit,
@@ -87,7 +83,7 @@ var MemoryAuditStore = class {
87
83
  gc(now = this.#now()) {
88
84
  const threshold = now - this.#retentionMs;
89
85
  let purged = 0;
90
- while (this.#events.length > 0 && this.#events[0].ts < threshold) {
86
+ while ((this.#events[0]?.ts ?? threshold) < threshold) {
91
87
  this.#events.shift();
92
88
  purged++;
93
89
  }
@@ -39,11 +39,15 @@ function createAuditBridge(source, publish, channel, opts = {}) {
39
39
  let timer = null;
40
40
  const flush = () => {
41
41
  timer = null;
42
- if (count === 0 && dropped === 0) return;
43
- const events = new Array(count);
44
- for (let i = 0; i < count; i++) events[i] = ring[(head + i) % maxBatch];
42
+ const buf = ring;
43
+ if (buf === null || count === 0 && dropped === 0) return;
44
+ const events = [];
45
+ for (let i = 0; i < count; i++) {
46
+ const event = buf[(head + i) % maxBatch];
47
+ if (event !== void 0) events.push(event);
48
+ }
45
49
  const d = dropped;
46
- for (let i = 0; i < maxBatch; i++) ring[i] = void 0;
50
+ for (let i = 0; i < maxBatch; i++) buf[i] = void 0;
47
51
  head = 0;
48
52
  count = 0;
49
53
  dropped = 0;
@@ -53,7 +57,7 @@ function createAuditBridge(source, publish, channel, opts = {}) {
53
57
  });
54
58
  };
55
59
  const onEvent = (event) => {
56
- if (ring === null) ring = new Array(maxBatch);
60
+ ring ??= new Array(maxBatch);
57
61
  if (count === maxBatch) {
58
62
  ring[head] = event;
59
63
  head = (head + 1) % maxBatch;
@@ -50,14 +50,14 @@ var ApiKeyAuthenticator = class {
50
50
  }
51
51
  /** La requête porte-t-elle un `Authorization: Bearer <prefix>_…` ? (test bon marché) */
52
52
  supports(context) {
53
- const auth = context.request?.headers?.authorization;
53
+ const auth = context.request?.headers.authorization;
54
54
  if (typeof auth !== "string") return false;
55
55
  const token = bearerToken(auth);
56
56
  return token !== null && looksLikeApiKey(token, this.#prefix);
57
57
  }
58
58
  /** Extrait la valeur brute (non vérifiée) → portée par un `UserToken` type `"apikey"`. */
59
59
  createToken(context) {
60
- const auth = context.request?.headers?.authorization;
60
+ const auth = context.request?.headers.authorization;
61
61
  return Promise.resolve(new UserToken("apikey", bearerToken(auth) ?? ""));
62
62
  }
63
63
  /**
@@ -77,7 +77,7 @@ var ApiKeyAuthenticator = class {
77
77
  const store = this.#resolveStore();
78
78
  const record = await store.findByHash(parsed.secretHash);
79
79
  const now = Date.now();
80
- if (!record || record.kind !== "pat" || record.revokedAt !== null || record.expiresAt !== null && record.expiresAt <= now) throw new AuthenticationError(INVALID_TOKEN);
80
+ if (record?.kind !== "pat" || record.revokedAt !== null || record.expiresAt !== null && record.expiresAt <= now) throw new AuthenticationError(INVALID_TOKEN);
81
81
  const invalidBefore = await store.getInvalidBefore(record.subjectId);
82
82
  if (invalidBefore !== null && record.createdAt < invalidBefore) throw new AuthenticationError(INVALID_TOKEN);
83
83
  const user = await this.#resolveUserOrReject(record.subjectId);
@@ -83,7 +83,7 @@ var ExternalJwtAuthenticator = class {
83
83
  * un jeton d'un émetteur inconnu n'a pas à provoquer le moindre travail.
84
84
  */
85
85
  supports(context) {
86
- const auth = context.request?.headers?.authorization;
86
+ const auth = context.request?.headers.authorization;
87
87
  if (typeof auth !== "string") return false;
88
88
  const raw = bearerToken(auth);
89
89
  if (raw === null) return false;
@@ -104,7 +104,7 @@ var ExternalJwtAuthenticator = class {
104
104
  * visée.
105
105
  */
106
106
  createToken(context) {
107
- const auth = context.request?.headers?.authorization;
107
+ const auth = context.request?.headers.authorization;
108
108
  const token = new UserToken("external-jwt", bearerToken(auth) ?? "");
109
109
  const area = context.security;
110
110
  if (area?.resource) token.setAttribute(AUDIENCE, area.resource);
@@ -100,7 +100,7 @@ var FirewallRealtimeAuthenticator = class {
100
100
  const jti = issued.getAttribute("jti") ?? (typeof claims.jti === "string" ? claims.jti : null);
101
101
  const resolveStore = this.#resolveStore;
102
102
  if (expMs === null && (resolveStore === null || jti === null && iatMs === null)) {
103
- Nodefony.getKernel()?.log?.(`Realtime ${type} token "${identifier}" carries neither an expiry nor any revocation handle ("jti"/"iat") — connection will be revoked (fail-closed). Ensure the authenticator forwards the token claims.`, "WARNING");
103
+ Nodefony.getKernel()?.log(`Realtime ${type} token "${identifier}" carries neither an expiry nor any revocation handle ("jti"/"iat") — connection will be revoked (fail-closed). Ensure the authenticator forwards the token claims.`, "WARNING");
104
104
  return () => Promise.resolve(false);
105
105
  }
106
106
  return async (nowMs = Date.now()) => {
@@ -149,7 +149,7 @@ function buildSessionRevalidator(identifier) {
149
149
  const id = session?.id;
150
150
  const storage = session?.storage;
151
151
  if (typeof id !== "string" || !storage || typeof storage.read !== "function") {
152
- Nodefony.getKernel()?.log?.(`Realtime session token "${identifier}" has no revalidatable session at handshake — connection will be revoked (fail-closed). Ensure the realtime zone runs after startSession.`, "WARNING");
152
+ Nodefony.getKernel()?.log(`Realtime session token "${identifier}" has no revalidatable session at handshake — connection will be revoked (fail-closed). Ensure the realtime zone runs after startSession.`, "WARNING");
153
153
  return () => Promise.resolve(false);
154
154
  }
155
155
  return async () => {
@@ -58,7 +58,7 @@ var JwtAuthenticator = class {
58
58
  * qu'un credential qui disparaîtrait sans laisser de trace.
59
59
  */
60
60
  supports(context) {
61
- const auth = context.request?.headers?.authorization;
61
+ const auth = context.request?.headers.authorization;
62
62
  if (typeof auth !== "string") return false;
63
63
  const token = bearerToken(auth);
64
64
  if (token === null || !COMPACT_JWS.test(token)) return false;
@@ -67,7 +67,7 @@ var JwtAuthenticator = class {
67
67
  }
68
68
  /** Extrait le token brut (non vérifié) → porté par un `UserToken` type `"jwt"`. */
69
69
  createToken(context) {
70
- const auth = context.request?.headers?.authorization;
70
+ const auth = context.request?.headers.authorization;
71
71
  return Promise.resolve(new UserToken("jwt", bearerToken(auth) ?? ""));
72
72
  }
73
73
  /**