atproto libraries implementation in ocaml

release: v0.1.2 - migrate examples to hcs, fix package dependencies

- Bump version to 0.1.2
- Remove examples from atproto package (fix missing cohttp-eio error)
- Migrate all examples from cohttp-eio to hcs library:
- bsky_bot: use Hcs.Client for HTTP requests
- identity_tool: use Hcs.Client for HTTP requests
- repo_inspector: use Hcs.Client with 100MB response limit
- feed_generator: use Hcs.Websocket for firehose connection
- Update dune files to use hcs instead of cohttp-eio/tls-eio
- All 311 tests pass

+1 -1
.beads/.local_version
··· 1 - 0.29.0 1 + 0.43.0
+1 -1
atproto-api.opam
··· 1 1 # This file is generated by dune, edit dune-project instead 2 2 opam-version: "2.0" 3 - version: "0.1.1" 3 + version: "0.1.2" 4 4 synopsis: "High-level API client for AT Protocol" 5 5 description: 6 6 "User-friendly API client for AT Protocol with session management, posting, and social actions"
+1 -1
atproto-crypto.opam
··· 1 1 # This file is generated by dune, edit dune-project instead 2 2 opam-version: "2.0" 3 - version: "0.1.1" 3 + version: "0.1.2" 4 4 synopsis: "Cryptographic operations for AT Protocol" 5 5 description: 6 6 "P-256 and K-256 elliptic curve support with low-S normalization, did:key encoding"
+1 -1
atproto-effects.opam
··· 1 1 # This file is generated by dune, edit dune-project instead 2 2 opam-version: "2.0" 3 - version: "0.1.1" 3 + version: "0.1.2" 4 4 synopsis: "Effects-based I/O abstraction for AT Protocol" 5 5 description: 6 6 "Unified effect types for HTTP, DNS, WebSocket, time, and random operations. Allows libraries to be runtime-agnostic."
+1 -1
atproto-identity.opam
··· 1 1 # This file is generated by dune, edit dune-project instead 2 2 opam-version: "2.0" 3 - version: "0.1.1" 3 + version: "0.1.2" 4 4 synopsis: "DID and Handle resolution for AT Protocol" 5 5 description: 6 6 "DID and Handle resolution including did:plc, did:web, and DNS/HTTPS handle resolution"
+1 -1
atproto-ipld.opam
··· 1 1 # This file is generated by dune, edit dune-project instead 2 2 opam-version: "2.0" 3 - version: "0.1.1" 3 + version: "0.1.2" 4 4 synopsis: "IPLD support for AT Protocol" 5 5 description: 6 6 "Content Identifiers (CID) and DAG-CBOR encoding for AT Protocol"
+1 -1
atproto-json.opam
··· 1 1 # This file is generated by dune, edit dune-project instead 2 2 opam-version: "2.0" 3 - version: "0.1.1" 3 + version: "0.1.2" 4 4 synopsis: "JSON utilities for AT Protocol" 5 5 description: 6 6 "JSON wrapper used across AT Protocol packages (currently backed by simdjsont)"
+1 -1
atproto-lexicon.opam
··· 1 1 # This file is generated by dune, edit dune-project instead 2 2 opam-version: "2.0" 3 - version: "0.1.1" 3 + version: "0.1.2" 4 4 synopsis: "Lexicon schema support for AT Protocol" 5 5 description: "Lexicon schema parsing and validation for AT Protocol" 6 6 maintainer: ["Gabriel Díaz"]
+1 -1
atproto-mst.opam
··· 1 1 # This file is generated by dune, edit dune-project instead 2 2 opam-version: "2.0" 3 - version: "0.1.1" 3 + version: "0.1.2" 4 4 synopsis: "Merkle Search Tree for AT Protocol" 5 5 description: 6 6 "Content-addressed key-value storage for AT Protocol repositories"
+1 -1
atproto-multibase.opam
··· 1 1 # This file is generated by dune, edit dune-project instead 2 2 opam-version: "2.0" 3 - version: "0.1.1" 3 + version: "0.1.2" 4 4 synopsis: "Base encoding utilities for AT Protocol" 5 5 description: 6 6 "Multibase encoding/decoding including base32-sortable for TIDs and base58btc for did:key"
+1 -1
atproto-repo.opam
··· 1 1 # This file is generated by dune, edit dune-project instead 2 2 opam-version: "2.0" 3 - version: "0.1.1" 3 + version: "0.1.2" 4 4 synopsis: "Repository support for AT Protocol" 5 5 description: 6 6 "Repository structure, commits, and record operations for AT Protocol"
+1 -1
atproto-sync.opam
··· 1 1 # This file is generated by dune, edit dune-project instead 2 2 opam-version: "2.0" 3 - version: "0.1.1" 3 + version: "0.1.2" 4 4 synopsis: "Repository sync and event streams for AT Protocol" 5 5 description: 6 6 "Firehose event stream client and repository synchronization for AT Protocol"
+1 -1
atproto-syntax.opam
··· 1 1 # This file is generated by dune, edit dune-project instead 2 2 opam-version: "2.0" 3 - version: "0.1.1" 3 + version: "0.1.2" 4 4 synopsis: "Syntax validation for AT Protocol identifiers" 5 5 description: 6 6 "Parser-based validation for handles, DIDs, NSIDs, TIDs, AT-URIs, and other AT Protocol syntax"
+1 -1
atproto-xrpc.opam
··· 1 1 # This file is generated by dune, edit dune-project instead 2 2 opam-version: "2.0" 3 - version: "0.1.1" 3 + version: "0.1.2" 4 4 synopsis: "XRPC client/server for AT Protocol" 5 5 description: 6 6 "XRPC HTTP API protocol implementation for AT Protocol client-server communication"
+1 -1
atproto.opam
··· 1 1 # This file is generated by dune, edit dune-project instead 2 2 opam-version: "2.0" 3 - version: "0.1.1" 3 + version: "0.1.2" 4 4 synopsis: "AT Protocol implementation in OCaml" 5 5 description: 6 6 "Complete AT Protocol implementation including syntax validation, cryptography, IPLD, and identity resolution"
-2
bin/dune
··· 1 1 (executable 2 - (public_name atproto) 3 - (package atproto) 4 2 (name main) 5 3 (libraries atproto))
+1 -1
dune-project
··· 2 2 3 3 (name atproto) 4 4 5 - (version 0.1.1) 5 + (version 0.1.2) 6 6 7 7 (generate_opam_files true) 8 8
+62 -77
examples/bsky_bot/bsky_bot.ml
··· 5 5 module Richtext = Atproto_api.Richtext 6 6 module Client = Atproto_xrpc.Client 7 7 8 - (** {1 HTTP Client with cohttp-eio} *) 9 - 10 - let http_request ~sw ~client (req : Client.request) : Client.response = 11 - let headers = Cohttp.Header.of_list req.headers in 12 - let body = 13 - match req.body with 14 - | Some b -> Cohttp_eio.Body.of_string b 15 - | None -> Cohttp_eio.Body.of_string "" 8 + let http_request ~hcs_client (req : Client.request) : Client.response = 9 + let url = Uri.to_string req.uri in 10 + let result = 11 + match req.meth with 12 + | `GET -> Hcs.Client.request hcs_client url 13 + | `POST -> 14 + let body = Option.value ~default:"" req.body in 15 + Hcs.Client.request_post hcs_client url ~body 16 16 in 17 - let meth = match req.meth with `GET -> `GET | `POST -> `POST in 18 - try 19 - let resp, resp_body = 20 - Cohttp_eio.Client.call ~sw client meth req.uri ~headers ~body 21 - in 22 - let status = Cohttp.Response.status resp |> Cohttp.Code.code_of_status in 23 - let headers = Cohttp.Response.headers resp |> Cohttp.Header.to_list in 24 - let body = 25 - Eio.Buf_read.(of_flow ~max_size:(10 * 1024 * 1024) resp_body |> take_all) 26 - in 27 - { Client.status; headers; body } 28 - with e -> { Client.status = 0; headers = []; body = Printexc.to_string e } 29 - 30 - (** {1 Effect Handler} *) 17 + match result with 18 + | Ok resp -> 19 + { Client.status = resp.status; headers = resp.headers; body = resp.body } 20 + | Error e -> 21 + let msg = 22 + match e with 23 + | Hcs.Client.Connection_failed s -> "Connection failed: " ^ s 24 + | Hcs.Client.Tls_error s -> "TLS error: " ^ s 25 + | Hcs.Client.Protocol_error s -> "Protocol error: " ^ s 26 + | Hcs.Client.Timeout -> "Timeout" 27 + | Hcs.Client.Invalid_response s -> "Invalid response: " ^ s 28 + | Hcs.Client.Too_many_redirects -> "Too many redirects" 29 + in 30 + { Client.status = 0; headers = []; body = msg } 31 31 32 - let run_with_eio ~sw ~client f = 32 + let run_with_hcs ~hcs_client f = 33 33 Effect.Deep.try_with f () 34 34 { 35 35 effc = ··· 38 38 | Client.Http_request req -> 39 39 Some 40 40 (fun (k : (a, _) Effect.Deep.continuation) -> 41 - Effect.Deep.continue k (http_request ~sw ~client req)) 41 + Effect.Deep.continue k (http_request ~hcs_client req)) 42 42 | _ -> None); 43 43 } 44 44 45 - (** {1 Commands} *) 46 - 47 - let login ~sw ~client ~pds ~identifier ~password = 48 - run_with_eio ~sw ~client (fun () -> 45 + let login ~hcs_client ~pds ~identifier ~password = 46 + run_with_hcs ~hcs_client (fun () -> 49 47 let agent = Agent.create_from_url ~url:pds in 50 48 match Agent.login agent ~identifier ~password with 51 49 | Error e -> ··· 56 54 (Option.value ~default:"?" (Agent.handle agent)); 57 55 Some agent) 58 56 59 - let post ~sw ~client agent text = 60 - run_with_eio ~sw ~client (fun () -> 57 + let post ~hcs_client agent text = 58 + run_with_hcs ~hcs_client (fun () -> 61 59 let rt = Richtext.detect_facets text in 62 60 match Agent.create_post_richtext agent ~richtext:rt () with 63 61 | Error e -> ··· 67 65 Printf.printf "Posted: %s\n" r.uri; 68 66 0) 69 67 70 - let timeline ~sw ~client agent limit = 71 - run_with_eio ~sw ~client (fun () -> 68 + let timeline ~hcs_client agent limit = 69 + run_with_hcs ~hcs_client (fun () -> 72 70 match Agent.get_timeline agent ~limit () with 73 71 | Error e -> 74 72 Printf.printf "Timeline failed: %s\n" (Agent.error_to_string e); ··· 81 79 feed.items; 82 80 0) 83 81 84 - let profile ~sw ~client agent actor = 85 - run_with_eio ~sw ~client (fun () -> 82 + let profile ~hcs_client agent actor = 83 + run_with_hcs ~hcs_client (fun () -> 86 84 match Agent.get_profile agent ~actor with 87 85 | Error e -> 88 86 Printf.printf "Profile failed: %s\n" (Agent.error_to_string e); ··· 95 93 p.followers_count p.follows_count p.posts_count; 96 94 0) 97 95 98 - let follow ~sw ~client agent did = 99 - run_with_eio ~sw ~client (fun () -> 96 + let follow ~hcs_client agent did = 97 + run_with_hcs ~hcs_client (fun () -> 100 98 match Agent.follow agent ~did with 101 99 | Error e -> 102 100 Printf.printf "Follow failed: %s\n" (Agent.error_to_string e); ··· 105 103 Printf.printf "Followed: %s\n" r.uri; 106 104 0) 107 105 108 - (** {1 CLI} *) 109 - 110 106 type cmd = 111 107 | Post of string 112 108 | Timeline of int ··· 141 137 Mirage_crypto_rng_unix.use_default (); 142 138 Eio_main.run @@ fun env -> 143 139 Eio.Switch.run @@ fun sw -> 144 - let https_config = 145 - match 146 - Tls.Config.client ~authenticator:(fun ?ip:_ ~host:_ _ -> Ok None) () 147 - with 148 - | Ok c -> c 149 - | Error (`Msg m) -> failwith m 140 + let config = Hcs.Client.default_config |> Hcs.Client.with_insecure_tls in 141 + let hcs_client = 142 + Hcs.Client.create ~sw ~net:(Eio.Stdenv.net env) 143 + ~clock:(Eio.Stdenv.clock env) ~config () 150 144 in 151 - let https uri socket = 152 - let tls_host = 153 - match Uri.host uri with 154 - | Some h -> ( 155 - match Domain_name.of_string h with 156 - | Error _ -> Option.None 157 - | Ok dn -> Domain_name.host dn |> Result.to_option) 158 - | Option.None -> Option.None 159 - in 160 - Tls_eio.client_of_flow https_config ?host:tls_host socket 161 - in 162 - let client = Cohttp_eio.Client.make ~https:(Some https) env#net in 163 145 let pds, identifier, password, cmd = 164 146 Climate.Command.run ~program_name:(Climate.Program_name.Literal "bsky_bot") 165 147 (Climate.Command.singleton ~doc:"Bluesky bot - post, timeline, follow" cli) 166 148 in 167 - match cmd with 168 - | None -> 169 - Printf.printf 170 - "Usage: bsky_bot --user USER --password PASS [--post \ 171 - TEXT|--timeline|--profile ACTOR|--follow DID]\n"; 172 - exit 0 173 - | _ -> ( 174 - match (identifier, password) with 175 - | Some id, Some pw -> ( 176 - match login ~sw ~client ~pds ~identifier:id ~password:pw with 177 - | None -> exit 1 178 - | Some agent -> 179 - exit 180 - (match cmd with 181 - | Post t -> post ~sw ~client agent t 182 - | Timeline n -> timeline ~sw ~client agent n 183 - | Profile a -> profile ~sw ~client agent a 184 - | Follow d -> follow ~sw ~client agent d 149 + let result = 150 + match cmd with 151 + | None -> 152 + Printf.printf 153 + "Usage: bsky_bot --user USER --password PASS [--post \ 154 + TEXT|--timeline|--profile ACTOR|--follow DID]\n"; 155 + 0 156 + | _ -> ( 157 + match (identifier, password) with 158 + | Some id, Some pw -> ( 159 + match login ~hcs_client ~pds ~identifier:id ~password:pw with 160 + | None -> 1 161 + | Some agent -> ( 162 + match cmd with 163 + | Post t -> post ~hcs_client agent t 164 + | Timeline n -> timeline ~hcs_client agent n 165 + | Profile a -> profile ~hcs_client agent a 166 + | Follow d -> follow ~hcs_client agent d 185 167 | None -> 0)) 186 - | _ -> 187 - Printf.printf "Error: --user and --password required\n"; 188 - exit 1) 168 + | _ -> 169 + Printf.printf "Error: --user and --password required\n"; 170 + 1) 171 + in 172 + Hcs.Client.close hcs_client; 173 + exit result
+1 -4
examples/bsky_bot/dune
··· 1 1 (executable 2 2 (name bsky_bot) 3 - (public_name bsky_bot) 4 - (package atproto) 5 3 (libraries 6 4 atproto-api 7 5 atproto-xrpc 8 6 atproto-effects 9 7 climate 10 8 eio_main 11 - cohttp-eio 12 - tls-eio 9 + hcs 13 10 mirage-crypto-rng.unix 14 11 uri))
+2 -7
examples/feed_generator/dune
··· 1 1 (executable 2 2 (name feed_generator) 3 - (public_name feed_generator) 4 - (package atproto) 5 3 (libraries 6 4 atproto-sync 7 5 atproto-ipld ··· 9 7 uri 10 8 eio 11 9 eio_main 12 - tls-eio 13 - ca-certs-nss 14 - mirage-crypto-rng.unix 15 - base64 16 - cstruct)) 10 + hcs 11 + mirage-crypto-rng.unix))
+35 -243
examples/feed_generator/feed_generator.ml
··· 178 178 | Firehose.Tombstone _ -> List.mem Tombstones filters 179 179 | Firehose.Info _ | Firehose.StreamError _ -> true) 180 180 181 - (** {1 Keyword Filtering} *) 182 - 183 181 let contains_keyword keyword text = 184 182 let kw = String.lowercase_ascii keyword in 185 183 let txt = String.lowercase_ascii text in ··· 216 214 && text_matches_keyword kw blocks op) 217 215 evt.ops 218 216 | _ -> false) 219 - 220 - (** {1 Feed Skeleton} *) 221 217 222 218 let max_feed_size = 1000 223 219 let feed_posts : string Queue.t = Queue.create () ··· 264 260 Printf.printf " ]\n}\n" 265 261 end 266 262 267 - (** {1 WebSocket Client} *) 268 - 269 - module Ws = struct 270 - type conn = { 271 - socket : Tls_eio.t; 272 - buf : bytes; 273 - mutable buf_len : int; 274 - frag_buf : Buffer.t; 275 - mutable frag_opcode : int; 276 - } 277 - 278 - let ws_nonce () = 279 - let b = Bytes.create 16 in 280 - for i = 0 to 15 do 281 - Bytes.set b i (Char.chr (Random.int 256)) 282 - done; 283 - Base64.encode_exn (Bytes.to_string b) 284 - 285 - let parse_frame_header buf off len = 286 - if len < 2 then None 287 - else 288 - let b0, b1 = 289 - (Char.code (Bytes.get buf off), Char.code (Bytes.get buf (off + 1))) 290 - in 291 - let fin, opcode = (b0 land 0x80 <> 0, b0 land 0x0f) in 292 - let masked, plen = (b1 land 0x80 <> 0, b1 land 0x7f) in 293 - let hlen, plen = 294 - if plen = 126 then 295 - if len < 4 then (0, -1) 296 - else 297 - ( 4, 298 - (Char.code (Bytes.get buf (off + 2)) lsl 8) 299 - lor Char.code (Bytes.get buf (off + 3)) ) 300 - else if plen = 127 then 301 - if len < 10 then (0, -1) 302 - else 303 - ( 10, 304 - (Char.code (Bytes.get buf (off + 6)) lsl 24) 305 - lor (Char.code (Bytes.get buf (off + 7)) lsl 16) 306 - lor (Char.code (Bytes.get buf (off + 8)) lsl 8) 307 - lor Char.code (Bytes.get buf (off + 9)) ) 308 - else (2, plen) 309 - in 310 - if plen < 0 then None 311 - else 312 - let mlen = if masked then 4 else 0 in 313 - if len < hlen + mlen then None 314 - else 315 - Some 316 - ( fin, 317 - opcode, 318 - plen, 319 - (if masked then Some (Bytes.sub buf (off + hlen) 4) else None), 320 - hlen + mlen ) 321 - 322 - let make_frame opcode payload = 323 - let plen = String.length payload in 324 - let mask = Bytes.init 4 (fun _ -> Char.chr (Random.int 256)) in 325 - let hdr = 326 - if plen < 126 then ( 327 - let h = Bytes.create 6 in 328 - Bytes.set h 0 (Char.chr (0x80 lor opcode)); 329 - Bytes.set h 1 (Char.chr (0x80 lor plen)); 330 - Bytes.blit mask 0 h 2 4; 331 - Bytes.to_string h) 332 - else if plen < 65536 then ( 333 - let h = Bytes.create 8 in 334 - Bytes.set h 0 (Char.chr (0x80 lor opcode)); 335 - Bytes.set h 1 (Char.chr (0x80 lor 126)); 336 - Bytes.set h 2 (Char.chr ((plen lsr 8) land 0xff)); 337 - Bytes.set h 3 (Char.chr (plen land 0xff)); 338 - Bytes.blit mask 0 h 4 4; 339 - Bytes.to_string h) 340 - else 341 - let h = Bytes.create 14 in 342 - Bytes.set h 0 (Char.chr (0x80 lor opcode)); 343 - Bytes.set h 1 (Char.chr (0x80 lor 127)); 344 - for i = 2 to 5 do 345 - Bytes.set h i '\000' 346 - done; 347 - Bytes.set h 6 (Char.chr ((plen lsr 24) land 0xff)); 348 - Bytes.set h 7 (Char.chr ((plen lsr 16) land 0xff)); 349 - Bytes.set h 8 (Char.chr ((plen lsr 8) land 0xff)); 350 - Bytes.set h 9 (Char.chr (plen land 0xff)); 351 - Bytes.blit mask 0 h 10 4; 352 - Bytes.to_string h 353 - in 354 - let masked = 355 - Bytes.mapi 356 - (fun i c -> 357 - Char.chr (Char.code c lxor Char.code (Bytes.get mask (i mod 4)))) 358 - (Bytes.of_string payload) 359 - in 360 - hdr ^ Bytes.to_string masked 361 - 362 - let connect ~net ~sw uri = 363 - let host = Uri.host uri |> Option.value ~default:"localhost" in 364 - let port = Uri.port uri |> Option.value ~default:443 in 365 - let auth = 366 - match Ca_certs_nss.authenticator () with 367 - | Ok a -> a 368 - | Error (`Msg m) -> failwith m 369 - in 370 - let tls_config = 371 - match 372 - Tls.Config.client ~authenticator:auth ~alpn_protocols:[ "http/1.1" ] () 373 - with 374 - | Ok c -> c 375 - | Error (`Msg m) -> failwith m 376 - in 377 - let hostname = 378 - match Domain_name.of_string host with 379 - | Error _ -> None 380 - | Ok dn -> ( 381 - match Domain_name.host dn with Ok h -> Some h | Error _ -> None) 382 - in 383 - let addr = 384 - match 385 - Eio.Net.getaddrinfo_stream net host ~service:(string_of_int port) 386 - with 387 - | [] -> failwith ("DNS failed: " ^ host) 388 - | a :: _ -> a 389 - in 390 - let socket = Eio.Net.connect ~sw net addr in 391 - let tls_socket = Tls_eio.client_of_flow tls_config ?host:hostname socket in 392 - let nonce = ws_nonce () in 393 - Eio.Flow.copy_string 394 - (Printf.sprintf 395 - "GET %s HTTP/1.1\r\n\ 396 - Host: %s\r\n\ 397 - Upgrade: websocket\r\n\ 398 - Connection: Upgrade\r\n\ 399 - Sec-WebSocket-Key: %s\r\n\ 400 - Sec-WebSocket-Version: 13\r\n\ 401 - \r\n" 402 - (Uri.path_and_query uri) host nonce) 403 - tls_socket; 404 - let resp_buf = Cstruct.create 4096 in 405 - let n = Eio.Flow.single_read tls_socket resp_buf in 406 - let resp = Cstruct.to_string (Cstruct.sub resp_buf 0 n) in 407 - if not (String.length resp >= 12 && String.sub resp 0 12 = "HTTP/1.1 101") 408 - then 409 - failwith 410 - ("WebSocket upgrade failed: " 411 - ^ String.sub resp 0 (min 50 (String.length resp))); 412 - { 413 - socket = tls_socket; 414 - buf = Bytes.create 1048576; 415 - buf_len = 0; 416 - frag_buf = Buffer.create 65536; 417 - frag_opcode = 0; 418 - } 419 - 420 - let read_more conn = 421 - let cs = Cstruct.create 65536 in 422 - let n = Eio.Flow.single_read conn.socket cs in 423 - Cstruct.blit_to_bytes cs 0 conn.buf conn.buf_len n; 424 - conn.buf_len <- conn.buf_len + n 425 - 426 - let recv conn = 427 - let rec read_frame () = 428 - if conn.buf_len < 2 then ( 429 - read_more conn; 430 - read_frame ()) 431 - else 432 - match parse_frame_header conn.buf 0 conn.buf_len with 433 - | None -> 434 - read_more conn; 435 - read_frame () 436 - | Some (fin, opcode, plen, mask, hlen) -> ( 437 - let total = hlen + plen in 438 - while conn.buf_len < total do 439 - read_more conn 440 - done; 441 - let payload = Bytes.sub conn.buf hlen plen in 442 - (match mask with 443 - | Some k -> 444 - for i = 0 to plen - 1 do 445 - Bytes.set payload i 446 - (Char.chr 447 - (Char.code (Bytes.get payload i) 448 - lxor Char.code (Bytes.get k (i mod 4)))) 449 - done 450 - | None -> ()); 451 - let rem = conn.buf_len - total in 452 - if rem > 0 then Bytes.blit conn.buf total conn.buf 0 rem; 453 - conn.buf_len <- rem; 454 - match opcode with 455 - | 0x0 -> 456 - Buffer.add_bytes conn.frag_buf payload; 457 - if fin then ( 458 - let data = Buffer.contents conn.frag_buf in 459 - Buffer.clear conn.frag_buf; 460 - if conn.frag_opcode = 0x2 then Ok data else read_frame ()) 461 - else read_frame () 462 - | 0x1 -> 463 - if not fin then ( 464 - Buffer.clear conn.frag_buf; 465 - Buffer.add_bytes conn.frag_buf payload; 466 - conn.frag_opcode <- 0x1); 467 - read_frame () 468 - | 0x2 -> 469 - if fin then Ok (Bytes.to_string payload) 470 - else ( 471 - Buffer.clear conn.frag_buf; 472 - Buffer.add_bytes conn.frag_buf payload; 473 - conn.frag_opcode <- 0x2; 474 - read_frame ()) 475 - | 0x8 -> Error "Connection closed" 476 - | 0x9 -> 477 - Eio.Flow.copy_string 478 - (make_frame 0xA (Bytes.to_string payload)) 479 - conn.socket; 480 - read_frame () 481 - | _ -> read_frame ()) 482 - in 483 - try read_frame () with exn -> Error (Printexc.to_string exn) 484 - 485 - let close _conn = () 486 - end 487 - 488 - let with_websocket ~net ~sw f = 263 + let with_websocket ~sw ~net f = 489 264 let open Effect.Deep in 490 265 try_with f () 491 266 { ··· 495 270 | Firehose.Ws_connect uri -> 496 271 Some 497 272 (fun (k : (a, _) continuation) -> 498 - try 499 - continue k 500 - (Ok 501 - (Obj.magic (Ws.connect ~net ~sw uri) 502 - : Firehose.websocket)) 503 - with exn -> continue k (Error (Printexc.to_string exn))) 273 + let url = Uri.to_string uri in 274 + match Hcs.Websocket.connect ~sw ~net url with 275 + | Ok ws -> continue k (Ok (Obj.magic ws : Firehose.websocket)) 276 + | Error e -> 277 + let msg = 278 + match e with 279 + | Hcs.Websocket.Connection_closed -> "Connection closed" 280 + | Hcs.Websocket.Protocol_error s -> 281 + "Protocol error: " ^ s 282 + | Hcs.Websocket.Io_error s -> "IO error: " ^ s 283 + | Hcs.Websocket.Payload_too_large n -> 284 + "Payload too large: " ^ string_of_int n 285 + in 286 + continue k (Error msg)) 504 287 | Firehose.Ws_recv ws -> 505 - Some (fun k -> continue k (Ws.recv (Obj.magic ws : Ws.conn))) 288 + Some 289 + (fun k -> 290 + let hcs_ws = (Obj.magic ws : Hcs.Websocket.t) in 291 + match Hcs.Websocket.recv hcs_ws with 292 + | Ok frame -> 293 + if frame.opcode = Hcs.Websocket.Opcode.Binary then 294 + continue k (Ok frame.content) 295 + else if frame.opcode = Hcs.Websocket.Opcode.Close then 296 + continue k (Error "Connection closed") 297 + else continue k (Ok frame.content) 298 + | Error Hcs.Websocket.Connection_closed -> 299 + continue k (Error "Connection closed") 300 + | Error (Hcs.Websocket.Protocol_error s) -> 301 + continue k (Error ("Protocol error: " ^ s)) 302 + | Error (Hcs.Websocket.Io_error s) -> 303 + continue k (Error ("IO error: " ^ s)) 304 + | Error (Hcs.Websocket.Payload_too_large n) -> 305 + continue k 306 + (Error ("Payload too large: " ^ string_of_int n))) 506 307 | Firehose.Ws_close ws -> 507 308 Some 508 309 (fun k -> 509 - Ws.close (Obj.magic ws); 310 + Hcs.Websocket.close (Obj.magic ws); 510 311 continue k ()) 511 312 | _ -> None); 512 313 } 513 314 514 - (** {1 Configuration} *) 515 - 516 315 type config = { 517 316 cursor : int64 option; 518 317 limit : int option; ··· 563 362 Climate.Command.singleton 564 363 ~doc:"AT Protocol firehose client and feed generator" config_parser 565 364 566 - (** {1 Stats} *) 567 - 568 365 type stats = { 569 366 mutable total : int; 570 367 mutable matched : int; ··· 582 379 let stats = { total = 0; matched = 0; last_seq = 0L; start = 0. } 583 380 let interrupted = ref false 584 381 585 - (** {1 Main} *) 586 - 587 382 let run ~net ~sw config = 588 383 let uri = 589 384 Uri.of_string "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos" ··· 601 396 (match Firehose.event_seq event with 602 397 | Some s -> stats.last_seq <- s 603 398 | None -> ()); 604 - (* Collect posts for skeleton if enabled *) 605 399 (if config.skeleton then 606 400 match event with 607 401 | Firehose.Commit evt -> collect_post_uris evt config.keyword 608 402 | _ -> ()); 609 - (* Check filters *) 610 403 let type_match = event_matches config.filters event in 611 404 let keyword_match = event_matches_keyword config.keyword event in 612 405 if type_match && keyword_match then begin ··· 642 435 Sys.set_signal Sys.sigint (Sys.Signal_handle (fun _ -> interrupted := true)); 643 436 stats.start <- Unix.gettimeofday (); 644 437 Mirage_crypto_rng_unix.use_default (); 645 - Random.self_init (); 646 438 (try 647 439 Eio_main.run @@ fun env -> 648 440 Eio.Switch.run @@ fun sw -> run ~net:(Eio.Stdenv.net env) ~sw config
+1 -4
examples/identity_tool/dune
··· 1 1 (executable 2 2 (name identity_tool) 3 - (public_name identity_tool) 4 - (package atproto) 5 3 (libraries 6 4 atproto-identity 7 5 atproto-syntax 8 6 climate 9 7 eio_main 10 - cohttp-eio 11 - tls-eio 8 + hcs 12 9 mirage-crypto-rng.unix 13 10 uri))
+36 -49
examples/identity_tool/identity_tool.ml
··· 5 5 open Atproto_syntax 6 6 open Atproto_identity 7 7 8 - (** {1 HTTP Client with cohttp-eio} *) 8 + let http_get ~hcs_client uri = 9 + let url = Uri.to_string uri in 10 + match Hcs.Client.request hcs_client url with 11 + | Ok resp -> Did_resolver.{ status = resp.status; body = resp.body } 12 + | Error e -> 13 + let msg = 14 + match e with 15 + | Hcs.Client.Connection_failed s -> "Connection failed: " ^ s 16 + | Hcs.Client.Tls_error s -> "TLS error: " ^ s 17 + | Hcs.Client.Protocol_error s -> "Protocol error: " ^ s 18 + | Hcs.Client.Timeout -> "Timeout" 19 + | Hcs.Client.Invalid_response s -> "Invalid response: " ^ s 20 + | Hcs.Client.Too_many_redirects -> "Too many redirects" 21 + in 22 + Did_resolver.{ status = 0; body = msg } 9 23 10 - let http_get ~sw ~client uri = 11 - try 12 - let resp, resp_body = Cohttp_eio.Client.call ~sw client `GET uri in 13 - let status = Cohttp.Response.status resp |> Cohttp.Code.code_of_status in 14 - let body = 15 - Eio.Buf_read.(of_flow ~max_size:(10 * 1024 * 1024) resp_body |> take_all) 16 - in 17 - Did_resolver.{ status; body } 18 - with e -> Did_resolver.{ status = 0; body = Printexc.to_string e } 19 - 20 - (** {1 Effect Handler} *) 21 - 22 - let run_with_eio ~sw ~client f = 24 + let run_with_hcs ~hcs_client f = 23 25 Effect.Deep.try_with f () 24 26 { 25 27 effc = ··· 28 30 | Did_resolver.Http_get uri -> 29 31 Some 30 32 (fun (k : (a, _) Effect.Deep.continuation) -> 31 - Effect.Deep.continue k (http_get ~sw ~client uri)) 33 + Effect.Deep.continue k (http_get ~hcs_client uri)) 32 34 | Handle_resolver.Http_get uri -> 33 35 Some 34 36 (fun k -> 35 - let r = http_get ~sw ~client uri in 37 + let r = http_get ~hcs_client uri in 36 38 Effect.Deep.continue k 37 39 Handle_resolver.{ status = r.status; body = r.body }) 38 40 | Handle_resolver.Dns_txt _ -> ··· 41 43 | _ -> None); 42 44 } 43 45 44 - (** {1 Commands} *) 45 - 46 - let resolve_handle ~sw ~client h = 46 + let resolve_handle ~hcs_client h = 47 47 match Handle.of_string h with 48 48 | Error _ -> 49 49 Printf.printf "Error: Invalid handle\n"; 50 50 1 51 51 | Ok handle -> 52 - run_with_eio ~sw ~client (fun () -> 52 + run_with_hcs ~hcs_client (fun () -> 53 53 match Handle_resolver.resolve handle with 54 54 | Error e -> 55 55 Printf.printf "Error: %s\n" (Handle_resolver.error_to_string e); ··· 59 59 (Did.to_string did); 60 60 0) 61 61 62 - let resolve_did ~sw ~client d = 62 + let resolve_did ~hcs_client d = 63 63 match Did.of_string d with 64 64 | Error _ -> 65 65 Printf.printf "Error: Invalid DID\n"; 66 66 1 67 67 | Ok did -> 68 - run_with_eio ~sw ~client (fun () -> 68 + run_with_hcs ~hcs_client (fun () -> 69 69 match Did_resolver.resolve_did did with 70 70 | Error e -> 71 71 Printf.printf "Error: %s\n" (Did_resolver.error_to_string e); ··· 87 87 doc.service; 88 88 0) 89 89 90 - let verify ~sw ~client id = 90 + let verify ~hcs_client id = 91 91 let is_did = String.length id > 4 && String.sub id 0 4 = "did:" in 92 - run_with_eio ~sw ~client (fun () -> 92 + run_with_hcs ~hcs_client (fun () -> 93 93 let result = 94 94 if is_did then 95 95 match Did.of_string id with ··· 116 116 v.pds_endpoint; 117 117 0) 118 118 119 - (** {1 CLI} *) 120 - 121 119 type mode = Resolve_handle | Resolve_did | Verify 122 120 123 121 let cli = ··· 136 134 Mirage_crypto_rng_unix.use_default (); 137 135 Eio_main.run @@ fun env -> 138 136 Eio.Switch.run @@ fun sw -> 139 - let https_config = 140 - match 141 - Tls.Config.client ~authenticator:(fun ?ip:_ ~host:_ _ -> Ok None) () 142 - with 143 - | Ok c -> c 144 - | Error (`Msg m) -> failwith m 145 - in 146 - let https uri socket = 147 - let tls_host = 148 - match Uri.host uri with 149 - | Some h -> ( 150 - match Domain_name.of_string h with 151 - | Error _ -> None 152 - | Ok dn -> Domain_name.host dn |> Result.to_option) 153 - | None -> None 154 - in 155 - Tls_eio.client_of_flow https_config ?host:tls_host socket 137 + let config = Hcs.Client.default_config |> Hcs.Client.with_insecure_tls in 138 + let hcs_client = 139 + Hcs.Client.create ~sw ~net:(Eio.Stdenv.net env) 140 + ~clock:(Eio.Stdenv.clock env) ~config () 156 141 in 157 - let client = Cohttp_eio.Client.make ~https:(Some https) env#net in 158 142 let mode, id = 159 143 Climate.Command.run 160 144 ~program_name:(Climate.Program_name.Literal "identity_tool") 161 145 (Climate.Command.singleton ~doc:"AT Protocol identity lookup tool" cli) 162 146 in 163 - exit 164 - (match mode with 165 - | Resolve_handle -> resolve_handle ~sw ~client id 166 - | Resolve_did -> resolve_did ~sw ~client id 167 - | Verify -> verify ~sw ~client id) 147 + let result = 148 + match mode with 149 + | Resolve_handle -> resolve_handle ~hcs_client id 150 + | Resolve_did -> resolve_did ~hcs_client id 151 + | Verify -> verify ~hcs_client id 152 + in 153 + Hcs.Client.close hcs_client; 154 + exit result
+1 -5
examples/repo_inspector/dune
··· 1 1 (executable 2 2 (name repo_inspector) 3 - (public_name repo_inspector) 4 - (package atproto) 5 3 (libraries 6 4 atproto-repo 7 5 atproto-mst ··· 11 9 atproto-crypto 12 10 climate 13 11 eio_main 14 - cohttp-eio 15 - tls-eio 16 - ca-certs-nss 12 + hcs 17 13 base64 18 14 mirage-crypto-rng.unix 19 15 uri))
+35 -57
examples/repo_inspector/repo_inspector.ml
··· 18 18 module Did = Atproto_syntax.Did 19 19 module Did_resolver = Atproto_identity.Did_resolver 20 20 21 - (** {1 HTTP Client with cohttp-eio} *) 22 - 23 - let http_get ~sw ~client uri = 24 - try 25 - let resp, resp_body = Cohttp_eio.Client.call ~sw client `GET uri in 26 - let status = Cohttp.Response.status resp |> Cohttp.Code.code_of_status in 27 - let body = 28 - Eio.Buf_read.(of_flow ~max_size:(100 * 1024 * 1024) resp_body |> take_all) 29 - in 30 - (status, body) 31 - with e -> (0, Printexc.to_string e) 32 - 33 - (** {1 DID Resolution Effect Handler} *) 21 + let http_get ~hcs_client uri = 22 + let url = Uri.to_string uri in 23 + match Hcs.Client.request hcs_client url with 24 + | Ok resp -> (resp.status, resp.body) 25 + | Error e -> 26 + let msg = 27 + match e with 28 + | Hcs.Client.Connection_failed s -> "Connection failed: " ^ s 29 + | Hcs.Client.Tls_error s -> "TLS error: " ^ s 30 + | Hcs.Client.Protocol_error s -> "Protocol error: " ^ s 31 + | Hcs.Client.Timeout -> "Timeout" 32 + | Hcs.Client.Invalid_response s -> "Invalid response: " ^ s 33 + | Hcs.Client.Too_many_redirects -> "Too many redirects" 34 + in 35 + (0, msg) 34 36 35 - let run_with_resolver ~sw ~client f = 37 + let run_with_resolver ~hcs_client f = 36 38 Effect.Deep.try_with f () 37 39 { 38 40 effc = ··· 41 43 | Did_resolver.Http_get uri -> 42 44 Some 43 45 (fun (k : (a, _) Effect.Deep.continuation) -> 44 - let status, body = http_get ~sw ~client uri in 46 + let status, body = http_get ~hcs_client uri in 45 47 Effect.Deep.continue k Did_resolver.{ status; body }) 46 48 | _ -> None); 47 49 } 48 50 49 - (** {1 PDS Resolution} *) 50 - 51 - let resolve_pds ~sw ~client did_str = 51 + let resolve_pds ~hcs_client did_str = 52 52 match Did.of_string did_str with 53 53 | Error _ -> Error "Invalid DID format" 54 54 | Ok did -> 55 - run_with_resolver ~sw ~client (fun () -> 55 + run_with_resolver ~hcs_client (fun () -> 56 56 match Did_resolver.resolve_did did with 57 57 | Error e -> Error (Did_resolver.error_to_string e) 58 58 | Ok doc -> ( 59 - (* Find AtprotoPersonalDataServer service *) 60 59 match 61 60 List.find_opt 62 61 (fun (s : Did_resolver.service) -> ··· 66 65 | Some s -> Ok s.service_endpoint 67 66 | None -> Error "No PDS service found in DID document")) 68 67 69 - (** {1 Repository Fetching} *) 70 - 71 - let fetch_repo ~sw ~client ~pds_url did = 68 + let fetch_repo ~hcs_client ~pds_url did = 72 69 let uri = 73 70 Uri.of_string (pds_url ^ "/xrpc/com.atproto.sync.getRepo") 74 71 |> Fun.flip Uri.add_query_param ("did", [ did ]) 75 72 in 76 73 Printf.printf "Fetching %s...\n%!" (Uri.to_string uri); 77 - let status, body = http_get ~sw ~client uri in 74 + let status, body = http_get ~hcs_client uri in 78 75 if status = 200 then Ok body 79 76 else 80 77 Error 81 78 (Printf.sprintf "HTTP %d: %s" status 82 79 (String.sub body 0 (min 200 (String.length body)))) 83 80 84 - (** {1 Repository Analysis} *) 85 - 86 81 let truncate n s = 87 82 if String.length s <= n then s else String.sub s 0 (n - 3) ^ "..." 88 83 ··· 110 105 (fun r -> Printf.printf " - %s\n" (Cid.to_string r)) 111 106 header.roots; 112 107 Printf.printf "Blocks: %d\n\n" (List.length blocks); 113 - (* Build blockstore *) 114 108 let store = Blockstore.create () in 115 109 List.iter 116 110 (fun (b : Car.block) -> Blockstore.put store b.cid b.data) 117 111 blocks; 118 - (* Find commit *) 119 112 let commit_cid = List.hd header.roots in 120 113 let commit_data = Blockstore.get store commit_cid in 121 114 match commit_data with ··· 181 174 (fun (key, cid) -> 182 175 if !shown < limit then begin 183 176 Printf.printf "%s\n CID: %s\n" key (Cid.to_string cid); 184 - (* Try to get and show content preview *) 185 177 (match Blockstore.get store cid with 186 178 | Some data -> ( 187 179 match Dag_cbor.decode data with ··· 207 199 else "(none)" 208 200 in 209 201 Printf.printf "Signature: %s\n" sig_b64; 210 - (* Get signing key from DID - would need identity resolution *) 211 202 Printf.printf 212 203 "Status: Signature present but key resolution not implemented\n"; 213 204 Printf.printf " (would need to resolve DID document to verify)\n" 214 205 215 - (** {1 CLI} *) 216 - 217 206 type mode = Summary | Collections | Records of string option | Verify 218 207 219 208 let cli = ··· 239 228 Mirage_crypto_rng_unix.use_default (); 240 229 Eio_main.run @@ fun env -> 241 230 Eio.Switch.run @@ fun sw -> 242 - let auth = 243 - match Ca_certs_nss.authenticator () with 244 - | Ok a -> a 245 - | Error (`Msg m) -> failwith m 231 + let config = 232 + Hcs.Client.default_config |> Hcs.Client.with_max_response_body 104857600L 246 233 in 247 - let https_config = 248 - match Tls.Config.client ~authenticator:auth () with 249 - | Ok c -> c 250 - | Error (`Msg m) -> failwith m 251 - in 252 - let https uri socket = 253 - let tls_host = 254 - match Uri.host uri with 255 - | Some h -> ( 256 - match Domain_name.of_string h with 257 - | Error _ -> None 258 - | Ok dn -> Domain_name.host dn |> Result.to_option) 259 - | None -> None 260 - in 261 - Tls_eio.client_of_flow https_config ?host:tls_host socket 234 + let hcs_client = 235 + Hcs.Client.create ~sw ~net:(Eio.Stdenv.net env) 236 + ~clock:(Eio.Stdenv.clock env) ~config () 262 237 in 263 - let client = Cohttp_eio.Client.make ~https:(Some https) env#net in 264 238 let did, pds_opt, mode, limit = 265 239 Climate.Command.run 266 240 ~program_name:(Climate.Program_name.Literal "repo_inspector") 267 241 (Climate.Command.singleton ~doc:"AT Protocol repository inspector" cli) 268 242 in 269 243 Printf.printf "Repository Inspector\n====================\n\n"; 270 - (* Resolve PDS if not provided *) 271 244 let pds_url = 272 245 match pds_opt with 273 246 | Some url -> url 274 247 | None -> ( 275 248 Printf.printf "Resolving PDS for %s...\n%!" did; 276 - match resolve_pds ~sw ~client did with 249 + match resolve_pds ~hcs_client did with 277 250 | Ok url -> 278 251 Printf.printf "PDS: %s\n\n%!" url; 279 252 url 280 253 | Error e -> 281 254 Printf.printf "Error resolving PDS: %s\n" e; 255 + Hcs.Client.close hcs_client; 282 256 exit 1) 283 257 in 284 - match fetch_repo ~sw ~client ~pds_url did with 258 + (match fetch_repo ~hcs_client ~pds_url did with 285 259 | Error e -> 286 260 Printf.printf "Error: %s\n" e; 261 + Hcs.Client.close hcs_client; 287 262 exit 1 288 263 | Ok car_data -> ( 289 264 match analyze_repo car_data with 290 - | None -> exit 1 265 + | None -> 266 + Hcs.Client.close hcs_client; 267 + exit 1 291 268 | Some (store, commit) -> ( 292 269 show_commit commit; 293 270 match mode with 294 271 | Summary -> show_collections store commit 295 272 | Collections -> show_collections store commit 296 273 | Records coll -> show_records ~limit ~collection:coll store commit 297 - | Verify -> verify_commit store commit)) 274 + | Verify -> verify_commit store commit))); 275 + Hcs.Client.close hcs_client