Cookie helpers
parseCookies, serializeCookie, signValue and unsignValue: read the Cookie header, write Set-Cookie values with secure defaults, and sign values against tampering.
import { parseCookies, serializeCookie, signValue, unsignValue } from '@gio.js/core';All four are server-side (they use node:crypto). For encrypted sessions, use createSessionStorage, which is built on them.
Reference
parseCookies
parseCookies(header: string | undefined): Record<string, string>parseCookies() parses a Cookie request header into name and value. It is the parser behind ctx.cookies, req.cookies and socket.cookies, so you need it only for a raw header - in a plugin, or a header you received elsewhere.
- The first occurrence of a name wins.
- Names and values are trimmed. Values are not decoded:
%20stays%20. - Parts without
=or with an empty name are skipped, and a cookie named__proto__is never stored. - Read the result by own property (
Object.hasOwn(cookies, name)) when the name comes from elsewhere: a plain object also "has"constructorthrough its prototype.
parseCookies('theme=dark; lang = fr ; theme=light; q=a%20b');
// { theme: 'dark', lang: 'fr', q: 'a%20b' }serializeCookie
serializeCookie(name: string, value: string, options?: CookieOptions): stringserializeCookie() returns one Set-Cookie header value. Values are written as given: encode free text yourself, for example with encodeURIComponent.
| Option | Type | Default | Description |
|---|---|---|---|
path | string | '/' | Must start with /; no control characters or ;. |
domain | string | - | Omitted by default: a host-only cookie. |
maxAge | number | - | Lifetime in whole seconds; 0 deletes the cookie. Without maxAge or expires, it is a browser-session cookie. |
expires | Date | - | An absolute expiry; must be a valid Date. |
httpOnly | boolean | true | Page scripts cannot read the cookie. Set false for a value client code reads. |
secure | boolean | true in production | Off in development - except for __Secure- and __Host- names, sameSite: 'none' and partitioned, which browsers accept only with it. Pass false explicitly to serve a production build over plain http. |
sameSite | 'lax' | 'strict' | 'none' | 'lax' | 'none' requires secure. |
partitioned | boolean | false | A CHIPS partitioned cookie; requires secure. |
Attributes come out in a fixed order: Max-Age, Expires, Domain, Path, HttpOnly, Secure, SameSite, Partitioned.
serializeCookie('theme', 'dark');
// production: 'theme=dark; Path=/; HttpOnly; Secure; SameSite=Lax'
// development: 'theme=dark; Path=/; HttpOnly; SameSite=Lax'It throws a TypeError instead of writing a header a browser would misread or drop:
- a name that is not an RFC 6265 token, or a value with whitespace, quotes, commas, semicolons, backslashes or control characters;
- a
maxAgethat is not a whole number, or an invalidexpires,path,domainorsameSite; - an explicit
secure: falsewithsameSite: 'none',partitionedor a__Secure-name; - a
__Host-cookie that is not secure, has adomain, or apathother than/.
signValue
signValue(value: string, secrets: string | readonly string[]): stringsignValue() returns <value>.<signature>: an HMAC-SHA256 of the value, base64url-encoded, with a key derived from the first secret. Signing proves the value was not changed; it does not hide it. secrets is a string or an array of strings, each at least 32 bytes - shorter ones throw.
unsignValue
unsignValue(signed: string, secrets: string | readonly string[]): string | nullunsignValue() returns the original value when the signature matches any of secrets, and null otherwise (tampered, signed with another key, or not a signed value at all). Every secret is tried and signatures are compared in constant time, so the time taken does not tell an attacker which part was wrong. Keeping an old secret in the list lets values signed before a rotation keep verifying.
Examples
Set and clear cookies in a route handler
Append one Set-Cookie per cookie; each is sent as its own header.
import { serializeCookie, signValue, unsignValue, type GioRequest } from '@gio.js/core';
const secrets = (process.env.PREFS_SECRET ?? '').split(','); // each at least 32 bytes
export function GET(req: GioRequest) {
const signed = req.cookies['prefs'];
const theme = signed === undefined ? null : unsignValue(signed, secrets);
return { theme: theme === null ? 'light' : decodeURIComponent(theme) };
}
export function POST(req: GioRequest) {
const { theme } = req.json<{ theme: string }>();
const headers = new Headers();
// Encode first: a signed value is cookie-safe only when the value is.
headers.append('Set-Cookie', serializeCookie('prefs', signValue(encodeURIComponent(theme), secrets), {
maxAge: 60 * 60 * 24 * 365,
}));
// Readable by client code:
headers.append('Set-Cookie', serializeCookie('theme', encodeURIComponent(theme), { httpOnly: false }));
return new Response(null, { status: 204, headers });
}
export function DELETE() {
return new Response(null, {
status: 204,
headers: { 'Set-Cookie': serializeCookie('prefs', '', { maxAge: 0 }) },
});
}In production, POST with {"theme":"dark mode"} sends:
Set-Cookie: prefs=dark%20mode.<43-character signature>; Max-Age=31536000; Path=/; HttpOnly; Secure; SameSite=Lax
Set-Cookie: theme=dark%20mode; Path=/; Secure; SameSite=LaxCookies from getServerSideProps
import { serializeCookie, type GetServerSidePropsContext } from '@gio.js/core';
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const seen = ctx.cookies['seen'] === '1';
return {
props: { firstVisit: !seen },
headers: { 'set-cookie': [serializeCookie('seen', '1', { maxAge: 60 * 60 * 24 * 30 })] },
};
}A page that sends set-cookie (or reads ctx.cookies) is never stored in the shared page cache.
Good to know
- No encoding is done for you, in either direction: encode with
encodeURIComponentbeforeserializeCookie, decode after reading. - Development vs production is the server's mode:
NODE_ENV=developmentmeans development, anything else (includingtest) production, whereSecureis on. Browsers acceptSecurecookies fromhttp://localhost. - Delete with the same
pathanddomainthe cookie was set with; a cookie set on/adminis not removed by one cleared on/. - Secrets for
signValueare your own; they need not beGIO_SESSION_SECRET. Generate one withnode -e "console.log(require('crypto').randomBytes(32).toString('base64url'))".
Related
- Authentication: Cookies and Signed values
- Route Handlers: Setting cookies
- Fetching Data: Response headers and cookies
- createSessionStorage
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | Introduced serializeCookie (secure defaults, validation), parseCookies, signValue and unsignValue. |