@nxgt/janus-mongo 0.4.3 → 0.5.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.
package/README.md CHANGED
@@ -44,6 +44,10 @@ error class and throws the peer's own, so `instanceof StoreFailure` holds in
44
44
  your code. Like it, this package expects `"moduleResolution": "bundler"`: the
45
45
  declarations import without extensions, so `nodenext` is not supported.
46
46
 
47
+ The `@nxgt/mongo` floor is tested, not claimed: the package's specs and its
48
+ typecheck run on `@nxgt/mongo` 0.17.0 as well, in the Floors job, on every CI
49
+ run.
50
+
47
51
  ## API
48
52
 
49
53
  | Export | What it is |
@@ -74,7 +78,9 @@ encoded. A document read in a shell reads like the record in the code.
74
78
  No secret is stored: sessions and tokens hold `sha256` of the secret,
75
79
  passwords a self-describing hash, and a user's `secondFactor.secret` a TOTP
76
80
  secret `@nxgt/janus` has already sealed with your application's key —
77
- `v1.<key id>.…` — which the store keeps byte for byte.
81
+ `v1.<key id>.…` — which the store keeps byte for byte. Its
82
+ `secondFactor.recoveryCodes` are keyed hashes of the recovery codes, never a
83
+ code, kept in order; a factor without the field reads as having none.
78
84
 
79
85
  `countAttempt` counts an attempt at a code in one `findOneAndUpdate` with
80
86
  `$inc`, so twenty concurrent attempts answer twenty distinct counts.
@@ -106,6 +112,14 @@ spend the same one.
106
112
  run, every sign-up and every one-time token fails with `STORE_FAILED`,
107
113
  caused by `Document failed validation`.
108
114
  [Upgrading](docs/guide/sync.md#upgrading-sync-before-you-deploy) has the steps.
115
+ - **Upgrading to 0.5: run the sync before deploying, again.** No document is
116
+ rewritten — a second factor without `recoveryCodes` reads as having none —
117
+ but the validator the previous sync wrote refuses the new field. Until
118
+ `syncMongoAdapter(db)` has run, every write of a second factor — `enroll`,
119
+ `activate`, a confirmed code — fails with `STORE_FAILED`, caused by
120
+ `Document failed validation`. And finish the rollout before users hold
121
+ recovery codes: an instance still on 0.4 writes a factor back without
122
+ them when it accepts a code.
109
123
  - **The TTL indexes are storage hygiene, not the expiry.** MongoDB's TTL monitor
110
124
  runs every sixty seconds, so a lapsed session can stay readable for up to a
111
125
  minute. The core compares `expiresAt` on every read, and that is what expires
@@ -15,6 +15,7 @@ export declare const users: import("@nxgt/mongo").CollectionDefinition<import("@
15
15
  secret: z.ZodString;
16
16
  confirmedAt: z.ZodNullable<z.ZodDate>;
17
17
  lastStep: z.ZodNullable<z.ZodInt>;
18
+ recoveryCodes: z.ZodOptional<z.ZodArray<z.ZodString>>;
18
19
  }, z.core.$strip>>>;
19
20
  emailVerifiedAt: z.ZodNullable<z.ZodDate>;
20
21
  version: z.ZodInt;
@@ -63,6 +64,7 @@ export declare const janusCollections: readonly [import("@nxgt/mongo").Collectio
63
64
  secret: z.ZodString;
64
65
  confirmedAt: z.ZodNullable<z.ZodDate>;
65
66
  lastStep: z.ZodNullable<z.ZodInt>;
67
+ recoveryCodes: z.ZodOptional<z.ZodArray<z.ZodString>>;
66
68
  }, z.core.$strip>>>;
67
69
  emailVerifiedAt: z.ZodNullable<z.ZodDate>;
68
70
  version: z.ZodInt;
@@ -1 +1 @@
1
- {"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../src/collections.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAuBxB,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;+FAgDhB,CAAC;AAEH,eAAO,MAAM,QAAQ;;;;;;;;+FA6BnB,CAAC;AAEH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;+FA2BjB,CAAC;AAEH,wEAAwE;AACxE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gGAAqC,CAAC"}
1
+ {"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../src/collections.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAuBxB,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;+FAqDhB,CAAC;AAEH,eAAO,MAAM,QAAQ;;;;;;;;+FA6BnB,CAAC;AAEH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;+FA2BjB,CAAC;AAEH,wEAAwE;AACxE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gGAAqC,CAAC"}
package/dist/index.js CHANGED
@@ -22,7 +22,8 @@ var users = defineCollection({
22
22
  method: z.literal("totp"),
23
23
  secret: z.string(),
24
24
  confirmedAt: at.nullable(),
25
- lastStep: z.int().nonnegative().nullable()
25
+ lastStep: z.int().nonnegative().nullable(),
26
+ recoveryCodes: z.array(z.string()).optional()
26
27
  }).nullable().optional(),
27
28
  emailVerifiedAt: at.nullable(),
28
29
  version: z.int().nonnegative(),
@@ -89,16 +90,11 @@ var tokens = defineCollection({
89
90
  });
90
91
  var janusCollections = [users, sessions, tokens];
91
92
 
92
- // src/relations.ts
93
+ // src/relations/index.ts
93
94
  import {
94
- isSubjectSet
95
- } from "@nxgt/janus";
96
- import {
97
- defineCollection as defineCollection2,
98
95
  getCollection,
99
96
  syncCollections
100
97
  } from "@nxgt/mongo";
101
- import { z as z2 } from "zod";
102
98
 
103
99
  // src/translate.ts
104
100
  import { JanusError, StoreConflict, StoreFailure } from "@nxgt/janus";
@@ -135,11 +131,17 @@ function takenLogin(duplicate) {
135
131
  function loginTaken(operation, { type, login }, cause) {
136
132
  return new StoreConflict("login", `${operation}: the login is taken by another ${type}`, { login, userType: type, operation, cause });
137
133
  }
134
+ function refuseDuplicate(operation, duplicate) {
135
+ const taken = takenLogin(duplicate);
136
+ return taken === null ? unexpectedDuplicate("users", operation, duplicate) : loginTaken(operation, taken, duplicate);
137
+ }
138
138
  function unexpectedDuplicate(slot, operation, duplicate) {
139
139
  return new StoreFailure(`${slot}.${operation}: a duplicate key on ${Object.keys(duplicate.keyPattern ?? {}).join(", ") || "an unknown index"}, which this adapter never writes on purpose`, { slot, operation, cause: duplicate });
140
140
  }
141
141
 
142
- // src/relations.ts
142
+ // src/relations/collection.ts
143
+ import { defineCollection as defineCollection2 } from "@nxgt/mongo";
144
+ import { z as z2 } from "zod";
143
145
  var relations = defineCollection2({
144
146
  name: "relations",
145
147
  schema: z2.object({
@@ -171,82 +173,14 @@ var relations = defineCollection2({
171
173
  }
172
174
  ]
173
175
  });
174
- function createMongoRelations(db) {
175
- const collection = getCollection(db, relations).raw;
176
- const run$ = (operation, body) => run("relations", operation, body);
177
- const holding = async (object, relation, sets) => (await collection.find({
178
- "_id.object.type": object.type,
179
- "_id.object.id": object.id,
180
- "_id.relation": relation,
181
- "_id.subject.relation": { $exists: sets }
182
- }).toArray()).map((document) => document._id.subject);
183
- return {
184
- write: ({ add = [], remove = [] }) => run$("write", async () => {
185
- const removals = remove.map(keyOf);
186
- const additions = add.map(keyOf);
187
- const apply = async (session) => {
188
- if (removals.length > 0) {
189
- await collection.deleteMany({ _id: { $in: removals } }, session === undefined ? {} : { session });
190
- }
191
- if (additions.length > 0) {
192
- await collection.bulkWrite(additions.map((key) => ({
193
- replaceOne: {
194
- filter: { _id: key },
195
- replacement: { _id: key },
196
- upsert: true
197
- }
198
- })), session === undefined ? {} : { session });
199
- }
200
- };
201
- if (removals.length + additions.length <= 1)
202
- return apply();
203
- const session = db.client.startSession();
204
- try {
205
- session.startTransaction();
206
- await apply(session);
207
- await session.commitTransaction();
208
- } finally {
209
- await session.endSession();
210
- }
211
- }),
212
- has: (tuple) => run$("has", async () => await collection.findOne({ _id: keyOf(tuple) }, { projection: { _id: 1 } }) !== null),
213
- findSubjectSets: (object, relation) => run$("findSubjectSets", async () => (await holding(object, relation, true)).map((subject) => ({
214
- type: subject.type,
215
- id: subject.id,
216
- relation: subject.relation
217
- }))),
218
- findEntities: (object, relation) => run$("findEntities", async () => (await holding(object, relation, false)).map((subject) => ({ type: subject.type, id: subject.id }))),
219
- findObjects: ({ type, relation, subject, after, limit }) => run$("findObjects", async () => {
220
- const filter = {
221
- "_id.subject.type": subject.type,
222
- "_id.subject.id": subject.id,
223
- "_id.subject.relation": isSubjectSet(subject) ? subject.relation : null,
224
- "_id.relation": relation,
225
- "_id.object.type": type,
226
- ...after === null ? {} : { "_id.object.id": { $gt: after } }
227
- };
228
- const found = await collection.find(filter, { projection: { "_id.object.id": 1 } }).sort({ "_id.object.id": 1 }).limit(limit + 1).toArray();
229
- const items = found.slice(0, limit).map((document) => document._id.object.id);
230
- const last = items.at(-1);
231
- return {
232
- items,
233
- nextCursor: found.length > limit && last !== undefined ? last : null
234
- };
235
- }),
236
- deleteEntity: (entity) => run$("deleteEntity", async () => {
237
- const result = await collection.deleteMany({
238
- $or: [
239
- { "_id.object.type": entity.type, "_id.object.id": entity.id },
240
- { "_id.subject.type": entity.type, "_id.subject.id": entity.id }
241
- ]
242
- });
243
- return result.deletedCount;
244
- })
245
- };
246
- }
247
- function syncMongoRelations(db, options) {
248
- return syncCollections(db, [relations], options);
249
- }
176
+
177
+ // src/relations/reads.ts
178
+ import {
179
+ isSubjectSet as isSubjectSet2
180
+ } from "@nxgt/janus";
181
+
182
+ // src/relations/tuples.ts
183
+ import { isSubjectSet } from "@nxgt/janus";
250
184
  function keyOf(tuple) {
251
185
  return {
252
186
  object: { type: tuple.object.type, id: tuple.object.id },
@@ -258,158 +192,115 @@ function subjectKey(subject) {
258
192
  return isSubjectSet(subject) ? { type: subject.type, id: subject.id, relation: subject.relation } : { type: subject.type, id: subject.id };
259
193
  }
260
194
 
261
- // src/stores.ts
262
- import { NotFoundError, StoreConflict as StoreConflict2 } from "@nxgt/janus";
263
- import {
264
- getCollection as getCollection2,
265
- syncCollections as syncCollections2
266
- } from "@nxgt/mongo";
267
- function createMongoStores(db) {
268
- return {
269
- users: userStore(db),
270
- sessions: sessionStore(db),
271
- tokens: tokenStore(db)
272
- };
195
+ // src/relations/reads.ts
196
+ async function has({ collection }, tuple) {
197
+ return await collection.findOne({ _id: keyOf(tuple) }, { projection: { _id: 1 } }) !== null;
273
198
  }
274
- function syncMongoStores(db, options) {
275
- return syncCollections2(db, janusCollections, options);
199
+ async function findSubjectSets(context, object, relation) {
200
+ return (await holding(context, object, relation, true)).map((subject) => ({
201
+ type: subject.type,
202
+ id: subject.id,
203
+ relation: subject.relation
204
+ }));
276
205
  }
277
- function userStore(db) {
278
- const collection = getCollection2(db, users);
279
- const run$ = (operation, body) => run("users", operation, body);
280
- const find = async (id) => {
281
- const found = await collection.findById(id);
282
- return found === undefined ? null : toUser(found);
283
- };
284
- const refuse = (operation, duplicate) => {
285
- const taken = takenLogin(duplicate);
286
- return taken === null ? unexpectedDuplicate("users", operation, duplicate) : loginTaken(operation, taken, duplicate);
206
+ async function findEntities(context, object, relation) {
207
+ return (await holding(context, object, relation, false)).map((subject) => ({ type: subject.type, id: subject.id }));
208
+ }
209
+ async function holding({ collection }, object, relation, sets) {
210
+ return (await collection.find({
211
+ "_id.object.type": object.type,
212
+ "_id.object.id": object.id,
213
+ "_id.relation": relation,
214
+ "_id.subject.relation": { $exists: sets }
215
+ }).toArray()).map((document) => document._id.subject);
216
+ }
217
+ async function findObjects({ collection }, { type, relation, subject, after, limit }) {
218
+ const filter = {
219
+ "_id.subject.type": subject.type,
220
+ "_id.subject.id": subject.id,
221
+ "_id.subject.relation": isSubjectSet2(subject) ? subject.relation : null,
222
+ "_id.relation": relation,
223
+ "_id.object.type": type,
224
+ ...after === null ? {} : { "_id.object.id": { $gt: after } }
287
225
  };
226
+ const found = await collection.find(filter, { projection: { "_id.object.id": 1 } }).sort({ "_id.object.id": 1 }).limit(limit + 1).toArray();
227
+ const items = found.slice(0, limit).map((document) => document._id.object.id);
228
+ const last = items.at(-1);
288
229
  return {
289
- insertUser: (record) => run$("insertUser", async () => {
290
- const document = toUserDocument(record);
291
- const outcome = await settle(collection.raw.insertOne(document));
292
- if (!("duplicate" in outcome))
293
- return toUser(document);
294
- const stored = await find(record.id);
295
- if (stored !== null)
296
- return stored;
297
- throw refuse("insertUser", outcome.duplicate);
298
- }),
299
- findUser: (id) => run$("findUser", () => find(id)),
300
- findUserByLogin: (type, login) => run$("findUserByLogin", async () => {
301
- const found = await collection.findFirst({ type, logins: login });
302
- return found === undefined ? null : toUser(found);
303
- }),
304
- listUsers: ({ type, after, limit }) => run$("listUsers", async () => {
305
- const found = await collection.findMany({
306
- filter: after === null ? { type } : { type, _id: { $gt: after } },
307
- sort: { _id: 1 },
308
- limit: limit + 1
309
- });
310
- const items = found.slice(0, limit).map(toUser);
311
- const last = items.at(-1);
312
- return {
313
- items,
314
- nextCursor: found.length > limit && last !== undefined ? last.id : null
315
- };
316
- }),
317
- updateUser: (id, patch, ifVersion) => run$("updateUser", async () => {
318
- const outcome = await settle(collection.raw.findOneAndUpdate({ _id: id, version: ifVersion }, { $set: toUserSet(patch), $inc: { version: 1 } }, { returnDocument: "after" }));
319
- if ("duplicate" in outcome) {
320
- throw refuse("updateUser", outcome.duplicate);
321
- }
322
- if (outcome.written !== null)
323
- return toUser(outcome.written);
324
- const stored = await find(id);
325
- if (stored === null) {
326
- throw new NotFoundError("updateUser: no user has this id", {
327
- userId: id,
328
- operation: "updateUser"
329
- });
330
- }
331
- throw new StoreConflict2("version", `updateUser: expected version ${ifVersion}, found ${stored.version}`, {
332
- userId: id,
333
- expectedVersion: ifVersion,
334
- actualVersion: stored.version,
335
- operation: "updateUser"
336
- });
337
- }),
338
- deleteUser: (id) => run$("deleteUser", async () => {
339
- const result = await collection.raw.deleteOne({ _id: id });
340
- return result.deletedCount === 1;
341
- })
230
+ items,
231
+ nextCursor: found.length > limit && last !== undefined ? last : null
342
232
  };
343
233
  }
344
- function sessionStore(db) {
345
- const collection = getCollection2(db, sessions);
346
- const run$ = (operation, body) => run("sessions", operation, body);
347
- return {
348
- insertSession: (record) => run$("insertSession", async () => {
349
- const outcome = await settle(collection.raw.insertOne(toSessionDocument(record)));
350
- if (!("duplicate" in outcome))
351
- return;
352
- if (await collection.findById(record.id) !== undefined)
353
- return;
354
- throw unexpectedDuplicate("sessions", "insertSession", outcome.duplicate);
355
- }),
356
- findSessionByTokenHash: (tokenHash) => run$("findSessionByTokenHash", async () => {
357
- const found = await collection.findFirst({ tokenHash });
358
- return found === undefined ? null : toSession(found);
359
- }),
360
- extendSession: (id, expiresAt) => run$("extendSession", async () => {
361
- const written = await collection.raw.findOneAndUpdate({ _id: id, revokedAt: null }, { $set: { expiresAt } }, { returnDocument: "after" });
362
- return written === null ? null : toSession(written);
363
- }),
364
- revokeSession: (id, at) => run$("revokeSession", async () => {
365
- const result = await collection.raw.updateOne({ _id: id }, [
366
- { $set: { revokedAt: { $ifNull: ["$revokedAt", at] } } }
367
- ]);
368
- return result.matchedCount === 1;
369
- }),
370
- revokeUserSessions: (userId, at, except) => run$("revokeUserSessions", async () => {
371
- const result = await collection.raw.updateMany(except === undefined ? { userId, revokedAt: null } : { userId, revokedAt: null, _id: { $ne: except } }, { $set: { revokedAt: at } });
372
- return result.modifiedCount;
373
- }),
374
- deleteUserSessions: (userId) => run$("deleteUserSessions", async () => {
375
- const result = await collection.raw.deleteMany({ userId });
376
- return result.deletedCount;
377
- })
378
- };
234
+
235
+ // src/relations/writes.ts
236
+ async function write({ db, collection }, add, remove) {
237
+ const removals = remove.map(keyOf);
238
+ const additions = add.map(keyOf);
239
+ if (removals.length + additions.length <= 1) {
240
+ return apply(collection, additions, removals);
241
+ }
242
+ const session = db.client.startSession();
243
+ try {
244
+ session.startTransaction();
245
+ await apply(collection, additions, removals, session);
246
+ await session.commitTransaction();
247
+ } finally {
248
+ await session.endSession();
249
+ }
379
250
  }
380
- function tokenStore(db) {
381
- const collection = getCollection2(db, tokens);
382
- const run$ = (operation, body) => run("tokens", operation, body);
251
+ async function apply(collection, additions, removals, session) {
252
+ if (removals.length > 0) {
253
+ await collection.deleteMany({ _id: { $in: removals } }, session === undefined ? {} : { session });
254
+ }
255
+ if (additions.length > 0) {
256
+ await collection.bulkWrite(additions.map((key) => ({
257
+ replaceOne: {
258
+ filter: { _id: key },
259
+ replacement: { _id: key },
260
+ upsert: true
261
+ }
262
+ })), session === undefined ? {} : { session });
263
+ }
264
+ }
265
+ async function deleteEntity({ collection }, entity) {
266
+ const result = await collection.deleteMany({
267
+ $or: [
268
+ { "_id.object.type": entity.type, "_id.object.id": entity.id },
269
+ { "_id.subject.type": entity.type, "_id.subject.id": entity.id }
270
+ ]
271
+ });
272
+ return result.deletedCount;
273
+ }
274
+
275
+ // src/relations/index.ts
276
+ function createMongoRelations(db) {
277
+ const context = {
278
+ db,
279
+ collection: getCollection(db, relations).raw
280
+ };
281
+ const run$ = (operation, body) => run("relations", operation, body);
383
282
  return {
384
- insertToken: (record) => run$("insertToken", async () => {
385
- await settle(collection.raw.insertOne(toTokenDocument(record)));
386
- }),
387
- consumeToken: (tokenHash, kind, at) => run$("consumeToken", async () => {
388
- const before = await collection.raw.findOneAndUpdate({ _id: tokenHash, kind }, [{ $set: { spentAt: { $ifNull: ["$spentAt", at] } } }], { returnDocument: "before" });
389
- return before === null ? null : toToken(before);
390
- }),
391
- countAttempt: (tokenHash, kind) => run$("countAttempt", async () => {
392
- const after = await collection.raw.findOneAndUpdate({ _id: tokenHash, kind, spentAt: null }, { $inc: { attempts: 1 } }, { returnDocument: "after" });
393
- if (after !== null)
394
- return toToken(after);
395
- const spent = await collection.raw.findOne({ _id: tokenHash, kind });
396
- return spent === null ? null : toToken(spent);
397
- }),
398
- spendUserTokens: (userId, kind, at, except) => run$("spendUserTokens", async () => {
399
- const result = await collection.raw.updateMany({
400
- userId,
401
- kind,
402
- spentAt: null,
403
- ...except === undefined ? {} : { _id: { $ne: except } }
404
- }, { $set: { spentAt: at } });
405
- return result.modifiedCount;
406
- }),
407
- deleteUserTokens: (userId) => run$("deleteUserTokens", async () => {
408
- const result = await collection.raw.deleteMany({ userId });
409
- return result.deletedCount;
410
- })
283
+ write: ({ add = [], remove = [] }) => run$("write", () => write(context, add, remove)),
284
+ has: (tuple) => run$("has", () => has(context, tuple)),
285
+ findSubjectSets: (object, relation) => run$("findSubjectSets", () => findSubjectSets(context, object, relation)),
286
+ findEntities: (object, relation) => run$("findEntities", () => findEntities(context, object, relation)),
287
+ findObjects: ({ type, relation, subject, after, limit }) => run$("findObjects", () => findObjects(context, { type, relation, subject, after, limit })),
288
+ deleteEntity: (entity) => run$("deleteEntity", () => deleteEntity(context, entity))
411
289
  };
412
290
  }
291
+ function syncMongoRelations(db, options) {
292
+ return syncCollections(db, [relations], options);
293
+ }
294
+
295
+ // src/stores/index.ts
296
+ import {
297
+ syncCollections as syncCollections2
298
+ } from "@nxgt/mongo";
299
+
300
+ // src/stores/sessions.ts
301
+ import { getCollection as getCollection2 } from "@nxgt/mongo";
302
+
303
+ // src/stores/records.ts
413
304
  function toUserDocument(record) {
414
305
  return structuredClone({
415
306
  _id: record.id,
@@ -419,7 +310,10 @@ function toUserDocument(record) {
419
310
  fields: record.fields,
420
311
  logins: [...record.logins],
421
312
  password: record.password,
422
- secondFactor: record.secondFactor,
313
+ secondFactor: record.secondFactor === null ? null : {
314
+ ...record.secondFactor,
315
+ recoveryCodes: [...record.secondFactor.recoveryCodes]
316
+ },
423
317
  emailVerifiedAt: record.emailVerifiedAt,
424
318
  version: record.version,
425
319
  createdAt: record.createdAt,
@@ -457,7 +351,8 @@ function toUser(document) {
457
351
  method: secondFactor.method,
458
352
  secret: secondFactor.secret,
459
353
  confirmedAt: secondFactor.confirmedAt,
460
- lastStep: secondFactor.lastStep
354
+ lastStep: secondFactor.lastStep,
355
+ recoveryCodes: [...secondFactor.recoveryCodes ?? []]
461
356
  },
462
357
  emailVerifiedAt: document.emailVerifiedAt,
463
358
  version: document.version,
@@ -514,6 +409,165 @@ function toToken(document) {
514
409
  };
515
410
  }
516
411
 
412
+ // src/stores/sessions.ts
413
+ function sessionStore(db) {
414
+ const collection = getCollection2(db, sessions);
415
+ const run$ = (operation, body) => run("sessions", operation, body);
416
+ return {
417
+ insertSession: (record) => run$("insertSession", async () => {
418
+ const outcome = await settle(collection.raw.insertOne(toSessionDocument(record)));
419
+ if (!("duplicate" in outcome))
420
+ return;
421
+ if (await collection.findById(record.id) !== undefined)
422
+ return;
423
+ throw unexpectedDuplicate("sessions", "insertSession", outcome.duplicate);
424
+ }),
425
+ findSessionByTokenHash: (tokenHash) => run$("findSessionByTokenHash", async () => {
426
+ const found = await collection.findFirst({ tokenHash });
427
+ return found === undefined ? null : toSession(found);
428
+ }),
429
+ extendSession: (id, expiresAt) => run$("extendSession", async () => {
430
+ const written = await collection.raw.findOneAndUpdate({ _id: id, revokedAt: null }, { $set: { expiresAt } }, { returnDocument: "after" });
431
+ return written === null ? null : toSession(written);
432
+ }),
433
+ revokeSession: (id, at) => run$("revokeSession", () => revokeSession(collection, id, at)),
434
+ revokeUserSessions: (userId, at, except) => run$("revokeUserSessions", () => revokeUserSessions(collection, userId, at, except)),
435
+ deleteUserSessions: (userId) => run$("deleteUserSessions", async () => {
436
+ const result = await collection.raw.deleteMany({ userId });
437
+ return result.deletedCount;
438
+ })
439
+ };
440
+ }
441
+ async function revokeSession(collection, id, at) {
442
+ const result = await collection.raw.updateOne({ _id: id }, [
443
+ { $set: { revokedAt: { $ifNull: ["$revokedAt", at] } } }
444
+ ]);
445
+ return result.matchedCount === 1;
446
+ }
447
+ async function revokeUserSessions(collection, userId, at, except) {
448
+ const result = await collection.raw.updateMany(except === undefined ? { userId, revokedAt: null } : { userId, revokedAt: null, _id: { $ne: except } }, { $set: { revokedAt: at } });
449
+ return result.modifiedCount;
450
+ }
451
+
452
+ // src/stores/tokens.ts
453
+ import { getCollection as getCollection3 } from "@nxgt/mongo";
454
+ function tokenStore(db) {
455
+ const collection = getCollection3(db, tokens);
456
+ const run$ = (operation, body) => run("tokens", operation, body);
457
+ return {
458
+ insertToken: (record) => run$("insertToken", async () => {
459
+ await settle(collection.raw.insertOne(toTokenDocument(record)));
460
+ }),
461
+ consumeToken: (tokenHash, kind, at) => run$("consumeToken", () => consumeToken(collection, tokenHash, kind, at)),
462
+ countAttempt: (tokenHash, kind) => run$("countAttempt", () => countAttempt(collection, tokenHash, kind)),
463
+ spendUserTokens: (userId, kind, at, except) => run$("spendUserTokens", async () => {
464
+ const result = await collection.raw.updateMany({
465
+ userId,
466
+ kind,
467
+ spentAt: null,
468
+ ...except === undefined ? {} : { _id: { $ne: except } }
469
+ }, { $set: { spentAt: at } });
470
+ return result.modifiedCount;
471
+ }),
472
+ deleteUserTokens: (userId) => run$("deleteUserTokens", async () => {
473
+ const result = await collection.raw.deleteMany({ userId });
474
+ return result.deletedCount;
475
+ })
476
+ };
477
+ }
478
+ async function consumeToken(collection, tokenHash, kind, at) {
479
+ const before = await collection.raw.findOneAndUpdate({ _id: tokenHash, kind }, [{ $set: { spentAt: { $ifNull: ["$spentAt", at] } } }], { returnDocument: "before" });
480
+ return before === null ? null : toToken(before);
481
+ }
482
+ async function countAttempt(collection, tokenHash, kind) {
483
+ const after = await collection.raw.findOneAndUpdate({ _id: tokenHash, kind, spentAt: null }, { $inc: { attempts: 1 } }, { returnDocument: "after" });
484
+ if (after !== null)
485
+ return toToken(after);
486
+ const spent = await collection.raw.findOne({ _id: tokenHash, kind });
487
+ return spent === null ? null : toToken(spent);
488
+ }
489
+
490
+ // src/stores/users.ts
491
+ import { NotFoundError, StoreConflict as StoreConflict2 } from "@nxgt/janus";
492
+ import { getCollection as getCollection4 } from "@nxgt/mongo";
493
+ function userStore(db) {
494
+ const collection = getCollection4(db, users);
495
+ const run$ = (operation, body) => run("users", operation, body);
496
+ return {
497
+ insertUser: (record) => run$("insertUser", () => insertUser(collection, record)),
498
+ findUser: (id) => run$("findUser", () => findUser(collection, id)),
499
+ findUserByLogin: (type, login) => run$("findUserByLogin", async () => {
500
+ const found = await collection.findFirst({ type, logins: login });
501
+ return found === undefined ? null : toUser(found);
502
+ }),
503
+ listUsers: ({ type, after, limit }) => run$("listUsers", async () => {
504
+ const found = await collection.findMany({
505
+ filter: after === null ? { type } : { type, _id: { $gt: after } },
506
+ sort: { _id: 1 },
507
+ limit: limit + 1
508
+ });
509
+ const items = found.slice(0, limit).map(toUser);
510
+ const last = items.at(-1);
511
+ return {
512
+ items,
513
+ nextCursor: found.length > limit && last !== undefined ? last.id : null
514
+ };
515
+ }),
516
+ updateUser: (id, patch, ifVersion) => run$("updateUser", () => updateUser(collection, id, patch, ifVersion)),
517
+ deleteUser: (id) => run$("deleteUser", async () => {
518
+ const result = await collection.raw.deleteOne({ _id: id });
519
+ return result.deletedCount === 1;
520
+ })
521
+ };
522
+ }
523
+ async function findUser(collection, id) {
524
+ const found = await collection.findById(id);
525
+ return found === undefined ? null : toUser(found);
526
+ }
527
+ async function insertUser(collection, record) {
528
+ const document = toUserDocument(record);
529
+ const outcome = await settle(collection.raw.insertOne(document));
530
+ if (!("duplicate" in outcome))
531
+ return toUser(document);
532
+ const stored = await findUser(collection, record.id);
533
+ if (stored !== null)
534
+ return stored;
535
+ throw refuseDuplicate("insertUser", outcome.duplicate);
536
+ }
537
+ async function updateUser(collection, id, patch, ifVersion) {
538
+ const outcome = await settle(collection.raw.findOneAndUpdate({ _id: id, version: ifVersion }, { $set: toUserSet(patch), $inc: { version: 1 } }, { returnDocument: "after" }));
539
+ if ("duplicate" in outcome) {
540
+ throw refuseDuplicate("updateUser", outcome.duplicate);
541
+ }
542
+ if (outcome.written !== null)
543
+ return toUser(outcome.written);
544
+ const stored = await findUser(collection, id);
545
+ if (stored === null) {
546
+ throw new NotFoundError("updateUser: no user has this id", {
547
+ userId: id,
548
+ operation: "updateUser"
549
+ });
550
+ }
551
+ throw new StoreConflict2("version", `updateUser: expected version ${ifVersion}, found ${stored.version}`, {
552
+ userId: id,
553
+ expectedVersion: ifVersion,
554
+ actualVersion: stored.version,
555
+ operation: "updateUser"
556
+ });
557
+ }
558
+
559
+ // src/stores/index.ts
560
+ function createMongoStores(db) {
561
+ return {
562
+ users: userStore(db),
563
+ sessions: sessionStore(db),
564
+ tokens: tokenStore(db)
565
+ };
566
+ }
567
+ function syncMongoStores(db, options) {
568
+ return syncCollections2(db, janusCollections, options);
569
+ }
570
+
517
571
  // src/adapter.ts
518
572
  function createMongoAdapter(db) {
519
573
  return { store: createMongoStores(db), relations: createMongoRelations(db) };
@@ -535,5 +589,5 @@ export {
535
589
  users
536
590
  };
537
591
 
538
- //# debugId=2B060AC3ACC055FA64756E2164756E21
592
+ //# debugId=2F309D39329EBE5364756E2164756E21
539
593
  //# sourceMappingURL=index.js.map