···11+(*
22+ * ISC License
33+ *
44+ * Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>
55+ *
66+ * Permission to use, copy, modify, and distribute this software for any
77+ * purpose with or without fee is hereby granted, provided that the above
88+ * copyright notice and this permission notice appear in all copies.
99+ *
1010+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
1111+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1212+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1313+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1414+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1515+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1616+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1717+ *
1818+ *)
+64
README.md
···11+# Cookeio - HTTP Cookie Management for OCaml
22+33+Cookeio is an OCaml library for managing HTTP cookies.
44+55+## Overview
66+77+HTTP cookies are a mechanism for maintaining client-side state in web
88+applications. Originally specified to allow "server side connections to store
99+and retrieve information on the client side," cookies enable persistent storage
1010+of user preferences, session data, shopping cart contents, and authentication
1111+tokens.
1212+1313+This library provides a complete cookie jar implementation following
1414+established standards while integrating with OCaml's for efficient asynchronous
1515+operations.
1616+1717+## Cookie Attributes
1818+1919+The library supports all standard HTTP cookie attributes:
2020+2121+- **Domain**: Controls which domains can access the cookie using tail matching
2222+- **Path**: Defines URL subsets where the cookie is valid
2323+- **Secure**: Restricts transmission to HTTPS connections only
2424+- **HttpOnly**: Prevents JavaScript access to the cookie
2525+- **Expires**: Sets cookie lifetime (session cookies when omitted)
2626+- **SameSite**: Controls cross-site request behavior (`Strict`, `Lax`, or `None`)
2727+2828+## Usage
2929+3030+```ocaml
3131+(* Create a new cookie jar *)
3232+let jar = Cookeio.create () in
3333+3434+(* Parse a Set-Cookie header *)
3535+let cookie = Cookeio.parse_set_cookie
3636+ ~domain:"example.com"
3737+ ~path:"/"
3838+ "session=abc123; Secure; HttpOnly; SameSite=Strict" in
3939+4040+(* Add cookie to jar *)
4141+Option.iter (Cookeio.add_cookie jar) cookie;
4242+4343+(* Get cookies for a request *)
4444+let cookies = Cookeio.get_cookies jar
4545+ ~domain:"example.com"
4646+ ~path:"/api"
4747+ ~is_secure:true in
4848+4949+(* Generate Cookie header *)
5050+let header = Cookeio.make_cookie_header cookies
5151+```
5252+5353+## Storage and Persistence
5454+5555+Cookies can be persisted to disk in Mozilla format for compatibility with other
5656+tools:
5757+5858+```ocaml
5959+(* Save cookies to file *)
6060+Cookeio.save (Eio.Path.of_string "cookies.txt") jar;
6161+6262+(* Load cookies from file *)
6363+let jar = Cookeio.load (Eio.Path.of_string "cookies.txt")
6464+```
+35
cookeio.opam
···11+# This file is generated by dune, edit dune-project instead
22+opam-version: "2.0"
33+synopsis: "Cookie parsing and management library using Eio"
44+description:
55+ "Cookeio provides cookie management functionality for OCaml applications, including parsing Set-Cookie headers, managing cookie jars, and supporting the Mozilla cookies.txt format for persistence."
66+maintainer: ["Anil Madhavapeddy"]
77+authors: ["Anil Madhavapeddy"]
88+license: "ISC"
99+homepage: "https://github.com/avsm/cookeio"
1010+bug-reports: "https://github.com/avsm/cookeio/issues"
1111+depends: [
1212+ "ocaml" {>= "5.2.0"}
1313+ "dune" {>= "3.19"}
1414+ "eio" {>= "1.0"}
1515+ "logs" {>= "0.9.0"}
1616+ "ptime" {>= "1.1.0"}
1717+ "alcotest" {with-test}
1818+ "odoc" {with-doc}
1919+]
2020+build: [
2121+ ["dune" "subst"] {dev}
2222+ [
2323+ "dune"
2424+ "build"
2525+ "-p"
2626+ name
2727+ "-j"
2828+ jobs
2929+ "@install"
3030+ "@runtest" {with-test}
3131+ "@doc" {with-doc}
3232+ ]
3333+]
3434+dev-repo: "git+https://github.com/avsm/cookeio.git"
3535+x-maintenance-intent: ["(latest)"]
+23
dune-project
···11+(lang dune 3.19)
22+33+(name cookeio)
44+55+(generate_opam_files true)
66+77+(source (github avsm/cookeio))
88+99+(authors "Anil Madhavapeddy")
1010+(maintainers "Anil Madhavapeddy")
1111+(license ISC)
1212+1313+(package
1414+ (name cookeio)
1515+ (synopsis "Cookie parsing and management library using Eio")
1616+ (description "Cookeio provides cookie management functionality for OCaml applications, including parsing Set-Cookie headers, managing cookie jars, and supporting the Mozilla cookies.txt format for persistence.")
1717+ (depends
1818+ (ocaml (>= 5.2.0))
1919+ dune
2020+ (eio (>= 1.0))
2121+ (logs (>= 0.9.0))
2222+ (ptime (>= 1.1.0))
2323+ (alcotest :with-test)))
+397
lib/cookeio.ml
···11+let src = Logs.Src.create "cookeio" ~doc:"Cookie management"
22+33+module Log = (val Logs.src_log src : Logs.LOG)
44+55+type same_site = [ `Strict | `Lax | `None ]
66+(** Cookie same-site policy *)
77+88+type t = {
99+ domain : string;
1010+ path : string;
1111+ name : string;
1212+ value : string;
1313+ secure : bool;
1414+ http_only : bool;
1515+ expires : Ptime.t option;
1616+ same_site : same_site option;
1717+ creation_time : Ptime.t;
1818+ last_access : Ptime.t;
1919+}
2020+(** HTTP Cookie *)
2121+2222+type jar = { mutable cookies : t list; mutex : Eio.Mutex.t }
2323+(** Cookie jar for storing and managing cookies *)
2424+2525+(** {1 Cookie Accessors} *)
2626+2727+let domain cookie = cookie.domain
2828+let path cookie = cookie.path
2929+let name cookie = cookie.name
3030+let value cookie = cookie.value
3131+let secure cookie = cookie.secure
3232+let http_only cookie = cookie.http_only
3333+let expires cookie = cookie.expires
3434+let same_site cookie = cookie.same_site
3535+let creation_time cookie = cookie.creation_time
3636+let last_access cookie = cookie.last_access
3737+3838+let make ~domain ~path ~name ~value ?(secure = false) ?(http_only = false)
3939+ ?expires ?same_site ~creation_time ~last_access () =
4040+ { domain; path; name; value; secure; http_only; expires; same_site; creation_time; last_access }
4141+4242+(** {1 Cookie Jar Creation} *)
4343+4444+let create () =
4545+ Log.debug (fun m -> m "Creating new empty cookie jar");
4646+ { cookies = []; mutex = Eio.Mutex.create () }
4747+4848+(** {1 Cookie Matching Helpers} *)
4949+5050+let domain_matches cookie_domain request_domain =
5151+ (* Cookie domain .example.com matches example.com and sub.example.com *)
5252+ if String.starts_with ~prefix:"." cookie_domain then
5353+ let domain_suffix = String.sub cookie_domain 1 (String.length cookie_domain - 1) in
5454+ request_domain = domain_suffix
5555+ || String.ends_with ~suffix:("." ^ domain_suffix) request_domain
5656+ else cookie_domain = request_domain
5757+5858+let path_matches cookie_path request_path =
5959+ (* Cookie path /foo matches /foo, /foo/, /foo/bar *)
6060+ String.starts_with ~prefix:cookie_path request_path
6161+6262+let is_expired cookie clock =
6363+ match cookie.expires with
6464+ | None -> false (* Session cookie *)
6565+ | Some exp_time ->
6666+ let now =
6767+ Ptime.of_float_s (Eio.Time.now clock)
6868+ |> Option.value ~default:Ptime.epoch
6969+ in
7070+ Ptime.compare now exp_time > 0
7171+7272+(** {1 Cookie Parsing} *)
7373+7474+let parse_cookie_attribute attr attr_value cookie =
7575+ let attr_lower = String.lowercase_ascii attr in
7676+ match attr_lower with
7777+ | "domain" -> make ~domain:attr_value ~path:(path cookie) ~name:(name cookie)
7878+ ~value:(value cookie) ~secure:(secure cookie) ~http_only:(http_only cookie)
7979+ ?expires:(expires cookie) ?same_site:(same_site cookie)
8080+ ~creation_time:(creation_time cookie) ~last_access:(last_access cookie) ()
8181+ | "path" -> make ~domain:(domain cookie) ~path:attr_value ~name:(name cookie)
8282+ ~value:(value cookie) ~secure:(secure cookie) ~http_only:(http_only cookie)
8383+ ?expires:(expires cookie) ?same_site:(same_site cookie)
8484+ ~creation_time:(creation_time cookie) ~last_access:(last_access cookie) ()
8585+ | "expires" -> (
8686+ try
8787+ let time, _tz_offset, _tz_string =
8888+ Ptime.of_rfc3339 attr_value |> Result.get_ok
8989+ in
9090+ make ~domain:(domain cookie) ~path:(path cookie) ~name:(name cookie)
9191+ ~value:(value cookie) ~secure:(secure cookie) ~http_only:(http_only cookie)
9292+ ~expires:time ?same_site:(same_site cookie)
9393+ ~creation_time:(creation_time cookie) ~last_access:(last_access cookie) ()
9494+ with _ ->
9595+ Log.debug (fun m -> m "Failed to parse expires: %s" attr_value);
9696+ cookie)
9797+ | "max-age" -> (
9898+ try
9999+ let seconds = int_of_string attr_value in
100100+ let now = Unix.time () in
101101+ let expires = Ptime.of_float_s (now +. float_of_int seconds) in
102102+ make ~domain:(domain cookie) ~path:(path cookie) ~name:(name cookie)
103103+ ~value:(value cookie) ~secure:(secure cookie) ~http_only:(http_only cookie)
104104+ ?expires ?same_site:(same_site cookie)
105105+ ~creation_time:(creation_time cookie) ~last_access:(last_access cookie) ()
106106+ with _ -> cookie)
107107+ | "secure" -> make ~domain:(domain cookie) ~path:(path cookie) ~name:(name cookie)
108108+ ~value:(value cookie) ~secure:true ~http_only:(http_only cookie)
109109+ ?expires:(expires cookie) ?same_site:(same_site cookie)
110110+ ~creation_time:(creation_time cookie) ~last_access:(last_access cookie) ()
111111+ | "httponly" -> make ~domain:(domain cookie) ~path:(path cookie) ~name:(name cookie)
112112+ ~value:(value cookie) ~secure:(secure cookie) ~http_only:true
113113+ ?expires:(expires cookie) ?same_site:(same_site cookie)
114114+ ~creation_time:(creation_time cookie) ~last_access:(last_access cookie) ()
115115+ | "samesite" ->
116116+ let same_site_val =
117117+ match String.lowercase_ascii attr_value with
118118+ | "strict" -> Some `Strict
119119+ | "lax" -> Some `Lax
120120+ | "none" -> Some `None
121121+ | _ -> None
122122+ in
123123+ make ~domain:(domain cookie) ~path:(path cookie) ~name:(name cookie)
124124+ ~value:(value cookie) ~secure:(secure cookie) ~http_only:(http_only cookie)
125125+ ?expires:(expires cookie) ?same_site:same_site_val
126126+ ~creation_time:(creation_time cookie) ~last_access:(last_access cookie) ()
127127+ | _ -> cookie
128128+129129+let rec parse_set_cookie ~domain:request_domain ~path:request_path header_value =
130130+ Log.debug (fun m -> m "Parsing Set-Cookie: %s" header_value);
131131+132132+ (* Split into attributes *)
133133+ let parts = String.split_on_char ';' header_value |> List.map String.trim in
134134+135135+ match parts with
136136+ | [] -> None
137137+ | name_value :: attrs -> (
138138+ (* Parse name=value *)
139139+ match String.index_opt name_value '=' with
140140+ | None -> None
141141+ | Some eq_pos ->
142142+ let name = String.sub name_value 0 eq_pos |> String.trim in
143143+ let cookie_value =
144144+ String.sub name_value (eq_pos + 1)
145145+ (String.length name_value - eq_pos - 1)
146146+ |> String.trim
147147+ in
148148+149149+ let now =
150150+ Ptime.of_float_s (Unix.time ()) |> Option.value ~default:Ptime.epoch
151151+ in
152152+ let base_cookie =
153153+ make ~domain:request_domain ~path:request_path ~name ~value:cookie_value ~secure:false ~http_only:false
154154+ ?expires:None ?same_site:None ~creation_time:now ~last_access:now ()
155155+ in
156156+157157+ (* Parse attributes *)
158158+ let cookie =
159159+ List.fold_left
160160+ (fun cookie attr ->
161161+ match String.index_opt attr '=' with
162162+ | None -> parse_cookie_attribute attr "" cookie
163163+ | Some eq ->
164164+ let attr_name = String.sub attr 0 eq |> String.trim in
165165+ let attr_value =
166166+ String.sub attr (eq + 1) (String.length attr - eq - 1)
167167+ |> String.trim
168168+ in
169169+ parse_cookie_attribute attr_name attr_value cookie)
170170+ base_cookie attrs
171171+ in
172172+173173+ Log.debug (fun m -> m "Parsed cookie: %a" pp cookie);
174174+ Some cookie)
175175+176176+and make_cookie_header cookies =
177177+ cookies
178178+ |> List.map (fun c -> Printf.sprintf "%s=%s" (name c) (value c))
179179+ |> String.concat "; "
180180+181181+(** {1 Pretty Printing} *)
182182+183183+and pp_same_site ppf = function
184184+ | `Strict -> Format.pp_print_string ppf "Strict"
185185+ | `Lax -> Format.pp_print_string ppf "Lax"
186186+ | `None -> Format.pp_print_string ppf "None"
187187+188188+and pp ppf cookie =
189189+ Format.fprintf ppf
190190+ "@[<hov 2>{ name=%S;@ value=%S;@ domain=%S;@ path=%S;@ secure=%b;@ \
191191+ http_only=%b;@ expires=%a;@ same_site=%a }@]"
192192+ (name cookie) (value cookie) (domain cookie) (path cookie) (secure cookie)
193193+ (http_only cookie)
194194+ (Format.pp_print_option Ptime.pp)
195195+ (expires cookie)
196196+ (Format.pp_print_option pp_same_site)
197197+ (same_site cookie)
198198+199199+let pp_jar ppf jar =
200200+ Eio.Mutex.lock jar.mutex;
201201+ let cookies = jar.cookies in
202202+ Eio.Mutex.unlock jar.mutex;
203203+204204+ Format.fprintf ppf "@[<v>CookieJar with %d cookies:@," (List.length cookies);
205205+ List.iter (fun cookie -> Format.fprintf ppf " %a@," pp cookie) cookies;
206206+ Format.fprintf ppf "@]"
207207+208208+(** {1 Cookie Management} *)
209209+210210+let add_cookie jar cookie =
211211+ Log.debug (fun m ->
212212+ m "Adding cookie: %s=%s for domain %s" (name cookie) (value cookie)
213213+ (domain cookie));
214214+215215+ Eio.Mutex.lock jar.mutex;
216216+ (* Remove existing cookie with same name, domain, and path *)
217217+ jar.cookies <-
218218+ List.filter
219219+ (fun c ->
220220+ not
221221+ (name c = name cookie && domain c = domain cookie
222222+ && path c = path cookie))
223223+ jar.cookies;
224224+ jar.cookies <- cookie :: jar.cookies;
225225+ Eio.Mutex.unlock jar.mutex
226226+227227+let get_cookies jar ~domain:request_domain ~path:request_path ~is_secure =
228228+ Log.debug (fun m ->
229229+ m "Getting cookies for domain=%s path=%s secure=%b" request_domain request_path is_secure);
230230+231231+ Eio.Mutex.lock jar.mutex;
232232+ let applicable =
233233+ List.filter
234234+ (fun cookie ->
235235+ domain_matches (domain cookie) request_domain
236236+ && path_matches (path cookie) request_path
237237+ && ((not (secure cookie)) || is_secure))
238238+ jar.cookies
239239+ in
240240+241241+ (* Update last access time *)
242242+ let now =
243243+ Ptime.of_float_s (Unix.time ()) |> Option.value ~default:Ptime.epoch
244244+ in
245245+ let updated =
246246+ List.map
247247+ (fun c ->
248248+ if List.memq c applicable then
249249+ make ~domain:(domain c) ~path:(path c) ~name:(name c) ~value:(value c)
250250+ ~secure:(secure c) ~http_only:(http_only c) ?expires:(expires c)
251251+ ?same_site:(same_site c) ~creation_time:(creation_time c) ~last_access:now ()
252252+ else c)
253253+ jar.cookies
254254+ in
255255+ jar.cookies <- updated;
256256+ Eio.Mutex.unlock jar.mutex;
257257+258258+ Log.debug (fun m -> m "Found %d applicable cookies" (List.length applicable));
259259+ applicable
260260+261261+let clear jar =
262262+ Log.info (fun m -> m "Clearing all cookies");
263263+ Eio.Mutex.lock jar.mutex;
264264+ jar.cookies <- [];
265265+ Eio.Mutex.unlock jar.mutex
266266+267267+let clear_expired jar ~clock =
268268+ Eio.Mutex.lock jar.mutex;
269269+ let before_count = List.length jar.cookies in
270270+ jar.cookies <- List.filter (fun c -> not (is_expired c clock)) jar.cookies;
271271+ let removed = before_count - List.length jar.cookies in
272272+ Eio.Mutex.unlock jar.mutex;
273273+ Log.info (fun m -> m "Cleared %d expired cookies" removed)
274274+275275+let clear_session_cookies jar =
276276+ Eio.Mutex.lock jar.mutex;
277277+ let before_count = List.length jar.cookies in
278278+ jar.cookies <- List.filter (fun c -> expires c <> None) jar.cookies;
279279+ let removed = before_count - List.length jar.cookies in
280280+ Eio.Mutex.unlock jar.mutex;
281281+ Log.info (fun m -> m "Cleared %d session cookies" removed)
282282+283283+let count jar =
284284+ Eio.Mutex.lock jar.mutex;
285285+ let n = List.length jar.cookies in
286286+ Eio.Mutex.unlock jar.mutex;
287287+ n
288288+289289+let get_all_cookies jar =
290290+ Eio.Mutex.lock jar.mutex;
291291+ let cookies = jar.cookies in
292292+ Eio.Mutex.unlock jar.mutex;
293293+ cookies
294294+295295+let is_empty jar =
296296+ Eio.Mutex.lock jar.mutex;
297297+ let empty = jar.cookies = [] in
298298+ Eio.Mutex.unlock jar.mutex;
299299+ empty
300300+301301+(** {1 Mozilla Format} *)
302302+303303+let to_mozilla_format_internal jar =
304304+ let buffer = Buffer.create 1024 in
305305+ Buffer.add_string buffer "# Netscape HTTP Cookie File\n";
306306+ Buffer.add_string buffer "# This is a generated file! Do not edit.\n\n";
307307+308308+ List.iter
309309+ (fun cookie ->
310310+ let include_subdomains =
311311+ if String.starts_with ~prefix:"." (domain cookie) then "TRUE" else "FALSE"
312312+ in
313313+ let secure_flag = if secure cookie then "TRUE" else "FALSE" in
314314+ let expires_str =
315315+ match expires cookie with
316316+ | None -> "0" (* Session cookie *)
317317+ | Some t ->
318318+ let epoch = Ptime.to_float_s t |> int_of_float |> string_of_int in
319319+ epoch
320320+ in
321321+322322+ Buffer.add_string buffer
323323+ (Printf.sprintf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" (domain cookie)
324324+ include_subdomains (path cookie) secure_flag expires_str (name cookie)
325325+ (value cookie)))
326326+ jar.cookies;
327327+328328+ Buffer.contents buffer
329329+330330+let to_mozilla_format jar =
331331+ Eio.Mutex.lock jar.mutex;
332332+ let result = to_mozilla_format_internal jar in
333333+ Eio.Mutex.unlock jar.mutex;
334334+ result
335335+336336+let from_mozilla_format content =
337337+ Log.debug (fun m -> m "Parsing Mozilla format cookies");
338338+ let jar = create () in
339339+340340+ let lines = String.split_on_char '\n' content in
341341+ List.iter
342342+ (fun line ->
343343+ let line = String.trim line in
344344+ if line <> "" && not (String.starts_with ~prefix:"#" line) then
345345+ match String.split_on_char '\t' line with
346346+ | [ domain; _include_subdomains; path; secure; expires; name; value ] ->
347347+ let now =
348348+ Ptime.of_float_s (Unix.time ())
349349+ |> Option.value ~default:Ptime.epoch
350350+ in
351351+ let expires =
352352+ let exp_int = try int_of_string expires with _ -> 0 in
353353+ if exp_int = 0 then None
354354+ else Ptime.of_float_s (float_of_int exp_int)
355355+ in
356356+357357+ let cookie =
358358+ make ~domain ~path ~name ~value
359359+ ~secure:(secure = "TRUE") ~http_only:false
360360+ ?expires ?same_site:None
361361+ ~creation_time:now ~last_access:now ()
362362+ in
363363+ add_cookie jar cookie;
364364+ Log.debug (fun m -> m "Loaded cookie: %s=%s" name value)
365365+ | _ -> Log.warn (fun m -> m "Invalid cookie line: %s" line))
366366+ lines;
367367+368368+ Log.info (fun m -> m "Loaded %d cookies" (List.length jar.cookies));
369369+ jar
370370+371371+(** {1 File Operations} *)
372372+373373+let load path =
374374+ Log.info (fun m -> m "Loading cookies from %a" Eio.Path.pp path);
375375+376376+ try
377377+ let content = Eio.Path.load path in
378378+ from_mozilla_format content
379379+ with
380380+ | Eio.Io _ ->
381381+ Log.info (fun m -> m "Cookie file not found, creating empty jar");
382382+ create ()
383383+ | exn ->
384384+ Log.err (fun m -> m "Failed to load cookies: %s" (Printexc.to_string exn));
385385+ create ()
386386+387387+let save path jar =
388388+ Log.info (fun m ->
389389+ m "Saving %d cookies to %a" (List.length jar.cookies) Eio.Path.pp path);
390390+391391+ let content = to_mozilla_format jar in
392392+393393+ try
394394+ Eio.Path.save ~create:(`Or_truncate 0o600) path content;
395395+ Log.debug (fun m -> m "Cookies saved successfully")
396396+ with exn ->
397397+ Log.err (fun m -> m "Failed to save cookies: %s" (Printexc.to_string exn))
+181
lib/cookeio.mli
···11+(** Cookie management library for OCaml
22+33+ HTTP cookies are a mechanism that allows "server side
44+ connections to store and retrieve information on the client side."
55+ Originally designed to enable persistent client-side state for web
66+ applications, cookies are essential for storing user preferences, session
77+ data, shopping cart contents, and authentication tokens.
88+99+ This library provides a complete cookie jar implementation following
1010+ established web standards while integrating Eio for efficient asynchronous operations.
1111+1212+ {2 Cookie Format and Structure}
1313+1414+ Cookies are set via the Set-Cookie HTTP response header with the basic
1515+ format: [NAME=VALUE] with optional attributes including:
1616+ - [expires]: Optional cookie lifetime specification
1717+ - [domain]: Specifying valid domains using tail matching
1818+ - [path]: Defining URL subset for cookie validity
1919+ - [secure]: Transmission over secure channels only
2020+ - [httponly]: Not accessible to JavaScript
2121+ - [samesite]: Cross-site request behavior control
2222+2323+ {2 Domain and Path Matching}
2424+2525+ The library implements standard domain and path matching rules:
2626+ - Domain matching uses "tail matching" (e.g., "acme.com" matches
2727+ "anvil.acme.com")
2828+ - Path matching allows subset URL specification for fine-grained control
2929+ - More specific path mappings are sent first in Cookie headers
3030+3131+ *)
3232+3333+type same_site = [ `Strict | `Lax | `None ]
3434+(** Cookie same-site policy for controlling cross-site request behavior.
3535+3636+ - [`Strict]: Cookie only sent for same-site requests, providing maximum
3737+ protection
3838+ - [`Lax]: Cookie sent for same-site requests and top-level navigation
3939+ (default for modern browsers)
4040+ - [`None]: Cookie sent for all cross-site requests (requires [secure] flag)
4141+*)
4242+4343+type t
4444+(** HTTP Cookie representation with all standard attributes.
4545+4646+ A cookie represents a name-value pair with associated metadata that controls
4747+ its scope, security, and lifetime. Cookies with the same [name], [domain],
4848+ and [path] will overwrite each other when added to a cookie jar. *)
4949+5050+type jar
5151+(** Cookie jar for storing and managing cookies.
5252+5353+ A cookie jar maintains a collection of cookies with automatic cleanup of
5454+ expired entries and enforcement of storage limits. It implements the
5555+ standard browser behavior for cookie storage, including:
5656+ - Automatic removal of expired cookies
5757+ - LRU eviction when storage limits are exceeded
5858+ - Domain and path-based cookie retrieval
5959+ - Mozilla format persistence for cross-tool compatibility *)
6060+6161+(** {1 Cookie Accessors} *)
6262+6363+val domain : t -> string
6464+(** Get the domain of a cookie *)
6565+6666+val path : t -> string
6767+(** Get the path of a cookie *)
6868+6969+val name : t -> string
7070+(** Get the name of a cookie *)
7171+7272+val value : t -> string
7373+(** Get the value of a cookie *)
7474+7575+val secure : t -> bool
7676+(** Check if cookie is secure only *)
7777+7878+val http_only : t -> bool
7979+(** Check if cookie is HTTP only *)
8080+8181+val expires : t -> Ptime.t option
8282+(** Get the expiry time of a cookie *)
8383+8484+val same_site : t -> same_site option
8585+(** Get the same-site policy of a cookie *)
8686+8787+val creation_time : t -> Ptime.t
8888+(** Get the creation time of a cookie *)
8989+9090+val last_access : t -> Ptime.t
9191+(** Get the last access time of a cookie *)
9292+9393+val make : domain:string -> path:string -> name:string -> value:string ->
9494+ ?secure:bool -> ?http_only:bool -> ?expires:Ptime.t ->
9595+ ?same_site:same_site -> creation_time:Ptime.t -> last_access:Ptime.t ->
9696+ unit -> t
9797+(** Create a new cookie with the given attributes *)
9898+9999+(** {1 Cookie Jar Creation and Loading} *)
100100+101101+val create : unit -> jar
102102+(** Create an empty cookie jar *)
103103+104104+val load : Eio.Fs.dir_ty Eio.Path.t -> jar
105105+(** Load cookies from Mozilla format file *)
106106+107107+val save : Eio.Fs.dir_ty Eio.Path.t -> jar -> unit
108108+(** Save cookies to Mozilla format file *)
109109+110110+(** {1 Cookie Jar Management} *)
111111+112112+val add_cookie : jar -> t -> unit
113113+(** Add a cookie to the jar *)
114114+115115+val get_cookies :
116116+ jar -> domain:string -> path:string -> is_secure:bool -> t list
117117+(** Get cookies applicable for a URL *)
118118+119119+val clear : jar -> unit
120120+(** Clear all cookies *)
121121+122122+val clear_expired : jar -> clock:_ Eio.Time.clock -> unit
123123+(** Clear expired cookies *)
124124+125125+val clear_session_cookies : jar -> unit
126126+(** Clear session cookies (those without expiry) *)
127127+128128+val count : jar -> int
129129+(** Get the number of cookies in the jar *)
130130+131131+val get_all_cookies : jar -> t list
132132+(** Get all cookies in the jar *)
133133+134134+val is_empty : jar -> bool
135135+(** Check if the jar is empty *)
136136+137137+(** {1 Cookie Creation and Parsing} *)
138138+139139+val parse_set_cookie : domain:string -> path:string -> string -> t option
140140+(** Parse Set-Cookie header value into a cookie.
141141+142142+ Parses a Set-Cookie header value following RFC specifications:
143143+ - Basic format: [NAME=VALUE; attribute1; attribute2=value2]
144144+ - Supports all standard attributes: [expires], [domain], [path], [secure],
145145+ [httponly], [samesite]
146146+ - Returns [None] if parsing fails or cookie is invalid
147147+ - The [domain] and [path] parameters provide the request context for default
148148+ values
149149+150150+ Example:
151151+ [parse_set_cookie ~domain:"example.com" ~path:"/" "session=abc123; Secure;
152152+ HttpOnly"] *)
153153+154154+val make_cookie_header : t list -> string
155155+(** Create cookie header value from cookies.
156156+157157+ Formats a list of cookies into a Cookie header value suitable for HTTP
158158+ requests.
159159+ - Format: [name1=value1; name2=value2; name3=value3]
160160+ - Only includes cookie names and values, not attributes
161161+ - Cookies should already be filtered for the target domain/path
162162+ - More specific path mappings should be ordered first in the input list
163163+164164+ Example: [make_cookie_header cookies] might return
165165+ ["session=abc123; theme=dark"] *)
166166+167167+(** {1 Pretty Printing} *)
168168+169169+val pp : Format.formatter -> t -> unit
170170+(** Pretty print a cookie *)
171171+172172+val pp_jar : Format.formatter -> jar -> unit
173173+(** Pretty print a cookie jar *)
174174+175175+(** {1 Mozilla Format} *)
176176+177177+val to_mozilla_format : jar -> string
178178+(** Write cookies in Mozilla format *)
179179+180180+val from_mozilla_format : string -> jar
181181+(** Parse Mozilla format cookies *)