/** * MongoDB adapter for oidc-provider. * * Stores OIDC tokens, sessions, and grants in an `oidc_payloads` collection * with TTL indexes for automatic cleanup. Uses the existing Mongoose connection. */ import mongoose from "mongoose"; import { connectDB } from "./mongoose.js"; const collectionName = "oidc_payloads"; type MongoPayload = { _id: string; payload: Record; expiresAt?: Date; userCode?: string; uid?: string; grantId?: string; }; let collectionReady = false; async function getCollection() { await connectDB(); const db = mongoose.connection.db!; const col = db.collection(collectionName); if (!collectionReady) { // TTL index — MongoDB automatically removes documents after expiresAt await col .createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }) .catch(() => {}); // Lookup indexes await col.createIndex({ "payload.grantId": 1 }).catch(() => {}); await col.createIndex({ "payload.userCode": 1 }).catch(() => {}); await col.createIndex({ "payload.uid": 1 }).catch(() => {}); collectionReady = true; } return col; } function prefixedId(model: string, id: string) { return `${model}:${id}`; } export class MongoAdapter { model: string; constructor(model: string) { this.model = model; } async upsert( id: string, payload: Record, expiresIn: number ) { const col = await getCollection(); const expiresAt = expiresIn ? new Date(Date.now() + expiresIn * 1000) : undefined; await col.updateOne( { _id: prefixedId(this.model, id) as any }, { $set: { payload, ...(expiresAt ? { expiresAt } : {}), }, }, { upsert: true } ); } async find(id: string) { const col = await getCollection(); const doc = await col.findOne({ _id: prefixedId(this.model, id) as any }); if (!doc) return undefined; return doc.payload; } async findByUserCode(userCode: string) { const col = await getCollection(); const doc = await col.findOne({ "payload.userCode": userCode }); if (!doc) return undefined; return doc.payload; } async findByUid(uid: string) { const col = await getCollection(); const doc = await col.findOne({ "payload.uid": uid }); if (!doc) return undefined; return doc.payload; } async consume(id: string) { const col = await getCollection(); await col.updateOne( { _id: prefixedId(this.model, id) as any }, { $set: { "payload.consumed": Math.floor(Date.now() / 1000) } } ); } async destroy(id: string) { const col = await getCollection(); await col.deleteOne({ _id: prefixedId(this.model, id) as any }); } async revokeByGrantId(grantId: string) { const col = await getCollection(); await col.deleteMany({ "payload.grantId": grantId }); } }