Skip to content

Commit 81c301d

Browse files
committed
fix: make decrypt errors in get() single-callback and add test (plan#runtime)
1 parent 8dcae07 commit 81c301d

4 files changed

Lines changed: 32 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- **Packaging:** npm package now ships dual ESM/CJS bundles via `tsdown`, with an explicit exports map and cleaned type declarations (`.d.ts`/`.d.cts`).
1212
- **Types:** `MongoStore` and option hooks are strongly typed to avoid `any` leaks.
1313
- **Fixed:** `store.clear()` now uses `deleteMany({})` instead of `collection.drop()`, preserving TTL indexes and treating `NamespaceNotFound` as success so clears are idempotent.
14+
- **Fixed:** Decryption failures in `get()` now short-circuit after the first callback, preventing double-callback regressions when the crypto secret is wrong.
1415

1516
## [5.1.0] - 2023-10-14
1617

docs/PLANS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
- store.clear() issues collection.drop(), which wipes the TTL index required for autoRemove: 'native' and throws NamespaceNotFound for empty stores (src/lib/MongoStore.ts:530-535). Switch to deleteMany({}) (keeping indexes) and swallow the namespace error so clear() is idempotent.
2222
- [done 2025-11-18] Swapped drop() for deleteMany(), swallowing NamespaceNotFound and adding coverage to ensure TTL index survives clear() (agent: Codex)
2323
- Decryption failures call the callback twice because the rejection handler just invokes callback(err) and then execution continues to the success path (src/lib/MongoStore.ts:314-326). Re-throw inside the catch or guard against multiple invocations to avoid “callback was already called” regressions.
24+
- [done 2025-11-18] Guard decrypt errors so get() calls its callback once; add regression test for wrong secret (agent: Codex)
2425
- Session TTL math ignores cookie.maxAge; it only respects cookie.expires or a global default in both set() and touch(), so rolling sessions expire too early (src/lib/MongoStore.ts:355-368, 435-439). Mirror express-session's logic: prefer maxAge, fall back to expires, then to ttl.
2526
- close() always shuts down the underlying MongoClient, even if the user supplied their own client/promise, which can tear down the rest of the app's DB connections (src/lib/MongoStore.ts:188-210, 541-543). Track whether the store created the client and only close in that case; otherwise just clear timers.
2627
- Interval-based cleanup leaks and relies on deprecated write concern: the timer created in setAutoRemove() is never cleared on shutdown and writes with w:0/j:false, which newer clusters reject (src/lib/MongoStore.ts:217-247). Store the handle, clearInterval it in close(), and use the configured write concern or

src/lib/MongoStore.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,29 @@ test.serial('clear preserves TTL index and is idempotent', async (t) => {
192192
await t.notThrowsAsync(() => storePromise.clear())
193193
})
194194

195+
test.serial('decrypt failure only calls callback once', async (t) => {
196+
;({ store, storePromise } = createStoreHelper({
197+
crypto: {
198+
secret: 'right-secret',
199+
},
200+
}))
201+
const sid = 'decrypt-failure'
202+
await storePromise.set(sid, makeData())
203+
// Tamper with the secret so decryption fails
204+
;(store as any).options.crypto.secret = 'wrong-secret'
205+
206+
await new Promise<void>((resolve) => {
207+
let calls = 0
208+
store.get(sid, (err, session) => {
209+
calls += 1
210+
t.truthy(err)
211+
t.is(session, undefined)
212+
t.is(calls, 1)
213+
resolve()
214+
})
215+
})
216+
})
217+
195218
test.serial('test destory event', async (t) => {
196219
;({ store, storePromise } = createStoreHelper())
197220
const orgSession = makeData()

src/lib/MongoStore.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,7 @@ export default class MongoStore<
300300
const plaintext = (await this.cryptoGet(
301301
this.options.crypto.secret as string,
302302
sessionDoc.session
303-
).catch((err) => {
304-
throw new Error(err)
305-
})) as string
303+
)) as string
306304
sessionDoc.session = JSON.parse(plaintext) as StoredSessionValue
307305
}
308306
}
@@ -324,7 +322,12 @@ export default class MongoStore<
324322
],
325323
})
326324
if (this.crypto && sessionDoc) {
327-
await this.decryptSession(sessionDoc).catch((err) => callback(err))
325+
try {
326+
await this.decryptSession(sessionDoc)
327+
} catch (error) {
328+
callback(error)
329+
return
330+
}
328331
}
329332
let result: T | undefined
330333
if (sessionDoc) {

0 commit comments

Comments
 (0)