@nxgt/janus-mongo 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steve Tsala
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # @nxgt/janus-mongo
2
+
3
+ The MongoDB adapter for [`@nxgt/janus`](https://www.npmjs.com/package/@nxgt/janus): its three stores —
4
+ users, sessions and one-time tokens — and the relation store of
5
+ `@nxgt/janus/permissions`, over one database, on
6
+ [`@nxgt/mongo`](https://www.npmjs.com/package/@nxgt/mongo).
7
+
8
+ It passes both `@nxgt/janus/conformance` suites against a real mongod, outages
9
+ included.
10
+
11
+ ```ts
12
+ import { janus, scryptHasher } from '@nxgt/janus';
13
+ import { permissions } from '@nxgt/janus/permissions';
14
+ import {
15
+ createMongoRelations,
16
+ createMongoStores,
17
+ syncMongoRelations,
18
+ syncMongoStores,
19
+ } from '@nxgt/janus-mongo';
20
+
21
+ await syncMongoStores(db); // a deployment step: creates the collections and indexes
22
+
23
+ export const auth = janus({
24
+ user: User,
25
+ password: { login: 'email' },
26
+ store: createMongoStores(db),
27
+ hasher: scryptHasher(),
28
+ relations: createMongoRelations(db), // deleting a user deletes their tuples
29
+ });
30
+
31
+ await syncMongoRelations(db);
32
+ export const access = permissions({ model, store: createMongoRelations(db) });
33
+ ```
34
+
35
+ ## Install
36
+
37
+ ```sh
38
+ bun add @nxgt/janus-mongo @nxgt/janus @nxgt/mongo mongodb zod
39
+ ```
40
+
41
+ Every peer is required: `@nxgt/janus`, `@nxgt/mongo` (`>=0.17 <1`), `mongodb`
42
+ (7), `zod` (4.6.5 or later, which `@nxgt/mongo` defines collections with) and `typescript`
43
+ (6). `@nxgt/janus` is a **peer**, never a dependency: this package defines no
44
+ error class and throws the peer's own, so `instanceof StoreFailure` holds in
45
+ your code. Like it, this package expects `"moduleResolution": "bundler"`: the
46
+ declarations import without extensions, so `nodenext` is not supported.
47
+
48
+ ## API
49
+
50
+ | Export | What it is |
51
+ | --- | --- |
52
+ | `createMongoStores(db)` | The `{ users, sessions, tokens }` that `janus()` takes as `store`. Connects to nothing. |
53
+ | `syncMongoStores(db, options?)` | Creates the three collections, their validators and indexes, and answers what it changed. Needs `dbAdmin`; run it when you deploy, never per request. |
54
+ | `users`, `sessions`, `tokens` | The `@nxgt/mongo` definitions. Defining them registers them, so `syncAll(db)` syncs them with your own collections. |
55
+ | `janusCollections` | The three definitions, in sync order. |
56
+ | `createMongoRelations(db)` | The `RelationStore` that `permissions()` takes as `store`, and `janus()` as `relations`. Connects to nothing. |
57
+ | `syncMongoRelations(db, options?)` | Creates the `relations` collection and its indexes. A separate step: an application that only authenticates keeps no tuples. |
58
+ | `relations` | Its `@nxgt/mongo` definition. |
59
+
60
+ ## What the database holds
61
+
62
+ The port's records, with the id under `_id` and nothing else renamed or
63
+ encoded. A document read in a shell reads like the record in the code.
64
+
65
+ | Collection | Indexes |
66
+ | --- | --- |
67
+ | `users` | `loginUnique` on `{ type, logins }`, unique — a login is unique **per user type** · `typeId` on `{ type, _id }` for listing |
68
+ | `sessions` | `tokenHashUnique` · `userId` · `expiry`, a TTL index |
69
+ | `tokens` | `_id` is the token's hash · `userId` · `expiry`, a TTL index |
70
+ | `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) |
71
+
72
+ No secret is stored: sessions and tokens hold `sha256` of the secret, and
73
+ passwords a self-describing hash.
74
+
75
+ ## Traps
76
+
77
+ - **The TTL indexes are storage hygiene, not the expiry.** MongoDB's TTL monitor
78
+ runs every sixty seconds, so a lapsed session can stay readable for up to a
79
+ minute. The core compares `expiresAt` on every read, and that is what expires
80
+ it.
81
+ - **`auth.collectExpired()` answers `UNSUPPORTED`** with this adapter. The TTL
82
+ index already drops lapsed sessions, so `deleteExpiredSessions` is
83
+ deliberately not implemented.
84
+ - **`syncMongoStores` is not called for you.** The core never manages a schema.
85
+ Without it there is no unique index, and nothing stops two concurrent sign-ups
86
+ with one login.
87
+ - **A duplicate key on an index other than the login index is a
88
+ `StoreFailure`, not `LOGIN_TAKEN`.** Such a duplicate is an adapter bug, and
89
+ reporting it as a taken login would tell somebody their e-mail is in use when
90
+ it is not.
91
+ - **A relation write of more than one tuple is a transaction**, and MongoDB runs
92
+ transactions on a replica set only. `grant` and `revoke` write one tuple and
93
+ run anywhere; on a standalone mongod a multi-tuple write fails with
94
+ `STORE_FAILED`. The transaction is not retried: the driver's `withTransaction`
95
+ would retry an outage for two minutes before answering, and a write is
96
+ idempotent, so retry it yourself.
97
+ - **`findObjects` orders ids as MongoDB does, by UTF-8 bytes.** `list()` sorts
98
+ what it gathers itself, so its pages do not depend on it; a caller of the
99
+ store directly sees bytes order, which differs from JavaScript's `<` only past
100
+ U+FFFF.
101
+
102
+ ## Documentation
103
+
104
+ - [Guides](docs/README.md) — wiring the stores, syncing the collections
105
+ - [Troubleshooting](docs/troubleshooting.md) — by the error message you see
106
+ - [Roadmap](docs/roadmap.md) — what is next, and what is not planned
@@ -0,0 +1,75 @@
1
+ import { z } from 'zod';
2
+ export declare const users: import("@nxgt/mongo").CollectionDefinition<import("@nxgt/mongo").StampedSchema<{
3
+ _id: z.ZodString;
4
+ type: z.ZodString;
5
+ schemaVersion: z.ZodString;
6
+ active: z.ZodBoolean;
7
+ fields: z.ZodRecord<z.ZodString, z.ZodUnknown>;
8
+ logins: z.ZodArray<z.ZodString>;
9
+ password: z.ZodNullable<z.ZodObject<{
10
+ hash: z.ZodString;
11
+ updatedAt: z.ZodDate;
12
+ }, z.core.$strip>>;
13
+ emailVerifiedAt: z.ZodNullable<z.ZodDate>;
14
+ version: z.ZodInt;
15
+ createdAt: z.ZodDate;
16
+ updatedAt: z.ZodDate;
17
+ }, false, false, false, false>, import("@nxgt/mongo").StampNamesOf<false, false, false, false>>;
18
+ export declare const sessions: import("@nxgt/mongo").CollectionDefinition<import("@nxgt/mongo").StampedSchema<{
19
+ _id: z.ZodString;
20
+ tokenHash: z.ZodString;
21
+ userId: z.ZodString;
22
+ authenticatedAt: z.ZodDate;
23
+ expiresAt: z.ZodDate;
24
+ revokedAt: z.ZodNullable<z.ZodDate>;
25
+ createdAt: z.ZodDate;
26
+ }, false, false, false, false>, import("@nxgt/mongo").StampNamesOf<false, false, false, false>>;
27
+ export declare const tokens: import("@nxgt/mongo").CollectionDefinition<import("@nxgt/mongo").StampedSchema<{
28
+ _id: z.ZodString;
29
+ kind: z.ZodEnum<{
30
+ verifyEmail: "verifyEmail";
31
+ resetPassword: "resetPassword";
32
+ }>;
33
+ userId: z.ZodString;
34
+ address: z.ZodString;
35
+ expiresAt: z.ZodDate;
36
+ spentAt: z.ZodNullable<z.ZodDate>;
37
+ createdAt: z.ZodDate;
38
+ }, false, false, false, false>, import("@nxgt/mongo").StampNamesOf<false, false, false, false>>;
39
+ /** The three definitions, in the order `syncMongoStores` syncs them. */
40
+ export declare const janusCollections: readonly [import("@nxgt/mongo").CollectionDefinition<import("@nxgt/mongo").StampedSchema<{
41
+ _id: z.ZodString;
42
+ type: z.ZodString;
43
+ schemaVersion: z.ZodString;
44
+ active: z.ZodBoolean;
45
+ fields: z.ZodRecord<z.ZodString, z.ZodUnknown>;
46
+ logins: z.ZodArray<z.ZodString>;
47
+ password: z.ZodNullable<z.ZodObject<{
48
+ hash: z.ZodString;
49
+ updatedAt: z.ZodDate;
50
+ }, z.core.$strip>>;
51
+ emailVerifiedAt: z.ZodNullable<z.ZodDate>;
52
+ version: z.ZodInt;
53
+ createdAt: z.ZodDate;
54
+ updatedAt: z.ZodDate;
55
+ }, false, false, false, false>, import("@nxgt/mongo").StampNamesOf<false, false, false, false>>, import("@nxgt/mongo").CollectionDefinition<import("@nxgt/mongo").StampedSchema<{
56
+ _id: z.ZodString;
57
+ tokenHash: z.ZodString;
58
+ userId: z.ZodString;
59
+ authenticatedAt: z.ZodDate;
60
+ expiresAt: z.ZodDate;
61
+ revokedAt: z.ZodNullable<z.ZodDate>;
62
+ createdAt: z.ZodDate;
63
+ }, false, false, false, false>, import("@nxgt/mongo").StampNamesOf<false, false, false, false>>, import("@nxgt/mongo").CollectionDefinition<import("@nxgt/mongo").StampedSchema<{
64
+ _id: z.ZodString;
65
+ kind: z.ZodEnum<{
66
+ verifyEmail: "verifyEmail";
67
+ resetPassword: "resetPassword";
68
+ }>;
69
+ userId: z.ZodString;
70
+ address: z.ZodString;
71
+ expiresAt: z.ZodDate;
72
+ spentAt: z.ZodNullable<z.ZodDate>;
73
+ createdAt: z.ZodDate;
74
+ }, false, false, false, false>, import("@nxgt/mongo").StampNamesOf<false, false, false, false>>];
75
+ //# sourceMappingURL=collections.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `@nxgt/janus-mongo` — the MongoDB adapter for `@nxgt/janus`.
3
+ *
4
+ * Implements all three stores of the `@nxgt/janus` store port, and the relation
5
+ * store of `@nxgt/janus/permissions`, over one database, on `@nxgt/mongo`. It defines **no error class**: every refusal is `@nxgt/janus`'s
6
+ * own, from the peer the application installed, so `instanceof` holds across
7
+ * the two packages.
8
+ */
9
+ export { janusCollections, sessions, tokens, users } from './collections';
10
+ export { createMongoRelations, relations, syncMongoRelations, } from './relations';
11
+ export { createMongoStores, syncMongoStores } from './stores';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EACN,oBAAoB,EACpB,SAAS,EACT,kBAAkB,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC"}