@nodefony/security 10.0.0-alpha.9 → 10.0.0-beta.2

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 (74) hide show
  1. package/README.md +7 -5
  2. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/index.js +2 -2
  5. package/dist/nodefony/command/security-token.js +4 -4
  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/service/apiKeys.js +2 -2
  9. package/dist/nodefony/service/auditService.js +3 -3
  10. package/dist/nodefony/service/authFlow.js +2 -2
  11. package/dist/nodefony/service/authorization.js +3 -4
  12. package/dist/nodefony/service/firewall.js +21 -9
  13. package/dist/nodefony/service/oauth2.js +5 -3
  14. package/dist/nodefony/service/securityHeaders.js +13 -2
  15. package/dist/nodefony/service/tokenService.js +27 -18
  16. package/dist/nodefony/service/webAuthn.js +38 -31
  17. package/dist/nodefony/service/webhooks.js +50 -41
  18. package/dist/nodefony/src/admin/SecurityAdminApi.js +2 -1
  19. package/dist/nodefony/src/admin/WebhookAdminApi.js +1 -0
  20. package/dist/nodefony/src/admin/userRevocationCascade.js +1 -1
  21. package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -1
  22. package/dist/nodefony/src/audit/MemoryAuditStore.js +4 -8
  23. package/dist/nodefony/src/audit/auditBridge.js +9 -5
  24. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -3
  25. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +2 -2
  26. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +2 -2
  27. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +2 -2
  28. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +5 -3
  29. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +2 -2
  30. package/dist/nodefony/src/csp.js +4 -5
  31. package/dist/nodefony/src/oauth/providers/github.js +1 -1
  32. package/dist/nodefony/src/realtime/frameAuthorizer.js +6 -6
  33. package/dist/nodefony/src/token/JwtKeystore.js +7 -6
  34. package/dist/nodefony/src/token/MemoryTokenStore.js +1 -1
  35. package/dist/nodefony/src/token/jwtRuntime.js +13 -1
  36. package/dist/nodefony/src/token/tokenCriteria.js +1 -0
  37. package/dist/nodefony/src/totp/totpCrypto.js +9 -9
  38. package/dist/nodefony/src/webhook/WebhookDispatcher.js +4 -3
  39. package/dist/types/nodefony/contracts/IAuditEvent.d.ts +6 -2
  40. package/dist/types/nodefony/contracts/IFirewall.d.ts +2 -2
  41. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +5 -5
  42. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +4 -4
  43. package/dist/types/nodefony/contracts/ITokenStore.d.ts +3 -3
  44. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +4 -4
  45. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +1 -1
  46. package/dist/types/nodefony/errors/InvalidTargetError.d.ts +1 -1
  47. package/dist/types/nodefony/errors/UnverifiableTokenError.d.ts +1 -1
  48. package/dist/types/nodefony/service/firewall.d.ts +3 -2
  49. package/dist/types/nodefony/service/securityHeaders.d.ts +7 -6
  50. package/dist/types/nodefony/src/SecuredArea.d.ts +2 -2
  51. package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +5 -3
  52. package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +2 -2
  53. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +2 -2
  54. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +3 -3
  55. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +2 -2
  56. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +2 -2
  57. package/dist/types/nodefony/src/token/jwtRuntime.d.ts +10 -0
  58. package/docs/api-keys.md +16 -16
  59. package/docs/audit.md +27 -20
  60. package/docs/authenticators.md +8 -8
  61. package/docs/authorization.md +15 -15
  62. package/docs/cors.md +7 -7
  63. package/docs/csrf.md +8 -8
  64. package/docs/external-jwt.md +19 -10
  65. package/docs/firewall.md +28 -28
  66. package/docs/headers.md +20 -20
  67. package/docs/index.md +6 -4
  68. package/docs/lexique.md +15 -15
  69. package/docs/oauth2.md +23 -20
  70. package/docs/tokens.md +11 -10
  71. package/docs/totp.md +15 -14
  72. package/docs/webauthn.md +51 -54
  73. package/docs/webhooks.md +27 -30
  74. package/package.json +13 -13
@@ -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
  }
@@ -27,8 +27,8 @@ function base32Encode(buf) {
27
27
  let bits = 0;
28
28
  let value = 0;
29
29
  let out = "";
30
- for (let i = 0; i < buf.length; i++) {
31
- value = value << 8 | buf[i];
30
+ for (const byte of buf) {
31
+ value = value << 8 | byte;
32
32
  bits += 8;
33
33
  while (bits >= 5) {
34
34
  out += BASE32_ALPHABET[value >>> bits - 5 & 31];
@@ -48,9 +48,9 @@ function base32Decode(input) {
48
48
  let bits = 0;
49
49
  let value = 0;
50
50
  const out = [];
51
- for (let i = 0; i < clean.length; i++) {
52
- const idx = BASE32_ALPHABET.indexOf(clean[i]);
53
- if (idx === -1) throw new Error(`base32: caractère invalide « ${clean[i]} »`);
51
+ for (const char of clean) {
52
+ const idx = BASE32_ALPHABET.indexOf(char);
53
+ if (idx === -1) throw new Error(`base32: caractère invalide « ${char} »`);
54
54
  value = value << 5 | idx;
55
55
  bits += 5;
56
56
  if (bits >= 8) {
@@ -85,8 +85,8 @@ function counterBuffer(counter) {
85
85
  */
86
86
  function hotp(secret, counter, digits = TOTP_DEFAULTS.digits, algorithm = TOTP_DEFAULTS.algorithm) {
87
87
  const hs = createHmac(hmacName(algorithm), secret).update(counterBuffer(counter)).digest();
88
- const offset = hs[hs.length - 1] & 15;
89
- return (((hs[offset] & 127) << 24 | (hs[offset + 1] & 255) << 16 | (hs[offset + 2] & 255) << 8 | hs[offset + 3] & 255) % 10 ** digits).toString().padStart(digits, "0");
88
+ const offset = hs.readUInt8(hs.length - 1) & 15;
89
+ return ((hs.readUInt32BE(offset) & 2147483647) % 10 ** digits).toString().padStart(digits, "0");
90
90
  }
91
91
  /** Numéro de tranche temporelle `T` (RFC 6238 §4.2) pour un instant donné. */
92
92
  function totpCounter(epochSec, step = TOTP_DEFAULTS.step, t0 = TOTP_DEFAULTS.t0) {
@@ -216,8 +216,8 @@ function hashRecoveryCode(code) {
216
216
  function matchRecoveryCode(presented, hashes) {
217
217
  const target = Buffer.from(hashRecoveryCode(presented));
218
218
  let found = -1;
219
- for (let i = 0; i < hashes.length; i++) {
220
- const candidate = Buffer.from(hashes[i]);
219
+ for (const [i, hash] of hashes.entries()) {
220
+ const candidate = Buffer.from(hash);
221
221
  if (candidate.length === target.length && timingSafeEqual(candidate, target)) found = i;
222
222
  }
223
223
  return found;
@@ -78,7 +78,7 @@ var WebhookDispatcher = class {
78
78
  /** Empile une livraison ; **abandonne** (best-effort) si la file est pleine. */
79
79
  #enqueue(job) {
80
80
  if (this.#stopped) return;
81
- if (this.#queue === null) this.#queue = [];
81
+ this.#queue ??= [];
82
82
  if (this.#queue.length >= this.#deps.policy.maxQueue) {
83
83
  this.#dropped++;
84
84
  if (this.#dropped === 1 || this.#dropped % 100 === 0) this.#deps.log(`webhooks: file pleine (${this.#deps.policy.maxQueue}) — ${this.#dropped} livraison(s) abandonnée(s) (best-effort)`);
@@ -100,8 +100,9 @@ var WebhookDispatcher = class {
100
100
  #pump() {
101
101
  if (this.#queue === null || this.#stopped) return;
102
102
  const max = this.#deps.policy.maxConcurrent;
103
- while (this.#inFlight < max && this.#queue.length > 0) {
103
+ while (this.#inFlight < max) {
104
104
  const job = this.#queue.shift();
105
+ if (job === void 0) break;
105
106
  this.#inFlight++;
106
107
  this.#process(job).finally(() => {
107
108
  this.#inFlight--;
@@ -178,7 +179,7 @@ var WebhookDispatcher = class {
178
179
  /** Replanifie la livraison après backoff (repasse par la file bornée). */
179
180
  #scheduleRetry(job) {
180
181
  if (this.#stopped) return;
181
- if (this.#timers === null) this.#timers = /* @__PURE__ */ new Set();
182
+ this.#timers ??= /* @__PURE__ */ new Set();
182
183
  const cancel = this.#deps.schedule(() => {
183
184
  this.#timers?.delete(cancel);
184
185
  this.#enqueue({
@@ -18,7 +18,11 @@
18
18
  * filtre qui ne rend jamais rien induit en erreur — cf `studio/.../auditModel.ts`).
19
19
  * En câbler une = ajouter son émetteur ICI (l'émetteur), puis la métadonnée
20
20
  * d'affichage dans `AUDIT_CATEGORIES`. Émettent réellement : auth, authz, token,
21
- * session, webauthn, ws, webhook, config (`config.update`).
21
+ * session, webauthn, ws, webhook, config (`config.update`, `log.debug.*`).
22
+ *
23
+ * Un émetteur hors de `@nodefony/security` ne peut pas importer ce type et passe
24
+ * la catégorie en `string` : `tests/unit/auditCategoryEmitters.test.ts` relit
25
+ * les sources de tous les paquets pour refuser une catégorie hors contrat.
22
26
  */
23
27
  export type AuditCategory = "auth" | "authz" | "token" | "session" | "oauth" | "webauthn" | "csrf" | "cors" | "ws" | "webhook" | "config";
24
28
  /**
@@ -85,7 +89,7 @@ export interface IAuditEvent {
85
89
  */
86
90
  flags?: IAuditEventFlags;
87
91
  /** Extras applicatifs libres (anti-migration) ; absents par défaut. */
88
- metadata?: Record<string, unknown>;
92
+ metadata?: Record<string, unknown> | undefined;
89
93
  }
90
94
  /**
91
95
  * Champs fournis par l'**émetteur** d'un événement — l'`AuditService` y ajoute
@@ -1,4 +1,4 @@
1
- import type { ContextType } from "@nodefony/http";
1
+ import type { ContextType, IFirewallGate } from "@nodefony/http";
2
2
  import type { IAuthenticator } from "./IAuthenticator.js";
3
3
  import type { ISecuredArea } from "./ISecuredArea.js";
4
4
  import type { CspFragment } from "../src/csp.js";
@@ -11,7 +11,7 @@ import type { IProtectedResourceInput } from "nodefony";
11
11
  * `context.security`. `handleSecurity()` (lazy, seulement si la requête est dans
12
12
  * une zone) exécute CORS → headers → authenticators → Zero Trust → CSRF.
13
13
  */
14
- export interface IFirewall {
14
+ export interface IFirewall extends IFirewallGate {
15
15
  /** Match rapide de zone (pose `context.security`). `true` si zone capturée. */
16
16
  isSecure(context: ContextType): boolean;
17
17
  /** Pipeline complet d'authentification de la zone. Rejette (401/403) ou résout. */
@@ -78,11 +78,11 @@ export interface IFirewallDefensesDescription {
78
78
  frameguard: "deny" | "sameorigin";
79
79
  noSniff: boolean;
80
80
  referrerPolicy: string;
81
- coop?: string;
82
- coep?: string;
83
- corp?: string;
84
- originAgentCluster?: boolean;
85
- permissionsPolicy?: string;
81
+ coop?: string | undefined;
82
+ coep?: string | undefined;
83
+ corp?: string | undefined;
84
+ originAgentCluster?: boolean | undefined;
85
+ permissionsPolicy?: string | undefined;
86
86
  };
87
87
  rateLimit: {
88
88
  enabled: boolean;
@@ -1,4 +1,4 @@
1
- import type { ContextType } from "@nodefony/http";
1
+ import type { ContextType, ISecurityZone } from "@nodefony/http";
2
2
  /**
3
3
  * Zone sécurisée — un pattern d'URL + la liste des authenticators à exécuter.
4
4
  *
@@ -7,7 +7,7 @@ import type { ContextType } from "@nodefony/http";
7
7
  * `defineSecurityConfig({ areas })` — patterns compilés et triés au boot, conflits
8
8
  * détectés au boot (pas au premier match runtime).
9
9
  */
10
- export interface ISecuredArea {
10
+ export interface ISecuredArea extends ISecurityZone {
11
11
  /** Nom de la zone (`"main_api"`, `"admin"`…). */
12
12
  readonly name: string;
13
13
  /** Pattern d'URL compilé. */
@@ -29,7 +29,7 @@ export interface ISecuredArea {
29
29
  /** Noms des authenticators à exécuter (sémantique selon {@link mode}). */
30
30
  readonly authenticators: readonly string[];
31
31
  /** Domaine/vhost de la zone (ex. `admin.exemple.com`). Omis = tous domaines. */
32
- readonly host?: string;
32
+ readonly host?: string | undefined;
33
33
  /**
34
34
  * URI canonique de la ressource protégée par cette zone — l'**audience**
35
35
  * qu'un jeton doit porter pour y être accepté (RFC 8707 §2).
@@ -44,7 +44,7 @@ export interface ISecuredArea {
44
44
  * Omise, une zone reste parfaitement utilisable par les authenticators qui
45
45
  * n'en ont pas besoin (session, mot de passe, jetons maison).
46
46
  */
47
- readonly resource?: string;
47
+ readonly resource?: string | undefined;
48
48
  /**
49
49
  * Zone valable AUSSI pour le WebSocket (frames `api.request` + `subscribe`),
50
50
  * pas seulement HTTP. Défaut `true` (Zero Trust : une zone protégée ferme TOUS
@@ -27,7 +27,7 @@ export interface IResourcePermission {
27
27
  /** Type de ressource (ex. `"repo"`, `"project"`). */
28
28
  type: string;
29
29
  /** Identifiants ciblés (omis = toutes les ressources de ce type). */
30
- ids?: string[];
30
+ ids?: string[] | undefined;
31
31
  /** Permissions accordées sur ces ressources. */
32
32
  perms: Array<"read" | "write">;
33
33
  }
@@ -36,9 +36,9 @@ export interface ITokenUsage {
36
36
  /** Instant d'usage (epoch ms). */
37
37
  at: number;
38
38
  /** IP source, si disponible. */
39
- ip?: string | null;
39
+ ip?: string | null | undefined;
40
40
  /** User-Agent source, si disponible. */
41
- userAgent?: string | null;
41
+ userAgent?: string | null | undefined;
42
42
  }
43
43
  /**
44
44
  * Enregistrement d'un jeton persistant — un PAT (clé API style GitHub) ou un
@@ -46,13 +46,13 @@ export interface ITotpListQuery extends IPageQuery {
46
46
  /** Champs mutables d'un secret TOTP (patch partiel). */
47
47
  export interface TotpSecretUpdate {
48
48
  /** Confirmation de l'enrôlement (epoch ms) — passe le secret en « actif ». */
49
- confirmedAt?: number | null;
49
+ confirmedAt?: number | null | undefined;
50
50
  /** Stock résiduel de codes de récupération hachés (après consommation). */
51
- recoveryCodes?: string[];
51
+ recoveryCodes?: string[] | undefined;
52
52
  /** Dernière tranche `T` validée (anti-rejeu RFC 6238 §5.2). */
53
- lastUsedStep?: number;
53
+ lastUsedStep?: number | undefined;
54
54
  /** Horodatage du dernier usage réussi (epoch ms). */
55
- lastUsedAt?: number;
55
+ lastUsedAt?: number | undefined;
56
56
  }
57
57
  /**
58
58
  * Store **pluggable** du secret TOTP par utilisateur — découple le cœur du backend
@@ -28,7 +28,7 @@ export interface IWebAuthnCredentialSummary {
28
28
  /** Compteur de signatures (anti-clone WebAuthn §6.1.1). */
29
29
  readonly signCount: number;
30
30
  /** Nom donné à l'appareil par l'utilisateur, si renseigné. */
31
- readonly nickname?: string;
31
+ readonly nickname?: string | undefined;
32
32
  /** Enrôlement (epoch ms). */
33
33
  readonly createdAt: number;
34
34
  /** Dernière authentification réussie (epoch ms), ou `null` = jamais servie. */
@@ -23,7 +23,7 @@ export declare class InvalidTargetError extends nodefonyError {
23
23
  /** Code d'erreur OAuth à rendre au client (RFC 6749 §5.2 / RFC 8707 §2). */
24
24
  readonly oauthError = "invalid_target";
25
25
  /** Ce que le client a demandé, tel qu'il l'a écrit — pour le journal. */
26
- readonly requested?: string;
26
+ readonly requested?: string | undefined;
27
27
  /**
28
28
  * @param description - raison, destinée à `error_description` (aucune fuite :
29
29
  * elle qualifie la DEMANDE, jamais la configuration du serveur)
@@ -27,7 +27,7 @@ import { nodefonyError } from "nodefony";
27
27
  */
28
28
  export declare class UnverifiableTokenError extends nodefonyError {
29
29
  /** Cause technique, destinée au JOURNAL — jamais au client. */
30
- readonly detail?: string;
30
+ readonly detail?: string | undefined;
31
31
  /**
32
32
  * @param detail - cause technique pour le journal ; n'apparaît jamais dans le
33
33
  * message rendu au client
@@ -25,8 +25,9 @@ import type { IFirewallDescription, IRoleHierarchyDescription } from "../contrac
25
25
  * d'identité down, câblage manquant) → log ERROR serveur + 401 générique
26
26
  * (aucun détail ne fuite au client).
27
27
  *
28
- * Conformité : tout 401 porte un challenge `WWW-Authenticate` (RFC 7235) fourni
29
- * par le premier authenticator de la zone qui en déclare un.
28
+ * Conformité : tout 401 porte un challenge `WWW-Authenticate` (RFC 9110
29
+ * §15.5.2) — celui du premier authenticator de la zone qui en déclare un, sinon
30
+ * le défi de repli {@link FALLBACK_CHALLENGE}.
30
31
  *
31
32
  * CORS, CSRF et autorisation par décorateurs viennent se brancher en S4/S5.
32
33
  * Toutes les structures sont **lazy** (perf : une app sans zone = zéro alloc).
@@ -13,11 +13,11 @@ export interface ISecurityHeadersOptions {
13
13
  csp: string;
14
14
  cspNonces: boolean;
15
15
  referrerPolicy: string;
16
- coop?: string;
17
- coep?: string;
18
- corp?: string;
19
- originAgentCluster?: boolean;
20
- permissionsPolicy?: string;
16
+ coop?: string | undefined;
17
+ coep?: string | undefined;
18
+ corp?: string | undefined;
19
+ originAgentCluster?: boolean | undefined;
20
+ permissionsPolicy?: string | undefined;
21
21
  }
22
22
  /**
23
23
  * En-têtes de sécurité **applicatifs** de Nodefony (couche `@nodefony/security`,
@@ -43,7 +43,8 @@ export declare class SecurityHeaders {
43
43
  get hasNonce(): boolean;
44
44
  /**
45
45
  * Recompose le CSP en injectant le `nonce` de la requête aux emplacements
46
- * `{{nonce}}`. 1 `join` (segments pré-split au boot). Le nonce est base64 (jamais
46
+ * `{{nonce}}` : une concaténation s'il n'y en a qu'un, un `join` sinon (segments
47
+ * pré-split au boot). Le nonce est base64 (jamais
47
48
  * `'`/`;`/espace) → aucune évasion possible du token CSP.
48
49
  *
49
50
  * @param nonce - nonce base64 de la requête (`Context.cspNonce`).
@@ -15,9 +15,9 @@ export declare class SecuredArea implements ISecuredArea {
15
15
  readonly stateless: boolean;
16
16
  readonly mode: "first" | "all";
17
17
  readonly authenticators: readonly string[];
18
- readonly host?: string;
18
+ readonly host?: string | undefined;
19
19
  readonly realtime: boolean;
20
- readonly resource?: string;
20
+ readonly resource?: string | undefined;
21
21
  /**
22
22
  * Rôles exigés par défaut dans la zone, ou `null` quand elle n'en exige
23
23
  * aucun. `null` plutôt qu'un tableau vide : le hot path teste une référence,
@@ -14,9 +14,11 @@ import type { IToken } from "../../contracts/IToken.js";
14
14
  * démarre jamais rien lui-même. L'identité est re-résolue à CHAQUE requête
15
15
  * via {@link resolveSessionIdentity} (rôles frais, révocation immédiate).
16
16
  *
17
- * Pas de `challenge()` : une session absente/expirée donne un 401 nu — le
18
- * client web redirige vers son écran de login, jamais de popup Basic. Si la
19
- * zone liste aussi `userpassword`, le firewall pose SON challenge (RFC 7235).
17
+ * Pas de `challenge()` : sur une zone à session seule, le firewall pose son
18
+ * défi de repli `Session realm="nodefony"` (RFC 9110 §15.5.2 : tout 401 en
19
+ * porte un) — schéma inconnu des navigateurs, donc jamais de popup Basic ; le
20
+ * client web redirige vers son écran de login. Si la zone liste aussi
21
+ * `userpassword`, le firewall pose SON challenge.
20
22
  */
21
23
  export declare class SessionAuthenticator implements IAuthenticator {
22
24
  #private;
@@ -137,9 +137,9 @@ export interface IOAuth2ClientOptions {
137
137
  * Implémentation de `fetch` à employer — la voie pour éprouver l'échange sans
138
138
  * réseau, plutôt que de remplacer le `fetch` global du processus.
139
139
  */
140
- readonly fetch?: typeof globalThis.fetch;
140
+ readonly fetch?: typeof globalThis.fetch | undefined;
141
141
  /** Délai d'attente de l'échange, en millisecondes. */
142
- readonly timeoutMs?: number;
142
+ readonly timeoutMs?: number | undefined;
143
143
  }
144
144
  /**
145
145
  * Client d'un serveur d'autorisation donné : construit l'URL d'autorisation puis
@@ -33,7 +33,7 @@ export interface IOAuthProviderContext {
33
33
  * champ, la forme serait ouverte dans le client et INATTEIGNABLE depuis une
34
34
  * application — donc absente.
35
35
  */
36
- readonly clientAuthMethod?: OAuth2ClientAuthMethod;
36
+ readonly clientAuthMethod?: OAuth2ClientAuthMethod | undefined;
37
37
  /** URL de callback exacte (RFC 9700). */
38
38
  readonly redirectUri: string;
39
39
  /**
@@ -41,7 +41,7 @@ export interface IOAuthProviderContext {
41
41
  * `https://kc.example/realms/app`) — `undefined` pour les fournisseurs dont
42
42
  * l'émetteur est connu d'avance (Google) ou qui n'en publient pas (GitHub).
43
43
  */
44
- readonly issuer?: string;
44
+ readonly issuer?: string | undefined;
45
45
  }
46
46
  /**
47
47
  * Fabrique d'un fournisseur OAuth pour un nom donné.
@@ -83,11 +83,11 @@ export type FrameAuthorizer = (frame: unknown, token: IRealtimeToken) => boolean
83
83
  */
84
84
  export interface IChannelPolicy {
85
85
  /** Exige une connexion authentifiée (token non anonyme). */
86
- readonly authenticated?: boolean;
86
+ readonly authenticated?: boolean | undefined;
87
87
  /** Un de ces rôles suffit (évalué AVEC la hiérarchie de rôles du firewall). */
88
- readonly roles?: readonly string[];
88
+ readonly roles?: readonly string[] | undefined;
89
89
  /** Un de ces scopes suffit (axe API : JWT/clé API ; session BFF n'en porte pas). */
90
- readonly scopes?: readonly string[];
90
+ readonly scopes?: readonly string[] | undefined;
91
91
  }
92
92
  /**
93
93
  * Surface consommée du `realtimeService` (miroir partiel de `RealtimeService`) —
@@ -5,9 +5,9 @@ type LogFn = (message: string, severity: string) => void;
5
5
  /** Source de clé configurée (`config.jwt.keystore`). */
6
6
  interface KeystoreSource {
7
7
  /** JWK Set (clés privées) injecté depuis l'env — source `env` (prod). */
8
- readonly keySetJson?: string;
8
+ readonly keySetJson?: string | undefined;
9
9
  /** Dossier de persistance `keyset.json` — source `fichier` (opt-in dev/VPS). */
10
- readonly dir?: string;
10
+ readonly dir?: string | undefined;
11
11
  }
12
12
  /**
13
13
  * Le trousseau va-t-il partir dans l'image de conteneur ?
@@ -20,7 +20,7 @@ export interface ITrustedIssuer {
20
20
  * une requête au démarrage à froid. Déclaré, il fait autorité : rien n'est
21
21
  * découvert.
22
22
  */
23
- jwksUri?: string;
23
+ jwksUri?: string | undefined;
24
24
  /**
25
25
  * Jeu de clés fourni LOCALEMENT — aucune requête, aucune découverte.
26
26
  *
@@ -58,7 +58,7 @@ export interface ITrustedIssuer {
58
58
  * réglé finement. La séparation entre jetons est déjà assurée par l'audience,
59
59
  * qui, elle, n'est pas facultative.
60
60
  */
61
- typ?: string;
61
+ typ?: string | undefined;
62
62
  /** Claims dont la PRÉSENCE est exigée, en plus de `iss`/`aud`/`sub`. */
63
63
  requiredClaims?: readonly string[];
64
64
  }
@@ -26,3 +26,13 @@ export interface IJwtRuntime {
26
26
  * propre audience (`[issuer]`).
27
27
  */
28
28
  export declare function resolveJwtRuntime(jwt: ISecurityConfig["jwt"]): IJwtRuntime;
29
+ /**
30
+ * Audience inscrite dans un jeton quand aucune ressource n'est demandée : la
31
+ * première de la liste blanche. La liste n'est jamais vide après
32
+ * {@link resolveJwtRuntime} ; un runtime construit à la main qui la laisserait
33
+ * vide retombe sur la même règle — l'application est sa propre audience.
34
+ *
35
+ * @param runtime - paramètres JWT résolus
36
+ * @returns l'audience par défaut
37
+ */
38
+ export declare function defaultAudience(runtime: IJwtRuntime): string;