@nodefony/mongoose 10.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +97 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +90 -0
  6. package/dist/nodefony/config/config.js +57 -0
  7. package/dist/nodefony/config/defineModuleConfig.js +60 -0
  8. package/dist/nodefony/entity/sessionEntity.js +63 -0
  9. package/dist/nodefony/entity/tokenEntity.js +184 -0
  10. package/dist/nodefony/entity/userEntity.js +106 -0
  11. package/dist/nodefony/entity/webAuthnCredentialEntity.js +97 -0
  12. package/dist/nodefony/entity/webhookEndpointEntity.js +109 -0
  13. package/dist/nodefony/interfaces/IMongooseConfig.js +1 -0
  14. package/dist/nodefony/interfaces/index.js +1 -0
  15. package/dist/nodefony/registerStores.js +89 -0
  16. package/dist/nodefony/service/MongooseService.js +95 -0
  17. package/dist/nodefony/src/MongooseTokenStore.js +237 -0
  18. package/dist/nodefony/src/MongooseUserRepository.js +205 -0
  19. package/dist/nodefony/src/MongooseWebAuthnCredentialStore.js +144 -0
  20. package/dist/nodefony/src/MongooseWebhookStore.js +181 -0
  21. package/dist/nodefony/src/SessionStorage.js +241 -0
  22. package/dist/nodefony/src/mongoOrder.js +49 -0
  23. package/dist/nodefony/src/orm-core/MongooseOrm.js +440 -0
  24. package/dist/nodefony/src/orm-core/MongooseRepository.js +300 -0
  25. package/dist/nodefony/src/orm-core/MongooseTransaction.js +53 -0
  26. package/dist/nodefony/src/orm-core/index.js +4 -0
  27. package/dist/types/index.d.ts +74 -0
  28. package/dist/types/nodefony/config/config.d.ts +21 -0
  29. package/dist/types/nodefony/config/defineModuleConfig.d.ts +26 -0
  30. package/dist/types/nodefony/entity/sessionEntity.d.ts +42 -0
  31. package/dist/types/nodefony/entity/tokenEntity.d.ts +59 -0
  32. package/dist/types/nodefony/entity/userEntity.d.ts +54 -0
  33. package/dist/types/nodefony/entity/webAuthnCredentialEntity.d.ts +61 -0
  34. package/dist/types/nodefony/entity/webhookEndpointEntity.d.ts +62 -0
  35. package/dist/types/nodefony/interfaces/IMongooseConfig.d.ts +17 -0
  36. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  37. package/dist/types/nodefony/registerStores.d.ts +37 -0
  38. package/dist/types/nodefony/service/MongooseService.d.ts +48 -0
  39. package/dist/types/nodefony/src/MongooseTokenStore.d.ts +126 -0
  40. package/dist/types/nodefony/src/MongooseUserRepository.d.ts +82 -0
  41. package/dist/types/nodefony/src/MongooseWebAuthnCredentialStore.d.ts +52 -0
  42. package/dist/types/nodefony/src/MongooseWebhookStore.d.ts +72 -0
  43. package/dist/types/nodefony/src/SessionStorage.d.ts +63 -0
  44. package/dist/types/nodefony/src/mongoOrder.d.ts +40 -0
  45. package/dist/types/nodefony/src/orm-core/MongooseOrm.d.ts +132 -0
  46. package/dist/types/nodefony/src/orm-core/MongooseRepository.d.ts +51 -0
  47. package/dist/types/nodefony/src/orm-core/MongooseTransaction.d.ts +37 -0
  48. package/dist/types/nodefony/src/orm-core/index.d.ts +9 -0
  49. package/docs/configuration.md +776 -0
  50. package/docs/index.md +881 -0
  51. package/package.json +97 -0
@@ -0,0 +1,109 @@
1
+ import { entityRegistry } from "@nodefony/orm-core";
2
+ //#region nodefony/entity/webhookEndpointEntity.ts
3
+ /**
4
+ * Schéma Mongoose du **store d'endpoints webhook** `@nodefony/security` (P6.13,
5
+ * pendant documentaire de la table Drizzle) — implémentation NoSQL d'
6
+ * `IWebhookStore` (registre durable des destinations notifiées).
7
+ *
8
+ * ⚠️ **`_id` = clé naturelle (String), PAS un ObjectId auto** : l'`id` d'un
9
+ * endpoint est un identifiant `wh_<random>` **fourni** (pas généré par Mongo) →
10
+ * on force `_id: String` → l'id EST la clé primaire. Le contrat `IRepository`
11
+ * traduit le critère `{ id }` en `{ _id }` ; le virtuel `id` (activé par
12
+ * `MongooseOrm`, `toObject:{virtuals:true}`) renvoie `String(_id)` = l'id.
13
+ *
14
+ * ⚠️ **Horodatages = `Number` (epoch ms), `timestamps:false`** : `IWebhookEndpoint`
15
+ * porte des `number` (`Date.now()`) et l'appelant fournit `createdAt`/`updatedAt`.
16
+ *
17
+ * `events` = tableau de strings ; `metadata` = objet libre (`Object`/Mixed, idem
18
+ * `tokenEntity.metadata`). Le store réécrit ces champs en bloc (pas de mutation
19
+ * partielle in-place) → pas de souci de change-tracking Mixed.
20
+ */
21
+ const webhookEndpointSchema = {
22
+ _id: { type: String },
23
+ url: {
24
+ type: String,
25
+ required: true
26
+ },
27
+ secretEnc: {
28
+ type: String,
29
+ required: true
30
+ },
31
+ events: {
32
+ type: [String],
33
+ default: []
34
+ },
35
+ enabled: {
36
+ type: Boolean,
37
+ required: true
38
+ },
39
+ description: {
40
+ type: String,
41
+ default: null
42
+ },
43
+ tenantId: {
44
+ type: String,
45
+ default: null
46
+ },
47
+ createdBy: {
48
+ type: String,
49
+ default: null
50
+ },
51
+ createdAt: {
52
+ type: Number,
53
+ required: true
54
+ },
55
+ updatedAt: {
56
+ type: Number,
57
+ required: true
58
+ },
59
+ lastDeliveryAt: {
60
+ type: Number,
61
+ default: null
62
+ },
63
+ lastDeliveryStatus: {
64
+ type: Number,
65
+ default: null
66
+ },
67
+ lastDeliveryError: {
68
+ type: String,
69
+ default: null
70
+ },
71
+ failureCount: {
72
+ type: Number,
73
+ required: true
74
+ },
75
+ metadata: {
76
+ type: Object,
77
+ default: {}
78
+ }
79
+ };
80
+ /** Nom logique de l'entité (clé de lookup `getRepository`). */
81
+ const WEBHOOK_ENDPOINT_ENTITY = "webhook_endpoint";
82
+ /**
83
+ * Construit le descripteur d'entité du store webhook pour un ORM nommé.
84
+ *
85
+ * Le `connector` est **dynamique** (nom du connecteur de l'app, ex. `"nodefony"`) : le
86
+ * schéma est statique mais sa liaison à un ORM dépend de la config → pas
87
+ * d'`@entity` figé. `timestamps:false`. À enregistrer **avant** `orm.connect()`.
88
+ *
89
+ * @param orm - clé de l'ORM cible dans le `ormRegistry`.
90
+ */
91
+ function createWebhookEndpointEntity(connector) {
92
+ return {
93
+ connector,
94
+ name: WEBHOOK_ENDPOINT_ENTITY,
95
+ module: "security",
96
+ schema: webhookEndpointSchema
97
+ };
98
+ }
99
+ /**
100
+ * Enregistre l'entité du store webhook dans le `entityRegistry` pour un ORM
101
+ * donné. À appeler **avant** `orm.connect()` (le modèle est compilé au connect).
102
+ *
103
+ * @param connector - nom de la connexion cible (clé du registre).
104
+ */
105
+ function registerWebhookEndpointEntity(connector) {
106
+ entityRegistry.register(createWebhookEndpointEntity(connector));
107
+ }
108
+ //#endregion
109
+ export { WEBHOOK_ENDPOINT_ENTITY, createWebhookEndpointEntity, registerWebhookEndpointEntity, webhookEndpointSchema };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ import { MongooseOrm } from "./src/orm-core/MongooseOrm.js";
2
+ import "./src/orm-core/index.js";
3
+ import { TOKEN_ENTITY_NAMES, registerTokenEntities } from "./entity/tokenEntity.js";
4
+ import { WEBAUTHN_CREDENTIAL_ENTITY, registerWebAuthnCredentialEntity } from "./entity/webAuthnCredentialEntity.js";
5
+ import { WEBHOOK_ENDPOINT_ENTITY, registerWebhookEndpointEntity } from "./entity/webhookEndpointEntity.js";
6
+ import { MongooseTokenStore } from "./src/MongooseTokenStore.js";
7
+ import { MongooseWebAuthnCredentialStore } from "./src/MongooseWebAuthnCredentialStore.js";
8
+ import { MongooseWebhookStore } from "./src/MongooseWebhookStore.js";
9
+ import { entityRegistry, ormRegistry } from "@nodefony/orm-core";
10
+ import { getTokenStoreFactory, getWebAuthnStoreFactory, getWebhookStoreFactory, registerTokenStore, registerWebAuthnStore, registerWebhookStore } from "@nodefony/security";
11
+ //#region nodefony/registerStores.ts
12
+ /**
13
+ * AUTO-ENREGISTREMENT des backends framework portés par Mongoose — « charger le
14
+ * module = ses backends deviennent sélectionnables par simple nom » (convention-
15
+ * frère : `registerDrizzleFrameworkStores` de `@nodefony/drizzle`).
16
+ *
17
+ * Appelé par `Mongoose.onKernelRegister` (AVANT le connect de `onBoot` — les
18
+ * modèles sont compilés à la connexion). Pas de dialecte (NoSQL) : les schémas
19
+ * sont portables par construction.
20
+ *
21
+ * Couverture PARTIELLE assumée : session (auto via `@entity`), tokens, webauthn,
22
+ * webhooks. PAS d'implémentation mongoose pour l'audit ni l'idempotence — les
23
+ * sélectionner sur mongoose échoue franc (« store inconnu »), jamais en silence.
24
+ *
25
+ * Mêmes garde-fous que Drizzle : entité `has`-guarded (l'app garde la main),
26
+ * fabrique `get`-guarded (premier-arrivé-premier-servi).
27
+ */
28
+ /**
29
+ * Connecteur conventionnel qui héberge le schéma framework (`"nodefony"` pour
30
+ * Mongoose, ≠ `"default"` de Drizzle — isole les entités homonymes dans le
31
+ * `entityRegistry` process-wide si les deux ORM cohabitent).
32
+ */
33
+ const FRAMEWORK_CONNECTOR = "nodefony";
34
+ /**
35
+ * Résout l'ORM `nodefony` CONNECTÉ pour une fabrique de store — échec FRANC avec
36
+ * la cause exacte (module absent / ordre de boot) : principe « pas de dégradation
37
+ * silencieuse ».
38
+ */
39
+ function resolveConnectedOrm(store) {
40
+ let orm;
41
+ try {
42
+ orm = ormRegistry.get(FRAMEWORK_CONNECTOR);
43
+ } catch {
44
+ throw new Error(`${store} : ORM "${FRAMEWORK_CONNECTOR}" introuvable — charger @nodefony/mongoose AVANT @nodefony/security dans le manifeste "modules".`);
45
+ }
46
+ if (!(orm instanceof MongooseOrm)) throw new Error(`${store} : l'ORM "${FRAMEWORK_CONNECTOR}" n'est pas un MongooseOrm (connecteur homonyme d'un autre driver ?).`);
47
+ if (!orm.isConnected()) throw new Error(`${store} : ORM "${FRAMEWORK_CONNECTOR}" non connecté au montage du store (ordre de boot).`);
48
+ return orm;
49
+ }
50
+ /**
51
+ * Déclare les entités framework Mongoose (connecteur `nodefony`) et enregistre
52
+ * leurs fabriques de stores dans les registres de `@nodefony/security`.
53
+ * Idempotent (guards) — rejouable sans effet.
54
+ *
55
+ * @returns bilan à logger (registered / appOwned)
56
+ */
57
+ function registerMongooseFrameworkStores() {
58
+ const report = {
59
+ registered: [],
60
+ appOwned: []
61
+ };
62
+ const wire = (entityName, registerEntity, registerFactory) => {
63
+ if (entityRegistry.has(entityName, "nodefony")) report.appOwned.push(entityName);
64
+ else {
65
+ registerEntity();
66
+ report.registered.push(entityName);
67
+ }
68
+ registerFactory();
69
+ };
70
+ wire(TOKEN_ENTITY_NAMES.records, () => registerTokenEntities(FRAMEWORK_CONNECTOR), () => {
71
+ if (getTokenStoreFactory("mongoose")) return;
72
+ registerTokenStore("mongoose", (ctx) => {
73
+ const orm = resolveConnectedOrm(`tokenStore "mongoose"`);
74
+ const days = ctx?.config?.tokenStore?.retentionRevokedDays;
75
+ return MongooseTokenStore.from(orm, void 0, typeof days === "number" ? days * 864e5 : void 0);
76
+ });
77
+ });
78
+ wire(WEBAUTHN_CREDENTIAL_ENTITY, () => registerWebAuthnCredentialEntity(FRAMEWORK_CONNECTOR), () => {
79
+ if (getWebAuthnStoreFactory("mongoose")) return;
80
+ registerWebAuthnStore("mongoose", () => MongooseWebAuthnCredentialStore.from(resolveConnectedOrm(`passkeys.store "mongoose"`)));
81
+ });
82
+ wire(WEBHOOK_ENDPOINT_ENTITY, () => registerWebhookEndpointEntity(FRAMEWORK_CONNECTOR), () => {
83
+ if (getWebhookStoreFactory("mongoose")) return;
84
+ registerWebhookStore("mongoose", () => MongooseWebhookStore.from(resolveConnectedOrm(`webhooks.store "mongoose"`)));
85
+ });
86
+ return report;
87
+ }
88
+ //#endregion
89
+ export { FRAMEWORK_CONNECTOR, registerMongooseFrameworkStores };
@@ -0,0 +1,95 @@
1
+ import { MongooseOrm } from "../src/orm-core/MongooseOrm.js";
2
+ import mongoose from "mongoose";
3
+ import { Service } from "nodefony";
4
+ import { queryFlowMonitor, resolveOrmFlowEnabled } from "@nodefony/orm-core";
5
+ //#region nodefony/service/MongooseService.ts
6
+ const serviceName = "mongoose";
7
+ /**
8
+ * Service bootable du module `@nodefony/mongoose` (driver NoSQL).
9
+ *
10
+ * Au boot du kernel (`onBoot`), instancie un {@link MongooseOrm} (adapter
11
+ * orm-core) **par connecteur** déclaré dans la config et le connecte ; chaque
12
+ * ORM s'auto-enregistre dans le `ormRegistry`. Ferme proprement les connexions
13
+ * à `onTerminate`.
14
+ *
15
+ * Refonte de l'ancien `Mongoose extends Orm` (core legacy) : ce service ne
16
+ * dérive plus de la base ORM du core — il **orchestre** des adapters orm-core
17
+ * autonomes, exactement comme `DrizzleService`. Le core ne connaît plus l'ORM.
18
+ */
19
+ var MongooseService = class MongooseService extends Service {
20
+ module;
21
+ /** ORM connectés, indexés par nom de connecteur. */
22
+ #orms = /* @__PURE__ */ new Map();
23
+ constructor(module) {
24
+ super(serviceName, module.container, module.notificationsCenter, module.options ?? {});
25
+ this.module = module;
26
+ this.module.hookKernel("onBoot", async () => {
27
+ queryFlowMonitor.setEnabled(resolveOrmFlowEnabled(this.kernel));
28
+ await this.connectAll().catch((e) => {
29
+ this.log(e, "ERROR");
30
+ throw e;
31
+ });
32
+ });
33
+ this.kernel?.once("onTerminate", async () => {
34
+ await this.disconnectAll().catch(() => {});
35
+ });
36
+ }
37
+ /** Config validée (Zod) exposée par le Module (`this.module.config`). */
38
+ #config() {
39
+ return this.module.config;
40
+ }
41
+ /** Connecte tous les connecteurs déclarés en config (validée Zod). */
42
+ async connectAll() {
43
+ const config = this.#config();
44
+ if (config?.debug) mongoose.set("debug", true);
45
+ const connectors = config?.connectors ?? {};
46
+ for (const [name, cfg] of Object.entries(connectors)) await this.#connectOne(name, cfg);
47
+ }
48
+ /** Assemble l'URI de connexion à partir de la config (`uri` ou composants). */
49
+ static buildUri(cfg) {
50
+ if (cfg.uri) return cfg.uri;
51
+ return `mongodb://${cfg.host ?? "localhost"}:${cfg.port ?? 27017}/${cfg.dbname ?? "nodefony"}`;
52
+ }
53
+ /**
54
+ * Options de connexion Mongoose d'un connecteur.
55
+ *
56
+ * `options` reste un fourre-tout transmis tel quel — Mongoose valide ses
57
+ * propres `ConnectOptions`, les re-modéliser en Zod serait une duplication
58
+ * qui dériverait. `autoIndex` fait exception, et une seule : il décide si les
59
+ * contraintes d'unicité sont construites au démarrage, ce qui mérite un champ
60
+ * typé, décrit, et visible dans la configuration d'un connecteur.
61
+ *
62
+ * Il **prime** donc sur une clé homonyme écrite dans `options` : entre deux
63
+ * canaux, celui qui est déclaré gagne — la même règle que les délais de
64
+ * connexion, où un choix explicite l'emporte sur un défaut.
65
+ *
66
+ * @param cfg - configuration validée du connecteur.
67
+ * @returns les options à passer à la connexion, ou `undefined` s'il n'y en a aucune.
68
+ */
69
+ static buildConnectOptions(cfg) {
70
+ if (cfg.autoIndex === void 0) return cfg.options;
71
+ return {
72
+ ...cfg.options ?? {},
73
+ autoIndex: cfg.autoIndex
74
+ };
75
+ }
76
+ /** Connecte un connecteur (URI + options d'auth/pool). */
77
+ async #connectOne(name, cfg) {
78
+ const uri = MongooseService.buildUri(cfg);
79
+ const orm = new MongooseOrm(name, uri, MongooseService.buildConnectOptions(cfg));
80
+ await orm.connect();
81
+ this.#orms.set(name, orm);
82
+ this.log(`Mongoose ORM "${name}" connected (${orm.safeTarget()})`, "INFO");
83
+ }
84
+ /** Ferme toutes les connexions. */
85
+ async disconnectAll() {
86
+ for (const orm of this.#orms.values()) await orm.disconnect();
87
+ this.#orms.clear();
88
+ }
89
+ /** Retourne l'ORM Mongoose d'un connecteur (défaut : `"nodefony"`). */
90
+ getOrm(name = "nodefony") {
91
+ return this.#orms.get(name);
92
+ }
93
+ };
94
+ //#endregion
95
+ export { MongooseService as default };
@@ -0,0 +1,237 @@
1
+ import { TOKEN_ENTITY_NAMES } from "../entity/tokenEntity.js";
2
+ import { mongoOrder } from "./mongoOrder.js";
3
+ import { assertPageQuery } from "nodefony";
4
+ import { paginate } from "@nodefony/orm-core";
5
+ import { TOKEN_DEFAULT_ORDER, TOKEN_SORTABLE_FIELDS, tokenStatusCriteria } from "@nodefony/security";
6
+ //#region nodefony/src/MongooseTokenStore.ts
7
+ /**
8
+ * Traduit les filtres de listing en `Criteria` portable (`id`→`_id` géré par le
9
+ * repo).
10
+ *
11
+ * L'état de vie (`status`) vient de `tokenStatusCriteria`, partagé avec
12
+ * l'adapter SQL : une seule écriture de la règle « révoqué l'emporte sur
13
+ * expiré », donc aucune divergence possible entre les deux backends.
14
+ */
15
+ function tokenListCriteria(query, now) {
16
+ const criteria = { ...tokenStatusCriteria(query.status, now) };
17
+ if (query.subjectId !== void 0) criteria.subjectId = query.subjectId;
18
+ if (query.kind !== void 0) criteria.kind = query.kind;
19
+ return criteria;
20
+ }
21
+ /** Fenêtre par défaut de conservation d'un PAT révoqué sans expiration (30 j). */
22
+ const DEFAULT_RETENTION_REVOKED_MS = 2592e6;
23
+ /**
24
+ * Store de jetons **Mongoose** (NoSQL) — implémentation d'{@link ITokenStore} au
25
+ * dessus de trois repositories `@nodefony/orm-core` (`access_token`, `denied_jti`,
26
+ * `subject_revocation`). Pendant documentaire de `DrizzleTokenStore`.
27
+ *
28
+ * **Approche B** : `@nodefony/security` n'est connu qu'en `import type` (0 dép
29
+ * runtime). C'est l'application qui enregistre la fabrique (`registerTokenStore`)
30
+ * et les entités (`registerTokenEntities(orm)` avant `orm.connect()`).
31
+ *
32
+ * **Spécificité Mongo** : la clé naturelle (`jti` / `subjectId`) est portée par
33
+ * `_id` (cf {@link tokenEntity}). Le contrat traduit `{ id }` → `{ _id }`, donc
34
+ * les lookups passent par le champ `id` ; les **écritures** posent explicitement
35
+ * `_id` (Mongo ne génère pas notre jti). Les reads sont normalisés (`id` ← `_id`)
36
+ * pour ne pas dépendre du virtuel. Le reste est identique au store Drizzle : `gc`
37
+ * via `$lte` (le type bracketing Mongo exclut les `null`), idempotence de `revoke`
38
+ * par read-then-write.
39
+ */
40
+ var MongooseTokenStore = class MongooseTokenStore {
41
+ /**
42
+ * {@inheritDoc ITokenStore.sortableFields}
43
+ *
44
+ * Capacité pleine : le vocabulaire public entier est trié par Mongo, `id`
45
+ * compris — traduit en `_id` au moment de la requête.
46
+ */
47
+ sortableFields = TOKEN_SORTABLE_FIELDS;
48
+ #records;
49
+ #denied;
50
+ #revocations;
51
+ #now;
52
+ #retentionRevokedMs;
53
+ /**
54
+ * @param records - repository de `access_token` (PAT + refresh).
55
+ * @param denied - repository de la denylist `denied_jti`.
56
+ * @param revocations - repository des seuils `subject_revocation`.
57
+ * @param now - horloge (epoch ms) injectable pour les tests.
58
+ * @param retentionRevokedMs - rétention d'un PAT révoqué sans `exp` avant purge.
59
+ */
60
+ constructor(records, denied, revocations, now = Date.now, retentionRevokedMs = DEFAULT_RETENTION_REVOKED_MS) {
61
+ this.#records = records;
62
+ this.#denied = denied;
63
+ this.#revocations = revocations;
64
+ this.#now = now;
65
+ this.#retentionRevokedMs = retentionRevokedMs;
66
+ }
67
+ /**
68
+ * Construit le store depuis un {@link MongooseOrm} connecté. Les entités
69
+ * (`registerTokenEntities`) doivent avoir été enregistrées **avant** `connect()`.
70
+ *
71
+ * @param orm - ORM Mongoose connecté hébergeant les collections du store.
72
+ * @param now - horloge injectable (tests).
73
+ * @param retentionRevokedMs - rétention des PAT révoqués sans `exp`.
74
+ */
75
+ static from(orm, now, retentionRevokedMs) {
76
+ return new MongooseTokenStore(orm.getRepository(TOKEN_ENTITY_NAMES.records), orm.getRepository(TOKEN_ENTITY_NAMES.denied), orm.getRepository(TOKEN_ENTITY_NAMES.revocations), now, retentionRevokedMs);
77
+ }
78
+ /** Identité réelle d'un record (jti) : `_id` fait foi, le virtuel `id` en repli. */
79
+ #idOf(row) {
80
+ return row._id ?? row.id;
81
+ }
82
+ /** Normalise `id` (← `_id`) sur un record lu, sans dépendre du virtuel Mongoose. */
83
+ #withId(row) {
84
+ if (row) row.id = this.#idOf(row);
85
+ return row;
86
+ }
87
+ /**
88
+ * Insère ou remplace un record (PAT / refresh) — 1 round-trip, `upsert`
89
+ * atomique sur la PK plutôt qu'un `findOne` d'existence + `create`/`updateOne`
90
+ * (dont l'`await` laisse deux put concurrents du même id lire « absent » et
91
+ * insérer tous les deux → E11000 pour le perdant). `put` pose le record
92
+ * COMPLET → tout hors `id` est ré-appliqué au conflit.
93
+ *
94
+ * `id` en critère suffit à poser `_id` : Mongo ajoute les égalités du filtre
95
+ * au document inséré (cf `MongooseRepository.upsert`), plus besoin du `_id`
96
+ * explicite. Parité stricte avec l'adapter Drizzle.
97
+ *
98
+ * @param record - le record complet à persister.
99
+ */
100
+ async put(record) {
101
+ const { id, ...rest } = record;
102
+ await this.#records.upsert({ id }, rest);
103
+ }
104
+ async findById(id) {
105
+ return this.#withId(await this.#records.findOne({ id }));
106
+ }
107
+ async findByHash(secretHash) {
108
+ return this.#withId(await this.#records.findOne({ secretHash }));
109
+ }
110
+ async findBySubject(subjectId) {
111
+ const rows = await this.#records.find({ subjectId });
112
+ for (const row of rows) row.id = this.#idOf(row);
113
+ return rows;
114
+ }
115
+ /** Tous les jetons (PAT + refresh) — vue d'administration cross-porteur. */
116
+ async listAll() {
117
+ const rows = await this.#records.find({});
118
+ for (const row of rows) row.id = this.#idOf(row);
119
+ return rows;
120
+ }
121
+ /**
122
+ * {@inheritDoc ITokenStore.listPage}
123
+ *
124
+ * `paginate()` d'orm-core (skip/limit + countDocuments) sur un filtre portable ;
125
+ * les `id` sont re-normalisés (`_id` → `id`) comme dans {@link listAll}.
126
+ *
127
+ * Le tri demandé est **traduit** avant de descendre : au repos, le jeton n'a
128
+ * pas de champ `id` (le `jti` EST le `_id`), et Mongo ne se plaint pas d'un tri
129
+ * sur un champ absent — il rend un ordre arbitraire. Sans traduction, un
130
+ * `?order=id` serait donc inerte ici et correct partout ailleurs.
131
+ */
132
+ async listPage(query) {
133
+ assertPageQuery(query, "offset");
134
+ const page = await paginate(this.#records, {
135
+ criteria: tokenListCriteria(query, this.#now()),
136
+ limit: query.limit,
137
+ offset: query.offset,
138
+ withTotal: query.withTotal,
139
+ order: mongoOrder(query.order, this.sortableFields, TOKEN_DEFAULT_ORDER)
140
+ });
141
+ for (const row of page.items) row.id = this.#idOf(row);
142
+ return page;
143
+ }
144
+ /** {@inheritDoc ITokenStore.countTokens} */
145
+ countTokens(query) {
146
+ return this.#records.count(tokenListCriteria(query, this.#now()));
147
+ }
148
+ async markUsed(id, usage) {
149
+ await this.#records.updateOne({ id }, {
150
+ lastUsedAt: usage.at,
151
+ lastUsedIp: usage.ip ?? null,
152
+ lastUsedUserAgent: usage.userAgent ?? null
153
+ });
154
+ }
155
+ /**
156
+ * Révoque un jeton — **idempotent** : la 1ʳᵉ date/raison est conservée.
157
+ *
158
+ * Le « pas encore révoqué » est dans le filtre (`revokedAt: { $null: true }`),
159
+ * pas dans un `if` JS après lecture : une seule instruction, donc deux
160
+ * révocations concurrentes ne se recouvrent plus (la 2ᵉ ne matche rien au lieu
161
+ * d'écraser la date/raison de la 1ʳᵉ). Parité stricte avec l'adapter Drizzle.
162
+ *
163
+ * @param id - identifiant du jeton.
164
+ * @param reason - motif, posé seulement à la 1ʳᵉ révocation.
165
+ */
166
+ async revoke(id, reason) {
167
+ await this.#records.updateOne({
168
+ id,
169
+ revokedAt: { $null: true }
170
+ }, {
171
+ revokedAt: this.#now(),
172
+ revokedReason: reason
173
+ });
174
+ }
175
+ /**
176
+ * Coupe toute une famille de refresh (détection de rejeu, RFC 9700) — les
177
+ * membres déjà révoqués gardent leur raison d'origine.
178
+ *
179
+ * Un seul `updateMany` filtré : atomique, et N+1 requêtes (1 find + 1 update
180
+ * par membre actif) tombent à 1.
181
+ *
182
+ * @param family - famille de refresh à couper.
183
+ * @param reason - motif appliqué aux membres encore actifs.
184
+ */
185
+ async revokeFamily(family, reason) {
186
+ await this.#records.updateMany({
187
+ family,
188
+ revokedAt: { $null: true }
189
+ }, {
190
+ revokedAt: this.#now(),
191
+ revokedReason: reason
192
+ });
193
+ }
194
+ async denyJti(jti, expiresAt) {
195
+ await this.#denied.upsert({ id: jti }, { expiresAt });
196
+ }
197
+ async isJtiDenied(jti) {
198
+ return await this.#denied.findOne({
199
+ id: jti,
200
+ expiresAt: { $gt: this.#now() }
201
+ }) !== null;
202
+ }
203
+ /**
204
+ * Pose le seuil de révocation en masse d'un porteur (« déconnecte-moi de
205
+ * partout ») : tout jeton émis avant `invalidBefore` est mort.
206
+ *
207
+ * **Monotone — le seuil ne recule JAMAIS**, y compris sous deux logouts
208
+ * simultanés : la comparaison vit dans la valeur écrite (`$max`, natif Mongo),
209
+ * pas dans un `if` JS après lecture. Une lecture suivie d'une écriture
210
+ * laisserait les deux appels voir le même état et écrire tous les deux — c'est
211
+ * le dernier qui resterait, même porteur d'un seuil plus ANCIEN, et **des
212
+ * jetons révoqués redeviendraient valides**. Parité stricte avec l'adapter
213
+ * Drizzle (`GREATEST`/`MAX` SQL).
214
+ *
215
+ * @param subjectId - porteur visé.
216
+ * @param invalidBefore - seuil (epoch ms) ; ignoré s'il est antérieur au seuil courant.
217
+ */
218
+ async revokeAllForSubject(subjectId, invalidBefore) {
219
+ await this.#revocations.upsert({ id: subjectId }, { invalidBefore: { $max: invalidBefore } });
220
+ }
221
+ async getInvalidBefore(subjectId) {
222
+ const row = await this.#revocations.findOne({ id: subjectId });
223
+ return row ? row.invalidBefore : null;
224
+ }
225
+ async gc(now = this.#now()) {
226
+ let purged = 0;
227
+ purged += await this.#denied.delete({ expiresAt: { $lte: now } });
228
+ purged += await this.#records.delete({ expiresAt: { $lte: now } });
229
+ purged += await this.#records.delete({
230
+ revokedAt: { $lte: now - this.#retentionRevokedMs },
231
+ expiresAt: { $null: true }
232
+ });
233
+ return purged;
234
+ }
235
+ };
236
+ //#endregion
237
+ export { MongooseTokenStore };