OCaml HTTP cookie handling library with support for Eio-based storage jars
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** Cookie management library for OCaml
7
8 HTTP cookies are a mechanism defined in
9 {{:https://datatracker.ietf.org/doc/html/rfc6265} RFC 6265} that allows
10 "server side connections to store and retrieve information on the client
11 side." Originally designed to enable persistent client-side state for web
12 applications, cookies are essential for storing user preferences, session
13 data, shopping cart contents, and authentication tokens.
14
15 This library provides a complete cookie implementation following RFC 6265
16 while integrating Eio for efficient asynchronous operations.
17
18 {2 Cookie Format and Structure}
19
20 Cookies are set via the Set-Cookie HTTP response header
21 ({{:https://datatracker.ietf.org/doc/html/rfc6265#section-4.1} Section 4.1})
22 with the basic format: [NAME=VALUE] with optional attributes including:
23 - [expires]: Cookie lifetime specification
24 ({{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1} Section 5.2.1})
25 - [max-age]: Cookie lifetime in seconds
26 ({{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2} Section 5.2.2})
27 - [domain]: Valid domains using tail matching
28 ({{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3} Section 5.2.3})
29 - [path]: URL subset for cookie validity
30 ({{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.4} Section 5.2.4})
31 - [secure]: Transmission over secure channels only
32 ({{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.5} Section 5.2.5})
33 - [httponly]: Not accessible to JavaScript
34 ({{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.6} Section 5.2.6})
35 - [samesite]: Cross-site request behavior (RFC 6265bis)
36 - [partitioned]: CHIPS partitioned storage
37
38 {2 Domain and Path Matching}
39
40 The library implements standard domain and path matching rules from
41 {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3} Section 5.1.3}
42 and {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4} Section 5.1.4}:
43 - Domain matching uses suffix matching for hostnames (e.g., "example.com"
44 matches "sub.example.com")
45 - IP addresses require exact match only
46 - Path matching requires exact match or prefix with "/" separator
47
48 @see <https://datatracker.ietf.org/doc/html/rfc6265> RFC 6265 - HTTP State Management Mechanism
49
50 {2 Standards and References}
51
52 This library implements and references the following IETF specifications:
53
54 {ul
55 {- {{:https://datatracker.ietf.org/doc/html/rfc6265}RFC 6265} -
56 HTTP State Management Mechanism (April 2011) - Primary specification}
57 {- {{:https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis}RFC 6265bis} -
58 Cookies: HTTP State Management Mechanism (Draft) - SameSite attribute and modern updates}
59 {- {{:https://datatracker.ietf.org/doc/html/rfc1034#section-3.5}RFC 1034 Section 3.5} -
60 Domain Names - Preferred Name Syntax for domain validation}
61 {- {{:https://datatracker.ietf.org/doc/html/rfc2616#section-2.2}RFC 2616 Section 2.2} -
62 HTTP/1.1 - Token syntax definition}
63 {- {{:https://datatracker.ietf.org/doc/html/rfc1123#section-5.2.14}RFC 1123 Section 5.2.14} -
64 Internet Host Requirements - Date format (rfc1123-date)}}
65
66 Additional standards:
67 {ul
68 {- {{:https://publicsuffix.org/}Mozilla Public Suffix List} - Registry
69 of public suffixes for cookie domain validation per RFC 6265 Section 5.3 Step 5}} *)
70
71(** {1 Types} *)
72
73module SameSite : sig
74 type t = [ `Strict | `Lax | `None ]
75 (** Cookie same-site policy for controlling cross-site request behavior.
76
77 Defined in RFC 6265bis draft.
78
79 - [`Strict]: Cookie only sent for same-site requests, providing maximum
80 protection
81 - [`Lax]: Cookie sent for same-site requests and top-level navigation
82 (default for modern browsers)
83 - [`None]: Cookie sent for all cross-site requests (requires [secure]
84 flag per RFC 6265bis)
85
86 @see <https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7> RFC 6265bis Section 5.4.7 - The SameSite Attribute *)
87
88 val equal : t -> t -> bool
89 (** Equality function for same-site values. *)
90
91 val pp : Format.formatter -> t -> unit
92 (** Pretty printer for same-site values. *)
93end
94
95module Expiration : sig
96 type t = [ `Session | `DateTime of Ptime.t ]
97 (** Cookie expiration strategy.
98
99 Per {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.3} RFC 6265 Section 5.3}:
100 - [`Session]: Session cookie that expires when user agent session ends
101 (persistent-flag = false)
102 - [`DateTime time]: Persistent cookie that expires at specific time
103 (persistent-flag = true)
104
105 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model *)
106
107 val equal : t -> t -> bool
108 (** Equality function for expiration values. *)
109
110 val pp : Format.formatter -> t -> unit
111 (** Pretty printer for expiration values. *)
112end
113
114type t
115(** HTTP Cookie representation with all standard attributes.
116
117 A cookie represents a name-value pair with associated metadata that controls
118 its scope, security, and lifetime. Per
119 {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.3} RFC 6265 Section 5.3},
120 cookies with the same [name], [domain], and [path] will overwrite each other
121 when stored.
122
123 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model *)
124
125(** {1 Cookie Accessors} *)
126
127val domain : t -> string
128(** Get the domain of a cookie.
129
130 The domain is normalized per
131 {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3} RFC 6265 Section 5.2.3}
132 (leading dots removed). *)
133
134val path : t -> string
135(** Get the path of a cookie.
136
137 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.4> RFC 6265 Section 5.2.4 - The Path Attribute *)
138
139val name : t -> string
140(** Get the name of a cookie. *)
141
142val value : t -> string
143(** Get the value of a cookie. *)
144
145val value_trimmed : t -> string
146(** Get cookie value with surrounding double-quotes removed if they form a
147 matching pair.
148
149 Only removes quotes when both opening and closing quotes are present. The
150 raw value is always preserved in {!value}. This is useful for handling
151 quoted cookie values.
152
153 Examples:
154 - ["value"] → ["value"]
155 - ["\"value\""] → ["value"]
156 - ["\"value"] → ["\"value"] (no matching pair)
157 - ["\"val\"\""] → ["val\""] (removes outer pair only) *)
158
159val secure : t -> bool
160(** Check if cookie has the Secure flag.
161
162 Per {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.5} RFC 6265 Section 5.2.5},
163 Secure cookies are only sent over HTTPS connections.
164
165 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.5> RFC 6265 Section 5.2.5 - The Secure Attribute *)
166
167val http_only : t -> bool
168(** Check if cookie has the HttpOnly flag.
169
170 Per {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.6} RFC 6265 Section 5.2.6},
171 HttpOnly cookies are not accessible to client-side scripts.
172
173 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.6> RFC 6265 Section 5.2.6 - The HttpOnly Attribute *)
174
175val partitioned : t -> bool
176(** Check if cookie has the Partitioned attribute.
177
178 Partitioned cookies are part of CHIPS (Cookies Having Independent
179 Partitioned State) and are stored separately per top-level site, enabling
180 privacy-preserving third-party cookie functionality. Partitioned cookies
181 must always be Secure.
182
183 @see <https://developer.chrome.com/docs/privacy-sandbox/chips/> CHIPS - Cookies Having Independent Partitioned State *)
184
185val host_only : t -> bool
186(** Check if cookie has the host-only flag set.
187
188 Per {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.3} RFC 6265 Section 5.3 Step 6}:
189 - If the Set-Cookie header included a Domain attribute, host-only-flag is
190 false and the cookie matches the domain and all subdomains.
191 - If no Domain attribute was present, host-only-flag is true and the cookie
192 only matches the exact request host.
193
194 Example:
195 - Cookie set on "example.com" with Domain=example.com: host_only=false,
196 matches example.com and sub.example.com
197 - Cookie set on "example.com" without Domain attribute: host_only=true,
198 matches only example.com, not sub.example.com
199
200 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model *)
201
202val expires : t -> Expiration.t option
203(** Get the expiration attribute if set.
204
205 Per {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1} RFC 6265 Section 5.2.1}:
206 - [None]: No expiration specified (session cookie)
207 - [Some `Session]: Session cookie (expires when user agent session ends)
208 - [Some (`DateTime t)]: Expires at specific time [t]
209
210 Both [max_age] and [expires] can be present simultaneously. This library
211 stores both independently.
212
213 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1> RFC 6265 Section 5.2.1 - The Expires Attribute *)
214
215val max_age : t -> Ptime.Span.t option
216(** Get the max-age attribute if set.
217
218 Per {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2} RFC 6265 Section 5.2.2},
219 Max-Age specifies the cookie lifetime in seconds. Both [max_age] and
220 [expires] can be present simultaneously. When both are present in a
221 Set-Cookie header, browsers prioritize [max_age] per
222 {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.3} Section 5.3 Step 3}.
223
224 This library stores both independently and serializes both when present.
225
226 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2> RFC 6265 Section 5.2.2 - The Max-Age Attribute *)
227
228val same_site : t -> SameSite.t option
229(** Get the same-site policy of a cookie.
230
231 @see <https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7> RFC 6265bis Section 5.4.7 - The SameSite Attribute *)
232
233val creation_time : t -> Ptime.t
234(** Get the creation time of a cookie.
235
236 Per {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.3} RFC 6265 Section 5.3},
237 this is set when the cookie is first received. *)
238
239val last_access : t -> Ptime.t
240(** Get the last access time of a cookie.
241
242 Per {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.3} RFC 6265 Section 5.3},
243 this is updated each time the cookie is retrieved for a request. *)
244
245val make :
246 domain:string ->
247 path:string ->
248 name:string ->
249 value:string ->
250 ?secure:bool ->
251 ?http_only:bool ->
252 ?expires:Expiration.t ->
253 ?max_age:Ptime.Span.t ->
254 ?same_site:SameSite.t ->
255 ?partitioned:bool ->
256 ?host_only:bool ->
257 creation_time:Ptime.t ->
258 last_access:Ptime.t ->
259 unit ->
260 t
261(** Create a new cookie with the given attributes.
262
263 @param domain The cookie domain (will be normalized)
264 @param path The cookie path
265 @param name The cookie name
266 @param value The cookie value
267 @param secure If true, cookie only sent over HTTPS (default: false)
268 @param http_only If true, cookie not accessible to scripts (default: false)
269 @param expires Expiration time
270 @param max_age Lifetime in seconds
271 @param same_site Cross-site request policy
272 @param partitioned CHIPS partitioned storage (default: false)
273 @param host_only If true, exact domain match only (default: false). Per
274 {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.3} RFC 6265 Section 5.3},
275 this should be true when no Domain attribute was present in the
276 Set-Cookie header.
277 @param creation_time When the cookie was created
278 @param last_access Last time the cookie was accessed
279
280 Note: If [partitioned] is [true], the cookie must also be [secure]. Invalid
281 combinations will result in validation errors.
282
283 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model *)
284
285(** {1 RFC 6265 Validation}
286
287 Validation functions for cookie names, values, and attributes per
288 {{:https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1} RFC 6265 Section 4.1.1}.
289
290 These functions implement the syntactic requirements from RFC 6265 to ensure
291 cookies conform to the specification before being sent in HTTP headers.
292 All validation failures return detailed error messages citing the specific
293 RFC requirement that was violated.
294
295 {2 Validation Philosophy}
296
297 Per RFC 6265 Section 4, there is an important distinction between:
298 - {b Server requirements} (Section 4.1): Strict syntax for generating Set-Cookie headers
299 - {b User agent requirements} (Section 5): Lenient parsing for receiving Set-Cookie headers
300
301 These validation functions enforce the {b server requirements}, ensuring that
302 cookies generated by this library conform to RFC 6265 syntax. When parsing
303 cookies from HTTP headers, the library may be more lenient to maximize
304 interoperability with non-compliant servers.
305
306 {2 Character Set Requirements}
307
308 RFC 6265 restricts cookies to US-ASCII characters with specific exclusions:
309 - Cookie names: RFC 2616 tokens (no CTLs, no separators)
310 - Cookie values: cookie-octet characters (0x21, 0x23-0x2B, 0x2D-0x3A, 0x3C-0x5B, 0x5D-0x7E)
311 - Domain values: RFC 1034 domain name syntax or IP addresses
312 - Path values: Any character except CTLs and semicolon
313
314 These functions return [Ok value] on success or [Error msg] with a detailed
315 explanation of why validation failed.
316
317 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1> RFC 6265 Section 4.1.1 - Syntax *)
318
319module Validate : sig
320 val cookie_name : string -> (string, string) result
321 (** Validate a cookie name per RFC 6265.
322
323 Cookie names must be valid RFC 2616 tokens: one or more characters
324 excluding control characters and separators.
325
326 Per {{:https://datatracker.ietf.org/doc/html/rfc2616#section-2.2}RFC 2616 Section 2.2},
327 a token is defined as: one or more characters excluding control characters
328 and the following 19 separator characters: parentheses, angle brackets, at-sign,
329 comma, semicolon, colon, backslash, double-quote, forward slash, square brackets,
330 question mark, equals, curly braces, space, and horizontal tab.
331
332 This means tokens consist of visible ASCII characters (33-126) excluding
333 control characters (0-31, 127) and the separator characters listed above.
334
335 @param name The cookie name to validate
336 @return [Ok name] if valid, [Error message] with explanation if invalid
337
338 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1> RFC 6265 Section 4.1.1
339 @see <https://datatracker.ietf.org/doc/html/rfc2616#section-2.2> RFC 2616 Section 2.2 - Basic Rules *)
340
341 val cookie_value : string -> (string, string) result
342 (** Validate a cookie value per RFC 6265.
343
344 Cookie values must contain only cookie-octets, optionally wrapped in
345 double quotes. Invalid characters include: control characters, space,
346 double quote (except as wrapper), comma, semicolon, and backslash.
347
348 Per {{:https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1}RFC 6265 Section 4.1.1},
349 cookie-value may be:
350 - Zero or more cookie-octet characters, or
351 - Double-quoted string containing cookie-octet characters
352
353 Where cookie-octet excludes: CTLs (0x00-0x1F, 0x7F), space (0x20),
354 double-quote (0x22), comma (0x2C), semicolon (0x3B), and backslash (0x5C).
355
356 Valid cookie-octet characters: 0x21, 0x23-0x2B, 0x2D-0x3A, 0x3C-0x5B, 0x5D-0x7E
357
358 @param value The cookie value to validate
359 @return [Ok value] if valid, [Error message] with explanation if invalid
360
361 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1> RFC 6265 Section 4.1.1 *)
362
363 val domain_value : string -> (string, string) result
364 (** Validate a domain attribute value.
365
366 Domain values must be either:
367 - A valid domain name per RFC 1034 Section 3.5
368 - A valid IPv4 address
369 - A valid IPv6 address
370
371 Per {{:https://datatracker.ietf.org/doc/html/rfc1034#section-3.5}RFC 1034 Section 3.5},
372 preferred domain name syntax requires:
373 - Labels separated by dots
374 - Labels must start with a letter
375 - Labels must end with a letter or digit
376 - Labels may contain letters, digits, and hyphens
377 - Labels are case-insensitive
378 - Total length limited to 255 octets
379
380 Leading dots are stripped per RFC 6265 Section 5.2.3 before validation.
381
382 @param domain The domain value to validate (leading dot is stripped first)
383 @return [Ok domain] if valid, [Error message] with explanation if invalid
384
385 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.2.3> RFC 6265 Section 4.1.2.3
386 @see <https://datatracker.ietf.org/doc/html/rfc1034#section-3.5> RFC 1034 Section 3.5 *)
387
388 val path_value : string -> (string, string) result
389 (** Validate a path attribute value.
390
391 Per RFC 6265 Section 4.1.1, path-value may contain any CHAR except
392 control characters and semicolon.
393
394 @param path The path value to validate
395 @return [Ok path] if valid, [Error message] with explanation if invalid
396
397 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1> RFC 6265 Section 4.1.1 *)
398
399 val max_age : int -> (int, string) result
400 (** Validate a Max-Age attribute value.
401
402 Per RFC 6265 Section 4.1.1, max-age-av uses non-zero-digit *DIGIT.
403 However, per Section 5.2.2, user agents should treat values <= 0 as
404 "delete immediately". This function returns [Ok] for any integer since
405 the parsing code handles negative values by converting to 0.
406
407 @param seconds The Max-Age value in seconds
408 @return [Ok seconds] always (negative values are handled in parsing)
409
410 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1> RFC 6265 Section 4.1.1
411 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2> RFC 6265 Section 5.2.2 *)
412end
413
414(** {1 Cookie Creation and Parsing} *)
415
416val of_set_cookie_header :
417 now:(unit -> Ptime.t) ->
418 domain:string ->
419 path:string ->
420 string ->
421 (t, string) result
422(** Parse Set-Cookie response header value into a cookie.
423
424 Parses a Set-Cookie header following
425 {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.2} RFC 6265 Section 5.2}:
426 - Basic format: [NAME=VALUE; attribute1; attribute2=value2]
427 - Supports all standard attributes: [expires], [max-age], [domain], [path],
428 [secure], [httponly], [samesite], [partitioned]
429 - Returns [Error msg] if parsing fails or cookie validation fails, with
430 a detailed explanation of what was invalid
431 - The [domain] and [path] parameters provide the request context for default
432 values
433 - The [now] parameter is used for calculating expiry times from [max-age]
434 attributes and setting creation/access times
435
436 Validation rules applied:
437 - Cookie name must be a valid RFC 2616 token (no CTLs or separators)
438 - Cookie value must contain only valid cookie-octets
439 - Domain must be a valid domain name (RFC 1034) or IP address
440 - Path must not contain control characters or semicolons
441 - Max-Age must be non-negative
442 - [SameSite=None] requires the [Secure] flag to be set (RFC 6265bis)
443 - [Partitioned] requires the [Secure] flag to be set (CHIPS)
444 - Domain must not be a public suffix per
445 {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.3} RFC 6265 Section 5.3 Step 5}
446 (unless the request host exactly matches the domain). This uses the
447 {{:https://publicsuffix.org/list/} Mozilla Public Suffix List} to prevent
448 domain-wide cookie attacks.
449
450 {3 Public Suffix Validation}
451
452 Cookies with Domain attributes that are public suffixes (e.g., [.com], [.co.uk],
453 [.github.io]) are rejected to prevent a malicious site from setting cookies
454 that would affect all sites under that TLD.
455
456 Examples:
457 - Request from [www.example.com], Domain=[.com] → rejected (public suffix)
458 - Request from [www.example.com], Domain=[.example.com] → allowed
459 - Request from [blogspot.com], Domain=[.blogspot.com] → allowed (request matches)
460
461 Example:
462 {[of_set_cookie_header ~now:(fun () -> Ptime_clock.now ())
463 ~domain:"example.com" ~path:"/" "session=abc123; Secure; HttpOnly"]}
464
465 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2> RFC 6265 Section 5.2 - The Set-Cookie Header
466 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model (public suffix check)
467 @see <https://publicsuffix.org/list/> Public Suffix List *)
468
469val of_cookie_header :
470 now:(unit -> Ptime.t) ->
471 domain:string ->
472 path:string ->
473 string ->
474 (t list, string) result
475(** Parse Cookie request header containing semicolon-separated name=value pairs.
476
477 Parses a Cookie header following
478 {{:https://datatracker.ietf.org/doc/html/rfc6265#section-4.2} RFC 6265 Section 4.2}.
479 Cookie headers contain only name=value pairs without attributes:
480 ["name1=value1; name2=value2; name3=value3"]
481
482 Validates each cookie name and value per RFC 6265 and detects duplicate
483 cookie names (which is forbidden per Section 4.2.1).
484
485 Creates cookies with:
486 - Provided [domain] and [path] from request context
487 - All security flags set to [false] (defaults)
488 - All optional attributes set to [None]
489 - [host_only = true] (since we cannot determine from the header alone
490 whether cookies originally had a Domain attribute)
491 - [creation_time] and [last_access] set to current time from [now]
492
493 Returns [Ok cookies] if all cookies parse successfully with no duplicates,
494 or [Error msg] if any validation fails.
495
496 Example:
497 {[of_cookie_header ~now:(fun () -> Ptime_clock.now ()) ~domain:"example.com"
498 ~path:"/" "session=abc; theme=dark"]}
499
500 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.2> RFC 6265 Section 4.2 - The Cookie Header *)
501
502val make_cookie_header : t list -> string
503(** Create Cookie header value from cookies.
504
505 Formats a list of cookies into a Cookie header value suitable for HTTP
506 requests per {{:https://datatracker.ietf.org/doc/html/rfc6265#section-4.2} RFC 6265 Section 4.2}.
507 - Format: [name1=value1; name2=value2; name3=value3]
508 - Only includes cookie names and values, not attributes
509 - Cookies should already be filtered for the target domain/path
510
511 Example: [make_cookie_header cookies] might return
512 ["session=abc123; theme=dark"]
513
514 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.2> RFC 6265 Section 4.2 - The Cookie Header *)
515
516val make_set_cookie_header : t -> string
517(** Create Set-Cookie header value from a cookie.
518
519 Formats a cookie into a Set-Cookie header value suitable for HTTP responses
520 per {{:https://datatracker.ietf.org/doc/html/rfc6265#section-4.1} RFC 6265 Section 4.1}.
521 Includes all cookie attributes: Max-Age, Expires, Domain, Path, Secure,
522 HttpOnly, Partitioned, and SameSite.
523
524 Note: The Expires attribute is currently formatted using RFC 3339 format,
525 which differs from the RFC-recommended rfc1123-date format specified in
526 {{:https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1} Section 4.1.1}.
527
528 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.1> RFC 6265 Section 4.1 - The Set-Cookie Header *)
529
530(** {1 Pretty Printing} *)
531
532val pp : Format.formatter -> t -> unit
533(** Pretty print a cookie. *)