Skip to content

Commit 69f50b1

Browse files
committed
feat: passthrough sign, unsign, encode options
1 parent 31a7d23 commit 69f50b1

File tree

4 files changed

+52
-26
lines changed

4 files changed

+52
-26
lines changed

src/session.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ export default function session<
1616
const name = options.name || "sid"
1717
const store = options.store || new MemoryStore()
1818
const genId = options.genid || nanoid
19-
const encode = options.encode
2019
const touchAfter = options.touchAfter ?? -1
21-
const cookieOpts = options.cookie || {}
20+
const { unsign, ...cookieOpts } = options.cookie ?? {}
2221

2322
function decorateSession(req: Req, res: Res, session: TypedSession, id: string, _now: number) {
2423
Object.defineProperties(session, {
@@ -53,8 +52,13 @@ export default function session<
5352

5453
const _now = Date.now()
5554

56-
let sessionId = req.cookies[name]?.value
55+
const sessionCookie = req.cookies[name]
56+
if (unsign != null && sessionCookie != null && !sessionCookie.signed) sessionCookie.unsign(unsign)
5757

58+
let sessionId: string | null = null
59+
try {
60+
sessionId = sessionCookie?.value ?? null
61+
} catch (err) {}
5862
const _session = sessionId ? await store.get(sessionId) : null
5963

6064
let session: TypedSession
@@ -101,7 +105,7 @@ export default function session<
101105

102106
res.registerLateHeaderAction(lateHeaderAction, (res: Res) => {
103107
if (!(session[isNew] && Object.keys(session).length > 1) && !session[isTouched] && !session[isDestroyed]) return
104-
appendSessionCookieHeader(res, name, session, encode)
108+
appendSessionCookieHeader(res, name, session, cookieOpts)
105109
})
106110

107111
return session

src/types.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,33 @@ export interface SessionStore {
3838
}
3939

4040
export interface Options {
41-
name?: string
42-
store?: SessionStore
43-
genid?: () => string
44-
encode?: (rawSid: string) => string
45-
decode?: (encryptedSid: string) => string | null
46-
touchAfter?: number
47-
cookie?: Partial<Pick<Cookie, "maxAge" | "httpOnly" | "path" | "domain" | "secure" | "sameSite">>
48-
autoCommit?: boolean
41+
name?: string | undefined
42+
store?: SessionStore | undefined
43+
genid?: (() => string) | undefined
44+
touchAfter?: number | undefined
45+
cookie?:
46+
| (Partial<Exclude<Cookie, "expires">> & {
47+
/**
48+
* `otterhttp` cookie `sign` function, will be passed to `res.cookie`.
49+
* @default undefined
50+
*/
51+
sign?: ((value: string) => string) | undefined
52+
53+
/**
54+
* `otterhttp` cookie 'unsign' function, will be used to unsign session cookies.
55+
*
56+
* You must ensure that encoded session cookies are not matched by your `otterhttp` `App`'s configured
57+
* `signedCookieMatcher`. Otherwise, `otterhttp` will attempt to decode session cookies using the `App`'s configured
58+
* `cookieUnsigner` instead, and unsigning with this function will not be attempted.
59+
* @default undefined
60+
*/
61+
unsign?: ((signedValue: string) => string) | undefined
62+
63+
/**
64+
* `otterhttp` cookie 'encode' function, will be passed to `res.cookie`.
65+
* @default undefined
66+
*/
67+
encode?: ((value: string) => string) | undefined
68+
})
69+
| undefined
4970
}

src/utils.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@ import type { Request, Response } from "@otterhttp/app"
22

33
import type { Options, Session } from "./types"
44

5-
export function appendSessionCookieHeader<
6-
Req extends Request = Request,
7-
Res extends Response<Req> = Response<Req>,
8-
>(
5+
export function appendSessionCookieHeader<Req extends Request = Request, Res extends Response<Req> = Response<Req>>(
96
res: Res,
107
name: string,
118
{ cookie, id }: Pick<Session, "cookie" | "id">,
12-
encodeFn?: Options["encode"],
9+
{ encode, sign }: Pick<Exclude<Options["cookie"], undefined>, "encode" | "sign">,
1310
) {
1411
if (res.headersSent) return
1512
res.cookie(name, id, {
@@ -19,6 +16,7 @@ export function appendSessionCookieHeader<
1916
domain: cookie.domain,
2017
sameSite: cookie.sameSite,
2118
secure: cookie.secure,
22-
encode: encodeFn,
19+
encode,
20+
sign,
2321
})
2422
}

test/session.test.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ describe("session()", () => {
6363
const cookie = {
6464
httpOnly: false,
6565
}
66-
const sess = await session({ cookie })({} as Request, { registerLateHeaderAction: vi.fn() } as unknown as Response)
66+
const sess = await session({ cookie })(
67+
{ cookies: {} } as Request,
68+
{ registerLateHeaderAction: vi.fn() } as unknown as Response,
69+
)
6770

6871
expect(sess.cookie.httpOnly).toBeFalsy()
6972
})
@@ -282,16 +285,16 @@ describe("session()", () => {
282285
.end()
283286
})
284287
})
285-
test("allow encode and decode sid", async () => {
286-
const decode = (key: string) => {
288+
test("allow sign and unsign sid", async () => {
289+
const unsign = (key: string) => {
287290
if (key.startsWith("sig.")) return key.substring(4)
288-
return null
291+
throw new Error()
289292
}
290-
const encode = (key: string) => {
293+
const sign = (key: string) => {
291294
return `sig.${key}`
292295
}
293296
const store = new MemoryStore()
294-
const sessionFn = session({ store, encode, decode })
297+
const sessionFn = session({ store, cookie: { sign, unsign } })
295298
let sid: string | undefined
296299
const app = new App<Request, Response>()
297300
app.use("/", async (req: Request, res: Response, next) => {
@@ -317,10 +320,10 @@ describe("session()", () => {
317320

318321
const res1 = await fetch("/first")
319322
expect(sid).toBeDefined()
320-
expect(res1.headers.getSetCookie()).toContain(`sid=${encode(sid as string)}; Path=/; HttpOnly`)
323+
expect(res1.headers.getSetCookie()).toContain(`sid=${sign(sid as string)}; Path=/; HttpOnly`)
321324
expect(store.store.has(sid as string)).toBe(true)
322325

323-
const res2 = await fetch("/second", { headers: { cookie: `sid=${encode(sid as string)}` } })
326+
const res2 = await fetch("/second", { headers: { cookie: `sid=${sign(sid as string)}` } })
324327
await expect(res2.text()).resolves.toEqual("bar")
325328

326329
const res3 = await fetch("/second", { headers: { cookie: `sid=${sid}` } })

0 commit comments

Comments
 (0)