├── docker └── tls │ └── .gitignore ├── .commitlintrc.yml ├── .husky ├── pre-commit └── commit-msg ├── .prettierignore ├── test-fixtures └── connect-mongo-5.1.0.tgz ├── .vscode ├── settings.json └── launch.json ├── .gitignore ├── example ├── shared │ ├── env.js │ ├── mongo-config.d.ts │ └── mongo-config.js ├── tsconfig.json ├── package.json ├── .env.example ├── index.js ├── mongoose.js ├── ts-example.ts ├── mongoose-multiple-connections.js └── package-lock.json ├── tsdown.config.ts ├── .github ├── CONTRIBUTING.md ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md ├── stale.yml └── workflows │ └── sanity.yml ├── .npmignore ├── src ├── index.ts ├── types │ └── kruptein.d.ts ├── test │ ├── testHelper.ts │ ├── integration.spec.ts │ └── upgrade-compat.spec.ts └── lib │ ├── cryptoAdapters.ts │ ├── MongoStore.ts │ └── MongoStore.spec.ts ├── .editorconfig ├── ava.config.mjs ├── docker-compose.yaml ├── docker-compose.tls.yaml ├── LICENSE ├── MIGRATION_V4.md ├── AGENTS.md ├── eslint.config.mjs ├── tsconfig.json ├── scripts └── generate-mongo-tls.sh ├── package.json ├── CHANGELOG.md └── README.md /docker/tls/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - '@commitlint/config-conventional' 3 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | [ "${HUSKY-}" = "0" ] && exit 0 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # package.json is formatted by package managers, so we ignore it here 2 | package.json -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | [ "${HUSKY-}" = "0" ] && exit 0 3 | 4 | npx commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /test-fixtures/connect-mongo-5.1.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdesboeufs/connect-mongo/HEAD/test-fixtures/connect-mongo-5.1.0.tgz -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "typescript.tsdk": "node_modules/typescript/lib", 4 | "typescript.enablePromptUseWorkspaceTsdk": true 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | .nyc_output 3 | build 4 | dist 5 | node_modules 6 | src/**.js 7 | coverage 8 | *.log 9 | .eslintcache 10 | example/.env 11 | docker/tls/* 12 | !docker/tls/.gitignore 13 | tsconfig.tsbuildinfo 14 | -------------------------------------------------------------------------------- /example/shared/env.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs') 2 | const path = require('node:path') 3 | const dotenv = require('dotenv') 4 | 5 | const envPath = path.resolve(__dirname, '../.env') 6 | 7 | if (fs.existsSync(envPath)) { 8 | dotenv.config({ path: envPath }) 9 | } 10 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['esm', 'cjs'], 6 | dts: true, 7 | outDir: 'dist', 8 | sourcemap: true, 9 | platform: 'node', 10 | exports: true, 11 | }) 12 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Example Contributing Guidelines 2 | 3 | This is an example of GitHub's contributing guidelines file. Check out GitHub's [CONTRIBUTING.md help center article](https://help.github.com/articles/setting-guidelines-for-repository-contributors/) for more information. 4 | -------------------------------------------------------------------------------- /example/shared/mongo-config.d.ts: -------------------------------------------------------------------------------- 1 | export interface ExampleMongoConfig { 2 | dbName: string 3 | mongoUrl: string 4 | mongoOptions: Record 5 | sessionSecret: string 6 | cryptoSecret?: string 7 | } 8 | 9 | export function getMongoConfig(): ExampleMongoConfig 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .npmignore 2 | 3 | History.md 4 | Makefile 5 | Readme.md 6 | .DS_Store 7 | 8 | examples/ 9 | support/ 10 | test/ 11 | coverage/ 12 | .vscode/ 13 | .github/ 14 | .eslintrc 15 | .prettierrc.yml 16 | .travis.yml 17 | commitlint.config.js 18 | .nyc_output/ 19 | jest.config.js 20 | src/test/ 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import MongoStore from './lib/MongoStore.js' 2 | export { 3 | createKrupteinAdapter, 4 | createWebCryptoAdapter, 5 | type CryptoAdapter, 6 | type CryptoOptions, 7 | type WebCryptoAdapterOptions, 8 | } from './lib/cryptoAdapters.js' 9 | 10 | export default MongoStore 11 | export { MongoStore } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /ava.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | files: [ 3 | 'src/**/*.{test,spec}.ts' 4 | ], 5 | failFast: false, 6 | typescript: { 7 | // map TS paths -> compiled JS paths 8 | rewritePaths: { 9 | 'src/': 'build/' 10 | }, 11 | compile: 'tsc' 12 | }, 13 | timeout: '15s', 14 | environmentVariables: { 15 | NODE_ENV: 'test' 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 2 | 3 | - **What is the current behavior?** (You can also link to an open issue here) 4 | 5 | - **What is the new behavior (if this is a feature change)?** 6 | 7 | - **Other information**: 8 | 9 | - **Checklist:** 10 | 11 | - [ ] Added test cases 12 | - [ ] Updated changelog 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **I'm submitting a ...** 2 | [ ] bug report 3 | [ ] feature request 4 | [ ] question about the decisions made in the repository 5 | [ ] question about how to use this project 6 | 7 | - **Summary** 8 | 9 | - **Other information** (e.g. detailed explanation, stack traces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.) 10 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "Node16", 5 | "moduleResolution": "node16", 6 | "allowJs": true, 7 | "outDir": "./build", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "sourceMap": true 13 | }, 14 | "include": ["./**/*.ts", "./shared/**/*.js"] 15 | } 16 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | mongo: 3 | image: "mongo:${MONGO_SERVER_TAG:-7.0}" 4 | container_name: connect-mongo-dev 5 | restart: unless-stopped 6 | environment: 7 | MONGO_INITDB_ROOT_USERNAME: root 8 | MONGO_INITDB_ROOT_PASSWORD: example 9 | ports: 10 | - '27017:27017' 11 | healthcheck: 12 | test: 13 | [ 14 | "CMD-SHELL", 15 | "mongosh --quiet mongodb://root:example@localhost:27017/admin --eval 'db.runCommand({ ping: 1 })'" 16 | ] 17 | interval: 5s 18 | timeout: 2s 19 | retries: 20 20 | volumes: 21 | - mongo-data:/data/db 22 | 23 | volumes: 24 | mongo-data: 25 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-mongo-example", 3 | "version": "1.0.0", 4 | "description": "connect-mongo example code", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "start:js": "node index.js", 9 | "start:mongoose": "node mongoose.js", 10 | "start:ts": "tsc && node build/ts-example.js" 11 | }, 12 | "dependencies": { 13 | "connect-mongo": "^5.1.0", 14 | "dotenv": "^16.4.5", 15 | "express": "^4.18.2", 16 | "express-session": "^1.17.3", 17 | "mongoose": "^8.0.0" 18 | }, 19 | "devDependencies": { 20 | "@types/express": "^4.17.21", 21 | "@types/express-session": "^1.18.2", 22 | "@types/node": "^20.16.5", 23 | "typescript": "^5.9.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /example/.env.example: -------------------------------------------------------------------------------- 1 | # Default local container (docker compose up -d) 2 | MONGO_URL=mongodb://root:example@127.0.0.1:27017/example-db?authSource=admin 3 | MONGO_DB_NAME=example-db 4 | 5 | # Optional session tweaks 6 | SESSION_SECRET=connect-mongo-example-secret 7 | # SESSION_CRYPTO_SECRET=replace-with-a-long-secret 8 | # MONGO_MAX_POOL_SIZE=10 9 | 10 | # TLS flags (after running `npm run tls:setup`) 11 | # MONGO_TLS_CA_FILE=../docker/tls/ca.crt 12 | # MONGO_TLS_CERT_KEY_FILE=../docker/tls/client.pem 13 | # MONGO_TLS_ALLOW_INVALID_CERTIFICATES=false 14 | # MONGO_TLS_ALLOW_INVALID_HOSTNAMES=false 15 | 16 | # SRV clusters (Atlas, DocumentDB, etc.) 17 | # Replace the value below with your mongodb+srv string. The config helper 18 | # automatically detects the scheme and merges TLS options if present. 19 | # MONGO_URL=mongodb+srv://cluster0.xxxxxx.mongodb.net/example-db?retryWrites=true&w=majority 20 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const session = require('express-session') 3 | const MongoStore = require('connect-mongo') 4 | const { getMongoConfig } = require('./shared/mongo-config') 5 | 6 | const app = express() 7 | const port = 3000 8 | 9 | const { 10 | mongoUrl, 11 | mongoOptions, 12 | dbName, 13 | sessionSecret, 14 | cryptoSecret 15 | } = getMongoConfig() 16 | 17 | const store = MongoStore.create({ 18 | mongoUrl, 19 | dbName, 20 | mongoOptions, 21 | stringify: false, 22 | ...(cryptoSecret ? { crypto: { secret: cryptoSecret } } : {}) 23 | }) 24 | 25 | app.use(session({ 26 | secret: sessionSecret, 27 | resave: false, 28 | saveUninitialized: false, 29 | store 30 | })); 31 | 32 | app.get('/', (req, res) => { 33 | req.session.foo = 'test-id' 34 | res.send('Hello World!') 35 | }) 36 | 37 | app.listen(port, () => { 38 | console.log(`Example app listening at http://localhost:${port}`) 39 | }) 40 | -------------------------------------------------------------------------------- /docker-compose.tls.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | mongo-tls: 3 | profiles: 4 | - tls 5 | image: "mongo:${MONGO_SERVER_TAG:-7.0}" 6 | container_name: connect-mongo-dev-tls 7 | restart: unless-stopped 8 | environment: 9 | MONGO_INITDB_ROOT_USERNAME: root 10 | MONGO_INITDB_ROOT_PASSWORD: example 11 | command: > 12 | mongod --bind_ip_all --tlsMode requireTLS --tlsCertificateKeyFile /certs/server.pem 13 | --tlsCAFile /certs/ca.crt 14 | ports: 15 | - '27443:27017' 16 | healthcheck: 17 | test: 18 | [ 19 | "CMD-SHELL", 20 | "mongosh --quiet --tls --tlsCAFile /certs/ca.crt --tlsCertificateKeyFile /certs/server.pem mongodb://root:example@localhost:27017/admin --eval 'db.runCommand({ ping: 1 })'" 21 | ] 22 | interval: 5s 23 | timeout: 5s 24 | retries: 20 25 | volumes: 26 | - mongo-tls-data:/data/db 27 | - ./docker/tls:/certs:ro 28 | 29 | volumes: 30 | mongo-tls-data: 31 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | // To debug, make sure a *.spec.ts file is active in the editor, then run a configuration 5 | { 6 | "type": "node", 7 | "request": "launch", 8 | "name": "Debug Active Spec", 9 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava", 10 | "runtimeArgs": ["debug", "--break", "--serial", "${file}"], 11 | "port": 9229, 12 | "outputCapture": "std", 13 | "skipFiles": ["/**/*.js"], 14 | "preLaunchTask": "npm: build" 15 | // "smartStep": true 16 | }, 17 | { 18 | // Use this one if you're already running `yarn watch` 19 | "type": "node", 20 | "request": "launch", 21 | "name": "Debug Active Spec (no build)", 22 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava", 23 | "runtimeArgs": ["debug", "--break", "--serial", "${file}"], 24 | "port": 9229, 25 | "outputCapture": "std", 26 | "skipFiles": ["/**/*.js"] 27 | // "smartStep": true 28 | }] 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jérôme Desboeufs 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. 22 | -------------------------------------------------------------------------------- /example/mongoose.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const session = require('express-session') 3 | const MongoStore = require('connect-mongo') 4 | const mongoose = require('mongoose') 5 | const { getMongoConfig } = require('./shared/mongo-config') 6 | 7 | const app = express() 8 | const port = 3000 9 | 10 | const { 11 | mongoUrl, 12 | mongoOptions, 13 | dbName, 14 | sessionSecret, 15 | cryptoSecret 16 | } = getMongoConfig() 17 | 18 | const clientPromise = mongoose.connect( 19 | mongoUrl, 20 | { 21 | dbName, 22 | ...mongoOptions 23 | } 24 | ).then((connection) => connection.connection.getClient()) 25 | 26 | app.use(session({ 27 | secret: sessionSecret, 28 | resave: false, 29 | saveUninitialized: false, 30 | store: MongoStore.create({ 31 | clientPromise, 32 | dbName, 33 | stringify: false, 34 | autoRemove: 'interval', 35 | autoRemoveInterval: 1, 36 | ...(cryptoSecret ? { crypto: { secret: cryptoSecret } } : {}) 37 | }) 38 | })); 39 | 40 | app.get('/', (req, res) => { 41 | req.session.foo = 'test-id' 42 | res.send('Hello World!') 43 | }) 44 | 45 | app.listen(port, () => { 46 | console.log(`Example app listening at http://localhost:${port}`) 47 | }) 48 | -------------------------------------------------------------------------------- /example/ts-example.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express' 2 | import session from 'express-session' 3 | import MongoStore from 'connect-mongo' 4 | import { getMongoConfig } from './shared/mongo-config' 5 | 6 | const app = express() 7 | const port = 3000 8 | 9 | declare module 'express-session' { 10 | interface SessionData { 11 | foo: string 12 | } 13 | } 14 | 15 | const { 16 | mongoUrl, 17 | mongoOptions, 18 | dbName, 19 | sessionSecret, 20 | cryptoSecret 21 | } = getMongoConfig() 22 | 23 | const store = MongoStore.create({ 24 | mongoUrl, 25 | dbName, 26 | mongoOptions, 27 | stringify: false, 28 | ...(cryptoSecret ? { crypto: { secret: cryptoSecret } } : {}) 29 | }) 30 | 31 | // Cast to any to sidestep slight @types/express-session vs @types/express version skew. 32 | const sessionMiddleware = session({ 33 | secret: sessionSecret, 34 | resave: false, 35 | saveUninitialized: false, 36 | store 37 | }) as any 38 | 39 | app.use(sessionMiddleware); 40 | 41 | app.get('/', (req: Request, res: Response) => { 42 | req.session.foo = 'test-id' 43 | res.send('Hello World!') 44 | }) 45 | 46 | app.listen(port, () => { 47 | console.log(`Example app listening at http://localhost:${port}`) 48 | }) 49 | -------------------------------------------------------------------------------- /src/types/kruptein.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * If you import a dependency which does not include its own type definitions, 3 | * TypeScript will try to find a definition for it by following the `typeRoots` 4 | * compiler option in tsconfig.json. For this project, we've configured it to 5 | * fall back to this folder if nothing is found in node_modules/@types. 6 | * 7 | * Often, you can install the DefinitelyTyped 8 | * (https://github.com/DefinitelyTyped/DefinitelyTyped) type definition for the 9 | * dependency in question. However, if no one has yet contributed definitions 10 | * for the package, you may want to declare your own. (If you're using the 11 | * `noImplicitAny` compiler options, you'll be required to declare it.) 12 | * 13 | * This is an example type definition which allows import from `module-name`, 14 | * e.g.: 15 | * ```ts 16 | * import something from 'module-name'; 17 | * something(); 18 | * ``` 19 | */ 20 | declare module 'kruptein' { 21 | type Callback = (msg?: string, data?: string) => void 22 | class Kruptein { 23 | set(secret: string, plaintext: string | unknown, callback: Callback): void 24 | get(secret: string, ciphertext: string, callback: Callback): void 25 | } 26 | 27 | export default function (options: unknown): Kruptein 28 | } 29 | -------------------------------------------------------------------------------- /MIGRATION_V4.md: -------------------------------------------------------------------------------- 1 | # V4 migration guide 2 | 3 | To migrate the library from V3 to V4, re-install the dependencies. 4 | 5 | Use your package manager (npm example shown): 6 | 7 | ``` 8 | npm uninstall connect-mongo 9 | npm uninstall @types/connect-mongo 10 | npm install connect-mongo 11 | ``` 12 | 13 | Next step is to import the dependencies 14 | 15 | Javascript: 16 | ```js 17 | const MongoStore = require('connect-mongo'); 18 | ``` 19 | 20 | Typescript: 21 | ```ts 22 | import MongoStore from 'connect-mongo'; 23 | ``` 24 | 25 | Create the store using `MongoStore.create(options)` instead of `new MongoStore(options)` 26 | 27 | ```js 28 | app.use(session({ 29 | secret: 'foo', 30 | store: MongoStore.create(options) 31 | })); 32 | ``` 33 | 34 | For the options, you should make the following changes: 35 | 36 | * Change `url` to `mongoUrl` 37 | * Change `collection` to `collectionName` if you are using it 38 | * Keep `clientPromise` if you are using it 39 | * `mongooseConnection` has been removed. Please update your application code to use either `mongoUrl`, `client` or `clientPromise` 40 | * To reuse an existing mongoose connection retreive the mongoDb driver from you mongoose connection using `Connection.prototype.getClient()` and pass it to the store in the `client`-option. 41 | * Remove `fallbackMemory` option and if you are using it, you can import from: 42 | 43 | ```js 44 | const session = require('express-session'); 45 | 46 | app.use(session({ 47 | store: isDev ? new session.MemoryStore() : MongoStore.create(options) 48 | })); 49 | ``` 50 | 51 | > You can also take a look at [example](example) directory for example usage. 52 | -------------------------------------------------------------------------------- /src/test/testHelper.ts: -------------------------------------------------------------------------------- 1 | import util from 'util' 2 | import ExpressSession from 'express-session' 3 | 4 | import MongoStore, { ConnectMongoOptions } from '../lib/MongoStore.js' 5 | 6 | // Create a connect cookie instance 7 | export const makeCookie = () => { 8 | const cookie = new ExpressSession.Cookie() 9 | cookie.maxAge = 10000 // This sets cookie.expire through a setter 10 | cookie.secure = true 11 | cookie.domain = 'cow.com' 12 | cookie.sameSite = false 13 | 14 | return cookie 15 | } 16 | 17 | // Create session data 18 | export const makeData = () => { 19 | return { 20 | foo: 'bar', 21 | baz: { 22 | cow: 'moo', 23 | chicken: 'cluck', 24 | }, 25 | num: 1, 26 | cookie: makeCookie(), 27 | } 28 | } 29 | 30 | export const makeDataNoCookie = () => { 31 | return { 32 | foo: 'bar', 33 | baz: { 34 | cow: 'moo', 35 | fish: 'blub', 36 | fox: 'nobody knows!', 37 | }, 38 | num: 2, 39 | } 40 | } 41 | 42 | export const createStoreHelper = (opt: Partial = {}) => { 43 | const store = MongoStore.create({ 44 | mongoUrl: 'mongodb://root:example@127.0.0.1:27017', 45 | mongoOptions: {}, 46 | dbName: 'testDb', 47 | collectionName: 'test-collection', 48 | autoRemove: 'disabled', 49 | ...opt, 50 | }) 51 | 52 | const storePromise = { 53 | length: util.promisify(store.length).bind(store), 54 | clear: util.promisify(store.clear).bind(store), 55 | get: util.promisify(store.get).bind(store), 56 | set: util.promisify(store.set).bind(store), 57 | all: util.promisify(store.all).bind(store), 58 | touch: util.promisify(store.touch).bind(store), 59 | destroy: util.promisify(store.destroy).bind(store), 60 | close: store.close.bind(store), 61 | } 62 | return { store, storePromise } 63 | } 64 | -------------------------------------------------------------------------------- /example/mongoose-multiple-connections.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const mongoose = require('mongoose') 3 | const session = require('express-session') 4 | const MongoStore = require('connect-mongo') 5 | const { getMongoConfig } = require('./shared/mongo-config') 6 | 7 | const app = express() 8 | const port = 3000 9 | 10 | const { 11 | mongoUrl, 12 | mongoOptions, 13 | dbName, 14 | sessionSecret, 15 | cryptoSecret 16 | } = getMongoConfig() 17 | 18 | const appDbUrl = process.env.APP_MONGO_URL || mongoUrl 19 | const appDbName = process.env.APP_DB_NAME || `${dbName}-app` 20 | 21 | const appConnection = mongoose.createConnection(appDbUrl, { 22 | dbName: appDbName, 23 | ...mongoOptions 24 | }) 25 | 26 | const sessionConnection = mongoose.createConnection(mongoUrl, { 27 | dbName, 28 | ...mongoOptions 29 | }) 30 | 31 | const sessionInit = (client) => { 32 | app.use( 33 | session({ 34 | store: MongoStore.create({ 35 | client, 36 | dbName, 37 | mongoOptions, 38 | ...(cryptoSecret ? { crypto: { secret: cryptoSecret } } : {}) 39 | }), 40 | secret: sessionSecret, 41 | resave: false, 42 | saveUninitialized: false, 43 | cookie: { maxAge: 24 * 60 * 60 * 1000 } 44 | }) 45 | ) 46 | } 47 | 48 | async function bootstrap() { 49 | await Promise.all([appConnection.asPromise(), sessionConnection.asPromise()]) 50 | console.log('Connected to AppDB and SessionsDB.') 51 | const mongoClient = sessionConnection.getClient() 52 | sessionInit(mongoClient) 53 | 54 | const router = express.Router() 55 | router.get('/', (req, res) => { 56 | req.session.foo = 'bar' 57 | res.send('Session Updated') 58 | }) 59 | 60 | app.use('/', router) 61 | 62 | app.listen(port, () => { 63 | console.log(`Example app listening at http://localhost:${port}`) 64 | }) 65 | } 66 | 67 | bootstrap().catch((err) => { 68 | console.error('Unable to initialize Mongo connections', err) 69 | process.exitCode = 1 70 | }) 71 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # AGENTS GUIDE 2 | 3 | Welcome! This file keeps lightweight coordination notes for anyone (human or AI) hacking on `connect-mongo`. 4 | 5 | ## Current Focus 6 | 7 | 1. Follow `docs/PLANS.md` for the prioritized maintenance backlog. 8 | 2. When picking up a task, append a short status note under the relevant section in `docs/PLANS.md` (e.g. `- [started YYYY-MM-DD] `), then remove or update it when you finish. 9 | 3. Build pipeline now uses `tsdown` for dual ESM/CJS bundles into `dist/` plus `tsc` for typed transpilation (`npm run build` and `npm run typecheck`). Local sanity checks confirm `npm run build`, `npm run test:lint`, and `npm run test:prettier` pass (lint only warns on crypto key_size/iv_size/at_size camelCase). Full `npm test` still needs a running MongoDB (e.g., docker compose up) until tests migrate to mongodb-memory-server. 10 | 11 | ## Workflow Expectations 12 | 13 | - Run `npm ci && npm run build && npm test` locally before opening or updating a PR unless the change is docs-only. 14 | - Record any assumptions, surprises, or TODOs at the bottom of the touched file(s) in `// TODO(agent): ...` comments or in `docs/PLANS.md`. 15 | - When working on a task, always follow PLAN, EDIT and REVIEW steps. 16 | - When working on a task, always check if CHANGELOG.md or README.md need updates. If encounter breaking changes, add a note to CHANGELOG.md and also create separate migration docs if needed. 17 | - Use `git commit --amend` to tidy up your commits before pushing, and prefer small, focused PRs that address a single task. 18 | - Use context7 or web search to find relevant code snippets, tests, or docs that relate to the task at hand. 19 | 20 | ## Communication 21 | 22 | - Prefer concise commit messages referencing the plan item, e.g. `fix: stabilize clear() semantics (plan#2)`. 23 | - For complex changes, request a human review to ensure the change aligns with project goals and also ask for clarification on any ambiguous points in the plan. 24 | 25 | Thanks for helping keep the project healthy! 26 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import { FlatCompat } from '@eslint/eslintrc' 4 | import js from '@eslint/js' 5 | 6 | const __filename = fileURLToPath(import.meta.url) 7 | const __dirname = path.dirname(__filename) 8 | 9 | const compat = new FlatCompat({ 10 | baseDirectory: __dirname, 11 | recommendedConfig: js.configs.recommended, 12 | allConfig: js.configs.all, 13 | }) 14 | 15 | export default [ 16 | ...compat.config({ 17 | root: true, 18 | env: { 19 | node: true, 20 | es6: true, 21 | }, 22 | ignorePatterns: [ 23 | 'node_modules/*', 24 | 'build/*', 25 | 'coverage/*', 26 | 'example/*', 27 | 'tsdown.config.ts', 28 | ], 29 | parser: '@typescript-eslint/parser', 30 | parserOptions: { 31 | ecmaVersion: 2022, 32 | project: './tsconfig.json', 33 | tsconfigRootDir: __dirname, 34 | }, 35 | extends: [ 36 | 'eslint:recommended', 37 | 'plugin:@typescript-eslint/recommended', 38 | 'plugin:@typescript-eslint/stylistic', 39 | 'plugin:eslint-comments/recommended', 40 | 'plugin:prettier/recommended', 41 | ], 42 | plugins: ['eslint-comments', '@typescript-eslint'], 43 | rules: { 44 | camelcase: 'warn', 45 | '@typescript-eslint/no-unused-vars': [ 46 | 'warn', 47 | { 48 | args: 'none', 49 | varsIgnorePattern: '^_', 50 | argsIgnorePattern: '^_', 51 | }, 52 | ], 53 | 'prefer-const': [ 54 | 'error', 55 | { 56 | destructuring: 'any', 57 | ignoreReadBeforeAssign: false, 58 | }, 59 | ], 60 | '@typescript-eslint/explicit-function-return-type': 'off', 61 | '@typescript-eslint/no-var-requires': 'off', 62 | '@typescript-eslint/ban-ts-comment': 'off', 63 | '@typescript-eslint/no-explicit-any': 'off', 64 | '@typescript-eslint/consistent-type-definitions': 'off', 65 | '@typescript-eslint/consistent-indexed-object-style': 'off', 66 | '@typescript-eslint/no-require-imports': 'off', 67 | }, 68 | }), 69 | ] 70 | -------------------------------------------------------------------------------- /example/shared/mongo-config.js: -------------------------------------------------------------------------------- 1 | require('./env') 2 | const path = require('node:path') 3 | 4 | const DEFAULT_DB_NAME = 'example-db' 5 | const DEFAULT_URI = `mongodb://root:example@127.0.0.1:27017/${DEFAULT_DB_NAME}?authSource=admin` 6 | 7 | const booleanTrue = new Set(['1', 'true', 'TRUE', 'yes']) 8 | 9 | function getMongoConfig() { 10 | const dbName = process.env.MONGO_DB_NAME || DEFAULT_DB_NAME 11 | 12 | return { 13 | dbName, 14 | mongoUrl: process.env.MONGO_URL || DEFAULT_URI.replace(DEFAULT_DB_NAME, dbName), 15 | mongoOptions: buildMongoOptions(), 16 | sessionSecret: process.env.SESSION_SECRET || 'connect-mongo-example-secret', 17 | cryptoSecret: process.env.SESSION_CRYPTO_SECRET 18 | } 19 | } 20 | 21 | function buildMongoOptions() { 22 | const options = {} 23 | 24 | if (process.env.MONGO_MAX_POOL_SIZE) { 25 | const poolSize = Number(process.env.MONGO_MAX_POOL_SIZE) 26 | if (!Number.isNaN(poolSize) && poolSize > 0) { 27 | options.maxPoolSize = poolSize 28 | } 29 | } 30 | 31 | const tlsOptions = buildTlsOptions() 32 | return Object.keys(tlsOptions).length === 0 ? options : { ...options, ...tlsOptions } 33 | } 34 | 35 | function buildTlsOptions() { 36 | const options = {} 37 | const caFile = toAbsolute(process.env.MONGO_TLS_CA_FILE) 38 | 39 | if (!caFile) { 40 | return options 41 | } 42 | 43 | options.tls = true 44 | options.tlsCAFile = caFile 45 | 46 | const certKey = toAbsolute(process.env.MONGO_TLS_CERT_KEY_FILE) 47 | if (certKey) { 48 | options.tlsCertificateKeyFile = certKey 49 | } 50 | 51 | if (isTruthy(process.env.MONGO_TLS_ALLOW_INVALID_CERTIFICATES)) { 52 | options.tlsAllowInvalidCertificates = true 53 | } 54 | 55 | if (isTruthy(process.env.MONGO_TLS_ALLOW_INVALID_HOSTNAMES)) { 56 | options.tlsAllowInvalidHostnames = true 57 | } 58 | 59 | return options 60 | } 61 | 62 | function toAbsolute(maybePath) { 63 | if (!maybePath) { 64 | return undefined 65 | } 66 | 67 | return path.isAbsolute(maybePath) ? maybePath : path.resolve(process.cwd(), maybePath) 68 | } 69 | 70 | function isTruthy(value) { 71 | if (!value) { 72 | return false 73 | } 74 | 75 | return booleanTrue.has(value) 76 | } 77 | 78 | module.exports = { 79 | getMongoConfig 80 | } 81 | -------------------------------------------------------------------------------- /.github/workflows/sanity.yml: -------------------------------------------------------------------------------- 1 | name: Sanity check 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - 2025-review 8 | pull_request: 9 | branches: 10 | - master 11 | - 2025-review 12 | 13 | jobs: 14 | build: 15 | name: Build with Node ${{ matrix.node-version }} | Mongo ${{ matrix.mongo-server }} | Driver ${{ matrix.mongodb-driver }} 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | node-version: [20.x, 22.x, 24.x] 22 | mongodb-driver: ['5.0.0', '5.6.0', '5.9.2', '6', '6.11.0', '6.21.0', '7.0.0'] 23 | mongo-server: ['4.4', '5.0', '6.0', '7.0', '8.0'] 24 | 25 | env: 26 | MONGODB_DRIVER_VERSION: ${{ matrix.mongodb-driver }} 27 | MONGO_SERVER_TAG: ${{ matrix.mongo-server }} 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | - name: Use Node.js ${{ matrix.node-version }} 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | cache: 'npm' 36 | - name: Install dependencies 37 | run: npm ci 38 | - name: Install MongoDB driver ${{ matrix.mongodb-driver }} 39 | run: | 40 | npm install mongodb@${MONGODB_DRIVER_VERSION} --no-save --no-package-lock --no-audit --legacy-peer-deps 41 | - name: Start MongoDB ${{ matrix.mongo-server }} 42 | run: docker compose up -d 43 | - name: Wait for MongoDB to accept connections 44 | run: | 45 | for i in {1..30}; do 46 | if docker compose exec -T mongo /bin/sh -c 'if command -v mongosh >/dev/null 2>&1; then mongosh --quiet --eval "db.runCommand({ ping: 1 })"; else mongo --quiet --eval "db.runCommand({ ping: 1 })"; fi' >/dev/null; then 47 | exit 0 48 | fi 49 | sleep 2 50 | done 51 | echo "MongoDB failed to start" >&2 52 | exit 1 53 | - run: npm run typecheck 54 | - run: npm test 55 | - name: Upload coverage 56 | if: matrix.node-version == '22.x' && matrix.mongodb-driver == '6' && matrix.mongo-server == '6.0' 57 | run: npm run cov:send 58 | env: 59 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 60 | - name: Build artifacts 61 | run: npm run build 62 | - name: Tear down MongoDB 63 | if: always() 64 | run: docker compose down -v 65 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2022", 5 | "outDir": "build", 6 | "rootDir": "src", 7 | "moduleResolution": "NodeNext", 8 | "module": "NodeNext", 9 | "declaration": true, 10 | "sourceMap": true, 11 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 12 | "resolveJsonModule": true /* Include modules imported with .json extension. */, 13 | 14 | "strict": true /* Enable all strict type-checking options. */, 15 | 16 | /* Strict Type-Checking Options */ 17 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 18 | // "strictNullChecks": true /* Enable strict null checks. */, 19 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 20 | // "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 21 | // "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 22 | // "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 23 | 24 | /* Additional Checks */ 25 | "noUnusedLocals": true /* Report errors on unused locals. */, 26 | "noUnusedParameters": true /* Report errors on unused parameters. */, 27 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 28 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 29 | 30 | /* Debugging Options */ 31 | "traceResolution": false /* Report module resolution log messages. */, 32 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */, 33 | "listFiles": false /* Print names of files part of the compilation. */, 34 | "pretty": true /* Stylize errors and messages using color and context. */, 35 | 36 | /* Experimental Options */ 37 | // "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 38 | // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, 39 | 40 | "lib": ["es2022", "esnext.disposable"], 41 | "types": ["node"], 42 | "typeRoots": ["node_modules/@types", "src/types"] 43 | }, 44 | "include": ["src/**/*.ts"], 45 | "exclude": ["node_modules/**", "example/**"], 46 | "compileOnSave": false 47 | } 48 | -------------------------------------------------------------------------------- /scripts/generate-mongo-tls.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" 5 | TLS_DIR="$ROOT_DIR/docker/tls" 6 | FORCE=0 7 | 8 | while [[ $# -gt 0 ]]; do 9 | case "$1" in 10 | --force) 11 | FORCE=1 12 | shift 13 | ;; 14 | *) 15 | echo "Unknown option: $1" >&2 16 | exit 2 17 | ;; 18 | esac 19 | done 20 | 21 | if [[ $FORCE -eq 0 && -f "$TLS_DIR/ca.crt" ]]; then 22 | echo "TLS fixtures already exist in $TLS_DIR (use --force to regenerate)." >&2 23 | exit 0 24 | fi 25 | 26 | mkdir -p "$TLS_DIR" 27 | rm -f "$TLS_DIR"/*.{crt,key,pem,csr,cnf,srl} 2>/dev/null || true 28 | 29 | echo "Generating local CA..." 30 | openssl req \ 31 | -x509 \ 32 | -nodes \ 33 | -days 365 \ 34 | -newkey rsa:4096 \ 35 | -keyout "$TLS_DIR/ca.key" \ 36 | -out "$TLS_DIR/ca.crt" \ 37 | -subj "/CN=connect-mongo-dev CA" 38 | 39 | cat >"$TLS_DIR/server.cnf" <<'EOF' 40 | [ req ] 41 | prompt = no 42 | distinguished_name = dn 43 | req_extensions = v3_req 44 | 45 | [ dn ] 46 | CN = localhost 47 | 48 | [ v3_req ] 49 | keyUsage = digitalSignature,keyEncipherment 50 | extendedKeyUsage = serverAuth 51 | subjectAltName = @alt_names 52 | 53 | [ alt_names ] 54 | DNS.1 = localhost 55 | IP.1 = 127.0.0.1 56 | EOF 57 | 58 | echo "Generating server certificate..." 59 | openssl req \ 60 | -nodes \ 61 | -newkey rsa:4096 \ 62 | -keyout "$TLS_DIR/server.key" \ 63 | -out "$TLS_DIR/server.csr" \ 64 | -config "$TLS_DIR/server.cnf" 65 | 66 | openssl x509 \ 67 | -req \ 68 | -in "$TLS_DIR/server.csr" \ 69 | -CA "$TLS_DIR/ca.crt" \ 70 | -CAkey "$TLS_DIR/ca.key" \ 71 | -CAcreateserial \ 72 | -out "$TLS_DIR/server.crt" \ 73 | -days 365 \ 74 | -sha256 \ 75 | -extensions v3_req \ 76 | -extfile "$TLS_DIR/server.cnf" 77 | 78 | cat "$TLS_DIR/server.crt" "$TLS_DIR/server.key" >"$TLS_DIR/server.pem" 79 | 80 | cat >"$TLS_DIR/client.cnf" <<'EOF' 81 | [ req ] 82 | prompt = no 83 | distinguished_name = dn 84 | req_extensions = v3_req 85 | 86 | [ dn ] 87 | CN = connect-mongo-example 88 | 89 | [ v3_req ] 90 | keyUsage = digitalSignature,keyEncipherment 91 | extendedKeyUsage = clientAuth 92 | subjectAltName = @alt_names 93 | 94 | [ alt_names ] 95 | DNS.1 = localhost 96 | EOF 97 | 98 | echo "Generating optional client certificate..." 99 | openssl req \ 100 | -nodes \ 101 | -newkey rsa:4096 \ 102 | -keyout "$TLS_DIR/client.key" \ 103 | -out "$TLS_DIR/client.csr" \ 104 | -config "$TLS_DIR/client.cnf" 105 | 106 | openssl x509 \ 107 | -req \ 108 | -in "$TLS_DIR/client.csr" \ 109 | -CA "$TLS_DIR/ca.crt" \ 110 | -CAkey "$TLS_DIR/ca.key" \ 111 | -CAcreateserial \ 112 | -out "$TLS_DIR/client.crt" \ 113 | -days 365 \ 114 | -sha256 \ 115 | -extensions v3_req \ 116 | -extfile "$TLS_DIR/client.cnf" 117 | 118 | cat "$TLS_DIR/client.crt" "$TLS_DIR/client.key" >"$TLS_DIR/client.pem" 119 | 120 | rm -f "$TLS_DIR"/*.csr "$TLS_DIR"/*.cnf 121 | 122 | echo "TLS fixtures ready in $TLS_DIR" 123 | echo "Use MONGO_TLS_CA_FILE=docker/tls/ca.crt and MONGO_TLS_CERT_KEY_FILE=docker/tls/client.pem when testing mutual TLS." 124 | -------------------------------------------------------------------------------- /src/test/integration.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import request from 'supertest' 3 | import express from 'express' 4 | import session, { SessionOptions } from 'express-session' 5 | import MongoStore from '../lib/MongoStore.js' 6 | import { ConnectMongoOptions } from '../lib/MongoStore.js' 7 | 8 | declare module 'express-session' { 9 | interface SessionData { 10 | [key: string]: any 11 | } 12 | } 13 | 14 | type AgentWithCleanup = { 15 | agent: ReturnType 16 | cleanup: () => Promise 17 | store: MongoStore 18 | } 19 | 20 | function createSupertestAgent( 21 | sessionOpts: SessionOptions, 22 | mongoStoreOpts: ConnectMongoOptions 23 | ): AgentWithCleanup { 24 | const app = express() 25 | const store = MongoStore.create(mongoStoreOpts) 26 | app.use( 27 | session({ 28 | ...sessionOpts, 29 | store: store, 30 | }) 31 | ) 32 | app.get('/', function (req, res) { 33 | if (typeof req.session.views === 'number') { 34 | req.session.views++ 35 | } else { 36 | req.session.views = 0 37 | } 38 | res.status(200).send({ views: req.session.views }) 39 | }) 40 | app.get('/ping', function (req, res) { 41 | res.status(200).send({ views: req.session.views }) 42 | }) 43 | const agent = request.agent(app) 44 | return { 45 | agent, 46 | store, 47 | cleanup: async () => { 48 | await store.close() 49 | }, 50 | } 51 | } 52 | 53 | function createSupertestAgentWithDefault( 54 | sessionOpts: Omit = {}, 55 | mongoStoreOpts: ConnectMongoOptions = {} 56 | ) { 57 | return createSupertestAgent( 58 | { secret: 'foo', ...sessionOpts }, 59 | { 60 | mongoUrl: 'mongodb://root:example@127.0.0.1:27017', 61 | dbName: 'integration-test-db', 62 | stringify: false, 63 | ...mongoStoreOpts, 64 | } 65 | ) 66 | } 67 | 68 | test.serial('simple case', async (t) => { 69 | const { agent, cleanup } = createSupertestAgentWithDefault() 70 | try { 71 | await agent.get('/').expect(200) 72 | const res = await agent.get('/').expect(200) 73 | t.deepEqual(res.body, { views: 1 }) 74 | } finally { 75 | await cleanup() 76 | } 77 | }) 78 | 79 | test.serial('simple case with touch after', async (t) => { 80 | const { agent, cleanup } = createSupertestAgentWithDefault( 81 | { resave: false, saveUninitialized: false, rolling: true }, 82 | { touchAfter: 1 } 83 | ) 84 | 85 | try { 86 | await agent.get('/').expect(200) 87 | const res = await agent.get('/').expect(200) 88 | t.deepEqual(res.body, { views: 1 }) 89 | await new Promise((resolve) => setTimeout(resolve, 1200)) 90 | const pingRes = await agent.get('/ping').expect(200) 91 | t.deepEqual(pingRes.body, { views: 1 }) 92 | } finally { 93 | await cleanup() 94 | } 95 | }) 96 | 97 | test.serial( 98 | 'timestamps option adds createdAt/updatedAt in integration flow', 99 | async (t) => { 100 | const { agent, cleanup, store } = createSupertestAgentWithDefault( 101 | { resave: false, saveUninitialized: false, rolling: true }, 102 | { timestamps: true, collectionName: 'integration-timestamps' } 103 | ) 104 | 105 | try { 106 | await agent.get('/').expect(200) 107 | const collection = await store.collectionP 108 | const doc = await collection.findOne({}) 109 | t.truthy(doc?.createdAt) 110 | t.truthy(doc?.updatedAt) 111 | 112 | const firstUpdated = doc?.updatedAt?.getTime() 113 | await new Promise((resolve) => setTimeout(resolve, 20)) 114 | await agent.get('/ping').expect(200) 115 | const doc2 = await collection.findOne({ _id: doc?._id }) 116 | t.truthy((doc2?.updatedAt?.getTime() ?? 0) >= (firstUpdated ?? 0)) 117 | } finally { 118 | await cleanup() 119 | } 120 | } 121 | ) 122 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-mongo", 3 | "version": "6.0.0", 4 | "description": "MongoDB session store for Express and Connect", 5 | "type": "module", 6 | "main": "./dist/index.cjs", 7 | "module": "./dist/index.mjs", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/index.mjs", 11 | "require": "./dist/index.cjs" 12 | }, 13 | "./package.json": "./package.json" 14 | }, 15 | "types": "./dist/index.d.cts", 16 | "keywords": [ 17 | "connect", 18 | "mongo", 19 | "mongodb", 20 | "session", 21 | "express" 22 | ], 23 | "contributors": [ 24 | "Casey Banner ", 25 | "Jerome Desboeufs ", 26 | "MC Or " 27 | ], 28 | "license": "MIT", 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/jdesboeufs/connect-mongo.git" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/jdesboeufs/connect-mongo/issues" 35 | }, 36 | "scripts": { 37 | "typecheck": "tsc --noEmit", 38 | "build": "tsdown", 39 | "fix": "run-s fix:*", 40 | "fix:prettier": "prettier \"src/**/*.ts\" --write", 41 | "fix:lint": "eslint --cache src --ext .ts --fix", 42 | "test": "run-s test:*", 43 | "test:lint": "eslint --cache src --ext .ts", 44 | "test:prettier": "prettier \"src/**/*.ts\" --list-different", 45 | "test:unit": "c8 ava", 46 | "watch:test": "c8 ava --watch", 47 | "cov": "run-s test:unit cov:html cov:lcov && open-cli coverage/index.html", 48 | "cov:html": "c8 report --reporter=html", 49 | "cov:lcov": "c8 report --reporter=lcov", 50 | "cov:send": "run-s cov:lcov && codecov", 51 | "cov:check": "c8 report --check-coverage", 52 | "doc": "run-s doc:html && open-cli dist/docs/index.html", 53 | "doc:html": "typedoc --entryPointStrategy expand --entryPoints src --exclude **/*.spec.ts --out dist/docs", 54 | "doc:json": "typedoc --entryPointStrategy expand --entryPoints src --exclude **/*.spec.ts --json dist/docs/typedoc.json", 55 | "doc:publish": "gh-pages -m \"[ci skip] Updates\" -d dist/docs", 56 | "prepare-release": "run-s test cov:check doc:html version doc:publish", 57 | "prepare": "husky", 58 | "tls:setup": "bash ./scripts/generate-mongo-tls.sh" 59 | }, 60 | "engines": { 61 | "node": ">=20.8.0" 62 | }, 63 | "peerDependencies": { 64 | "express-session": "^1.17.1", 65 | "mongodb": ">=5.0.0" 66 | }, 67 | "dependencies": { 68 | "debug": "^4.4.3", 69 | "kruptein": "3.0.8" 70 | }, 71 | "devDependencies": { 72 | "@ava/typescript": "^6.0.0", 73 | "@commitlint/cli": "^20.1.0", 74 | "@commitlint/config-conventional": "^20.0.0", 75 | "@eslint/eslintrc": "^3.3.3", 76 | "@eslint/js": "^9.39.1", 77 | "@types/debug": "^4.1.12", 78 | "@types/express": "^4.17.25", 79 | "@types/express-session": "^1.18.2", 80 | "@types/node": "^24.10.1", 81 | "@types/supertest": "^6.0.3", 82 | "@typescript-eslint/eslint-plugin": "^8.48.0", 83 | "@typescript-eslint/parser": "^8.48.0", 84 | "ava": "^6.4.1", 85 | "c8": "^10.1.3", 86 | "codecov": "^3.8.2", 87 | "eslint": "^9.39.1", 88 | "eslint-config-prettier": "^10.1.8", 89 | "eslint-plugin-eslint-comments": "^3.2.0", 90 | "eslint-plugin-prettier": "^5.5.4", 91 | "express": "^4.21.2", 92 | "express-session": "^1.18.2", 93 | "gh-pages": "^6.3.0", 94 | "husky": "^9.1.7", 95 | "lint-staged": "^16.2.7", 96 | "mongodb": "^7.0.0", 97 | "npm-run-all": "^4.1.5", 98 | "open-cli": "^8.0.0", 99 | "prettier": "^3.7.3", 100 | "standard-version": "^9.5.0", 101 | "supertest": "^7.1.4", 102 | "tsdown": "^0.16.8", 103 | "typedoc": "^0.28.15", 104 | "typescript": "^5.9.3" 105 | }, 106 | "files": [ 107 | "dist", 108 | "!**/*.spec.*", 109 | "!**/*.json", 110 | "!dist/*/test/*", 111 | "CHANGELOG.md", 112 | "LICENSE", 113 | "README.md" 114 | ], 115 | "prettier": { 116 | "singleQuote": true, 117 | "semi": false, 118 | "trailingComma": "es5" 119 | }, 120 | "c8": { 121 | "include": [ 122 | "build/**/*.js" 123 | ], 124 | "exclude": [ 125 | "build/**/*.{spec,test}.js", 126 | "build/**/test/**/*.js", 127 | "node_modules/**" 128 | ], 129 | "reporter": [ 130 | "text", 131 | "lcov", 132 | "html" 133 | ], 134 | "all": true, 135 | "skipFull": false, 136 | "check-coverage": true, 137 | "branches": 85, 138 | "functions": 85, 139 | "lines": 85, 140 | "statements": 85 141 | }, 142 | "lint-staged": { 143 | "**/*.{ts,js}": [ 144 | "eslint --cache --fix" 145 | ] 146 | }, 147 | "standard-version": { 148 | "skip": { 149 | "changelog": true 150 | } 151 | }, 152 | "packageManager": "npm@11.6.2" 153 | } 154 | -------------------------------------------------------------------------------- /src/lib/cryptoAdapters.ts: -------------------------------------------------------------------------------- 1 | import util from 'node:util' 2 | import { webcrypto } from 'node:crypto' 3 | import kruptein from 'kruptein' 4 | 5 | export interface CryptoAdapter { 6 | encrypt: (unencryptedPayload: string) => Promise 7 | decrypt: (encryptedPayload: string) => Promise 8 | } 9 | 10 | export type CryptoOptions = { 11 | secret: false | string 12 | algorithm?: string 13 | hashing?: string 14 | encodeas?: string 15 | key_size?: number 16 | iv_size?: number 17 | at_size?: number 18 | } 19 | 20 | export type ConcretCryptoOptions = Required 21 | 22 | /* eslint-disable camelcase */ 23 | export const defaultCryptoOptions: ConcretCryptoOptions = { 24 | secret: false, 25 | algorithm: 'aes-256-gcm', 26 | hashing: 'sha512', 27 | encodeas: 'base64', 28 | key_size: 32, 29 | iv_size: 16, 30 | at_size: 16, 31 | } 32 | /* eslint-enable camelcase */ 33 | 34 | export const createKrupteinAdapter = ( 35 | options: CryptoOptions 36 | ): CryptoAdapter => { 37 | const merged: ConcretCryptoOptions = { 38 | ...defaultCryptoOptions, 39 | ...options, 40 | } 41 | if (!merged.secret) { 42 | throw new Error('createKrupteinAdapter requires a non-empty secret') 43 | } 44 | const instance = kruptein(merged) 45 | const encrypt = util.promisify(instance.set).bind(instance) 46 | const decrypt = util.promisify(instance.get).bind(instance) 47 | 48 | return { 49 | async encrypt(plaintext: string): Promise { 50 | const ciphertext = await encrypt(merged.secret as string, plaintext) 51 | return String(ciphertext) 52 | }, 53 | async decrypt(ciphertext: string): Promise { 54 | const plaintext = await decrypt(merged.secret as string, ciphertext) 55 | if (typeof plaintext === 'string') { 56 | return plaintext 57 | } 58 | return JSON.stringify(plaintext) 59 | }, 60 | } 61 | } 62 | 63 | export type WebCryptoEncoding = 'base64' | 'base64url' | 'hex' 64 | 65 | export type WebCryptoAdapterOptions = { 66 | secret: string | ArrayBuffer | ArrayBufferView 67 | ivLength?: number 68 | encoding?: WebCryptoEncoding 69 | algorithm?: 'AES-GCM' | 'AES-CBC' 70 | salt?: string | ArrayBuffer | ArrayBufferView 71 | iterations?: number 72 | } 73 | 74 | const encoder = new TextEncoder() 75 | const decoder = new TextDecoder() 76 | 77 | const toUint8Array = ( 78 | input: string | ArrayBuffer | ArrayBufferView 79 | ): Uint8Array => { 80 | if (typeof input === 'string') { 81 | return encoder.encode(input) 82 | } 83 | if (ArrayBuffer.isView(input)) { 84 | return new Uint8Array(input.buffer, input.byteOffset, input.byteLength) 85 | } 86 | if (input instanceof ArrayBuffer) { 87 | return new Uint8Array(input) 88 | } 89 | throw new TypeError('Unsupported secret type for Web Crypto adapter') 90 | } 91 | 92 | const encodeBytes = ( 93 | bytes: Uint8Array, 94 | encoding: WebCryptoEncoding 95 | ): string => { 96 | switch (encoding) { 97 | case 'hex': 98 | return Buffer.from(bytes).toString('hex') 99 | case 'base64url': 100 | return Buffer.from(bytes).toString('base64url') 101 | case 'base64': 102 | default: 103 | return Buffer.from(bytes).toString('base64') 104 | } 105 | } 106 | 107 | const decodeBytes = ( 108 | payload: string, 109 | encoding: WebCryptoEncoding 110 | ): Uint8Array => { 111 | switch (encoding) { 112 | case 'hex': 113 | return new Uint8Array(Buffer.from(payload, 'hex')) 114 | case 'base64url': 115 | return new Uint8Array(Buffer.from(payload, 'base64url')) 116 | case 'base64': 117 | default: 118 | return new Uint8Array(Buffer.from(payload, 'base64')) 119 | } 120 | } 121 | 122 | export const createWebCryptoAdapter = ({ 123 | secret, 124 | ivLength, 125 | encoding = 'base64', 126 | algorithm = 'AES-GCM', 127 | salt, 128 | iterations = 310000, 129 | }: WebCryptoAdapterOptions): CryptoAdapter => { 130 | if (!secret) { 131 | throw new Error('createWebCryptoAdapter requires a secret') 132 | } 133 | const { subtle } = webcrypto 134 | 135 | if (!subtle?.decrypt || !webcrypto?.getRandomValues) { 136 | throw new Error('Web Crypto API is not available in this runtime') 137 | } 138 | 139 | const resolvedIvLength = ivLength ?? (algorithm === 'AES-GCM' ? 12 : 16) 140 | const resolvedSalt = 141 | salt ?? encoder.encode('connect-mongo:webcrypto-default-salt') 142 | 143 | const deriveKey = async () => { 144 | const secretBytes = toUint8Array(secret) 145 | const baseKey = await subtle.importKey( 146 | 'raw', 147 | secretBytes, 148 | 'PBKDF2', 149 | false, 150 | ['deriveKey'] 151 | ) 152 | const saltBytes = 153 | typeof resolvedSalt === 'string' 154 | ? encoder.encode(resolvedSalt) 155 | : toUint8Array(resolvedSalt) 156 | return subtle.deriveKey( 157 | { 158 | name: 'PBKDF2', 159 | salt: saltBytes, 160 | iterations, 161 | hash: 'SHA-256', 162 | }, 163 | baseKey, 164 | { name: algorithm, length: 256 }, 165 | false, 166 | ['encrypt', 'decrypt'] 167 | ) 168 | } 169 | 170 | const keyPromise = deriveKey() 171 | 172 | return { 173 | async encrypt(plaintext: string): Promise { 174 | const key = await keyPromise 175 | const iv = webcrypto.getRandomValues(new Uint8Array(resolvedIvLength)) 176 | const data = encoder.encode(plaintext) 177 | const encrypted = await subtle.encrypt({ name: algorithm, iv }, key, data) 178 | const cipherBytes = new Uint8Array(encrypted) 179 | const combined = new Uint8Array(resolvedIvLength + cipherBytes.byteLength) 180 | combined.set(iv, 0) 181 | combined.set(cipherBytes, resolvedIvLength) 182 | return encodeBytes(combined, encoding) 183 | }, 184 | async decrypt(ciphertext: string): Promise { 185 | const key = await keyPromise 186 | const combined = decodeBytes(ciphertext, encoding) 187 | const iv = combined.slice(0, resolvedIvLength) 188 | const data = combined.slice(resolvedIvLength) 189 | const decrypted = await subtle.decrypt({ name: algorithm, iv }, key, data) 190 | return decoder.decode(decrypted) 191 | }, 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/test/upgrade-compat.spec.ts: -------------------------------------------------------------------------------- 1 | import test, { type ExecutionContext } from 'ava' 2 | import express from 'express' 3 | import session from 'express-session' 4 | import request from 'supertest' 5 | import { execFileSync } from 'node:child_process' 6 | import { mkdtempSync, rmSync, existsSync, symlinkSync } from 'node:fs' 7 | import { MongoClient, Collection } from 'mongodb' 8 | import { createRequire } from 'node:module' 9 | import { tmpdir } from 'node:os' 10 | import { dirname, join } from 'node:path' 11 | import { fileURLToPath } from 'node:url' 12 | 13 | const SESSION_SECRET = 'upgrade-session-secret' 14 | const CRYPTO_SECRET = 'upgrade-crypto-secret' 15 | const COOKIE_MAX_AGE_MS = 5 * 60 * 1000 16 | const MONGO_URL = 17 | process.env.MONGO_URL ?? 'mongodb://root:example@127.0.0.1:27017' 18 | const FIXTURE_TGZ = join( 19 | dirname(fileURLToPath(import.meta.url)), 20 | '..', 21 | '..', 22 | 'test-fixtures', 23 | 'connect-mongo-5.1.0.tgz' 24 | ) 25 | const PROJECT_NODE_MODULES = join( 26 | dirname(fileURLToPath(import.meta.url)), 27 | '..', 28 | '..', 29 | 'node_modules' 30 | ) 31 | 32 | type StoreCtor = { 33 | create: (opts: any) => any 34 | } 35 | 36 | const getTTLIndex = async (collection: Collection) => { 37 | const indexes = await collection.listIndexes().toArray() 38 | return indexes.find((idx) => idx.name === 'expires_1') 39 | } 40 | 41 | const ensureFixturePresent = (t: ExecutionContext) => { 42 | if (!existsSync(FIXTURE_TGZ)) { 43 | t.fail( 44 | `Missing ${FIXTURE_TGZ}. Run "npm pack connect-mongo@5.1.0 --pack-destination test-fixtures --cache ./.npm-cache".` 45 | ) 46 | } 47 | } 48 | 49 | const unpackOldPackage = () => { 50 | const tmpDir = mkdtempSync(join(tmpdir(), 'connect-mongo-5.1.0-')) 51 | // Node core lacks tar extraction; rely on the system tar available in dev envs. 52 | execFileSync('tar', ['-xzf', FIXTURE_TGZ, '-C', tmpDir]) 53 | const packageRoot = join(tmpDir, 'package') 54 | const linkedNodeModules = join(packageRoot, 'node_modules') 55 | if (!existsSync(linkedNodeModules) && existsSync(PROJECT_NODE_MODULES)) { 56 | symlinkSync(PROJECT_NODE_MODULES, linkedNodeModules, 'dir') 57 | } 58 | return { 59 | packageRoot, 60 | cleanup: () => rmSync(tmpDir, { recursive: true, force: true }), 61 | } 62 | } 63 | 64 | const loadOldStore = (): { ctor: StoreCtor; cleanup: () => void } => { 65 | const { packageRoot, cleanup } = unpackOldPackage() 66 | const requireFromPkg = createRequire(join(packageRoot, 'package.json')) 67 | const mod = requireFromPkg(packageRoot) 68 | return { ctor: (mod?.default ?? mod) as StoreCtor, cleanup } 69 | } 70 | 71 | const buildApp = (Store: StoreCtor, storeOpts: Record) => { 72 | const app = express() 73 | const store = Store.create({ 74 | autoRemove: 'native', 75 | stringify: false, 76 | ...storeOpts, 77 | }) 78 | app.use( 79 | session({ 80 | secret: SESSION_SECRET, 81 | resave: false, 82 | saveUninitialized: false, 83 | rolling: true, 84 | cookie: { maxAge: COOKIE_MAX_AGE_MS }, 85 | store, 86 | }) 87 | ) 88 | app.get('/write', (req, res) => { 89 | req.session.views = (req.session.views ?? 0) + 1 90 | req.session.payload = { nested: 'value' } 91 | res.status(200).json({ views: req.session.views }) 92 | }) 93 | app.get('/touch', (req, res) => { 94 | req.session.views = (req.session.views ?? 0) + 1 95 | res.status(200).json({ views: req.session.views }) 96 | }) 97 | app.get('/ping', (req, res) => { 98 | res.status(200).json({ views: req.session?.views ?? null }) 99 | }) 100 | return { app, store } 101 | } 102 | 103 | const seedOldSession = async ( 104 | t: ExecutionContext, 105 | collection: Collection, 106 | store: any, 107 | app: express.Express 108 | ) => { 109 | await (store.collectionP as Promise) 110 | const firstRes = await request(app).get('/write').expect(200) 111 | const cookie = firstRes.headers['set-cookie']?.[0] 112 | t.truthy(cookie, 'old store should issue session cookie') 113 | const trimmedCookie = cookie?.split(';')[0] ?? '' 114 | const secondRes = await request(app) 115 | .get('/write') 116 | .set('Cookie', trimmedCookie) 117 | .expect(200) 118 | t.is(secondRes.body.views, 2) 119 | const ttlIndex = await getTTLIndex(collection) 120 | const doc = await collection.findOne({}) 121 | return { cookie: trimmedCookie, ttlIndex, doc } 122 | } 123 | 124 | const runUpgradeScenario = async (t: ExecutionContext, crypto: boolean) => { 125 | ensureFixturePresent(t) 126 | const client = await MongoClient.connect(MONGO_URL).catch((err: unknown) => { 127 | t.log(`Mongo unavailable at ${MONGO_URL}: ${String(err)}`) 128 | return null 129 | }) 130 | if (!client) return 131 | 132 | const dbName = `compat-upgrade-${crypto ? 'crypto' : 'plain'}-${Date.now()}` 133 | const collectionName = `sessions-${crypto ? 'crypto' : 'plain'}` 134 | const db = client.db(dbName) 135 | await db.dropDatabase().catch(() => undefined) 136 | const collection = db.collection(collectionName) 137 | 138 | const { ctor: OldStore, cleanup: cleanupPkg } = loadOldStore() 139 | let oldStore: any | undefined 140 | let newStore: any | undefined 141 | 142 | try { 143 | const { app: oldApp, store } = buildApp(OldStore, { 144 | mongoUrl: MONGO_URL, 145 | dbName, 146 | collectionName, 147 | touchAfter: 1, 148 | crypto: crypto ? { secret: CRYPTO_SECRET } : undefined, 149 | }) 150 | oldStore = store 151 | const { 152 | cookie, 153 | ttlIndex: ttlBefore, 154 | doc: docBefore, 155 | } = await seedOldSession(t, collection, oldStore, oldApp) 156 | t.truthy(ttlBefore, 'TTL index should exist before upgrade') 157 | t.truthy(docBefore?.expires, 'session should have an expires value') 158 | 159 | await oldStore.close() 160 | 161 | const { app: newApp, store: upgradedStore } = buildApp( 162 | (await import('../lib/MongoStore.js')).default, 163 | { 164 | client, 165 | dbName, 166 | collectionName, 167 | touchAfter: 1, 168 | crypto: crypto ? { secret: CRYPTO_SECRET } : undefined, 169 | } 170 | ) 171 | newStore = upgradedStore 172 | 173 | const ping = await request(newApp) 174 | .get('/ping') 175 | .set('Cookie', cookie) 176 | .expect(200) 177 | t.is(ping.body.views, 2, 'upgrade should read existing session') 178 | 179 | const touch = await request(newApp) 180 | .get('/touch') 181 | .set('Cookie', cookie) 182 | .expect(200) 183 | t.true(touch.body.views >= 3, 'upgrade should be able to update session') 184 | 185 | const docAfter = await collection.findOne({}) 186 | t.truthy(docAfter?.expires) 187 | if (docAfter?.expires) { 188 | const delta = Math.abs( 189 | docAfter.expires.getTime() - (Date.now() + COOKIE_MAX_AGE_MS) 190 | ) 191 | t.true( 192 | delta < 10_000, 193 | 'expires should respect cookie.maxAge after upgrade' 194 | ) 195 | } 196 | 197 | const ttlAfter = await getTTLIndex(collection) 198 | t.truthy(ttlAfter, 'TTL index should persist after upgrade') 199 | t.is(ttlBefore?.expireAfterSeconds, ttlAfter?.expireAfterSeconds) 200 | t.deepEqual(ttlBefore?.key, ttlAfter?.key) 201 | } finally { 202 | await db.dropDatabase().catch(() => undefined) 203 | await client.close().catch(() => undefined) 204 | if (newStore) { 205 | await newStore.close().catch(() => undefined) 206 | } 207 | if (oldStore) { 208 | await oldStore.close().catch(() => undefined) 209 | } 210 | cleanupPkg() 211 | } 212 | } 213 | 214 | test.serial('upgrade from 5.1.0 preserves non-crypto sessions', async (t) => { 215 | await runUpgradeScenario(t, false) 216 | }) 217 | 218 | test.serial('upgrade from 5.1.0 preserves crypto sessions', async (t) => { 219 | await runUpgradeScenario(t, true) 220 | }) 221 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [6.0.0] - 2025-12-01 10 | 11 | - **Breaking:** Requires Node.js 20.8+ (aligns with MongoDB driver 5–7 support). 12 | - **Compatibility:** Supported/tested matrix: Node 20/22/24 + MongoDB driver 5.x–7.x + MongoDB server 4.4–8.0 (peer range remains `>=5.0.0`). 13 | - **Added:** Optional `timestamps` flag to record `createdAt`/`updatedAt` on session documents for auditing while keeping the default schema unchanged. 14 | - **Added:** Pluggable `cryptoAdapter` interface with helpers `createWebCryptoAdapter` (AES-GCM via Web Crypto API) and `createKrupteinAdapter`; legacy `crypto` options are auto-wrapped and mutually exclusive with `cryptoAdapter` to avoid ambiguity. 15 | - **Fixed:** `store.clear()` now uses `deleteMany({})` instead of `collection.drop()`, preserving TTL indexes and treating `NamespaceNotFound` as success so clears are idempotent. 16 | - **Fixed:** Decryption failures in `get()` now short-circuit after the first callback, preventing double-callback regressions when the crypto secret is wrong. 17 | - **Packaging:** npm package now ships dual ESM/CJS bundles via `tsdown`, with an explicit exports map and cleaned type declarations (`.d.mts`/`.d.cts`). 18 | - **Types:** `MongoStore` and option hooks are strongly typed to avoid `any` leaks. 19 | 20 | ## [5.1.0] - 2023-10-14 21 | 22 | ### Changed 23 | 24 | - Extend `mongodb` peer dependency allowed versions to `6.x` 25 | - Upgrade dependency 26 | 27 | ## [5.0.0] - 2023-03-14 28 | 29 | ### **BREAKING CHANGES** 30 | 31 | - Upgraded peer dependency `mongodb` to 5.0.0 32 | - Change `engines` to require Node 12.9 or newer, matching the upgrade to `mongodb` that occurred in `v4.5.0` 33 | 34 | ### Fixed 35 | 36 | - Declare `express-session` as a peer dependency. 37 | 38 | ## [4.6.0] - 2021-09-17 39 | 40 | ### Changed 41 | 42 | - Moved `mongodb` to a peer dependency (and also as a dev dependency for `connect-mongo` developers). `connect-mongo` is no longer pinned to a specific version of `mongodb`. This allows end users to avoid errors due to Typescript definition changes when moving to new versions of `mongodb`. Users can use any version of `mongodb` that provides a compatible (non-breaking) interface to `mongodb ^4.1.0`. Tested on `mongodb` `4.1.0` and `4.1.1`. Should fix: [#433](https://github.com/jdesboeufs/connect-mongo/issues/433) [#434](https://github.com/jdesboeufs/connect-mongo/issues/434) [#436](https://github.com/jdesboeufs/connect-mongo/issues/436) 43 | 44 | ### Fixed 45 | 46 | - Fixed "Callback was already called" when some code throws immediately after calling the set function 47 | 48 | ## [4.5.0] - 2021-08-17 49 | 50 | ### **BREAKING CHANGES** 51 | 52 | - Drop Node 10 support 53 | 54 | ### Changed 55 | 56 | - Upgrade `mongodb` to V4 [#422] [#426] 57 | 58 | ### Fixed 59 | 60 | - Move `writeConcern` away from top-level option to fix deprecation warning [#422](https://github.com/jdesboeufs/connect-mongo/issues/422) 61 | 62 | ## [4.4.1] - 2021-03-23 63 | 64 | ### Fixed 65 | 66 | - `store.all()` method not working with encrypted store [#410](https://github.com/jdesboeufs/connect-mongo/issues/410) [#411](https://github.com/jdesboeufs/connect-mongo/issues/411) 67 | - Update and unpin `mongodb` dependency due to upstream fix has been deployed [#409](https://github.com/jdesboeufs/connect-mongo/issues/409) 68 | 69 | ## [4.4.0] - 2021-03-11 70 | 71 | ### **BREAKING CHANGES** 72 | 73 | - Use `export =` for better cjs require without `.default` 74 | 75 | ### Added 76 | 77 | - Add typescript example 78 | 79 | ## [4.3.1] - 2021-03-09 80 | 81 | ### Fixed 82 | 83 | - Fix incorrect assertion checking after adding `client` options 84 | 85 | ## [4.3.0] - 2021-03-08 86 | 87 | ### Added 88 | 89 | - Add `client` option for non-promise client 90 | 91 | ## [4.2.2] - 2021-03-02 92 | 93 | ### Fixed 94 | 95 | - Fix crypto parsing error by upgrading `kruptein` to `v3.0.0` and change encodeas to `base64` 96 | 97 | ## [4.2.0] - 2021-02-24 98 | 99 | ### Added 100 | 101 | - Added mongoose example 102 | - Revert `createAutoRemoveIdx` and add back `autoRemove` and `autoRemoveInterval` 103 | 104 | ### Fixed 105 | 106 | - Use `matchedCount` instead of `modifiedCount` to avoid throwing exceptions when nothing to modify [#390](https://github.com/jdesboeufs/connect-mongo/issues/390) 107 | - Fixed `Warning: Accessing non-existent property 'MongoError' of module exports inside circular dependency` by downgrade to `mongodb@3.6.3` 108 | - Revert update session when touch #351 109 | - Fix cannot read property `lastModified` of null 110 | - Fix TS typing error 111 | 112 | ## [4.1.0] - 2021-02-22 113 | 114 | ### **BREAKING CHANGES** 115 | 116 | - Support Node.Js 10.x, 12.x and 14.x and drop older support. 117 | - Review method to connect to MongoDB and keep only `mongoUrl` and `clientPromise` options. 118 | - Remove the "Remove expired sessions compatibility mode". Now library user can choose to create auto remove index on startup or not. 119 | - Remove `fallbackMemory` options. 120 | - Rewrite the library and test case using typescript. 121 | 122 | > Checkout the complete [migration guide](MIGRATION_V4.md) for more details. 123 | 124 | ## [3.2.0] - 2019-11-29 125 | 126 | ### Added 127 | 128 | - Add dbName option (#343) 129 | 130 | ### Fixed 131 | 132 | - Add missing `secret` option to TS definition (#342) 133 | 134 | ## [3.1.2] - 2019-11-01 135 | 136 | ### Fixed 137 | 138 | - Add @types/ dev dependencies for tsc. fixes #340 (#341) 139 | 140 | ## [3.1.1] - 2019-10-30 141 | 142 | ### Added 143 | 144 | - Add TS type definition 145 | 146 | ## [3.1.0] - 2019-10-23 147 | 148 | ### Added 149 | 150 | - Added `useUnifiedTopology=true` to mongo options 151 | 152 | ### Changed 153 | 154 | - Refactor merge config logic 155 | - chore: update depns (#326) 156 | 157 | ## [3.0.0] - 2019-06-17 158 | 159 | ### **BREAKING CHANGES** 160 | 161 | - Drop Node.js 4 & 6 support 162 | - Upgrade `mongoose` to v5 and `mongodb` to v3 and drop old version support 163 | - Replace deprecated mongo operation 164 | - MongoStore need to supply client/clientPromise instead of db/dbPromise due to depns upgrade 165 | 166 | ## Added 167 | 168 | - Add Node.js 10 & 12 support 169 | - Implement store.all function (#291) 170 | - Add option `writeOperationOptions` (#295) 171 | - Add Transparent crypto support (#314) 172 | 173 | ## Changed 174 | 175 | * Change test framework from Mocha to Jest 176 | * Change linter from `xo` to `eslint` 177 | 178 | ## [2.0.3] - 2018-12-03 179 | 180 | ## Fixed 181 | 182 | - Fixed interval autoremove mode to use current date with every interval (#304, #305) (jlampise) 183 | 184 | ## [2.0.2] - 2018-11-20 185 | 186 | ## Fixed 187 | 188 | - Fxi #300 DeprecationWarning: collection.remove is deprecated. Use deleteOne, deleteMany, or bulkWrite instead 189 | - Fxi #297 DeprecationWarning: collection.update is deprecated. Use updateOne, updateMany, or bulkWrite instead 190 | 191 | ## [2.0.1] - 2018-01-04 192 | 193 | ## Fixed 194 | 195 | - Fixed #271 TypeError: cb is not a function (brainthinks) 196 | 197 | ## [2.0.0] - 2017-10-09 198 | 199 | ### **BREAKING CHANGES** 200 | 201 | * __Drop__ Node.js 0.12 and io.js support 202 | * __Drop__ MongoDB 2.x support 203 | * __Drop__ mongodb driver < 2.0.36 support 204 | * __Drop__ mongoose < 4.1.2 support 205 | 206 | ## Changed 207 | 208 | * __Fix__ `ensureIndex` deprecation warning ([#268](https://github.com/jdesboeufs/connect-mongo/issues/268), [#269](https://github.com/jdesboeufs/connect-mongo/pulls/269), [#270](https://github.com/jdesboeufs/connect-mongo/pulls/270)) 209 | * Improve `get()` ([#246](https://github.com/jdesboeufs/connect-mongo/pulls/246)) 210 | * Pass session in `touch` event 211 | * Remove `bluebird` from dependencies 212 | 213 | 214 | 215 | 1.3.2 / 2016-07-27 216 | ================= 217 | 218 | * __Fix__ #228 Broken with mongodb@1.x 219 | 220 | 1.3.1 / 2016-07-23 221 | ================= 222 | 223 | * Restrict `bluebird` accepted versions to 3.x 224 | 225 | 1.3.0 / 2016-07-23 226 | ================= 227 | 228 | * __Add__ `create` and `update` events ([#215](https://github.com/jdesboeufs/connect-mongo/issues/215)) 229 | * Extend `mongodb` compatibility to `2.x` 230 | 231 | 1.2.1 / 2016-06-20 232 | ================= 233 | 234 | * __Fix__ bluebird warning (Awk34) 235 | 236 | 1.2.0 / 2016-05-13 237 | ================= 238 | 239 | * Accept `dbPromise` as connection param 240 | * __Add__ `close()` method to close current connection 241 | 242 | 1.1.0 / 2015-12-24 243 | ================= 244 | 245 | * Support mongodb `2.1.x` 246 | 247 | 1.0.2 / 2015-12-18 248 | ================= 249 | 250 | * Enforce entry-points 251 | 252 | 1.0.1 / 2015-12-17 253 | ================= 254 | 255 | * __Fix__ entry-point 256 | 257 | 1.0.0 (deprecated) / 2015-12-17 258 | ================== 259 | 260 | __Breaking changes:__ 261 | * __For older Node.js version (`< 4.0`), the module must be loaded using `require('connect-mongo/es5')`__ 262 | * __Drop__ `hash` option (advanced) 263 | 264 | __Others changes:__ 265 | * __Add__ `transformId` option to allow custom transformation on session id (advanced) 266 | * __Rewrite in ES6__ (w/ fallback) 267 | * Update dependencies 268 | * Improve compatibility 269 | 270 | 0.8.2 / 2015-07-14 271 | ================== 272 | 273 | * Bug fixes and improvements (whitef0x0, TimothyGu, behcet-li) 274 | 275 | 276 | 0.8.1 / 2015-04-21 277 | ================== 278 | 279 | * __Fix__ initialization when a connecting `mongodb` `2.0.x` instance is given (1999) 280 | 281 | 282 | 0.8.0 / 2015-03-24 283 | ================== 284 | 285 | * __Add__ `touchAfter` option to enable lazy update behavior on `touch()` method (rafaelcardoso) 286 | * __Add__ `fallbackMemory` option to switch to `MemoryStore` in some case. 287 | 288 | 289 | 0.7.0 / 2015-01-24 290 | ================== 291 | 292 | * __Add__ `touch()` method to be fully compliant with `express-session` `>= 1.10` (rafaelcardoso) 293 | 294 | 295 | 0.6.0 / 2015-01-12 296 | ================== 297 | 298 | * __Add__ `ttl` option 299 | * __Add__ `autoRemove` option 300 | * __Deprecate__ `defaultExpirationTime` option. Use `ttl` instead (in seconds) 301 | 302 | 303 | 0.5.3 / 2014-12-30 304 | ================== 305 | 306 | * Make callbacks optional 307 | 308 | 309 | 0.5.2 / 2014-12-29 310 | ================== 311 | 312 | * Extend compatibility to `mongodb` `2.0.x` 313 | 314 | 315 | 0.5.1 / 2014-12-28 316 | ================== 317 | 318 | * [bugfix] #143 Missing Sessions from DB should still make callback (brekkehj) 319 | 320 | 321 | 0.5.0 (deprecated) / 2014-12-25 322 | ================== 323 | 324 | * Accept full-featured [MongoDB connection strings](http://docs.mongodb.org/manual/reference/connection-string/) as `url` + [advanced options](http://mongodb.github.io/node-mongodb-native/1.4/driver-articles/mongoclient.html) 325 | * Re-use existing or upcoming mongoose connection 326 | * [DEPRECATED] `mongoose_connection` is renamed `mongooseConnection` 327 | * [DEPRECATED] `auto_reconnect` is renamed `autoReconnect` 328 | * [BREAKING] `autoReconnect` option is now `true` by default 329 | * [BREAKING] Insert `collection` option in `url` in not possible any more 330 | * [BREAKING] Replace for-testing-purpose `callback` by `connected` event 331 | * Add debug (use with `DEBUG=connect-mongo`) 332 | * Improve error management 333 | * Compatibility with `mongodb` `>= 1.2.0` and `< 2.0.0` 334 | * Fix many bugs 335 | 336 | 337 | 0.4.2 / 2014-12-18 338 | ================== 339 | 340 | * Bumped mongodb version from 1.3.x to 1.4.x (B0k0) 341 | * Add `sid` hash capability (ZheFeng) 342 | * Add `serialize` and `unserialize` options (ksheedlo) 343 | 344 | 345 | 0.3.3 / 2013-07-04 346 | ================== 347 | 348 | * Merged a change which reduces data duplication 349 | 350 | 351 | 0.3.0 / 2013-01-20 352 | ================== 353 | 354 | * Merged several changes by Ken Pratt, including Write Concern support 355 | * Updated to `mongodb` version 1.2 356 | 357 | 0.2.0 / 2012-09-09 358 | ================== 359 | 360 | * Integrated pull request for `mongoose_connection` option 361 | * Move to mongodb 1.0.x 362 | 363 | 0.1.5 / 2010-07-07 364 | ================== 365 | 366 | * Made collection setup more robust to avoid race condition 367 | 368 | 369 | 0.1.4 / 2010-06-28 370 | ================== 371 | 372 | * Added session expiry 373 | 374 | 375 | 0.1.3 / 2010-06-27 376 | ================== 377 | 378 | * Added url support 379 | 380 | 381 | 0.1.2 / 2010-05-18 382 | ================== 383 | 384 | * Added auto_reconnect option 385 | 386 | 387 | 0.1.1 / 2010-03-18 388 | ================== 389 | 390 | * Fixed authentication 391 | 392 | 393 | 0.1.0 / 2010-03-08 394 | ================== 395 | 396 | * Initial release 397 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # connect-mongo 2 | 3 | MongoDB session store for [Connect](https://github.com/senchalabs/connect) and [Express](http://expressjs.com/) written in Typescript. 4 | 5 | [![npm version](https://img.shields.io/npm/v/connect-mongo.svg)](https://www.npmjs.com/package/connect-mongo) 6 | [![downloads](https://img.shields.io/npm/dm/connect-mongo.svg)](https://www.npmjs.com/package/connect-mongo) 7 | [![Sanity check](https://github.com/jdesboeufs/connect-mongo/actions/workflows/sanity.yml/badge.svg)](https://github.com/jdesboeufs/connect-mongo/actions/workflows/sanity.yml) 8 | [![coverage](https://codecov.io/gh/jdesboeufs/connect-mongo/branch/master/graph/badge.svg)](https://app.codecov.io/gh/jdesboeufs/connect-mongo) 9 | 10 | > Breaking change in V4 and rewritten the whole project using Typescript. Please checkout the [migration guide](MIGRATION_V4.md) and [changelog](CHANGELOG.md) for details. 11 | 12 | - [Install](#install) 13 | - [Compatibility](#compatibility) 14 | - [Usage](#usage) 15 | - [Express or Connect integration](#express-or-connect-integration) 16 | - [Connection to MongoDB](#connection-to-mongodb) 17 | - [Known issues](#known-issues) 18 | - [Native autoRemove causing error on close](#native-autoremove-causing-error-on-close) 19 | - [MongoError exports circular dependency](#mongoerror-exports-circular-dependency) 20 | - [Existing encrypted v3.2.0 sessions are not decrypted correctly by v4](#existing-encrypted-v320-sessions-are-not-decrypted-correctly-by-v4) 21 | - [Events](#events) 22 | - [Session expiration](#session-expiration) 23 | - [Remove expired sessions](#remove-expired-sessions) 24 | - [Set MongoDB to clean expired sessions (default mode)](#set-mongodb-to-clean-expired-sessions-default-mode) 25 | - [Set the compatibility mode](#set-the-compatibility-mode) 26 | - [Disable expired sessions cleaning](#disable-expired-sessions-cleaning) 27 | - [Lazy session update](#lazy-session-update) 28 | - [Transparent encryption/decryption of session data](#transparent-encryptiondecryption-of-session-data) 29 | - [Options](#options) 30 | - [Connection-related options (required)](#connection-related-options-required) 31 | - [More options](#more-options) 32 | - [Crypto-related options](#crypto-related-options) 33 | - [Development](#development) 34 | - [Example application](#example-application) 35 | - [Release](#release) 36 | - [License](#license) 37 | 38 | ## Install 39 | 40 | ``` 41 | npm install connect-mongo 42 | ``` 43 | 44 | * Install `mongodb` alongside `connect-mongo`; it is a required peer dependency so you pick the driver version that matches your cluster. 45 | * If you are upgrading from v3.x to v4, please checkout the [migration guide](./MIGRATION_V4.md) for details. 46 | * If you are upgrading v4.x to latest version, you may check the [example](./example) and [options](#options) for details. 47 | 48 | ## Compatibility 49 | 50 | * Support Express up to `5.0` 51 | * Support [native MongoDB driver](https://www.mongodb.com/docs/drivers/node/current/) `>= 5.x` (peer dependency range `>=5.0.0`, tested in CI with 5.x, 6.x, and 7.x) 52 | * Support Node.js 20 LTS, 22 LTS and 24 (Current LTS) 53 | * Support [MongoDB](https://www.mongodb.com/) server versions `4.4` - `8.0` 54 | 55 | We follow MongoDB's official [Node.js driver compatibility tables](https://www.mongodb.com/docs/drivers/compatibility/?driver-language=javascript&javascript-driver-framework=nodejs) and exercise **every** combination of the versions above (3 Node releases × 3 driver majors × 5 server tags) in CI so that mismatches surface quickly. Note that driver 5.x officially supports Node 20, while Node 22/24 coverage relies on driver 6.x/7.x, matching the upstream guidance. 56 | 57 | For extended compatibility, see previous versions [v3.x](https://github.com/jdesboeufs/connect-mongo/tree/v3.x). 58 | But please note that we are not maintaining v3.x anymore. 59 | 60 | ## Usage 61 | 62 | ### Express or Connect integration 63 | 64 | ```js 65 | // CJS 66 | const session = require('express-session'); 67 | const { MongoStore } = require('connect-mongo'); 68 | 69 | app.use(session({ 70 | secret: 'foo', 71 | store: MongoStore.create(options) 72 | })); 73 | ``` 74 | 75 | ```ts 76 | // ESM or TS 77 | import session from 'express-session' 78 | import MongoStore from 'connect-mongo' 79 | 80 | app.use(session({ 81 | secret: 'foo', 82 | store: MongoStore.create(options) 83 | })); 84 | ``` 85 | 86 | ### Connection to MongoDB 87 | 88 | In many circumstances, `connect-mongo` will not be the only part of your application which need a connection to a MongoDB database. It could be interesting to re-use an existing connection. 89 | 90 | Alternatively, you can configure `connect-mongo` to establish a new connection. 91 | 92 | #### Create a new connection from a MongoDB connection string 93 | 94 | [MongoDB connection strings](http://docs.mongodb.org/manual/reference/connection-string/) are __the best way__ to configure a new connection. For advanced usage, [more options](http://mongodb.github.io/node-mongodb-native/driver-articles/mongoclient.html#mongoclient-connect-options) can be configured with `mongoOptions` property. 95 | 96 | ```js 97 | // Basic usage 98 | app.use(session({ 99 | store: MongoStore.create({ mongoUrl: 'mongodb://localhost/test-app' }) 100 | })); 101 | 102 | // Advanced usage 103 | app.use(session({ 104 | store: MongoStore.create({ 105 | mongoUrl: 'mongodb://user12345:foobar@localhost/test-app?authSource=admin&w=1', 106 | mongoOptions: advancedOptions // See below for details 107 | }) 108 | })); 109 | ``` 110 | 111 | #### Re-use an existing native MongoDB driver client promise 112 | 113 | In this case, you just have to give your `MongoClient` instance to `connect-mongo`. 114 | 115 | ```js 116 | /* 117 | ** There are many ways to create MongoClient. 118 | ** You should refer to the driver documentation. 119 | */ 120 | 121 | // Database name present in the connection string will be used 122 | app.use(session({ 123 | store: MongoStore.create({ clientPromise }) 124 | })); 125 | 126 | // Explicitly specifying database name 127 | app.use(session({ 128 | store: MongoStore.create({ 129 | clientPromise, 130 | dbName: 'test-app' 131 | }) 132 | })); 133 | ``` 134 | 135 | ## Known issues 136 | 137 | [Known issues](https://github.com/jdesboeufs/connect-mongo/issues?q=is%3Aopen+is%3Aissue+label%3Abug) in GitHub Issues page. 138 | 139 | ### Native autoRemove causing error on close 140 | 141 | - Calling `close()` immediately after creating the session store may cause error when the async index creation is in process when `autoRemove: 'native'`. You may want to manually manage the autoRemove index. [#413](https://github.com/jdesboeufs/connect-mongo/issues/413) 142 | 143 | ### MongoError exports circular dependency 144 | 145 | The following error can be safely ignored from [official reply](https://developer.mongodb.com/community/forums/t/warning-accessing-non-existent-property-mongoerror-of-module-exports-inside-circular-dependency/15411/5). 146 | 147 | ``` 148 | (node:16580) Warning: Accessing non-existent property 'MongoError' of module exports inside circular dependency 149 | (Use `node --trace-warnings ...` to show where the warning was created) 150 | ``` 151 | 152 | ### Existing encrypted v3.2.0 sessions are not decrypted correctly by v4 153 | 154 | v4 cannot decrypt the session encrypted from v3.2 due to a bug. Please take a look on this issue for possible workaround. [#420](https://github.com/jdesboeufs/connect-mongo/issues/420) 155 | 156 | ## Events 157 | 158 | A `MongoStore` instance will emit the following events: 159 | 160 | | Event name | Description | Payload 161 | | ----- | ----- | ----- | 162 | | `create` | A session has been created | `sessionId` | 163 | | `touch` | A session has been touched (but not modified) | `sessionId` | 164 | | `update` | A session has been updated | `sessionId` | 165 | | `set` | A session has been created OR updated _(for compatibility purpose)_ | `sessionId` | 166 | | `destroy` | A session has been destroyed manually | `sessionId` | 167 | 168 | ## Session expiration 169 | 170 | When the session cookie has an expiration date, `connect-mongo` will use it. 171 | 172 | Otherwise, it will create a new one, using `ttl` option. 173 | 174 | ```js 175 | app.use(session({ 176 | store: MongoStore.create({ 177 | mongoUrl: 'mongodb://localhost/test-app', 178 | ttl: 14 * 24 * 60 * 60 // = 14 days. Default 179 | }) 180 | })); 181 | ``` 182 | 183 | __Note:__ Each time a user interacts with the server, its session expiration date is refreshed. 184 | 185 | ## Remove expired sessions 186 | 187 | By default, `connect-mongo` uses MongoDB's TTL collection feature (2.2+) to have mongodb automatically remove expired sessions. But you can change this behavior. 188 | 189 | ### Set MongoDB to clean expired sessions (default mode) 190 | 191 | `connect-mongo` will create a TTL index for you at startup. You MUST have MongoDB 2.2+ and administration permissions. 192 | 193 | ```js 194 | app.use(session({ 195 | store: MongoStore.create({ 196 | mongoUrl: 'mongodb://localhost/test-app', 197 | autoRemove: 'native' // Default 198 | }) 199 | })); 200 | ``` 201 | 202 | __Note:__ If you use `connect-mongo` in a very concurrent environment, you should avoid this mode and prefer setting the index yourself, once! 203 | 204 | ### Set the compatibility mode 205 | 206 | In some cases you can't or don't want to create a TTL index, e.g. Azure Cosmos DB. 207 | 208 | `connect-mongo` will take care of removing expired sessions, using defined interval. 209 | 210 | ```js 211 | app.use(session({ 212 | store: MongoStore.create({ 213 | mongoUrl: 'mongodb://localhost/test-app', 214 | autoRemove: 'interval', 215 | autoRemoveInterval: 10 // In minutes. Default 216 | }) 217 | })); 218 | ``` 219 | 220 | ### Disable expired sessions cleaning 221 | 222 | You are in production environnement and/or you manage the TTL index elsewhere. 223 | 224 | ```js 225 | app.use(session({ 226 | store: MongoStore.create({ 227 | mongoUrl: 'mongodb://localhost/test-app', 228 | autoRemove: 'disabled' 229 | }) 230 | })); 231 | ``` 232 | 233 | ## Lazy session update 234 | 235 | If you are using [express-session](https://github.com/expressjs/session) >= [1.10.0](https://github.com/expressjs/session/releases/tag/v1.10.0) and don't want to resave all the session on database every single time that the user refreshes the page, you can lazy update the session, by limiting a period of time. 236 | 237 | ```js 238 | app.use(express.session({ 239 | secret: 'keyboard cat', 240 | saveUninitialized: false, // don't create session until something stored 241 | resave: false, //don't save session if unmodified 242 | store: MongoStore.create({ 243 | mongoUrl: 'mongodb://localhost/test-app', 244 | touchAfter: 24 * 3600 // time period in seconds 245 | }) 246 | })); 247 | ``` 248 | 249 | by doing this, setting `touchAfter: 24 * 3600` you are saying to the session be updated only one time in a period of 24 hours, does not matter how many request's are made (with the exception of those that change something on the session data) 250 | 251 | 252 | ## Transparent encryption/decryption of session data 253 | 254 | When working with sensitive session data it is [recommended](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Session_Management_Cheat_Sheet.md) to use encryption. 255 | Use the new `cryptoAdapter` option to plug in your encryption strategy. The preferred helper uses the Web Crypto API (AES-GCM): 256 | 257 | ```ts 258 | import MongoStore, { createWebCryptoAdapter } from 'connect-mongo' 259 | 260 | const store = MongoStore.create({ 261 | mongoUrl: 'mongodb://localhost/test-app', 262 | cryptoAdapter: createWebCryptoAdapter({ 263 | secret: process.env.SESSION_SECRET!, 264 | }), 265 | }) 266 | ``` 267 | 268 | If you need the legacy [kruptein](https://www.npmjs.com/package/kruptein) behavior, wrap it explicitly: 269 | 270 | ```ts 271 | import { createKrupteinAdapter } from 'connect-mongo' 272 | 273 | const store = MongoStore.create({ 274 | mongoUrl: 'mongodb://localhost/test-app', 275 | cryptoAdapter: createKrupteinAdapter({ secret: 'squirrel' }), 276 | }) 277 | ``` 278 | 279 | The legacy `crypto` option still works for backwards compatibility; it is automatically wrapped into a kruptein-based adapter. Supplying both `crypto` and `cryptoAdapter` throws an error so it is clear which path is used. 280 | 281 | ## Options 282 | 283 | ### Connection-related options (required) 284 | 285 | One of the following options should be provided. If more than one option are provided, each option will take precedence over others according to priority. 286 | 287 | |Priority|Option|Description| 288 | |:------:|------|-----------| 289 | |1|`mongoUrl`|A [connection string](https://docs.mongodb.com/manual/reference/connection-string/) for creating a new MongoClient connection. If database name is not present in the connection string, database name should be provided using `dbName` option. | 290 | |2|`clientPromise`|A Promise that is resolved with MongoClient connection. If the connection was established without database name being present in the connection string, database name should be provided using `dbName` option.| 291 | |3|`client`|An existing MongoClient connection. If the connection was established without database name being present in the connection string, database name should be provided using `dbName` option.| 292 | 293 | ### More options 294 | 295 | |Option|Default|Description| 296 | |------|:-----:|-----------| 297 | |`mongoOptions`|`{}`|Options object forwarded to [`MongoClient.connect`](https://www.mongodb.com/docs/drivers/node/current/fundamentals/connection/#mongodb-uri-connection-string), e.g. TLS/SRV settings. Can be used with `mongoUrl` option.| 298 | |`dbName`||A name of database used for storing sessions. Can be used with `mongoUrl`, or `clientPromise` options. Takes precedence over database name present in the connection string.| 299 | |`collectionName`|`'sessions'`|A name of collection used for storing sessions.| 300 | |`ttl`|`1209600`|The maximum lifetime (in seconds) of the session which will be used to set `session.cookie.expires` if it is not yet set. Default is 14 days.| 301 | |`autoRemove`|`'native'`|Behavior for removing expired sessions. Possible values: `'native'`, `'interval'` and `'disabled'`.| 302 | |`autoRemoveInterval`|`10`|Interval (in minutes) used when `autoRemove` option is set to `interval`.| 303 | |`touchAfter`|`0`|Interval (in seconds) between session updates.| 304 | |`timestamps`|`false`|When `true`, stores `createdAt` (on insert) and `updatedAt` (on every write/touch) fields on each session document for auditing. Disabled by default to preserve existing schemas.| 305 | |`stringify`|`true`|If `true`, connect-mongo will serialize sessions using `JSON.stringify` before setting them, and deserialize them with `JSON.parse` when getting them. This is useful if you are using types that MongoDB doesn't support.| 306 | |`serialize`||Custom hook for serializing sessions to MongoDB. This is helpful if you need to modify the session before writing it out.| 307 | |`unserialize`||Custom hook for unserializing sessions from MongoDB. This can be used in scenarios where you need to support different types of serializations (e.g., objects and JSON strings) or need to modify the session before using it in your app.| 308 | |`writeOperationOptions`||Options object to pass to every MongoDB write operation call that supports it (e.g. `update`, `remove`). Useful for adjusting the write concern. Only exception: If `autoRemove` is set to `'interval'`, the write concern from the `writeOperationOptions` object will get overwritten.| 309 | |`transformId`||Transform original `sessionId` in whatever you want to use as storage key.| 310 | |`cryptoAdapter`||Preferred hook for encrypting/decrypting session payloads. Accepts any object with async `encrypt`/`decrypt` functions; helpers `createWebCryptoAdapter` (AES-GCM via Web Crypto API) and `createKrupteinAdapter` are provided.| 311 | |`crypto`||Crypto related options. See below.| 312 | 313 | If you enable `timestamps`, each session document will include `createdAt` (first insert) and `updatedAt` (every subsequent `set`/`touch`) fields. These fields are informational only and do not change TTL behavior. 314 | 315 | ### Crypto-related options (legacy) 316 | 317 | Prefer `cryptoAdapter` for new integrations. The legacy `crypto` options are wrapped internally into a kruptein adapter to preserve backwards compatibility: 318 | 319 | |Option|Default|Description| 320 | |------|:-----:|-----------| 321 | |`secret`|`false`|Enables transparent crypto in accordance with [OWASP session management recommendations](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Session_Management_Cheat_Sheet.md).| 322 | |`algorithm`|`'aes-256-gcm'`|Allows for changes to the default symmetric encryption cipher. See [`crypto.getCiphers()`](https://nodejs.org/api/crypto.html#crypto_crypto_getciphers) for supported algorithms.| 323 | |`hashing`|`'sha512'`|May be used to change the default hashing algorithm. See [`crypto.getHashes()`](https://nodejs.org/api/crypto.html#crypto_crypto_gethashes) for supported hashing algorithms.| 324 | |`encodeas`|`'hex'`|Specify to change the session data cipher text encoding.| 325 | |`key_size`|`32`|When using varying algorithms the key size may be used. Default value `32` is based on the `AES` blocksize.| 326 | |`iv_size`|`16`|This can be used to adjust the default [IV](https://csrc.nist.gov/glossary/term/IV) size if a different algorithm requires a different size.| 327 | |`at_size`|`16`|When using newer `AES` modes such as the default `GCM` or `CCM` an authentication tag size can be defined.| 328 | 329 | ## Development 330 | 331 | ``` 332 | npm install 333 | docker compose up -d 334 | npm run watch:test 335 | ``` 336 | 337 | ### TLS & SRV fixtures 338 | 339 | - Generate local certificates once with `npm run tls:setup` (drops files in `docker/tls`). 340 | - Launch the optional TLS container with `docker compose -f docker-compose.yaml -f docker-compose.tls.yaml --profile tls up -d`. 341 | - Copy `example/.env.example` to `example/.env` and point `MONGO_URL` to the TLS port (`mongodb://root:example@127.0.0.1:27443/example-db?authSource=admin`). Add `MONGO_TLS_CA_FILE=../docker/tls/ca.crt` so the driver trusts the self-signed CA. Set `MONGO_TLS_CERT_KEY_FILE=../docker/tls/client.pem` if you need mutual TLS. 342 | - To exercise SRV/TLS against a managed cluster (Atlas, DocumentDB, CosmosDB), set `MONGO_URL` to your `mongodb+srv://` string and either `MONGO_TLS_CA_FILE` or `NODE_EXTRA_CA_CERTS` to the provider CA bundle. The example scripts automatically reuse those settings in every variant (plain JS, Mongoose, and TS). 343 | 344 | ### Example application 345 | 346 | ``` 347 | # from the repo root 348 | cp example/.env.example example/.env 349 | npm link 350 | cd example 351 | npm link "connect-mongo" # optional if you want live code from this checkout 352 | npm install 353 | npm run start:js 354 | # or npm run start:mongoose / npm run start:ts 355 | ``` 356 | 357 | After the first run you can edit `example/.env` to swap between the local docker fixture, the TLS profile, or any `mongodb+srv://` cluster without changing the code. 358 | 359 | ### Release 360 | 361 | Until the GitHub release workflow lands, do the manual flow: 362 | 363 | 1. Bump version, update `CHANGELOG.md` and README. Commit and push. 364 | 2. Run `npm test && npm run build` (build uses `tsdown` to emit dual ESM/CJS bundles to `dist/`). 365 | 3. Publish: `npm publish` 366 | 4. Tag: `git tag vX.Y.Z && git push --tags` 367 | 368 | ## License 369 | 370 | The MIT License 371 | -------------------------------------------------------------------------------- /src/lib/MongoStore.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import * as session from 'express-session' 3 | import { 4 | Collection, 5 | MongoClient, 6 | MongoClientOptions, 7 | WriteConcernSettings, 8 | } from 'mongodb' 9 | import Debug from 'debug' 10 | import { createKrupteinAdapter } from './cryptoAdapters.js' 11 | import type { CryptoAdapter, CryptoOptions } from './cryptoAdapters.js' 12 | export type { 13 | CryptoAdapter, 14 | CryptoOptions, 15 | WebCryptoAdapterOptions, 16 | } from './cryptoAdapters.js' 17 | export { 18 | createKrupteinAdapter, 19 | createWebCryptoAdapter, 20 | } from './cryptoAdapters.js' 21 | 22 | const debug = Debug('connect-mongo') 23 | 24 | type StoredSessionValue = session.SessionData | string 25 | type Serialize = ( 26 | session: T 27 | ) => StoredSessionValue 28 | type Unserialize = ( 29 | payload: StoredSessionValue 30 | ) => T 31 | type TransformFunctions = { 32 | serialize: Serialize 33 | unserialize: Unserialize 34 | } 35 | 36 | export type ConnectMongoOptions< 37 | T extends session.SessionData = session.SessionData, 38 | > = { 39 | mongoUrl?: string 40 | clientPromise?: Promise 41 | client?: MongoClient 42 | collectionName?: string 43 | mongoOptions?: MongoClientOptions 44 | dbName?: string 45 | ttl?: number 46 | touchAfter?: number 47 | stringify?: boolean 48 | createAutoRemoveIdx?: boolean 49 | autoRemove?: 'native' | 'interval' | 'disabled' 50 | autoRemoveInterval?: number 51 | serialize?: Serialize 52 | unserialize?: Unserialize 53 | writeOperationOptions?: WriteConcernSettings 54 | transformId?: (sid: string) => string 55 | crypto?: CryptoOptions 56 | cryptoAdapter?: CryptoAdapter 57 | timestamps?: boolean 58 | } 59 | 60 | type ConcretConnectMongoOptions< 61 | T extends session.SessionData = session.SessionData, 62 | > = { 63 | mongoUrl?: string 64 | clientPromise?: Promise 65 | client?: MongoClient 66 | collectionName: string 67 | mongoOptions: MongoClientOptions 68 | dbName?: string 69 | ttl: number 70 | createAutoRemoveIdx?: boolean 71 | autoRemove: 'native' | 'interval' | 'disabled' 72 | autoRemoveInterval: number 73 | touchAfter: number 74 | stringify: boolean 75 | timestamps: boolean 76 | serialize?: Serialize 77 | unserialize?: Unserialize 78 | writeOperationOptions?: WriteConcernSettings 79 | transformId?: (sid: string) => string 80 | crypto?: CryptoOptions 81 | cryptoAdapter?: CryptoAdapter | null 82 | } 83 | 84 | type InternalSessionType = { 85 | _id: string 86 | session: StoredSessionValue | T 87 | expires?: Date 88 | lastModified?: Date 89 | createdAt?: Date 90 | updatedAt?: Date 91 | } 92 | 93 | // eslint-disable-next-line @typescript-eslint/no-empty-function 94 | const noop = () => {} 95 | const unit: (a: T) => T = (a) => a 96 | 97 | function defaultSerializeFunction( 98 | currentSession: T 99 | ): T { 100 | const result: session.SessionData = { 101 | cookie: currentSession.cookie, 102 | } as session.SessionData 103 | Object.entries(currentSession).forEach(([key, value]) => { 104 | if ( 105 | key === 'cookie' && 106 | value && 107 | typeof (value as { toJSON?: () => unknown }).toJSON === 'function' 108 | ) { 109 | result.cookie = ( 110 | value as { toJSON: () => unknown } 111 | ).toJSON() as session.Cookie 112 | } else { 113 | ;(result as Record)[key] = value as unknown 114 | } 115 | }) 116 | 117 | return result as T 118 | } 119 | 120 | function computeTransformFunctions( 121 | options: ConcretConnectMongoOptions 122 | ): TransformFunctions { 123 | if (options.serialize || options.unserialize) { 124 | return { 125 | serialize: options.serialize || defaultSerializeFunction, 126 | unserialize: (options.unserialize || unit) as Unserialize, 127 | } 128 | } 129 | 130 | if (options.stringify === false) { 131 | return { 132 | serialize: defaultSerializeFunction, 133 | unserialize: unit as Unserialize, 134 | } 135 | } 136 | // Default case 137 | return { 138 | serialize: (value) => JSON.stringify(value), 139 | unserialize: (payload) => JSON.parse(payload as string) as T, 140 | } 141 | } 142 | 143 | function computeExpires( 144 | session: session.SessionData | undefined, 145 | fallbackTtlSeconds: number 146 | ): Date { 147 | const cookie = session?.cookie as session.Cookie | undefined 148 | if (cookie?.expires) { 149 | return new Date(cookie.expires) 150 | } 151 | const now = Date.now() 152 | return new Date(now + fallbackTtlSeconds * 1000) 153 | } 154 | 155 | export default class MongoStore< 156 | T extends session.SessionData = session.SessionData, 157 | > 158 | extends session.Store 159 | { 160 | private clientP: Promise 161 | private readonly cryptoAdapter: CryptoAdapter | null = null 162 | private timer?: NodeJS.Timeout 163 | collectionP: Promise>> 164 | private options: ConcretConnectMongoOptions 165 | private transformFunctions: TransformFunctions 166 | 167 | constructor({ 168 | collectionName = 'sessions', 169 | ttl = 1209600, 170 | mongoOptions = {}, 171 | autoRemove = 'native', 172 | autoRemoveInterval = 10, 173 | touchAfter = 0, 174 | stringify = true, 175 | timestamps = false, 176 | crypto, 177 | cryptoAdapter, 178 | ...required 179 | }: ConnectMongoOptions) { 180 | super() 181 | debug('create MongoStore instance') 182 | const options: ConcretConnectMongoOptions = { 183 | collectionName, 184 | ttl, 185 | mongoOptions, 186 | autoRemove, 187 | autoRemoveInterval, 188 | touchAfter, 189 | stringify, 190 | timestamps, 191 | crypto, 192 | cryptoAdapter: cryptoAdapter ?? null, 193 | ...required, 194 | } 195 | // Check params 196 | assert( 197 | options.mongoUrl || options.clientPromise || options.client, 198 | 'You must provide either mongoUrl|clientPromise|client in options' 199 | ) 200 | assert( 201 | options.createAutoRemoveIdx === null || 202 | options.createAutoRemoveIdx === undefined, 203 | 'options.createAutoRemoveIdx has been reverted to autoRemove and autoRemoveInterval' 204 | ) 205 | assert( 206 | !options.autoRemoveInterval || options.autoRemoveInterval <= 71582, 207 | /* (Math.pow(2, 32) - 1) / (1000 * 60) */ 'autoRemoveInterval is too large. options.autoRemoveInterval is in minutes but not seconds nor mills' 208 | ) 209 | if (crypto !== undefined && cryptoAdapter !== undefined) { 210 | throw new Error( 211 | 'Provide either the legacy crypto option or cryptoAdapter, not both' 212 | ) 213 | } 214 | 215 | const legacyCryptoRequested = 216 | crypto !== undefined && crypto.secret !== false 217 | if (options.cryptoAdapter) { 218 | this.cryptoAdapter = options.cryptoAdapter 219 | } else if (legacyCryptoRequested) { 220 | this.cryptoAdapter = createKrupteinAdapter(options.crypto!) 221 | } 222 | options.cryptoAdapter = this.cryptoAdapter 223 | 224 | this.transformFunctions = computeTransformFunctions(options) 225 | let _clientP: Promise 226 | if (options.mongoUrl) { 227 | _clientP = MongoClient.connect(options.mongoUrl, options.mongoOptions) 228 | } else if (options.clientPromise) { 229 | _clientP = options.clientPromise 230 | } else if (options.client) { 231 | _clientP = Promise.resolve(options.client) 232 | } else { 233 | throw new Error('Cannot init client. Please provide correct options') 234 | } 235 | assert(!!_clientP, 'Client is null|undefined') 236 | this.clientP = _clientP 237 | this.options = options 238 | this.collectionP = _clientP.then(async (con) => { 239 | const collection = con 240 | .db(options.dbName) 241 | .collection>(options.collectionName) 242 | await this.setAutoRemove(collection) 243 | return collection 244 | }) 245 | } 246 | 247 | static create( 248 | options: ConnectMongoOptions 249 | ): MongoStore { 250 | return new MongoStore(options) 251 | } 252 | 253 | private setAutoRemove( 254 | collection: Collection> 255 | ): Promise { 256 | const removeQuery = () => ({ 257 | expires: { 258 | $lt: new Date(), 259 | }, 260 | }) 261 | switch (this.options.autoRemove) { 262 | case 'native': 263 | debug('Creating MongoDB TTL index') 264 | return collection.createIndex( 265 | { expires: 1 }, 266 | { 267 | background: true, 268 | expireAfterSeconds: 0, 269 | } 270 | ) 271 | case 'interval': { 272 | debug('create Timer to remove expired sessions') 273 | const runIntervalRemove = () => 274 | collection 275 | .deleteMany(removeQuery(), { 276 | writeConcern: { 277 | w: 0, 278 | }, 279 | }) 280 | .catch((err) => { 281 | debug( 282 | 'autoRemove interval cleanup failed: %s', 283 | (err as Error)?.message ?? err 284 | ) 285 | }) 286 | this.timer = setInterval( 287 | () => { 288 | void runIntervalRemove() 289 | }, 290 | this.options.autoRemoveInterval * 1000 * 60 291 | ) 292 | this.timer.unref() 293 | return Promise.resolve() 294 | } 295 | case 'disabled': 296 | default: 297 | return Promise.resolve() 298 | } 299 | } 300 | 301 | private computeStorageId(sessionId: string) { 302 | if ( 303 | this.options.transformId && 304 | typeof this.options.transformId === 'function' 305 | ) { 306 | return this.options.transformId(sessionId) 307 | } 308 | return sessionId 309 | } 310 | 311 | /** 312 | * Normalize payload before encryption so decrypt can restore the original 313 | * serialized session value. 314 | */ 315 | private serializeForCrypto(payload: StoredSessionValue): string { 316 | if (typeof payload === 'string') { 317 | return payload 318 | } 319 | try { 320 | return JSON.stringify(payload) 321 | } catch (error) { 322 | debug( 323 | 'Falling back to string serialization for crypto payload: %O', 324 | error 325 | ) 326 | return String(payload) 327 | } 328 | } 329 | 330 | private parseDecryptedPayload(plaintext: string): StoredSessionValue { 331 | let parsed: unknown 332 | try { 333 | parsed = JSON.parse(plaintext) 334 | } catch { 335 | parsed = plaintext 336 | } 337 | 338 | if (this.options.stringify === false) { 339 | if (typeof parsed === 'string') { 340 | try { 341 | return JSON.parse(parsed) as StoredSessionValue 342 | } catch { 343 | return parsed as StoredSessionValue 344 | } 345 | } 346 | return parsed as StoredSessionValue 347 | } 348 | 349 | // Default stringify path expects a string for unserialize(JSON.parse) 350 | if (!this.options.serialize && !this.options.unserialize) { 351 | return typeof parsed === 'string' 352 | ? (parsed as StoredSessionValue) 353 | : (JSON.stringify(parsed) as StoredSessionValue) 354 | } 355 | 356 | return parsed as StoredSessionValue 357 | } 358 | 359 | /** 360 | * Decrypt given session data 361 | * @param session session data to be decrypt. Mutate the input session. 362 | */ 363 | private async decryptSession( 364 | sessionDoc: InternalSessionType | undefined | null 365 | ) { 366 | if ( 367 | this.cryptoAdapter && 368 | sessionDoc && 369 | typeof sessionDoc.session === 'string' 370 | ) { 371 | const plaintext = await this.cryptoAdapter.decrypt(sessionDoc.session) 372 | sessionDoc.session = this.parseDecryptedPayload(plaintext) 373 | } 374 | } 375 | 376 | /** 377 | * Get a session from the store given a session ID (sid) 378 | * @param sid session ID 379 | */ 380 | get(sid: string, callback: (err: any, session?: T | null) => void): void { 381 | ;(async () => { 382 | try { 383 | debug(`MongoStore#get=${sid}`) 384 | const collection = await this.collectionP 385 | const sessionDoc = await collection.findOne({ 386 | _id: this.computeStorageId(sid), 387 | $or: [ 388 | { expires: { $exists: false } }, 389 | { expires: { $gt: new Date() } }, 390 | ], 391 | }) 392 | if (this.cryptoAdapter && sessionDoc) { 393 | try { 394 | await this.decryptSession(sessionDoc) 395 | } catch (error) { 396 | callback(error) 397 | return 398 | } 399 | } 400 | let result: T | undefined 401 | if (sessionDoc) { 402 | result = this.transformFunctions.unserialize(sessionDoc.session) 403 | if (this.options.touchAfter > 0 && sessionDoc.lastModified) { 404 | ;(result as T & { lastModified?: Date }).lastModified = 405 | sessionDoc.lastModified 406 | } 407 | } 408 | this.emit('get', sid) 409 | callback(null, result ?? null) 410 | } catch (error) { 411 | callback(error) 412 | } 413 | })() 414 | } 415 | 416 | /** 417 | * Upsert a session into the store given a session ID (sid) and session (session) object. 418 | * @param sid session ID 419 | * @param session session object 420 | */ 421 | set(sid: string, session: T, callback: (err: any) => void = noop): void { 422 | ;(async () => { 423 | try { 424 | debug(`MongoStore#set=${sid}`) 425 | // Removing the lastModified prop from the session object before update 426 | if (this.options.touchAfter > 0 && session?.lastModified) { 427 | delete (session as T & { lastModified?: Date }).lastModified 428 | } 429 | const s: InternalSessionType = { 430 | _id: this.computeStorageId(sid), 431 | session: this.transformFunctions.serialize(session), 432 | } 433 | // Expire handling 434 | s.expires = computeExpires(session, this.options.ttl) 435 | // Last modify handling 436 | if (this.options.touchAfter > 0) { 437 | s.lastModified = new Date() 438 | } 439 | if (this.cryptoAdapter) { 440 | const plaintext = this.serializeForCrypto(s.session) 441 | const encrypted = await this.cryptoAdapter.encrypt(plaintext) 442 | s.session = encrypted as StoredSessionValue 443 | } 444 | const collection = await this.collectionP 445 | const update: Record = { $set: s } 446 | if (this.options.timestamps) { 447 | update.$setOnInsert = { createdAt: new Date() } 448 | update.$currentDate = { updatedAt: true } 449 | } 450 | const rawResp = await collection.updateOne({ _id: s._id }, update, { 451 | upsert: true, 452 | writeConcern: this.options.writeOperationOptions, 453 | }) 454 | if (rawResp.upsertedCount > 0) { 455 | this.emit('create', sid) 456 | } else { 457 | this.emit('update', sid) 458 | } 459 | this.emit('set', sid) 460 | } catch (error) { 461 | return callback(error) 462 | } 463 | return callback(null) 464 | })() 465 | } 466 | 467 | touch( 468 | sid: string, 469 | session: T & { lastModified?: Date }, 470 | callback: (err: any) => void = noop 471 | ): void { 472 | ;(async () => { 473 | try { 474 | debug(`MongoStore#touch=${sid}`) 475 | const updateFields: { 476 | lastModified?: Date 477 | expires?: Date 478 | session?: T 479 | } = {} 480 | const touchAfter = this.options.touchAfter * 1000 481 | const lastModified = session.lastModified 482 | ? session.lastModified.getTime() 483 | : 0 484 | const currentDate = new Date() 485 | 486 | // If the given options has a touchAfter property, check if the 487 | // current timestamp - lastModified timestamp is bigger than 488 | // the specified, if it's not, don't touch the session 489 | if (touchAfter > 0 && lastModified > 0) { 490 | const timeElapsed = currentDate.getTime() - lastModified 491 | if (timeElapsed < touchAfter) { 492 | debug(`Skip touching session=${sid}`) 493 | return callback(null) 494 | } 495 | updateFields.lastModified = currentDate 496 | } 497 | 498 | updateFields.expires = computeExpires(session, this.options.ttl) 499 | const collection = await this.collectionP 500 | const updateQuery: Record = { $set: updateFields } 501 | if (this.options.timestamps) { 502 | updateQuery.$currentDate = { updatedAt: true } 503 | } 504 | const rawResp = await collection.updateOne( 505 | { _id: this.computeStorageId(sid) }, 506 | updateQuery, 507 | { writeConcern: this.options.writeOperationOptions } 508 | ) 509 | if (rawResp.matchedCount === 0) { 510 | return callback(new Error('Unable to find the session to touch')) 511 | } else { 512 | this.emit('touch', sid, session) 513 | return callback(null) 514 | } 515 | } catch (error) { 516 | return callback(error) 517 | } 518 | })() 519 | } 520 | 521 | /** 522 | * Get all sessions in the store as an array 523 | */ 524 | all( 525 | callback: (err: any, obj?: T[] | { [sid: string]: T } | null) => void 526 | ): void { 527 | ;(async () => { 528 | try { 529 | debug('MongoStore#all()') 530 | const collection = await this.collectionP 531 | const sessions = collection.find({ 532 | $or: [ 533 | { expires: { $exists: false } }, 534 | { expires: { $gt: new Date() } }, 535 | ], 536 | }) 537 | const results: T[] = [] 538 | for await (const sessionDoc of sessions) { 539 | if (this.cryptoAdapter && sessionDoc) { 540 | await this.decryptSession(sessionDoc) 541 | } 542 | results.push(this.transformFunctions.unserialize(sessionDoc.session)) 543 | } 544 | this.emit('all', results) 545 | callback(null, results) 546 | } catch (error) { 547 | callback(error) 548 | } 549 | })() 550 | } 551 | 552 | /** 553 | * Destroy/delete a session from the store given a session ID (sid) 554 | * @param sid session ID 555 | */ 556 | destroy(sid: string, callback: (err: any) => void = noop): void { 557 | debug(`MongoStore#destroy=${sid}`) 558 | this.collectionP 559 | .then((colleciton) => 560 | colleciton.deleteOne( 561 | { _id: this.computeStorageId(sid) }, 562 | { writeConcern: this.options.writeOperationOptions } 563 | ) 564 | ) 565 | .then(() => { 566 | this.emit('destroy', sid) 567 | callback(null) 568 | }) 569 | .catch((err) => callback(err)) 570 | } 571 | 572 | /** 573 | * Get the count of all sessions in the store 574 | */ 575 | length(callback: (err: any, length: number) => void): void { 576 | debug('MongoStore#length()') 577 | this.collectionP 578 | .then((collection) => collection.countDocuments()) 579 | .then((c) => callback(null, c)) 580 | .catch((err: unknown) => callback(err, 0)) 581 | } 582 | 583 | /** 584 | * Delete all sessions from the store. 585 | */ 586 | clear(callback: (err: any) => void = noop): void { 587 | debug('MongoStore#clear()') 588 | this.collectionP 589 | .then((collection) => 590 | collection.deleteMany( 591 | {}, 592 | { writeConcern: this.options.writeOperationOptions } 593 | ) 594 | ) 595 | .then(() => callback(null)) 596 | .catch((err: unknown) => { 597 | const message = (err as Error | undefined)?.message ?? '' 598 | // NamespaceNotFound (code 26) occurs if the collection was dropped earlier; treat as success to keep clear() idempotent. 599 | if ( 600 | (err as { code?: number })?.code === 26 || 601 | /ns not found/i.test(message) 602 | ) { 603 | callback(null) 604 | return 605 | } 606 | callback(err) 607 | }) 608 | } 609 | 610 | /** 611 | * Close database connection 612 | */ 613 | close(): Promise { 614 | debug('MongoStore#close()') 615 | if (this.timer) { 616 | clearInterval(this.timer) 617 | this.timer = undefined 618 | } 619 | return this.clientP.then((c) => c.close()) 620 | } 621 | } 622 | -------------------------------------------------------------------------------- /src/lib/MongoStore.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { SessionData } from 'express-session' 3 | import { MongoClient } from 'mongodb' 4 | 5 | import MongoStore, { 6 | createWebCryptoAdapter, 7 | createKrupteinAdapter, 8 | type CryptoAdapter, 9 | } from './MongoStore.js' 10 | import { 11 | createStoreHelper, 12 | makeData, 13 | makeDataNoCookie, 14 | makeCookie, 15 | } from '../test/testHelper.js' 16 | 17 | let { store, storePromise } = createStoreHelper() 18 | 19 | test.before(async () => { 20 | await storePromise.clear().catch((err: unknown) => { 21 | if (err instanceof Error && err.message.match(/ns not found/)) { 22 | return null 23 | } else { 24 | throw err 25 | } 26 | }) 27 | }) 28 | 29 | test.afterEach.always(async () => { 30 | await storePromise.close() 31 | }) 32 | 33 | test.serial('create store w/o provide required options', (t) => { 34 | t.throws(() => MongoStore.create({}), { 35 | message: /You must provide either mongoUrl\|clientPromise\|client/, 36 | }) 37 | }) 38 | 39 | test.serial( 40 | 'create store with explicit undefined clientPromise still errors', 41 | (t) => { 42 | t.throws( 43 | () => 44 | MongoStore.create({ 45 | clientPromise: undefined as unknown as Promise, 46 | }), 47 | { message: /You must provide either mongoUrl\|clientPromise\|client/ } 48 | ) 49 | } 50 | ) 51 | 52 | test.serial('create store with explicit undefined client still errors', (t) => { 53 | t.throws( 54 | () => 55 | MongoStore.create({ 56 | client: undefined as unknown as MongoClient, 57 | }), 58 | { message: /You must provide either mongoUrl\|clientPromise\|client/ } 59 | ) 60 | }) 61 | 62 | test.serial('create store with clientPromise', async (t) => { 63 | const clientP = MongoClient.connect('mongodb://root:example@127.0.0.1:27017') 64 | const store = MongoStore.create({ clientPromise: clientP }) 65 | t.not(store, null) 66 | t.not(store, undefined) 67 | await store.collectionP 68 | store.close() 69 | }) 70 | 71 | test.serial('create store with client', async (t) => { 72 | const client = await MongoClient.connect( 73 | 'mongodb://root:example@127.0.0.1:27017' 74 | ) 75 | const store = MongoStore.create({ client: client }) 76 | t.not(store, null) 77 | t.not(store, undefined) 78 | await store.collectionP 79 | store.close() 80 | }) 81 | 82 | test.serial('length should be 0', async (t) => { 83 | ;({ store, storePromise } = createStoreHelper()) 84 | const length = await storePromise.length() 85 | t.is(length, 0) 86 | }) 87 | 88 | test.serial('get non-exist session should throw error', async (t) => { 89 | ;({ store, storePromise } = createStoreHelper()) 90 | const res = await storePromise.get('fake-sid') 91 | t.is(res, null) 92 | }) 93 | 94 | test.serial('get all session should work for no session', async (t) => { 95 | ;({ store, storePromise } = createStoreHelper()) 96 | const allSessions = await storePromise.all() 97 | t.deepEqual(allSessions, []) 98 | }) 99 | 100 | test.serial('basic operation flow', async (t) => { 101 | ;({ store, storePromise } = createStoreHelper()) 102 | let orgSession = makeData() 103 | const sid = 'test-basic-flow' 104 | const res = await storePromise.set(sid, orgSession) 105 | t.is(res, undefined) 106 | const session = await storePromise.get(sid) 107 | t.is(typeof session, 'object') 108 | orgSession = JSON.parse(JSON.stringify(orgSession)) 109 | t.deepEqual(session, orgSession) 110 | const allSessions = await storePromise.all() 111 | t.deepEqual(allSessions, [orgSession]) 112 | t.is(await storePromise.length(), 1) 113 | const err = await storePromise.destroy(sid) 114 | t.is(err, undefined) 115 | t.is(await storePromise.length(), 0) 116 | }) 117 | 118 | test.serial('set and listen to event', async (t) => { 119 | ;({ store, storePromise } = createStoreHelper()) 120 | const sid = 'test-set-event' 121 | const orgSession = makeData() 122 | const expectedSession = JSON.parse(JSON.stringify(orgSession)) 123 | 124 | const waitForSet = new Promise((resolve, reject) => { 125 | store.once('set', async (sessionId: string) => { 126 | try { 127 | t.is(sessionId, sid) 128 | const session = await storePromise.get(sid) 129 | t.truthy(session) 130 | t.is(typeof session, 'object') 131 | t.deepEqual(session, expectedSession) 132 | resolve() 133 | } catch (err) { 134 | reject(err) 135 | } 136 | }) 137 | }) 138 | 139 | await storePromise.set(sid, orgSession) 140 | await waitForSet 141 | }) 142 | 143 | test.serial('timestamps are disabled by default', async (t) => { 144 | ;({ store, storePromise } = createStoreHelper()) 145 | const sid = 'timestamps-disabled' 146 | await storePromise.set(sid, makeData()) 147 | const collection = await store.collectionP 148 | const sessionDoc = await collection.findOne({ _id: sid }) 149 | 150 | t.truthy(sessionDoc) 151 | t.is(sessionDoc?.createdAt, undefined) 152 | t.is(sessionDoc?.updatedAt, undefined) 153 | }) 154 | 155 | test.serial( 156 | 'timestamps opt-in sets createdAt once and updates updatedAt', 157 | async (t) => { 158 | ;({ store, storePromise } = createStoreHelper({ timestamps: true })) 159 | const sid = 'timestamps-enabled' 160 | await storePromise.set(sid, makeData()) 161 | const collection = await store.collectionP 162 | const first = await collection.findOne({ _id: sid }) 163 | 164 | t.truthy(first?.createdAt) 165 | t.truthy(first?.updatedAt) 166 | const createdAtMs = first?.createdAt?.getTime() 167 | const updatedAtMs = first?.updatedAt?.getTime() 168 | t.truthy(createdAtMs) 169 | t.truthy(updatedAtMs) 170 | 171 | await new Promise((resolve) => setTimeout(resolve, 20)) 172 | await storePromise.set(sid, { ...makeData(), fizz: 'buzz' } as SessionData) 173 | const second = await collection.findOne({ _id: sid }) 174 | 175 | t.is(second?.createdAt?.getTime(), createdAtMs) 176 | t.truthy((second?.updatedAt?.getTime() ?? 0) > (updatedAtMs ?? 0)) 177 | } 178 | ) 179 | 180 | test.serial('set and listen to create event', async (t) => { 181 | ;({ store, storePromise } = createStoreHelper()) 182 | const sid = 'test-create-event' 183 | const orgSession = makeData() 184 | 185 | const waitForCreate = new Promise((resolve, reject) => { 186 | store.once('create', (sessionId: string) => { 187 | try { 188 | t.is(sessionId, sid) 189 | resolve() 190 | } catch (err) { 191 | reject(err) 192 | } 193 | }) 194 | }) 195 | 196 | await storePromise.set(sid, orgSession) 197 | await waitForCreate 198 | }) 199 | 200 | test.serial('set and listen to update event', async (t) => { 201 | ;({ store, storePromise } = createStoreHelper()) 202 | const sid = 'test-update-event' 203 | const orgSession = makeData() 204 | 205 | await storePromise.set(sid, orgSession) 206 | 207 | const waitForUpdate = new Promise((resolve, reject) => { 208 | store.once('update', (sessionId: string) => { 209 | try { 210 | t.is(sessionId, sid) 211 | resolve() 212 | } catch (err) { 213 | reject(err) 214 | } 215 | }) 216 | }) 217 | 218 | await storePromise.set(sid, { ...orgSession, foo: 'new-bar' } as SessionData) 219 | await waitForUpdate 220 | }) 221 | 222 | test.serial('set with no stringify', async (t) => { 223 | ;({ store, storePromise } = createStoreHelper({ stringify: false })) 224 | const orgSession = makeData() 225 | const cookie = orgSession.cookie 226 | const sid = 'test-no-stringify' 227 | const res = await storePromise.set(sid, orgSession) 228 | t.is(res, undefined) 229 | const session = await storePromise.get(sid) 230 | t.is(typeof session, 'object') 231 | t.deepEqual(orgSession.cookie, cookie) 232 | // @ts-ignore 233 | t.deepEqual(cookie.expires.toJSON(), session.cookie.expires.toJSON()) 234 | // @ts-ignore 235 | t.deepEqual(cookie.secure, session.cookie.secure) 236 | const err = await storePromise.clear() 237 | t.is(err, undefined) 238 | t.is(await storePromise.length(), 0) 239 | }) 240 | 241 | test.serial( 242 | 'ttl uses cookie.maxAge before cookie.expires and ttl fallback', 243 | async (t) => { 244 | // Choose distinct magnitudes so ordering is unambiguous: 2s < 30s < 90s 245 | const defaultTtl = 30_000 246 | ;({ store, storePromise } = createStoreHelper({ ttl: defaultTtl / 1000 })) 247 | const cookieMaxAge = makeCookie() 248 | const sid = 'ttl-precedence' 249 | cookieMaxAge.maxAge = 2_000 250 | const sessionData = { foo: 'ttl', cookie: cookieMaxAge } 251 | 252 | // @ts-ignore 253 | await storePromise.set(sid, sessionData) 254 | const collection = await store.collectionP 255 | const doc = await collection.findOne({ _id: sid }) 256 | 257 | // separate cookie with only expires set to test precedence 258 | const cookieExpires = makeCookie() 259 | cookieExpires.maxAge = undefined 260 | cookieExpires.expires = new Date(Date.now() + 90_000) 261 | const sid2 = 'ttl-precedence-expires' 262 | // @ts-ignore 263 | await storePromise.set(sid2, { foo: 'ttl2', cookie: cookieExpires }) 264 | const doc2 = await collection.findOne({ _id: sid2 }) 265 | 266 | // remove both to test ttl fallback 267 | const sid3 = 'ttl-precedence-ttl' 268 | // @ts-ignore 269 | await storePromise.set(sid3, { foo: 'ttl3' }) 270 | const doc3 = await collection.findOne({ _id: sid3 }) 271 | 272 | const expMs = doc?.expires?.getTime() ?? 0 273 | const expMs2 = doc2?.expires?.getTime() ?? 0 274 | const expMs3 = doc3?.expires?.getTime() ?? 0 275 | 276 | t.true(expMs > 0 && expMs2 > 0 && expMs3 > 0) 277 | // ordering: maxAge (2s) < ttl fallback (30s) < cookie.expires (90s) 278 | t.true(expMs < expMs3) 279 | t.true(expMs3 < expMs2) 280 | } 281 | ) 282 | 283 | test.serial('clear preserves TTL index and is idempotent', async (t) => { 284 | ;({ store, storePromise } = createStoreHelper({ autoRemove: 'native' })) 285 | const collection = await store.collectionP 286 | await collection.insertOne({ 287 | _id: 'clear-ttl-index', 288 | session: makeData(), 289 | expires: new Date(Date.now() + 1000), 290 | }) 291 | const indexesBefore = await collection.listIndexes().toArray() 292 | t.true(indexesBefore.some((idx) => idx.name === 'expires_1')) 293 | 294 | await t.notThrowsAsync(() => storePromise.clear()) 295 | 296 | const indexesAfter = await collection.listIndexes().toArray() 297 | t.true(indexesAfter.some((idx) => idx.name === 'expires_1')) 298 | 299 | await t.notThrowsAsync(() => storePromise.clear()) 300 | }) 301 | 302 | test.serial('decrypt failure only calls callback once', async (t) => { 303 | let secret = 'right-secret' 304 | const adapter: CryptoAdapter = { 305 | async encrypt(plaintext) { 306 | return `${secret}:${plaintext}` 307 | }, 308 | async decrypt(ciphertext) { 309 | const prefix = `${secret}:` 310 | if (!ciphertext.startsWith(prefix)) { 311 | throw new Error('bad secret') 312 | } 313 | return ciphertext.slice(prefix.length) 314 | }, 315 | } 316 | 317 | ;({ store, storePromise } = createStoreHelper({ cryptoAdapter: adapter })) 318 | const sid = 'decrypt-failure' 319 | await storePromise.set(sid, makeData()) 320 | 321 | // Tamper with the secret so decryption fails 322 | secret = 'wrong-secret' 323 | 324 | await new Promise((resolve) => { 325 | let calls = 0 326 | store.get(sid, (err, session) => { 327 | calls += 1 328 | t.truthy(err) 329 | t.is(session, undefined) 330 | t.is(calls, 1) 331 | resolve() 332 | }) 333 | }) 334 | }) 335 | 336 | test.serial( 337 | 'interval autoRemove suppresses rejections and clears timer on close', 338 | async (t) => { 339 | const originalSetInterval = global.setInterval 340 | const originalClearInterval = global.clearInterval 341 | const callbacks: (() => void)[] = [] 342 | const fakeTimer = { 343 | ref() { 344 | return this 345 | }, 346 | unref() { 347 | return this 348 | }, 349 | } as unknown as NodeJS.Timeout 350 | let cleared = false 351 | ;(global as typeof globalThis).setInterval = ((fn: () => void) => { 352 | callbacks.push(fn) 353 | return fakeTimer 354 | }) as typeof setInterval 355 | ;(global as typeof globalThis).clearInterval = (( 356 | handle: NodeJS.Timeout 357 | ) => { 358 | if (handle === fakeTimer) { 359 | cleared = true 360 | } 361 | }) as typeof clearInterval 362 | 363 | const fakeCollection = { 364 | deleteMany: () => Promise.reject(new Error('interval failure')), 365 | } 366 | const fakeClient = { 367 | db: () => ({ 368 | collection: () => fakeCollection, 369 | }), 370 | close: () => Promise.resolve(), 371 | } 372 | const unhandled: unknown[] = [] 373 | const onUnhandled = (reason: unknown) => { 374 | unhandled.push(reason) 375 | } 376 | process.on('unhandledRejection', onUnhandled) 377 | 378 | let intervalStore: MongoStore | undefined 379 | try { 380 | intervalStore = MongoStore.create({ 381 | clientPromise: Promise.resolve(fakeClient as unknown as MongoClient), 382 | autoRemove: 'interval', 383 | autoRemoveInterval: 1, 384 | collectionName: 'interval-test', 385 | dbName: 'interval-db', 386 | }) 387 | await intervalStore.collectionP 388 | t.is(callbacks.length, 1) 389 | callbacks[0]?.() 390 | await new Promise((resolve) => setImmediate(resolve)) 391 | t.is(unhandled.length, 0) 392 | await intervalStore.close() 393 | t.true(cleared) 394 | t.is( 395 | (intervalStore as unknown as { timer?: NodeJS.Timeout }).timer, 396 | undefined 397 | ) 398 | } finally { 399 | process.off('unhandledRejection', onUnhandled) 400 | global.setInterval = originalSetInterval 401 | global.clearInterval = originalClearInterval 402 | } 403 | } 404 | ) 405 | 406 | test.serial('test destroy event', async (t) => { 407 | ;({ store, storePromise } = createStoreHelper()) 408 | const orgSession = makeData() 409 | const sid = 'test-destroy-event' 410 | 411 | const waitForDestroy = new Promise((resolve, reject) => { 412 | store.once('destroy', (sessionId: string) => { 413 | try { 414 | t.is(sessionId, sid) 415 | resolve() 416 | } catch (err) { 417 | reject(err) 418 | } 419 | }) 420 | }) 421 | 422 | await storePromise.set(sid, orgSession) 423 | await storePromise.destroy(sid) 424 | await waitForDestroy 425 | }) 426 | 427 | test.serial('test set default TTL', async (t) => { 428 | const defaultTTL = 10 429 | ;({ store, storePromise } = createStoreHelper({ 430 | ttl: defaultTTL, 431 | })) 432 | const orgSession = makeDataNoCookie() 433 | const sid = 'test-set-default-ttl' 434 | const timeBeforeSet = new Date().valueOf() 435 | // @ts-ignore 436 | await storePromise.set(sid, orgSession) 437 | const collection = await store.collectionP 438 | const session = await collection.findOne({ _id: sid }) 439 | const timeAfterSet = new Date().valueOf() 440 | const expires = session?.expires?.valueOf() 441 | t.truthy(expires) 442 | if (expires) { 443 | t.truthy(timeBeforeSet + defaultTTL * 1000 <= expires) 444 | t.truthy(expires <= timeAfterSet + defaultTTL * 1000) 445 | } 446 | }) 447 | 448 | test.serial('test default TTL', async (t) => { 449 | const defaultExpirationTime = 1000 * 60 * 60 * 24 * 14 450 | ;({ store, storePromise } = createStoreHelper()) 451 | const orgSession = makeDataNoCookie() 452 | const sid = 'test-no-set-default-ttl' 453 | const timeBeforeSet = new Date().valueOf() 454 | // @ts-ignore 455 | await storePromise.set(sid, orgSession) 456 | const collection = await store.collectionP 457 | const session = await collection.findOne({ _id: sid }) 458 | const timeAfterSet = new Date().valueOf() 459 | const expires = session?.expires?.valueOf() 460 | t.truthy(expires) 461 | if (expires) { 462 | t.truthy(timeBeforeSet + defaultExpirationTime <= expires) 463 | t.truthy(expires <= timeAfterSet + defaultExpirationTime) 464 | } 465 | }) 466 | 467 | test.serial('test custom serializer', async (t) => { 468 | ;({ store, storePromise } = createStoreHelper({ 469 | serialize: (obj: any) => { 470 | obj.ice = 'test-ice-serializer' 471 | return JSON.stringify(obj) 472 | }, 473 | })) 474 | const orgSession = makeData() 475 | const sid = 'test-custom-serializer' 476 | await storePromise.set(sid, orgSession) 477 | const session = await storePromise.get(sid) 478 | t.is(typeof session, 'string') 479 | t.not(session, undefined) 480 | // @ts-ignore 481 | orgSession.ice = 'test-ice-serializer' 482 | // @ts-ignore 483 | t.is(session, JSON.stringify(orgSession)) 484 | }) 485 | 486 | test.serial('test custom deserializer', async (t) => { 487 | ;({ store, storePromise } = createStoreHelper({ 488 | unserialize: (obj: any) => { 489 | const materialized = 490 | typeof obj === 'string' 491 | ? (JSON.parse(obj) as unknown as SessionData) 492 | : (obj as SessionData) 493 | ;(materialized as Record).ice = 'test-ice-deserializer' 494 | return materialized 495 | }, 496 | })) 497 | const orgSession = makeData() 498 | const sid = 'test-custom-deserializer' 499 | await storePromise.set(sid, orgSession) 500 | const session = await storePromise.get(sid) 501 | t.is(typeof session, 'object') 502 | // @ts-ignore 503 | orgSession.cookie = orgSession.cookie.toJSON() 504 | // @ts-ignore 505 | orgSession.ice = 'test-ice-deserializer' 506 | if (session && typeof session === 'object' && 'cookie' in session) { 507 | const cookie = (session as Record).cookie 508 | if (cookie && typeof cookie === 'object') { 509 | // express-session 1.18 normalizes optional cookie props to null instead of leaving them undefined. 510 | // Mirror whatever shape we read back so the equality check stays resilient. 511 | if ('partitioned' in cookie) { 512 | // @ts-ignore Cookie typings don't expose partitioned yet. 513 | orgSession.cookie.partitioned = cookie.partitioned 514 | } 515 | if ('priority' in cookie) { 516 | // @ts-ignore Cookie typings don't expose priority yet. 517 | orgSession.cookie.priority = cookie.priority 518 | } 519 | } 520 | } 521 | t.not(session, undefined) 522 | t.deepEqual(session, orgSession) 523 | }) 524 | 525 | test.serial('touch ops', async (t) => { 526 | ;({ store, storePromise } = createStoreHelper()) 527 | const orgSession = makeDataNoCookie() 528 | const sid = 'test-touch' 529 | // @ts-ignore 530 | await storePromise.set(sid, orgSession) 531 | const collection = await store.collectionP 532 | const session = await collection.findOne({ _id: sid }) 533 | await new Promise((resolve) => setTimeout(resolve, 500)) 534 | t.not(session, undefined) 535 | await storePromise.touch(sid, session?.session as SessionData) 536 | const session2 = await collection.findOne({ _id: sid }) 537 | t.not(session2, undefined) 538 | // Check if both expiry date are different 539 | t.truthy(session2?.expires?.getTime()) 540 | t.truthy(session?.expires?.getTime()) 541 | if (session?.expires?.getTime() && session2?.expires?.getTime()) { 542 | t.truthy(session2?.expires.getTime() > session?.expires.getTime()) 543 | } 544 | }) 545 | 546 | test.serial('touch updates updatedAt when timestamps enabled', async (t) => { 547 | ;({ store, storePromise } = createStoreHelper({ timestamps: true })) 548 | const orgSession = makeDataNoCookie() 549 | const sid = 'test-touch-timestamps' 550 | // @ts-ignore 551 | await storePromise.set(sid, orgSession) 552 | const collection = await store.collectionP 553 | const session = await collection.findOne({ _id: sid }) 554 | const initialUpdatedAt = session?.updatedAt?.getTime() 555 | 556 | await new Promise((resolve) => setTimeout(resolve, 20)) 557 | await storePromise.touch(sid, session?.session as SessionData) 558 | const touched = await collection.findOne({ _id: sid }) 559 | const touchedUpdatedAt = touched?.updatedAt?.getTime() 560 | 561 | t.truthy(initialUpdatedAt) 562 | t.truthy(touchedUpdatedAt) 563 | if (initialUpdatedAt && touchedUpdatedAt) { 564 | t.true(touchedUpdatedAt > initialUpdatedAt) 565 | } 566 | }) 567 | 568 | test.serial('touch ops with touchAfter', async (t) => { 569 | ;({ store, storePromise } = createStoreHelper({ touchAfter: 1 })) 570 | const orgSession = makeDataNoCookie() 571 | const sid = 'test-touch-with-touchAfter' 572 | // @ts-ignore 573 | await storePromise.set(sid, orgSession) 574 | const collection = await store.collectionP 575 | const session = await collection.findOne({ _id: sid }) 576 | const lastModifiedBeforeTouch = session?.lastModified?.getTime() 577 | t.not(session, undefined) 578 | await storePromise.touch(sid, session as unknown as SessionData) 579 | const session2 = await collection.findOne({ _id: sid }) 580 | t.not(session2, undefined) 581 | const lastModifiedAfterTouch = session2?.lastModified?.getTime() 582 | // Check if both expiry date are different 583 | t.is(lastModifiedBeforeTouch, lastModifiedAfterTouch) 584 | }) 585 | 586 | test.serial('touch ops with touchAfter with touch', async (t) => { 587 | ;({ store, storePromise } = createStoreHelper({ touchAfter: 1 })) 588 | const orgSession = makeDataNoCookie() 589 | const sid = 'test-touch-with-touchAfter-should-touch' 590 | // @ts-ignore 591 | await storePromise.set(sid, orgSession) 592 | const collection = await store.collectionP 593 | const session = await collection.findOne({ _id: sid }) 594 | const lastModifiedBeforeTouch = session?.lastModified?.getTime() 595 | await new Promise((resolve) => setTimeout(resolve, 1200)) 596 | t.not(session, undefined) 597 | await storePromise.touch(sid, session as unknown as SessionData) 598 | const session2 = await collection.findOne({ _id: sid }) 599 | t.not(session2, undefined) 600 | const lastModifiedAfterTouch = session2?.lastModified?.getTime() 601 | // Check if both expiry date are different 602 | t.truthy(lastModifiedAfterTouch) 603 | t.truthy(lastModifiedBeforeTouch) 604 | if (lastModifiedAfterTouch && lastModifiedBeforeTouch) { 605 | t.truthy(lastModifiedAfterTouch > lastModifiedBeforeTouch) 606 | } 607 | }) 608 | 609 | test.serial( 610 | 'touchAfter throttle keeps updatedAt unchanged until threshold when timestamps on', 611 | async (t) => { 612 | ;({ store, storePromise } = createStoreHelper({ 613 | touchAfter: 1, 614 | timestamps: true, 615 | })) 616 | const sid = 'touchAfter-timestamps' 617 | // @ts-ignore 618 | await storePromise.set(sid, makeDataNoCookie()) 619 | const collection = await store.collectionP 620 | const doc = await collection.findOne({ _id: sid }) 621 | const initialUpdated = doc?.updatedAt?.getTime() 622 | 623 | const sessionWithMeta = await storePromise.get(sid) 624 | await storePromise.touch(sid, sessionWithMeta as SessionData) 625 | const docNoUpdate = await collection.findOne({ _id: sid }) 626 | t.is(docNoUpdate?.updatedAt?.getTime(), initialUpdated) 627 | 628 | await new Promise((resolve) => setTimeout(resolve, 1100)) 629 | const sessionWithMetaAfterWait = await storePromise.get(sid) 630 | await storePromise.touch(sid, sessionWithMetaAfterWait as SessionData) 631 | const docUpdated = await collection.findOne({ _id: sid }) 632 | t.truthy((docUpdated?.updatedAt?.getTime() ?? 0) > (initialUpdated ?? 0)) 633 | } 634 | ) 635 | 636 | test.serial('cryptoAdapter conflicts with legacy crypto option', (t) => { 637 | const adapter: CryptoAdapter = { 638 | encrypt: async (payload) => payload, 639 | decrypt: async (payload) => payload, 640 | } 641 | t.throws( 642 | () => 643 | MongoStore.create({ 644 | mongoUrl: 'mongodb://root:example@127.0.0.1:27017', 645 | crypto: { secret: 'secret' }, 646 | cryptoAdapter: adapter, 647 | }), 648 | { message: /legacy crypto option or cryptoAdapter/ } 649 | ) 650 | }) 651 | 652 | test.serial('custom cryptoAdapter roundtrips session data', async (t) => { 653 | const adapter: CryptoAdapter = { 654 | encrypt: async (payload) => `enc:${payload}`, 655 | decrypt: async (payload) => payload.replace(/^enc:/, ''), 656 | } 657 | ;({ store, storePromise } = createStoreHelper({ 658 | cryptoAdapter: adapter, 659 | collectionName: 'custom-adapter', 660 | })) 661 | const sid = 'adapter-roundtrip' 662 | const original = makeData() 663 | await storePromise.set(sid, original) 664 | const session = await storePromise.get(sid) 665 | t.deepEqual(session, JSON.parse(JSON.stringify(original))) 666 | }) 667 | 668 | test.serial( 669 | 'kruptein adapter helper merges defaults and works with only secret', 670 | async (t) => { 671 | ;({ store, storePromise } = createStoreHelper({ 672 | cryptoAdapter: createKrupteinAdapter({ secret: 'secret' }), 673 | collectionName: 'kruptein-adapter', 674 | })) 675 | const sid = 'kruptein-adapter' 676 | const original = makeData() 677 | await storePromise.set(sid, original) 678 | const session = await storePromise.get(sid) 679 | t.deepEqual(session, JSON.parse(JSON.stringify(original))) 680 | } 681 | ) 682 | 683 | test.serial('web crypto adapter encrypts and decrypts sessions', async (t) => { 684 | const adapter = createWebCryptoAdapter({ secret: 'sup3r-secr3t' }) 685 | ;({ store, storePromise } = createStoreHelper({ 686 | cryptoAdapter: adapter, 687 | collectionName: 'webcrypto-adapter', 688 | })) 689 | const sid = 'webcrypto-session' 690 | const original = makeData() 691 | await storePromise.set(sid, original) 692 | const session = await storePromise.get(sid) 693 | t.deepEqual(session, JSON.parse(JSON.stringify(original))) 694 | }) 695 | 696 | test.serial('web crypto adapter supports base64url encoding', async (t) => { 697 | const adapter = createWebCryptoAdapter({ 698 | secret: 'sup3r-secr3t', 699 | encoding: 'base64url', 700 | }) 701 | ;({ store, storePromise } = createStoreHelper({ 702 | cryptoAdapter: adapter, 703 | collectionName: 'webcrypto-base64url', 704 | })) 705 | const sid = 'webcrypto-base64url' 706 | const original = makeData() 707 | await storePromise.set(sid, original) 708 | const session = await storePromise.get(sid) 709 | t.deepEqual(session, JSON.parse(JSON.stringify(original))) 710 | }) 711 | 712 | test.serial('web crypto adapter supports hex encoding', async (t) => { 713 | const adapter = createWebCryptoAdapter({ 714 | secret: 'sup3r-secr3t', 715 | encoding: 'hex', 716 | }) 717 | ;({ store, storePromise } = createStoreHelper({ 718 | cryptoAdapter: adapter, 719 | collectionName: 'webcrypto-hex', 720 | })) 721 | const sid = 'webcrypto-hex' 722 | const original = makeData() 723 | await storePromise.set(sid, original) 724 | const session = await storePromise.get(sid) 725 | t.deepEqual(session, JSON.parse(JSON.stringify(original))) 726 | }) 727 | 728 | test.serial( 729 | 'web crypto adapter derives key with PBKDF2 salt/iterations overrides', 730 | async (t) => { 731 | const adapter = createWebCryptoAdapter({ 732 | secret: 'sup3r-secr3t', 733 | encoding: 'base64url', 734 | salt: 'custom-salt', 735 | iterations: 100_000, 736 | }) 737 | ;({ store, storePromise } = createStoreHelper({ 738 | cryptoAdapter: adapter, 739 | collectionName: 'webcrypto-pbkdf2', 740 | })) 741 | const sid = 'webcrypto-pbkdf2' 742 | const original = makeData() 743 | await storePromise.set(sid, original) 744 | const session = await storePromise.get(sid) 745 | t.deepEqual(session, JSON.parse(JSON.stringify(original))) 746 | } 747 | ) 748 | 749 | test.serial('web crypto adapter supports AES-CBC algorithm', async (t) => { 750 | const adapter = createWebCryptoAdapter({ 751 | secret: 'sup3r-secr3t', 752 | algorithm: 'AES-CBC', 753 | ivLength: 16, 754 | }) 755 | ;({ store, storePromise } = createStoreHelper({ 756 | cryptoAdapter: adapter, 757 | collectionName: 'webcrypto-aes-cbc', 758 | })) 759 | const sid = 'webcrypto-aes-cbc' 760 | const original = makeData() 761 | await storePromise.set(sid, original) 762 | const session = await storePromise.get(sid) 763 | t.deepEqual(session, JSON.parse(JSON.stringify(original))) 764 | }) 765 | 766 | test.serial( 767 | 'cryptoAdapter works with default stringify (string payload)', 768 | async (t) => { 769 | const adapter: CryptoAdapter = { 770 | encrypt: async (payload) => `enc:${payload}`, 771 | decrypt: async (payload) => payload.replace(/^enc:/, ''), 772 | } 773 | ;({ store, storePromise } = createStoreHelper({ 774 | cryptoAdapter: adapter, 775 | collectionName: 'crypto-default-stringify', 776 | })) 777 | const sid = 'crypto-default-stringify' 778 | const original = makeData() 779 | await storePromise.set(sid, original) 780 | const session = await storePromise.get(sid) 781 | t.deepEqual(session, JSON.parse(JSON.stringify(original))) 782 | } 783 | ) 784 | 785 | test.serial( 786 | 'cryptoAdapter works with stringify=false (raw object path)', 787 | async (t) => { 788 | const adapter: CryptoAdapter = { 789 | encrypt: async (payload) => `enc:${payload}`, 790 | decrypt: async (payload) => payload.replace(/^enc:/, ''), 791 | } 792 | ;({ store, storePromise } = createStoreHelper({ 793 | cryptoAdapter: adapter, 794 | stringify: false, 795 | collectionName: 'crypto-stringify-false', 796 | })) 797 | const sid = 'crypto-stringify-false' 798 | const original = makeDataNoCookie() 799 | // @ts-ignore 800 | await storePromise.set(sid, original) 801 | const session = await storePromise.get(sid) 802 | t.deepEqual(session, original) 803 | } 804 | ) 805 | 806 | test.serial( 807 | 'cryptoAdapter works with custom serialize/unserialize functions', 808 | async (t) => { 809 | const adapter: CryptoAdapter = { 810 | encrypt: async (payload) => `enc:${payload}`, 811 | decrypt: async (payload) => payload.replace(/^enc:/, ''), 812 | } 813 | const serialize = (session: SessionData) => ({ 814 | ...session, 815 | marker: true, 816 | }) 817 | const unserialize = (payload: unknown) => { 818 | /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ 819 | const { marker, ...rest } = payload as Record 820 | return rest as SessionData 821 | } 822 | 823 | ;({ store, storePromise } = createStoreHelper({ 824 | cryptoAdapter: adapter, 825 | serialize, 826 | unserialize, 827 | collectionName: 'crypto-custom-serialize', 828 | })) 829 | const sid = 'crypto-custom-serialize' 830 | const original = makeData() 831 | await storePromise.set(sid, original) 832 | const session = await storePromise.get(sid) 833 | t.deepEqual(session, JSON.parse(JSON.stringify(original))) 834 | } 835 | ) 836 | 837 | test.serial('basic operation flow with crypto', async (t) => { 838 | ;({ store, storePromise } = createStoreHelper({ 839 | crypto: { secret: 'secret' }, 840 | collectionName: 'crypto-test', 841 | autoRemove: 'disabled', 842 | })) 843 | let orgSession = makeData() 844 | const sid = 'test-basic-flow-with-crypto' 845 | const res = await storePromise.set(sid, orgSession) 846 | t.is(res, undefined) 847 | const session = await storePromise.get(sid) 848 | orgSession = JSON.parse(JSON.stringify(orgSession)) 849 | t.deepEqual(session, orgSession) 850 | const sessions = await storePromise.all() 851 | t.not(sessions, undefined) 852 | t.not(sessions, null) 853 | t.is(sessions?.length, 1) 854 | }) 855 | 856 | test.serial('crypto with stringify=false roundtrips raw objects', async (t) => { 857 | ;({ store, storePromise } = createStoreHelper({ 858 | crypto: { secret: 'secret' }, 859 | stringify: false, 860 | collectionName: 'crypto-no-stringify', 861 | })) 862 | const sid = 'crypto-no-stringify' 863 | const payload = makeDataNoCookie() 864 | // @ts-ignore 865 | await storePromise.set(sid, payload) 866 | const session = await storePromise.get(sid) 867 | t.deepEqual(session, payload) 868 | }) 869 | 870 | test.serial( 871 | 'transformId stores and retrieves using transformed key', 872 | async (t) => { 873 | const transformId = (sid: string) => `t-${sid}` 874 | ;({ store, storePromise } = createStoreHelper({ transformId })) 875 | const sid = 'transform-id' 876 | await storePromise.set(sid, makeData()) 877 | const collection = await store.collectionP 878 | const doc = await collection.findOne({ _id: transformId(sid) }) 879 | t.truthy(doc) 880 | const session = await storePromise.get(sid) 881 | t.truthy(session) 882 | } 883 | ) 884 | 885 | test.serial('writeOperationOptions forwarded to updateOne', async (t) => { 886 | const calls: any[] = [] 887 | const fakeCollection = { 888 | createIndex: () => Promise.resolve(), 889 | updateOne: (...args: any[]) => { 890 | calls.push(args) 891 | return Promise.resolve({ upsertedCount: 1 }) 892 | }, 893 | } 894 | const fakeClient = { 895 | db: () => ({ collection: () => fakeCollection }), 896 | close: () => Promise.resolve(), 897 | } 898 | 899 | const writeConcern = { w: 0 as const } 900 | const localStore = MongoStore.create({ 901 | clientPromise: Promise.resolve(fakeClient as unknown as MongoClient), 902 | writeOperationOptions: writeConcern, 903 | collectionName: 'wopts', 904 | dbName: 'wopts-db', 905 | }) 906 | await new Promise((resolve, reject) => 907 | localStore.set('wopts', makeData(), (err) => 908 | err ? reject(err) : resolve() 909 | ) 910 | ) 911 | t.true(calls.length > 0) 912 | const opts = calls[0]?.[2] 913 | t.deepEqual(opts?.writeConcern, writeConcern) 914 | await localStore.close() 915 | }) 916 | 917 | test.serial('custom serializer error surfaces from set()', async (t) => { 918 | const boom = new Error('serialize-fail') 919 | ;({ store, storePromise } = createStoreHelper({ 920 | serialize: () => { 921 | throw boom 922 | }, 923 | })) 924 | const sid = 'serializer-error' 925 | await t.throwsAsync(() => storePromise.set(sid, makeData()), { 926 | message: boom.message, 927 | }) 928 | }) 929 | 930 | test.serial('corrupted JSON payload bubbles error on get', async (t) => { 931 | ;({ store, storePromise } = createStoreHelper()) 932 | const collection = await store.collectionP 933 | await collection.insertOne({ 934 | _id: 'corrupt-json', 935 | session: '{bad json', 936 | }) 937 | await t.throwsAsync(() => storePromise.get('corrupt-json')) 938 | }) 939 | 940 | test.serial('with touch after and get non-exist session', async (t) => { 941 | ;({ store, storePromise } = createStoreHelper({ 942 | touchAfter: 10, 943 | })) 944 | const sid = 'fake-sid-for-test-touch-after' 945 | const res = await storePromise.get(sid) 946 | t.is(res, null) 947 | }) 948 | -------------------------------------------------------------------------------- /example/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-mongo-example", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "connect-mongo-example", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "connect-mongo": "^5.1.0", 13 | "dotenv": "^16.4.5", 14 | "express": "^4.18.2", 15 | "express-session": "^1.17.3", 16 | "mongoose": "^8.0.0" 17 | }, 18 | "devDependencies": { 19 | "@types/express": "^4.17.21", 20 | "@types/express-session": "^1.18.2", 21 | "@types/node": "^20.16.5", 22 | "typescript": "^5.9.3" 23 | } 24 | }, 25 | "node_modules/@mongodb-js/saslprep": { 26 | "version": "1.3.2", 27 | "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", 28 | "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", 29 | "license": "MIT", 30 | "dependencies": { 31 | "sparse-bitfield": "^3.0.3" 32 | } 33 | }, 34 | "node_modules/@types/body-parser": { 35 | "version": "1.19.6", 36 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", 37 | "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", 38 | "dev": true, 39 | "license": "MIT", 40 | "dependencies": { 41 | "@types/connect": "*", 42 | "@types/node": "*" 43 | } 44 | }, 45 | "node_modules/@types/connect": { 46 | "version": "3.4.38", 47 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", 48 | "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", 49 | "dev": true, 50 | "license": "MIT", 51 | "dependencies": { 52 | "@types/node": "*" 53 | } 54 | }, 55 | "node_modules/@types/express": { 56 | "version": "4.17.25", 57 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", 58 | "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", 59 | "dev": true, 60 | "license": "MIT", 61 | "dependencies": { 62 | "@types/body-parser": "*", 63 | "@types/express-serve-static-core": "^4.17.33", 64 | "@types/qs": "*", 65 | "@types/serve-static": "^1" 66 | } 67 | }, 68 | "node_modules/@types/express-serve-static-core": { 69 | "version": "4.19.7", 70 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", 71 | "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", 72 | "dev": true, 73 | "license": "MIT", 74 | "dependencies": { 75 | "@types/node": "*", 76 | "@types/qs": "*", 77 | "@types/range-parser": "*", 78 | "@types/send": "*" 79 | } 80 | }, 81 | "node_modules/@types/express-session": { 82 | "version": "1.18.2", 83 | "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.2.tgz", 84 | "integrity": "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==", 85 | "dev": true, 86 | "license": "MIT", 87 | "dependencies": { 88 | "@types/express": "*" 89 | } 90 | }, 91 | "node_modules/@types/http-errors": { 92 | "version": "2.0.5", 93 | "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", 94 | "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", 95 | "dev": true, 96 | "license": "MIT" 97 | }, 98 | "node_modules/@types/mime": { 99 | "version": "1.3.5", 100 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", 101 | "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", 102 | "dev": true, 103 | "license": "MIT" 104 | }, 105 | "node_modules/@types/node": { 106 | "version": "20.19.25", 107 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", 108 | "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", 109 | "dev": true, 110 | "license": "MIT", 111 | "dependencies": { 112 | "undici-types": "~6.21.0" 113 | } 114 | }, 115 | "node_modules/@types/qs": { 116 | "version": "6.14.0", 117 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", 118 | "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", 119 | "dev": true, 120 | "license": "MIT" 121 | }, 122 | "node_modules/@types/range-parser": { 123 | "version": "1.2.7", 124 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", 125 | "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", 126 | "dev": true, 127 | "license": "MIT" 128 | }, 129 | "node_modules/@types/send": { 130 | "version": "1.2.1", 131 | "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", 132 | "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", 133 | "dev": true, 134 | "license": "MIT", 135 | "dependencies": { 136 | "@types/node": "*" 137 | } 138 | }, 139 | "node_modules/@types/serve-static": { 140 | "version": "1.15.10", 141 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", 142 | "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", 143 | "dev": true, 144 | "license": "MIT", 145 | "dependencies": { 146 | "@types/http-errors": "*", 147 | "@types/node": "*", 148 | "@types/send": "<1" 149 | } 150 | }, 151 | "node_modules/@types/serve-static/node_modules/@types/send": { 152 | "version": "0.17.6", 153 | "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", 154 | "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", 155 | "dev": true, 156 | "license": "MIT", 157 | "dependencies": { 158 | "@types/mime": "^1", 159 | "@types/node": "*" 160 | } 161 | }, 162 | "node_modules/@types/webidl-conversions": { 163 | "version": "7.0.3", 164 | "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", 165 | "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", 166 | "license": "MIT" 167 | }, 168 | "node_modules/@types/whatwg-url": { 169 | "version": "11.0.5", 170 | "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", 171 | "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", 172 | "license": "MIT", 173 | "dependencies": { 174 | "@types/webidl-conversions": "*" 175 | } 176 | }, 177 | "node_modules/accepts": { 178 | "version": "1.3.8", 179 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 180 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 181 | "license": "MIT", 182 | "dependencies": { 183 | "mime-types": "~2.1.34", 184 | "negotiator": "0.6.3" 185 | }, 186 | "engines": { 187 | "node": ">= 0.6" 188 | } 189 | }, 190 | "node_modules/array-flatten": { 191 | "version": "1.1.1", 192 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 193 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", 194 | "license": "MIT" 195 | }, 196 | "node_modules/asn1.js": { 197 | "version": "5.4.1", 198 | "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", 199 | "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", 200 | "license": "MIT", 201 | "dependencies": { 202 | "bn.js": "^4.0.0", 203 | "inherits": "^2.0.1", 204 | "minimalistic-assert": "^1.0.0", 205 | "safer-buffer": "^2.1.0" 206 | } 207 | }, 208 | "node_modules/bn.js": { 209 | "version": "4.12.2", 210 | "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", 211 | "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", 212 | "license": "MIT" 213 | }, 214 | "node_modules/body-parser": { 215 | "version": "1.20.3", 216 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", 217 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", 218 | "license": "MIT", 219 | "dependencies": { 220 | "bytes": "3.1.2", 221 | "content-type": "~1.0.5", 222 | "debug": "2.6.9", 223 | "depd": "2.0.0", 224 | "destroy": "1.2.0", 225 | "http-errors": "2.0.0", 226 | "iconv-lite": "0.4.24", 227 | "on-finished": "2.4.1", 228 | "qs": "6.13.0", 229 | "raw-body": "2.5.2", 230 | "type-is": "~1.6.18", 231 | "unpipe": "1.0.0" 232 | }, 233 | "engines": { 234 | "node": ">= 0.8", 235 | "npm": "1.2.8000 || >= 1.4.16" 236 | } 237 | }, 238 | "node_modules/body-parser/node_modules/debug": { 239 | "version": "2.6.9", 240 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 241 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 242 | "license": "MIT", 243 | "dependencies": { 244 | "ms": "2.0.0" 245 | } 246 | }, 247 | "node_modules/body-parser/node_modules/ms": { 248 | "version": "2.0.0", 249 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 250 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 251 | "license": "MIT" 252 | }, 253 | "node_modules/bson": { 254 | "version": "6.10.4", 255 | "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", 256 | "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", 257 | "license": "Apache-2.0", 258 | "engines": { 259 | "node": ">=16.20.1" 260 | } 261 | }, 262 | "node_modules/bytes": { 263 | "version": "3.1.2", 264 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 265 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 266 | "license": "MIT", 267 | "engines": { 268 | "node": ">= 0.8" 269 | } 270 | }, 271 | "node_modules/call-bind-apply-helpers": { 272 | "version": "1.0.2", 273 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 274 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 275 | "license": "MIT", 276 | "dependencies": { 277 | "es-errors": "^1.3.0", 278 | "function-bind": "^1.1.2" 279 | }, 280 | "engines": { 281 | "node": ">= 0.4" 282 | } 283 | }, 284 | "node_modules/call-bound": { 285 | "version": "1.0.4", 286 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 287 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 288 | "license": "MIT", 289 | "dependencies": { 290 | "call-bind-apply-helpers": "^1.0.2", 291 | "get-intrinsic": "^1.3.0" 292 | }, 293 | "engines": { 294 | "node": ">= 0.4" 295 | }, 296 | "funding": { 297 | "url": "https://github.com/sponsors/ljharb" 298 | } 299 | }, 300 | "node_modules/connect-mongo": { 301 | "version": "5.1.0", 302 | "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.1.0.tgz", 303 | "integrity": "sha512-xT0vxQLqyqoUTxPLzlP9a/u+vir0zNkhiy9uAdHjSCcUUf7TS5b55Icw8lVyYFxfemP3Mf9gdwUOgeF3cxCAhw==", 304 | "license": "MIT", 305 | "dependencies": { 306 | "debug": "^4.3.1", 307 | "kruptein": "^3.0.0" 308 | }, 309 | "engines": { 310 | "node": ">=12.9.0" 311 | }, 312 | "peerDependencies": { 313 | "express-session": "^1.17.1", 314 | "mongodb": ">= 5.1.0 < 7" 315 | } 316 | }, 317 | "node_modules/content-disposition": { 318 | "version": "0.5.4", 319 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 320 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 321 | "license": "MIT", 322 | "dependencies": { 323 | "safe-buffer": "5.2.1" 324 | }, 325 | "engines": { 326 | "node": ">= 0.6" 327 | } 328 | }, 329 | "node_modules/content-type": { 330 | "version": "1.0.5", 331 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 332 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 333 | "license": "MIT", 334 | "engines": { 335 | "node": ">= 0.6" 336 | } 337 | }, 338 | "node_modules/cookie": { 339 | "version": "0.7.1", 340 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", 341 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", 342 | "license": "MIT", 343 | "engines": { 344 | "node": ">= 0.6" 345 | } 346 | }, 347 | "node_modules/cookie-signature": { 348 | "version": "1.0.6", 349 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 350 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", 351 | "license": "MIT" 352 | }, 353 | "node_modules/debug": { 354 | "version": "4.4.3", 355 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 356 | "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 357 | "license": "MIT", 358 | "dependencies": { 359 | "ms": "^2.1.3" 360 | }, 361 | "engines": { 362 | "node": ">=6.0" 363 | }, 364 | "peerDependenciesMeta": { 365 | "supports-color": { 366 | "optional": true 367 | } 368 | } 369 | }, 370 | "node_modules/depd": { 371 | "version": "2.0.0", 372 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 373 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 374 | "license": "MIT", 375 | "engines": { 376 | "node": ">= 0.8" 377 | } 378 | }, 379 | "node_modules/destroy": { 380 | "version": "1.2.0", 381 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 382 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 383 | "license": "MIT", 384 | "engines": { 385 | "node": ">= 0.8", 386 | "npm": "1.2.8000 || >= 1.4.16" 387 | } 388 | }, 389 | "node_modules/dotenv": { 390 | "version": "16.6.1", 391 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", 392 | "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", 393 | "license": "BSD-2-Clause", 394 | "engines": { 395 | "node": ">=12" 396 | }, 397 | "funding": { 398 | "url": "https://dotenvx.com" 399 | } 400 | }, 401 | "node_modules/dunder-proto": { 402 | "version": "1.0.1", 403 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 404 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 405 | "license": "MIT", 406 | "dependencies": { 407 | "call-bind-apply-helpers": "^1.0.1", 408 | "es-errors": "^1.3.0", 409 | "gopd": "^1.2.0" 410 | }, 411 | "engines": { 412 | "node": ">= 0.4" 413 | } 414 | }, 415 | "node_modules/ee-first": { 416 | "version": "1.1.1", 417 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 418 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 419 | "license": "MIT" 420 | }, 421 | "node_modules/encodeurl": { 422 | "version": "2.0.0", 423 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 424 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 425 | "license": "MIT", 426 | "engines": { 427 | "node": ">= 0.8" 428 | } 429 | }, 430 | "node_modules/es-define-property": { 431 | "version": "1.0.1", 432 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 433 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 434 | "license": "MIT", 435 | "engines": { 436 | "node": ">= 0.4" 437 | } 438 | }, 439 | "node_modules/es-errors": { 440 | "version": "1.3.0", 441 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 442 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 443 | "license": "MIT", 444 | "engines": { 445 | "node": ">= 0.4" 446 | } 447 | }, 448 | "node_modules/es-object-atoms": { 449 | "version": "1.1.1", 450 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 451 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 452 | "license": "MIT", 453 | "dependencies": { 454 | "es-errors": "^1.3.0" 455 | }, 456 | "engines": { 457 | "node": ">= 0.4" 458 | } 459 | }, 460 | "node_modules/escape-html": { 461 | "version": "1.0.3", 462 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 463 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 464 | "license": "MIT" 465 | }, 466 | "node_modules/etag": { 467 | "version": "1.8.1", 468 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 469 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 470 | "license": "MIT", 471 | "engines": { 472 | "node": ">= 0.6" 473 | } 474 | }, 475 | "node_modules/express": { 476 | "version": "4.21.2", 477 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", 478 | "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", 479 | "license": "MIT", 480 | "dependencies": { 481 | "accepts": "~1.3.8", 482 | "array-flatten": "1.1.1", 483 | "body-parser": "1.20.3", 484 | "content-disposition": "0.5.4", 485 | "content-type": "~1.0.4", 486 | "cookie": "0.7.1", 487 | "cookie-signature": "1.0.6", 488 | "debug": "2.6.9", 489 | "depd": "2.0.0", 490 | "encodeurl": "~2.0.0", 491 | "escape-html": "~1.0.3", 492 | "etag": "~1.8.1", 493 | "finalhandler": "1.3.1", 494 | "fresh": "0.5.2", 495 | "http-errors": "2.0.0", 496 | "merge-descriptors": "1.0.3", 497 | "methods": "~1.1.2", 498 | "on-finished": "2.4.1", 499 | "parseurl": "~1.3.3", 500 | "path-to-regexp": "0.1.12", 501 | "proxy-addr": "~2.0.7", 502 | "qs": "6.13.0", 503 | "range-parser": "~1.2.1", 504 | "safe-buffer": "5.2.1", 505 | "send": "0.19.0", 506 | "serve-static": "1.16.2", 507 | "setprototypeof": "1.2.0", 508 | "statuses": "2.0.1", 509 | "type-is": "~1.6.18", 510 | "utils-merge": "1.0.1", 511 | "vary": "~1.1.2" 512 | }, 513 | "engines": { 514 | "node": ">= 0.10.0" 515 | }, 516 | "funding": { 517 | "type": "opencollective", 518 | "url": "https://opencollective.com/express" 519 | } 520 | }, 521 | "node_modules/express-session": { 522 | "version": "1.18.2", 523 | "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", 524 | "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", 525 | "license": "MIT", 526 | "peer": true, 527 | "dependencies": { 528 | "cookie": "0.7.2", 529 | "cookie-signature": "1.0.7", 530 | "debug": "2.6.9", 531 | "depd": "~2.0.0", 532 | "on-headers": "~1.1.0", 533 | "parseurl": "~1.3.3", 534 | "safe-buffer": "5.2.1", 535 | "uid-safe": "~2.1.5" 536 | }, 537 | "engines": { 538 | "node": ">= 0.8.0" 539 | } 540 | }, 541 | "node_modules/express-session/node_modules/cookie": { 542 | "version": "0.7.2", 543 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 544 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 545 | "license": "MIT", 546 | "engines": { 547 | "node": ">= 0.6" 548 | } 549 | }, 550 | "node_modules/express-session/node_modules/cookie-signature": { 551 | "version": "1.0.7", 552 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", 553 | "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", 554 | "license": "MIT" 555 | }, 556 | "node_modules/express-session/node_modules/debug": { 557 | "version": "2.6.9", 558 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 559 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 560 | "license": "MIT", 561 | "dependencies": { 562 | "ms": "2.0.0" 563 | } 564 | }, 565 | "node_modules/express-session/node_modules/ms": { 566 | "version": "2.0.0", 567 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 568 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 569 | "license": "MIT" 570 | }, 571 | "node_modules/express/node_modules/debug": { 572 | "version": "2.6.9", 573 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 574 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 575 | "license": "MIT", 576 | "dependencies": { 577 | "ms": "2.0.0" 578 | } 579 | }, 580 | "node_modules/express/node_modules/ms": { 581 | "version": "2.0.0", 582 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 583 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 584 | "license": "MIT" 585 | }, 586 | "node_modules/finalhandler": { 587 | "version": "1.3.1", 588 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", 589 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", 590 | "license": "MIT", 591 | "dependencies": { 592 | "debug": "2.6.9", 593 | "encodeurl": "~2.0.0", 594 | "escape-html": "~1.0.3", 595 | "on-finished": "2.4.1", 596 | "parseurl": "~1.3.3", 597 | "statuses": "2.0.1", 598 | "unpipe": "~1.0.0" 599 | }, 600 | "engines": { 601 | "node": ">= 0.8" 602 | } 603 | }, 604 | "node_modules/finalhandler/node_modules/debug": { 605 | "version": "2.6.9", 606 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 607 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 608 | "license": "MIT", 609 | "dependencies": { 610 | "ms": "2.0.0" 611 | } 612 | }, 613 | "node_modules/finalhandler/node_modules/ms": { 614 | "version": "2.0.0", 615 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 616 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 617 | "license": "MIT" 618 | }, 619 | "node_modules/forwarded": { 620 | "version": "0.2.0", 621 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 622 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 623 | "license": "MIT", 624 | "engines": { 625 | "node": ">= 0.6" 626 | } 627 | }, 628 | "node_modules/fresh": { 629 | "version": "0.5.2", 630 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 631 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 632 | "license": "MIT", 633 | "engines": { 634 | "node": ">= 0.6" 635 | } 636 | }, 637 | "node_modules/function-bind": { 638 | "version": "1.1.2", 639 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 640 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 641 | "license": "MIT", 642 | "funding": { 643 | "url": "https://github.com/sponsors/ljharb" 644 | } 645 | }, 646 | "node_modules/get-intrinsic": { 647 | "version": "1.3.0", 648 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 649 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 650 | "license": "MIT", 651 | "dependencies": { 652 | "call-bind-apply-helpers": "^1.0.2", 653 | "es-define-property": "^1.0.1", 654 | "es-errors": "^1.3.0", 655 | "es-object-atoms": "^1.1.1", 656 | "function-bind": "^1.1.2", 657 | "get-proto": "^1.0.1", 658 | "gopd": "^1.2.0", 659 | "has-symbols": "^1.1.0", 660 | "hasown": "^2.0.2", 661 | "math-intrinsics": "^1.1.0" 662 | }, 663 | "engines": { 664 | "node": ">= 0.4" 665 | }, 666 | "funding": { 667 | "url": "https://github.com/sponsors/ljharb" 668 | } 669 | }, 670 | "node_modules/get-proto": { 671 | "version": "1.0.1", 672 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 673 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 674 | "license": "MIT", 675 | "dependencies": { 676 | "dunder-proto": "^1.0.1", 677 | "es-object-atoms": "^1.0.0" 678 | }, 679 | "engines": { 680 | "node": ">= 0.4" 681 | } 682 | }, 683 | "node_modules/gopd": { 684 | "version": "1.2.0", 685 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 686 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 687 | "license": "MIT", 688 | "engines": { 689 | "node": ">= 0.4" 690 | }, 691 | "funding": { 692 | "url": "https://github.com/sponsors/ljharb" 693 | } 694 | }, 695 | "node_modules/has-symbols": { 696 | "version": "1.1.0", 697 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 698 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 699 | "license": "MIT", 700 | "engines": { 701 | "node": ">= 0.4" 702 | }, 703 | "funding": { 704 | "url": "https://github.com/sponsors/ljharb" 705 | } 706 | }, 707 | "node_modules/hasown": { 708 | "version": "2.0.2", 709 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 710 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 711 | "license": "MIT", 712 | "dependencies": { 713 | "function-bind": "^1.1.2" 714 | }, 715 | "engines": { 716 | "node": ">= 0.4" 717 | } 718 | }, 719 | "node_modules/http-errors": { 720 | "version": "2.0.0", 721 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 722 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 723 | "license": "MIT", 724 | "dependencies": { 725 | "depd": "2.0.0", 726 | "inherits": "2.0.4", 727 | "setprototypeof": "1.2.0", 728 | "statuses": "2.0.1", 729 | "toidentifier": "1.0.1" 730 | }, 731 | "engines": { 732 | "node": ">= 0.8" 733 | } 734 | }, 735 | "node_modules/iconv-lite": { 736 | "version": "0.4.24", 737 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 738 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 739 | "license": "MIT", 740 | "dependencies": { 741 | "safer-buffer": ">= 2.1.2 < 3" 742 | }, 743 | "engines": { 744 | "node": ">=0.10.0" 745 | } 746 | }, 747 | "node_modules/inherits": { 748 | "version": "2.0.4", 749 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 750 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 751 | "license": "ISC" 752 | }, 753 | "node_modules/ipaddr.js": { 754 | "version": "1.9.1", 755 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 756 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 757 | "license": "MIT", 758 | "engines": { 759 | "node": ">= 0.10" 760 | } 761 | }, 762 | "node_modules/kareem": { 763 | "version": "2.6.3", 764 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", 765 | "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", 766 | "license": "Apache-2.0", 767 | "engines": { 768 | "node": ">=12.0.0" 769 | } 770 | }, 771 | "node_modules/kruptein": { 772 | "version": "3.1.4", 773 | "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.1.4.tgz", 774 | "integrity": "sha512-Hqxvup8QFK2KXtvbEBQTe9yGZJbZtjiqhrHJkLweXHmeC4WuQompi0h5YfSizlG+EM61AXw6HeufTXbpc1lltg==", 775 | "license": "MIT", 776 | "dependencies": { 777 | "asn1.js": "^5.4.1" 778 | }, 779 | "engines": { 780 | "node": ">8" 781 | } 782 | }, 783 | "node_modules/math-intrinsics": { 784 | "version": "1.1.0", 785 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 786 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 787 | "license": "MIT", 788 | "engines": { 789 | "node": ">= 0.4" 790 | } 791 | }, 792 | "node_modules/media-typer": { 793 | "version": "0.3.0", 794 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 795 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 796 | "license": "MIT", 797 | "engines": { 798 | "node": ">= 0.6" 799 | } 800 | }, 801 | "node_modules/memory-pager": { 802 | "version": "1.5.0", 803 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 804 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", 805 | "license": "MIT" 806 | }, 807 | "node_modules/merge-descriptors": { 808 | "version": "1.0.3", 809 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", 810 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", 811 | "license": "MIT", 812 | "funding": { 813 | "url": "https://github.com/sponsors/sindresorhus" 814 | } 815 | }, 816 | "node_modules/methods": { 817 | "version": "1.1.2", 818 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 819 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 820 | "license": "MIT", 821 | "engines": { 822 | "node": ">= 0.6" 823 | } 824 | }, 825 | "node_modules/mime": { 826 | "version": "1.6.0", 827 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 828 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 829 | "license": "MIT", 830 | "bin": { 831 | "mime": "cli.js" 832 | }, 833 | "engines": { 834 | "node": ">=4" 835 | } 836 | }, 837 | "node_modules/mime-db": { 838 | "version": "1.52.0", 839 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 840 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 841 | "license": "MIT", 842 | "engines": { 843 | "node": ">= 0.6" 844 | } 845 | }, 846 | "node_modules/mime-types": { 847 | "version": "2.1.35", 848 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 849 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 850 | "license": "MIT", 851 | "dependencies": { 852 | "mime-db": "1.52.0" 853 | }, 854 | "engines": { 855 | "node": ">= 0.6" 856 | } 857 | }, 858 | "node_modules/minimalistic-assert": { 859 | "version": "1.0.1", 860 | "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", 861 | "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", 862 | "license": "ISC" 863 | }, 864 | "node_modules/mongodb": { 865 | "version": "6.21.0", 866 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", 867 | "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", 868 | "license": "Apache-2.0", 869 | "peer": true, 870 | "dependencies": { 871 | "@mongodb-js/saslprep": "^1.3.0", 872 | "bson": "^6.10.4", 873 | "mongodb-connection-string-url": "^3.0.2" 874 | }, 875 | "engines": { 876 | "node": ">=16.20.1" 877 | }, 878 | "peerDependencies": { 879 | "@aws-sdk/credential-providers": "^3.188.0", 880 | "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", 881 | "gcp-metadata": "^5.2.0", 882 | "kerberos": "^2.0.1", 883 | "mongodb-client-encryption": ">=6.0.0 <7", 884 | "snappy": "^7.3.2", 885 | "socks": "^2.7.1" 886 | }, 887 | "peerDependenciesMeta": { 888 | "@aws-sdk/credential-providers": { 889 | "optional": true 890 | }, 891 | "@mongodb-js/zstd": { 892 | "optional": true 893 | }, 894 | "gcp-metadata": { 895 | "optional": true 896 | }, 897 | "kerberos": { 898 | "optional": true 899 | }, 900 | "mongodb-client-encryption": { 901 | "optional": true 902 | }, 903 | "snappy": { 904 | "optional": true 905 | }, 906 | "socks": { 907 | "optional": true 908 | } 909 | } 910 | }, 911 | "node_modules/mongodb-connection-string-url": { 912 | "version": "3.0.2", 913 | "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", 914 | "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", 915 | "license": "Apache-2.0", 916 | "dependencies": { 917 | "@types/whatwg-url": "^11.0.2", 918 | "whatwg-url": "^14.1.0 || ^13.0.0" 919 | } 920 | }, 921 | "node_modules/mongoose": { 922 | "version": "8.20.1", 923 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.20.1.tgz", 924 | "integrity": "sha512-G+n3maddlqkQrP1nXxsI0q20144OSo+pe+HzRRGqaC4yK3FLYKqejqB9cbIi+SX7eoRsnG23LHGYNp8n7mWL2Q==", 925 | "license": "MIT", 926 | "dependencies": { 927 | "bson": "^6.10.4", 928 | "kareem": "2.6.3", 929 | "mongodb": "~6.20.0", 930 | "mpath": "0.9.0", 931 | "mquery": "5.0.0", 932 | "ms": "2.1.3", 933 | "sift": "17.1.3" 934 | }, 935 | "engines": { 936 | "node": ">=16.20.1" 937 | }, 938 | "funding": { 939 | "type": "opencollective", 940 | "url": "https://opencollective.com/mongoose" 941 | } 942 | }, 943 | "node_modules/mongoose/node_modules/mongodb": { 944 | "version": "6.20.0", 945 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", 946 | "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", 947 | "license": "Apache-2.0", 948 | "dependencies": { 949 | "@mongodb-js/saslprep": "^1.3.0", 950 | "bson": "^6.10.4", 951 | "mongodb-connection-string-url": "^3.0.2" 952 | }, 953 | "engines": { 954 | "node": ">=16.20.1" 955 | }, 956 | "peerDependencies": { 957 | "@aws-sdk/credential-providers": "^3.188.0", 958 | "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", 959 | "gcp-metadata": "^5.2.0", 960 | "kerberos": "^2.0.1", 961 | "mongodb-client-encryption": ">=6.0.0 <7", 962 | "snappy": "^7.3.2", 963 | "socks": "^2.7.1" 964 | }, 965 | "peerDependenciesMeta": { 966 | "@aws-sdk/credential-providers": { 967 | "optional": true 968 | }, 969 | "@mongodb-js/zstd": { 970 | "optional": true 971 | }, 972 | "gcp-metadata": { 973 | "optional": true 974 | }, 975 | "kerberos": { 976 | "optional": true 977 | }, 978 | "mongodb-client-encryption": { 979 | "optional": true 980 | }, 981 | "snappy": { 982 | "optional": true 983 | }, 984 | "socks": { 985 | "optional": true 986 | } 987 | } 988 | }, 989 | "node_modules/mpath": { 990 | "version": "0.9.0", 991 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", 992 | "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", 993 | "license": "MIT", 994 | "engines": { 995 | "node": ">=4.0.0" 996 | } 997 | }, 998 | "node_modules/mquery": { 999 | "version": "5.0.0", 1000 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", 1001 | "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", 1002 | "license": "MIT", 1003 | "dependencies": { 1004 | "debug": "4.x" 1005 | }, 1006 | "engines": { 1007 | "node": ">=14.0.0" 1008 | } 1009 | }, 1010 | "node_modules/ms": { 1011 | "version": "2.1.3", 1012 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1013 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1014 | "license": "MIT" 1015 | }, 1016 | "node_modules/negotiator": { 1017 | "version": "0.6.3", 1018 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 1019 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 1020 | "license": "MIT", 1021 | "engines": { 1022 | "node": ">= 0.6" 1023 | } 1024 | }, 1025 | "node_modules/object-inspect": { 1026 | "version": "1.13.4", 1027 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 1028 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 1029 | "license": "MIT", 1030 | "engines": { 1031 | "node": ">= 0.4" 1032 | }, 1033 | "funding": { 1034 | "url": "https://github.com/sponsors/ljharb" 1035 | } 1036 | }, 1037 | "node_modules/on-finished": { 1038 | "version": "2.4.1", 1039 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 1040 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 1041 | "license": "MIT", 1042 | "dependencies": { 1043 | "ee-first": "1.1.1" 1044 | }, 1045 | "engines": { 1046 | "node": ">= 0.8" 1047 | } 1048 | }, 1049 | "node_modules/on-headers": { 1050 | "version": "1.1.0", 1051 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", 1052 | "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", 1053 | "license": "MIT", 1054 | "engines": { 1055 | "node": ">= 0.8" 1056 | } 1057 | }, 1058 | "node_modules/parseurl": { 1059 | "version": "1.3.3", 1060 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1061 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 1062 | "license": "MIT", 1063 | "engines": { 1064 | "node": ">= 0.8" 1065 | } 1066 | }, 1067 | "node_modules/path-to-regexp": { 1068 | "version": "0.1.12", 1069 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", 1070 | "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", 1071 | "license": "MIT" 1072 | }, 1073 | "node_modules/proxy-addr": { 1074 | "version": "2.0.7", 1075 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 1076 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 1077 | "license": "MIT", 1078 | "dependencies": { 1079 | "forwarded": "0.2.0", 1080 | "ipaddr.js": "1.9.1" 1081 | }, 1082 | "engines": { 1083 | "node": ">= 0.10" 1084 | } 1085 | }, 1086 | "node_modules/punycode": { 1087 | "version": "2.3.1", 1088 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 1089 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 1090 | "license": "MIT", 1091 | "engines": { 1092 | "node": ">=6" 1093 | } 1094 | }, 1095 | "node_modules/qs": { 1096 | "version": "6.13.0", 1097 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 1098 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 1099 | "license": "BSD-3-Clause", 1100 | "dependencies": { 1101 | "side-channel": "^1.0.6" 1102 | }, 1103 | "engines": { 1104 | "node": ">=0.6" 1105 | }, 1106 | "funding": { 1107 | "url": "https://github.com/sponsors/ljharb" 1108 | } 1109 | }, 1110 | "node_modules/random-bytes": { 1111 | "version": "1.0.0", 1112 | "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", 1113 | "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", 1114 | "license": "MIT", 1115 | "engines": { 1116 | "node": ">= 0.8" 1117 | } 1118 | }, 1119 | "node_modules/range-parser": { 1120 | "version": "1.2.1", 1121 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1122 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 1123 | "license": "MIT", 1124 | "engines": { 1125 | "node": ">= 0.6" 1126 | } 1127 | }, 1128 | "node_modules/raw-body": { 1129 | "version": "2.5.2", 1130 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 1131 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 1132 | "license": "MIT", 1133 | "dependencies": { 1134 | "bytes": "3.1.2", 1135 | "http-errors": "2.0.0", 1136 | "iconv-lite": "0.4.24", 1137 | "unpipe": "1.0.0" 1138 | }, 1139 | "engines": { 1140 | "node": ">= 0.8" 1141 | } 1142 | }, 1143 | "node_modules/safe-buffer": { 1144 | "version": "5.2.1", 1145 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1146 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1147 | "funding": [ 1148 | { 1149 | "type": "github", 1150 | "url": "https://github.com/sponsors/feross" 1151 | }, 1152 | { 1153 | "type": "patreon", 1154 | "url": "https://www.patreon.com/feross" 1155 | }, 1156 | { 1157 | "type": "consulting", 1158 | "url": "https://feross.org/support" 1159 | } 1160 | ], 1161 | "license": "MIT" 1162 | }, 1163 | "node_modules/safer-buffer": { 1164 | "version": "2.1.2", 1165 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1166 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1167 | "license": "MIT" 1168 | }, 1169 | "node_modules/send": { 1170 | "version": "0.19.0", 1171 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", 1172 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 1173 | "license": "MIT", 1174 | "dependencies": { 1175 | "debug": "2.6.9", 1176 | "depd": "2.0.0", 1177 | "destroy": "1.2.0", 1178 | "encodeurl": "~1.0.2", 1179 | "escape-html": "~1.0.3", 1180 | "etag": "~1.8.1", 1181 | "fresh": "0.5.2", 1182 | "http-errors": "2.0.0", 1183 | "mime": "1.6.0", 1184 | "ms": "2.1.3", 1185 | "on-finished": "2.4.1", 1186 | "range-parser": "~1.2.1", 1187 | "statuses": "2.0.1" 1188 | }, 1189 | "engines": { 1190 | "node": ">= 0.8.0" 1191 | } 1192 | }, 1193 | "node_modules/send/node_modules/debug": { 1194 | "version": "2.6.9", 1195 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 1196 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 1197 | "license": "MIT", 1198 | "dependencies": { 1199 | "ms": "2.0.0" 1200 | } 1201 | }, 1202 | "node_modules/send/node_modules/debug/node_modules/ms": { 1203 | "version": "2.0.0", 1204 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1205 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 1206 | "license": "MIT" 1207 | }, 1208 | "node_modules/send/node_modules/encodeurl": { 1209 | "version": "1.0.2", 1210 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 1211 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 1212 | "license": "MIT", 1213 | "engines": { 1214 | "node": ">= 0.8" 1215 | } 1216 | }, 1217 | "node_modules/serve-static": { 1218 | "version": "1.16.2", 1219 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", 1220 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", 1221 | "license": "MIT", 1222 | "dependencies": { 1223 | "encodeurl": "~2.0.0", 1224 | "escape-html": "~1.0.3", 1225 | "parseurl": "~1.3.3", 1226 | "send": "0.19.0" 1227 | }, 1228 | "engines": { 1229 | "node": ">= 0.8.0" 1230 | } 1231 | }, 1232 | "node_modules/setprototypeof": { 1233 | "version": "1.2.0", 1234 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1235 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 1236 | "license": "ISC" 1237 | }, 1238 | "node_modules/side-channel": { 1239 | "version": "1.1.0", 1240 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 1241 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 1242 | "license": "MIT", 1243 | "dependencies": { 1244 | "es-errors": "^1.3.0", 1245 | "object-inspect": "^1.13.3", 1246 | "side-channel-list": "^1.0.0", 1247 | "side-channel-map": "^1.0.1", 1248 | "side-channel-weakmap": "^1.0.2" 1249 | }, 1250 | "engines": { 1251 | "node": ">= 0.4" 1252 | }, 1253 | "funding": { 1254 | "url": "https://github.com/sponsors/ljharb" 1255 | } 1256 | }, 1257 | "node_modules/side-channel-list": { 1258 | "version": "1.0.0", 1259 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 1260 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 1261 | "license": "MIT", 1262 | "dependencies": { 1263 | "es-errors": "^1.3.0", 1264 | "object-inspect": "^1.13.3" 1265 | }, 1266 | "engines": { 1267 | "node": ">= 0.4" 1268 | }, 1269 | "funding": { 1270 | "url": "https://github.com/sponsors/ljharb" 1271 | } 1272 | }, 1273 | "node_modules/side-channel-map": { 1274 | "version": "1.0.1", 1275 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 1276 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 1277 | "license": "MIT", 1278 | "dependencies": { 1279 | "call-bound": "^1.0.2", 1280 | "es-errors": "^1.3.0", 1281 | "get-intrinsic": "^1.2.5", 1282 | "object-inspect": "^1.13.3" 1283 | }, 1284 | "engines": { 1285 | "node": ">= 0.4" 1286 | }, 1287 | "funding": { 1288 | "url": "https://github.com/sponsors/ljharb" 1289 | } 1290 | }, 1291 | "node_modules/side-channel-weakmap": { 1292 | "version": "1.0.2", 1293 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 1294 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 1295 | "license": "MIT", 1296 | "dependencies": { 1297 | "call-bound": "^1.0.2", 1298 | "es-errors": "^1.3.0", 1299 | "get-intrinsic": "^1.2.5", 1300 | "object-inspect": "^1.13.3", 1301 | "side-channel-map": "^1.0.1" 1302 | }, 1303 | "engines": { 1304 | "node": ">= 0.4" 1305 | }, 1306 | "funding": { 1307 | "url": "https://github.com/sponsors/ljharb" 1308 | } 1309 | }, 1310 | "node_modules/sift": { 1311 | "version": "17.1.3", 1312 | "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", 1313 | "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", 1314 | "license": "MIT" 1315 | }, 1316 | "node_modules/sparse-bitfield": { 1317 | "version": "3.0.3", 1318 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 1319 | "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", 1320 | "license": "MIT", 1321 | "dependencies": { 1322 | "memory-pager": "^1.0.2" 1323 | } 1324 | }, 1325 | "node_modules/statuses": { 1326 | "version": "2.0.1", 1327 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1328 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1329 | "license": "MIT", 1330 | "engines": { 1331 | "node": ">= 0.8" 1332 | } 1333 | }, 1334 | "node_modules/toidentifier": { 1335 | "version": "1.0.1", 1336 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1337 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1338 | "license": "MIT", 1339 | "engines": { 1340 | "node": ">=0.6" 1341 | } 1342 | }, 1343 | "node_modules/tr46": { 1344 | "version": "5.1.1", 1345 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", 1346 | "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", 1347 | "license": "MIT", 1348 | "dependencies": { 1349 | "punycode": "^2.3.1" 1350 | }, 1351 | "engines": { 1352 | "node": ">=18" 1353 | } 1354 | }, 1355 | "node_modules/type-is": { 1356 | "version": "1.6.18", 1357 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1358 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1359 | "license": "MIT", 1360 | "dependencies": { 1361 | "media-typer": "0.3.0", 1362 | "mime-types": "~2.1.24" 1363 | }, 1364 | "engines": { 1365 | "node": ">= 0.6" 1366 | } 1367 | }, 1368 | "node_modules/typescript": { 1369 | "version": "5.9.3", 1370 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 1371 | "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 1372 | "dev": true, 1373 | "license": "Apache-2.0", 1374 | "bin": { 1375 | "tsc": "bin/tsc", 1376 | "tsserver": "bin/tsserver" 1377 | }, 1378 | "engines": { 1379 | "node": ">=14.17" 1380 | } 1381 | }, 1382 | "node_modules/uid-safe": { 1383 | "version": "2.1.5", 1384 | "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", 1385 | "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", 1386 | "license": "MIT", 1387 | "dependencies": { 1388 | "random-bytes": "~1.0.0" 1389 | }, 1390 | "engines": { 1391 | "node": ">= 0.8" 1392 | } 1393 | }, 1394 | "node_modules/undici-types": { 1395 | "version": "6.21.0", 1396 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 1397 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 1398 | "dev": true, 1399 | "license": "MIT" 1400 | }, 1401 | "node_modules/unpipe": { 1402 | "version": "1.0.0", 1403 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1404 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1405 | "license": "MIT", 1406 | "engines": { 1407 | "node": ">= 0.8" 1408 | } 1409 | }, 1410 | "node_modules/utils-merge": { 1411 | "version": "1.0.1", 1412 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1413 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 1414 | "license": "MIT", 1415 | "engines": { 1416 | "node": ">= 0.4.0" 1417 | } 1418 | }, 1419 | "node_modules/vary": { 1420 | "version": "1.1.2", 1421 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1422 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1423 | "license": "MIT", 1424 | "engines": { 1425 | "node": ">= 0.8" 1426 | } 1427 | }, 1428 | "node_modules/webidl-conversions": { 1429 | "version": "7.0.0", 1430 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", 1431 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", 1432 | "license": "BSD-2-Clause", 1433 | "engines": { 1434 | "node": ">=12" 1435 | } 1436 | }, 1437 | "node_modules/whatwg-url": { 1438 | "version": "14.2.0", 1439 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", 1440 | "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", 1441 | "license": "MIT", 1442 | "dependencies": { 1443 | "tr46": "^5.1.0", 1444 | "webidl-conversions": "^7.0.0" 1445 | }, 1446 | "engines": { 1447 | "node": ">=18" 1448 | } 1449 | } 1450 | } 1451 | } 1452 | --------------------------------------------------------------------------------