feat(session): enable accessing sid via `c.var.session.id` (#1272)

* feat(session): enable accessing sid via `c.var.session.id`

* changeset
pull/1304/head
Yusuke Wada 2025-07-13 17:12:05 +09:00 committed by GitHub
parent b5186da328
commit 68cbb31af4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 106 additions and 0 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/session': minor
---
feat: enable accessing sid via `c.var.session.id`

View File

@ -939,3 +939,85 @@ describe('session events', () => {
expect(onUpdate).toHaveBeenCalledWith({ sub }) expect(onUpdate).toHaveBeenCalledWith({ sub })
}) })
}) })
describe('session.id', () => {
const app = new Hono()
.get('/session-id', useSession<TestData>({ secret }), async (c) => {
await c.var.session.get()
return c.json({ id: c.var.session.id })
})
.put('/session-id/:sub', useSession<TestData>({ secret }), async (c) => {
const sub = c.req.param('sub')
await c.var.session.update({ sub })
return c.json({ id: c.var.session.id })
})
it('throws an error when session is not initialised', async () => {
const app = new Hono().get('/session-id', useSession<TestData>({ secret }), (c) => {
return c.json({ id: c.var.session.id })
})
const res = await app.request('/session-id')
expect(res.status).toBe(500)
})
it('returns session id after get()', async () => {
const res = await app.request('/session-id')
const data = await res.json()
expect(res.status).toBe(200)
expect(data.id).toBeTypeOf('string')
expect(data.id.length).toBeGreaterThan(0)
})
it('returns session id after update()', async () => {
const res = await app.request('/session-id/test-user', { method: 'PUT' })
const data = await res.json()
expect(res.status).toBe(200)
expect(data.id).toBeTypeOf('string')
expect(data.id.length).toBeGreaterThan(0)
})
it('returns same session id for existing session', async () => {
const cookie = await encrypt(session.payload)
const res = await app.request('/session-id', {
headers: { cookie: `sid=${cookie}` },
})
const data = await res.json()
expect(res.status).toBe(200)
expect(data.id).toBe(sid) // 'some-session-id'
})
it('generates different session ids for new sessions', async () => {
const res1 = await app.request('/session-id')
const res2 = await app.request('/session-id')
const data1 = await res1.json()
const data2 = await res2.json()
expect(res1.status).toBe(200)
expect(res2.status).toBe(200)
expect(data1.id).not.toBe(data2.id)
expect(data1.id).toBeTypeOf('string')
expect(data2.id).toBeTypeOf('string')
})
it('preserves session id across requests with same cookie', async () => {
// First request - create session
const firstRes = await app.request('/session-id/user1', { method: 'PUT' })
const sessionCookie = await getSetCookie(firstRes, 'sid', decrypt)
const firstData = await firstRes.json()
// Second request - use same cookie
const secondRes = await app.request('/session-id', {
headers: { cookie: `sid=${sessionCookie?.value}` },
})
const secondData = await secondRes.json()
expect(firstRes.status).toBe(200)
expect(secondRes.status).toBe(200)
expect(firstData.id).toBe(secondData.id)
expect(firstData.id).toBe(sessionCookie?.payload.sid)
})
})

View File

@ -104,6 +104,9 @@ export const useSession = <Data extends SessionData>(
get data() { get data() {
return session.data return session.data
}, },
get id() {
return session.id
},
delete() { delete() {
session.delete() session.delete()
}, },

View File

@ -26,6 +26,11 @@ export interface Session<Data> {
*/ */
readonly data: Data | null readonly data: Data | null
/**
* Current session ID.
*/
readonly id: string | null
/** /**
* Delete the current session, removing the session cookie and data from storage. * Delete the current session, removing the session cookie and data from storage.
*/ */
@ -122,6 +127,17 @@ export function getSession<Data extends SessionData>({
return session.data ?? null return session.data ?? null
}, },
get id() {
if (!session) {
throw new Error('Session not initialised. Call get() or update() first.')
}
if (session.action === 'destroy') {
throw new Error('Session has been destroyed.')
}
return session.payload?.sid ?? null
},
delete() { delete() {
if (session) { if (session) {
onDelete?.(session.data) onDelete?.(session.data)