Skip to content

Commit a436f6c

Browse files
committed
perf(router): defer session encryption runtime
1 parent 6937940 commit a436f6c

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

storage/framework/core/router/src/encrypted-session-store.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,23 @@
3636
*/
3737

3838
import type { SessionData, SessionStore } from '@stacksjs/bun-router'
39-
import { decrypt, encrypt } from '@stacksjs/security'
39+
40+
type SecurityModule = typeof import('@stacksjs/security')
41+
42+
let securityModuleLoad: Promise<SecurityModule> | undefined
43+
44+
/**
45+
* Encryption is optional session-store work, not router boot work.
46+
*
47+
* Keeping this import behind the first encrypted read/write prevents the
48+
* normal `@stacksjs/router` barrel from evaluating the security package and
49+
* its transitive cryptography/config graph in apps that do not use encrypted
50+
* sessions. The promise is cached so an active store resolves the module once.
51+
*/
52+
function loadSecurityModule(): Promise<SecurityModule> {
53+
securityModuleLoad ??= import('@stacksjs/security')
54+
return securityModuleLoad
55+
}
4056

4157
/**
4258
* Envelope shape persisted to the underlying store. The `_enc`
@@ -143,6 +159,7 @@ export class EncryptedSessionStore implements SessionStore<SessionData> {
143159
// Don't encrypt the `id` field — middleware reads it back without
144160
// decrypting (for SID-match) and the value is already in the URL/cookie.
145161
const { id, ...rest } = session
162+
const { encrypt } = await loadSecurityModule()
146163
const ciphertext = await encrypt(JSON.stringify(rest), this.opts.appKey)
147164
return {
148165
_enc: true,
@@ -166,6 +183,7 @@ export class EncryptedSessionStore implements SessionStore<SessionData> {
166183

167184
if (candidate._enc === true && typeof candidate.data === 'string') {
168185
try {
186+
const { decrypt } = await loadSecurityModule()
169187
const decrypted = await decrypt(candidate.data, this.opts.appKey)
170188
const parsed = JSON.parse(decrypted) as SessionData
171189
if (candidate.id !== undefined) parsed.id = candidate.id

storage/framework/core/router/tests/import-graph.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,21 @@ describe('router import graph', () => {
5656
&& (entry.path.includes('ts-rate-limiter') || entry.path.endsWith('error-handling/src/http.ts'))) ?? []
5757
expect(eagerActionLimiterDependencies).toEqual([])
5858
})
59+
60+
it('defers session encryption until an encrypted store is used', async () => {
61+
const result = await Bun.build({
62+
entrypoints: [join(import.meta.dir, '../src/encrypted-session-store.ts')],
63+
target: 'bun',
64+
metafile: true,
65+
write: false,
66+
})
67+
68+
expect(result.success).toBe(true)
69+
const entry = Object.entries(result.metafile?.inputs ?? {})
70+
.find(([source]) => source.endsWith('/encrypted-session-store.ts'))
71+
const eagerEncryptionDependencies = entry?.[1].imports
72+
.filter(dependency => dependency.kind !== 'dynamic-import'
73+
&& dependency.original === '@stacksjs/security') ?? []
74+
expect(eagerEncryptionDependencies).toEqual([])
75+
})
5976
})

0 commit comments

Comments
 (0)