@nodefony/security 10.0.0-beta.3 → 10.0.0-beta.4

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 (162) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/index.js +6 -8
  3. package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +18 -11
  4. package/dist/nodefony/command/security-oauth-doctor.js +5 -3
  5. package/dist/nodefony/command/security-secrets.js +10 -4
  6. package/dist/nodefony/command/security-user-add.js +9 -3
  7. package/dist/nodefony/command/security-user-delete.js +5 -3
  8. package/dist/nodefony/command/security-user-list.js +5 -3
  9. package/dist/nodefony/command/security-user-password.js +5 -3
  10. package/dist/nodefony/config/config.js +20 -4
  11. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  12. package/dist/nodefony/contracts/IAccessVoter.js +1 -2
  13. package/dist/nodefony/errors/AccessDeniedError.js +1 -2
  14. package/dist/nodefony/errors/ApiKeyError.js +3 -2
  15. package/dist/nodefony/errors/AuthenticationError.js +1 -2
  16. package/dist/nodefony/errors/CsrfError.js +1 -2
  17. package/dist/nodefony/errors/InsecureTransportError.js +1 -2
  18. package/dist/nodefony/errors/InvalidTargetError.js +1 -2
  19. package/dist/nodefony/errors/SsrfError.js +1 -2
  20. package/dist/nodefony/errors/ThrottledError.js +1 -2
  21. package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
  22. package/dist/nodefony/errors/WebAuthnError.js +1 -2
  23. package/dist/nodefony/keycloak/keycloakAdmin.js +1 -2
  24. package/dist/nodefony/keycloak/keycloakRealm.js +1 -2
  25. package/dist/nodefony/keycloak/keycloakRealmInput.js +1 -2
  26. package/dist/nodefony/keycloak/scaffold.js +3 -3
  27. package/dist/nodefony/keycloak/security-keycloak-realm.js +5 -3
  28. package/dist/nodefony/scaffold/contribute.js +1 -2
  29. package/dist/nodefony/service/accessTokenVerifier.js +101 -34
  30. package/dist/nodefony/service/apiKeys.js +45 -5
  31. package/dist/nodefony/service/auditService.js +4 -5
  32. package/dist/nodefony/service/authFlow.js +10 -8
  33. package/dist/nodefony/service/authorization.js +1 -2
  34. package/dist/nodefony/service/cors.js +1 -2
  35. package/dist/nodefony/service/csrf.js +1 -2
  36. package/dist/nodefony/service/firewall.js +5 -4
  37. package/dist/nodefony/service/keycloak.js +1 -2
  38. package/dist/nodefony/service/oauth2.js +29 -7
  39. package/dist/nodefony/service/securityHeaders.js +1 -2
  40. package/dist/nodefony/service/tokenService.js +38 -22
  41. package/dist/nodefony/service/totp.js +4 -5
  42. package/dist/nodefony/service/webAuthn.js +4 -5
  43. package/dist/nodefony/service/webhooks.js +4 -5
  44. package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
  45. package/dist/nodefony/src/SecuredArea.js +1 -2
  46. package/dist/nodefony/src/accountBinding.js +96 -0
  47. package/dist/nodefony/src/admin/SecurityAdminApi.js +1 -2
  48. package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
  49. package/dist/nodefony/src/admin/adminAudit.js +1 -2
  50. package/dist/nodefony/src/admin/userRevocationCascade.js +79 -15
  51. package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
  52. package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
  53. package/dist/nodefony/src/audit/auditBridge.js +1 -2
  54. package/dist/nodefony/src/audit/auditFilters.js +1 -2
  55. package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
  56. package/dist/nodefony/src/audit/readAuditContext.js +1 -2
  57. package/dist/nodefony/src/audit/recordAudit.js +1 -2
  58. package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
  59. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
  60. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +8 -113
  61. package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
  62. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +42 -4
  63. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +91 -16
  64. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
  65. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +1 -2
  66. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +3 -17
  67. package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
  68. package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
  69. package/dist/nodefony/src/boot/publicRoutesNotice.js +1 -2
  70. package/dist/nodefony/src/crypto/secretCipher.js +1 -2
  71. package/dist/nodefony/src/csp.js +1 -2
  72. package/dist/nodefony/src/csrfToken.js +1 -2
  73. package/dist/nodefony/src/entryPoint.js +1 -2
  74. package/dist/nodefony/src/net/ssrfGuard.js +1 -2
  75. package/dist/nodefony/src/net/transportFailure.js +57 -0
  76. package/dist/nodefony/src/oauth/clientCredentials.js +1 -2
  77. package/dist/nodefony/src/oauth/httpJson.js +3 -3
  78. package/dist/nodefony/src/oauth/metadata.js +5 -16
  79. package/dist/nodefony/src/oauth/oauth2Client.js +1 -2
  80. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +1 -2
  81. package/dist/nodefony/src/oauth/providerDiagnosis.js +5 -6
  82. package/dist/nodefony/src/oauth/providerRoles.js +1 -2
  83. package/dist/nodefony/src/oauth/providers/github.js +16 -6
  84. package/dist/nodefony/src/oauth/providers/oidc.js +9 -6
  85. package/dist/nodefony/src/openToSignup.js +1 -2
  86. package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
  87. package/dist/nodefony/src/realtime/frameAuthorizer.js +1 -2
  88. package/dist/nodefony/src/rolesWithin.js +57 -0
  89. package/dist/nodefony/src/sessionAuthentication.js +1 -2
  90. package/dist/nodefony/src/sessionIdentity.js +29 -3
  91. package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
  92. package/dist/nodefony/src/token/AnonymousToken.js +1 -2
  93. package/dist/nodefony/src/token/JwtKeystore.js +14 -5
  94. package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
  95. package/dist/nodefony/src/token/RemoteJwtVerifier.js +6 -3
  96. package/dist/nodefony/src/token/UserToken.js +1 -2
  97. package/dist/nodefony/src/token/jwtRuntime.js +1 -2
  98. package/dist/nodefony/src/token/scopeGrants.js +109 -0
  99. package/dist/nodefony/src/token/secretFile.js +1 -2
  100. package/dist/nodefony/src/token/tokenCriteria.js +1 -2
  101. package/dist/nodefony/src/token/tokenFilters.js +1 -2
  102. package/dist/nodefony/src/token/tokenSort.js +1 -2
  103. package/dist/nodefony/src/token/tokenStatus.js +1 -2
  104. package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
  105. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
  106. package/dist/nodefony/src/totp/totpCipher.js +1 -2
  107. package/dist/nodefony/src/totp/totpCrypto.js +1 -2
  108. package/dist/nodefony/src/totp/totpOperations.js +1 -2
  109. package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
  110. package/dist/nodefony/src/transport/CredentialTransportPolicy.js +1 -2
  111. package/dist/nodefony/src/voter/RoleVoter.js +1 -2
  112. package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
  113. package/dist/nodefony/src/voter/voterRegistry.js +1 -2
  114. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
  115. package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
  116. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
  117. package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
  118. package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
  119. package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
  120. package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
  121. package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
  122. package/dist/nodefony/src/webhook/webhookSort.js +1 -2
  123. package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
  124. package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +7 -6
  125. package/dist/types/nodefony/config/config.d.ts +1 -0
  126. package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
  127. package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
  128. package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
  129. package/dist/types/nodefony/service/oauth2.d.ts +20 -0
  130. package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
  131. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +23 -5
  132. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -44
  133. package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
  134. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +4 -1
  135. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
  136. package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
  137. package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
  138. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
  139. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
  140. package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
  141. package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
  142. package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
  143. package/docs/api-keys.md +181 -174
  144. package/docs/audit.md +140 -140
  145. package/docs/authenticators.md +101 -101
  146. package/docs/authorization.md +116 -69
  147. package/docs/cors.md +64 -64
  148. package/docs/csrf.md +74 -71
  149. package/docs/external-jwt.md +22 -12
  150. package/docs/firewall.md +85 -82
  151. package/docs/headers.md +81 -81
  152. package/docs/index.md +4 -4
  153. package/docs/keycloak.md +95 -78
  154. package/docs/login-page.md +23 -23
  155. package/docs/oauth2.md +157 -146
  156. package/docs/obtenir-un-jeton.md +116 -26
  157. package/docs/tokens.md +128 -122
  158. package/docs/totp.md +136 -136
  159. package/docs/webauthn.md +153 -153
  160. package/docs/webhooks.md +127 -124
  161. package/package.json +13 -13
  162. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
@@ -1,6 +1,6 @@
1
1
  import { RoleHierarchyWalker } from "../RoleHierarchyWalker.js";
2
2
  import "../../contracts/IAccessVoter.js";
3
- //#region nodefony/src/voter/RoleVoter.ts
3
+ // nodefony/src/voter/RoleVoter.ts
4
4
  /**
5
5
  * Voter built-in **niveau A** — résout les attributs `ROLE_*` via la hiérarchie
6
6
  * de rôles ({@link RoleHierarchyWalker}).
@@ -28,5 +28,4 @@ var RoleVoter = class {
28
28
  return Promise.resolve(vote);
29
29
  }
30
30
  };
31
- //#endregion
32
31
  export { RoleVoter, RoleVoter as default };
@@ -1,5 +1,5 @@
1
1
  import "../../contracts/IAccessVoter.js";
2
- //#region nodefony/src/voter/ScopeVoter.ts
2
+ // nodefony/src/voter/ScopeVoter.ts
3
3
  /**
4
4
  * Types de jetons **non scopables** — identité humaine interactive (session BFF
5
5
  * web/Studio/WS, login mot de passe) ou visiteur anonyme. Un scope `api:action`
@@ -48,5 +48,4 @@ var ScopeVoter = class {
48
48
  return Promise.resolve(token.getScopes().includes(attribute) ? "GRANT" : "ABSTAIN");
49
49
  }
50
50
  };
51
- //#endregion
52
51
  export { ScopeVoter, ScopeVoter as default };
@@ -1,6 +1,6 @@
1
1
  import { RoleVoter } from "./RoleVoter.js";
2
2
  import { ScopeVoter } from "./ScopeVoter.js";
3
- //#region nodefony/src/voter/voterRegistry.ts
3
+ // nodefony/src/voter/voterRegistry.ts
4
4
  const factories = /* @__PURE__ */ new Map();
5
5
  /**
6
6
  * Enregistre (ou remplace) la fabrique d'un voter. Appelé par les builtins au
@@ -16,5 +16,4 @@ function listVoterFactories() {
16
16
  }
17
17
  registerVoterFactory("role", ({ container }) => new RoleVoter(container));
18
18
  registerVoterFactory("scope", () => new ScopeVoter());
19
- //#endregion
20
19
  export { listVoterFactories, registerVoterFactory };
@@ -1,5 +1,5 @@
1
1
  import { assertPageQuery } from "nodefony";
2
- //#region nodefony/src/webauthn/MemoryWebAuthnCredentialStore.ts
2
+ // nodefony/src/webauthn/MemoryWebAuthnCredentialStore.ts
3
3
  /**
4
4
  * Projection contractuelle : credential complet → vue admin **sans `publicKey`**.
5
5
  * Partagée par les backends qui matérialisent des credentials en mémoire.
@@ -135,5 +135,4 @@ var MemoryWebAuthnCredentialStore = class {
135
135
  }
136
136
  }
137
137
  };
138
- //#endregion
139
138
  export { MemoryWebAuthnCredentialStore, MemoryWebAuthnCredentialStore as default, toWebAuthnSummary };
@@ -1,5 +1,5 @@
1
1
  import { MemoryWebAuthnCredentialStore } from "./MemoryWebAuthnCredentialStore.js";
2
- //#region nodefony/src/webauthn/webAuthnCredentialStoreRegistry.ts
2
+ // nodefony/src/webauthn/webAuthnCredentialStoreRegistry.ts
3
3
  const factories = /* @__PURE__ */ new Map();
4
4
  /** Enregistre (ou remplace) la fabrique d'un store de credentials WebAuthn. */
5
5
  function registerWebAuthnStore(name, factory) {
@@ -14,5 +14,4 @@ function listWebAuthnStores() {
14
14
  return [...factories.keys()];
15
15
  }
16
16
  registerWebAuthnStore("memory", () => new MemoryWebAuthnCredentialStore());
17
- //#endregion
18
17
  export { getWebAuthnStoreFactory, listWebAuthnStores, registerWebAuthnStore };
@@ -1,6 +1,6 @@
1
1
  import { WEBHOOK_DEFAULT_ORDER, WEBHOOK_SORTABLE_FIELDS } from "./webhookSort.js";
2
2
  import { assertPageQuery, compareByOrder, pickOrder } from "nodefony";
3
- //#region nodefony/src/webhook/MemoryWebhookStore.ts
3
+ // nodefony/src/webhook/MemoryWebhookStore.ts
4
4
  /**
5
5
  * Applique les filtres d'{@link IWebhookListQuery} à un endpoint — sémantique de
6
6
  * RÉFÉRENCE du contrat, partagée par les backends qui filtrent en mémoire.
@@ -91,5 +91,4 @@ var MemoryWebhookStore = class {
91
91
  return n;
92
92
  }
93
93
  };
94
- //#endregion
95
94
  export { MemoryWebhookStore, matchesWebhookQuery };
@@ -1,5 +1,5 @@
1
1
  import { webhookSignatureHeaders } from "./webhookSignature.js";
2
- //#region nodefony/src/webhook/WebhookDispatcher.ts
2
+ // nodefony/src/webhook/WebhookDispatcher.ts
3
3
  /**
4
4
  * Dispatcher de webhooks sortants — abonné au journal d'audit, il filtre les
5
5
  * événements par souscription, signe (Standard Webhooks v1) et livre, **sans
@@ -205,5 +205,4 @@ var WebhookDispatcher = class {
205
205
  this.#queue = null;
206
206
  }
207
207
  };
208
- //#endregion
209
208
  export { WebhookDispatcher, backoffMs, classifyDelivery, matchesSubscription };
@@ -1,6 +1,6 @@
1
1
  import { decryptSecret, deriveKey, encryptSecret, generateEphemeralKey } from "../crypto/secretCipher.js";
2
2
  import { Buffer } from "node:buffer";
3
- //#region nodefony/src/webhook/webhookCipher.ts
3
+ // nodefony/src/webhook/webhookCipher.ts
4
4
  /**
5
5
  * Chiffrement réversible du secret de signature webhook au repos — façade de
6
6
  * domaine au-dessus de {@link ../crypto/secretCipher} (calque {@link ../totp/totpCipher}).
@@ -23,5 +23,4 @@ const WEBHOOK_DERIVATION = {
23
23
  function deriveWebhookKey(material) {
24
24
  return deriveKey(material, WEBHOOK_DERIVATION);
25
25
  }
26
- //#endregion
27
26
  export { decryptSecret, deriveWebhookKey, encryptSecret, generateEphemeralKey };
@@ -2,7 +2,7 @@ import { Buffer } from "node:buffer";
2
2
  import { isIP } from "node:net";
3
3
  import { request } from "node:https";
4
4
  import { request as request$1 } from "node:http";
5
- //#region nodefony/src/webhook/webhookDelivery.ts
5
+ // nodefony/src/webhook/webhookDelivery.ts
6
6
  /**
7
7
  * Émetteur HTTP d'une livraison webhook — `node:http(s)` natif (0 dép). Trois
8
8
  * propriétés de sécurité par rapport à `fetch` :
@@ -98,5 +98,4 @@ function deliverWebhook(url, body, headers, opts) {
98
98
  req.end();
99
99
  });
100
100
  }
101
- //#endregion
102
101
  export { deliverWebhook };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/webhook/webhookFilters.ts
1
+ // nodefony/src/webhook/webhookFilters.ts
2
2
  /**
3
3
  * **Le vocabulaire de filtre des endpoints webhook**, en noms PUBLICS — ceux
4
4
  * qu'un client écrit dans l'URL (`?enabled=false&event=user.created`).
@@ -52,5 +52,4 @@ const WEBHOOK_FACETS = {
52
52
  * Un test verrouille l'accord entre cette liste et {@link WEBHOOK_FACETS}.
53
53
  */
54
54
  const WEBHOOK_STATS_FILTERS = { event: "string" };
55
- //#endregion
56
55
  export { WEBHOOK_FACETS, WEBHOOK_FILTERS, WEBHOOK_STATS_FILTERS };
@@ -1,6 +1,6 @@
1
1
  import { createHmac } from "node:crypto";
2
2
  import { Buffer } from "node:buffer";
3
- //#region nodefony/src/webhook/webhookSignature.ts
3
+ // nodefony/src/webhook/webhookSignature.ts
4
4
  /**
5
5
  * Signature des webhooks sortants — schéma **Standard Webhooks v1**
6
6
  * (standardwebhooks.com), choisi plutôt que RFC 9421 (trop lourd pour un webhook,
@@ -47,5 +47,4 @@ function webhookSignatureHeaders(secret, id, timestampS, body) {
47
47
  "webhook-signature": signStandardWebhook(secret, id, timestampS, body)
48
48
  };
49
49
  }
50
- //#endregion
51
50
  export { buildSignatureBase, parseWebhookSecret, signStandardWebhook, webhookSignatureHeaders };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/webhook/webhookSort.ts
1
+ // nodefony/src/webhook/webhookSort.ts
2
2
  /**
3
3
  * **Le vocabulaire de tri des endpoints webhook**, en noms PUBLICS — ceux qu'un
4
4
  * client écrit dans l'URL (`?order=failureCount:DESC`), jamais des noms de
@@ -44,5 +44,4 @@ const WEBHOOK_SORTABLE_FIELDS = [
44
44
  * des deux ne jamais apparaître.
45
45
  */
46
46
  const WEBHOOK_DEFAULT_ORDER = [["createdAt", "DESC"], ["id", "ASC"]];
47
- //#endregion
48
47
  export { WEBHOOK_DEFAULT_ORDER, WEBHOOK_SORTABLE_FIELDS };
@@ -1,5 +1,5 @@
1
1
  import { MemoryWebhookStore } from "./MemoryWebhookStore.js";
2
- //#region nodefony/src/webhook/webhookStoreRegistry.ts
2
+ // nodefony/src/webhook/webhookStoreRegistry.ts
3
3
  const factories = /* @__PURE__ */ new Map();
4
4
  /** Enregistre (ou remplace) la fabrique d'un store d'endpoints webhook. */
5
5
  function registerWebhookStore(name, factory) {
@@ -14,5 +14,4 @@ function listWebhookStores() {
14
14
  return [...factories.keys()];
15
15
  }
16
16
  registerWebhookStore("memory", () => new MemoryWebhookStore());
17
- //#endregion
18
17
  export { getWebhookStoreFactory, listWebhookStores, registerWebhookStore };
@@ -20,7 +20,7 @@ export declare function ttlSeconds(raw: string | undefined): number | undefined
20
20
  */
21
21
  export declare function tokenTtlSeconds(raw: string | undefined, write: boolean): number | undefined | Error;
22
22
  /**
23
- * `nodefony security:token` — émet un jeton d'accès pour une porte de cette
23
+ * `nodefony security:mcp:token` — émet un jeton d'accès pour une porte de cette
24
24
  * application (la porte MCP par défaut).
25
25
  *
26
26
  * **Pourquoi une commande, et pas un `curl`.** Le jeton s'obtenait par un appel
@@ -34,11 +34,12 @@ export declare function tokenTtlSeconds(raw: string | undefined, write: boolean)
34
34
  * raison d'être de la liaison d'audience. La commande la vise d'elle-même,
35
35
  * `--resource` ne sert qu'à en viser une autre.
36
36
  *
37
- * Suit `security:secrets` : `--write` pose la valeur là où l'AGENT la lit
38
- * (gitignoré), jamais dans le `.env` commité, et ne remplace jamais une valeur
39
- * existante — une rotation est un geste explicite.
37
+ * `--write` pose la valeur là où l'AGENT la lit (gitignoré), jamais dans le
38
+ * `.env` de l'application. Relancée, elle REMPLACE le jeton chez les agents qui
39
+ * le portent déjà — la rotation est le geste le plus fréquent — et n'en câble
40
+ * aucun nouveau sans le proposer.
40
41
  */
41
- declare class SecurityToken extends Command {
42
+ declare class SecurityMcpToken extends Command {
42
43
  #private;
43
44
  constructor(cli: CliKernel);
44
45
  generate(identifierArg: string | undefined, opts: {
@@ -50,4 +51,4 @@ declare class SecurityToken extends Command {
50
51
  json?: boolean;
51
52
  }): Promise<this>;
52
53
  }
53
- export default SecurityToken;
54
+ export default SecurityMcpToken;
@@ -31,6 +31,7 @@ export declare const securityConfigSchema: z.ZodObject<{
31
31
  rounds: z.ZodDefault<z.ZodNumber>;
32
32
  }, z.core.$strict>>>;
33
33
  roleHierarchy: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
34
+ grantableScopes: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
34
35
  areas: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
35
36
  pattern: z.ZodString;
36
37
  security: z.ZodDefault<z.ZodBoolean>;
@@ -5,6 +5,8 @@ import { nodefonyError } from "nodefony";
5
5
  * classes de `@nodefony/security`).
6
6
  *
7
7
  * - `400` — entrée invalide (nom vide, scope hors catalogue) ;
8
+ * - `403` — scope demandé que le porteur ne peut pas obtenir (`grantableScopes`,
9
+ * ou `admin:*` sans le rôle d'administration) ;
8
10
  * - `409` — plafond `apiKeys.maxPerSubject` atteint ;
9
11
  * - `503` — store de jetons indisponible (clés activées mais non provisionnées).
10
12
  *
@@ -12,6 +14,6 @@ import { nodefonyError } from "nodefony";
12
14
  * 401, message uniforme anti-énumération).
13
15
  */
14
16
  export declare class ApiKeyError extends nodefonyError {
15
- constructor(message: string, code: 400 | 409 | 503);
17
+ constructor(message: string, code: 400 | 403 | 409 | 503);
16
18
  }
17
19
  export default ApiKeyError;
@@ -1,29 +1,50 @@
1
1
  import { Service, Module } from "nodefony";
2
- import { RemoteJwtVerifier } from "../src/token/RemoteJwtVerifier.js";
2
+ import type { IAccessTokenVerifier } from "nodefony";
3
3
  /**
4
- * Pose (ou non) le vérificateur de jetons d'accès TIERS dans le conteneur.
4
+ * Pose (ou non) le vérificateur de jetons d'accès dans le conteneur — celui
5
+ * qu'une ressource protégée (porte MCP, zone `external-jwt`) interroge.
5
6
  *
6
- * Ce service est une **décision de câblage**, pas de la cryptographie : toute la
7
- * mécanique vit dans {@link RemoteJwtVerifier}, et la doctrine de refus dans le
8
- * cœur (`nodefony/src/oauth/`). Ici, on lit la configuration et on tranche une
9
- * seule question — cette application accepte-t-elle des jetons émis ailleurs ?
7
+ * Ce service est une **décision de câblage**, pas de la cryptographie : la
8
+ * vérification d'un jeton TIERS vit dans {@link RemoteJwtVerifier}, celle d'un
9
+ * jeton de CETTE application dans {@link JwtAuthenticator}, et la doctrine de
10
+ * refus dans le cœur (`nodefony/src/oauth/`). Ici, on tranche deux questions :
11
+ * l'application signe-t-elle ses jetons, et en accepte-t-elle d'ailleurs ?
10
12
  *
11
- * **Aucun émetteur déclaré = rien n'est posé**, et c'est le comportement voulu :
12
- * une porte protégée qui ne trouve pas de vérificateur refuse de servir en le
13
- * disant (503 + CRITIC), là où un vérificateur présent mais vide refuserait
13
+ * ⭐ **Un jeton signé par cette application est TOUJOURS vérifiable**, dès que
14
+ * sa capacité JWT est active (`security.jwt.enabled`, le défaut) — sans rien
15
+ * déclarer dans `resourceServer.issuers`. Une application qui émet un jeton
16
+ * pour sa propre porte (`nodefony security:mcp:token --resource …`) doit pouvoir
17
+ * le lire : sans cette règle, la porte refusait un jeton parfaitement valide,
18
+ * et l'application était un serveur d'autorisation dont aucune ressource ne
19
+ * reconnaissait la signature. Rien de nouveau ne devient valide : l'audience
20
+ * reste exigée, et seules celles de `jwt.audiences` sont émises.
21
+ *
22
+ * 🔴 **Il est vérifié par les règles de l'authentificateur `jwt`, pas par son
23
+ * jeu de clés seul** : révocation ciblée et en masse, compte encore actif. Lu
24
+ * comme un jeton tiers, un jeton RÉVOQUÉ — ou celui d'un compte désactivé —
25
+ * ouvrait encore la porte jusqu'à son expiration.
26
+ *
27
+ * L'aiguillage se fait sur l'émetteur revendiqué (`peekIssuer`), comme dans le
28
+ * pare-feu : l'émetteur de l'application va à la vérification locale, tout
29
+ * autre à la liste fermée des émetteurs tiers. Un émetteur inconnu est refusé
30
+ * sans la moindre requête sortante.
31
+ *
32
+ * **Rien à vérifier = rien n'est posé** (capacité JWT coupée, aucun émetteur
33
+ * tiers) : une porte protégée qui ne trouve pas de vérificateur refuse alors de
34
+ * servir en le disant (503 + CRITIC), là où un vérificateur vide refuserait
14
35
  * chaque jeton un par un — même résultat pour l'appelant, diagnostic beaucoup
15
36
  * plus difficile pour l'exploitant.
16
37
  *
17
- * Le coût est nul quand la capacité n'est pas utilisée : rien n'est instancié,
18
- * aucune requête n'est faite au démarrage. La découverte des clés n'a lieu qu'au
19
- * PREMIER jeton réellement présenté, et une seule fois par émetteur.
38
+ * Le coût est nul tant qu'aucun jeton n'est présenté : aucune requête au
39
+ * démarrage, clés locales lues au premier jeton, découverte d'un émetteur tiers
40
+ * au premier jeton qui s'en réclame.
20
41
  */
21
42
  declare class AccessTokenVerifierService extends Service {
22
43
  #private;
23
44
  module: Module;
24
45
  constructor(module: Module);
25
- /** Le vérificateur, ou `null` si aucun émetteur n'est déclaré. */
26
- get verifier(): RemoteJwtVerifier | null;
46
+ /** Le vérificateur posé, ou `null` si rien n'est vérifiable. */
47
+ get verifier(): IAccessTokenVerifier | null;
27
48
  }
28
49
  export default AccessTokenVerifierService;
29
50
  export { AccessTokenVerifierService };
@@ -40,11 +40,22 @@ declare class ApiKeyService extends Service {
40
40
  * Émet une nouvelle clé API pour un porteur — renvoie sa vue publique **+ le
41
41
  * token en clair** (affiché une seule fois).
42
42
  *
43
+ * Un scope que le porteur ne peut pas OBTENIR — réservé par
44
+ * `grantableScopes` à des rôles qu'il n'a pas, ou `admin:*` sans le rôle
45
+ * d'administration — refuse la création entière : une clé émise avec moins
46
+ * que demandé surprendrait sans qu'aucune réponse ne le dise. Même juge que
47
+ * l'émission des jetons ({@link refusedScopes}).
48
+ *
49
+ * @param roles - rôles du porteur tels que son compte les porte ; omis =
50
+ * aucun rôle, donc tout scope réservé est refusé (fail-closed).
43
51
  * @throws ApiKeyError 400 — nom vide/trop long, scope hors catalogue, expiry invalide.
52
+ * @throws ApiKeyError 403 — scope réservé à des rôles que le porteur n'a pas.
44
53
  * @throws ApiKeyError 409 — plafond `maxPerSubject` atteint.
45
- * @throws ApiKeyError 503 — store indisponible.
54
+ * @throws ApiKeyError 403 — aucun compte sous cet identifiant (la clé est liée
55
+ * à l'`id` immuable du compte, lu dans l'annuaire).
56
+ * @throws ApiKeyError 503 — store ou annuaire `users` indisponible.
46
57
  */
47
- createForSubject(subjectId: string, subjectType: "user" | "service", opts: ICreateApiKeyOptions): Promise<IApiKeyCreated>;
58
+ createForSubject(subjectId: string, subjectType: "user" | "service", opts: ICreateApiKeyOptions, roles?: readonly string[]): Promise<IApiKeyCreated>;
48
59
  /** Liste les clés (PAT) d'un porteur — vue publique, sans secret, récentes d'abord. */
49
60
  listForSubject(subjectId: string): Promise<IApiKeyView[]>;
50
61
  /**
@@ -71,6 +71,26 @@ export interface IOAuthAuthorization {
71
71
  * @throws BootConfigurationError - émetteur requis absent, ou mal formé.
72
72
  */
73
73
  export declare function checkProviderIssuer(name: string, issuer: string | undefined): void;
74
+ /**
75
+ * Annonce qu'un annuaire accorde des rôles de PLATEFORME, à la sévérité de
76
+ * l'environnement — ou `null` quand il n'y a rien à dire.
77
+ *
78
+ * L'ouverture est ÉCRITE (`allowPlatformRoles`), donc voulue : l'annonce ne
79
+ * la conteste pas, elle la rappelle. Le risque — un annuaire compromis
80
+ * administre l'instance — n'est réel qu'en production : là, `WARNING` ;
81
+ * ailleurs le même fait est dit en `INFO`, sans sonner une alarme à chaque
82
+ * démarrage d'un décor de développement.
83
+ *
84
+ * @param name - nom du fournisseur (`oauth2.providers.<name>`).
85
+ * @param allowed - `allowPlatformRoles` tel que déclaré.
86
+ * @param granted - rôles de l'application que la table accorde.
87
+ * @param production - l'application tourne en production.
88
+ * @returns le message et sa sévérité, ou `null`.
89
+ */
90
+ export declare function platformRolesNotice(name: string, allowed: boolean | undefined, granted: readonly string[], production: boolean): {
91
+ message: string;
92
+ severity: "WARNING" | "INFO";
93
+ } | null;
74
94
  /**
75
95
  * **Social login OAuth 2.0** (P6 J9) — orchestrateur du flux *Authorization Code*.
76
96
  *
@@ -0,0 +1,82 @@
1
+ import type { IUser, IUserProvider } from "@nodefony/user";
2
+ /**
3
+ * Claim d'un jeton d'accès émis par l'application qui porte l'identifiant
4
+ * IMMUABLE du compte (`IUser.id`) — `sub` porte l'identifiant fonctionnel.
5
+ */
6
+ export declare const ACCOUNT_ID_CLAIM = "uid";
7
+ /**
8
+ * Clé, dans `metadata` d'un enregistrement de jeton (refresh, clé d'API) comme
9
+ * dans le `metaBag` d'une session, qui porte l'identifiant immuable du compte.
10
+ */
11
+ export declare const ACCOUNT_ID_KEY = "accountId";
12
+ /**
13
+ * Claim d'un jeton d'accès émis par l'application qui porte la version des
14
+ * identifiants du compte à l'émission (`IUser.credentialsVersion`).
15
+ */
16
+ export declare const CREDENTIALS_VERSION_CLAIM = "cv";
17
+ /**
18
+ * Clé, dans `metadata` d'un enregistrement de jeton comme dans le `metaBag`
19
+ * d'une session, qui porte la version des identifiants lue à l'émission.
20
+ */
21
+ export declare const CREDENTIALS_VERSION_KEY = "credentialsVersion";
22
+ /**
23
+ * La version des identifiants à inscrire dans un artefact qu'on émet pour ce
24
+ * compte — 0 quand le compte n'en porte pas encore.
25
+ *
26
+ * @param user - compte pour lequel l'artefact est émis.
27
+ * @returns la version courante, jamais `null`.
28
+ */
29
+ export declare function credentialsVersionOf(user: IUser): number;
30
+ /**
31
+ * Dit si un artefact d'accès (jeton, session, clé) appartient bien au compte
32
+ * qu'on vient de recharger par son identifiant fonctionnel, ET s'il a été émis
33
+ * depuis le dernier changement de mot de passe.
34
+ *
35
+ * Les artefacts sont rangés sous l'`identifier`, qu'un compte RECRÉÉ peut
36
+ * reprendre : supprimé hors du framework (`DELETE` SQL, restauration, import)
37
+ * puis revenu sous le même nom, l'identité se rechargerait et l'artefact de
38
+ * l'ancien titulaire ouvrirait le compte du nouveau. L'identifiant immuable
39
+ * lève l'ambiguïté sans dépendre d'aucun événement ni d'aucun stockage : deux
40
+ * comptes successifs portent deux `id` différents. SEULE règle de
41
+ * comparaison — chaque authentificateur l'appelle, aucun ne la recopie.
42
+ *
43
+ * Un artefact SANS liaison est refusé : il a été émis avant que la règle
44
+ * existe, et rien ne dit à qui il appartient.
45
+ *
46
+ * La version des identifiants fait de la coupure au changement de mot de passe
47
+ * un ÉTAT du compte, et non une suppression : si la cascade de révocation
48
+ * échoue, ou si une requête en vol sur un autre processus réécrit la session
49
+ * qu'on vient d'effacer, l'artefact porte toujours l'ancienne version, et il
50
+ * tombe ici. Un artefact émis avant cette règle n'en porte pas : il vaut 0,
51
+ * comme un compte qui n'a jamais changé de mot de passe — le déploiement ne
52
+ * déconnecte personne. Une valeur d'un autre type est refusée.
53
+ *
54
+ * ⚠️ Chemin chaud — appelée sur CHAQUE requête authentifiée : deux comparaisons
55
+ * de chaîne et d'entier, aucune allocation.
56
+ *
57
+ * @param user - compte rechargé par l'identifiant que porte l'artefact.
58
+ * @param boundAccountId - identifiant immuable inscrit dans l'artefact.
59
+ * @param boundCredentialsVersion - version des identifiants inscrite dans
60
+ * l'artefact ({@link CREDENTIALS_VERSION_KEY}), `undefined` si absente.
61
+ * Obligatoire dans la signature pour qu'aucune porte ne l'oublie.
62
+ * @returns `true` si l'artefact a été émis pour CE compte, à sa version courante.
63
+ */
64
+ export declare function isBoundToAccount(user: IUser, boundAccountId: unknown, boundCredentialsVersion: unknown): boolean;
65
+ /**
66
+ * Recharge le compte qu'un jeton d'accès de CETTE application désigne, ou
67
+ * `null` : sujet inconnu, compte inactif ou verrouillé, jeton émis pour un
68
+ * autre compte du même nom ou avant un changement de mot de passe.
69
+ *
70
+ * SEULE implémentation de la règle « ce jeton ouvre-t-il encore ce compte ? »
71
+ * pour un jeton maison : la zone `jwt` (`JwtAuthenticator`) et le résolveur de
72
+ * compte d'une ressource protégée l'appellent, aucun ne la recopie. Un refus
73
+ * est `null`, jamais une exception : chaque appelant le traduit dans son
74
+ * propre contrat (401 uniforme, outil en `isError`).
75
+ *
76
+ * @param provider - annuaire des comptes de l'application.
77
+ * @param identifier - identifiant fonctionnel porté par le jeton (`sub`).
78
+ * @param boundAccountId - identifiant immuable inscrit dans le jeton ({@link ACCOUNT_ID_CLAIM}).
79
+ * @param boundCredentialsVersion - version des identifiants inscrite ({@link CREDENTIALS_VERSION_CLAIM}).
80
+ * @returns le compte, ou `null` s'il n'est pas (ou plus) celui du jeton.
81
+ */
82
+ export declare function loadBoundAccount(provider: IUserProvider, identifier: string, boundAccountId: unknown, boundCredentialsVersion: unknown): Promise<IUser | null>;
@@ -29,6 +29,23 @@ interface KernelListenerLike {
29
29
  * @param now - horloge (epoch ms) injectable pour les tests.
30
30
  */
31
31
  export declare function cascadeUserRevocation(container: Container, event: IUserRevokedEvent, now?: number): Promise<void>;
32
+ /**
33
+ * Purge, à la NAISSANCE d'un compte, tout ce qu'un ancien titulaire du même
34
+ * identifiant a laissé derrière lui : sessions, jetons et facteurs. Un compte
35
+ * qui vient de naître n'a encore rien émis — tout artefact déjà rangé sous son
36
+ * identifiant appartient donc à quelqu'un d'autre. C'est ce qui ferme le cas
37
+ * d'un compte supprimé HORS du framework (`DELETE` SQL, aucun événement), dont
38
+ * l'accès redeviendrait valide dès qu'un compte renaît sous son nom.
39
+ *
40
+ * Le seuil est tronqué à la SECONDE : l'`iat` d'un JWT l'est aussi (RFC 7519
41
+ * §2, NumericDate), et un seuil à la milliseconde rejetterait le jeton du
42
+ * nouveau titulaire émis dans la même seconde que sa création.
43
+ *
44
+ * @param container - container du kernel.
45
+ * @param identifier - `IUser.identifier` du compte créé.
46
+ * @param now - horloge (epoch ms) injectable pour les tests.
47
+ */
48
+ export declare function purgeInheritedAccess(container: ServiceLookupLike, identifier: string, now?: number): Promise<void>;
32
49
  /**
33
50
  * Retire les facteurs rangés sous un identifiant — secret TOTP (codes de
34
51
  * récupération compris) et passkeys. SEULE implémentation du geste, appelée à
@@ -45,11 +62,12 @@ export declare function removeAccountFactors(container: ServiceLookupLike, ident
45
62
  * `@nodefony/security` — avant qu'aucun compte ne puisse être créé ou révoqué
46
63
  * (les comptes semés au démarrage compris). Deux abonnements :
47
64
  * - {@link USER_REVOKED_EVENT} → {@link cascadeUserRevocation} ;
48
- * - {@link USER_CREATED_EVENT} → purge des facteurs HÉRITÉS : un compte qui
49
- * vient de naître n'a encore rien enrôlé, donc tout facteur déjà rangé sous
50
- * son identifiant appartient à un ancien titulaire. Règle exacte, aucune
51
- * date à comparer. ⚠️ Un import qui chargerait les facteurs AVANT les
52
- * comptes les verrait retirés : importer les comptes d'abord.
65
+ * - {@link USER_CREATED_EVENT} → {@link purgeInheritedAccess} : sessions,
66
+ * jetons et facteurs HÉRITÉS. Un compte qui vient de naître n'a encore rien
67
+ * émis ni enrôlé, donc tout artefact déjà rangé sous son identifiant
68
+ * appartient à un ancien titulaire. Règle exacte, aucune date à comparer.
69
+ * ⚠️ Un import qui chargerait les facteurs AVANT les comptes les verrait
70
+ * retirés : importer les comptes d'abord.
53
71
  *
54
72
  * Les deux sont émis par `UserService` seul, et attendus : chaque handler rend
55
73
  * sa promesse. **Extensible** : tout autre module (webhooks…) s'abonne aux
@@ -3,50 +3,8 @@ import type { ContextType } from "@nodefony/http";
3
3
  import type { IAuthenticator } from "../../contracts/IAuthenticator.js";
4
4
  import type { ISecuredArea } from "../../contracts/ISecuredArea.js";
5
5
  import type { IToken } from "../../contracts/IToken.js";
6
- import { type ExternalSubjectMapping } from "./externalSubject.js";
7
- import { type IProviderRoleMapping } from "../oauth/providerRoles.js";
8
- /** Comment le sujet d'un jeton devient un utilisateur de cette application. */
9
- export type ExternalSubjectPolicy = "require" | "ephemeral";
10
- /** Un émetteur reconnu, et la façon dont ses sujets entrent chez nous. */
11
- export interface IExternalIssuerBinding {
12
- /** Émetteur de confiance, sous sa forme canonique. */
13
- issuer: string;
14
- /** Comment le `sub` de CET émetteur devient un identifiant local. */
15
- subjectMapping: ExternalSubjectMapping;
16
- }
17
- /** Ce que la fabrique doit fournir à l'authenticator. */
18
- export interface IExternalJwtAuthenticatorOptions {
19
- /**
20
- * Émetteurs de confiance, et leur politique de sujet.
21
- *
22
- * La liste sert à deux choses, qu'il ne faut pas confondre : reconnaître les
23
- * jetons qui relèvent de cet authenticator (aiguillage — elle n'accorde
24
- * rien, le vérificateur refait le contrôle sur sa propre liste), et savoir
25
- * dans quel espace de noms lire le sujet de chacun.
26
- */
27
- issuers: readonly IExternalIssuerBinding[];
28
- /** Politique de rattachement du sujet à un utilisateur local. */
29
- subjectPolicy: ExternalSubjectPolicy;
30
- /** Rôles accordés en mode `ephemeral`. */
31
- ephemeralRoles: readonly string[];
32
- /**
33
- * Fournisseurs de CONNEXION (`oauth2.providers`) qui déclarent leur émetteur.
34
- *
35
- * Quand l'émetteur d'un jeton est celui d'un fournisseur de connexion, la
36
- * même personne a pu se connecter par le navigateur : son compte local porte
37
- * alors le lien `(fournisseur, sub)`. Le chercher d'abord évite qu'un jeton
38
- * d'API désigne un AUTRE compte que la session de son porteur.
39
- */
40
- oauthProviders?: readonly {
41
- name: string;
42
- issuer: string;
43
- /**
44
- * Table de correspondance des rôles du fournisseur, compilée — absente ou
45
- * `null` : les rôles du compte lié restent locaux.
46
- */
47
- roleMapping?: IProviderRoleMapping | null;
48
- }[];
49
- }
6
+ import { type IExternalJwtAuthenticatorOptions } from "./ExternalSubjectResolver.js";
7
+ export type { ExternalSubjectPolicy, IExternalIssuerBinding, IExternalJwtAuthenticatorOptions, } from "./ExternalSubjectResolver.js";
50
8
  /**
51
9
  * Authentification par **jeton d'accès émis par un serveur d'autorisation
52
10
  * TIERS** (Keycloak, Auth0, Entra, ou l'émetteur d'une flotte d'agents).