Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,10 +601,12 @@ export const composeHandler = ({
const cookieMeta: {
secrets?: string | string[]
sign: string[] | true
encode?: CookieOptions['encode']
properties: { [x: string]: Object }
} = validator.cookie?.config
? mergeCookie(validator?.cookie?.config, app.config.cookie as any)
: app.config.cookie
const hasCookieEncode = typeof cookieMeta?.encode === 'function'

let _encodeCookie = ''
const encodeCookie = () => {
Expand Down Expand Up @@ -736,6 +738,7 @@ export const composeHandler = ({
get('priority') +
get('sameSite') +
get('secure') +
(hasCookieEncode ? 'encode:cookieEncode,' : '') +
'}'
: 'undefined'

Expand Down Expand Up @@ -2151,6 +2154,7 @@ export const composeHandler = ({
allocateIf(`parseCookie,`, hasCookie) +
allocateIf(`signCookie,`, hasCookie) +
allocateIf(`decodeURIComponent,`, hasQuery) +
allocateIf(`cookieEncode,`, hasCookieEncode) +
`ElysiaCustomStatusResponse,` +
allocateIf(`ELYSIA_TRACE,`, hasTrace) +
allocateIf(`ELYSIA_REQUEST_ID,`, hasTrace) +
Expand Down Expand Up @@ -2205,6 +2209,7 @@ export const composeHandler = ({
ERROR_CODE,
parseCookie: hasCookie ? parseCookie : undefined,
signCookie: hasCookie ? signCookie : undefined,
cookieEncode: hasCookieEncode ? cookieMeta.encode : undefined,
Cookie: hasCookie ? Cookie : undefined,
decodeURIComponent: hasQuery ? decode : undefined,
ElysiaCustomStatusResponse,
Expand Down
16 changes: 16 additions & 0 deletions src/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ export interface CookieOptions {
*/
secure?: boolean | undefined

/**
* Specifies a function that will be used to encode a cookie value before
* serializing it into the `Set-Cookie` header.
*
* @default encodeURIComponent
*/
encode?: ((value: string) => string) | undefined

/**
* Secret key for signing cookie
*
Expand Down Expand Up @@ -297,6 +305,14 @@ export class Cookie<T> implements ElysiaCookie {
this.setCookie.partitioned = partitioned
}

get encode() {
return this.cookie.encode
}

set encode(encode: ElysiaCookie['encode']) {
this.setCookie.encode = encode
}

get secrets() {
return this.cookie.secrets
}
Expand Down
63 changes: 27 additions & 36 deletions src/dynamic-handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,47 +447,38 @@ export const createDynamicHandler = (app: AnyElysia) => {
for (const [key, value] of request.headers.entries())
context.headers[key] = value

const routeCookieConfig = (
(hooks as {
cookie?: {
config?: CookieOptions & {
sign?: true | string | string[]
}
}
}).cookie?.config ??
(validator?.cookie?.config as
| (CookieOptions & {
sign?: true | string | string[]
})
| undefined)
)

const cookieMeta = {
domain:
app.config.cookie?.domain ??
// @ts-expect-error
validator?.cookie?.config.domain,
expires:
app.config.cookie?.expires ??
// @ts-expect-error
validator?.cookie?.config.expires,
domain: app.config.cookie?.domain ?? routeCookieConfig?.domain,
expires: app.config.cookie?.expires ?? routeCookieConfig?.expires,
httpOnly:
app.config.cookie?.httpOnly ??
// @ts-expect-error
validator?.cookie?.config.httpOnly,
maxAge:
app.config.cookie?.maxAge ??
// @ts-expect-error
validator?.cookie?.config.maxAge,
// @ts-expect-error
path: app.config.cookie?.path ?? validator?.cookie?.config.path,
app.config.cookie?.httpOnly ?? routeCookieConfig?.httpOnly,
maxAge: app.config.cookie?.maxAge ?? routeCookieConfig?.maxAge,
path: app.config.cookie?.path ?? routeCookieConfig?.path,
priority:
app.config.cookie?.priority ??
// @ts-expect-error
validator?.cookie?.config.priority,
app.config.cookie?.priority ?? routeCookieConfig?.priority,
partitioned:
app.config.cookie?.partitioned ??
// @ts-expect-error
validator?.cookie?.config.partitioned,
sameSite:
app.config.cookie?.sameSite ??
// @ts-expect-error
validator?.cookie?.config.sameSite,
secure:
app.config.cookie?.secure ??
// @ts-expect-error
validator?.cookie?.config.secure,
secrets:
app.config.cookie?.secrets ??
// @ts-expect-error
validator?.cookie?.config.secrets,
// @ts-expect-error
sign: app.config.cookie?.sign ?? validator?.cookie?.config.sign
routeCookieConfig?.partitioned,
sameSite: app.config.cookie?.sameSite ?? routeCookieConfig?.sameSite,
secure: app.config.cookie?.secure ?? routeCookieConfig?.secure,
encode: routeCookieConfig?.encode ?? app.config.cookie?.encode,
secrets: app.config.cookie?.secrets ?? routeCookieConfig?.secrets,
sign: app.config.cookie?.sign ?? routeCookieConfig?.sign
} as CookieOptions & {
sign?: true | string | string[]
}
Expand Down
2 changes: 2 additions & 0 deletions src/type-system/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ export const ElysiaType = {
priority,
sameSite,
secure,
encode,
secrets,
sign,
...options
Expand All @@ -537,6 +538,7 @@ export const ElysiaType = {
priority,
sameSite,
secure,
encode,
secrets,
sign
}
Expand Down
127 changes: 127 additions & 0 deletions test/cookie/response.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,133 @@ describe('Cookie Response', () => {
])
})

it('uses cookie encode from constructor', async () => {
const app = new Elysia({
cookie: {
encode: (value) => value.replaceAll(' ', '+')
}
}).get('/', ({ cookie: { name } }) => {
name.value = 'seminar Himari'
})

const response = await app.handle(req('/'))

expect(response.headers.getAll('Set-Cookie')).toEqual([
'name=seminar+Himari; Path=/'
])
})

it('uses cookie encode from constructor in dynamic mode', async () => {
const app = new Elysia({
aot: false,
cookie: {
encode: (value) => value.replaceAll(' ', '+')
}
}).get('/', ({ cookie: { name } }) => {
name.value = 'seminar Himari'
})

const response = await app.handle(req('/'))

expect(response.headers.getAll('Set-Cookie')).toEqual([
'name=seminar+Himari; Path=/'
])
})

it('uses cookie encode from route cookie config', async () => {
const app = new Elysia().get(
'/',
({ cookie: { name } }) => {
name.value = 'seminar Himari'
},
{
cookie: t.Cookie(
{
name: t.Optional(t.String())
},
{
encode: (value) => value.replaceAll(' ', '+')
}
)
}
)

const response = await app.handle(req('/'))

expect(response.headers.getAll('Set-Cookie')).toEqual([
'name=seminar+Himari; Path=/'
])
})

it('uses cookie encode from route cookie config in dynamic mode', async () => {
const app = new Elysia({ aot: false }).get(
'/',
({ cookie: { name } }) => {
name.value = 'seminar Himari'
},
{
cookie: t.Cookie(
{
name: t.Optional(t.String())
},
{
encode: (value) => value.replaceAll(' ', '+')
}
)
}
)

const response = await app.handle(req('/'))

expect(response.headers.getAll('Set-Cookie')).toEqual([
'name=seminar+Himari; Path=/'
])
})

it('uses route cookie encode over constructor in dynamic mode', async () => {
const app = new Elysia({
aot: false,
cookie: {
encode: (value) => `app-${value.replaceAll(' ', '-')}`
}
}).get(
'/',
({ cookie: { name } }) => {
name.value = 'seminar Himari'
},
{
cookie: t.Cookie(
{
name: t.Optional(t.String())
},
{
encode: (value) =>
`route-${value.replaceAll(' ', '-')}`
}
)
}
)

const response = await app.handle(req('/'))

expect(response.headers.getAll('Set-Cookie')).toEqual([
'name=route-seminar-Himari; Path=/'
])
})

it('uses cookie encode set on a cookie instance', async () => {
const app = new Elysia().get('/', ({ cookie: { name } }) => {
name.encode = (value) => value.replaceAll(' ', '+')
name.value = 'seminar Himari'
})

const response = await app.handle(req('/'))

expect(response.headers.getAll('Set-Cookie')).toEqual([
'name=seminar+Himari; Path=/'
])
})

it('retain cookie value when using set if not provided', async () => {
const response = await app.handle(req('/set'))

Expand Down