OCaml HTTP cookie handling library with support for Eio-based storage jars

Enhance documentation with odoc extension plugins

- Convert RFC URL references to semantic @rfc tags across cookeio, imap, jmap
- Add security admonition warning to cookeio cookie documentation
- Add MSC protocol flow diagram to imap.mli showing IMAP session sequence
- Add Mermaid sequence diagram to jmap.mli showing JMAP request batching

Uses the new odoc-rfc-extension, odoc-admonition-extension, odoc-msc-extension,
and odoc-mermaid-extension plugins to generate styled, linked documentation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+49 -45
+31 -27
lib/core/cookeio.mli
··· 15 15 This library provides a complete cookie implementation following RFC 6265 16 16 while integrating Eio for efficient asynchronous operations. 17 17 18 + @admonition.warning Cookies may contain sensitive information such as 19 + session tokens. Always use the [Secure] flag for authentication cookies 20 + and consider [HttpOnly] to prevent XSS attacks from accessing cookie values. 21 + 18 22 {2 Cookie Format and Structure} 19 23 20 24 Cookies are set via the Set-Cookie HTTP response header ··· 45 49 - IP addresses require exact match only 46 50 - Path matching requires exact match or prefix with "/" separator 47 51 48 - @see <https://datatracker.ietf.org/doc/html/rfc6265> RFC 6265 - HTTP State Management Mechanism 52 + @rfc 6265 49 53 50 54 {2 Standards and References} 51 55 ··· 89 93 - [`None]: Cookie sent for all cross-site requests (requires [secure] 90 94 flag per RFC 6265bis) 91 95 92 - @see <https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7> RFC 6265bis Section 5.4.7 - The SameSite Attribute *) 96 + @admonition.note The SameSite attribute is defined in RFC 6265bis (draft), not the original RFC 6265. *) 93 97 94 98 val equal : t -> t -> bool 95 99 (** Equality function for same-site values. *) ··· 108 112 - [`DateTime time]: Persistent cookie that expires at specific time 109 113 (persistent-flag = true) 110 114 111 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model *) 115 + @rfc 6265 Section 5.3 *) 112 116 113 117 val equal : t -> t -> bool 114 118 (** Equality function for expiration values. *) ··· 126 130 cookies with the same [name], [domain], and [path] will overwrite each other 127 131 when stored. 128 132 129 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model *) 133 + @rfc 6265 Section 5.3 *) 130 134 131 135 (** {1 Cookie Accessors} *) 132 136 ··· 140 144 val path : t -> string 141 145 (** Get the path of a cookie. 142 146 143 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.4> RFC 6265 Section 5.2.4 - The Path Attribute *) 147 + @rfc 6265 Section 5.2.4 *) 144 148 145 149 val name : t -> string 146 150 (** Get the name of a cookie. *) ··· 168 172 Per {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.5} RFC 6265 Section 5.2.5}, 169 173 Secure cookies are only sent over HTTPS connections. 170 174 171 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.5> RFC 6265 Section 5.2.5 - The Secure Attribute *) 175 + @rfc 6265 Section 5.2.5 *) 172 176 173 177 val http_only : t -> bool 174 178 (** Check if cookie has the HttpOnly flag. ··· 176 180 Per {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.6} RFC 6265 Section 5.2.6}, 177 181 HttpOnly cookies are not accessible to client-side scripts. 178 182 179 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.6> RFC 6265 Section 5.2.6 - The HttpOnly Attribute *) 183 + @rfc 6265 Section 5.2.6 *) 180 184 181 185 val partitioned : t -> bool 182 186 (** Check if cookie has the Partitioned attribute. ··· 203 207 - Cookie set on "example.com" without Domain attribute: host_only=true, 204 208 matches only example.com, not sub.example.com 205 209 206 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model *) 210 + @rfc 6265 Section 5.3 *) 207 211 208 212 val expires : t -> Expiration.t option 209 213 (** Get the expiration attribute if set. ··· 216 220 Both [max_age] and [expires] can be present simultaneously. This library 217 221 stores both independently. 218 222 219 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1> RFC 6265 Section 5.2.1 - The Expires Attribute *) 223 + @rfc 6265 Section 5.2.1 *) 220 224 221 225 val max_age : t -> Ptime.Span.t option 222 226 (** Get the max-age attribute if set. ··· 229 233 230 234 This library stores both independently and serializes both when present. 231 235 232 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2> RFC 6265 Section 5.2.2 - The Max-Age Attribute *) 236 + @rfc 6265 Section 5.2.2 *) 233 237 234 238 val same_site : t -> SameSite.t option 235 239 (** Get the same-site policy of a cookie. 236 240 237 - @see <https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7> RFC 6265bis Section 5.4.7 - The SameSite Attribute *) 241 + *) 238 242 239 243 val creation_time : t -> Ptime.t 240 244 (** Get the creation time of a cookie. ··· 286 290 Note: If [partitioned] is [true], the cookie must also be [secure]. Invalid 287 291 combinations will result in validation errors. 288 292 289 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model *) 293 + @rfc 6265 Section 5.3 *) 290 294 291 295 (** {1 RFC 6265 Validation} 292 296 ··· 320 324 These functions return [Ok value] on success or [Error msg] with a detailed 321 325 explanation of why validation failed. 322 326 323 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1> RFC 6265 Section 4.1.1 - Syntax *) 327 + @rfc 6265 Section 4.1.1 *) 324 328 325 329 module Validate : sig 326 330 val cookie_name : string -> (string, string) result ··· 341 345 @param name The cookie name to validate 342 346 @return [Ok name] if valid, [Error message] with explanation if invalid 343 347 344 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1> RFC 6265 Section 4.1.1 345 - @see <https://datatracker.ietf.org/doc/html/rfc2616#section-2.2> RFC 2616 Section 2.2 - Basic Rules *) 348 + @rfc 6265 Section 4.1.1 349 + @rfc 2616 Section 2.2 *) 346 350 347 351 val cookie_value : string -> (string, string) result 348 352 (** Validate a cookie value per RFC 6265. ··· 364 368 @param value The cookie value to validate 365 369 @return [Ok value] if valid, [Error message] with explanation if invalid 366 370 367 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1> RFC 6265 Section 4.1.1 *) 371 + @rfc 6265 Section 4.1.1 *) 368 372 369 373 val domain_value : string -> (string, string) result 370 374 (** Validate a domain attribute value. ··· 388 392 @param domain The domain value to validate (leading dot is stripped first) 389 393 @return [Ok domain] if valid, [Error message] with explanation if invalid 390 394 391 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.2.3> RFC 6265 Section 4.1.2.3 392 - @see <https://datatracker.ietf.org/doc/html/rfc1034#section-3.5> RFC 1034 Section 3.5 *) 395 + @rfc 6265 Section 4.1.2.3 396 + @rfc 1034 Section 3.5 *) 393 397 394 398 val path_value : string -> (string, string) result 395 399 (** Validate a path attribute value. ··· 400 404 @param path The path value to validate 401 405 @return [Ok path] if valid, [Error message] with explanation if invalid 402 406 403 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1> RFC 6265 Section 4.1.1 *) 407 + @rfc 6265 Section 4.1.1 *) 404 408 405 409 val max_age : int -> (int, string) result 406 410 (** Validate a Max-Age attribute value. ··· 413 417 @param seconds The Max-Age value in seconds 414 418 @return [Ok seconds] always (negative values are handled in parsing) 415 419 416 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1> RFC 6265 Section 4.1.1 417 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2> RFC 6265 Section 5.2.2 *) 420 + @rfc 6265 Section 4.1.1 421 + @rfc 6265 Section 5.2.2 *) 418 422 end 419 423 420 424 (** {1 Cookie Creation and Parsing} *) ··· 468 472 {[of_set_cookie_header ~now:(fun () -> Ptime_clock.now ()) 469 473 ~domain:"example.com" ~path:"/" "session=abc123; Secure; HttpOnly"]} 470 474 471 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2> RFC 6265 Section 5.2 - The Set-Cookie Header 472 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model (public suffix check) 473 - @see <https://publicsuffix.org/list/> Public Suffix List *) 475 + @rfc 6265 Section 5.2 476 + @rfc 6265 Section 5.3 477 + *) 474 478 475 479 val of_cookie_header : 476 480 now:(unit -> Ptime.t) -> ··· 503 507 {[of_cookie_header ~now:(fun () -> Ptime_clock.now ()) ~domain:"example.com" 504 508 ~path:"/" "session=abc; theme=dark"]} 505 509 506 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.2> RFC 6265 Section 4.2 - The Cookie Header *) 510 + @rfc 6265 Section 4.2 *) 507 511 508 512 val make_cookie_header : t list -> string 509 513 (** Create Cookie header value from cookies. ··· 517 521 Example: [make_cookie_header cookies] might return 518 522 ["session=abc123; theme=dark"] 519 523 520 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.2> RFC 6265 Section 4.2 - The Cookie Header *) 524 + @rfc 6265 Section 4.2 *) 521 525 522 526 val make_set_cookie_header : t -> string 523 527 (** Create Set-Cookie header value from a cookie. ··· 530 534 The Expires attribute uses rfc1123-date format ("Sun, 06 Nov 1994 08:49:37 GMT") 531 535 as specified in {{:https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1} Section 4.1.1}. 532 536 533 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.1> RFC 6265 Section 4.1 - The Set-Cookie Header *) 537 + @rfc 6265 Section 4.1 *) 534 538 535 539 (** {1 Pretty Printing} *) 536 540
+11 -11
lib/jar/cookeio_jar.ml
··· 25 25 (** Two cookies are considered identical if they have the same name, domain, 26 26 and path. This is used when replacing or removing cookies. 27 27 28 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model *) 28 + @rfc 6265 Section 5.3 *) 29 29 let cookie_identity_matches c1 c2 = 30 30 Cookeio.name c1 = Cookeio.name c2 31 31 && Cookeio.domain c1 = Cookeio.domain c2 ··· 36 36 Per RFC 6265, the Domain attribute value is canonicalized by removing any 37 37 leading dot before storage. 38 38 39 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3> RFC 6265 Section 5.2.3 - The Domain Attribute *) 39 + @rfc 6265 Section 5.2.3 *) 40 40 let normalize_domain domain = 41 41 match String.starts_with ~prefix:"." domain with 42 42 | true when String.length domain > 1 -> ··· 63 63 Per RFC 6265 Section 5.1.3, domain matching should only apply to hostnames, 64 64 not IP addresses. IP addresses require exact match only. 65 65 66 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3> RFC 6265 Section 5.1.3 - Domain Matching *) 66 + @rfc 6265 Section 5.1.3 *) 67 67 let is_ip_address domain = Result.is_ok (Ipaddr.of_string domain) 68 68 69 69 (** Check if a cookie domain matches a request domain. ··· 83 83 @param request_domain The domain from the HTTP request 84 84 @return true if the cookie should be sent for this domain 85 85 86 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3> RFC 6265 Section 5.1.3 - Domain Matching 87 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model (host-only-flag) *) 86 + @rfc 6265 Section 5.1.3 87 + @rfc 6265 Section 5.3 *) 88 88 let domain_matches ~host_only cookie_domain request_domain = 89 89 request_domain = cookie_domain 90 90 || (not (is_ip_address request_domain || host_only) ··· 103 103 @param request_path The path from the HTTP request 104 104 @return true if the cookie should be sent for this path 105 105 106 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4> RFC 6265 Section 5.1.4 - Paths and Path-Match *) 106 + @rfc 6265 Section 5.1.4 *) 107 107 let path_matches cookie_path request_path = 108 108 if cookie_path = request_path then true 109 109 else if String.starts_with ~prefix:cookie_path request_path then ··· 124 124 @param clock The Eio clock for current time 125 125 @return true if the cookie has expired 126 126 127 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model *) 127 + @rfc 6265 Section 5.3 *) 128 128 let is_expired cookie clock = 129 129 match Cookeio.expires cookie with 130 130 | None -> false (* No expiration *) ··· 169 169 @param new_cookie The new cookie to add 170 170 @return The new cookie with creation_time preserved from old_cookie if present 171 171 172 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model *) 172 + @rfc 6265 Section 5.3 *) 173 173 let preserve_creation_time old_cookie_opt new_cookie = 174 174 match old_cookie_opt with 175 175 | None -> new_cookie ··· 261 261 @param clock The Eio clock for timestamps 262 262 @return A new cookie configured to cause deletion 263 263 264 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model *) 264 + @rfc 6265 Section 5.3 *) 265 265 let make_removal_cookie cookie ~clock = 266 266 let now = 267 267 Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch ··· 315 315 1. Cookies with longer paths listed first 316 316 2. Among equal-length paths, cookies with earlier creation-times first 317 317 318 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.4> RFC 6265 Section 5.4 - The Cookie Header *) 318 + @rfc 6265 Section 5.4 *) 319 319 let compare_cookie_order c1 c2 = 320 320 let path1_len = String.length (Cookeio.path c1) in 321 321 let path2_len = String.length (Cookeio.path c2) in ··· 343 343 @param is_secure Whether the request is over a secure channel (HTTPS) 344 344 @return List of cookies that should be included in the Cookie header, sorted 345 345 346 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.4> RFC 6265 Section 5.4 - The Cookie Header *) 346 + @rfc 6265 Section 5.4 *) 347 347 let get_cookies jar ~clock ~domain:request_domain ~path:request_path ~is_secure 348 348 = 349 349 Log.debug (fun m ->
+7 -7
lib/jar/cookeio_jar.mli
··· 18 18 - Delta tracking for Set-Cookie headers 19 19 - Mozilla format persistence for cross-tool compatibility 20 20 21 - @see <https://datatracker.ietf.org/doc/html/rfc6265> RFC 6265 - HTTP State Management Mechanism 21 + @rfc 6265 22 22 23 23 {2 Standards and References} 24 24 ··· 81 81 creation-time is preserved. This ensures stable cookie ordering per 82 82 Section 5.4, Step 2. 83 83 84 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model *) 84 + @rfc 6265 Section 5.3 *) 85 85 86 86 val add_original : t -> Cookeio.t -> unit 87 87 (** Add an original cookie to the jar. ··· 93 93 Per Section 5.3, Step 11.3, when replacing an existing cookie, the original 94 94 creation-time is preserved. 95 95 96 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model *) 96 + @rfc 6265 Section 5.3 *) 97 97 98 98 val delta : t -> Cookeio.t list 99 99 (** Get cookies that need to be sent in Set-Cookie headers. ··· 102 102 for original cookies that have been removed. Does not include original 103 103 cookies that were added via {!add_original}. 104 104 105 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-4.1> RFC 6265 Section 4.1 - Set-Cookie *) 105 + @rfc 6265 Section 4.1 *) 106 106 107 107 val remove : t -> clock:_ Eio.Time.clock -> Cookeio.t -> unit 108 108 (** Remove a cookie from the jar. ··· 114 114 Per {{:https://datatracker.ietf.org/doc/html/rfc6265#section-5.3} RFC 6265 Section 5.3}, 115 115 cookies are removed by sending a Set-Cookie with an expiry date in the past. 116 116 117 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model *) 117 + @rfc 6265 Section 5.3 *) 118 118 119 119 val get_cookies : 120 120 t -> ··· 169 169 @param is_secure Whether the request is over a secure channel (HTTPS) 170 170 @return List of matching cookies, sorted per RFC 6265 171 171 172 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model (expiry) 173 - @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.4> RFC 6265 Section 5.4 - The Cookie Header *) 172 + @rfc 6265 Section 5.3 173 + @rfc 6265 Section 5.4 *) 174 174 175 175 val clear : t -> unit 176 176 (** Clear all cookies. *)