@nxgt/janus-mongo

中文文档
0.13.0发布于 2026/11/02MITTypeScriptES Module零依赖

The MongoDB adapter for @nxgt/janus: users, sessions, one-time tokens and permission tuples

原始使用文档
文档目录7 节
  1. Install
  2. API
  3. What the database holds
  4. Traps
  5. Documentation
  6. Type safety, counted
  7. Licence

@nxgt/janus-mongo

The MongoDB adapter for @nxgt/janus: its identity stores — users, sessions and one-time tokens — the relation store of @nxgt/janus/permissions, and the opt-in API key store, over one database, on @nxgt/mongo.

It passes both @nxgt/janus/conformance suites against a real mongod, outages included. createMongoAdapter(db) gives both sides under the names janus() takes them, so one spread wires them; each is also usable alone, as in @nxgt/janus: createMongoStores for identities, createMongoRelations for permissions.

import { janus, scryptHasher } from '@nxgt/janus';
import { permissions } from '@nxgt/janus/permissions';
import { createMongoAdapter, syncMongoAdapter } from '@nxgt/janus-mongo';

await syncMongoAdapter(db); // a deployment step: creates the collections and indexes (API keys: syncMongoApiKeys, apart)

const mongo = createMongoAdapter(db); // { store, relations }

export const auth = janus({
  user: User,
  password: { login: 'email' },
  hasher: scryptHasher(),
  ...mongo, // deleting a user deletes their tuples too
});

export const access = permissions({ model, store: mongo.relations });

Install

bun add @nxgt/janus-mongo @nxgt/janus @nxgt/mongo mongodb zod
bun add -d typescript

Every peer is required: @nxgt/janus, @nxgt/mongo (>=0.17.0 <1), mongodb (7), zod (4.6.5 or later, which @nxgt/mongo defines collections with) and typescript (6 or 7). @nxgt/janus is a peer, never a dependency: this package defines no error class and throws the peer's own, so instanceof StoreFailure holds in your code. Like it, this package expects "moduleResolution": "bundler": the declarations import without extensions, so nodenext is not supported.

The @nxgt/mongo floor is tested, not claimed: the package's specs and its typecheck run on @nxgt/mongo 0.17.0 as well, in the Floors job, on every CI run.

API

ExportWhat it is
createMongoAdapter(db){ store, relations }: both of the below, keyed as janus() takes them, so janus({ …, ...mongo }) wires both. Connects to nothing.
syncMongoAdapter(db, options?)syncMongoStores and syncMongoRelations in one step: the four collections. It does not create apiKeys. syncAll(db) of @nxgt/mongo creates five (users, sessions, tokens, relations and apiKeys), since importing the package registers all five definitions.
MongoAdapterThe type of what createMongoAdapter answers.
createMongoStores(db)The { users, sessions, tokens } that janus() takes as store. Connects to nothing. Implements the optional users.findUsers, so auth.<type>.findMany(ids) is one $in query.
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.
users, sessions, tokensThe @nxgt/mongo definitions. Defining them registers them, so syncAll(db) syncs them with your own collections.
janusCollectionsThe three definitions, in sync order.
createMongoRelations(db)The RelationStore that permissions() takes as store, and janus() as relations. Connects to nothing.
syncMongoRelations(db, options?)Creates the relations collection and its indexes. A separate step: an application that uses identities alone keeps no tuples.
relationsIts @nxgt/mongo definition.
createMongoApiKeys(db)The ApiKeyStore of @nxgt/janus' opt-in API key port. Opt-in, so it is not part of createMongoAdapter. Connects to nothing.
syncMongoApiKeys(db, options?)Creates the apiKeys collection and its indexes. A separate step, like syncMongoRelations: syncMongoAdapter does not run it.
apiKeysIts @nxgt/mongo definition. Importing the package registers it, so syncAll(db) creates it.

What the database holds

The port's records, with the id under _id and nothing else renamed or encoded. A document read in a shell reads like the record in the code.

CollectionIndexes
usersloginUnique on { type, logins }, unique — a login is unique per user type · typeId on { type, _id } for listing
sessionstokenHashUnique · userId · expiry, a TTL index
tokens_id is the token's hash · userId · expiry, a TTL index
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)
apiKeys_id is the key's id · userId on { userId, _id }, for listing a user's keys · expiry, a TTL index that drops a key 30 days after expiresAt (the grace keeps a lapsed key listable as "expired")

No secret is stored: sessions and tokens hold sha256 of the secret, passwords a self-describing hash, and a user's secondFactor.secret a TOTP secret @nxgt/janus has already sealed with your application's key — v1.<key id>.… — which the store keeps byte for byte. Its secondFactor.recoveryCodes are keyed hashes of the recovery codes, never a code, kept in order; a factor without the field reads as having none.

countAttempt counts an attempt at a code in one findOneAndUpdate with $inc, so twenty concurrent attempts answer twenty distinct counts. spendUserTokens is one updateMany on { userId, kind, spentAt: null } — and _id: { $ne: except } when except is given — through the index deleteUserTokens already reads: each token it spends is matched and written in one step, so a racing redemption and it never both spend the same one.

Traps

  • Read from the primary. @nxgt/janus counts on a read seeing every write that completed before it: a sign-in re-reads the user to catch a password written while it ran. A db whose client reads from secondaries (readPreference: 'secondaryPreferred') misses that write, and the sign-in goes through. Hand the adapter a db that reads from the primary — the driver's default:

    import { MongoClient } from 'mongodb';
    
    const client = new MongoClient(url); // readPreference 'primary' by default
    const adapter = createMongoAdapter(client.db('app'));
    
  • Upgrading to 0.3: run the sync before deploying. No document is rewritten — a user without secondFactor reads as having none, a token without codeHash and attempts as null and 0 — but the validators the previous sync wrote refuse the new fields. Until syncMongoAdapter(db) has run, every sign-up and every one-time token fails with STORE_FAILED, caused by Document failed validation. Upgrading has the steps.

  • Upgrading to 0.5: run the sync before deploying, again. No document is rewritten — a second factor without recoveryCodes reads as having none — but the validator the previous sync wrote refuses the new field. Until syncMongoAdapter(db) has run, every write of a second factor — enroll, activate, a confirmed code — fails with STORE_FAILED, caused by Document failed validation. And finish the rollout before users hold recovery codes: an instance still on 0.4 writes a factor back without them when it accepts a code.

  • Upgrading to 0.6: run the sync before deploying, once more. The token kind stepUp is new, and the kind enum the previous sync wrote refuses it: until syncMongoAdapter(db) has run, every step-up request fails with STORE_FAILED, caused by Document failed validation. No document is rewritten.

  • Upgrading to 0.7: the sync again, for the same reason. The token kind magicLink — @nxgt/janus 0.15's sign-in link — is new: until the sync has run, every magicLink.request fails with STORE_FAILED, caused by Document failed validation. No document is rewritten.

  • Upgrading to 0.9: the sync again, for the same reason. The token kind changeEmail — @nxgt/janus 0.23's link to a new address — is new: until the sync has run, the first write of such a token fails with STORE_FAILED, caused by Document failed validation. No document is rewritten.

  • Upgrading to 0.10: run the sync before deploying. The avatar slot — @nxgt/janus 0.29's — is new, and the strict validator the previous sync wrote refuses it: until syncMongoAdapter(db) has run, only a write that sets an avatar fails, with STORE_FAILED, caused by Document failed validation. Sign-up and every other write pass, and no document is rewritten. The sync guide has the detail.

  • The TTL indexes are storage hygiene, not the expiry. MongoDB's TTL monitor runs every sixty seconds, so a lapsed session can stay readable for up to a minute. The core compares expiresAt on every read, and that is what expires it.

  • auth.collectExpired() answers UNSUPPORTED with this adapter. The TTL indexes already drop lapsed sessions and tokens, so deleteExpiredSessions and deleteExpiredTokens are deliberately not implemented.

  • No sync function is called for you — syncMongoAdapter, or syncMongoStores for identities alone. The core never manages a schema. Without it there is no unique index, and nothing stops two concurrent sign-ups with one login.

  • A duplicate key on an index other than the login index is a StoreFailure, not LOGIN_TAKEN. Such a duplicate is an adapter bug, and reporting it as a taken login would tell somebody their e-mail is in use when it is not.

  • A relation write of more than one tuple is a transaction, and MongoDB runs transactions on a replica set only. grant and revoke write one tuple and run anywhere; on a standalone mongod a multi-tuple write fails with STORE_FAILED. The transaction is not retried: the driver's withTransaction would retry an outage for two minutes before answering, and a write is idempotent, so retry it yourself.

  • findObjects orders ids as MongoDB does, by UTF-8 bytes. list() sorts what it gathers itself, so its pages do not depend on it; a caller of the store directly sees bytes order, which differs from JavaScript's < only past U+FFFF.

Documentation

  • Guides — wiring the stores, syncing the collections
  • Troubleshooting — by the error message you see
  • Roadmap — what is next, and what is not planned

Type safety, counted

Eleven plausible mistakes are refused by the compiler, each with a @ts-expect-error case in test/types/adapter.ts:

  • a connection string instead of a database;
  • @nxgt/mongo's connection instead of its db, to createMongoAdapter;
  • the driver's MongoClient instead of one of its databases;
  • @nxgt/mongo's connection instead of its db, to syncMongoAdapter;
  • the same, to syncMongoRelations;
  • the promise connectMongo answers, not awaited;
  • a sync option in the wrong case: dryrun for dryRun;
  • the whole adapter as janus()'s store, instead of spreading it;
  • the relation store as janus()'s store;
  • the whole adapter as permissions()'s store, instead of mongo.relations;
  • the identity stores as permissions()'s store.

Licence

MIT

版本与发布记录

@nxgt/janus-mongo 共发布 41 个版本,首次发布于 2026/09/24;过去 12 个月发布了 41 个版本。

版本发布日期距今
0.13.0latest2026/11/021 天前
0.12.02026/11/021 天前
0.11.42026/10/304 天前
0.11.32026/10/276 天前
0.11.22026/10/277 天前
0.11.12026/10/277 天前

关于 @nxgt/janus-mongo 的常见问题

@nxgt/janus-mongo 是什么?

用于@nxgt/janus的MongoDB适配器:用户、会话、一次性令牌和权限元组

如何安装 @nxgt/janus-mongo?

在项目目录运行 npm install @nxgt/janus-mongo,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:zod(>=4.6.5 <5)、mongodb(>=7.0.0 <8)、typescript(^6.0.3 || ^7.0.0)、@nxgt/janus(^0.36.0)、@nxgt/mongo(>=0.17.0 <1)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@nxgt/janus-mongo 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@nxgt/janus-mongo 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@nxgt/janus-mongo 可以商用吗?

@nxgt/janus-mongo 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@nxgt/janus-mongo 还在维护吗?

最新版本 0.13.0 发布于 2026/11/02(1 天前);至今共发布 41 个版本,其中过去 12 个月发布了 41 个;首次发布于 2026/09/24。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。