@nxgt/janus-mongo 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -70,11 +70,23 @@ encoded. A document read in a shell reads like the record in the code.
70
70
  | `tokens` | `_id` is the token's hash · `userId` · `expiry`, a TTL index |
71
71
  | `relations` | `_id` **is the tuple**, `{ object: { type, id }, relation, subject: { type, id, relation? } }` — unique by construction · `objectRelation` for one hop forwards · `subjectObjects` for `findObjects`, which it serves with no in-memory sort (measured: 11 keys examined for a page of 10) |
72
72
 
73
- No secret is stored: sessions and tokens hold `sha256` of the secret, and
74
- passwords a self-describing hash.
73
+ No secret is stored: sessions and tokens hold `sha256` of the secret,
74
+ passwords a self-describing hash, and a user's `secondFactor.secret` a TOTP
75
+ secret `@nxgt/janus` has already sealed with your application's key —
76
+ `v1.<key id>.…` — which the store keeps byte for byte.
77
+
78
+ `countAttempt` counts an attempt at a code in one `findOneAndUpdate` with
79
+ `$inc`, so twenty concurrent attempts answer twenty distinct counts.
75
80
 
76
81
  ## Traps
77
82
 
83
+ - **Upgrading to 0.3: run the sync before deploying.** No document is
84
+ rewritten — a user without `secondFactor` reads as having none, a token
85
+ without `codeHash` and `attempts` as `null` and `0` — but the validators the
86
+ previous sync wrote refuse the new fields. Until `syncMongoAdapter(db)` has
87
+ run, every sign-up and every one-time token fails with `STORE_FAILED`,
88
+ caused by `Document failed validation`.
89
+ [Upgrading](docs/guide/sync.md#upgrading-sync-before-you-deploy) has the steps.
78
90
  - **The TTL indexes are storage hygiene, not the expiry.** MongoDB's TTL monitor
79
91
  runs every sixty seconds, so a lapsed session can stay readable for up to a
80
92
  minute. The core compares `expiresAt` on every read, and that is what expires
@@ -10,6 +10,12 @@ export declare const users: import("@nxgt/mongo").CollectionDefinition<import("@
10
10
  hash: z.ZodString;
11
11
  updatedAt: z.ZodDate;
12
12
  }, z.core.$strip>>;
13
+ secondFactor: z.ZodOptional<z.ZodNullable<z.ZodObject<{
14
+ method: z.ZodLiteral<"totp">;
15
+ secret: z.ZodString;
16
+ confirmedAt: z.ZodNullable<z.ZodDate>;
17
+ lastStep: z.ZodNullable<z.ZodInt>;
18
+ }, z.core.$strip>>>;
13
19
  emailVerifiedAt: z.ZodNullable<z.ZodDate>;
14
20
  version: z.ZodInt;
15
21
  createdAt: z.ZodDate;
@@ -27,11 +33,15 @@ export declare const sessions: import("@nxgt/mongo").CollectionDefinition<import
27
33
  export declare const tokens: import("@nxgt/mongo").CollectionDefinition<import("@nxgt/mongo").StampedSchema<{
28
34
  _id: z.ZodString;
29
35
  kind: z.ZodEnum<{
36
+ secondFactor: "secondFactor";
30
37
  verifyEmail: "verifyEmail";
31
38
  resetPassword: "resetPassword";
39
+ signInCode: "signInCode";
32
40
  }>;
33
41
  userId: z.ZodString;
34
42
  address: z.ZodString;
43
+ codeHash: z.ZodOptional<z.ZodNullable<z.ZodString>>;
44
+ attempts: z.ZodOptional<z.ZodInt>;
35
45
  expiresAt: z.ZodDate;
36
46
  spentAt: z.ZodNullable<z.ZodDate>;
37
47
  createdAt: z.ZodDate;
@@ -48,6 +58,12 @@ export declare const janusCollections: readonly [import("@nxgt/mongo").Collectio
48
58
  hash: z.ZodString;
49
59
  updatedAt: z.ZodDate;
50
60
  }, z.core.$strip>>;
61
+ secondFactor: z.ZodOptional<z.ZodNullable<z.ZodObject<{
62
+ method: z.ZodLiteral<"totp">;
63
+ secret: z.ZodString;
64
+ confirmedAt: z.ZodNullable<z.ZodDate>;
65
+ lastStep: z.ZodNullable<z.ZodInt>;
66
+ }, z.core.$strip>>>;
51
67
  emailVerifiedAt: z.ZodNullable<z.ZodDate>;
52
68
  version: z.ZodInt;
53
69
  createdAt: z.ZodDate;
@@ -63,11 +79,15 @@ export declare const janusCollections: readonly [import("@nxgt/mongo").Collectio
63
79
  }, false, false, false, false>, import("@nxgt/mongo").StampNamesOf<false, false, false, false>>, import("@nxgt/mongo").CollectionDefinition<import("@nxgt/mongo").StampedSchema<{
64
80
  _id: z.ZodString;
65
81
  kind: z.ZodEnum<{
82
+ secondFactor: "secondFactor";
66
83
  verifyEmail: "verifyEmail";
67
84
  resetPassword: "resetPassword";
85
+ signInCode: "signInCode";
68
86
  }>;
69
87
  userId: z.ZodString;
70
88
  address: z.ZodString;
89
+ codeHash: z.ZodOptional<z.ZodNullable<z.ZodString>>;
90
+ attempts: z.ZodOptional<z.ZodInt>;
71
91
  expiresAt: z.ZodDate;
72
92
  spentAt: z.ZodNullable<z.ZodDate>;
73
93
  createdAt: z.ZodDate;
@@ -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;;;;;;;;;;;;;;;+FAmChB,CAAC;AAEH,eAAO,MAAM,QAAQ;;;;;;;;+FA6BnB,CAAC;AAEH,eAAO,MAAM,MAAM;;;;;;;;;;;+FAmBjB,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;;;;;;;;;;;;;;;;;;;;;+FAgDhB,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
@@ -18,6 +18,12 @@ var users = defineCollection({
18
18
  fields: jsonObject,
19
19
  logins: z.array(z.string()),
20
20
  password: z.object({ hash: z.string(), updatedAt: at }).nullable(),
21
+ secondFactor: z.object({
22
+ method: z.literal("totp"),
23
+ secret: z.string(),
24
+ confirmedAt: at.nullable(),
25
+ lastStep: z.int().nonnegative().nullable()
26
+ }).nullable().optional(),
21
27
  emailVerifiedAt: at.nullable(),
22
28
  version: z.int().nonnegative(),
23
29
  createdAt: at,
@@ -62,9 +68,16 @@ var tokens = defineCollection({
62
68
  name: "tokens",
63
69
  schema: z.object({
64
70
  _id: z.string(),
65
- kind: z.enum(["verifyEmail", "resetPassword"]),
71
+ kind: z.enum([
72
+ "verifyEmail",
73
+ "resetPassword",
74
+ "secondFactor",
75
+ "signInCode"
76
+ ]),
66
77
  userId: z.string(),
67
78
  address: z.string(),
79
+ codeHash: z.string().nullable().optional(),
80
+ attempts: z.int().nonnegative().optional(),
68
81
  expiresAt: at,
69
82
  spentAt: at.nullable(),
70
83
  createdAt: at
@@ -375,6 +388,13 @@ function tokenStore(db) {
375
388
  const before = await collection.raw.findOneAndUpdate({ _id: tokenHash, kind }, [{ $set: { spentAt: { $ifNull: ["$spentAt", at] } } }], { returnDocument: "before" });
376
389
  return before === null ? null : toToken(before);
377
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
+ }),
378
398
  deleteUserTokens: (userId) => run$("deleteUserTokens", async () => {
379
399
  const result = await collection.raw.deleteMany({ userId });
380
400
  return result.deletedCount;
@@ -390,6 +410,7 @@ function toUserDocument(record) {
390
410
  fields: record.fields,
391
411
  logins: [...record.logins],
392
412
  password: record.password,
413
+ secondFactor: record.secondFactor,
393
414
  emailVerifiedAt: record.emailVerifiedAt,
394
415
  version: record.version,
395
416
  createdAt: record.createdAt,
@@ -404,6 +425,7 @@ function toUserSet(patch) {
404
425
  "fields",
405
426
  "logins",
406
427
  "password",
428
+ "secondFactor",
407
429
  "emailVerifiedAt"
408
430
  ]) {
409
431
  if (patch[field] !== undefined)
@@ -413,6 +435,7 @@ function toUserSet(patch) {
413
435
  }
414
436
  function toUser(document) {
415
437
  const password = document.password;
438
+ const secondFactor = document.secondFactor ?? null;
416
439
  return {
417
440
  id: document._id,
418
441
  type: document.type,
@@ -421,6 +444,12 @@ function toUser(document) {
421
444
  fields: document.fields,
422
445
  logins: [...document.logins],
423
446
  password: password === null ? null : { hash: password.hash, updatedAt: password.updatedAt },
447
+ secondFactor: secondFactor === null ? null : {
448
+ method: secondFactor.method,
449
+ secret: secondFactor.secret,
450
+ confirmedAt: secondFactor.confirmedAt,
451
+ lastStep: secondFactor.lastStep
452
+ },
424
453
  emailVerifiedAt: document.emailVerifiedAt,
425
454
  version: document.version,
426
455
  createdAt: document.createdAt,
@@ -455,6 +484,8 @@ function toTokenDocument(record) {
455
484
  kind: record.kind,
456
485
  userId: record.userId,
457
486
  address: record.address,
487
+ codeHash: record.codeHash,
488
+ attempts: record.attempts,
458
489
  expiresAt: record.expiresAt,
459
490
  spentAt: record.spentAt,
460
491
  createdAt: record.createdAt
@@ -466,6 +497,8 @@ function toToken(document) {
466
497
  kind: document.kind,
467
498
  userId: document.userId,
468
499
  address: document.address,
500
+ codeHash: document.codeHash ?? null,
501
+ attempts: document.attempts ?? 0,
469
502
  expiresAt: document.expiresAt,
470
503
  spentAt: document.spentAt,
471
504
  createdAt: document.createdAt
@@ -493,5 +526,5 @@ export {
493
526
  users
494
527
  };
495
528
 
496
- //# debugId=55FFE47757FDC9B264756E2164756E21
529
+ //# debugId=2CC7BD3E8A25C38E64756E2164756E21
497
530
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -3,12 +3,12 @@
3
3
  "sources": ["../src/adapter.ts", "../src/collections.ts", "../src/relations.ts", "../src/translate.ts", "../src/stores.ts"],
4
4
  "sourcesContent": [
5
5
  "import type { JanusStores } from '@nxgt/janus';\nimport type { RelationStore } from '@nxgt/janus/permissions';\nimport {\n\ttype SyncOptions,\n\ttype SyncReport,\n\tsyncCollections,\n} from '@nxgt/mongo';\nimport type { Db } from 'mongodb';\nimport { janusCollections } from './collections';\nimport { createMongoRelations, relations } from './relations';\nimport { createMongoStores } from './stores';\n\n/** The whole adapter over one database, keyed as `janus()` takes it. */\nexport interface MongoAdapter {\n\t/** The identity stores: `createMongoStores(db)`. */\n\treadonly store: JanusStores;\n\t/** The relation store: `createMongoRelations(db)`. */\n\treadonly relations: RelationStore;\n}\n\n/**\n * The identity stores and the relation store over one database, under the\n * names `janus()` takes them — so one spread wires both, and deleting a user\n * deletes every tuple naming them:\n *\n * ```ts\n * const mongo = createMongoAdapter(db);\n * const auth = janus({ user, password: { login: 'email' }, hasher, ...mongo });\n * const access = permissions({ model, store: mongo.relations });\n * ```\n *\n * It connects to nothing and sends nothing, as the two it combines. An\n * application that only authenticates can take `createMongoStores(db)` alone.\n */\nexport function createMongoAdapter(db: Db): MongoAdapter {\n\treturn { store: createMongoStores(db), relations: createMongoRelations(db) };\n}\n\n/**\n * Creates the four collections, their validators and their indexes, and says\n * what it changed: `syncMongoStores` and `syncMongoRelations` in one\n * deployment step. Run it twice and the second run sends nothing.\n */\nexport function syncMongoAdapter(\n\tdb: Db,\n\toptions?: SyncOptions,\n): Promise<SyncReport[]> {\n\treturn syncCollections(db, [...janusCollections, relations], options);\n}\n",
6
- "import { defineCollection } from '@nxgt/mongo';\nimport { z } from 'zod';\n\n/**\n * The three collections, as `@nxgt/mongo` definitions.\n *\n * **What the database holds is the port's record**, with the id under `_id`.\n * Nothing else is renamed and nothing is encoded, so a record comes back byte\n * for byte (rule 4) and a document read in a shell reads like the record in\n * the code.\n *\n * Defining them registers them with `@nxgt/mongo`, so an application that\n * deploys with `syncAll(db)` creates these three with the rest of its own.\n *\n * `@nxgt/mongo`'s optimistic lock and soft delete stay off. The port keeps its\n * own `version`, because it must tell `NOT_FOUND` from `VERSION_CONFLICT`,\n * which takes a second read the lock does not make; and the port has no\n * deletion to soften.\n */\n\n/** Anything JSON. The core validated it against the application's schema already. */\nconst jsonObject = z.record(z.string(), z.unknown());\nconst at = z.date();\n\nexport const users = defineCollection({\n\tname: 'users',\n\tschema: z.object({\n\t\t/** The UUIDv7 the core minted. A string, never an `ObjectId`: the store mints nothing. */\n\t\t_id: z.string(),\n\t\ttype: z.string(),\n\t\tschemaVersion: z.string(),\n\t\tactive: z.boolean(),\n\t\tfields: jsonObject,\n\t\tlogins: z.array(z.string()),\n\t\tpassword: z.object({ hash: z.string(), updatedAt: at }).nullable(),\n\t\temailVerifiedAt: at.nullable(),\n\t\tversion: z.int().nonnegative(),\n\t\tcreatedAt: at,\n\t\tupdatedAt: at,\n\t}),\n\tindexes: [\n\t\t/**\n\t\t * A login is unique **per type**: a scalar `type` and a flat array of\n\t\t * strings, so the multikey semantics are the plain ones, no collation is\n\t\t * needed, and the driver's `keyValue` names both the type and the login\n\t\t * that collided.\n\t\t */\n\t\t{\n\t\t\tkey: { type: 1, logins: 1 },\n\t\t\tname: 'loginUnique',\n\t\t\tunique: true,\n\t\t\t// Without it, two users holding no login at all collide: an empty\n\t\t\t// array is indexed as one missing value, and a unique index admits\n\t\t\t// one of those per type.\n\t\t\tpartialFilterExpression: { logins: { $type: 'string' } },\n\t\t},\n\t\t/** `listUsers` reads one type in `_id` order. */\n\t\t{ key: { type: 1, _id: 1 }, name: 'typeId' },\n\t],\n});\n\nexport const sessions = defineCollection({\n\tname: 'sessions',\n\tschema: z.object({\n\t\t_id: z.string(),\n\t\ttokenHash: z.string(),\n\t\tuserId: z.string(),\n\t\tauthenticatedAt: at,\n\t\texpiresAt: at,\n\t\trevokedAt: at.nullable(),\n\t\tcreatedAt: at,\n\t}),\n\tindexes: [\n\t\t{\n\t\t\tkey: { tokenHash: 1 },\n\t\t\tname: 'tokenHashUnique',\n\t\t\tunique: true,\n\t\t},\n\t\t{ key: { userId: 1 }, name: 'userId' },\n\t\t/**\n\t\t * Storage hygiene, **not** the expiry: MongoDB's TTL monitor runs every\n\t\t * sixty seconds, so a lapsed session stays readable for up to a minute.\n\t\t * The core compares `expiresAt` on every read, which is what expires it.\n\t\t */\n\t\t{\n\t\t\tkey: { expiresAt: 1 },\n\t\t\tname: 'expiry',\n\t\t\texpireAfterSeconds: 0,\n\t\t},\n\t],\n});\n\nexport const tokens = defineCollection({\n\tname: 'tokens',\n\tschema: z.object({\n\t\t/** The token's `sha256`. Unique by construction, so it is the key itself. */\n\t\t_id: z.string(),\n\t\tkind: z.enum(['verifyEmail', 'resetPassword']),\n\t\tuserId: z.string(),\n\t\taddress: z.string(),\n\t\texpiresAt: at,\n\t\tspentAt: at.nullable(),\n\t\tcreatedAt: at,\n\t}),\n\tindexes: [\n\t\t// The same hygiene as sessions'. A token the monitor dropped is answered\n\t\t// `null`, which the port allows: a lapsed token is refused either way.\n\t\t{ key: { expiresAt: 1 }, name: 'expiry', expireAfterSeconds: 0 },\n\t\t/** `deleteUserTokens` reads one user's tokens. */\n\t\t{ key: { userId: 1 }, name: 'userId' },\n\t],\n});\n\n/** The three definitions, in the order `syncMongoStores` syncs them. */\nexport const janusCollections = [users, sessions, tokens] as const;\n",
6
+ "import { defineCollection } from '@nxgt/mongo';\nimport { z } from 'zod';\n\n/**\n * The three collections, as `@nxgt/mongo` definitions.\n *\n * **What the database holds is the port's record**, with the id under `_id`.\n * Nothing else is renamed and nothing is encoded, so a record comes back byte\n * for byte (rule 4) and a document read in a shell reads like the record in\n * the code.\n *\n * Defining them registers them with `@nxgt/mongo`, so an application that\n * deploys with `syncAll(db)` creates these three with the rest of its own.\n *\n * `@nxgt/mongo`'s optimistic lock and soft delete stay off. The port keeps its\n * own `version`, because it must tell `NOT_FOUND` from `VERSION_CONFLICT`,\n * which takes a second read the lock does not make; and the port has no\n * deletion to soften.\n */\n\n/** Anything JSON. The core validated it against the application's schema already. */\nconst jsonObject = z.record(z.string(), z.unknown());\nconst at = z.date();\n\nexport const users = defineCollection({\n\tname: 'users',\n\tschema: z.object({\n\t\t/** The UUIDv7 the core minted. A string, never an `ObjectId`: the store mints nothing. */\n\t\t_id: z.string(),\n\t\ttype: z.string(),\n\t\tschemaVersion: z.string(),\n\t\tactive: z.boolean(),\n\t\tfields: jsonObject,\n\t\tlogins: z.array(z.string()),\n\t\tpassword: z.object({ hash: z.string(), updatedAt: at }).nullable(),\n\t\t/**\n\t\t * Absent on a user written before 0.3 — read as `null`, so no\n\t\t * migration is needed.\n\t\t */\n\t\tsecondFactor: z\n\t\t\t.object({\n\t\t\t\tmethod: z.literal('totp'),\n\t\t\t\tsecret: z.string(),\n\t\t\t\tconfirmedAt: at.nullable(),\n\t\t\t\tlastStep: z.int().nonnegative().nullable(),\n\t\t\t})\n\t\t\t.nullable()\n\t\t\t.optional(),\n\t\temailVerifiedAt: at.nullable(),\n\t\tversion: z.int().nonnegative(),\n\t\tcreatedAt: at,\n\t\tupdatedAt: at,\n\t}),\n\tindexes: [\n\t\t/**\n\t\t * A login is unique **per type**: a scalar `type` and a flat array of\n\t\t * strings, so the multikey semantics are the plain ones, no collation is\n\t\t * needed, and the driver's `keyValue` names both the type and the login\n\t\t * that collided.\n\t\t */\n\t\t{\n\t\t\tkey: { type: 1, logins: 1 },\n\t\t\tname: 'loginUnique',\n\t\t\tunique: true,\n\t\t\t// Without it, two users holding no login at all collide: an empty\n\t\t\t// array is indexed as one missing value, and a unique index admits\n\t\t\t// one of those per type.\n\t\t\tpartialFilterExpression: { logins: { $type: 'string' } },\n\t\t},\n\t\t/** `listUsers` reads one type in `_id` order. */\n\t\t{ key: { type: 1, _id: 1 }, name: 'typeId' },\n\t],\n});\n\nexport const sessions = defineCollection({\n\tname: 'sessions',\n\tschema: z.object({\n\t\t_id: z.string(),\n\t\ttokenHash: z.string(),\n\t\tuserId: z.string(),\n\t\tauthenticatedAt: at,\n\t\texpiresAt: at,\n\t\trevokedAt: at.nullable(),\n\t\tcreatedAt: at,\n\t}),\n\tindexes: [\n\t\t{\n\t\t\tkey: { tokenHash: 1 },\n\t\t\tname: 'tokenHashUnique',\n\t\t\tunique: true,\n\t\t},\n\t\t{ key: { userId: 1 }, name: 'userId' },\n\t\t/**\n\t\t * Storage hygiene, **not** the expiry: MongoDB's TTL monitor runs every\n\t\t * sixty seconds, so a lapsed session stays readable for up to a minute.\n\t\t * The core compares `expiresAt` on every read, which is what expires it.\n\t\t */\n\t\t{\n\t\t\tkey: { expiresAt: 1 },\n\t\t\tname: 'expiry',\n\t\t\texpireAfterSeconds: 0,\n\t\t},\n\t],\n});\n\nexport const tokens = defineCollection({\n\tname: 'tokens',\n\tschema: z.object({\n\t\t/** The token's `sha256`. Unique by construction, so it is the key itself. */\n\t\t_id: z.string(),\n\t\tkind: z.enum([\n\t\t\t'verifyEmail',\n\t\t\t'resetPassword',\n\t\t\t'secondFactor',\n\t\t\t'signInCode',\n\t\t]),\n\t\tuserId: z.string(),\n\t\taddress: z.string(),\n\t\t/** Absent on a token written before 0.3 — read as `null` and `0`. */\n\t\tcodeHash: z.string().nullable().optional(),\n\t\tattempts: z.int().nonnegative().optional(),\n\t\texpiresAt: at,\n\t\tspentAt: at.nullable(),\n\t\tcreatedAt: at,\n\t}),\n\tindexes: [\n\t\t// The same hygiene as sessions'. A token the monitor dropped is answered\n\t\t// `null`, which the port allows: a lapsed token is refused either way.\n\t\t{ key: { expiresAt: 1 }, name: 'expiry', expireAfterSeconds: 0 },\n\t\t/** `deleteUserTokens` reads one user's tokens. */\n\t\t{ key: { userId: 1 }, name: 'userId' },\n\t],\n});\n\n/** The three definitions, in the order `syncMongoStores` syncs them. */\nexport const janusCollections = [users, sessions, tokens] as const;\n",
7
7
  "import {\n\ttype Entity,\n\tisSubjectSet,\n\ttype RelationTuple,\n\ttype Subject,\n\ttype SubjectSet,\n} from '@nxgt/janus';\nimport type { RelationStore } from '@nxgt/janus/permissions';\nimport {\n\ttype DocumentOf,\n\tdefineCollection,\n\tgetCollection,\n\ttype SyncOptions,\n\ttype SyncReport,\n\tsyncCollections,\n} from '@nxgt/mongo';\nimport type { ClientSession, Db, Filter } from 'mongodb';\nimport { z } from 'zod';\nimport { run } from './translate';\n\n/**\n * The tuples, one document each, **keyed by the tuple itself**:\n * `{ _id: { object: { type, id }, relation, subject: { type, id, relation? } } }`.\n *\n * The key is the port's `RelationTuple`, as written, so uniqueness is the\n * `_id` index every collection has — a constraint, never a read — an insert\n * of a stored tuple is a retry, and a document read in a shell is the tuple\n * in the code. A subject set carries `relation`, an entity does not, so\n * `team:t1#members` and `team:t1` are two keys.\n *\n * The key is always built by {@link keyOf}, in one field order: MongoDB\n * compares embedded documents field by field, in order.\n */\nexport const relations = defineCollection({\n\tname: 'relations',\n\tschema: z.object({\n\t\t_id: z.object({\n\t\t\tobject: z.object({ type: z.string(), id: z.string() }),\n\t\t\trelation: z.string(),\n\t\t\tsubject: z.object({\n\t\t\t\ttype: z.string(),\n\t\t\t\tid: z.string(),\n\t\t\t\trelation: z.string().optional(),\n\t\t\t}),\n\t\t}),\n\t}),\n\tindexes: [\n\t\t/** One hop forwards: `findSubjectSets`, `findEntities`, and `deleteEntity` as the object. */\n\t\t{\n\t\t\tkey: { '_id.object.type': 1, '_id.object.id': 1, '_id.relation': 1 },\n\t\t\tname: 'objectRelation',\n\t\t},\n\t\t/**\n\t\t * The reverse index `list()` walks, in the order `findObjects` pages it:\n\t\t * every equality first, the object id last. Its prefix is `deleteEntity`\n\t\t * as a subject. An entity has no `subject.relation`, which the index\n\t\t * holds as `null` and `findObjects` asks for as `null`.\n\t\t */\n\t\t{\n\t\t\tkey: {\n\t\t\t\t'_id.subject.type': 1,\n\t\t\t\t'_id.subject.id': 1,\n\t\t\t\t'_id.subject.relation': 1,\n\t\t\t\t'_id.relation': 1,\n\t\t\t\t'_id.object.type': 1,\n\t\t\t\t'_id.object.id': 1,\n\t\t\t},\n\t\t\tname: 'subjectObjects',\n\t\t},\n\t],\n});\n\ntype RelationDocument = DocumentOf<typeof relations>;\ntype Key = RelationDocument['_id'];\n\n/**\n * The relation store `permissions()` takes, over one MongoDB database — and\n * the one `janus({ relations })` deletes a user's tuples from.\n *\n * ```ts\n * await syncMongoRelations(db); // a deployment step: creates the indexes\n * const access = permissions({ model, store: createMongoRelations(db) });\n * ```\n *\n * **A write of more than one tuple is a transaction**, which MongoDB runs on\n * a replica set only — as every production deployment is. `grant` and\n * `revoke` write one tuple, and run anywhere. The transaction is not retried\n * here: the driver's `withTransaction` would retry an outage for two minutes\n * before answering, and a write is idempotent, so the caller retries.\n */\nexport function createMongoRelations(db: Db): RelationStore {\n\tconst collection = getCollection(db, relations).raw;\n\tconst run$ = <T>(operation: string, body: () => Promise<T>) =>\n\t\trun('relations', operation, body);\n\n\t/** One hop from an object: its subject sets, or its entities. */\n\tconst holding = async (object: Entity, relation: string, sets: boolean) =>\n\t\t(\n\t\t\tawait collection\n\t\t\t\t.find({\n\t\t\t\t\t'_id.object.type': object.type,\n\t\t\t\t\t'_id.object.id': object.id,\n\t\t\t\t\t'_id.relation': relation,\n\t\t\t\t\t'_id.subject.relation': { $exists: sets },\n\t\t\t\t})\n\t\t\t\t.toArray()\n\t\t).map((document) => document._id.subject);\n\n\treturn {\n\t\twrite: ({ add = [], remove = [] }) =>\n\t\t\trun$('write', async () => {\n\t\t\t\tconst removals = remove.map(keyOf);\n\t\t\t\tconst additions = add.map(keyOf);\n\t\t\t\tconst apply = async (session?: ClientSession) => {\n\t\t\t\t\tif (removals.length > 0) {\n\t\t\t\t\t\tawait collection.deleteMany(\n\t\t\t\t\t\t\t{ _id: { $in: removals } },\n\t\t\t\t\t\t\tsession === undefined ? {} : { session },\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (additions.length > 0) {\n\t\t\t\t\t\t// A replace of a document by itself: a stored tuple is a no-op,\n\t\t\t\t\t\t// an absent one is inserted, and concurrent writers of one\n\t\t\t\t\t\t// tuple meet on the `_id` index, which the server retries.\n\t\t\t\t\t\tawait collection.bulkWrite(\n\t\t\t\t\t\t\tadditions.map((key) => ({\n\t\t\t\t\t\t\t\treplaceOne: {\n\t\t\t\t\t\t\t\t\tfilter: { _id: key },\n\t\t\t\t\t\t\t\t\treplacement: { _id: key },\n\t\t\t\t\t\t\t\t\tupsert: true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t})),\n\t\t\t\t\t\t\tsession === undefined ? {} : { session },\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tif (removals.length + additions.length <= 1) return apply();\n\n\t\t\t\tconst session = db.client.startSession();\n\t\t\t\ttry {\n\t\t\t\t\tsession.startTransaction();\n\t\t\t\t\tawait apply(session);\n\t\t\t\t\tawait session.commitTransaction();\n\t\t\t\t} finally {\n\t\t\t\t\t// Ends — and so aborts — a transaction that did not commit.\n\t\t\t\t\tawait session.endSession();\n\t\t\t\t}\n\t\t\t}),\n\n\t\thas: (tuple) =>\n\t\t\trun$(\n\t\t\t\t'has',\n\t\t\t\tasync () =>\n\t\t\t\t\t(await collection.findOne(\n\t\t\t\t\t\t{ _id: keyOf(tuple) },\n\t\t\t\t\t\t{ projection: { _id: 1 } },\n\t\t\t\t\t)) !== null,\n\t\t\t),\n\n\t\tfindSubjectSets: (object, relation) =>\n\t\t\trun$('findSubjectSets', async () =>\n\t\t\t\t(await holding(object, relation, true)).map(\n\t\t\t\t\t(subject): SubjectSet => ({\n\t\t\t\t\t\ttype: subject.type,\n\t\t\t\t\t\tid: subject.id,\n\t\t\t\t\t\trelation: subject.relation as string,\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t),\n\n\t\tfindEntities: (object, relation) =>\n\t\t\trun$('findEntities', async () =>\n\t\t\t\t(await holding(object, relation, false)).map(\n\t\t\t\t\t(subject): Entity => ({ type: subject.type, id: subject.id }),\n\t\t\t\t),\n\t\t\t),\n\n\t\tfindObjects: ({ type, relation, subject, after, limit }) =>\n\t\t\trun$('findObjects', async () => {\n\t\t\t\tconst filter: Filter<RelationDocument> = {\n\t\t\t\t\t'_id.subject.type': subject.type,\n\t\t\t\t\t'_id.subject.id': subject.id,\n\t\t\t\t\t// `null` matches the missing field: an entity, not a set.\n\t\t\t\t\t'_id.subject.relation': isSubjectSet(subject)\n\t\t\t\t\t\t? subject.relation\n\t\t\t\t\t\t: null,\n\t\t\t\t\t'_id.relation': relation,\n\t\t\t\t\t'_id.object.type': type,\n\t\t\t\t\t...(after === null ? {} : { '_id.object.id': { $gt: after } }),\n\t\t\t\t};\n\t\t\t\tconst found = await collection\n\t\t\t\t\t.find(filter, { projection: { '_id.object.id': 1 } })\n\t\t\t\t\t.sort({ '_id.object.id': 1 })\n\t\t\t\t\t.limit(limit + 1)\n\t\t\t\t\t.toArray();\n\t\t\t\tconst items = found\n\t\t\t\t\t.slice(0, limit)\n\t\t\t\t\t.map((document) => document._id.object.id);\n\t\t\t\tconst last = items.at(-1);\n\t\t\t\treturn {\n\t\t\t\t\titems,\n\t\t\t\t\tnextCursor: found.length > limit && last !== undefined ? last : null,\n\t\t\t\t};\n\t\t\t}),\n\n\t\tdeleteEntity: (entity) =>\n\t\t\trun$('deleteEntity', async () => {\n\t\t\t\tconst result = await collection.deleteMany({\n\t\t\t\t\t$or: [\n\t\t\t\t\t\t{ '_id.object.type': entity.type, '_id.object.id': entity.id },\n\t\t\t\t\t\t{ '_id.subject.type': entity.type, '_id.subject.id': entity.id },\n\t\t\t\t\t],\n\t\t\t\t});\n\t\t\t\treturn result.deletedCount;\n\t\t\t}),\n\t};\n}\n\n/**\n * Creates the collection and its indexes, and says what it changed. A\n * deployment step, as `syncMongoStores` is — and a separate one, because an\n * application that only authenticates keeps no tuples.\n */\nexport function syncMongoRelations(\n\tdb: Db,\n\toptions?: SyncOptions,\n): Promise<SyncReport[]> {\n\treturn syncCollections(db, [relations], options);\n}\n\n/** The `_id` of a tuple, in the one field order MongoDB compares. */\nfunction keyOf(tuple: RelationTuple): Key {\n\treturn {\n\t\tobject: { type: tuple.object.type, id: tuple.object.id },\n\t\trelation: tuple.relation,\n\t\tsubject: subjectKey(tuple.subject),\n\t};\n}\n\nfunction subjectKey(subject: Subject): Key['subject'] {\n\treturn isSubjectSet(subject)\n\t\t? { type: subject.type, id: subject.id, relation: subject.relation }\n\t\t: { type: subject.type, id: subject.id };\n}\n",
8
8
  "import { JanusError, StoreConflict, StoreFailure } from '@nxgt/janus';\n\n/**\n * **The driver's errors, as the port's.** One file, and one default.\n *\n * | What happens | Becomes |\n * | --- | --- |\n * | A duplicate key on `(type, logins)` | `StoreConflict('login')`, carrying `login` and `userType` |\n * | A duplicate key on `_id` | nothing — the insert is a retry, and answers what is stored |\n * | A duplicate key on **any other index** | **`StoreFailure`** |\n * | A `$jsonSchema` validation failure (121) | **`StoreFailure`** |\n * | Anything else, absolutely | `StoreFailure`, with the driver error as `cause` |\n *\n * The two bold rows go against the obvious reading. A duplicate on an index\n * the adapter did not mean to collide on is an adapter bug: reporting it as\n * `LOGIN_TAKEN` would tell somebody their login is taken when it is\n * not. And the core has validated every record before it reaches a store, so a\n * validator refusing one means the adapter and the port disagree — never the\n * caller's fault, never a 400.\n *\n * **The default is a failure, not an absence.** That branch is what keeps the\n * port's first rule: nothing here answers `null` for an error.\n */\n\n/** Runs one port method, and makes every rejection one the port allows. */\nexport async function run<T>(\n\tslot: 'users' | 'sessions' | 'tokens' | 'relations',\n\toperation: string,\n\tbody: () => Promise<T>,\n): Promise<T> {\n\ttry {\n\t\treturn await body();\n\t} catch (error) {\n\t\t// Already the port's — a conflict, a NOT_FOUND after a read — or a\n\t\t// failure raised below with its own message.\n\t\tif (error instanceof JanusError) throw error;\n\t\tthrow new StoreFailure(`${slot}.${operation}: the store could not answer`, {\n\t\t\tslot,\n\t\t\toperation,\n\t\t\tcause: error,\n\t\t});\n\t}\n}\n\n/** The shape of the driver's `E11000`, as far as this file reads it. */\nexport interface DuplicateKey {\n\treadonly code: 11000;\n\treadonly keyPattern?: Readonly<Record<string, unknown>>;\n\treadonly keyValue?: Readonly<Record<string, unknown>>;\n}\n\nexport function isDuplicateKey(error: unknown): error is DuplicateKey {\n\treturn (\n\t\ttypeof error === 'object' &&\n\t\terror !== null &&\n\t\t(error as { code?: unknown }).code === 11000\n\t);\n}\n\n/**\n * Settles a write: what it answered when it landed, or the duplicate-key error\n * when an index refused it. **Any other rejection is rethrown**, so a caller\n * of this is only ever told \"written\" or \"duplicate\", and never \"nothing\".\n */\nexport function settle<T>(\n\twrite: Promise<T>,\n): Promise<\n\t| { readonly written: T }\n\t| { readonly written?: never; duplicate: DuplicateKey }\n> {\n\treturn write.then(\n\t\t(written) => ({ written }),\n\t\t(error: unknown) => {\n\t\t\tif (isDuplicateKey(error)) return { duplicate: error };\n\t\t\tthrow error;\n\t\t},\n\t);\n}\n\n/** A login one type holds. */\nexport interface TypedLogin {\n\treadonly type: string;\n\treadonly login: string;\n}\n\n/**\n * The login a duplicate names, when it is the login index that refused the\n * write — and `null` for every other index, which the caller then reports as\n * a failure.\n */\nexport function takenLogin(duplicate: DuplicateKey): TypedLogin | null {\n\tconst type = duplicate.keyValue?.type;\n\tconst login = duplicate.keyValue?.logins;\n\tif (typeof type !== 'string' || typeof login !== 'string') return null;\n\treturn { type, login };\n}\n\n/** The refusal the port promises for a taken login. */\nexport function loginTaken(\n\toperation: string,\n\t{ type, login }: TypedLogin,\n\tcause: unknown,\n): StoreConflict {\n\treturn new StoreConflict(\n\t\t'login',\n\t\t`${operation}: the login is taken by another ${type}`,\n\t\t{ login, userType: type, operation, cause },\n\t);\n}\n\n/** A duplicate on an index the adapter never meant to collide on: a bug, reported as one. */\nexport function unexpectedDuplicate(\n\tslot: 'users' | 'sessions' | 'tokens',\n\toperation: string,\n\tduplicate: DuplicateKey,\n): StoreFailure {\n\treturn new StoreFailure(\n\t\t`${slot}.${operation}: a duplicate key on ${Object.keys(duplicate.keyPattern ?? {}).join(', ') || 'an unknown index'}, which this adapter never writes on purpose`,\n\t\t{ slot, operation, cause: duplicate },\n\t);\n}\n",
9
- "import type {\n\tJanusStores,\n\tSessionRecord,\n\tSessionStore,\n\tTokenRecord,\n\tTokenStore,\n\tUserPatch,\n\tUserRecord,\n\tUserStore,\n} from '@nxgt/janus';\nimport { type Id, NotFoundError, StoreConflict } from '@nxgt/janus';\nimport {\n\ttype DocumentOf,\n\tgetCollection,\n\ttype SyncOptions,\n\ttype SyncReport,\n\tsyncCollections,\n} from '@nxgt/mongo';\nimport type { Db } from 'mongodb';\nimport { janusCollections, sessions, tokens, users } from './collections';\nimport {\n\tloginTaken,\n\trun,\n\tsettle,\n\ttakenLogin,\n\tunexpectedDuplicate,\n} from './translate';\n\ntype UserDocument = DocumentOf<typeof users>;\ntype SessionDocument = DocumentOf<typeof sessions>;\ntype TokenDocument = DocumentOf<typeof tokens>;\n\n/**\n * The three stores `janus()` takes, over one MongoDB database.\n *\n * ```ts\n * const db = (await connectMongo(process.env.MONGO_URI)).db;\n * await syncMongoStores(db); // a deployment step: creates the indexes\n * const auth = janus({ user, password: { login: 'email' }, store: createMongoStores(db), hasher });\n * ```\n *\n * **Reads go through `@nxgt/mongo`'s `find*`, never its `get*`.** `findById`\n * answers `undefined` for an absence, so an absence arrives as a *value*,\n * turned into `null` here, and everything that arrives as a rejection is a\n * failure. There is no path on which an absence and an outage travel the same\n * channel, so there is none on which a `catch` could confuse them.\n *\n * **Every conditional write is one driver call** on `raw`, the driver's own\n * collection: `@nxgt/mongo` names no update conditioned on anything but its\n * own lock, and the port needs `version`, `revokedAt` and `kind`.\n *\n * `sessions.deleteExpiredSessions` is not implemented: a TTL index drops lapsed\n * sessions, so `sessions.collectExpired()` answers `UNSUPPORTED` — which is\n * what the port asks of a store with its own expiry.\n */\nexport function createMongoStores(db: Db): JanusStores {\n\treturn {\n\t\tusers: userStore(db),\n\t\tsessions: sessionStore(db),\n\t\ttokens: tokenStore(db),\n\t};\n}\n\n/**\n * Creates the three collections, their validators and their indexes, and says\n * what it changed. Run it twice and the second run sends nothing.\n *\n * A deployment step, never a request-time one: it needs the `dbAdmin` role,\n * and the core never calls it (rule 6). An application that deploys with\n * `@nxgt/mongo`'s `syncAll(db)` already syncs these three with its own.\n */\nexport function syncMongoStores(\n\tdb: Db,\n\toptions?: SyncOptions,\n): Promise<SyncReport[]> {\n\treturn syncCollections(db, janusCollections, options);\n}\n\nfunction userStore(db: Db): UserStore {\n\tconst collection = getCollection(db, users);\n\tconst run$ = <T>(operation: string, body: () => Promise<T>) =>\n\t\trun('users', operation, body);\n\n\tconst find = async (id: string) => {\n\t\tconst found = await collection.findById(id);\n\t\treturn found === undefined ? null : toUser(found);\n\t};\n\n\t/** A duplicate on the login index as the port's conflict, and any other as a bug. */\n\tconst refuse = (\n\t\toperation: string,\n\t\tduplicate: Parameters<typeof takenLogin>[0],\n\t) => {\n\t\tconst taken = takenLogin(duplicate);\n\t\treturn taken === null\n\t\t\t? unexpectedDuplicate('users', operation, duplicate)\n\t\t\t: loginTaken(operation, taken, duplicate);\n\t};\n\n\treturn {\n\t\tinsertUser: (record) =>\n\t\t\trun$('insertUser', async () => {\n\t\t\t\tconst document = toUserDocument(record);\n\t\t\t\tconst outcome = await settle(collection.raw.insertOne(document));\n\t\t\t\tif (!('duplicate' in outcome)) return toUser(document);\n\n\t\t\t\t// A retry whose first attempt landed: whichever index reported it,\n\t\t\t\t// the user with this id is there, and is the answer. That includes\n\t\t\t\t// their own logins, which are not taken by them.\n\t\t\t\tconst stored = await find(record.id);\n\t\t\t\tif (stored !== null) return stored;\n\t\t\t\tthrow refuse('insertUser', outcome.duplicate);\n\t\t\t}),\n\n\t\tfindUser: (id) => run$('findUser', () => find(id)),\n\n\t\tfindUserByLogin: (type, login) =>\n\t\t\trun$('findUserByLogin', async () => {\n\t\t\t\tconst found = await collection.findFirst({ type, logins: login });\n\t\t\t\treturn found === undefined ? null : toUser(found);\n\t\t\t}),\n\n\t\tlistUsers: ({ type, after, limit }) =>\n\t\t\trun$('listUsers', async () => {\n\t\t\t\t// One more than the page, to know whether another follows.\n\t\t\t\tconst found = await collection.findMany({\n\t\t\t\t\tfilter: after === null ? { type } : { type, _id: { $gt: after } },\n\t\t\t\t\tsort: { _id: 1 },\n\t\t\t\t\tlimit: limit + 1,\n\t\t\t\t});\n\t\t\t\tconst items = found.slice(0, limit).map(toUser);\n\t\t\t\tconst last = items.at(-1);\n\n\t\t\t\treturn {\n\t\t\t\t\titems,\n\t\t\t\t\tnextCursor:\n\t\t\t\t\t\tfound.length > limit && last !== undefined ? last.id : null,\n\t\t\t\t};\n\t\t\t}),\n\n\t\tupdateUser: (id, patch, ifVersion) =>\n\t\t\trun$('updateUser', async () => {\n\t\t\t\tconst outcome = await settle(\n\t\t\t\t\tcollection.raw.findOneAndUpdate(\n\t\t\t\t\t\t{ _id: id, version: ifVersion },\n\t\t\t\t\t\t{ $set: toUserSet(patch), $inc: { version: 1 } },\n\t\t\t\t\t\t{ returnDocument: 'after' },\n\t\t\t\t\t),\n\t\t\t\t);\n\n\t\t\t\tif ('duplicate' in outcome) {\n\t\t\t\t\tthrow refuse('updateUser', outcome.duplicate);\n\t\t\t\t}\n\t\t\t\tif (outcome.written !== null) return toUser(outcome.written);\n\n\t\t\t\t// The write matched nothing, which alone cannot tell an unknown id\n\t\t\t\t// from a moved version: read again, and say which.\n\t\t\t\tconst stored = await find(id);\n\t\t\t\tif (stored === null) {\n\t\t\t\t\tthrow new NotFoundError('updateUser: no user has this id', {\n\t\t\t\t\t\tuserId: id,\n\t\t\t\t\t\toperation: 'updateUser',\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tthrow new StoreConflict(\n\t\t\t\t\t'version',\n\t\t\t\t\t`updateUser: expected version ${ifVersion}, found ${stored.version}`,\n\t\t\t\t\t{\n\t\t\t\t\t\tuserId: id,\n\t\t\t\t\t\texpectedVersion: ifVersion,\n\t\t\t\t\t\tactualVersion: stored.version,\n\t\t\t\t\t\toperation: 'updateUser',\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}),\n\n\t\tdeleteUser: (id) =>\n\t\t\trun$('deleteUser', async () => {\n\t\t\t\tconst result = await collection.raw.deleteOne({ _id: id });\n\t\t\t\treturn result.deletedCount === 1;\n\t\t\t}),\n\t};\n}\n\nfunction sessionStore(db: Db): SessionStore {\n\tconst collection = getCollection(db, sessions);\n\tconst run$ = <T>(operation: string, body: () => Promise<T>) =>\n\t\trun('sessions', operation, body);\n\n\treturn {\n\t\tinsertSession: (record) =>\n\t\t\trun$('insertSession', async () => {\n\t\t\t\tconst outcome = await settle(\n\t\t\t\t\tcollection.raw.insertOne(toSessionDocument(record)),\n\t\t\t\t);\n\t\t\t\tif (!('duplicate' in outcome)) return;\n\n\t\t\t\t// Idempotent under retry: a session with this id is already there.\n\t\t\t\tif ((await collection.findById(record.id)) !== undefined) return;\n\t\t\t\tthrow unexpectedDuplicate(\n\t\t\t\t\t'sessions',\n\t\t\t\t\t'insertSession',\n\t\t\t\t\toutcome.duplicate,\n\t\t\t\t);\n\t\t\t}),\n\n\t\tfindSessionByTokenHash: (tokenHash) =>\n\t\t\trun$('findSessionByTokenHash', async () => {\n\t\t\t\tconst found = await collection.findFirst({ tokenHash });\n\t\t\t\treturn found === undefined ? null : toSession(found);\n\t\t\t}),\n\n\t\textendSession: (id, expiresAt) =>\n\t\t\trun$('extendSession', async () => {\n\t\t\t\t// `revokedAt: null` in the filter: an extension racing a revocation\n\t\t\t\t// matches nothing, and never brings the session back.\n\t\t\t\tconst written = await collection.raw.findOneAndUpdate(\n\t\t\t\t\t{ _id: id, revokedAt: null },\n\t\t\t\t\t{ $set: { expiresAt } },\n\t\t\t\t\t{ returnDocument: 'after' },\n\t\t\t\t);\n\t\t\t\treturn written === null ? null : toSession(written);\n\t\t\t}),\n\n\t\trevokeSession: (id, at) =>\n\t\t\trun$('revokeSession', async () => {\n\t\t\t\t// A pipeline, so a session already revoked keeps its first\n\t\t\t\t// `revokedAt` and still counts as matched.\n\t\t\t\tconst result = await collection.raw.updateOne({ _id: id }, [\n\t\t\t\t\t{ $set: { revokedAt: { $ifNull: ['$revokedAt', at] } } },\n\t\t\t\t]);\n\t\t\t\treturn result.matchedCount === 1;\n\t\t\t}),\n\n\t\trevokeUserSessions: (userId, at, except) =>\n\t\t\trun$('revokeUserSessions', async () => {\n\t\t\t\tconst result = await collection.raw.updateMany(\n\t\t\t\t\texcept === undefined\n\t\t\t\t\t\t? { userId, revokedAt: null }\n\t\t\t\t\t\t: { userId, revokedAt: null, _id: { $ne: except } },\n\t\t\t\t\t{ $set: { revokedAt: at } },\n\t\t\t\t);\n\t\t\t\treturn result.modifiedCount;\n\t\t\t}),\n\n\t\tdeleteUserSessions: (userId) =>\n\t\t\trun$('deleteUserSessions', async () => {\n\t\t\t\tconst result = await collection.raw.deleteMany({ userId });\n\t\t\t\treturn result.deletedCount;\n\t\t\t}),\n\t};\n}\n\nfunction tokenStore(db: Db): TokenStore {\n\tconst collection = getCollection(db, tokens);\n\tconst run$ = <T>(operation: string, body: () => Promise<T>) =>\n\t\trun('tokens', operation, body);\n\n\treturn {\n\t\tinsertToken: (record) =>\n\t\t\trun$('insertToken', async () => {\n\t\t\t\t// `_id` is the hash, the only unique index: a duplicate is a retry.\n\t\t\t\tawait settle(collection.raw.insertOne(toTokenDocument(record)));\n\t\t\t}),\n\n\t\tconsumeToken: (tokenHash, kind, at) =>\n\t\t\trun$('consumeToken', async () => {\n\t\t\t\t// **One conditional write**, answering the document as it was\n\t\t\t\t// before it. The pipeline keeps a first `spentAt`, so exactly one\n\t\t\t\t// caller ever reads `spentAt: null`.\n\t\t\t\tconst before = await collection.raw.findOneAndUpdate(\n\t\t\t\t\t{ _id: tokenHash, kind },\n\t\t\t\t\t[{ $set: { spentAt: { $ifNull: ['$spentAt', at] } } }],\n\t\t\t\t\t{ returnDocument: 'before' },\n\t\t\t\t);\n\t\t\t\treturn before === null ? null : toToken(before);\n\t\t\t}),\n\n\t\tdeleteUserTokens: (userId) =>\n\t\t\trun$('deleteUserTokens', async () => {\n\t\t\t\tconst result = await collection.raw.deleteMany({ userId });\n\t\t\t\treturn result.deletedCount;\n\t\t\t}),\n\t};\n}\n\n// ─── Documents and records ────────────────────────────────────────────────\n//\n// Each record is rebuilt field by field, never spread from a document: a\n// spread would hand the core `_id` and the `id` getter `@nxgt/mongo` adds,\n// and the port's records are exactly what it declares.\n\nfunction toUserDocument(record: UserRecord): UserDocument {\n\treturn structuredClone({\n\t\t_id: record.id,\n\t\ttype: record.type,\n\t\tschemaVersion: record.schemaVersion,\n\t\tactive: record.active,\n\t\tfields: record.fields,\n\t\tlogins: [...record.logins],\n\t\tpassword: record.password,\n\t\temailVerifiedAt: record.emailVerifiedAt,\n\t\tversion: record.version,\n\t\tcreatedAt: record.createdAt,\n\t\tupdatedAt: record.updatedAt,\n\t});\n}\n\n/**\n * The fields a patch names, as one `$set`. A key present with `undefined` is\n * absent, as the port says — never an erasure. `password: null` is named, and\n * removes the password.\n */\nfunction toUserSet(patch: UserPatch): Record<string, unknown> {\n\tconst set: Record<string, unknown> = { updatedAt: patch.updatedAt };\n\n\tfor (const field of [\n\t\t'schemaVersion',\n\t\t'active',\n\t\t'fields',\n\t\t'logins',\n\t\t'password',\n\t\t'emailVerifiedAt',\n\t] as const) {\n\t\tif (patch[field] !== undefined) set[field] = patch[field];\n\t}\n\n\treturn structuredClone(set);\n}\n\nfunction toUser(document: UserDocument): UserRecord {\n\tconst password = document.password;\n\treturn {\n\t\tid: document._id as Id,\n\t\ttype: document.type,\n\t\tschemaVersion: document.schemaVersion,\n\t\tactive: document.active,\n\t\tfields: document.fields as UserRecord['fields'],\n\t\tlogins: [...document.logins],\n\t\tpassword:\n\t\t\tpassword === null\n\t\t\t\t? null\n\t\t\t\t: { hash: password.hash, updatedAt: password.updatedAt },\n\t\temailVerifiedAt: document.emailVerifiedAt,\n\t\tversion: document.version,\n\t\tcreatedAt: document.createdAt,\n\t\tupdatedAt: document.updatedAt,\n\t};\n}\n\nfunction toSessionDocument(record: SessionRecord): SessionDocument {\n\treturn {\n\t\t_id: record.id,\n\t\ttokenHash: record.tokenHash,\n\t\tuserId: record.userId,\n\t\tauthenticatedAt: record.authenticatedAt,\n\t\texpiresAt: record.expiresAt,\n\t\trevokedAt: record.revokedAt,\n\t\tcreatedAt: record.createdAt,\n\t};\n}\n\nfunction toSession(document: SessionDocument): SessionRecord {\n\treturn {\n\t\tid: document._id,\n\t\ttokenHash: document.tokenHash,\n\t\tuserId: document.userId as Id,\n\t\tauthenticatedAt: document.authenticatedAt,\n\t\texpiresAt: document.expiresAt,\n\t\trevokedAt: document.revokedAt,\n\t\tcreatedAt: document.createdAt,\n\t};\n}\n\nfunction toTokenDocument(record: TokenRecord): TokenDocument {\n\treturn {\n\t\t_id: record.tokenHash,\n\t\tkind: record.kind,\n\t\tuserId: record.userId,\n\t\taddress: record.address,\n\t\texpiresAt: record.expiresAt,\n\t\tspentAt: record.spentAt,\n\t\tcreatedAt: record.createdAt,\n\t};\n}\n\nfunction toToken(document: TokenDocument): TokenRecord {\n\treturn {\n\t\ttokenHash: document._id,\n\t\tkind: document.kind,\n\t\tuserId: document.userId as Id,\n\t\taddress: document.address,\n\t\texpiresAt: document.expiresAt,\n\t\tspentAt: document.spentAt,\n\t\tcreatedAt: document.createdAt,\n\t};\n}\n"
9
+ "import type {\n\tJanusStores,\n\tSessionRecord,\n\tSessionStore,\n\tTokenRecord,\n\tTokenStore,\n\tUserPatch,\n\tUserRecord,\n\tUserStore,\n} from '@nxgt/janus';\nimport { type Id, NotFoundError, StoreConflict } from '@nxgt/janus';\nimport {\n\ttype DocumentOf,\n\tgetCollection,\n\ttype SyncOptions,\n\ttype SyncReport,\n\tsyncCollections,\n} from '@nxgt/mongo';\nimport type { Db } from 'mongodb';\nimport { janusCollections, sessions, tokens, users } from './collections';\nimport {\n\tloginTaken,\n\trun,\n\tsettle,\n\ttakenLogin,\n\tunexpectedDuplicate,\n} from './translate';\n\ntype UserDocument = DocumentOf<typeof users>;\ntype SessionDocument = DocumentOf<typeof sessions>;\ntype TokenDocument = DocumentOf<typeof tokens>;\n\n/**\n * The three stores `janus()` takes, over one MongoDB database.\n *\n * ```ts\n * const db = (await connectMongo(process.env.MONGO_URI)).db;\n * await syncMongoStores(db); // a deployment step: creates the indexes\n * const auth = janus({ user, password: { login: 'email' }, store: createMongoStores(db), hasher });\n * ```\n *\n * **Reads go through `@nxgt/mongo`'s `find*`, never its `get*`.** `findById`\n * answers `undefined` for an absence, so an absence arrives as a *value*,\n * turned into `null` here, and everything that arrives as a rejection is a\n * failure. There is no path on which an absence and an outage travel the same\n * channel, so there is none on which a `catch` could confuse them.\n *\n * **Every conditional write is one driver call** on `raw`, the driver's own\n * collection: `@nxgt/mongo` names no update conditioned on anything but its\n * own lock, and the port needs `version`, `revokedAt` and `kind`.\n *\n * `sessions.deleteExpiredSessions` is not implemented: a TTL index drops lapsed\n * sessions, so `sessions.collectExpired()` answers `UNSUPPORTED` — which is\n * what the port asks of a store with its own expiry.\n */\nexport function createMongoStores(db: Db): JanusStores {\n\treturn {\n\t\tusers: userStore(db),\n\t\tsessions: sessionStore(db),\n\t\ttokens: tokenStore(db),\n\t};\n}\n\n/**\n * Creates the three collections, their validators and their indexes, and says\n * what it changed. Run it twice and the second run sends nothing.\n *\n * A deployment step, never a request-time one: it needs the `dbAdmin` role,\n * and the core never calls it (rule 6). An application that deploys with\n * `@nxgt/mongo`'s `syncAll(db)` already syncs these three with its own.\n */\nexport function syncMongoStores(\n\tdb: Db,\n\toptions?: SyncOptions,\n): Promise<SyncReport[]> {\n\treturn syncCollections(db, janusCollections, options);\n}\n\nfunction userStore(db: Db): UserStore {\n\tconst collection = getCollection(db, users);\n\tconst run$ = <T>(operation: string, body: () => Promise<T>) =>\n\t\trun('users', operation, body);\n\n\tconst find = async (id: string) => {\n\t\tconst found = await collection.findById(id);\n\t\treturn found === undefined ? null : toUser(found);\n\t};\n\n\t/** A duplicate on the login index as the port's conflict, and any other as a bug. */\n\tconst refuse = (\n\t\toperation: string,\n\t\tduplicate: Parameters<typeof takenLogin>[0],\n\t) => {\n\t\tconst taken = takenLogin(duplicate);\n\t\treturn taken === null\n\t\t\t? unexpectedDuplicate('users', operation, duplicate)\n\t\t\t: loginTaken(operation, taken, duplicate);\n\t};\n\n\treturn {\n\t\tinsertUser: (record) =>\n\t\t\trun$('insertUser', async () => {\n\t\t\t\tconst document = toUserDocument(record);\n\t\t\t\tconst outcome = await settle(collection.raw.insertOne(document));\n\t\t\t\tif (!('duplicate' in outcome)) return toUser(document);\n\n\t\t\t\t// A retry whose first attempt landed: whichever index reported it,\n\t\t\t\t// the user with this id is there, and is the answer. That includes\n\t\t\t\t// their own logins, which are not taken by them.\n\t\t\t\tconst stored = await find(record.id);\n\t\t\t\tif (stored !== null) return stored;\n\t\t\t\tthrow refuse('insertUser', outcome.duplicate);\n\t\t\t}),\n\n\t\tfindUser: (id) => run$('findUser', () => find(id)),\n\n\t\tfindUserByLogin: (type, login) =>\n\t\t\trun$('findUserByLogin', async () => {\n\t\t\t\tconst found = await collection.findFirst({ type, logins: login });\n\t\t\t\treturn found === undefined ? null : toUser(found);\n\t\t\t}),\n\n\t\tlistUsers: ({ type, after, limit }) =>\n\t\t\trun$('listUsers', async () => {\n\t\t\t\t// One more than the page, to know whether another follows.\n\t\t\t\tconst found = await collection.findMany({\n\t\t\t\t\tfilter: after === null ? { type } : { type, _id: { $gt: after } },\n\t\t\t\t\tsort: { _id: 1 },\n\t\t\t\t\tlimit: limit + 1,\n\t\t\t\t});\n\t\t\t\tconst items = found.slice(0, limit).map(toUser);\n\t\t\t\tconst last = items.at(-1);\n\n\t\t\t\treturn {\n\t\t\t\t\titems,\n\t\t\t\t\tnextCursor:\n\t\t\t\t\t\tfound.length > limit && last !== undefined ? last.id : null,\n\t\t\t\t};\n\t\t\t}),\n\n\t\tupdateUser: (id, patch, ifVersion) =>\n\t\t\trun$('updateUser', async () => {\n\t\t\t\tconst outcome = await settle(\n\t\t\t\t\tcollection.raw.findOneAndUpdate(\n\t\t\t\t\t\t{ _id: id, version: ifVersion },\n\t\t\t\t\t\t{ $set: toUserSet(patch), $inc: { version: 1 } },\n\t\t\t\t\t\t{ returnDocument: 'after' },\n\t\t\t\t\t),\n\t\t\t\t);\n\n\t\t\t\tif ('duplicate' in outcome) {\n\t\t\t\t\tthrow refuse('updateUser', outcome.duplicate);\n\t\t\t\t}\n\t\t\t\tif (outcome.written !== null) return toUser(outcome.written);\n\n\t\t\t\t// The write matched nothing, which alone cannot tell an unknown id\n\t\t\t\t// from a moved version: read again, and say which.\n\t\t\t\tconst stored = await find(id);\n\t\t\t\tif (stored === null) {\n\t\t\t\t\tthrow new NotFoundError('updateUser: no user has this id', {\n\t\t\t\t\t\tuserId: id,\n\t\t\t\t\t\toperation: 'updateUser',\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tthrow new StoreConflict(\n\t\t\t\t\t'version',\n\t\t\t\t\t`updateUser: expected version ${ifVersion}, found ${stored.version}`,\n\t\t\t\t\t{\n\t\t\t\t\t\tuserId: id,\n\t\t\t\t\t\texpectedVersion: ifVersion,\n\t\t\t\t\t\tactualVersion: stored.version,\n\t\t\t\t\t\toperation: 'updateUser',\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t}),\n\n\t\tdeleteUser: (id) =>\n\t\t\trun$('deleteUser', async () => {\n\t\t\t\tconst result = await collection.raw.deleteOne({ _id: id });\n\t\t\t\treturn result.deletedCount === 1;\n\t\t\t}),\n\t};\n}\n\nfunction sessionStore(db: Db): SessionStore {\n\tconst collection = getCollection(db, sessions);\n\tconst run$ = <T>(operation: string, body: () => Promise<T>) =>\n\t\trun('sessions', operation, body);\n\n\treturn {\n\t\tinsertSession: (record) =>\n\t\t\trun$('insertSession', async () => {\n\t\t\t\tconst outcome = await settle(\n\t\t\t\t\tcollection.raw.insertOne(toSessionDocument(record)),\n\t\t\t\t);\n\t\t\t\tif (!('duplicate' in outcome)) return;\n\n\t\t\t\t// Idempotent under retry: a session with this id is already there.\n\t\t\t\tif ((await collection.findById(record.id)) !== undefined) return;\n\t\t\t\tthrow unexpectedDuplicate(\n\t\t\t\t\t'sessions',\n\t\t\t\t\t'insertSession',\n\t\t\t\t\toutcome.duplicate,\n\t\t\t\t);\n\t\t\t}),\n\n\t\tfindSessionByTokenHash: (tokenHash) =>\n\t\t\trun$('findSessionByTokenHash', async () => {\n\t\t\t\tconst found = await collection.findFirst({ tokenHash });\n\t\t\t\treturn found === undefined ? null : toSession(found);\n\t\t\t}),\n\n\t\textendSession: (id, expiresAt) =>\n\t\t\trun$('extendSession', async () => {\n\t\t\t\t// `revokedAt: null` in the filter: an extension racing a revocation\n\t\t\t\t// matches nothing, and never brings the session back.\n\t\t\t\tconst written = await collection.raw.findOneAndUpdate(\n\t\t\t\t\t{ _id: id, revokedAt: null },\n\t\t\t\t\t{ $set: { expiresAt } },\n\t\t\t\t\t{ returnDocument: 'after' },\n\t\t\t\t);\n\t\t\t\treturn written === null ? null : toSession(written);\n\t\t\t}),\n\n\t\trevokeSession: (id, at) =>\n\t\t\trun$('revokeSession', async () => {\n\t\t\t\t// A pipeline, so a session already revoked keeps its first\n\t\t\t\t// `revokedAt` and still counts as matched.\n\t\t\t\tconst result = await collection.raw.updateOne({ _id: id }, [\n\t\t\t\t\t{ $set: { revokedAt: { $ifNull: ['$revokedAt', at] } } },\n\t\t\t\t]);\n\t\t\t\treturn result.matchedCount === 1;\n\t\t\t}),\n\n\t\trevokeUserSessions: (userId, at, except) =>\n\t\t\trun$('revokeUserSessions', async () => {\n\t\t\t\tconst result = await collection.raw.updateMany(\n\t\t\t\t\texcept === undefined\n\t\t\t\t\t\t? { userId, revokedAt: null }\n\t\t\t\t\t\t: { userId, revokedAt: null, _id: { $ne: except } },\n\t\t\t\t\t{ $set: { revokedAt: at } },\n\t\t\t\t);\n\t\t\t\treturn result.modifiedCount;\n\t\t\t}),\n\n\t\tdeleteUserSessions: (userId) =>\n\t\t\trun$('deleteUserSessions', async () => {\n\t\t\t\tconst result = await collection.raw.deleteMany({ userId });\n\t\t\t\treturn result.deletedCount;\n\t\t\t}),\n\t};\n}\n\nfunction tokenStore(db: Db): TokenStore {\n\tconst collection = getCollection(db, tokens);\n\tconst run$ = <T>(operation: string, body: () => Promise<T>) =>\n\t\trun('tokens', operation, body);\n\n\treturn {\n\t\tinsertToken: (record) =>\n\t\t\trun$('insertToken', async () => {\n\t\t\t\t// `_id` is the hash, the only unique index: a duplicate is a retry.\n\t\t\t\tawait settle(collection.raw.insertOne(toTokenDocument(record)));\n\t\t\t}),\n\n\t\tconsumeToken: (tokenHash, kind, at) =>\n\t\t\trun$('consumeToken', async () => {\n\t\t\t\t// **One conditional write**, answering the document as it was\n\t\t\t\t// before it. The pipeline keeps a first `spentAt`, so exactly one\n\t\t\t\t// caller ever reads `spentAt: null`.\n\t\t\t\tconst before = await collection.raw.findOneAndUpdate(\n\t\t\t\t\t{ _id: tokenHash, kind },\n\t\t\t\t\t[{ $set: { spentAt: { $ifNull: ['$spentAt', at] } } }],\n\t\t\t\t\t{ returnDocument: 'before' },\n\t\t\t\t);\n\t\t\t\treturn before === null ? null : toToken(before);\n\t\t\t}),\n\n\t\tcountAttempt: (tokenHash, kind) =>\n\t\t\trun$('countAttempt', async () => {\n\t\t\t\t// **One conditional write**, answering the document after it: an\n\t\t\t\t// unspent token gets one more attempt, atomically. A spent one is\n\t\t\t\t// matched by the second read below, and written nothing.\n\t\t\t\tconst after = await collection.raw.findOneAndUpdate(\n\t\t\t\t\t{ _id: tokenHash, kind, spentAt: null },\n\t\t\t\t\t{ $inc: { attempts: 1 } },\n\t\t\t\t\t{ returnDocument: 'after' },\n\t\t\t\t);\n\t\t\t\tif (after !== null) return toToken(after);\n\t\t\t\tconst spent = await collection.raw.findOne({ _id: tokenHash, kind });\n\t\t\t\treturn spent === null ? null : toToken(spent);\n\t\t\t}),\n\n\t\tdeleteUserTokens: (userId) =>\n\t\t\trun$('deleteUserTokens', async () => {\n\t\t\t\tconst result = await collection.raw.deleteMany({ userId });\n\t\t\t\treturn result.deletedCount;\n\t\t\t}),\n\t};\n}\n\n// ─── Documents and records ────────────────────────────────────────────────\n//\n// Each record is rebuilt field by field, never spread from a document: a\n// spread would hand the core `_id` and the `id` getter `@nxgt/mongo` adds,\n// and the port's records are exactly what it declares.\n\nfunction toUserDocument(record: UserRecord): UserDocument {\n\treturn structuredClone({\n\t\t_id: record.id,\n\t\ttype: record.type,\n\t\tschemaVersion: record.schemaVersion,\n\t\tactive: record.active,\n\t\tfields: record.fields,\n\t\tlogins: [...record.logins],\n\t\tpassword: record.password,\n\t\tsecondFactor: record.secondFactor,\n\t\temailVerifiedAt: record.emailVerifiedAt,\n\t\tversion: record.version,\n\t\tcreatedAt: record.createdAt,\n\t\tupdatedAt: record.updatedAt,\n\t});\n}\n\n/**\n * The fields a patch names, as one `$set`. A key present with `undefined` is\n * absent, as the port says — never an erasure. `password: null` is named, and\n * removes the password.\n */\nfunction toUserSet(patch: UserPatch): Record<string, unknown> {\n\tconst set: Record<string, unknown> = { updatedAt: patch.updatedAt };\n\n\tfor (const field of [\n\t\t'schemaVersion',\n\t\t'active',\n\t\t'fields',\n\t\t'logins',\n\t\t'password',\n\t\t'secondFactor',\n\t\t'emailVerifiedAt',\n\t] as const) {\n\t\tif (patch[field] !== undefined) set[field] = patch[field];\n\t}\n\n\treturn structuredClone(set);\n}\n\nfunction toUser(document: UserDocument): UserRecord {\n\tconst password = document.password;\n\tconst secondFactor = document.secondFactor ?? null;\n\treturn {\n\t\tid: document._id as Id,\n\t\ttype: document.type,\n\t\tschemaVersion: document.schemaVersion,\n\t\tactive: document.active,\n\t\tfields: document.fields as UserRecord['fields'],\n\t\tlogins: [...document.logins],\n\t\tpassword:\n\t\t\tpassword === null\n\t\t\t\t? null\n\t\t\t\t: { hash: password.hash, updatedAt: password.updatedAt },\n\t\tsecondFactor:\n\t\t\tsecondFactor === null\n\t\t\t\t? null\n\t\t\t\t: {\n\t\t\t\t\t\tmethod: secondFactor.method,\n\t\t\t\t\t\tsecret: secondFactor.secret,\n\t\t\t\t\t\tconfirmedAt: secondFactor.confirmedAt,\n\t\t\t\t\t\tlastStep: secondFactor.lastStep,\n\t\t\t\t\t},\n\t\temailVerifiedAt: document.emailVerifiedAt,\n\t\tversion: document.version,\n\t\tcreatedAt: document.createdAt,\n\t\tupdatedAt: document.updatedAt,\n\t};\n}\n\nfunction toSessionDocument(record: SessionRecord): SessionDocument {\n\treturn {\n\t\t_id: record.id,\n\t\ttokenHash: record.tokenHash,\n\t\tuserId: record.userId,\n\t\tauthenticatedAt: record.authenticatedAt,\n\t\texpiresAt: record.expiresAt,\n\t\trevokedAt: record.revokedAt,\n\t\tcreatedAt: record.createdAt,\n\t};\n}\n\nfunction toSession(document: SessionDocument): SessionRecord {\n\treturn {\n\t\tid: document._id,\n\t\ttokenHash: document.tokenHash,\n\t\tuserId: document.userId as Id,\n\t\tauthenticatedAt: document.authenticatedAt,\n\t\texpiresAt: document.expiresAt,\n\t\trevokedAt: document.revokedAt,\n\t\tcreatedAt: document.createdAt,\n\t};\n}\n\nfunction toTokenDocument(record: TokenRecord): TokenDocument {\n\treturn {\n\t\t_id: record.tokenHash,\n\t\tkind: record.kind,\n\t\tuserId: record.userId,\n\t\taddress: record.address,\n\t\tcodeHash: record.codeHash,\n\t\tattempts: record.attempts,\n\t\texpiresAt: record.expiresAt,\n\t\tspentAt: record.spentAt,\n\t\tcreatedAt: record.createdAt,\n\t};\n}\n\nfunction toToken(document: TokenDocument): TokenRecord {\n\treturn {\n\t\ttokenHash: document._id,\n\t\tkind: document.kind,\n\t\tuserId: document.userId as Id,\n\t\taddress: document.address,\n\t\tcodeHash: document.codeHash ?? null,\n\t\tattempts: document.attempts ?? 0,\n\t\texpiresAt: document.expiresAt,\n\t\tspentAt: document.spentAt,\n\t\tcreatedAt: document.createdAt,\n\t};\n}\n"
10
10
  ],
11
- "mappings": "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",
12
- "debugId": "55FFE47757FDC9B264756E2164756E21",
11
+ "mappings": "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",
12
+ "debugId": "2CC7BD3E8A25C38E64756E2164756E21",
13
13
  "names": []
14
14
  }
package/docs/README.md CHANGED
@@ -6,6 +6,6 @@ give the detail.
6
6
  | Page | Read it when |
7
7
  | --- | --- |
8
8
  | [Wiring the stores and the relations](guide/wiring.md) | You are passing MongoDB to `janus()` and `permissions()`, mixing adapters, or handling what a failure throws |
9
- | [Collections and indexes](guide/sync.md) | You are creating the collections and indexes at deploy time, checking them in CI, or want to know what the database holds |
9
+ | [Collections and indexes](guide/sync.md) | You are creating the collections and indexes at deploy time, upgrading to a version that adds a field, checking them in CI, or want to know what the database holds |
10
10
  | [Troubleshooting](troubleshooting.md) | You have an error message and want its cause and its fix |
11
11
  | [Roadmap](roadmap.md) | You want to know what is coming, what shipped, and what is deliberately not planned |
@@ -93,13 +93,21 @@ encoded: a document read in a shell reads like the record in the code.
93
93
 
94
94
  | Collection | Holds | Indexes |
95
95
  | --- | --- | --- |
96
- | `users` | one user: `type`, `fields`, `logins`, `password`, `emailVerifiedAt`, `version`, … | `loginUnique` on `{ type, logins }`, unique — a login is unique **per user type** · `typeId` on `{ type, _id }`, for listing |
96
+ | `users` | one user: `type`, `fields`, `logins`, `password`, `secondFactor`, `emailVerifiedAt`, `version`, … | `loginUnique` on `{ type, logins }`, unique — a login is unique **per user type** · `typeId` on `{ type, _id }`, for listing |
97
97
  | `sessions` | one session, by the `sha256` of its token | `tokenHashUnique` · `userId` · `expiry`, a TTL index |
98
- | `tokens` | one one-time token; `_id` **is** its `sha256` | `userId` · `expiry`, a TTL index |
98
+ | `tokens` | one one-time token; `_id` **is** its `sha256`, beside `kind`, `codeHash`, `attempts`, … | `userId` · `expiry`, a TTL index |
99
99
  | `relations` | one tuple; `_id` **is** the tuple, `{ object: { type, id }, relation, subject: { type, id, relation? } }` | `objectRelation`, one hop forwards · `subjectObjects`, the reverse index `list()` walks |
100
100
 
101
101
  - **No secret is stored.** Sessions and tokens hold the `sha256` of the
102
- secret; passwords a self-describing hash (`$scrypt$…`, `$argon2id$…`).
102
+ secret; passwords a self-describing hash (`$scrypt$…`, `$argon2id$…`); a
103
+ second factor's `secret` a TOTP secret `@nxgt/janus` has already sealed
104
+ with your application's key, kept byte for byte. Its key id is the second
105
+ part of the value, so `{ 'secondFactor.secret': { $regex: '^v1\\.<id>\\.' } }`
106
+ finds the secrets still sealed with a key being
107
+ [rotated out](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/second-factor.md#rotating-the-keys).
108
+ - **Documents written by an earlier version need no migration.** A user
109
+ without `secondFactor` reads as having none; a token without `codeHash` or
110
+ `attempts` reads as `null` and `0`.
103
111
  - **Ids are strings**, the UUIDv7s the core minted — never an `ObjectId`: the
104
112
  store mints nothing.
105
113
  - **A tuple is unique by construction**, since it is its own `_id`, so writing
@@ -111,6 +119,27 @@ encoded: a document read in a shell reads like the record in the code.
111
119
  Give the adapter a database of its own — `client.db('janus')` — so they
112
120
  never meet a collection of your application's.
113
121
 
122
+ ## Upgrading: sync before you deploy
123
+
124
+ The validators are `additionalProperties: false`, `strict`, `error`. When a
125
+ version adds a field, the validator the previous sync wrote refuses it: from
126
+ 0.2 to 0.3, `secondFactor` on every new user and `codeHash` and `attempts` on
127
+ every new token. Run the sync with the new version **before** the code that
128
+ writes them:
129
+
130
+ ```ts
131
+ import { syncMongoStores } from '@nxgt/janus-mongo';
132
+
133
+ const reports = await syncMongoStores(db, { dryRun: true }); // look first
134
+ for (const report of reports) console.log(report.name, report);
135
+ await syncMongoStores(db); // then write the validators
136
+ ```
137
+
138
+ The new fields are optional in the validator, so the previous version keeps
139
+ working against it while the deployment rolls. Deployed before the sync,
140
+ every sign-up and every one-time token fails with `STORE_FAILED`, caused by
141
+ `Document failed validation` (code 121). No document is rewritten either way.
142
+
114
143
  ## The TTL indexes are not the expiry
115
144
 
116
145
  MongoDB's TTL monitor runs every sixty seconds, so a lapsed session can stay
package/docs/roadmap.md CHANGED
@@ -38,10 +38,17 @@ Nothing yet.
38
38
 
39
39
  ## Shipped
40
40
 
41
- The first public release, v0.1.
41
+ Each entry names the version it came in; `CHANGELOG.md` holds the rest.
42
42
 
43
+ - **A second factor on a user, and attempts counted on a token** — the stores
44
+ keep a user's `secondFactor` and a token's `codeHash` and `attempts`, and
45
+ `countAttempt` counts an attempt at a code in one conditional write, for
46
+ `@nxgt/janus` 0.4. Documents written before read as no second factor and no
47
+ attempt; run the sync before deploying. — v0.3.0
48
+ - **The permission model written with `related` and `permits`**, the keys of
49
+ `@nxgt/janus` 0.2, which it requires. — v0.2.0
43
50
  - **`LOGIN_TAKEN` no longer quotes the login in its message**; `error.login`
44
- still names it. — next patch
51
+ still names it. — v0.2.0
45
52
  - **One call for both sides** — `createMongoAdapter(db)` answers
46
53
  `{ store, relations }`, spread into `janus()` so deleting a user deletes
47
54
  their tuples without a second wiring; `syncMongoAdapter(db)` syncs the four
@@ -125,7 +125,7 @@ idempotent, so retry it yourself.
125
125
 
126
126
  **When:** a write — `insertUser`, `updateUser`, `insertSession`, … — rejected by the collection's `$jsonSchema` validator (MongoDB error code 121).
127
127
  **Why:** the core validates every record before it reaches the store, so a validator refusing one means the collection and the adapter disagree: the collection's validator was set by another version of this adapter, or a collection of the same name (`users`, `sessions`, `tokens`, `relations`) already belonged to your application, with its own validator. It is never the caller's fault, and never a 400.
128
- **Fix:** after upgrading this package, run `syncMongoStores(db)` again. If your application already has a `users` collection, the adapter is sharing its database: give it one of its own, `janus`:
128
+ **Fix:** after upgrading this package, run `syncMongoStores(db)` again — upgrading to 0.3 is exactly this case: the validator the previous sync wrote refuses `secondFactor`, `codeHash` and `attempts` until it runs. See [sync before you deploy](guide/sync.md#upgrading-sync-before-you-deploy). If your application already has a `users` collection, the adapter is sharing its database: give it one of its own, `janus`:
129
129
 
130
130
  ```ts
131
131
  const auth = janus({ ..., store: createMongoStores(client.db('janus')) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/janus-mongo",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "The MongoDB adapter for @nxgt/janus: users, sessions, one-time tokens and permission tuples",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -48,7 +48,7 @@
48
48
  ]
49
49
  },
50
50
  "devDependencies": {
51
- "@nxgt/janus": "^0.3.0",
51
+ "@nxgt/janus": "^0.5.0",
52
52
  "@nxgt/mongo": "^0.17.1",
53
53
  "@types/bun": "^1.4.2",
54
54
  "mongodb": "^7.6.0",
@@ -56,7 +56,7 @@
56
56
  "zod": "^4.6.5"
57
57
  },
58
58
  "peerDependencies": {
59
- "@nxgt/janus": "^0.3.0",
59
+ "@nxgt/janus": "^0.5.0",
60
60
  "@nxgt/mongo": ">=0.17.0 <1",
61
61
  "mongodb": ">=7.0.0 <8",
62
62
  "typescript": "^6.0.3",