···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
stack/cookeio/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
stack/cookeio/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
stack/cookeio/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)))
+398
stack/cookeio/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+ (* Parse various date formats *)
8787+ try
8888+ let time, _tz_offset, _tz_string =
8989+ Ptime.of_rfc3339 attr_value |> Result.get_ok
9090+ in
9191+ make ~domain:(domain cookie) ~path:(path cookie) ~name:(name cookie)
9292+ ~value:(value cookie) ~secure:(secure cookie) ~http_only:(http_only cookie)
9393+ ~expires:time ?same_site:(same_site cookie)
9494+ ~creation_time:(creation_time cookie) ~last_access:(last_access cookie) ()
9595+ with _ ->
9696+ Log.debug (fun m -> m "Failed to parse expires: %s" attr_value);
9797+ cookie)
9898+ | "max-age" -> (
9999+ try
100100+ let seconds = int_of_string attr_value in
101101+ let now = Unix.time () in
102102+ let expires = Ptime.of_float_s (now +. float_of_int seconds) in
103103+ make ~domain:(domain cookie) ~path:(path cookie) ~name:(name cookie)
104104+ ~value:(value cookie) ~secure:(secure cookie) ~http_only:(http_only cookie)
105105+ ?expires ?same_site:(same_site cookie)
106106+ ~creation_time:(creation_time cookie) ~last_access:(last_access cookie) ()
107107+ with _ -> cookie)
108108+ | "secure" -> make ~domain:(domain cookie) ~path:(path cookie) ~name:(name cookie)
109109+ ~value:(value cookie) ~secure:true ~http_only:(http_only cookie)
110110+ ?expires:(expires cookie) ?same_site:(same_site cookie)
111111+ ~creation_time:(creation_time cookie) ~last_access:(last_access cookie) ()
112112+ | "httponly" -> make ~domain:(domain cookie) ~path:(path cookie) ~name:(name cookie)
113113+ ~value:(value cookie) ~secure:(secure cookie) ~http_only:true
114114+ ?expires:(expires cookie) ?same_site:(same_site cookie)
115115+ ~creation_time:(creation_time cookie) ~last_access:(last_access cookie) ()
116116+ | "samesite" ->
117117+ let same_site_val =
118118+ match String.lowercase_ascii attr_value with
119119+ | "strict" -> Some `Strict
120120+ | "lax" -> Some `Lax
121121+ | "none" -> Some `None
122122+ | _ -> None
123123+ in
124124+ make ~domain:(domain cookie) ~path:(path cookie) ~name:(name cookie)
125125+ ~value:(value cookie) ~secure:(secure cookie) ~http_only:(http_only cookie)
126126+ ?expires:(expires cookie) ?same_site:same_site_val
127127+ ~creation_time:(creation_time cookie) ~last_access:(last_access cookie) ()
128128+ | _ -> cookie
129129+130130+let rec parse_set_cookie ~domain:request_domain ~path:request_path header_value =
131131+ Log.debug (fun m -> m "Parsing Set-Cookie: %s" header_value);
132132+133133+ (* Split into attributes *)
134134+ let parts = String.split_on_char ';' header_value |> List.map String.trim in
135135+136136+ match parts with
137137+ | [] -> None
138138+ | name_value :: attrs -> (
139139+ (* Parse name=value *)
140140+ match String.index_opt name_value '=' with
141141+ | None -> None
142142+ | Some eq_pos ->
143143+ let name = String.sub name_value 0 eq_pos |> String.trim in
144144+ let cookie_value =
145145+ String.sub name_value (eq_pos + 1)
146146+ (String.length name_value - eq_pos - 1)
147147+ |> String.trim
148148+ in
149149+150150+ let now =
151151+ Ptime.of_float_s (Unix.time ()) |> Option.value ~default:Ptime.epoch
152152+ in
153153+ let base_cookie =
154154+ make ~domain:request_domain ~path:request_path ~name ~value:cookie_value ~secure:false ~http_only:false
155155+ ?expires:None ?same_site:None ~creation_time:now ~last_access:now ()
156156+ in
157157+158158+ (* Parse attributes *)
159159+ let cookie =
160160+ List.fold_left
161161+ (fun cookie attr ->
162162+ match String.index_opt attr '=' with
163163+ | None -> parse_cookie_attribute attr "" cookie
164164+ | Some eq ->
165165+ let attr_name = String.sub attr 0 eq |> String.trim in
166166+ let attr_value =
167167+ String.sub attr (eq + 1) (String.length attr - eq - 1)
168168+ |> String.trim
169169+ in
170170+ parse_cookie_attribute attr_name attr_value cookie)
171171+ base_cookie attrs
172172+ in
173173+174174+ Log.debug (fun m -> m "Parsed cookie: %a" pp cookie);
175175+ Some cookie)
176176+177177+and make_cookie_header cookies =
178178+ cookies
179179+ |> List.map (fun c -> Printf.sprintf "%s=%s" (name c) (value c))
180180+ |> String.concat "; "
181181+182182+(** {1 Pretty Printing} *)
183183+184184+and pp_same_site ppf = function
185185+ | `Strict -> Format.pp_print_string ppf "Strict"
186186+ | `Lax -> Format.pp_print_string ppf "Lax"
187187+ | `None -> Format.pp_print_string ppf "None"
188188+189189+and pp ppf cookie =
190190+ Format.fprintf ppf
191191+ "@[<hov 2>{ name=%S;@ value=%S;@ domain=%S;@ path=%S;@ secure=%b;@ \
192192+ http_only=%b;@ expires=%a;@ same_site=%a }@]"
193193+ (name cookie) (value cookie) (domain cookie) (path cookie) (secure cookie)
194194+ (http_only cookie)
195195+ (Format.pp_print_option Ptime.pp)
196196+ (expires cookie)
197197+ (Format.pp_print_option pp_same_site)
198198+ (same_site cookie)
199199+200200+let pp_jar ppf jar =
201201+ Eio.Mutex.lock jar.mutex;
202202+ let cookies = jar.cookies in
203203+ Eio.Mutex.unlock jar.mutex;
204204+205205+ Format.fprintf ppf "@[<v>CookieJar with %d cookies:@," (List.length cookies);
206206+ List.iter (fun cookie -> Format.fprintf ppf " %a@," pp cookie) cookies;
207207+ Format.fprintf ppf "@]"
208208+209209+(** {1 Cookie Management} *)
210210+211211+let add_cookie jar cookie =
212212+ Log.debug (fun m ->
213213+ m "Adding cookie: %s=%s for domain %s" (name cookie) (value cookie)
214214+ (domain cookie));
215215+216216+ Eio.Mutex.lock jar.mutex;
217217+ (* Remove existing cookie with same name, domain, and path *)
218218+ jar.cookies <-
219219+ List.filter
220220+ (fun c ->
221221+ not
222222+ (name c = name cookie && domain c = domain cookie
223223+ && path c = path cookie))
224224+ jar.cookies;
225225+ jar.cookies <- cookie :: jar.cookies;
226226+ Eio.Mutex.unlock jar.mutex
227227+228228+let get_cookies jar ~domain:request_domain ~path:request_path ~is_secure =
229229+ Log.debug (fun m ->
230230+ m "Getting cookies for domain=%s path=%s secure=%b" request_domain request_path is_secure);
231231+232232+ Eio.Mutex.lock jar.mutex;
233233+ let applicable =
234234+ List.filter
235235+ (fun cookie ->
236236+ domain_matches (domain cookie) request_domain
237237+ && path_matches (path cookie) request_path
238238+ && ((not (secure cookie)) || is_secure))
239239+ jar.cookies
240240+ in
241241+242242+ (* Update last access time *)
243243+ let now =
244244+ Ptime.of_float_s (Unix.time ()) |> Option.value ~default:Ptime.epoch
245245+ in
246246+ let updated =
247247+ List.map
248248+ (fun c ->
249249+ if List.memq c applicable then
250250+ make ~domain:(domain c) ~path:(path c) ~name:(name c) ~value:(value c)
251251+ ~secure:(secure c) ~http_only:(http_only c) ?expires:(expires c)
252252+ ?same_site:(same_site c) ~creation_time:(creation_time c) ~last_access:now ()
253253+ else c)
254254+ jar.cookies
255255+ in
256256+ jar.cookies <- updated;
257257+ Eio.Mutex.unlock jar.mutex;
258258+259259+ Log.debug (fun m -> m "Found %d applicable cookies" (List.length applicable));
260260+ applicable
261261+262262+let clear jar =
263263+ Log.info (fun m -> m "Clearing all cookies");
264264+ Eio.Mutex.lock jar.mutex;
265265+ jar.cookies <- [];
266266+ Eio.Mutex.unlock jar.mutex
267267+268268+let clear_expired jar ~clock =
269269+ Eio.Mutex.lock jar.mutex;
270270+ let before_count = List.length jar.cookies in
271271+ jar.cookies <- List.filter (fun c -> not (is_expired c clock)) jar.cookies;
272272+ let removed = before_count - List.length jar.cookies in
273273+ Eio.Mutex.unlock jar.mutex;
274274+ Log.info (fun m -> m "Cleared %d expired cookies" removed)
275275+276276+let clear_session_cookies jar =
277277+ Eio.Mutex.lock jar.mutex;
278278+ let before_count = List.length jar.cookies in
279279+ jar.cookies <- List.filter (fun c -> expires c <> None) jar.cookies;
280280+ let removed = before_count - List.length jar.cookies in
281281+ Eio.Mutex.unlock jar.mutex;
282282+ Log.info (fun m -> m "Cleared %d session cookies" removed)
283283+284284+let count jar =
285285+ Eio.Mutex.lock jar.mutex;
286286+ let n = List.length jar.cookies in
287287+ Eio.Mutex.unlock jar.mutex;
288288+ n
289289+290290+let get_all_cookies jar =
291291+ Eio.Mutex.lock jar.mutex;
292292+ let cookies = jar.cookies in
293293+ Eio.Mutex.unlock jar.mutex;
294294+ cookies
295295+296296+let is_empty jar =
297297+ Eio.Mutex.lock jar.mutex;
298298+ let empty = jar.cookies = [] in
299299+ Eio.Mutex.unlock jar.mutex;
300300+ empty
301301+302302+(** {1 Mozilla Format} *)
303303+304304+let to_mozilla_format_internal jar =
305305+ let buffer = Buffer.create 1024 in
306306+ Buffer.add_string buffer "# Netscape HTTP Cookie File\n";
307307+ Buffer.add_string buffer "# This is a generated file! Do not edit.\n\n";
308308+309309+ List.iter
310310+ (fun cookie ->
311311+ let include_subdomains =
312312+ if String.starts_with ~prefix:"." (domain cookie) then "TRUE" else "FALSE"
313313+ in
314314+ let secure_flag = if secure cookie then "TRUE" else "FALSE" in
315315+ let expires_str =
316316+ match expires cookie with
317317+ | None -> "0" (* Session cookie *)
318318+ | Some t ->
319319+ let epoch = Ptime.to_float_s t |> int_of_float |> string_of_int in
320320+ epoch
321321+ in
322322+323323+ Buffer.add_string buffer
324324+ (Printf.sprintf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" (domain cookie)
325325+ include_subdomains (path cookie) secure_flag expires_str (name cookie)
326326+ (value cookie)))
327327+ jar.cookies;
328328+329329+ Buffer.contents buffer
330330+331331+let to_mozilla_format jar =
332332+ Eio.Mutex.lock jar.mutex;
333333+ let result = to_mozilla_format_internal jar in
334334+ Eio.Mutex.unlock jar.mutex;
335335+ result
336336+337337+let from_mozilla_format content =
338338+ Log.debug (fun m -> m "Parsing Mozilla format cookies");
339339+ let jar = create () in
340340+341341+ let lines = String.split_on_char '\n' content in
342342+ List.iter
343343+ (fun line ->
344344+ let line = String.trim line in
345345+ if line <> "" && not (String.starts_with ~prefix:"#" line) then
346346+ match String.split_on_char '\t' line with
347347+ | [ domain; _include_subdomains; path; secure; expires; name; value ] ->
348348+ let now =
349349+ Ptime.of_float_s (Unix.time ())
350350+ |> Option.value ~default:Ptime.epoch
351351+ in
352352+ let expires =
353353+ let exp_int = try int_of_string expires with _ -> 0 in
354354+ if exp_int = 0 then None
355355+ else Ptime.of_float_s (float_of_int exp_int)
356356+ in
357357+358358+ let cookie =
359359+ make ~domain ~path ~name ~value
360360+ ~secure:(secure = "TRUE") ~http_only:false
361361+ ?expires ?same_site:None
362362+ ~creation_time:now ~last_access:now ()
363363+ in
364364+ add_cookie jar cookie;
365365+ Log.debug (fun m -> m "Loaded cookie: %s=%s" name value)
366366+ | _ -> Log.warn (fun m -> m "Invalid cookie line: %s" line))
367367+ lines;
368368+369369+ Log.info (fun m -> m "Loaded %d cookies" (List.length jar.cookies));
370370+ jar
371371+372372+(** {1 File Operations} *)
373373+374374+let load path =
375375+ Log.info (fun m -> m "Loading cookies from %a" Eio.Path.pp path);
376376+377377+ try
378378+ let content = Eio.Path.load path in
379379+ from_mozilla_format content
380380+ with
381381+ | Eio.Io _ ->
382382+ Log.info (fun m -> m "Cookie file not found, creating empty jar");
383383+ create ()
384384+ | exn ->
385385+ Log.err (fun m -> m "Failed to load cookies: %s" (Printexc.to_string exn));
386386+ create ()
387387+388388+let save path jar =
389389+ Log.info (fun m ->
390390+ m "Saving %d cookies to %a" (List.length jar.cookies) Eio.Path.pp path);
391391+392392+ let content = to_mozilla_format jar in
393393+394394+ try
395395+ Eio.Path.save ~create:(`Or_truncate 0o600) path content;
396396+ Log.debug (fun m -> m "Cookies saved successfully")
397397+ with exn ->
398398+ Log.err (fun m -> m "Failed to save cookies: %s" (Printexc.to_string exn))
+181
stack/cookeio/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 *)