@classytic/mongokit 3.36.0 → 3.39.0

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 (47) hide show
  1. package/dist/{PaginationEngine-CBS1XGzp.d.mts → PaginationEngine-D-P19Y9m.d.mts} +34 -9
  2. package/dist/PaginationEngine-DQ7beTCR.mjs +1182 -0
  3. package/dist/{Repository-BArvU32x.mjs → Repository-D7fDpyFx.mjs} +405 -37
  4. package/dist/{Repository-BdSkQ8vc.d.mts → Repository-IxI983_m.d.mts} +55 -8
  5. package/dist/actions/index.d.mts +1 -1
  6. package/dist/actions/index.mjs +1 -1
  7. package/dist/adapter/index.d.mts +21 -0
  8. package/dist/adapter/index.mjs +1 -1
  9. package/dist/{adapter-Bfqz8yHE.mjs → adapter-BrVfPFSW.mjs} +45 -1
  10. package/dist/ai/index.d.mts +1 -1
  11. package/dist/better-auth/index.d.mts +78 -1
  12. package/dist/better-auth/index.mjs +233 -3
  13. package/dist/{create-repository-BU4x9Kb0.d.mts → create-repository-CRXjDa3N.d.mts} +5 -5
  14. package/dist/{create-repository-CPNbktFG.mjs → create-repository-DOe9E_5F.mjs} +3 -3
  15. package/dist/{index-DKEql4cL.d.mts → index-CmjNyulu.d.mts} +67 -8
  16. package/dist/index.d.mts +77 -13
  17. package/dist/index.mjs +31 -8
  18. package/dist/indexes/verify-indexes.d.mts +69 -0
  19. package/dist/indexes/verify-indexes.mjs +76 -0
  20. package/dist/{method-registry.plugin-DblM7V6j.mjs → method-registry.plugin-C63pGn1d.mjs} +54 -4
  21. package/dist/{multi-tenant.plugin-BwSvTirB.d.mts → multi-tenant.plugin-B-tzNeYL.d.mts} +60 -4
  22. package/dist/{operations-DZodvjC-.d.mts → operations-CrRIoc8Z.d.mts} +22 -7
  23. package/dist/outbox/index.d.mts +1 -1
  24. package/dist/outbox/index.mjs +2 -2
  25. package/dist/pagination/PaginationEngine.d.mts +1 -1
  26. package/dist/pagination/PaginationEngine.mjs +1 -1
  27. package/dist/pagination/utils/limits.d.mts +47 -0
  28. package/dist/pagination/utils/limits.mjs +85 -0
  29. package/dist/pagination-BpIv2bdS.d.mts +364 -0
  30. package/dist/{plugin-options-DXnlKM3B.d.mts → plugin-options-BuvtOSZf.d.mts} +2 -2
  31. package/dist/plugins/index.d.mts +3 -3
  32. package/dist/plugins/index.mjs +3 -3
  33. package/dist/{repository-H_ffGgpE.d.mts → repository-ZbLtL7Nk.d.mts} +30 -4
  34. package/dist/tenant/index.d.mts +89 -0
  35. package/dist/tenant/index.mjs +94 -0
  36. package/dist/testkit/index.d.mts +2 -2
  37. package/dist/testkit/index.mjs +1 -1
  38. package/dist/{timestamp.plugin-BEc6TANm.mjs → timestamp.plugin-B5eiaItQ.mjs} +75 -8
  39. package/dist/{to-plain-Dyz_qvuo.d.mts → to-plain-B8ufk3Qz.d.mts} +2 -2
  40. package/dist/{update-jSXp3nu3.mjs → update-c_01A0if.mjs} +126 -73
  41. package/dist/utils/index.d.mts +1 -1
  42. package/dist/{validation-chain.plugin-CyK1s9Ze.mjs → validation-chain.plugin-Bju1D1Sn.mjs} +2 -2
  43. package/dist/{validation-chain.plugin-yQRob3Av.d.mts → validation-chain.plugin-DvrLJIbJ.d.mts} +3 -19
  44. package/package.json +15 -3
  45. package/dist/PaginationEngine-GdyX29tE.mjs +0 -797
  46. package/dist/core-62oo3N1C.d.mts +0 -50
  47. package/dist/pagination-jSUdrACK.d.mts +0 -158
@@ -1,8 +1,7 @@
1
- import { _ as UpdateOptions, a as CreateOptions, b as LookupOptions, c as FindOneAndUpdateOptions, d as LookupPopulateResult, g as UpdateManyResult, h as SessionOptions, m as ReadOptions, o as DeleteManyResult, p as OperationOptions, r as CacheableOptions, s as DeleteResult, t as AggregateOptions, u as LookupPopulateOptions, v as WithTransactionOptions, y as LookupBuilder } from "./operations-DZodvjC-.mjs";
2
- import { a as PopulateSpec, i as ObjectId, l as SortSpec$1, o as ReadPreferenceType, s as SelectSpec } from "./core-62oo3N1C.mjs";
3
- import { c as PaginationConfig, r as CollationOptions, t as AggregatePaginationOptions } from "./pagination-jSUdrACK.mjs";
4
- import { _ as RepositoryOptions, d as PluginType, f as PrioritizedHook, o as Middleware, p as RepositoryContext } from "./repository-H_ffGgpE.mjs";
5
- import { t as PaginationEngine } from "./PaginationEngine-CBS1XGzp.mjs";
1
+ import { _ as UpdateOptions, a as CreateOptions, b as LookupOptions, c as FindOneAndUpdateOptions, d as LookupPopulateResult, g as UpdateManyResult, h as SessionOptions, m as ReadOptions, o as DeleteManyResult, p as OperationOptions, r as CacheableOptions, s as DeleteResult, t as AggregateOptions, u as LookupPopulateOptions, v as WithTransactionOptions, y as LookupBuilder } from "./operations-CrRIoc8Z.mjs";
2
+ import { _ as PopulateSpec, g as ObjectId, i as CountStrategy, r as CollationOptions, t as AggregatePaginationOptions, u as PaginationConfig, v as ReadPreferenceType, x as SortSpec$1, y as SelectSpec } from "./pagination-BpIv2bdS.mjs";
3
+ import { _ as RepositoryOptions, d as PluginType, f as PrioritizedHook, o as Middleware, p as RepositoryContext } from "./repository-ZbLtL7Nk.mjs";
4
+ import { t as PaginationEngine } from "./PaginationEngine-D-P19Y9m.mjs";
6
5
  import { AggPaginationRequest, AggRequest, AggResult, ArchiveOptions, ArchiveResult, ArchiveSink, ChangeEvent, FilterInput, KeysetAggPaginationResult, QueryOptions, RepoCapabilities, RepositoryBase, TenantPurgeOptions, TenantPurgeResult, TenantPurgeStrategy, WatchOptions } from "@classytic/repo-core/repository";
7
6
  import { ClientSession, Expression, Model, PipelineStage, PopulateOptions } from "mongoose";
8
7
  import { Filter } from "@classytic/repo-core/filter";
@@ -580,6 +579,18 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
580
579
  * the `Middleware<TDoc>` type for the protocol.
581
580
  */
582
581
  private readonly _middlewares;
582
+ /**
583
+ * Shard-key guard built once from `options.distribution`; `undefined`
584
+ * when not configured so the per-op check is a single falsy test.
585
+ */
586
+ private readonly _checkDistribution;
587
+ /**
588
+ * Whether id-addressed verbs are guarded too — true unless the
589
+ * distribution key IS the id field, where an `_id` match is genuinely
590
+ * routed and guarding would flag every by-id call. See
591
+ * {@link ID_ADDRESSED_OPERATIONS}.
592
+ */
593
+ private readonly _guardIdAddressed;
583
594
  constructor(Model: Model<TDoc, any, any, any>, plugins?: PluginType[], paginationConfig?: PaginationConfig, options?: RepositoryOptions);
584
595
  /**
585
596
  * Live read-through view of the hook engine's listener registry.
@@ -860,8 +871,12 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
860
871
  * repo without a filter to observe deletes.
861
872
  * - `options.signal` ends the iterator (closes the stream). A
862
873
  * pre-aborted signal rejects at the op boundary like every other op.
863
- * - `options.resumeAfter` forwards a previously captured Mongo
864
- * resume token for at-least-once consumption across restarts.
874
+ * - Every yielded event carries `resumeToken` (the change document's
875
+ * `_id`). Persist the last one you finished processing and pass it
876
+ * back as `options.resumeAfter` — or `options.startAfter` to also
877
+ * step past an invalidate event — for at-least-once consumption
878
+ * across restarts. Setting both is rejected up front (Mongo forbids
879
+ * the combination; the driver error is far less legible).
865
880
  *
866
881
  * Context keys for policy plugins (`organizationId`, `bypassTenant`,
867
882
  * `includeDeleted`, ...) ride the options bag at runtime — same as every
@@ -906,6 +921,8 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
906
921
  sort?: SortSpec$1 | string;
907
922
  cursor?: string;
908
923
  after?: string;
924
+ /** Cursor for the PREVIOUS page. Mutually exclusive with `after`/`cursor`. */
925
+ before?: string;
909
926
  page?: number;
910
927
  pagination?: {
911
928
  page?: number;
@@ -916,7 +933,9 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
916
933
  mode?: 'offset' | 'keyset';
917
934
  hint?: string | Record<string, 1 | -1>;
918
935
  maxTimeMS?: number;
919
- countStrategy?: 'exact' | 'estimated' | 'none';
936
+ countStrategy?: CountStrategy;
937
+ /** Ceiling for `countStrategy: 'capped'`. */
938
+ countLimit?: number;
920
939
  readPreference?: ReadPreferenceType;
921
940
  /** Advanced populate options (from QueryParser or Arc's BaseController) */
922
941
  populateOptions?: PopulateOptions[];
@@ -1782,6 +1801,16 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1782
1801
  * 4. DEFAULT (500) — user-registered hooks
1783
1802
  */
1784
1803
  _buildContext(operation: string, options: Record<string, unknown> | object): Promise<RepositoryContext>;
1804
+ /**
1805
+ * Feed the resolved filter of a guarded verb to the distribution guard.
1806
+ * The slot follows `OP_REGISTRY[op].policyKey` (`query` vs `filters`);
1807
+ * the portable aggregate verbs ALSO carry the caller's filter on
1808
+ * `aggRequest.filter` (merged into the pipeline later by
1809
+ * `_injectPolicyScopeIntoAgg`), and the kit-native pipeline verbs may
1810
+ * carry it as leading `$match` stages — both are folded in so a key the
1811
+ * caller did supply is never reported missing.
1812
+ */
1813
+ private _guardDistribution;
1785
1814
  /**
1786
1815
  * Walk the well-known filter-carrying keys (`query`, `filters`,
1787
1816
  * `having`) and route any Filter IR through `compileFilterToMongo`.
@@ -1796,6 +1825,9 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1796
1825
  private _resolveSort;
1797
1826
  /**
1798
1827
  * Detect whether to use keyset (cursor) or offset (page) pagination.
1828
+ * Precedence: explicit `mode` → `page` (offset) → `after` (keyset) →
1829
+ * `PaginationConfig.defaultMode` → the sort heuristic (a non-default
1830
+ * sort with no page means the caller wants a cursor).
1799
1831
  */
1800
1832
  private _detectPaginationMode;
1801
1833
  /**
@@ -1866,7 +1898,22 @@ declare class Repository<TDoc = unknown> extends RepositoryBase {
1866
1898
  * `errorLabels` exactly as we received it. The caller still sees a
1867
1899
  * thrown error — only the wrapping layer is skipped.
1868
1900
  */
1901
+ /**
1902
+ * Translate a driver error into the repository's error contract, WITHOUT
1903
+ * losing structured diagnostics an action attached to it.
1904
+ *
1905
+ * The translation replaces the object — an E11000 becomes a 409 — so any
1906
+ * property a caller was meant to read went with it. `createMany` attaches a
1907
+ * `partial` report naming which documents landed, and it arrived nowhere:
1908
+ * the report existed, the test for it failed, and the only evidence was the
1909
+ * absence.
1910
+ *
1911
+ * Carried explicitly rather than by copying every own-property, because the
1912
+ * wrap is deliberate: the point is to hide driver internals, and a blanket
1913
+ * copy would put them back.
1914
+ */
1869
1915
  _handleError(error: Error): HttpError;
1916
+ private _translateError;
1870
1917
  }
1871
1918
  //#endregion
1872
1919
  export { AggregationPlan as a, SortOrder as c, AggregationBuilder as i, SortSpec as l, Repository as n, GroupSpec as o, TransitionMachine as r, ProjectionSpec as s, HOOK_PRIORITY as t, VectorSearchOptions as u };
@@ -1,2 +1,2 @@
1
- import { a as create_d_exports, i as delete_d_exports, n as update_d_exports, o as aggregate_d_exports, r as read_d_exports } from "../index-DKEql4cL.mjs";
1
+ import { c as create_d_exports, i as delete_d_exports, n as update_d_exports, r as read_d_exports, u as aggregate_d_exports } from "../index-CmjNyulu.mjs";
2
2
  export { aggregate_d_exports as aggregate, create_d_exports as create, delete_d_exports as deleteActions, read_d_exports as read, update_d_exports as update };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "../rolldown-runtime-D7D4PA-g.mjs";
2
- import { g as create_exports, i as update_exports, p as delete_exports, u as read_exports, v as aggregate_exports } from "../update-jSXp3nu3.mjs";
2
+ import { b as create_exports, i as update_exports, m as aggregate_exports, p as delete_exports, u as read_exports } from "../update-c_01A0if.mjs";
3
3
  //#region src/actions/index.ts
4
4
  var actions_exports = /* @__PURE__ */ __exportAll({
5
5
  aggregate: () => aggregate_exports,
@@ -129,6 +129,27 @@ declare class MongooseAdapter<TDoc = unknown, TModelDoc = unknown> implements Da
129
129
  * `matchesRecordFilter` — one shared contract/IR across every kit; its
130
130
  * evaluator is id-coercion aware so a Mongo `ObjectId` `_id` matches its
131
131
  * string form. Arrow field: keeps its binding when passed by reference.
132
+ *
133
+ * The document is flattened to a plain record FIRST, and that step is the
134
+ * whole reason this is not a one-liner. The evaluator resolves field paths
135
+ * with `Object.hasOwn`, own-properties-only and deliberately so: it must not
136
+ * walk a prototype chain, or a crafted `{ 'constructor.x': … }` policy filter
137
+ * could match an inherited member. A HYDRATED Mongoose document keeps its
138
+ * values in `_doc` and exposes them through prototype getters, so it has no
139
+ * own `status`, no own `organizationId` — every path resolves to absent and
140
+ * the filter matches NOTHING. Fail-closed, so it does not leak; it denies.
141
+ *
142
+ * That turned any policy at all into a blanket 404 on every non-compound
143
+ * fetch (`getBySlug`, cache revalidation, the realtime feed) while the same
144
+ * policy worked perfectly on list/get, which filter at the DB. The asymmetry
145
+ * is what made it so hard to see: a resource reads fine until the day someone
146
+ * gives it a row-level policy, and then one route starts lying.
147
+ *
148
+ * Knowing that a Mongoose document is not a plain record is the MONGO kit's
149
+ * job, not the dialect-agnostic evaluator's, so the normalisation lives here.
150
+ * `toObject` rather than `toJSON`: it preserves `ObjectId` and `Date` values,
151
+ * which the evaluator compares by type. A `.lean()` result is already plain
152
+ * and passes through untouched.
132
153
  */
133
154
  readonly matchesFilter: (item: unknown, filters: Record<string, unknown>) => boolean;
134
155
  /**
@@ -1,2 +1,2 @@
1
- import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-Bfqz8yHE.mjs";
1
+ import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-BrVfPFSW.mjs";
2
2
  export { MongooseAdapter, createMongooseAdapter, isMongooseModel };
@@ -19,6 +19,29 @@ const REQUIRED_REPO_METHODS = [
19
19
  "update",
20
20
  "delete"
21
21
  ];
22
+ /**
23
+ * A Mongoose document as a plain record, for anything that reads fields directly.
24
+ *
25
+ * Hydrated documents hold their values in `_doc` and surface them through prototype getters, so
26
+ * `Object.hasOwn(doc, 'status')` is FALSE even though `doc.status` reads fine. Any consumer that
27
+ * refuses to walk the prototype chain — as a prototype-pollution-safe field resolver must — sees
28
+ * an empty document. See `matchesFilter` for what that cost us.
29
+ *
30
+ * `toObject` over `toJSON`: it keeps `ObjectId` and `Date` as values rather than stringifying
31
+ * them, and callers here compare by type. Anything already plain (a `.lean()` result, a POJO) is
32
+ * returned untouched, and a `toObject` that throws degrades to the original rather than taking
33
+ * the caller down.
34
+ */
35
+ function toPlainRecord(item) {
36
+ if (item === null || typeof item !== "object") return item;
37
+ const maybe = item;
38
+ if (typeof maybe.toObject !== "function") return item;
39
+ try {
40
+ return maybe.toObject();
41
+ } catch {
42
+ return item;
43
+ }
44
+ }
22
45
  function describeValue(value) {
23
46
  if (value === null) return "null";
24
47
  if (value === void 0) return "undefined";
@@ -215,8 +238,29 @@ var MongooseAdapter = class {
215
238
  * `matchesRecordFilter` — one shared contract/IR across every kit; its
216
239
  * evaluator is id-coercion aware so a Mongo `ObjectId` `_id` matches its
217
240
  * string form. Arrow field: keeps its binding when passed by reference.
241
+ *
242
+ * The document is flattened to a plain record FIRST, and that step is the
243
+ * whole reason this is not a one-liner. The evaluator resolves field paths
244
+ * with `Object.hasOwn`, own-properties-only and deliberately so: it must not
245
+ * walk a prototype chain, or a crafted `{ 'constructor.x': … }` policy filter
246
+ * could match an inherited member. A HYDRATED Mongoose document keeps its
247
+ * values in `_doc` and exposes them through prototype getters, so it has no
248
+ * own `status`, no own `organizationId` — every path resolves to absent and
249
+ * the filter matches NOTHING. Fail-closed, so it does not leak; it denies.
250
+ *
251
+ * That turned any policy at all into a blanket 404 on every non-compound
252
+ * fetch (`getBySlug`, cache revalidation, the realtime feed) while the same
253
+ * policy worked perfectly on list/get, which filter at the DB. The asymmetry
254
+ * is what made it so hard to see: a resource reads fine until the day someone
255
+ * gives it a row-level policy, and then one route starts lying.
256
+ *
257
+ * Knowing that a Mongoose document is not a plain record is the MONGO kit's
258
+ * job, not the dialect-agnostic evaluator's, so the normalisation lives here.
259
+ * `toObject` rather than `toJSON`: it preserves `ObjectId` and `Date` values,
260
+ * which the evaluator compares by type. A `.lean()` result is already plain
261
+ * and passes through untouched.
218
262
  */
219
- matchesFilter = (item, filters) => matchesRecordFilter(item, filters);
263
+ matchesFilter = (item, filters) => matchesRecordFilter(toPlainRecord(item), filters);
220
264
  /**
221
265
  * No-op — mongokit's per-call resources (`watch()` change streams,
222
266
  * `cursor()` iterators) are released by their own `AbortSignal` /
@@ -1,4 +1,4 @@
1
- import { l as Plugin } from "../repository-H_ffGgpE.mjs";
1
+ import { l as Plugin } from "../repository-ZbLtL7Nk.mjs";
2
2
  import { PipelineStage } from "mongoose";
3
3
  //#region src/ai/types.d.ts
4
4
  /** Supported similarity metrics for vector search */
@@ -259,6 +259,83 @@ declare function ensureBetterAuthSessionTtl(connection: {
259
259
  collection?: string;
260
260
  indexName?: string;
261
261
  }): Promise<boolean>;
262
+ /**
263
+ * One declared index on a Better Auth collection.
264
+ *
265
+ * `unique` is a REQUEST, not a guarantee: {@link ensureBetterAuthIndexes}
266
+ * downgrades to a non-unique index when existing rows already violate it,
267
+ * because failing a boot over historical duplicates is worse than running with
268
+ * a plain lookup index and reporting the degradation.
269
+ */
270
+ interface BetterAuthIndexSpec {
271
+ /** Canonical collection name (`'user'`, `'session'`, ...). */
272
+ collection: string;
273
+ keys: Record<string, 1 | -1>;
274
+ name: string;
275
+ unique?: boolean;
276
+ /** Present on self-expiring rows; always paired with `expireAfterSeconds: 0`. */
277
+ ttl?: boolean;
278
+ }
279
+ /**
280
+ * The indexes Better Auth's own queries need, which its mongo adapter does NOT
281
+ * create. Grouped by the plugin that owns the collection so a host only pays
282
+ * for what it enabled.
283
+ *
284
+ * Every entry below backs a query BA runs on a hot path:
285
+ * - `session.token` — read on EVERY authenticated request
286
+ * - `user.email` — read on every sign-in / sign-up, and must be unique
287
+ * - `account.(providerId|issuer, accountId)` — the credential lookup at sign-in
288
+ * - `member.(organizationId, userId)` — the membership check on org-scoped requests
289
+ * Without them each of those is a collection scan, and `email` / `slug` /
290
+ * `token` have no uniqueness guarantee at all.
291
+ *
292
+ * TTL entries (`expiresAt`) additionally keep the collection BOUNDED — see
293
+ * {@link ensureBetterAuthSessionTtl} for the measurements that motivated it.
294
+ */
295
+ declare const BA_INDEXES_BY_PLUGIN: Record<string, readonly BetterAuthIndexSpec[]>;
296
+ /** Outcome of one declared index. */
297
+ interface BetterAuthIndexResult {
298
+ collection: string;
299
+ name: string;
300
+ status: 'created' | 'degraded' | 'failed';
301
+ /** Set on `degraded` (unique dropped) and `failed`. */
302
+ reason?: string;
303
+ }
304
+ interface EnsureBetterAuthIndexesOptions {
305
+ /** Plugin sets to include beyond `core` (always implied). */
306
+ plugins?: BetterAuthPluginKey$1[];
307
+ /** Mirror BA's `usePlural` — appends `s` to every collection name. */
308
+ usePlural?: boolean;
309
+ /** Per-collection name override (mirrors BA's `user.modelName`). */
310
+ modelOverrides?: Partial<Record<string, string>>;
311
+ /** Canonical collection names to skip entirely. */
312
+ exclude?: string[];
313
+ }
314
+ interface IndexCreator {
315
+ collection(name: string): {
316
+ createIndex(keys: Record<string, 1 | -1>, opts: Record<string, unknown>): Promise<unknown>;
317
+ };
318
+ }
319
+ /**
320
+ * Create every index Better Auth's queries need. Idempotent, never throws.
321
+ *
322
+ * BA's mongo adapter creates NO indexes — not even on `session.token`, which is
323
+ * read on every authenticated request, nor a unique one on `user.email`. On a
324
+ * small database nobody notices; the cost arrives as a full collection scan per
325
+ * login once the tables grow, and until then nothing stops a duplicate email.
326
+ *
327
+ * This lives in the kit rather than a host for the same reason
328
+ * {@link ensureBetterAuthSessionTtl} does: the kit is what knows BA's collection
329
+ * names, so fixing it here protects every consumer instead of one host.
330
+ *
331
+ * **Uniqueness degrades rather than fails.** A `unique: true` spec is retried
332
+ * without uniqueness when the collection already holds violating rows, and
333
+ * reported as `degraded` — a boot must not die because historical data has a
334
+ * duplicate. Inspect the returned report to find those.
335
+ *
336
+ * Call once at boot, after the connection is live. Safe to call again.
337
+ */
338
+ declare function ensureBetterAuthIndexes(connection: IndexCreator, options?: EnsureBetterAuthIndexesOptions): Promise<BetterAuthIndexResult[]>;
262
339
  interface SessionUpdaterLike {
263
340
  updateMany(filter: Record<string, unknown>, update: Record<string, unknown>): Promise<{
264
341
  modifiedCount?: number;
@@ -293,4 +370,4 @@ declare function clearActiveOrganizationFromSessions(sessions: SessionUpdaterLik
293
370
  userId?: unknown;
294
371
  }): Promise<number>;
295
372
  //#endregion
296
- export { BetterAuthIdType, BetterAuthInstance, BetterAuthOverlayOptions, type BetterAuthPluginKey, MongooseLike, RegisterBetterAuthStubsOptions, SessionUpdaterLike, clearActiveOrganizationFromSessions, createBetterAuthOverlay, ensureBetterAuthSessionTtl, registerBetterAuthStubs };
373
+ export { BA_INDEXES_BY_PLUGIN, BetterAuthIdType, BetterAuthIndexResult, BetterAuthIndexSpec, BetterAuthInstance, BetterAuthOverlayOptions, type BetterAuthPluginKey, EnsureBetterAuthIndexesOptions, IndexCreator, MongooseLike, RegisterBetterAuthStubsOptions, SessionUpdaterLike, clearActiveOrganizationFromSessions, createBetterAuthOverlay, ensureBetterAuthIndexes, ensureBetterAuthSessionTtl, registerBetterAuthStubs };
@@ -1,5 +1,5 @@
1
- import { n as Repository } from "../Repository-BArvU32x.mjs";
2
- import { n as createMongooseAdapter } from "../adapter-Bfqz8yHE.mjs";
1
+ import { n as Repository } from "../Repository-D7fDpyFx.mjs";
2
+ import { n as createMongooseAdapter } from "../adapter-BrVfPFSW.mjs";
3
3
  import { asReadOnlyRepo } from "@classytic/repo-core/repository";
4
4
  import { pluralizeBetterAuthCollection, resolveBetterAuthCollections } from "@classytic/repo-core/better-auth";
5
5
  //#region src/better-auth/index.ts
@@ -203,6 +203,236 @@ async function ensureBetterAuthSessionTtl(connection, options = {}) {
203
203
  }
204
204
  }
205
205
  /**
206
+ * The indexes Better Auth's own queries need, which its mongo adapter does NOT
207
+ * create. Grouped by the plugin that owns the collection so a host only pays
208
+ * for what it enabled.
209
+ *
210
+ * Every entry below backs a query BA runs on a hot path:
211
+ * - `session.token` — read on EVERY authenticated request
212
+ * - `user.email` — read on every sign-in / sign-up, and must be unique
213
+ * - `account.(providerId|issuer, accountId)` — the credential lookup at sign-in
214
+ * - `member.(organizationId, userId)` — the membership check on org-scoped requests
215
+ * Without them each of those is a collection scan, and `email` / `slug` /
216
+ * `token` have no uniqueness guarantee at all.
217
+ *
218
+ * TTL entries (`expiresAt`) additionally keep the collection BOUNDED — see
219
+ * {@link ensureBetterAuthSessionTtl} for the measurements that motivated it.
220
+ */
221
+ const BA_INDEXES_BY_PLUGIN = {
222
+ core: [
223
+ {
224
+ collection: "user",
225
+ keys: { email: 1 },
226
+ name: "ba_user_email",
227
+ unique: true
228
+ },
229
+ {
230
+ collection: "session",
231
+ keys: { token: 1 },
232
+ name: "ba_session_token",
233
+ unique: true
234
+ },
235
+ {
236
+ collection: "session",
237
+ keys: { userId: 1 },
238
+ name: "ba_session_userId"
239
+ },
240
+ {
241
+ collection: "session",
242
+ keys: { expiresAt: 1 },
243
+ name: "ttl_session_expiresAt",
244
+ ttl: true
245
+ },
246
+ {
247
+ collection: "account",
248
+ keys: { userId: 1 },
249
+ name: "ba_account_userId"
250
+ },
251
+ {
252
+ collection: "account",
253
+ keys: {
254
+ providerId: 1,
255
+ accountId: 1
256
+ },
257
+ name: "ba_account_provider_accountId"
258
+ },
259
+ {
260
+ collection: "account",
261
+ keys: {
262
+ issuer: 1,
263
+ accountId: 1
264
+ },
265
+ name: "ba_account_issuer_accountId"
266
+ },
267
+ {
268
+ collection: "verification",
269
+ keys: { identifier: 1 },
270
+ name: "ba_verification_identifier"
271
+ },
272
+ {
273
+ collection: "verification",
274
+ keys: { expiresAt: 1 },
275
+ name: "ttl_verification_expiresAt",
276
+ ttl: true
277
+ }
278
+ ],
279
+ organization: [
280
+ {
281
+ collection: "organization",
282
+ keys: { slug: 1 },
283
+ name: "ba_organization_slug",
284
+ unique: true
285
+ },
286
+ {
287
+ collection: "member",
288
+ keys: {
289
+ organizationId: 1,
290
+ userId: 1
291
+ },
292
+ name: "ba_member_org_user",
293
+ unique: true
294
+ },
295
+ {
296
+ collection: "member",
297
+ keys: { userId: 1 },
298
+ name: "ba_member_userId"
299
+ },
300
+ {
301
+ collection: "invitation",
302
+ keys: { organizationId: 1 },
303
+ name: "ba_invitation_org"
304
+ },
305
+ {
306
+ collection: "invitation",
307
+ keys: { email: 1 },
308
+ name: "ba_invitation_email"
309
+ },
310
+ {
311
+ collection: "invitation",
312
+ keys: { expiresAt: 1 },
313
+ name: "ttl_invitation_expiresAt",
314
+ ttl: true
315
+ }
316
+ ],
317
+ "organization-teams": [{
318
+ collection: "team",
319
+ keys: { organizationId: 1 },
320
+ name: "ba_team_org"
321
+ }, {
322
+ collection: "teamMember",
323
+ keys: {
324
+ teamId: 1,
325
+ userId: 1
326
+ },
327
+ name: "ba_teamMember_team_user"
328
+ }],
329
+ twoFactor: [{
330
+ collection: "twoFactor",
331
+ keys: { userId: 1 },
332
+ name: "ba_twoFactor_userId"
333
+ }],
334
+ deviceAuthorization: [
335
+ {
336
+ collection: "deviceCode",
337
+ keys: { deviceCode: 1 },
338
+ name: "ba_deviceCode_code"
339
+ },
340
+ {
341
+ collection: "deviceCode",
342
+ keys: { userCode: 1 },
343
+ name: "ba_deviceCode_userCode"
344
+ },
345
+ {
346
+ collection: "deviceCode",
347
+ keys: { expiresAt: 1 },
348
+ name: "ttl_deviceCode_expiresAt",
349
+ ttl: true
350
+ }
351
+ ]
352
+ };
353
+ /**
354
+ * Create every index Better Auth's queries need. Idempotent, never throws.
355
+ *
356
+ * BA's mongo adapter creates NO indexes — not even on `session.token`, which is
357
+ * read on every authenticated request, nor a unique one on `user.email`. On a
358
+ * small database nobody notices; the cost arrives as a full collection scan per
359
+ * login once the tables grow, and until then nothing stops a duplicate email.
360
+ *
361
+ * This lives in the kit rather than a host for the same reason
362
+ * {@link ensureBetterAuthSessionTtl} does: the kit is what knows BA's collection
363
+ * names, so fixing it here protects every consumer instead of one host.
364
+ *
365
+ * **Uniqueness degrades rather than fails.** A `unique: true` spec is retried
366
+ * without uniqueness when the collection already holds violating rows, and
367
+ * reported as `degraded` — a boot must not die because historical data has a
368
+ * duplicate. Inspect the returned report to find those.
369
+ *
370
+ * Call once at boot, after the connection is live. Safe to call again.
371
+ */
372
+ async function ensureBetterAuthIndexes(connection, options = {}) {
373
+ const plugins = options.plugins ?? [];
374
+ const excluded = new Set(options.exclude ?? []);
375
+ const keys = ["core", ...plugins];
376
+ const resolveName = (canonical) => {
377
+ const overridden = options.modelOverrides?.[canonical] ?? canonical;
378
+ return options.usePlural ? pluralizeBetterAuthCollection(overridden) : overridden;
379
+ };
380
+ const specs = [];
381
+ const seen = /* @__PURE__ */ new Set();
382
+ for (const key of keys) for (const spec of BA_INDEXES_BY_PLUGIN[key] ?? []) {
383
+ if (excluded.has(spec.collection)) continue;
384
+ const id = `${spec.collection}.${spec.name}`;
385
+ if (seen.has(id)) continue;
386
+ seen.add(id);
387
+ specs.push(spec);
388
+ }
389
+ const results = [];
390
+ for (const spec of specs) {
391
+ const target = resolveName(spec.collection);
392
+ const base = { name: spec.name };
393
+ if (spec.ttl) base.expireAfterSeconds = 0;
394
+ try {
395
+ await connection.collection(target).createIndex(spec.keys, {
396
+ ...base,
397
+ ...spec.unique ? { unique: true } : {}
398
+ });
399
+ results.push({
400
+ collection: target,
401
+ name: spec.name,
402
+ status: "created"
403
+ });
404
+ } catch (err) {
405
+ const reason = err instanceof Error ? err.message : String(err);
406
+ if (!spec.unique) {
407
+ results.push({
408
+ collection: target,
409
+ name: spec.name,
410
+ status: "failed",
411
+ reason
412
+ });
413
+ continue;
414
+ }
415
+ try {
416
+ await connection.collection(target).createIndex(spec.keys, base);
417
+ results.push({
418
+ collection: target,
419
+ name: spec.name,
420
+ status: "degraded",
421
+ reason
422
+ });
423
+ } catch (err2) {
424
+ results.push({
425
+ collection: target,
426
+ name: spec.name,
427
+ status: "failed",
428
+ reason: err2 instanceof Error ? err2.message : String(err2)
429
+ });
430
+ }
431
+ }
432
+ }
433
+ return results;
434
+ }
435
+ /**
206
436
  * Clear a dangling `activeOrganizationId` from Better Auth `session` rows.
207
437
  *
208
438
  * WHY THIS EXISTS: Better Auth purges its `member` / `invitation` rows when an
@@ -233,4 +463,4 @@ async function clearActiveOrganizationFromSessions(sessions, organizationId, opt
233
463
  return (await sessions.updateMany(filter, { $set: { activeOrganizationId: null } })).modifiedCount ?? 0;
234
464
  }
235
465
  //#endregion
236
- export { clearActiveOrganizationFromSessions, createBetterAuthOverlay, ensureBetterAuthSessionTtl, registerBetterAuthStubs };
466
+ export { BA_INDEXES_BY_PLUGIN, clearActiveOrganizationFromSessions, createBetterAuthOverlay, ensureBetterAuthIndexes, ensureBetterAuthSessionTtl, registerBetterAuthStubs };
@@ -1,8 +1,8 @@
1
- import { c as PaginationConfig } from "./pagination-jSUdrACK.mjs";
2
- import { _ as RepositoryOptions, d as PluginType } from "./repository-H_ffGgpE.mjs";
3
- import { T as AuditTrailOptions, _ as RepositoryCachePluginOptions, i as CustomIdOptions, t as MultiTenantOptions } from "./multi-tenant.plugin-BwSvTirB.mjs";
4
- import { n as Repository } from "./Repository-BdSkQ8vc.mjs";
5
- import { i as Logger, o as SoftDeleteOptions } from "./plugin-options-DXnlKM3B.mjs";
1
+ import { u as PaginationConfig } from "./pagination-BpIv2bdS.mjs";
2
+ import { _ as RepositoryOptions, d as PluginType } from "./repository-ZbLtL7Nk.mjs";
3
+ import { T as AuditTrailOptions, _ as RepositoryCachePluginOptions, i as CustomIdOptions, t as MultiTenantOptions } from "./multi-tenant.plugin-B-tzNeYL.mjs";
4
+ import { n as Repository } from "./Repository-IxI983_m.mjs";
5
+ import { i as Logger, o as SoftDeleteOptions } from "./plugin-options-BuvtOSZf.mjs";
6
6
  import { Model } from "mongoose";
7
7
  //#region src/create-repository.d.ts
8
8
  /**
@@ -1,6 +1,6 @@
1
- import { n as Repository } from "./Repository-BArvU32x.mjs";
2
- import { a as customIdPlugin, f as auditTrailPlugin, i as multiTenantPlugin, m as auditLogPlugin, n as softDeletePlugin, t as timestampPlugin, u as cachePlugin } from "./timestamp.plugin-BEc6TANm.mjs";
3
- import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "./method-registry.plugin-DblM7V6j.mjs";
1
+ import { n as Repository } from "./Repository-D7fDpyFx.mjs";
2
+ import { a as customIdPlugin, f as auditTrailPlugin, i as multiTenantPlugin, m as auditLogPlugin, n as softDeletePlugin, t as timestampPlugin, u as cachePlugin } from "./timestamp.plugin-B5eiaItQ.mjs";
3
+ import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "./method-registry.plugin-C63pGn1d.mjs";
4
4
  //#region src/create-repository.ts
5
5
  function isLoggerAudit(value) {
6
6
  const v = value;