@nodefony/security 10.0.0-alpha.9 → 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 (68) hide show
  1. package/dist/nodefony/command/security-token.js +4 -4
  2. package/dist/nodefony/command/security-user-delete.js +2 -2
  3. package/dist/nodefony/command/security-user-list.js +1 -1
  4. package/dist/nodefony/service/apiKeys.js +2 -2
  5. package/dist/nodefony/service/auditService.js +3 -3
  6. package/dist/nodefony/service/authFlow.js +2 -2
  7. package/dist/nodefony/service/authorization.js +3 -4
  8. package/dist/nodefony/service/firewall.js +21 -9
  9. package/dist/nodefony/service/oauth2.js +5 -3
  10. package/dist/nodefony/service/securityHeaders.js +1 -1
  11. package/dist/nodefony/service/tokenService.js +27 -18
  12. package/dist/nodefony/service/webAuthn.js +38 -31
  13. package/dist/nodefony/service/webhooks.js +50 -41
  14. package/dist/nodefony/src/admin/SecurityAdminApi.js +2 -1
  15. package/dist/nodefony/src/admin/WebhookAdminApi.js +1 -0
  16. package/dist/nodefony/src/admin/userRevocationCascade.js +1 -1
  17. package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -1
  18. package/dist/nodefony/src/audit/MemoryAuditStore.js +4 -8
  19. package/dist/nodefony/src/audit/auditBridge.js +9 -5
  20. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -3
  21. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +2 -2
  22. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +2 -2
  23. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +2 -2
  24. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +5 -3
  25. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +2 -2
  26. package/dist/nodefony/src/csp.js +4 -5
  27. package/dist/nodefony/src/oauth/providers/github.js +1 -1
  28. package/dist/nodefony/src/realtime/frameAuthorizer.js +6 -6
  29. package/dist/nodefony/src/token/JwtKeystore.js +7 -6
  30. package/dist/nodefony/src/token/MemoryTokenStore.js +1 -1
  31. package/dist/nodefony/src/token/jwtRuntime.js +13 -1
  32. package/dist/nodefony/src/token/tokenCriteria.js +1 -0
  33. package/dist/nodefony/src/totp/totpCrypto.js +9 -9
  34. package/dist/nodefony/src/webhook/WebhookDispatcher.js +4 -3
  35. package/dist/types/nodefony/contracts/IAuditEvent.d.ts +6 -2
  36. package/dist/types/nodefony/contracts/IFirewall.d.ts +2 -2
  37. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +5 -5
  38. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +4 -4
  39. package/dist/types/nodefony/contracts/ITokenStore.d.ts +3 -3
  40. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +4 -4
  41. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +1 -1
  42. package/dist/types/nodefony/errors/InvalidTargetError.d.ts +1 -1
  43. package/dist/types/nodefony/errors/UnverifiableTokenError.d.ts +1 -1
  44. package/dist/types/nodefony/service/firewall.d.ts +3 -2
  45. package/dist/types/nodefony/service/securityHeaders.d.ts +5 -5
  46. package/dist/types/nodefony/src/SecuredArea.d.ts +2 -2
  47. package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +5 -3
  48. package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +2 -2
  49. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +2 -2
  50. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +3 -3
  51. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +2 -2
  52. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +2 -2
  53. package/dist/types/nodefony/src/token/jwtRuntime.d.ts +10 -0
  54. package/docs/api-keys.md +15 -15
  55. package/docs/audit.md +18 -13
  56. package/docs/authenticators.md +6 -6
  57. package/docs/authorization.md +15 -15
  58. package/docs/cors.md +6 -6
  59. package/docs/csrf.md +7 -7
  60. package/docs/external-jwt.md +17 -9
  61. package/docs/firewall.md +7 -7
  62. package/docs/headers.md +13 -13
  63. package/docs/index.md +3 -1
  64. package/docs/oauth2.md +2 -2
  65. package/docs/tokens.md +6 -6
  66. package/docs/webauthn.md +13 -13
  67. package/docs/webhooks.md +9 -12
  68. package/package.json +13 -13
@@ -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. */
@@ -262,14 +263,14 @@ var WebhookService = class extends Service {
262
263
  * @throws SsrfError si l'URL est invalide / cible non publique.
263
264
  */
264
265
  async register(input) {
265
- this.#assertReady();
266
- await this.#assertSafeUrl(input.url);
266
+ const { store, key, config } = this.#assertReady();
267
+ await this.#assertSafeUrl(config, input.url);
267
268
  const secret = generateSecret();
268
269
  const now = Date.now();
269
270
  const endpoint = {
270
271
  id: generateId(),
271
272
  url: input.url,
272
- secretEnc: encryptSecret(Buffer.from(secret, "utf8"), this.#key),
273
+ secretEnc: encryptSecret(Buffer.from(secret, "utf8"), key),
273
274
  events: [...input.events],
274
275
  enabled: input.enabled ?? true,
275
276
  description: input.description ?? null,
@@ -283,7 +284,7 @@ var WebhookService = class extends Service {
283
284
  failureCount: 0,
284
285
  metadata: input.metadata ?? {}
285
286
  };
286
- await this.#store.save(endpoint);
287
+ await store.save(endpoint);
287
288
  this.#endpoints?.set(endpoint.id, endpoint);
288
289
  return {
289
290
  endpoint: toSummary(endpoint),
@@ -298,8 +299,7 @@ var WebhookService = class extends Service {
298
299
  * @returns la page, chaque endpoint réduit à sa vue publique.
299
300
  */
300
301
  async listPage(query) {
301
- this.#assertReady();
302
- const page = await this.#store.listPage(query);
302
+ const page = await this.#assertReady().store.listPage(query);
303
303
  return {
304
304
  ...page,
305
305
  items: page.items.map(toSummary)
@@ -312,8 +312,7 @@ var WebhookService = class extends Service {
312
312
  * @returns le compte exact, ou `-1` si le backend ne sait pas compter.
313
313
  */
314
314
  async countEndpoints(query) {
315
- this.#assertReady();
316
- return this.#store.countEndpoints(query);
315
+ return this.#assertReady().store.countEndpoints(query);
317
316
  }
318
317
  /**
319
318
  * Les compteurs de tête de la console — posés sur la collection ENTIÈRE, pas
@@ -326,16 +325,15 @@ var WebhookService = class extends Service {
326
325
  * @param query - filtres à appliquer avant comptage (sans fenêtre).
327
326
  */
328
327
  async countWebhookFacets(query) {
329
- this.#assertReady();
330
- return countFacets(WEBHOOK_FACETS, (facet) => this.#store.countEndpoints({
328
+ const { store } = this.#assertReady();
329
+ return countFacets(WEBHOOK_FACETS, (facet) => store.countEndpoints({
331
330
  ...query,
332
331
  ...facet
333
332
  }));
334
333
  }
335
334
  /** Un endpoint par id (vue publique), ou `null`. */
336
335
  async getEndpoint(id) {
337
- this.#assertReady();
338
- const found = await this.#store.findById(id);
336
+ const found = await this.#assertReady().store.findById(id);
339
337
  return found ? toSummary(found) : null;
340
338
  }
341
339
  /**
@@ -344,15 +342,15 @@ var WebhookService = class extends Service {
344
342
  * ou `null` si absent.
345
343
  */
346
344
  async update(id, patch) {
347
- this.#assertReady();
348
- const current = await this.#store.findById(id);
345
+ const { store, config } = this.#assertReady();
346
+ const current = await store.findById(id);
349
347
  if (!current) return null;
350
- 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);
351
349
  const applied = {
352
350
  ...patch,
353
351
  updatedAt: Date.now()
354
352
  };
355
- await this.#store.update(id, applied);
353
+ await store.update(id, applied);
356
354
  const next = {
357
355
  ...current,
358
356
  ...applied
@@ -369,15 +367,15 @@ var WebhookService = class extends Service {
369
367
  * L'ancien cesse immédiatement d'être valide. `null` si l'endpoint est absent.
370
368
  */
371
369
  async rotateSecret(id) {
372
- this.#assertReady();
373
- const current = await this.#store.findById(id);
370
+ const { store, key } = this.#assertReady();
371
+ const current = await store.findById(id);
374
372
  if (!current) return null;
375
373
  const secret = generateSecret();
376
374
  const patch = {
377
- secretEnc: encryptSecret(Buffer.from(secret, "utf8"), this.#key),
375
+ secretEnc: encryptSecret(Buffer.from(secret, "utf8"), key),
378
376
  updatedAt: Date.now()
379
377
  };
380
- await this.#store.update(id, patch);
378
+ await store.update(id, patch);
381
379
  const next = {
382
380
  ...current,
383
381
  ...patch
@@ -393,16 +391,16 @@ var WebhookService = class extends Service {
393
391
  * par l'appelant). `null` si absent.
394
392
  */
395
393
  async revealSecret(id) {
396
- this.#assertReady();
397
- const current = await this.#store.findById(id);
394
+ const { store, key } = this.#assertReady();
395
+ const current = await store.findById(id);
398
396
  if (!current) return null;
399
- return decryptSecret(current.secretEnc, this.#key).toString("utf8");
397
+ return decryptSecret(current.secretEnc, key).toString("utf8");
400
398
  }
401
399
  /** Supprime un endpoint. Retourne `false` si absent. */
402
400
  async delete(id) {
403
- this.#assertReady();
404
- if (!await this.#store.findById(id)) return false;
405
- await this.#store.delete(id);
401
+ const { store } = this.#assertReady();
402
+ if (!await store.findById(id)) return false;
403
+ await store.delete(id);
406
404
  this.#endpoints?.delete(id);
407
405
  this.#deliveries?.delete(id);
408
406
  return true;
@@ -422,7 +420,7 @@ var WebhookService = class extends Service {
422
420
  * sur l'issue FINALE d'une livraison.
423
421
  */
424
422
  #recordDelivery(id, rec) {
425
- if (this.#deliveries === null) this.#deliveries = /* @__PURE__ */ new Map();
423
+ this.#deliveries ??= /* @__PURE__ */ new Map();
426
424
  let ring = this.#deliveries.get(id);
427
425
  if (ring === void 0) {
428
426
  ring = [];
@@ -461,13 +459,11 @@ var WebhookService = class extends Service {
461
459
  }
462
460
  /** Déchiffre le secret de signature d'un endpoint (pour signer une livraison). */
463
461
  decryptEndpointSecret(endpoint) {
464
- this.#assertReady();
465
- return decryptSecret(endpoint.secretEnc, this.#key);
462
+ return decryptSecret(endpoint.secretEnc, this.#assertReady().key);
466
463
  }
467
464
  /** Politique de livraison (tolérance/retries/timeout…) issue de la config. */
468
465
  getDeliveryPolicy() {
469
- this.#assertReady();
470
- const w = this.#config.webhooks;
466
+ const w = this.#assertReady().config.webhooks;
471
467
  return {
472
468
  timestampToleranceS: w.timestampToleranceS,
473
469
  maxRetries: w.maxRetries,
@@ -485,12 +481,14 @@ var WebhookService = class extends Service {
485
481
  * l'endpoint au-delà du seuil (façon GitHub). Le succès remet le compteur à 0.
486
482
  */
487
483
  async markDelivery(id, result) {
488
- if (!this.#ready || !this.#store) return;
489
- 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);
490
488
  if (!current) return;
491
489
  const failureCount = result.ok ? 0 : current.failureCount + 1;
492
490
  const now = Date.now();
493
- const threshold = this.#config.webhooks.autoDisableThreshold;
491
+ const threshold = config.webhooks.autoDisableThreshold;
494
492
  const disable = !result.ok && threshold > 0 && failureCount >= threshold;
495
493
  const patch = {
496
494
  lastDeliveryAt: now,
@@ -511,17 +509,28 @@ var WebhookService = class extends Service {
511
509
  reason: "max_failures"
512
510
  });
513
511
  }
514
- await this.#store.update(id, patch);
512
+ await store.update(id, patch);
515
513
  this.#endpoints?.set(id, {
516
514
  ...current,
517
515
  ...patch
518
516
  });
519
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
+ */
520
524
  #assertReady() {
521
- 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
+ };
522
531
  }
523
- async #assertSafeUrl(url) {
524
- const w = this.#config.webhooks;
532
+ async #assertSafeUrl(config, url) {
533
+ const w = config.webhooks;
525
534
  await assertPublicUrl(url, {
526
535
  allowPrivate: !w.denyPrivateIps,
527
536
  allowHttp: w.allowHttp
@@ -529,7 +538,7 @@ var WebhookService = class extends Service {
529
538
  }
530
539
  /** Re-contrôle SSRF avant livraison → IP validées à pinner (anti-rebinding). */
531
540
  async #resolveTarget(url) {
532
- const w = this.#config.webhooks;
541
+ const w = this.#assertReady().config.webhooks;
533
542
  const { addresses } = await assertPublicUrl(url, {
534
543
  allowPrivate: !w.denyPrivateIps,
535
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
  /**
@@ -13,9 +13,11 @@ import { UserToken } from "../token/UserToken.js";
13
13
  * démarre jamais rien lui-même. L'identité est re-résolue à CHAQUE requête
14
14
  * via {@link resolveSessionIdentity} (rôles frais, révocation immédiate).
15
15
  *
16
- * Pas de `challenge()` : une session absente/expirée donne un 401 nu — le
17
- * client web redirige vers son écran de login, jamais de popup Basic. Si la
18
- * zone liste aussi `userpassword`, le firewall pose SON challenge (RFC 7235).
16
+ * Pas de `challenge()` : sur une zone à session seule, le firewall pose son
17
+ * défi de repli `Session realm="nodefony"` (RFC 9110 §15.5.2 : tout 401 en
18
+ * porte un) — schéma inconnu des navigateurs, donc jamais de popup Basic ; le
19
+ * client web redirige vers son écran de login. Si la zone liste aussi
20
+ * `userpassword`, le firewall pose SON challenge.
19
21
  */
20
22
  var SessionAuthenticator = class {
21
23
  name = "session";
@@ -35,12 +35,12 @@ var UserPasswordAuthenticator = class {
35
35
  }
36
36
  /** La requête porte-t-elle un en-tête `Authorization: Basic ...` ? */
37
37
  supports(context) {
38
- const auth = context.request?.headers?.authorization;
38
+ const auth = context.request?.headers.authorization;
39
39
  return typeof auth === "string" && BASIC_SCHEME.test(auth);
40
40
  }
41
41
  /** Décode l'enveloppe Basic — un contenu malformé donne un credential vide (échec uniforme). */
42
42
  createToken(context) {
43
- const auth = context.request?.headers?.authorization;
43
+ const auth = context.request?.headers.authorization;
44
44
  const decoded = Buffer.from(auth.replace(BASIC_SCHEME, ""), "base64").toString("utf8");
45
45
  const sep = decoded.indexOf(":");
46
46
  const credentials = sep > 0 ? {
@@ -7,10 +7,9 @@
7
7
  function parseCsp(csp) {
8
8
  const out = [];
9
9
  for (const part of csp.split(";")) {
10
- const tokens = part.trim().split(/\s+/).filter(Boolean);
11
- if (tokens.length === 0) continue;
12
- const name = tokens[0];
13
- out.push([name, tokens.slice(1)]);
10
+ const [name, ...sources] = part.trim().split(/\s+/).filter(Boolean);
11
+ if (name === void 0) continue;
12
+ out.push([name, sources]);
14
13
  }
15
14
  return out;
16
15
  }
@@ -39,7 +38,7 @@ function mergeCspFragments(base, fragments) {
39
38
  for (const [name, src] of directives) index.set(name, src);
40
39
  for (const fragment of fragments) for (const name in fragment) {
41
40
  const additions = fragment[name];
42
- if (additions.length === 0) continue;
41
+ if (additions === void 0 || additions.length === 0) continue;
43
42
  let sources = index.get(name);
44
43
  if (!sources) {
45
44
  sources = [];
@@ -69,7 +69,7 @@ function createGithubProvider(ctx) {
69
69
  let emailVerified = email !== null;
70
70
  if (email === null) {
71
71
  const list = await ghGet(`${API}/user/emails`, accessToken);
72
- const primary = list.find((e) => e.primary === true) ?? list[0] ?? null;
72
+ const primary = list.find((e) => e.primary === true) ?? list.at(0) ?? null;
73
73
  if (primary && typeof primary.email === "string") {
74
74
  email = primary.email;
75
75
  emailVerified = primary.verified === true;
@@ -161,7 +161,7 @@ function containsCI(s, needle) {
161
161
  * pas en altérant la casse du namespace.
162
162
  */
163
163
  function matchSystemPolicy(channel, rules) {
164
- for (let i = 0; i < rules.length; i++) if (startsWithCI(channel, rules[i].prefix)) return floorReserved(channel, rules[i].policy);
164
+ for (const rule of rules) if (startsWithCI(channel, rule.prefix)) return floorReserved(channel, rule.policy);
165
165
  if (containsCI(channel, ":health") || containsCI(channel, ":stats")) return SYSTEM_CHANNEL_POLICY;
166
166
  return null;
167
167
  }
@@ -178,7 +178,7 @@ function matchSystemPolicy(channel, rules) {
178
178
  */
179
179
  function floorReserved(channel, policy) {
180
180
  if (policy.authenticated) return policy;
181
- for (let i = 0; i < RESERVED_FLOOR_PREFIXES.length; i++) if (startsWithCI(channel, RESERVED_FLOOR_PREFIXES[i])) return {
181
+ for (const prefix of RESERVED_FLOOR_PREFIXES) if (startsWithCI(channel, prefix)) return {
182
182
  ...policy,
183
183
  authenticated: true
184
184
  };
@@ -197,7 +197,7 @@ function satisfies(policy, token, firewall) {
197
197
  if (policy.roles && policy.roles.length > 0) {
198
198
  const userRoles = token.getRoles();
199
199
  let granted = false;
200
- for (let i = 0; i < policy.roles.length; i++) if (firewall.hasRole(userRoles, policy.roles[i])) {
200
+ for (const role of policy.roles) if (firewall.hasRole(userRoles, role)) {
201
201
  granted = true;
202
202
  break;
203
203
  }
@@ -206,7 +206,7 @@ function satisfies(policy, token, firewall) {
206
206
  if (policy.scopes && policy.scopes.length > 0) {
207
207
  const userScopes = token.getScopes();
208
208
  let granted = false;
209
- for (let i = 0; i < policy.scopes.length; i++) if (userScopes.includes(policy.scopes[i])) {
209
+ for (const scope of policy.scopes) if (userScopes.includes(scope)) {
210
210
  granted = true;
211
211
  break;
212
212
  }
@@ -279,9 +279,9 @@ function buildFrameAuthorizer(firewall, options) {
279
279
  return (frame, token) => {
280
280
  const f = frame;
281
281
  const method = f?.method;
282
- if (method === "api.request") return authorizeApiRequest(firewall, f.params, token, onDeny);
282
+ if (method === "api.request") return authorizeApiRequest(firewall, f?.params, token, onDeny);
283
283
  if (method === "subscribe") {
284
- const channel = f.params?.channel;
284
+ const channel = (f?.params)?.channel;
285
285
  if (typeof channel !== "string") return true;
286
286
  return authorizeChannel(channel, token, firewall, resolver, systemRules, onDeny);
287
287
  }
@@ -31,7 +31,7 @@ const CLEANED_DIRS = ["var", "tmp"];
31
31
  function keystoreLeaksIntoImage(dir) {
32
32
  const normalized = dir.replace(/\\/gu, "/").replace(/^\.\//u, "");
33
33
  if (normalized.startsWith("/") || /^[A-Za-z]:/u.test(normalized)) return null;
34
- const first = normalized.split("/")[0];
34
+ const [first = ""] = normalized.split("/");
35
35
  if (CLEANED_DIRS.includes(first)) return null;
36
36
  return `JWT keystore: « ${dir} » n'est PAS sous ${CLEANED_DIRS.map((d) => `\`${d}/\``).join(" ni ")}, les seuls dossiers que le Dockerfile généré efface avant de fabriquer l'image. La clé privée de signature partira donc dans ton image de conteneur, où elle reste lisible par quiconque la télécharge — même effacée par une couche suivante. Deux issues : déplacer le dossier sous \`var/\` (ex. \`var/keys\`), ou injecter la clé par jwt.keystore.keySetJson depuis l'environnement, ce qui est la voie de production.`;
37
37
  }
@@ -138,9 +138,9 @@ var JwtKeystore = class {
138
138
  kid: stored.kid,
139
139
  privateKey: imported,
140
140
  publicJwk: {
141
- kty: stored.kty,
142
- crv: stored.crv,
143
- x: stored.x,
141
+ ...stored.kty !== void 0 && { kty: stored.kty },
142
+ ...stored.crv !== void 0 && { crv: stored.crv },
143
+ ...stored.x !== void 0 && { x: stored.x },
144
144
  kid: stored.kid,
145
145
  use: "sig",
146
146
  alg: "EdDSA"
@@ -148,9 +148,10 @@ var JwtKeystore = class {
148
148
  createdAt: stored.createdAt ?? Date.now()
149
149
  });
150
150
  }
151
- if (loaded.length === 0) throw new Error("JwtKeystore: keyset vide");
151
+ const [firstKey] = loaded;
152
+ if (firstKey === void 0) throw new Error("JwtKeystore: keyset vide");
152
153
  this.#keys = loaded;
153
- this.#activeKid = keyset.active && loaded.some((k) => k.kid === keyset.active) ? keyset.active : loaded[0].kid;
154
+ this.#activeKid = keyset.active && loaded.some((k) => k.kid === keyset.active) ? keyset.active : firstKey.kid;
154
155
  }
155
156
  #parseKeyset(json) {
156
157
  let parsed;
@@ -219,7 +219,7 @@ var MemoryTokenStore = class {
219
219
  set.add(id);
220
220
  }
221
221
  #revokeRecord(record, reason) {
222
- if (record && record.revokedAt === null) {
222
+ if (record?.revokedAt === null) {
223
223
  record.revokedAt = this.#now();
224
224
  record.revokedReason = reason;
225
225
  }
@@ -15,5 +15,17 @@ function resolveJwtRuntime(jwt) {
15
15
  alg: "EdDSA"
16
16
  };
17
17
  }
18
+ /**
19
+ * Audience inscrite dans un jeton quand aucune ressource n'est demandée : la
20
+ * première de la liste blanche. La liste n'est jamais vide après
21
+ * {@link resolveJwtRuntime} ; un runtime construit à la main qui la laisserait
22
+ * vide retombe sur la même règle — l'application est sa propre audience.
23
+ *
24
+ * @param runtime - paramètres JWT résolus
25
+ * @returns l'audience par défaut
26
+ */
27
+ function defaultAudience(runtime) {
28
+ return runtime.audiences[0] ?? runtime.issuer;
29
+ }
18
30
  //#endregion
19
- export { resolveJwtRuntime };
31
+ export { defaultAudience, resolveJwtRuntime };
@@ -28,6 +28,7 @@ function tokenStatusCriteria(status, now) {
28
28
  revokedAt: { $null: true },
29
29
  $or: [{ expiresAt: { $null: true } }, { expiresAt: { $gt: now } }]
30
30
  };
31
+ case void 0:
31
32
  default: return {};
32
33
  }
33
34
  }