@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 +15 -1
- package/dist/collections.d.ts +2 -0
- package/dist/collections.d.ts.map +1 -1
- package/dist/index.js +283 -229
- package/dist/index.js.map +15 -7
- package/dist/relations/collection.d.ts +32 -0
- package/dist/relations/collection.d.ts.map +1 -0
- package/dist/relations/context.d.ts +13 -0
- package/dist/relations/context.d.ts.map +1 -0
- package/dist/relations/index.d.ts +27 -0
- package/dist/relations/index.d.ts.map +1 -0
- package/dist/relations/reads.d.ts +9 -0
- package/dist/relations/reads.d.ts.map +1 -0
- package/dist/relations/tuples.d.ts +5 -0
- package/dist/relations/tuples.d.ts.map +1 -0
- package/dist/relations/writes.d.ts +7 -0
- package/dist/relations/writes.d.ts.map +1 -0
- package/dist/{stores.d.ts → stores/index.d.ts} +1 -1
- package/dist/stores/index.d.ts.map +1 -0
- package/dist/stores/records.d.ts +20 -0
- package/dist/stores/records.d.ts.map +1 -0
- package/dist/stores/sessions.d.ts +4 -0
- package/dist/stores/sessions.d.ts.map +1 -0
- package/dist/stores/tokens.d.ts +4 -0
- package/dist/stores/tokens.d.ts.map +1 -0
- package/dist/stores/users.d.ts +4 -0
- package/dist/stores/users.d.ts.map +1 -0
- package/dist/translate.d.ts +2 -0
- package/dist/translate.d.ts.map +1 -1
- package/docs/guide/sync.md +14 -3
- package/docs/roadmap.md +5 -0
- package/docs/troubleshooting.md +1 -1
- package/package.json +4 -4
- package/dist/relations.d.ts +0 -54
- package/dist/relations.d.ts.map +0 -1
- package/dist/stores.d.ts.map +0 -1
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
|
package/dist/collections.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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/
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
275
|
-
return
|
|
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
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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=
|
|
592
|
+
//# debugId=2F309D39329EBE5364756E2164756E21
|
|
539
593
|
//# sourceMappingURL=index.js.map
|