GioJSdocs
On this page

Cookie helpers

parseCookies, serializeCookie, signValue and unsignValue: read the Cookie header, write Set-Cookie values with secure defaults, and sign values against tampering.

ts
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

ts
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: %20 stays %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" constructor through its prototype.
ts
parseCookies('theme=dark; lang = fr ; theme=light; q=a%20b');
// { theme: 'dark', lang: 'fr', q: 'a%20b' }

serializeCookie

ts
serializeCookie(name: string, value: string, options?: CookieOptions): string

serializeCookie() returns one Set-Cookie header value. Values are written as given: encode free text yourself, for example with encodeURIComponent.

OptionTypeDefaultDescription
pathstring'/'Must start with /; no control characters or ;.
domainstring-Omitted by default: a host-only cookie.
maxAgenumber-Lifetime in whole seconds; 0 deletes the cookie. Without maxAge or expires, it is a browser-session cookie.
expiresDate-An absolute expiry; must be a valid Date.
httpOnlybooleantruePage scripts cannot read the cookie. Set false for a value client code reads.
securebooleantrue in productionOff 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.
partitionedbooleanfalseA CHIPS partitioned cookie; requires secure.

Attributes come out in a fixed order: Max-Age, Expires, Domain, Path, HttpOnly, Secure, SameSite, Partitioned.

ts
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 maxAge that is not a whole number, or an invalid expires, path, domain or sameSite;
  • an explicit secure: false with sameSite: 'none', partitioned or a __Secure- name;
  • a __Host- cookie that is not secure, has a domain, or a path other than /.

signValue

ts
signValue(value: string, secrets: string | readonly string[]): string

signValue() 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

ts
unsignValue(signed: string, secrets: string | readonly string[]): string | null

unsignValue() 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.

app/api/prefs/route.ts
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:

text
Set-Cookie: prefs=dark%20mode.<43-character signature>; Max-Age=31536000; Path=/; HttpOnly; Secure; SameSite=Lax
Set-Cookie: theme=dark%20mode; Path=/; Secure; SameSite=Lax

Cookies from getServerSideProps

ts
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 encodeURIComponent before serializeCookie, decode after reading.
  • Development vs production is the server's mode: NODE_ENV=development means development, anything else (including test) production, where Secure is on. Browsers accept Secure cookies from http://localhost.
  • Delete with the same path and domain the cookie was set with; a cookie set on /admin is not removed by one cleared on /.
  • Secrets for signValue are your own; they need not be GIO_SESSION_SECRET. Generate one with node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))".

Version history

VersionChanges
v0.1.0-beta.8Introduced serializeCookie (secure defaults, validation), parseCookies, signValue and unsignValue.