From 68cbb31af43fd5916173f9f6b07c8cedb3dbe706 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Sun, 13 Jul 2025 17:12:05 +0900 Subject: [PATCH] feat(session): enable accessing sid via `c.var.session.id` (#1272) * feat(session): enable accessing sid via `c.var.session.id` * changeset --- .changeset/fast-jokes-agree.md | 5 ++ packages/session/src/index.test.ts | 82 ++++++++++++++++++++++++++++++ packages/session/src/index.ts | 3 ++ packages/session/src/session.ts | 16 ++++++ 4 files changed, 106 insertions(+) create mode 100644 .changeset/fast-jokes-agree.md diff --git a/.changeset/fast-jokes-agree.md b/.changeset/fast-jokes-agree.md new file mode 100644 index 00000000..1ba25de8 --- /dev/null +++ b/.changeset/fast-jokes-agree.md @@ -0,0 +1,5 @@ +--- +'@hono/session': minor +--- + +feat: enable accessing sid via `c.var.session.id` diff --git a/packages/session/src/index.test.ts b/packages/session/src/index.test.ts index 22049b52..e018ed2c 100644 --- a/packages/session/src/index.test.ts +++ b/packages/session/src/index.test.ts @@ -939,3 +939,85 @@ describe('session events', () => { expect(onUpdate).toHaveBeenCalledWith({ sub }) }) }) + +describe('session.id', () => { + const app = new Hono() + .get('/session-id', useSession({ secret }), async (c) => { + await c.var.session.get() + return c.json({ id: c.var.session.id }) + }) + .put('/session-id/:sub', useSession({ 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({ 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) + }) +}) diff --git a/packages/session/src/index.ts b/packages/session/src/index.ts index 017e3dc5..8a5db7fd 100644 --- a/packages/session/src/index.ts +++ b/packages/session/src/index.ts @@ -104,6 +104,9 @@ export const useSession = ( get data() { return session.data }, + get id() { + return session.id + }, delete() { session.delete() }, diff --git a/packages/session/src/session.ts b/packages/session/src/session.ts index 12ca2856..d05d4453 100644 --- a/packages/session/src/session.ts +++ b/packages/session/src/session.ts @@ -26,6 +26,11 @@ export interface Session { */ readonly data: Data | null + /** + * Current session ID. + */ + readonly id: string | null + /** * Delete the current session, removing the session cookie and data from storage. */ @@ -122,6 +127,17 @@ export function getSession({ 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() { if (session) { onDelete?.(session.data)