@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 +21 -0
- package/README.md +106 -0
- package/dist/collections.d.ts +75 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +480 -0
- package/dist/index.js.map +13 -0
- package/dist/relations.d.ts +54 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/stores.d.ts +37 -0
- package/dist/stores.d.ts.map +1 -0
- package/dist/translate.d.ts +58 -0
- package/dist/translate.d.ts.map +1 -0
- package/docs/README.md +11 -0
- package/docs/guide/sync.md +122 -0
- package/docs/guide/wiring.md +159 -0
- package/docs/roadmap.md +54 -0
- package/docs/troubleshooting.md +193 -0
- package/package.json +65 -0
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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|