this repo has no description
1(** 2 * JMAP protocol implementation based on RFC8620 3 * https://datatracker.ietf.org/doc/html/rfc8620 4 *) 5 6(** Whether to redact sensitive information *) 7let should_redact_sensitive = ref true 8 9(** Initialize and configure logging for JMAP *) 10let init_logging ?(level=2) ?(enable_logs=true) ?(redact_sensitive=true) () = 11 if enable_logs then begin 12 Logs.set_reporter (Logs.format_reporter ()); 13 match level with 14 | 0 -> Logs.set_level None 15 | 1 -> Logs.set_level (Some Logs.Error) 16 | 2 -> Logs.set_level (Some Logs.Info) 17 | 3 -> Logs.set_level (Some Logs.Debug) 18 | _ -> Logs.set_level (Some Logs.Debug) 19 end else 20 Logs.set_level None; 21 should_redact_sensitive := redact_sensitive 22 23(** Redact sensitive data like tokens *) 24let redact_token ?(redact=true) token = 25 if redact && !should_redact_sensitive && String.length token > 8 then 26 let prefix = String.sub token 0 4 in 27 let suffix = String.sub token (String.length token - 4) 4 in 28 prefix ^ "..." ^ suffix 29 else 30 token 31 32(** Redact sensitive headers like Authorization *) 33let redact_headers headers = 34 List.map (fun (k, v) -> 35 if String.lowercase_ascii k = "authorization" then 36 if !should_redact_sensitive then 37 let parts = String.split_on_char ' ' v in 38 match parts with 39 | scheme :: token :: _ -> (k, scheme ^ " " ^ redact_token token) 40 | _ -> (k, v) 41 else (k, v) 42 else (k, v) 43 ) headers 44 45(* Initialize logging with defaults *) 46let () = init_logging () 47 48(** Module for managing JMAP capability URIs and other constants *) 49module Capability = struct 50 (** Core JMAP capability URI *) 51 type core = Core 52 53 (** JMAP capability URI as specified in RFC8620 *) 54 let core_uri = "urn:ietf:params:jmap:core" 55 56 (** Convert core capability to URI string *) 57 let string_of_core = function 58 | Core -> core_uri 59 60 (** Parse a string to a core capability, returns None if not a valid capability URI *) 61 let core_of_string = function 62 | s when s = core_uri -> Some Core 63 | _ -> None 64 65 (** All JMAP capability types *) 66 type t = 67 | Core of core 68 | Extension of string 69 70 (** Convert capability to URI string *) 71 let to_string = function 72 | Core c -> string_of_core c 73 | Extension s -> s 74 75 (** Parse a string to a capability, returns Extension for non-core capabilities *) 76 let of_string s = 77 match core_of_string s with 78 | Some c -> Core c 79 | None -> Extension s 80 81 (** Check if a capability matches a core capability *) 82 let is_core = function 83 | Core _ -> true 84 | Extension _ -> false 85 86 (** Check if a capability string is a core capability *) 87 let is_core_string s = 88 match of_string s with 89 | Core _ -> true 90 | Extension _ -> false 91 92 (** Create a list of capability strings *) 93 let strings_of_capabilities capabilities = 94 List.map to_string capabilities 95end 96 97module Types = struct 98 (** Id string as per Section 1.2 *) 99 type id = string 100 101 (** Int bounded within the range -2^53+1 to 2^53-1 as per Section 1.3 *) 102 type int_t = int 103 104 (** UnsignedInt bounded within the range 0 to 2^53-1 as per Section 1.3 *) 105 type unsigned_int = int 106 107 (** Date string in RFC3339 format as per Section 1.4 *) 108 type date = string 109 110 (** UTCDate is a Date with 'Z' time zone as per Section 1.4 *) 111 type utc_date = string 112 113 (** Error object as per Section 3.6.2 *) 114 type error = { 115 type_: string; 116 description: string option; 117 } 118 119 (** Set error object as per Section 5.3 *) 120 type set_error = { 121 type_: string; 122 description: string option; 123 properties: string list option; 124 (* Additional properties for specific error types *) 125 existing_id: id option; (* For alreadyExists error *) 126 } 127 128 (** Invocation object as per Section 3.2 *) 129 type 'a invocation = { 130 name: string; 131 arguments: 'a; 132 method_call_id: string; 133 } 134 135 (** ResultReference object as per Section 3.7 *) 136 type result_reference = { 137 result_of: string; 138 name: string; 139 path: string; 140 } 141 142 (** FilterOperator, FilterCondition and Filter as per Section 5.5 *) 143 type filter_operator = { 144 operator: string; (* "AND", "OR", "NOT" *) 145 conditions: filter list; 146 } 147 and filter_condition = (string * Ezjsonm.value) list 148 and filter = 149 | Operator of filter_operator 150 | Condition of filter_condition 151 152 (** Comparator object for sorting as per Section 5.5 *) 153 type comparator = { 154 property: string; 155 is_ascending: bool option; (* Optional, defaults to true *) 156 collation: string option; (* Optional, server-dependent default *) 157 } 158 159 (** PatchObject as per Section 5.3 *) 160 type patch_object = (string * Ezjsonm.value) list 161 162 (** AddedItem structure as per Section 5.6 *) 163 type added_item = { 164 id: id; 165 index: unsigned_int; 166 } 167 168 (** Account object as per Section 1.6.2 *) 169 type account = { 170 name: string; 171 is_personal: bool; 172 is_read_only: bool; 173 account_capabilities: (string * Ezjsonm.value) list; 174 } 175 176 (** Core capability object as per Section 2 *) 177 type core_capability = { 178 max_size_upload: unsigned_int; 179 max_concurrent_upload: unsigned_int; 180 max_size_request: unsigned_int; 181 max_concurrent_requests: unsigned_int; 182 max_calls_in_request: unsigned_int; 183 max_objects_in_get: unsigned_int; 184 max_objects_in_set: unsigned_int; 185 collation_algorithms: string list; 186 } 187 188 (** PushSubscription keys object as per Section 7.2 *) 189 type push_keys = { 190 p256dh: string; 191 auth: string; 192 } 193 194 (** Session object as per Section 2 *) 195 type session = { 196 capabilities: (string * Ezjsonm.value) list; 197 accounts: (id * account) list; 198 primary_accounts: (string * id) list; 199 username: string; 200 api_url: string; 201 download_url: string; 202 upload_url: string; 203 event_source_url: string option; 204 state: string; 205 } 206 207 (** TypeState for state changes as per Section 7.1 *) 208 type type_state = (string * string) list 209 210 (** StateChange object as per Section 7.1 *) 211 type state_change = { 212 changed: (id * type_state) list; 213 } 214 215 (** PushVerification object as per Section 7.2.2 *) 216 type push_verification = { 217 push_subscription_id: id; 218 verification_code: string; 219 } 220 221 (** PushSubscription object as per Section 7.2 *) 222 type push_subscription = { 223 id: id; 224 device_client_id: string; 225 url: string; 226 keys: push_keys option; 227 verification_code: string option; 228 expires: utc_date option; 229 types: string list option; 230 } 231 232 (** Request object as per Section 3.3 *) 233 type request = { 234 using: string list; 235 method_calls: Ezjsonm.value invocation list; 236 created_ids: (id * id) list option; 237 } 238 239 (** Response object as per Section 3.4 *) 240 type response = { 241 method_responses: Ezjsonm.value invocation list; 242 created_ids: (id * id) list option; 243 session_state: string; 244 } 245 246 (** Standard method arguments and responses *) 247 248 (** Arguments for Foo/get method as per Section 5.1 *) 249 type 'a get_arguments = { 250 account_id: id; 251 ids: id list option; 252 properties: string list option; 253 } 254 255 (** Response for Foo/get method as per Section 5.1 *) 256 type 'a get_response = { 257 account_id: id; 258 state: string; 259 list: 'a list; 260 not_found: id list; 261 } 262 263 (** Arguments for Foo/changes method as per Section 5.2 *) 264 type changes_arguments = { 265 account_id: id; 266 since_state: string; 267 max_changes: unsigned_int option; 268 } 269 270 (** Response for Foo/changes method as per Section 5.2 *) 271 type changes_response = { 272 account_id: id; 273 old_state: string; 274 new_state: string; 275 has_more_changes: bool; 276 created: id list; 277 updated: id list; 278 destroyed: id list; 279 } 280 281 (** Arguments for Foo/set method as per Section 5.3 *) 282 type 'a set_arguments = { 283 account_id: id; 284 if_in_state: string option; 285 create: (id * 'a) list option; 286 update: (id * patch_object) list option; 287 destroy: id list option; 288 } 289 290 (** Response for Foo/set method as per Section 5.3 *) 291 type 'a set_response = { 292 account_id: id; 293 old_state: string option; 294 new_state: string; 295 created: (id * 'a) list option; 296 updated: (id * 'a option) list option; 297 destroyed: id list option; 298 not_created: (id * set_error) list option; 299 not_updated: (id * set_error) list option; 300 not_destroyed: (id * set_error) list option; 301 } 302 303 (** Arguments for Foo/copy method as per Section 5.4 *) 304 type 'a copy_arguments = { 305 from_account_id: id; 306 if_from_in_state: string option; 307 account_id: id; 308 if_in_state: string option; 309 create: (id * 'a) list; 310 on_success_destroy_original: bool option; 311 destroy_from_if_in_state: string option; 312 } 313 314 (** Response for Foo/copy method as per Section 5.4 *) 315 type 'a copy_response = { 316 from_account_id: id; 317 account_id: id; 318 old_state: string option; 319 new_state: string; 320 created: (id * 'a) list option; 321 not_created: (id * set_error) list option; 322 } 323 324 (** Arguments for Foo/query method as per Section 5.5 *) 325 type query_arguments = { 326 account_id: id; 327 filter: filter option; 328 sort: comparator list option; 329 position: int_t option; 330 anchor: id option; 331 anchor_offset: int_t option; 332 limit: unsigned_int option; 333 calculate_total: bool option; 334 } 335 336 (** Response for Foo/query method as per Section 5.5 *) 337 type query_response = { 338 account_id: id; 339 query_state: string; 340 can_calculate_changes: bool; 341 position: unsigned_int; 342 ids: id list; 343 total: unsigned_int option; 344 limit: unsigned_int option; 345 } 346 347 (** Arguments for Foo/queryChanges method as per Section 5.6 *) 348 type query_changes_arguments = { 349 account_id: id; 350 filter: filter option; 351 sort: comparator list option; 352 since_query_state: string; 353 max_changes: unsigned_int option; 354 up_to_id: id option; 355 calculate_total: bool option; 356 } 357 358 (** Response for Foo/queryChanges method as per Section 5.6 *) 359 type query_changes_response = { 360 account_id: id; 361 old_query_state: string; 362 new_query_state: string; 363 total: unsigned_int option; 364 removed: id list; 365 added: added_item list option; 366 } 367 368 (** Arguments for Blob/copy method as per Section 6.3 *) 369 type blob_copy_arguments = { 370 from_account_id: id; 371 account_id: id; 372 blob_ids: id list; 373 } 374 375 (** Response for Blob/copy method as per Section 6.3 *) 376 type blob_copy_response = { 377 from_account_id: id; 378 account_id: id; 379 copied: (id * id) list option; 380 not_copied: (id * set_error) list option; 381 } 382 383 (** Upload response as per Section 6.1 *) 384 type upload_response = { 385 account_id: id; 386 blob_id: id; 387 type_: string; 388 size: unsigned_int; 389 } 390 391 (** Problem details object as per RFC7807 and Section 3.6.1 *) 392 type problem_details = { 393 type_: string; 394 status: int option; 395 detail: string option; 396 limit: string option; (* For "limit" error *) 397 } 398end 399 400module Api = struct 401 open Lwt.Syntax 402 open Types 403 404 (** Error that may occur during API requests *) 405 type error = 406 | Connection_error of string 407 | HTTP_error of int * string 408 | Parse_error of string 409 | Authentication_error 410 411 (** Result type for API operations *) 412 type 'a result = ('a, error) Stdlib.result 413 414 (** Configuration for a JMAP API client *) 415 type config = { 416 api_uri: Uri.t; 417 username: string; 418 authentication_token: string; 419 } 420 421 (** Convert Ezjsonm.value to string *) 422 let json_to_string json = 423 Ezjsonm.value_to_string ~minify:false json 424 425 (** Parse response string as JSON value *) 426 let parse_json_string str = 427 try Ok (Ezjsonm.from_string str) 428 with e -> Error (Parse_error (Printexc.to_string e)) 429 430 (** Parse JSON response as a JMAP response object *) 431 let parse_response json = 432 try 433 let method_responses = 434 match Ezjsonm.find json ["methodResponses"] with 435 | `A items -> 436 List.map (fun json -> 437 match json with 438 | `A [`String name; args; `String method_call_id] -> 439 { name; arguments = args; method_call_id } 440 | _ -> raise (Invalid_argument "Invalid invocation format in response") 441 ) items 442 | _ -> raise (Invalid_argument "methodResponses is not an array") 443 in 444 let created_ids_opt = 445 try 446 let obj = Ezjsonm.find json ["createdIds"] in 447 match obj with 448 | `O items -> Some (List.map (fun (k, v) -> 449 match v with 450 | `String id -> (k, id) 451 | _ -> raise (Invalid_argument "createdIds value is not a string") 452 ) items) 453 | _ -> None 454 with Not_found -> None 455 in 456 let session_state = 457 match Ezjsonm.find json ["sessionState"] with 458 | `String s -> s 459 | _ -> raise (Invalid_argument "sessionState is not a string") 460 in 461 Ok { method_responses; created_ids = created_ids_opt; session_state } 462 with 463 | Not_found -> Error (Parse_error "Required field not found in response") 464 | Invalid_argument msg -> Error (Parse_error msg) 465 | e -> Error (Parse_error (Printexc.to_string e)) 466 467 (** Serialize a JMAP request object to JSON *) 468 let serialize_request req = 469 let method_calls_json = 470 `A (List.map (fun (inv : 'a invocation) -> 471 `A [`String inv.name; inv.arguments; `String inv.method_call_id] 472 ) req.method_calls) 473 in 474 let using_json = `A (List.map (fun s -> `String s) req.using) in 475 let json = `O [ 476 ("using", using_json); 477 ("methodCalls", method_calls_json) 478 ] in 479 let json = match req.created_ids with 480 | Some ids -> 481 let created_ids_json = `O (List.map (fun (k, v) -> (k, `String v)) ids) in 482 Ezjsonm.update json ["createdIds"] (Some created_ids_json) 483 | None -> json 484 in 485 json_to_string json 486 487 (** Make a raw HTTP request *) 488 let make_http_request ~method_ ~headers ~body uri = 489 let open Cohttp in 490 let open Cohttp_lwt_unix in 491 let headers = Header.add_list (Header.init ()) headers in 492 493 (* Log request details at debug level *) 494 let header_list = Cohttp.Header.to_list headers in 495 let redacted_headers = redact_headers header_list in 496 Logs.debug (fun m -> 497 m "\n===== HTTP REQUEST =====\n\ 498 URI: %s\n\ 499 METHOD: %s\n\ 500 HEADERS:\n%s\n\ 501 BODY:\n%s\n\ 502 ======================\n" 503 (Uri.to_string uri) 504 method_ 505 (String.concat "\n" (List.map (fun (k, v) -> Printf.sprintf " %s: %s" k v) redacted_headers)) 506 body); 507 508 Lwt.catch 509 (fun () -> 510 let* resp, body = 511 match method_ with 512 | "GET" -> Client.get ~headers uri 513 | "POST" -> Client.post ~headers ~body:(Cohttp_lwt.Body.of_string body) uri 514 | _ -> failwith (Printf.sprintf "Unsupported HTTP method: %s" method_) 515 in 516 let* body_str = Cohttp_lwt.Body.to_string body in 517 let status = Response.status resp |> Code.code_of_status in 518 519 (* Log response details at debug level *) 520 let header_list = Cohttp.Header.to_list (Response.headers resp) in 521 let redacted_headers = redact_headers header_list in 522 Logs.debug (fun m -> 523 m "\n===== HTTP RESPONSE =====\n\ 524 STATUS: %d\n\ 525 HEADERS:\n%s\n\ 526 BODY:\n%s\n\ 527 ======================\n" 528 status 529 (String.concat "\n" (List.map (fun (k, v) -> Printf.sprintf " %s: %s" k v) redacted_headers)) 530 body_str); 531 532 if status >= 200 && status < 300 then 533 Lwt.return (Ok body_str) 534 else 535 Lwt.return (Error (HTTP_error (status, body_str)))) 536 (fun e -> 537 let error_msg = Printexc.to_string e in 538 Logs.err (fun m -> m "%s" error_msg); 539 Lwt.return (Error (Connection_error error_msg))) 540 541 (** Make a raw JMAP API request 542 543 TODO:claude *) 544 let make_request config req = 545 let body = serialize_request req in 546 (* Choose appropriate authorization header based on whether it's a bearer token or basic auth *) 547 let auth_header = 548 if String.length config.username > 0 then 549 (* Standard username/password authentication *) 550 "Basic " ^ Base64.encode_string (config.username ^ ":" ^ config.authentication_token) 551 else 552 (* API token (bearer authentication) *) 553 "Bearer " ^ config.authentication_token 554 in 555 556 (* Log auth header at debug level with redaction *) 557 let redacted_header = 558 if String.length config.username > 0 then 559 "Basic " ^ redact_token (Base64.encode_string (config.username ^ ":" ^ config.authentication_token)) 560 else 561 "Bearer " ^ redact_token config.authentication_token 562 in 563 Logs.debug (fun m -> m "Using authorization header: %s" redacted_header); 564 565 let headers = [ 566 ("Content-Type", "application/json"); 567 ("Content-Length", string_of_int (String.length body)); 568 ("Authorization", auth_header) 569 ] in 570 let* result = make_http_request ~method_:"POST" ~headers ~body config.api_uri in 571 match result with 572 | Ok response_body -> 573 (match parse_json_string response_body with 574 | Ok json -> 575 Logs.debug (fun m -> m "Successfully parsed JSON response"); 576 Lwt.return (parse_response json) 577 | Error e -> 578 let msg = match e with Parse_error m -> m | _ -> "unknown error" in 579 Logs.err (fun m -> m "Failed to parse response: %s" msg); 580 Lwt.return (Error e)) 581 | Error e -> 582 (match e with 583 | Connection_error msg -> Logs.err (fun m -> m "Connection error: %s" msg) 584 | HTTP_error (code, _) -> Logs.err (fun m -> m "HTTP error %d" code) 585 | Parse_error msg -> Logs.err (fun m -> m "Parse error: %s" msg) 586 | Authentication_error -> Logs.err (fun m -> m "Authentication error")); 587 Lwt.return (Error e) 588 589 (** Parse a JSON object as a Session object *) 590 let parse_session_object json = 591 try 592 let capabilities = 593 match Ezjsonm.find json ["capabilities"] with 594 | `O items -> items 595 | _ -> raise (Invalid_argument "capabilities is not an object") 596 in 597 598 let accounts = 599 match Ezjsonm.find json ["accounts"] with 600 | `O items -> List.map (fun (id, json) -> 601 match json with 602 | `O _ -> 603 let name = Ezjsonm.get_string (Ezjsonm.find json ["name"]) in 604 let is_personal = Ezjsonm.get_bool (Ezjsonm.find json ["isPersonal"]) in 605 let is_read_only = Ezjsonm.get_bool (Ezjsonm.find json ["isReadOnly"]) in 606 let account_capabilities = 607 match Ezjsonm.find json ["accountCapabilities"] with 608 | `O items -> items 609 | _ -> raise (Invalid_argument "accountCapabilities is not an object") 610 in 611 (id, { name; is_personal; is_read_only; account_capabilities }) 612 | _ -> raise (Invalid_argument "account value is not an object") 613 ) items 614 | _ -> raise (Invalid_argument "accounts is not an object") 615 in 616 617 let primary_accounts = 618 match Ezjsonm.find_opt json ["primaryAccounts"] with 619 | Some (`O items) -> List.map (fun (k, v) -> 620 match v with 621 | `String id -> (k, id) 622 | _ -> raise (Invalid_argument "primaryAccounts value is not a string") 623 ) items 624 | Some _ -> raise (Invalid_argument "primaryAccounts is not an object") 625 | None -> [] 626 in 627 628 let username = Ezjsonm.get_string (Ezjsonm.find json ["username"]) in 629 let api_url = Ezjsonm.get_string (Ezjsonm.find json ["apiUrl"]) in 630 let download_url = Ezjsonm.get_string (Ezjsonm.find json ["downloadUrl"]) in 631 let upload_url = Ezjsonm.get_string (Ezjsonm.find json ["uploadUrl"]) in 632 let event_source_url = 633 try Some (Ezjsonm.get_string (Ezjsonm.find json ["eventSourceUrl"])) 634 with Not_found -> None 635 in 636 let state = Ezjsonm.get_string (Ezjsonm.find json ["state"]) in 637 638 Ok { capabilities; accounts; primary_accounts; username; 639 api_url; download_url; upload_url; event_source_url; state } 640 with 641 | Not_found -> Error (Parse_error "Required field not found in session object") 642 | Invalid_argument msg -> Error (Parse_error msg) 643 | e -> Error (Parse_error (Printexc.to_string e)) 644 645 (** Fetch a Session object from a JMAP server 646 647 TODO:claude *) 648 let get_session uri ?username ?authentication_token ?api_token () = 649 let headers = 650 match (username, authentication_token, api_token) with 651 | (Some u, Some t, _) -> 652 let auth = "Basic " ^ Base64.encode_string (u ^ ":" ^ t) in 653 let redacted_auth = "Basic " ^ redact_token (Base64.encode_string (u ^ ":" ^ t)) in 654 Logs.info (fun m -> m "Session using Basic auth: %s" redacted_auth); 655 [ 656 ("Content-Type", "application/json"); 657 ("Authorization", auth) 658 ] 659 | (_, _, Some token) -> 660 let auth = "Bearer " ^ token in 661 let redacted_token = redact_token token in 662 Logs.info (fun m -> m "Session using Bearer auth: %s" ("Bearer " ^ redacted_token)); 663 [ 664 ("Content-Type", "application/json"); 665 ("Authorization", auth) 666 ] 667 | _ -> [("Content-Type", "application/json")] 668 in 669 670 let* result = make_http_request ~method_:"GET" ~headers ~body:"" uri in 671 match result with 672 | Ok response_body -> 673 (match parse_json_string response_body with 674 | Ok json -> 675 Logs.debug (fun m -> m "Successfully parsed session response"); 676 Lwt.return (parse_session_object json) 677 | Error e -> 678 let msg = match e with Parse_error m -> m | _ -> "unknown error" in 679 Logs.err (fun m -> m "Failed to parse session response: %s" msg); 680 Lwt.return (Error e)) 681 | Error e -> 682 let err_msg = match e with 683 | Connection_error msg -> "Connection error: " ^ msg 684 | HTTP_error (code, _) -> Printf.sprintf "HTTP error %d" code 685 | Parse_error msg -> "Parse error: " ^ msg 686 | Authentication_error -> "Authentication error" 687 in 688 Logs.err (fun m -> m "Failed to get session: %s" err_msg); 689 Lwt.return (Error e) 690 691 (** Upload a binary blob to the server 692 693 TODO:claude *) 694 let upload_blob config ~account_id ~content_type data = 695 let upload_url_template = config.api_uri |> Uri.to_string in 696 (* Replace {accountId} with the actual account ID *) 697 let upload_url = Str.global_replace (Str.regexp "{accountId}") account_id upload_url_template in 698 let upload_uri = Uri.of_string upload_url in 699 700 let headers = [ 701 ("Content-Type", content_type); 702 ("Content-Length", string_of_int (String.length data)); 703 ("Authorization", "Basic " ^ Base64.encode_string (config.username ^ ":" ^ config.authentication_token)) 704 ] in 705 706 let* result = make_http_request ~method_:"POST" ~headers ~body:data upload_uri in 707 match result with 708 | Ok response_body -> 709 (match parse_json_string response_body with 710 | Ok json -> 711 (try 712 let account_id = Ezjsonm.get_string (Ezjsonm.find json ["accountId"]) in 713 let blob_id = Ezjsonm.get_string (Ezjsonm.find json ["blobId"]) in 714 let type_ = Ezjsonm.get_string (Ezjsonm.find json ["type"]) in 715 let size = Ezjsonm.get_int (Ezjsonm.find json ["size"]) in 716 Lwt.return (Ok { account_id; blob_id; type_; size }) 717 with 718 | Not_found -> Lwt.return (Error (Parse_error "Required field not found in upload response")) 719 | e -> Lwt.return (Error (Parse_error (Printexc.to_string e)))) 720 | Error e -> Lwt.return (Error e)) 721 | Error e -> Lwt.return (Error e) 722 723 (** Download a binary blob from the server 724 725 TODO:claude *) 726 let download_blob config ~account_id ~blob_id ?type_ ?name () = 727 let download_url_template = config.api_uri |> Uri.to_string in 728 729 (* Replace template variables with actual values *) 730 let url = Str.global_replace (Str.regexp "{accountId}") account_id download_url_template in 731 let url = Str.global_replace (Str.regexp "{blobId}") blob_id url in 732 733 let url = match type_ with 734 | Some t -> Str.global_replace (Str.regexp "{type}") (Uri.pct_encode t) url 735 | None -> Str.global_replace (Str.regexp "{type}") "" url 736 in 737 738 let url = match name with 739 | Some n -> Str.global_replace (Str.regexp "{name}") (Uri.pct_encode n) url 740 | None -> Str.global_replace (Str.regexp "{name}") "file" url 741 in 742 743 let download_uri = Uri.of_string url in 744 745 let headers = [ 746 ("Authorization", "Basic " ^ Base64.encode_string (config.username ^ ":" ^ config.authentication_token)) 747 ] in 748 749 let* result = make_http_request ~method_:"GET" ~headers ~body:"" download_uri in 750 Lwt.return result 751end