@nodefony/mongoose 10.0.0-alpha.8 → 10.0.0-alpha.9
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.
- package/README.md +32 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +10 -4
- package/dist/nodefony/entity/auditEventEntity.js +124 -0
- package/dist/nodefony/entity/idempotencyEntity.js +96 -0
- package/dist/nodefony/entity/sessionEntity.js +1 -1
- package/dist/nodefony/entity/totpSecretEntity.js +103 -0
- package/dist/nodefony/registerStores.js +41 -4
- package/dist/nodefony/service/MongooseService.js +6 -3
- package/dist/nodefony/src/MongooseAuditStore.js +213 -0
- package/dist/nodefony/src/MongooseIdempotencyStore.js +260 -0
- package/dist/nodefony/src/MongooseTokenStore.js +60 -17
- package/dist/nodefony/src/MongooseTotpSecretStore.js +151 -0
- package/dist/nodefony/src/MongooseWebAuthnCredentialStore.js +13 -1
- package/dist/nodefony/src/MongooseWebhookStore.js +14 -2
- package/dist/nodefony/src/SessionStorage.js +8 -0
- package/dist/nodefony/src/orm-core/MongooseOrm.js +62 -12
- package/dist/nodefony/src/orm-core/MongooseRepository.js +124 -14
- package/dist/nodefony/src/orm-core/MongooseTransaction.js +19 -6
- package/dist/types/index.d.ts +9 -0
- package/dist/types/nodefony/entity/auditEventEntity.d.ts +76 -0
- package/dist/types/nodefony/entity/idempotencyEntity.d.ts +75 -0
- package/dist/types/nodefony/entity/totpSecretEntity.d.ts +75 -0
- package/dist/types/nodefony/registerStores.d.ts +6 -3
- package/dist/types/nodefony/src/MongooseAuditStore.d.ts +70 -0
- package/dist/types/nodefony/src/MongooseIdempotencyStore.d.ts +121 -0
- package/dist/types/nodefony/src/MongooseTokenStore.d.ts +8 -1
- package/dist/types/nodefony/src/MongooseTotpSecretStore.d.ts +63 -0
- package/dist/types/nodefony/src/MongooseWebAuthnCredentialStore.d.ts +6 -0
- package/dist/types/nodefony/src/MongooseWebhookStore.d.ts +6 -0
- package/dist/types/nodefony/src/SessionStorage.d.ts +6 -0
- package/dist/types/nodefony/src/orm-core/MongooseOrm.d.ts +10 -3
- package/dist/types/nodefony/src/orm-core/MongooseRepository.d.ts +22 -1
- package/dist/types/nodefony/src/orm-core/MongooseTransaction.d.ts +12 -4
- package/docs/configuration.md +22 -22
- package/docs/index.md +107 -63
- package/package.json +21 -17
package/README.md
CHANGED
|
@@ -48,6 +48,38 @@ Mongoose : `user`/`pass`/`maxPoolSize`/timeouts).
|
|
|
48
48
|
La config est validée par **Zod** au boot (`config.ts` = source de vérité). Une config invalide
|
|
49
49
|
plante proprement avec un message clair.
|
|
50
50
|
|
|
51
|
+
## Les briques que ce module fournit au framework
|
|
52
|
+
|
|
53
|
+
**Une application peut tourner entièrement sur MongoDB** : les huit briques de persistance
|
|
54
|
+
durables sont portées, aucune ne retombe sur un autre backend. C'est la propriété à retenir avant
|
|
55
|
+
de choisir sa base — un backend durable est un chemin complet, ou il oblige à en rapatrier un
|
|
56
|
+
second pour une seule brique.
|
|
57
|
+
|
|
58
|
+
| Brique | Ce qu'elle range | Comment l'activer |
|
|
59
|
+
| ------------ | ----------------------------------------- | ----------------------------------------------------------- |
|
|
60
|
+
| Session | sessions HTTP | `use("@nodefony/http", { session: { store: "mongoose" } })` |
|
|
61
|
+
| Utilisateurs | l'annuaire | `NF_USER_STORE=mongoose` |
|
|
62
|
+
| Jetons | rafraîchissement, clés d'API, révocations | `tokenStore: { store: "mongoose" }` |
|
|
63
|
+
| Passkeys | credentials WebAuthn | `passkeys: { store: "mongoose" }` |
|
|
64
|
+
| TOTP | secrets du second facteur | `totp: { store: "mongoose" }` |
|
|
65
|
+
| Audit | journal de sécurité append-only | `audit: { store: "mongoose" }` |
|
|
66
|
+
| Webhooks | endpoints notifiés | `webhooks: { store: "mongoose" }` |
|
|
67
|
+
| Idempotence | dédoublonnage des mutations rejouées | `NF_IDEMPOTENCY_STORE=mongoose` |
|
|
68
|
+
|
|
69
|
+
Les quatre dernières colonnes de configuration vivent sous `use("@nodefony/security", …)`.
|
|
70
|
+
**Rien d'autre à écrire** : charger le module suffit à rendre ces backends sélectionnables, et
|
|
71
|
+
`store: "auto"` (le défaut) les choisit tout seul dès qu'une infra Mongo est déclarée
|
|
72
|
+
(`NF_DATABASE_URL=mongodb://…`).
|
|
73
|
+
|
|
74
|
+
Deux briques méritent un mot :
|
|
75
|
+
|
|
76
|
+
- **Idempotence** — la dédup fonctionne **entre pods**, sans exiger Redis. La réservation est
|
|
77
|
+
atomique côté serveur : un `findOneAndUpdate` en `upsert`, filtré sur l'entrée expirée, dont le
|
|
78
|
+
perdant reçoit une violation d'unicité. C'est l'équivalent Mongo du `SET … NX PX` de Redis.
|
|
79
|
+
Comme la variante SQL, il n'y a pas de TTL natif : le framework arme une purge périodique.
|
|
80
|
+
- **Audit** — le journal se lit par **curseur**, jamais par décalage : il reçoit des écritures
|
|
81
|
+
pendant qu'on le parcourt. Rétention purgée par `gc()` (365 jours par défaut).
|
|
82
|
+
|
|
51
83
|
## Stockage de session
|
|
52
84
|
|
|
53
85
|
L'import du module enregistre automatiquement un `SessionStorage` sous le handler `"mongoose"`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function __decorateMetadata(k, v) {
|
|
3
3
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
4
|
}
|
package/dist/index.js
CHANGED
|
@@ -8,15 +8,21 @@ import "./nodefony/src/orm-core/index.js";
|
|
|
8
8
|
import { TOKEN_ENTITY_NAMES, accessTokenSchema, createTokenEntities, deniedJtiSchema, registerTokenEntities, subjectRevocationSchema } from "./nodefony/entity/tokenEntity.js";
|
|
9
9
|
import { WEBAUTHN_CREDENTIAL_ENTITY, createWebAuthnCredentialEntity, registerWebAuthnCredentialEntity, webAuthnCredentialSchema } from "./nodefony/entity/webAuthnCredentialEntity.js";
|
|
10
10
|
import { WEBHOOK_ENDPOINT_ENTITY, createWebhookEndpointEntity, registerWebhookEndpointEntity, webhookEndpointSchema } from "./nodefony/entity/webhookEndpointEntity.js";
|
|
11
|
+
import { AUDIT_ENTITY_NAMES, auditEventSchema, createAuditEntities, registerAuditEntities } from "./nodefony/entity/auditEventEntity.js";
|
|
12
|
+
import { TOTP_SECRET_ENTITY, createTotpSecretEntity, registerTotpSecretEntity, totpSecretSchema } from "./nodefony/entity/totpSecretEntity.js";
|
|
13
|
+
import { IDEMPOTENCY_ENTITY_NAME, createIdempotencyEntities, idempotencyKeySchema, registerIdempotencyEntities } from "./nodefony/entity/idempotencyEntity.js";
|
|
14
|
+
import { createUserEntity, registerUserEntity, userSchema } from "./nodefony/entity/userEntity.js";
|
|
11
15
|
import { MongooseTokenStore } from "./nodefony/src/MongooseTokenStore.js";
|
|
12
16
|
import { MongooseWebAuthnCredentialStore } from "./nodefony/src/MongooseWebAuthnCredentialStore.js";
|
|
13
17
|
import { MongooseWebhookStore } from "./nodefony/src/MongooseWebhookStore.js";
|
|
18
|
+
import { MongooseAuditStore } from "./nodefony/src/MongooseAuditStore.js";
|
|
19
|
+
import { MongooseTotpSecretStore } from "./nodefony/src/MongooseTotpSecretStore.js";
|
|
20
|
+
import { MongooseIdempotencyStore } from "./nodefony/src/MongooseIdempotencyStore.js";
|
|
14
21
|
import { FRAMEWORK_CONNECTOR, registerMongooseFrameworkStores } from "./nodefony/registerStores.js";
|
|
15
|
-
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.
|
|
16
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
22
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
23
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
17
24
|
import sessionEntity_default, { SESSION_CONNECTOR, sessionSchema as schema } from "./nodefony/entity/sessionEntity.js";
|
|
18
25
|
import SessionStorage from "./nodefony/src/SessionStorage.js";
|
|
19
|
-
import { createUserEntity, registerUserEntity, userSchema } from "./nodefony/entity/userEntity.js";
|
|
20
26
|
import { MongooseUserRepository } from "./nodefony/src/MongooseUserRepository.js";
|
|
21
27
|
import mongoose from "mongoose";
|
|
22
28
|
import { Kernel, Module, registerErrorAdapter, services } from "nodefony";
|
|
@@ -87,4 +93,4 @@ let Mongoose = class Mongoose extends Module {
|
|
|
87
93
|
Mongoose = __decorate([services([MongooseService]), __decorateMetadata("design:paramtypes", [typeof Kernel === "undefined" ? Object : Kernel])], Mongoose);
|
|
88
94
|
var mongoose_default = Mongoose;
|
|
89
95
|
//#endregion
|
|
90
|
-
export { FRAMEWORK_CONNECTOR, MongooseOrm, MongooseRepository, MongooseService, MongooseTokenStore, MongooseTransaction, MongooseUserRepository, MongooseWebAuthnCredentialStore, MongooseWebhookStore, SESSION_CONNECTOR, sessionEntity_default as SessionEntity, SessionStorage, TOKEN_ENTITY_NAMES, WEBAUTHN_CREDENTIAL_ENTITY, WEBHOOK_ENDPOINT_ENTITY, accessTokenSchema, createTokenEntities, createUserEntity, createWebAuthnCredentialEntity, createWebhookEndpointEntity, mongoose_default as default, defineMongooseConfig, deniedJtiSchema, mongoose, mongooseConfigJsonSchema, mongooseConfigSchema, registerMongooseFrameworkStores, registerTokenEntities, registerUserEntity, registerWebAuthnCredentialEntity, registerWebhookEndpointEntity, schema as sessionSchema, subjectRevocationSchema, userSchema, webAuthnCredentialSchema, webhookEndpointSchema };
|
|
96
|
+
export { AUDIT_ENTITY_NAMES, FRAMEWORK_CONNECTOR, IDEMPOTENCY_ENTITY_NAME, MongooseAuditStore, MongooseIdempotencyStore, MongooseOrm, MongooseRepository, MongooseService, MongooseTokenStore, MongooseTotpSecretStore, MongooseTransaction, MongooseUserRepository, MongooseWebAuthnCredentialStore, MongooseWebhookStore, SESSION_CONNECTOR, sessionEntity_default as SessionEntity, SessionStorage, TOKEN_ENTITY_NAMES, TOTP_SECRET_ENTITY, WEBAUTHN_CREDENTIAL_ENTITY, WEBHOOK_ENDPOINT_ENTITY, accessTokenSchema, auditEventSchema, createAuditEntities, createIdempotencyEntities, createTokenEntities, createTotpSecretEntity, createUserEntity, createWebAuthnCredentialEntity, createWebhookEndpointEntity, mongoose_default as default, defineMongooseConfig, deniedJtiSchema, idempotencyKeySchema, mongoose, mongooseConfigJsonSchema, mongooseConfigSchema, registerAuditEntities, registerIdempotencyEntities, registerMongooseFrameworkStores, registerTokenEntities, registerTotpSecretEntity, registerUserEntity, registerWebAuthnCredentialEntity, registerWebhookEndpointEntity, schema as sessionSchema, subjectRevocationSchema, totpSecretSchema, userSchema, webAuthnCredentialSchema, webhookEndpointSchema };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { entityRegistry } from "@nodefony/orm-core";
|
|
2
|
+
//#region nodefony/entity/auditEventEntity.ts
|
|
3
|
+
/**
|
|
4
|
+
* Schéma Mongoose du **journal d'audit** `@nodefony/security` — pendant
|
|
5
|
+
* documentaire de la table Drizzle `audit_event`, implémentation NoSQL d'
|
|
6
|
+
* `IAuditStore` (append-only, tamper-evident).
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ **`_id` = clé naturelle (String), PAS un ObjectId auto** : l'`id` d'un
|
|
9
|
+
* événement est posé par l'`AuditService` (corrélation + curseur de pagination)
|
|
10
|
+
* → on force `_id: String`, l'id EST la clé primaire. Même convention que
|
|
11
|
+
* `webhookEndpointEntity` et `tokenEntity`.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ **Horodatage = `Number` (epoch ms), `timestamps:false`** : `IAuditEvent.ts`
|
|
14
|
+
* porte un `number` (`Date.now()`), pas une `Date` — et c'est ce champ qui porte
|
|
15
|
+
* l'ordre du journal.
|
|
16
|
+
*
|
|
17
|
+
* **Index** — mêmes axes que la variante SQL, et pour la même raison (filtrage
|
|
18
|
+
* de la console d'audit) ; perf de lecture seule, jamais de sémantique.
|
|
19
|
+
*
|
|
20
|
+
* L'ORDRE TOTAL du store (`ts DESC, _id DESC`) est porté par un index
|
|
21
|
+
* COMPOSITE déclaré sur l'entité (`indexes`, cf `createAuditEntities`) : un
|
|
22
|
+
* `SchemaDefinition` plat n'exprime que des index par champ, et sans lui la
|
|
23
|
+
* requête du journal parcourait toute la collection puis triait en mémoire
|
|
24
|
+
* (plan `COLLSCAN` + `SORT`, mesuré). C'est lui qui garantit, au coût d'un
|
|
25
|
+
* parcours d'index, qu'aucun événement ne se répète ni ne se perd d'une page à
|
|
26
|
+
* l'autre — `audit-store.test.ts` en lit le plan d'exécution.
|
|
27
|
+
*/
|
|
28
|
+
const auditEventSchema = {
|
|
29
|
+
_id: { type: String },
|
|
30
|
+
ts: {
|
|
31
|
+
type: Number,
|
|
32
|
+
required: true,
|
|
33
|
+
index: true
|
|
34
|
+
},
|
|
35
|
+
category: {
|
|
36
|
+
type: String,
|
|
37
|
+
required: true,
|
|
38
|
+
index: true
|
|
39
|
+
},
|
|
40
|
+
action: {
|
|
41
|
+
type: String,
|
|
42
|
+
required: true
|
|
43
|
+
},
|
|
44
|
+
outcome: {
|
|
45
|
+
type: String,
|
|
46
|
+
required: true
|
|
47
|
+
},
|
|
48
|
+
actor: {
|
|
49
|
+
type: String,
|
|
50
|
+
default: null,
|
|
51
|
+
index: true
|
|
52
|
+
},
|
|
53
|
+
resource: {
|
|
54
|
+
type: String,
|
|
55
|
+
default: null
|
|
56
|
+
},
|
|
57
|
+
reason: {
|
|
58
|
+
type: String,
|
|
59
|
+
default: null
|
|
60
|
+
},
|
|
61
|
+
ip: {
|
|
62
|
+
type: String,
|
|
63
|
+
default: null
|
|
64
|
+
},
|
|
65
|
+
userAgent: {
|
|
66
|
+
type: String,
|
|
67
|
+
default: null
|
|
68
|
+
},
|
|
69
|
+
requestId: {
|
|
70
|
+
type: String,
|
|
71
|
+
default: null,
|
|
72
|
+
index: true
|
|
73
|
+
},
|
|
74
|
+
flags: {
|
|
75
|
+
type: Object,
|
|
76
|
+
default: null
|
|
77
|
+
},
|
|
78
|
+
metadata: {
|
|
79
|
+
type: Object,
|
|
80
|
+
default: null
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/** Noms logiques des entités du store (clés de lookup `getRepository`). */
|
|
84
|
+
const AUDIT_ENTITY_NAMES = { events: "audit_event" };
|
|
85
|
+
/**
|
|
86
|
+
* Construit les descripteurs d'entités du journal d'audit pour un ORM nommé.
|
|
87
|
+
*
|
|
88
|
+
* Le `connector` est **dynamique** (nom du connecteur de l'app, ex. `"nodefony"`) :
|
|
89
|
+
* le schéma est statique mais sa liaison à un ORM dépend de la config → pas
|
|
90
|
+
* d'`@entity` figé. Pas de dialecte (NoSQL) : le schéma est portable par
|
|
91
|
+
* construction. À enregistrer **avant** `orm.connect()` (modèle compilé au connect).
|
|
92
|
+
*
|
|
93
|
+
* `module: "security"` → la collection est regroupée sous @nodefony/security dans
|
|
94
|
+
* l'ERD Studio (l'audit est une feature security, hébergée par l'ORM).
|
|
95
|
+
*
|
|
96
|
+
* @param connector - clé de l'ORM cible dans le `ormRegistry`.
|
|
97
|
+
* @returns les descripteurs {@link IEntity} du journal d'audit.
|
|
98
|
+
*/
|
|
99
|
+
function createAuditEntities(connector) {
|
|
100
|
+
return [{
|
|
101
|
+
connector,
|
|
102
|
+
name: AUDIT_ENTITY_NAMES.events,
|
|
103
|
+
module: "security",
|
|
104
|
+
schema: auditEventSchema,
|
|
105
|
+
indexes: [{
|
|
106
|
+
fields: {
|
|
107
|
+
ts: -1,
|
|
108
|
+
_id: -1
|
|
109
|
+
},
|
|
110
|
+
name: "audit_ts_id_desc"
|
|
111
|
+
}]
|
|
112
|
+
}];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Enregistre les entités du journal d'audit dans le `entityRegistry` pour un ORM
|
|
116
|
+
* donné. À appeler **avant** `orm.connect()` (le modèle est compilé au connect).
|
|
117
|
+
*
|
|
118
|
+
* @param connector - nom de la connexion cible (clé du registre).
|
|
119
|
+
*/
|
|
120
|
+
function registerAuditEntities(connector) {
|
|
121
|
+
for (const entity of createAuditEntities(connector)) entityRegistry.register(entity);
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { AUDIT_ENTITY_NAMES, auditEventSchema, createAuditEntities, registerAuditEntities };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { entityRegistry } from "@nodefony/orm-core";
|
|
2
|
+
//#region nodefony/entity/idempotencyEntity.ts
|
|
3
|
+
/**
|
|
4
|
+
* Schéma Mongoose du **store d'idempotence** — pendant documentaire de la table
|
|
5
|
+
* Drizzle `idempotency_key`, implémentation NoSQL d'`IIdempotencyStore` (contrat
|
|
6
|
+
* au CORE) pour dédoublonner les mutations rejouées PARTAGÉ cross-pod, là où le
|
|
7
|
+
* store mémoire par défaut reste affine à un pod.
|
|
8
|
+
*
|
|
9
|
+
* **Pourquoi MongoDB plutôt que Redis** : un cluster qui possède déjà une base
|
|
10
|
+
* Mongo mais pas de Redis obtient la dédup cross-pod sans nouvelle infra — le
|
|
11
|
+
* même argument qui fonde la variante SQL.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ **`_id` = la clé d'idempotence (String)** : c'est la contrainte d'unicité de
|
|
14
|
+
* la clé primaire qui porte la **réservation atomique** de `begin`
|
|
15
|
+
* (`findOneAndUpdate(… , {upsert:true})` → `E11000` pour le perdant). Faire
|
|
16
|
+
* porter l'unicité par un index secondaire serait un faux ami : Mongoose
|
|
17
|
+
* construit ses index en tâche de fond, donc la contrainte n'existerait pas
|
|
18
|
+
* pendant la fenêtre de construction, et deux `begin` concurrents renverraient
|
|
19
|
+
* tous deux `fresh` — exactement le double-effet que ce store existe pour
|
|
20
|
+
* empêcher. Sur `_id`, la contrainte existe dès le premier document.
|
|
21
|
+
*
|
|
22
|
+
* Le champ `key` est **dupliqué** à côté de `_id` : c'est lui que le vocabulaire
|
|
23
|
+
* public filtre (`q` = préfixe de clé) et que `listPage` rend. Les deux sont
|
|
24
|
+
* écrits ensemble, jamais séparément.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ **PAS de TTL index natif** (≠ `expireAfterSeconds`) : l'échéance d'une
|
|
27
|
+
* entrée n'est pas une durée fixe depuis sa création — c'est d'abord un **bail**
|
|
28
|
+
* *in-flight* (`now + lease`), puis une **rétention** (`now + ttl`) posée à la
|
|
29
|
+
* complétion. Un TTL index effacerait selon une règle qui ignore cette bascule.
|
|
30
|
+
* `expiresAt` est donc un `Number` (epoch ms) indexé, purgé par le `gc()`
|
|
31
|
+
* applicatif — même contrepartie que la variante SQL.
|
|
32
|
+
*
|
|
33
|
+
* `state` : `if` = réservation *in-flight* ; `done` = réponse mémorisée
|
|
34
|
+
* (rejouée en `replayed`). `response` = la réponse mémorisée, `null` tant
|
|
35
|
+
* qu'*in-flight* ; elle ne sort JAMAIS par `listPage` (anti-IDOR sur le cache).
|
|
36
|
+
*/
|
|
37
|
+
const idempotencyKeySchema = {
|
|
38
|
+
_id: { type: String },
|
|
39
|
+
key: {
|
|
40
|
+
type: String,
|
|
41
|
+
required: true
|
|
42
|
+
},
|
|
43
|
+
fingerprint: {
|
|
44
|
+
type: String,
|
|
45
|
+
required: true
|
|
46
|
+
},
|
|
47
|
+
state: {
|
|
48
|
+
type: String,
|
|
49
|
+
required: true
|
|
50
|
+
},
|
|
51
|
+
response: {
|
|
52
|
+
type: Object,
|
|
53
|
+
default: null
|
|
54
|
+
},
|
|
55
|
+
expiresAt: {
|
|
56
|
+
type: Number,
|
|
57
|
+
required: true,
|
|
58
|
+
index: true
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/** Nom logique de l'entité (clé de lookup `getRepository` / `entityRegistry`). */
|
|
62
|
+
const IDEMPOTENCY_ENTITY_NAME = "idempotency_key";
|
|
63
|
+
/**
|
|
64
|
+
* Construit le descripteur d'entité du store d'idempotence pour un ORM nommé.
|
|
65
|
+
*
|
|
66
|
+
* Le `connector` est **dynamique** (nom du connecteur de l'app, ex. `"nodefony"`) :
|
|
67
|
+
* le schéma est statique mais sa liaison à un ORM dépend de la config → pas
|
|
68
|
+
* d'`@entity` figé. À enregistrer **avant** `orm.connect()` (le modèle est
|
|
69
|
+
* compilé au connect).
|
|
70
|
+
*
|
|
71
|
+
* `module: "framework"` → la collection est regroupée sous @nodefony/framework
|
|
72
|
+
* dans l'ERD Studio (l'idempotence est une feature framework — data plane admin
|
|
73
|
+
* + `@Idempotent` — simplement hébergée par l'ORM).
|
|
74
|
+
*
|
|
75
|
+
* @param connector - clé de l'ORM cible dans le `ormRegistry`.
|
|
76
|
+
* @returns les descripteurs {@link IEntity} du store d'idempotence.
|
|
77
|
+
*/
|
|
78
|
+
function createIdempotencyEntities(connector) {
|
|
79
|
+
return [{
|
|
80
|
+
connector,
|
|
81
|
+
name: IDEMPOTENCY_ENTITY_NAME,
|
|
82
|
+
module: "framework",
|
|
83
|
+
schema: idempotencyKeySchema
|
|
84
|
+
}];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Enregistre l'entité du store d'idempotence dans le `entityRegistry` pour un
|
|
88
|
+
* ORM donné. À appeler **avant** `orm.connect()`.
|
|
89
|
+
*
|
|
90
|
+
* @param connector - nom de la connexion cible (clé du registre).
|
|
91
|
+
*/
|
|
92
|
+
function registerIdempotencyEntities(connector) {
|
|
93
|
+
for (const entity of createIdempotencyEntities(connector)) entityRegistry.register(entity);
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { IDEMPOTENCY_ENTITY_NAME, createIdempotencyEntities, idempotencyKeySchema, registerIdempotencyEntities };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
2
2
|
import { entity } from "@nodefony/orm-core";
|
|
3
3
|
//#region nodefony/entity/sessionEntity.ts
|
|
4
4
|
/** ORM cible du stockage de session (connecteur par défaut du module). */
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { entityRegistry } from "@nodefony/orm-core";
|
|
2
|
+
//#region nodefony/entity/totpSecretEntity.ts
|
|
3
|
+
/**
|
|
4
|
+
* Schéma Mongoose du **store de secrets TOTP** `@nodefony/security` (2FA) —
|
|
5
|
+
* pendant documentaire de la table Drizzle `totp_secret`, implémentation NoSQL
|
|
6
|
+
* d'`ITotpSecretStore`.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ **`_id` = `userId` (String)** : le modèle est **1 secret / utilisateur**, donc
|
|
9
|
+
* la clé naturelle EST l'utilisateur — `save` devient un upsert par `_id`, et
|
|
10
|
+
* l'unicité est portée par la clé primaire elle-même (aucun index à construire
|
|
11
|
+
* en tâche de fond, donc aucune fenêtre où la contrainte n'existerait pas).
|
|
12
|
+
*
|
|
13
|
+
* Le champ `userId` est **dupliqué** à côté de `_id` : c'est lui que le
|
|
14
|
+
* vocabulaire public trie et filtre (`q` = préfixe d'`userId`), et un document
|
|
15
|
+
* Mongo n'expose pas `_id` sous un autre nom. Les deux sont écrits ensemble par
|
|
16
|
+
* le store, jamais séparément.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ **`secretEnc` = secret déjà CHIFFRÉ** (AES-256-GCM par le service détenteur
|
|
19
|
+
* de la clé) : le store ne voit que des octets opaques, jamais le secret `K` en
|
|
20
|
+
* clair. Il ne sort JAMAIS par `listPage` (cf `ITotpEnrollmentSummary`).
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ **Horodatages = `Number` (epoch ms), `timestamps:false`** — sauf
|
|
23
|
+
* `lastUsedStep`, qui est une **tranche `T`** RFC 6238 (`floor(epochSeconds /
|
|
24
|
+
* period)`), pas un horodatage.
|
|
25
|
+
*
|
|
26
|
+
* `recoveryCodes` = tableau de condensats `sha256` des codes NON consommés ; le
|
|
27
|
+
* store les réécrit en bloc (jamais de mutation partielle in-place).
|
|
28
|
+
*/
|
|
29
|
+
const totpSecretSchema = {
|
|
30
|
+
_id: { type: String },
|
|
31
|
+
userId: {
|
|
32
|
+
type: String,
|
|
33
|
+
required: true
|
|
34
|
+
},
|
|
35
|
+
secretEnc: {
|
|
36
|
+
type: String,
|
|
37
|
+
required: true
|
|
38
|
+
},
|
|
39
|
+
algorithm: {
|
|
40
|
+
type: String,
|
|
41
|
+
required: true
|
|
42
|
+
},
|
|
43
|
+
digits: {
|
|
44
|
+
type: Number,
|
|
45
|
+
required: true
|
|
46
|
+
},
|
|
47
|
+
period: {
|
|
48
|
+
type: Number,
|
|
49
|
+
required: true
|
|
50
|
+
},
|
|
51
|
+
recoveryCodes: {
|
|
52
|
+
type: [String],
|
|
53
|
+
default: []
|
|
54
|
+
},
|
|
55
|
+
confirmedAt: {
|
|
56
|
+
type: Number,
|
|
57
|
+
default: null
|
|
58
|
+
},
|
|
59
|
+
lastUsedStep: {
|
|
60
|
+
type: Number,
|
|
61
|
+
default: null
|
|
62
|
+
},
|
|
63
|
+
createdAt: {
|
|
64
|
+
type: Number,
|
|
65
|
+
required: true
|
|
66
|
+
},
|
|
67
|
+
lastUsedAt: {
|
|
68
|
+
type: Number,
|
|
69
|
+
default: null
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
/** Nom logique de l'entité (clé de lookup `getRepository`). */
|
|
73
|
+
const TOTP_SECRET_ENTITY = "totp_secret";
|
|
74
|
+
/**
|
|
75
|
+
* Construit le descripteur d'entité du store de secrets TOTP pour un ORM nommé.
|
|
76
|
+
*
|
|
77
|
+
* Le `connector` est **dynamique** (nom du connecteur de l'app, ex. `"nodefony"`) :
|
|
78
|
+
* le schéma est statique mais sa liaison à un ORM dépend de la config → pas
|
|
79
|
+
* d'`@entity` figé. À enregistrer **avant** `orm.connect()` (le modèle est
|
|
80
|
+
* compilé au connect). `module: "security"` → regroupé sous @nodefony/security
|
|
81
|
+
* dans l'ERD Studio.
|
|
82
|
+
*
|
|
83
|
+
* @param connector - clé de l'ORM cible dans le `ormRegistry`.
|
|
84
|
+
*/
|
|
85
|
+
function createTotpSecretEntity(connector) {
|
|
86
|
+
return {
|
|
87
|
+
connector,
|
|
88
|
+
name: TOTP_SECRET_ENTITY,
|
|
89
|
+
module: "security",
|
|
90
|
+
schema: totpSecretSchema
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Enregistre l'entité du store de secrets TOTP dans le `entityRegistry` pour un
|
|
95
|
+
* ORM donné. À appeler **avant** `orm.connect()`.
|
|
96
|
+
*
|
|
97
|
+
* @param connector - nom de la connexion cible (clé du registre).
|
|
98
|
+
*/
|
|
99
|
+
function registerTotpSecretEntity(connector) {
|
|
100
|
+
entityRegistry.register(createTotpSecretEntity(connector));
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
export { TOTP_SECRET_ENTITY, createTotpSecretEntity, registerTotpSecretEntity, totpSecretSchema };
|
|
@@ -3,11 +3,19 @@ import "./src/orm-core/index.js";
|
|
|
3
3
|
import { TOKEN_ENTITY_NAMES, registerTokenEntities } from "./entity/tokenEntity.js";
|
|
4
4
|
import { WEBAUTHN_CREDENTIAL_ENTITY, registerWebAuthnCredentialEntity } from "./entity/webAuthnCredentialEntity.js";
|
|
5
5
|
import { WEBHOOK_ENDPOINT_ENTITY, registerWebhookEndpointEntity } from "./entity/webhookEndpointEntity.js";
|
|
6
|
+
import { AUDIT_ENTITY_NAMES, registerAuditEntities } from "./entity/auditEventEntity.js";
|
|
7
|
+
import { TOTP_SECRET_ENTITY, registerTotpSecretEntity } from "./entity/totpSecretEntity.js";
|
|
8
|
+
import { IDEMPOTENCY_ENTITY_NAME, registerIdempotencyEntities } from "./entity/idempotencyEntity.js";
|
|
9
|
+
import { registerUserEntity } from "./entity/userEntity.js";
|
|
6
10
|
import { MongooseTokenStore } from "./src/MongooseTokenStore.js";
|
|
7
11
|
import { MongooseWebAuthnCredentialStore } from "./src/MongooseWebAuthnCredentialStore.js";
|
|
8
12
|
import { MongooseWebhookStore } from "./src/MongooseWebhookStore.js";
|
|
13
|
+
import { MongooseAuditStore } from "./src/MongooseAuditStore.js";
|
|
14
|
+
import { MongooseTotpSecretStore } from "./src/MongooseTotpSecretStore.js";
|
|
15
|
+
import { MongooseIdempotencyStore } from "./src/MongooseIdempotencyStore.js";
|
|
9
16
|
import { entityRegistry, ormRegistry } from "@nodefony/orm-core";
|
|
10
|
-
import { getTokenStoreFactory, getWebAuthnStoreFactory, getWebhookStoreFactory, registerTokenStore, registerWebAuthnStore, registerWebhookStore } from "@nodefony/security";
|
|
17
|
+
import { getAuditStoreFactory, getTokenStoreFactory, getTotpStoreFactory, getWebAuthnStoreFactory, getWebhookStoreFactory, registerAuditStore, registerTokenStore, registerTotpStore, registerWebAuthnStore, registerWebhookStore } from "@nodefony/security";
|
|
18
|
+
import { getIdempotencyStoreFactory, registerIdempotencyStore } from "@nodefony/framework";
|
|
11
19
|
//#region nodefony/registerStores.ts
|
|
12
20
|
/**
|
|
13
21
|
* AUTO-ENREGISTREMENT des backends framework portés par Mongoose — « charger le
|
|
@@ -18,9 +26,12 @@ import { getTokenStoreFactory, getWebAuthnStoreFactory, getWebhookStoreFactory,
|
|
|
18
26
|
* modèles sont compilés à la connexion). Pas de dialecte (NoSQL) : les schémas
|
|
19
27
|
* sont portables par construction.
|
|
20
28
|
*
|
|
21
|
-
* Couverture
|
|
22
|
-
* webhooks
|
|
23
|
-
*
|
|
29
|
+
* Couverture : session (auto via `@entity`), utilisateurs, tokens, webauthn,
|
|
30
|
+
* webhooks, audit, TOTP et idempotence — soit **la totalité des briques
|
|
31
|
+
* durables**. C'est la propriété que Mongo doit tenir : une application doit
|
|
32
|
+
* pouvoir tourner SANS aucun backend SQL, et un backend durable est un chemin
|
|
33
|
+
* complet ou n'en est pas un. Une brique qui manquerait se sélectionnerait en
|
|
34
|
+
* échec franc (« store inconnu »), jamais en silence.
|
|
24
35
|
*
|
|
25
36
|
* Mêmes garde-fous que Drizzle : entité `has`-guarded (l'app garde la main),
|
|
26
37
|
* fabrique `get`-guarded (premier-arrivé-premier-servi).
|
|
@@ -67,6 +78,7 @@ function registerMongooseFrameworkStores() {
|
|
|
67
78
|
}
|
|
68
79
|
registerFactory();
|
|
69
80
|
};
|
|
81
|
+
wire("User", () => registerUserEntity(FRAMEWORK_CONNECTOR), () => {});
|
|
70
82
|
wire(TOKEN_ENTITY_NAMES.records, () => registerTokenEntities(FRAMEWORK_CONNECTOR), () => {
|
|
71
83
|
if (getTokenStoreFactory("mongoose")) return;
|
|
72
84
|
registerTokenStore("mongoose", (ctx) => {
|
|
@@ -83,6 +95,31 @@ function registerMongooseFrameworkStores() {
|
|
|
83
95
|
if (getWebhookStoreFactory("mongoose")) return;
|
|
84
96
|
registerWebhookStore("mongoose", () => MongooseWebhookStore.from(resolveConnectedOrm(`webhooks.store "mongoose"`)));
|
|
85
97
|
});
|
|
98
|
+
wire(AUDIT_ENTITY_NAMES.events, () => registerAuditEntities(FRAMEWORK_CONNECTOR), () => {
|
|
99
|
+
if (getAuditStoreFactory("mongoose")) return;
|
|
100
|
+
registerAuditStore("mongoose", (ctx) => {
|
|
101
|
+
const orm = resolveConnectedOrm(`audit.store "mongoose"`);
|
|
102
|
+
const days = ctx?.config?.audit?.retentionDays;
|
|
103
|
+
return MongooseAuditStore.from(orm, void 0, typeof days === "number" ? days * 864e5 : void 0);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
wire(TOTP_SECRET_ENTITY, () => registerTotpSecretEntity(FRAMEWORK_CONNECTOR), () => {
|
|
107
|
+
if (getTotpStoreFactory("mongoose")) return;
|
|
108
|
+
registerTotpStore("mongoose", () => MongooseTotpSecretStore.from(resolveConnectedOrm(`totp.store "mongoose"`)));
|
|
109
|
+
});
|
|
110
|
+
wire(IDEMPOTENCY_ENTITY_NAME, () => registerIdempotencyEntities(FRAMEWORK_CONNECTOR), () => {
|
|
111
|
+
if (getIdempotencyStoreFactory("mongoose")) return;
|
|
112
|
+
registerIdempotencyStore("mongoose", () => new MongooseIdempotencyStore(() => {
|
|
113
|
+
let orm;
|
|
114
|
+
try {
|
|
115
|
+
orm = ormRegistry.get(FRAMEWORK_CONNECTOR);
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
if (!(orm instanceof MongooseOrm) || !orm.isConnected()) return null;
|
|
120
|
+
return orm.getNativeConnection().model(IDEMPOTENCY_ENTITY_NAME);
|
|
121
|
+
}, void 0, void 0, void 0, FRAMEWORK_CONNECTOR));
|
|
122
|
+
});
|
|
86
123
|
return report;
|
|
87
124
|
}
|
|
88
125
|
//#endregion
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MongooseOrm } from "../src/orm-core/MongooseOrm.js";
|
|
2
2
|
import mongoose from "mongoose";
|
|
3
3
|
import { Service, runNeedsExternalServices } from "nodefony";
|
|
4
|
-
import { diagnoseConnectionFailure, parseConnectionTarget, queryFlowMonitor, resolveOrmFlowEnabled } from "@nodefony/orm-core";
|
|
4
|
+
import { describeConnectFailure, diagnoseConnectionFailure, parseConnectionTarget, queryFlowMonitor, reportOrphanEntities, resolveOrmFlowEnabled } from "@nodefony/orm-core";
|
|
5
5
|
//#region nodefony/service/MongooseService.ts
|
|
6
6
|
const serviceName = "mongoose";
|
|
7
7
|
/**
|
|
@@ -27,6 +27,9 @@ var MongooseService = class MongooseService extends Service {
|
|
|
27
27
|
queryFlowMonitor.setEnabled(resolveOrmFlowEnabled(this.kernel));
|
|
28
28
|
await this.connectAll(runNeedsExternalServices(this.kernel));
|
|
29
29
|
});
|
|
30
|
+
this.module.hookKernel("onReady", () => {
|
|
31
|
+
reportOrphanEntities((message, severity) => this.log(message, severity), this.kernel ?? this, Object.keys(this.#config()?.connectors ?? {}));
|
|
32
|
+
});
|
|
30
33
|
this.kernel?.once("onTerminate", async () => {
|
|
31
34
|
await this.disconnectAll().catch(() => {});
|
|
32
35
|
});
|
|
@@ -79,7 +82,7 @@ var MongooseService = class MongooseService extends Service {
|
|
|
79
82
|
/** Connecte un connecteur (URI + options d'auth/pool). */
|
|
80
83
|
async #connectOne(name, cfg, connect = true) {
|
|
81
84
|
const uri = MongooseService.buildUri(cfg);
|
|
82
|
-
const orm = new MongooseOrm(name, uri, MongooseService.buildConnectOptions(cfg));
|
|
85
|
+
const orm = new MongooseOrm(name, uri, MongooseService.buildConnectOptions(cfg), this.container ?? void 0);
|
|
83
86
|
if (!connect) {
|
|
84
87
|
this.#orms.set(name, orm);
|
|
85
88
|
this.log(`Mongoose « ${name} » enregistré SANS connexion (${orm.safeTarget()}) — ce run n'a pas déclaré \`externalServices\`.`, "DEBUG");
|
|
@@ -90,7 +93,7 @@ var MongooseService = class MongooseService extends Service {
|
|
|
90
93
|
} catch (e) {
|
|
91
94
|
const diagnosis = diagnoseConnectionFailure(e, parseConnectionTarget(uri));
|
|
92
95
|
const cause = e instanceof Error ? e.message : String(e);
|
|
93
|
-
throw new Error(`Mongoose : le connecteur "${name}" (${orm.safeTarget()})
|
|
96
|
+
throw new Error(describeConnectFailure(`Mongoose : le connecteur "${name}" (${orm.safeTarget()})`, diagnosis, cause), { cause: e });
|
|
94
97
|
}
|
|
95
98
|
this.#orms.set(name, orm);
|
|
96
99
|
this.log(`Mongoose ORM "${name}" connected (${orm.safeTarget()})`, "INFO");
|