# Cookie helpers

> The cookie helpers apply one encoding and security policy across the app. Duplicate cookie names are rejected instead of being interpreted differently by separate parts of the stack.

DaloyJS exposes the same low-level cookie helpers that `session()` and `csrf()` use internally. Routing every `Set-Cookie` write through one tiny, dependency-free implementation means there is exactly one place that knows the RFC 6265bis attribute rules, the `__Host-` / `__Secure-` prefix rules, and the secure-by-default posture. Reach for these when you need a custom cookie outside the built-in session and CSRF flows.

## Secure by default

When you omit attributes, the helpers choose the safest interpretation:

- `secure: true`
- `httpOnly: true`
- `sameSite: "Strict"`
- `path: "/"`

## Writing a cookie

`serializeCookie(name, value, attributes?)` returns a single `Set-Cookie` header value. The value is URI-encoded so binary signature bytes and base64 padding round-trip safely. Set it on your response headers.

```ts
import { App, serializeCookie } from "@daloyjs/core";

const app = new App();

app.post(
  "/prefs",
  {
    operationId: "savePrefs",
    responses: { 204: { description: "saved" } },
  },
  async () => ({
    status: 204 as const,
    body: undefined,
    headers: {
      "set-cookie": serializeCookie("__Host-theme", "dark", {
        maxAgeSeconds: 60 * 60 * 24 * 365,
      }),
    },
  }),
);
```

The `__Host-` prefix is the strongest anti-cookie-tossing choice: the browser only accepts it when it is `Secure`, has `Path=/`, and carries no `Domain`. The helper enforces exactly those constraints, so a misconfiguration throws instead of silently shipping a cookie the browser drops.

## Reading a cookie

`readRequestCookie(header, name)` parses a single cookie out of the request `Cookie` header, returning `null` when it is absent. It also includes a **cookie-tossing defense**: if the same name appears more than once, it returns `null` rather than guessing which copy is authentic, forcing a re-authentication instead of letting an attacker-injected shadow cookie win.

**Diagram: Cookie-tossing defense on read**

- **Cookie header** (ingress) - readRequestCookie(header, name)
- **How many copies of name?** (count) - scan the parsed header
- **Return the value** (exactly one) - authentic cookie to handler
- **Return null** (two or more) - ambiguous to force re-auth

When a name appears more than once the helper refuses to guess which copy is authentic and returns null, so an attacker-injected shadow cookie cannot win by ambiguity. Exactly one match returns the value.

```ts
import { readRequestCookie } from "@daloyjs/core";

app.get(
  "/prefs",
  {
    responses: { 200: { description: "ok" } },
  },
  async ({ request }) => {
    const theme = readRequestCookie(request.headers.get("cookie"), "__Host-theme");
    return { status: 200 as const, body: { theme: theme ?? "light" } };
  },
);
```

## Clearing a cookie

`serializeClearCookie(name, attributes?)` emits a `Set-Cookie` value with `Max-Age=0`, preserving the original attributes so intermediaries match the cookie they are meant to delete.

```ts
import { serializeClearCookie } from "@daloyjs/core";

return {
  status: 204 as const,
  body: undefined,
  headers: { "set-cookie": serializeClearCookie("__Host-theme") },
};
```

## Validating attributes up front

`assertCookieAttributes()` throws on the first RFC 6265bis or secure-default violation. `serializeCookie()` and `serializeClearCookie()` call it for you, but you can invoke it directly to validate a configuration at construction time, failing the boot rather than shipping a cookie the browser would silently drop.

```ts
import { assertCookieAttributes } from "@daloyjs/core";

assertCookieAttributes({
  scope: "my-feature",
  name: "__Secure-flags",
  attributes: { secure: true, sameSite: "Lax" },
  isProduction: process.env.NODE_ENV === "production",
});
```

Common refusals: `sameSite: "None"` without `secure: true`, a `__Host-` cookie with a `Domain` or a non-root `path`, and (in production) a `__Secure-` cookie without `secure: true`.

For client-readable tokens such as a CSRF mirror, set `httpOnly: false` explicitly. Everything else stays locked down.

## When to use the built-ins instead

For authenticated sessions reach for `session()`, and for CSRF protection reach for `csrf()`. They already route through these helpers with hardened defaults. Use the raw helpers for everything else: preferences, feature flags, consent banners, and other small bits of per-client state.

---

Source: https://daloyjs.dev/docs/security/cookies