Models
oauth.models // { Client, Grant, Code, Token, Request, Key, Audit }Seven Mongoose models, on your connection. An escape hatch — prefer the admin API, which carries the invariants these do not. See Data model for what each collection holds and why.
| Handle | Default model name | Default collection | Document type |
|---|---|---|---|
Client | OAuthClient | oauth_clients | OAuthClientDoc |
Grant | OAuthGrant | oauth_grants | OAuthGrantDoc |
Code | OAuthCode | oauth_codes | OAuthCodeDoc |
Token | OAuthToken | oauth_tokens | OAuthTokenDoc |
Request | OAuthRequest | oauth_requests | OAuthRequestDoc |
Key | OAuthKey | oauth_keys | OAuthKeyDoc |
Audit | OAuthAudit | oauth_audit | OAuthAuditDoc |
Model names are set by config.modelNames, collection names by config.collectionPrefix — the two are independent.
syncIndexes()
await oauth.syncIndexes();Awaits Model.init() on all seven. Call it at boot, before the first write. Mongoose builds indexes in the background, and on a cold database that is exactly long enough for a write to land before the unique partial index on grants exists.
Resolving it also flips oauth.ready. Until then the three routes that write — /authorize, POST /consent/:requestId, POST /token — answer 503 server_error naming this function, while the read-only surfaces keep serving. See Data model.
Reading them safely
The models have no guards attached. Two things the admin API does that a direct query does not:
- Projection.
Clientdocuments carrysecrets[].hashandpairwiseSubjects. A rawfind()hands you both. Everything that leaves the admin API goes through aPublicClientprojection. - Cascade. Setting
revokedAton a grant by hand leaves every access token under it working until introspection next reads the grant — which it does, so the effect arrives, but notrackevent fires, no audit row is written, andtokensRevokedis never counted. Usegrants.revoke().
Safe uses: counts, dashboards, one-off inspection.
await oauth.models.Grant.countDocuments({ revokedAt: null });
await oauth.models.Audit.find({ type: 'refresh_reuse_detected' }).sort({ createdAt: -1 }).limit(50);
await oauth.models.Client.findOne({ clientId }, { 'secrets.lastUsedAt': 1, 'secrets.label': 1 });That last one is the supported way to watch a secret rotation land.
createModels(opts)
Build the models without constructing a host. Useful for a migration script or a test harness.
import { createModels, syncModelIndexes } from '@jeffjassky/oauth-host';
const models = createModels({
connection: mongoose, // defaults to the global mongoose
modelNames: { grant: 'ThirdPartyGrant' },
collectionPrefix: 'oauth_',
auditRetentionDays: 400,
});
await syncModelIndexes(models);Both are exported from the package root. auditRetentionDays is baked into the TTL index on oauth_audit.createdAt at schema-build time, so it has to match whatever the running host uses.
Name collisions
The factory reuses an already-compiled model when the name is taken, because the Mongoose registry is process-global and a library cannot own seven global names unconditionally — you may already have a Grant, or the package may be loaded twice through a hoisting mismatch.
That is right for a double-load and wrong for a genuine collision: if the existing model is not this package's, queries fail confusingly rather than loudly. Set modelNames when a collision is plausible. See Name collisions.