Cookies
RFC 6265 cookie plumbing — parse, serialize, and sealed (vault- gated) cookies in one primitive. Replaces the ad-hoc Set-Cookie strings that used to live in middleware and route handlers.
Two surfaces:
1. Module-level (stateless): b.cookies.parse / b.cookies.serialize / b.cookies.parseSafe. Useful in test fixtures and code paths that don't have a vault wired.
2. Instance (b.cookies.create): bound defaults for cookie attributes, a wired vault for sealed reads/writes, and request/response helpers (read / write / clear / writeSealed / readSealed).
Defaults mirror modern browser expectations: HttpOnly on, Secure on, SameSite=Lax, Path=/. Operators developing locally over plain http opt out of Secure explicitly so the production posture isn't silently weakened.
Cookie-prefix invariants from RFC 6265bis §4.1.3 are enforced at serialize time: __Secure-* requires Secure; __Host-* requires Secure + Path=/ + no Domain. Operator typos (__Host- cookie without Path=/) throw at the source instead of silently failing on the browser side.
Header-injection defense: cookie names are RFC 6265 tokens; values reject CRLF / NUL / semicolon / comma pre-encoding, then percent- encode on write and percent-decode on read. Domain / Path attributes are CRLF/NUL-scrubbed before they reach Set-Cookie.
Sealed cookies wrap the value in a vault.seal envelope: without the framework's vault key no client can hand-craft a valid value, so curl-with-arbitrary-cookies (or any tool that hasn't been through the framework's crypto flow) can't reach a sealed-cookie- gated endpoint. The vault prefix is stripped on write and re-added on read so the cookie carries only the compact base64 envelope.
b.cookies.parse(cookieHeader) #
Lenient RFC 6265 Cookie-header parser. Returns a plain object { name: value } with last-write-wins semantics (matching every browser). Surrounding double-quotes are stripped per §5.2 and values are percent-decoded; malformed pairs are silently dropped because that's how browsers behave. For the threat-detecting variant that surfaces issues instead of dropping silently, use parseSafe.
var jar = b.cookies.parse("session=abc; theme=dark%20mode");
// → { session: "abc", theme: "dark mode" } // percent-decoded
b.cookies.serialize(name, value, attrs) #
{
maxAge: 3600, // integer seconds; emits `Max-Age=`
expires: new Date(), // Date or parseable date string
domain: "example.com",
path: "/",
httpOnly: true,
secure: true,
sameSite: "Lax", // "Strict" / "Lax" / "None"
partitioned: false, // CHIPS partitioning
priority: "Medium", // "Low" / "Medium" / "High"
}
Build a single Set-Cookie header value from a name, value, and attributes object. Validates the name as an RFC 6265 token, the value against CRLF / NUL / ; / ,, and enforces the __Secure- / __Host- prefix invariants from RFC 6265bis §4.1.3. SameSite is normalized to Strict / Lax / None, and SameSite=None implicitly turns Secure on so browsers don't silently drop the cookie. Throws CookieError on any invariant break — the operator sees the typo at the call site, not as a silently-missing cookie.
var header = b.cookies.serialize("__Host-sid", "abc", {
httpOnly: true,
secure: true,
sameSite: "Lax",
path: "/",
maxAge: 3600,
});
// → "__Host-sid=abc; Max-Age=3600; Path=/; HttpOnly; SameSite=Lax; Secure"
b.cookies.appendSetCookie(res, header) #
Queue one Set-Cookie header on a response without discarding the ones already queued. res.setHeader("Set-Cookie", value) REPLACES the header, so a second cookie written that way silently drops the first — a route that issues a session cookie and then a CSRF cookie ends up sending only the CSRF one. Set-Cookie is the one response header that is legitimately repeated, and this is the framework's single appender for it: it uses res.appendHeader where the runtime offers it, and falls back to reading the current value and array-merging where it doesn't.
The header string must already be serialized — pair it with b.cookies.serialize, which validates the name, value and attributes. b.cookies.create().write() / .clear() compose both for you; reach for this directly when you need to build the header at one point in a flow and queue it at another (validating early, emitting only after the side effect it accompanies has succeeded).
var header = b.cookies.serialize("sid", "", {
httpOnly: true, secure: true, sameSite: "Strict", path: "/", maxAge: 0,
});
b.cookies.appendSetCookie(res, header);
// → res now carries this expiry cookie alongside any already queued
b.cookies.assertAppendable(res) #
Throw unless res can carry an appended Set-Cookie. This is the same check b.cookies.appendSetCookie performs, exposed so a caller can run it BEFORE doing something it cannot undo.
A response has to be readable as well as writable to be appended to: without appendHeader, the merge is done here and needs to see what is already queued. Discovering that late is the problem this exists to prevent — b.session.logout revokes the session row before it queues the expiry cookie, so a throw at queue time would leave the session destroyed, the client still holding its cookie, and the request failing. The response's shape is fixed for the life of the process and owned by the caller, not by the request, so it can and should be asserted up front.
b.cookies.assertAppendable(res); // throws on a write-only response
await doSomethingIrreversible();
b.cookies.appendSetCookie(res, header);
// → the append cannot fail for a reason that was knowable earlier
b.cookies.create(opts) #
{
vault: b.vault, // required for sealed* methods
defaults: {
httpOnly: true,
secure: true,
sameSite: "Lax",
path: "/",
maxAge: 604800, // seconds (7 days)
},
}
Build a cookie helper bound to a default attribute set and an optional vault. Returned object exposes read(req, name), write(res, name, value, attrs), clear(res, name, attrs), and (when a vault is wired) writeSealed / readSealed for vault- gated cookie values. Per-call attrs merge over the bound defaults so callers override piecewise.
var cookies = b.cookies.create({
vault: b.vault,
defaults: { httpOnly: true, secure: true, sameSite: "Lax", path: "/" },
});
cookies.write(res, "theme", "dark", { maxAge: 86400 });
cookies.writeSealed(res, "session", "u-1");
var sid = cookies.readSealed(req, "session");
// → "u-1" or null
b.cookies.parseSafe(cookieHeader, opts) #
{
maxHeaderBytes: 8192, // total Cookie-header byte cap
maxNameBytes: 256, // per-name byte cap
maxValueBytes: 4096, // per-value byte cap
}
Threat-detecting inbound-cookie parser. Returns { jar, issues } where every detected anomaly surfaces as an issue instead of being silently dropped (as the lenient parse does). Detected issues: oversized header / pair, duplicate cookie name (cookie-tossing class), malformed pair, CR / LF / NUL in the raw header (proxy-side injection vector), and non-string input. Issue shape: { kind, severity: "high" | "warn", snippet, name? }.
var result = b.cookies.parseSafe("session=abc; session=evil", {
maxHeaderBytes: 8192,
});
// → { jar: { session: "evil" },
// issues: [{ kind: "duplicate-name", severity: "high",
// name: "session", snippet: "..." }] }