···197197 write_type_and_argument t 5 (Int64.of_int len) ;
198198 ordered_map_keys m
199199 |> List.iter (fun k ->
200200- write_string t k ;
201201- write_value t (String_map.find k m) )
200200+ write_string t k ;
201201+ write_value t (String_map.find k m) )
202202 | `Link cid ->
203203 write_cid t cid
204204
···11let handler =
22- Xrpc.handler ~auth:Authorization (fun {req; db; auth} ->
22+ Xrpc.handler ~auth:Authorization (fun {req; auth; db; _} ->
33 let did = Auth.get_authed_did_exn auth in
44 let%lwt body = Dream.body req in
55 let prefs =
+6-6
pegasus/lib/api/identity/resolveHandle.ml
···1414 Dream.json @@ Yojson.Safe.to_string
1515 @@ response_to_yojson {did= actor.did}
1616 | None -> (
1717- match%lwt Id_resolver.Handle.resolve handle with
1818- | Ok did ->
1919- Dream.json @@ Yojson.Safe.to_string @@ response_to_yojson {did}
2020- | Error e ->
2121- Errors.log_exn (Failure e) ;
2222- Errors.internal_error ~msg:"could not resolve handle" () ) )
1717+ match%lwt Id_resolver.Handle.resolve handle with
1818+ | Ok did ->
1919+ Dream.json @@ Yojson.Safe.to_string @@ response_to_yojson {did}
2020+ | Error e ->
2121+ Errors.log_exn (Failure e) ;
2222+ Errors.internal_error ~msg:"could not resolve handle" () ) )
+55-58
pegasus/lib/api/identity/updateHandle.ml
···11type request = {handle: string} [@@deriving yojson]
2233let handler =
44- Xrpc.handler ~auth:Authorization (fun {req; auth; db} ->
44+ Xrpc.handler ~auth:Authorization (fun {req; auth; db; _} ->
55 let did = Auth.get_authed_did_exn auth in
66 let%lwt body = Dream.body req in
77 let handle =
···1515 | Error e ->
1616 raise e
1717 | Ok () -> (
1818- match%lwt Data_store.get_actor_by_identifier handle db with
1919- | Some _ ->
2020- Errors.invalid_request ~name:"InvalidHandle"
2121- "handle already in use"
2222- | None ->
2323- let%lwt () = Data_store.update_actor_handle ~did ~handle db in
2424- let%lwt _ =
2525- if String.starts_with ~prefix:"did:plc:" did then
2626- match%lwt Plc.get_audit_log did with
2727- | Error e ->
2828- Dream.error (fun log -> log ~request:req "%s" e) ;
2929- Errors.internal_error ~msg:"failed to fetch did doc" ()
3030- | Ok log -> (
3131- let latest = List.rev log |> List.hd in
3232- let aka =
3333- match
3434- List.mem ("at://" ^ handle)
3535- latest.operation.also_known_as
3636- with
3737- | true ->
3838- latest.operation.also_known_as
3939- | false ->
4040- ("at://" ^ handle) :: latest.operation.also_known_as
4141- in
4242- let%lwt signing_key =
4343- match%lwt Data_store.get_actor_by_identifier did db with
4444- | Some {signing_key; _} ->
4545- Lwt.return @@ Kleidos.parse_multikey_str signing_key
4646- | _ ->
4747- Errors.internal_error ()
4848- in
4949- let signed =
5050- Plc.sign_operation signing_key
5151- (Operation
5252- { type'= "plc_operation"
5353- ; prev= Some latest.cid
5454- ; also_known_as= aka
5555- ; rotation_keys= latest.operation.rotation_keys
5656- ; verification_methods=
5757- latest.operation.verification_methods
5858- ; services= latest.operation.services } )
5959- in
6060- match%lwt Plc.submit_operation did signed with
6161- | Ok _ ->
6262- Lwt.return_unit
6363- | Error (status, msg) ->
6464- Dream.error (fun log ->
6565- log ~request:req "%d %s" status msg ) ;
6666- Errors.internal_error
6767- ~msg:"failed to submit plc operation" () )
6868- else Lwt.return_unit
6969- in
7070- let () =
7171- Ttl_cache.String_cache.remove Id_resolver.Did.cache did
7272- in
7373- let%lwt _ = Sequencer.sequence_identity db ~did ~handle () in
7474- Dream.empty `OK ) )
1818+ match%lwt Data_store.get_actor_by_identifier handle db with
1919+ | Some _ ->
2020+ Errors.invalid_request ~name:"InvalidHandle" "handle already in use"
2121+ | None ->
2222+ let%lwt () = Data_store.update_actor_handle ~did ~handle db in
2323+ let%lwt _ =
2424+ if String.starts_with ~prefix:"did:plc:" did then
2525+ match%lwt Plc.get_audit_log did with
2626+ | Error e ->
2727+ Dream.error (fun log -> log ~request:req "%s" e) ;
2828+ Errors.internal_error ~msg:"failed to fetch did doc" ()
2929+ | Ok log -> (
3030+ let latest = List.rev log |> List.hd in
3131+ let aka =
3232+ match
3333+ List.mem ("at://" ^ handle)
3434+ latest.operation.also_known_as
3535+ with
3636+ | true ->
3737+ latest.operation.also_known_as
3838+ | false ->
3939+ ("at://" ^ handle) :: latest.operation.also_known_as
4040+ in
4141+ let%lwt signing_key =
4242+ match%lwt Data_store.get_actor_by_identifier did db with
4343+ | Some {signing_key; _} ->
4444+ Lwt.return @@ Kleidos.parse_multikey_str signing_key
4545+ | _ ->
4646+ Errors.internal_error ()
4747+ in
4848+ let signed =
4949+ Plc.sign_operation signing_key
5050+ (Operation
5151+ { type'= "plc_operation"
5252+ ; prev= Some latest.cid
5353+ ; also_known_as= aka
5454+ ; rotation_keys= latest.operation.rotation_keys
5555+ ; verification_methods=
5656+ latest.operation.verification_methods
5757+ ; services= latest.operation.services } )
5858+ in
5959+ match%lwt Plc.submit_operation did signed with
6060+ | Ok _ ->
6161+ Lwt.return_unit
6262+ | Error (status, msg) ->
6363+ Dream.error (fun log ->
6464+ log ~request:req "%d %s" status msg ) ;
6565+ Errors.internal_error
6666+ ~msg:"failed to submit plc operation" () )
6767+ else Lwt.return_unit
6868+ in
6969+ let () = Ttl_cache.String_cache.remove Id_resolver.Did.cache did in
7070+ let%lwt _ = Sequencer.sequence_identity db ~did ~handle () in
7171+ Dream.empty `OK ) )
+184
pegasus/lib/api/oauth_/authorize.ml
···11+open Oauth
22+open Oauth.Types
33+44+let get_session_user (ctx : Xrpc.context) =
55+ match Dream.session_field ctx.req "did" with
66+ | Some did ->
77+ Lwt.return_some did
88+ | None ->
99+ Lwt.return_none
1010+1111+let get_handler =
1212+ Xrpc.handler (fun ctx ->
1313+ let login_redirect =
1414+ Uri.make ~path:"/account/login" ~query:(Util.copy_query ctx.req) ()
1515+ |> Uri.to_string |> Dream.redirect ctx.req
1616+ in
1717+ let client_id = Dream.query ctx.req "client_id" in
1818+ let request_uri = Dream.query ctx.req "request_uri" in
1919+ match (client_id, request_uri) with
2020+ | None, _ | _, None ->
2121+ login_redirect
2222+ | Some client_id, Some request_uri -> (
2323+ let prefix = Constants.request_uri_prefix in
2424+ if not (String.starts_with ~prefix request_uri) then login_redirect
2525+ else
2626+ let request_id =
2727+ String.sub request_uri (String.length prefix)
2828+ (String.length request_uri - String.length prefix)
2929+ in
3030+ match%lwt Queries.get_par_request ctx.db request_id with
3131+ | None ->
3232+ login_redirect
3333+ | Some req_record -> (
3434+ if req_record.client_id <> client_id then login_redirect
3535+ else
3636+ let req =
3737+ Yojson.Safe.from_string req_record.request_data
3838+ |> par_request_of_yojson
3939+ |> Result.map_error (fun _ ->
4040+ Errors.internal_error ~msg:"failed to parse par request"
4141+ () )
4242+ |> Result.get_ok
4343+ in
4444+ let%lwt metadata =
4545+ try%lwt Client.fetch_client_metadata client_id
4646+ with _ ->
4747+ Errors.internal_error
4848+ ~msg:"failed to fetch client metadata" ()
4949+ in
5050+ let code =
5151+ "cod-"
5252+ ^ Uuidm.to_string
5353+ (Uuidm.v4_gen (Random.State.make_self_init ()) ())
5454+ in
5555+ let expires_at = Util.now_ms () + Constants.code_expiry_ms in
5656+ let%lwt () =
5757+ Queries.insert_auth_code ctx.db
5858+ { code
5959+ ; request_id
6060+ ; authorized_by= None
6161+ ; authorized_at= None
6262+ ; expires_at
6363+ ; used= false }
6464+ in
6565+ match%lwt get_session_user ctx with
6666+ | None ->
6767+ login_redirect
6868+ | Some did -> (
6969+ match req.login_hint with
7070+ | Some hint when hint <> did ->
7171+ login_redirect
7272+ | _ ->
7373+ let%lwt handle =
7474+ match%lwt
7575+ Data_store.get_actor_by_identifier did ctx.db
7676+ with
7777+ | Some {handle; _} ->
7878+ Lwt.return handle
7979+ | None ->
8080+ Errors.internal_error
8181+ ~msg:"failed to resolve user" ()
8282+ in
8383+ let scopes = String.split_on_char ' ' req.scope in
8484+ let csrf_token = Dream.csrf_token ctx.req in
8585+ let html =
8686+ JSX.render
8787+ (Templates.Oauth_authorize.make ~metadata ~handle
8888+ ~scopes ~code ~request_uri ~csrf_token () )
8989+ in
9090+ Dream.html html ) ) ) )
9191+9292+let post_handler =
9393+ Xrpc.handler (fun ctx ->
9494+ match%lwt get_session_user ctx with
9595+ | None ->
9696+ Errors.auth_required "missing authentication"
9797+ | Some user_did -> (
9898+ match%lwt Dream.form ctx.req with
9999+ | `Ok fields -> (
100100+ let action = List.assoc_opt "action" fields in
101101+ let code = List.assoc_opt "code" fields in
102102+ let request_uri = List.assoc_opt "request_uri" fields in
103103+ match (action, code, request_uri) with
104104+ | Some "deny", _, Some request_uri -> (
105105+ let prefix = Constants.request_uri_prefix in
106106+ let request_id =
107107+ String.sub request_uri (String.length prefix)
108108+ (String.length request_uri - String.length prefix)
109109+ in
110110+ let%lwt req_record =
111111+ Queries.get_par_request ctx.db request_id
112112+ in
113113+ match req_record with
114114+ | Some rec_ ->
115115+ let req =
116116+ Yojson.Safe.from_string rec_.request_data
117117+ |> par_request_of_yojson |> Result.get_ok
118118+ in
119119+ let params =
120120+ [ ("error", "access_denied")
121121+ ; ("error_description", "Unable to authorize user.")
122122+ ; ("state", req.state)
123123+ ; ("iss", "https://" ^ Env.hostname) ]
124124+ in
125125+ let query =
126126+ String.concat "&"
127127+ (List.map
128128+ (fun (k, v) -> k ^ "=" ^ Uri.pct_encode v)
129129+ params )
130130+ in
131131+ Dream.redirect ctx.req (req.redirect_uri ^ "?" ^ query)
132132+ | None ->
133133+ Errors.invalid_request "request expired" )
134134+ | Some "allow", Some code, Some _request_uri -> (
135135+ let%lwt code_record = Queries.get_auth_code ctx.db code in
136136+ match code_record with
137137+ | None ->
138138+ Errors.invalid_request "invalid code"
139139+ | Some code_rec -> (
140140+ if code_rec.authorized_by <> None then
141141+ Errors.invalid_request "code already authorized"
142142+ else if code_rec.used then
143143+ Errors.invalid_request "code already used"
144144+ else if Util.now_ms () > code_rec.expires_at then
145145+ Errors.invalid_request "code expired"
146146+ else
147147+ let%lwt () =
148148+ Queries.activate_auth_code ctx.db code user_did
149149+ in
150150+ let%lwt req_record =
151151+ Queries.get_par_request ctx.db code_rec.request_id
152152+ in
153153+ match req_record with
154154+ | None ->
155155+ Errors.internal_error ~msg:"request not found" ()
156156+ | Some rec_ ->
157157+ let req =
158158+ Yojson.Safe.from_string rec_.request_data
159159+ |> par_request_of_yojson |> Result.get_ok
160160+ in
161161+ let params =
162162+ [ ("code", code)
163163+ ; ("state", req.state)
164164+ ; ("iss", "https://" ^ Env.hostname) ]
165165+ in
166166+ let query =
167167+ String.concat "&"
168168+ (List.map
169169+ (fun (k, v) -> k ^ "=" ^ Uri.pct_encode v)
170170+ params )
171171+ in
172172+ let separator =
173173+ match req.response_mode with
174174+ | Some "fragment" ->
175175+ "#"
176176+ | _ ->
177177+ "?"
178178+ in
179179+ Dream.redirect ctx.req
180180+ (req.redirect_uri ^ separator ^ query) ) )
181181+ | _ ->
182182+ Errors.invalid_request "invalid request" )
183183+ | _ ->
184184+ Errors.invalid_request "invalid request" ) )
+41
pegasus/lib/api/oauth_/par.ml
···11+open Oauth
22+open Oauth.Types
33+44+let options_handler = Xrpc.handler (fun _ -> Dream.empty `No_Content)
55+66+let post_handler =
77+ Xrpc.handler ~auth:DPoP (fun ctx ->
88+ let proof = Auth.get_dpop_proof_exn ctx.auth in
99+ let%lwt req = Xrpc.parse_body ctx.req par_request_of_yojson in
1010+ let%lwt client =
1111+ try%lwt Client.fetch_client_metadata req.client_id
1212+ with e ->
1313+ Errors.log_exn ~req:ctx.req e ;
1414+ Errors.invalid_request "failed to fetch client metadata"
1515+ in
1616+ if req.response_type <> "code" then
1717+ Errors.invalid_request "only response_type=code supported"
1818+ else if req.code_challenge_method <> "S256" then
1919+ Errors.invalid_request "only code_challenge_method=S256 supported"
2020+ else if not (List.mem req.redirect_uri client.redirect_uris) then
2121+ Errors.invalid_request "invalid redirect_uri"
2222+ else
2323+ let request_id =
2424+ "req-"
2525+ ^ Uuidm.to_string (Uuidm.v4_gen (Random.State.make_self_init ()) ())
2626+ in
2727+ let request_uri = Constants.request_uri_prefix ^ request_id in
2828+ let expires_at = Util.now_ms () + Constants.par_request_ttl_ms in
2929+ let request : oauth_request =
3030+ { request_id
3131+ ; client_id= req.client_id
3232+ ; request_data= Yojson.Safe.to_string (par_request_to_yojson req)
3333+ ; dpop_jkt= Some proof.jkt
3434+ ; expires_at
3535+ ; created_at= Util.now_ms () }
3636+ in
3737+ let%lwt () = Queries.insert_par_request ctx.db request in
3838+ Dream.json ~status:`Created
3939+ @@ Yojson.Safe.to_string
4040+ @@ `Assoc
4141+ [("request_uri", `String request_uri); ("expires_in", `Int 300)] )
+179
pegasus/lib/api/oauth_/token.ml
···11+open Oauth
22+33+let options_handler = Xrpc.handler (fun _ -> Dream.empty `No_Content)
44+55+let post_handler =
66+ Xrpc.handler ~auth:DPoP (fun ctx ->
77+ let%lwt req = Xrpc.parse_body ctx.req Types.token_request_of_yojson in
88+ let proof = Auth.get_dpop_proof_exn ctx.auth in
99+ match req.grant_type with
1010+ | "authorization_code" -> (
1111+ match req.code with
1212+ | None ->
1313+ Errors.invalid_request "code required"
1414+ | Some code -> (
1515+ let%lwt code_record = Queries.consume_auth_code ctx.db code in
1616+ match code_record with
1717+ | None ->
1818+ Errors.invalid_request "invalid code"
1919+ | Some code_rec -> (
2020+ if Util.now_ms () > code_rec.expires_at then
2121+ Errors.invalid_request "code expired"
2222+ else
2323+ match code_rec.authorized_by with
2424+ | None ->
2525+ Errors.invalid_request "code not authorized"
2626+ | Some did -> (
2727+ let%lwt par_req =
2828+ Queries.get_par_request ctx.db code_rec.request_id
2929+ in
3030+ match par_req with
3131+ | None ->
3232+ Errors.internal_error ~msg:"request not found" ()
3333+ | Some par_record ->
3434+ let orig_req =
3535+ Yojson.Safe.from_string par_record.request_data
3636+ |> Types.par_request_of_yojson |> Result.get_ok
3737+ in
3838+ ( match req.redirect_uri with
3939+ | None ->
4040+ Errors.invalid_request "redirect_uri required"
4141+ | Some uri when uri <> orig_req.redirect_uri ->
4242+ Errors.invalid_request "redirect_uri mismatch"
4343+ | _ ->
4444+ () ) ;
4545+ ( match req.code_verifier with
4646+ | None ->
4747+ Errors.invalid_request "code_verifier required"
4848+ | Some verifier ->
4949+ let computed =
5050+ Digestif.SHA256.digest_string verifier
5151+ |> Digestif.SHA256.to_raw_string
5252+ |> Base64.(
5353+ encode_exn ~pad:false
5454+ ~alphabet:uri_safe_alphabet )
5555+ in
5656+ if orig_req.code_challenge <> computed then
5757+ Errors.invalid_request "invalid code_verifier"
5858+ ) ;
5959+ ( match par_record.dpop_jkt with
6060+ | Some stored when stored <> proof.jkt ->
6161+ Errors.invalid_request "DPoP key mismatch"
6262+ | _ ->
6363+ () ) ;
6464+ let token_id =
6565+ "tok-"
6666+ ^ Uuidm.to_string
6767+ (Uuidm.v4_gen
6868+ (Random.State.make_self_init ())
6969+ () )
7070+ in
7171+ let refresh_token =
7272+ "ref-"
7373+ ^ Uuidm.to_string
7474+ (Uuidm.v4_gen
7575+ (Random.State.make_self_init ())
7676+ () )
7777+ in
7878+ let now_sec = int_of_float (Unix.gettimeofday ()) in
7979+ let expires_in =
8080+ Constants.access_token_expiry_ms / 1000
8181+ in
8282+ let exp_sec = now_sec + expires_in in
8383+ let expires_at = exp_sec * 1000 in
8484+ let claims =
8585+ `Assoc
8686+ [ ("jti", `String token_id)
8787+ ; ("sub", `String did)
8888+ ; ("iat", `Int now_sec)
8989+ ; ("exp", `Int exp_sec)
9090+ ; ("scope", `String orig_req.scope)
9191+ ; ("aud", `String ("https://" ^ Env.hostname))
9292+ ; ("cnf", `Assoc [("jkt", `String proof.jkt)]) ]
9393+ in
9494+ let access_token =
9595+ Jwt.sign_jwt claims ~typ:"at+jwt" Env.jwt_key
9696+ in
9797+ let%lwt () =
9898+ Queries.insert_oauth_token ctx.db
9999+ { refresh_token
100100+ ; client_id= req.client_id
101101+ ; did
102102+ ; dpop_jkt= proof.jkt
103103+ ; scope= orig_req.scope
104104+ ; expires_at }
105105+ in
106106+ let nonce = Dpop.next_nonce () in
107107+ Dream.json
108108+ ~headers:
109109+ [ ("DPoP-Nonce", nonce)
110110+ ; ("Access-Control-Expose-Headers", "DPoP-Nonce")
111111+ ; ("Cache-Control", "no-store") ]
112112+ @@ Yojson.Safe.to_string
113113+ @@ `Assoc
114114+ [ ("access_token", `String access_token)
115115+ ; ("token_type", `String "DPoP")
116116+ ; ("refresh_token", `String refresh_token)
117117+ ; ("expires_in", `Int expires_in)
118118+ ; ("scope", `String orig_req.scope)
119119+ ; ("sub", `String did) ] ) ) ) )
120120+ | "refresh_token" -> (
121121+ match req.refresh_token with
122122+ | None ->
123123+ Errors.invalid_request "refresh_token required"
124124+ | Some refresh_token -> (
125125+ let%lwt token_record =
126126+ Queries.get_oauth_token_by_refresh ctx.db refresh_token
127127+ in
128128+ match token_record with
129129+ | None ->
130130+ Errors.invalid_request "invalid refresh token"
131131+ | Some session ->
132132+ if session.client_id <> req.client_id then
133133+ Errors.invalid_request "client_id mismatch"
134134+ else if session.dpop_jkt <> proof.jkt then
135135+ Errors.invalid_request "DPoP key mismatch"
136136+ else
137137+ let new_token_id =
138138+ "tok-"
139139+ ^ Uuidm.to_string
140140+ (Uuidm.v4_gen (Random.State.make_self_init ()) ())
141141+ in
142142+ let new_refresh =
143143+ "ref-"
144144+ ^ Uuidm.to_string
145145+ (Uuidm.v4_gen (Random.State.make_self_init ()) ())
146146+ in
147147+ let now_sec = int_of_float (Unix.gettimeofday ()) in
148148+ let expires_in = Constants.access_token_expiry_ms / 1000 in
149149+ let exp_sec = now_sec + expires_in in
150150+ let new_expires_at = exp_sec * 1000 in
151151+ let claims =
152152+ `Assoc
153153+ [ ("jti", `String new_token_id)
154154+ ; ("sub", `String session.did)
155155+ ; ("iat", `Int now_sec)
156156+ ; ("exp", `Int exp_sec)
157157+ ; ("scope", `String session.scope)
158158+ ; ("aud", `String ("https://" ^ Env.hostname))
159159+ ; ("cnf", `Assoc [("jkt", `String proof.jkt)]) ]
160160+ in
161161+ let new_access_token =
162162+ Jwt.sign_jwt claims ~typ:"at+jwt" Env.jwt_key
163163+ in
164164+ let%lwt () =
165165+ Queries.update_oauth_token ctx.db
166166+ ~old_refresh_token:refresh_token
167167+ ~new_refresh_token:new_refresh ~expires_at:new_expires_at
168168+ in
169169+ Dream.json ~headers:[("Cache-Control", "no-store")]
170170+ @@ Yojson.Safe.to_string
171171+ @@ `Assoc
172172+ [ ("access_token", `String new_access_token)
173173+ ; ("token_type", `String "DPoP")
174174+ ; ("refresh_token", `String new_refresh)
175175+ ; ("expires_in", `Int expires_in)
176176+ ; ("scope", `String session.scope)
177177+ ; ("sub", `String session.did) ] ) )
178178+ | _ ->
179179+ Errors.invalid_request ("unsupported grant_type: " ^ req.grant_type) )
+10-10
pegasus/lib/api/repo/createAccount.ml
···5757 let%lwt did =
5858 match input.did with
5959 | Some did -> (
6060- match%lwt Data_store.get_actor_by_identifier did ctx.db with
6161- | Some _ ->
6262- Errors.invalid_request "an account with that did already exists"
6363- | None ->
6464- Lwt.return did )
6060+ match%lwt Data_store.get_actor_by_identifier did ctx.db with
6161+ | Some _ ->
6262+ Errors.invalid_request "an account with that did already exists"
6363+ | None ->
6464+ Lwt.return did )
6565 | None -> (
6666 let sk_did = Kleidos.K256.pubkey_to_did_key signing_pubkey in
6767 let rotation_did_keys =
···7979 let%lwt _ =
8080 match input.invite_code with
8181 | Some code -> (
8282- match%lwt Data_store.use_invite ~code ctx.db with
8383- | Some _ ->
8484- Lwt.return ()
8585- | None ->
8686- failwith "failed to use invite code" )
8282+ match%lwt Data_store.use_invite ~code ctx.db with
8383+ | Some _ ->
8484+ Lwt.return ()
8585+ | None ->
8686+ failwith "failed to use invite code" )
8787 | None ->
8888 Lwt.return ()
8989 in
···1515 | Admin
1616 | Access of {did: string}
1717 | Refresh of {did: string; jti: string}
1818+ | OAuth of {did: string; proof: Oauth.Dpop.proof}
1919+ | DPoP of {proof: Oauth.Dpop.proof}
18201921let verify_bearer_jwt t token expected_scope =
2022 match Jwt.verify_jwt token Env.jwt_key with
···4244 match credentials with
4345 | Admin ->
4446 true
4545- | Access {did= creds} when creds = did ->
4747+ | (Access {did= creds} | OAuth {did= creds; _}) when creds = did ->
4648 true
4749 | Refresh {did= creds; _} when creds = did && refresh ->
4850 true
···5052 false
51535254let get_authed_did_exn = function
5353- | Access {did} ->
5555+ | Access {did} | OAuth {did; _} ->
5456 did
5557 | Refresh {did; _} ->
5658 did
5759 | _ ->
5858- Errors.auth_required "Invalid authorization header"
6060+ Errors.auth_required "invalid authorization header"
6161+6262+let get_dpop_proof_exn = function
6363+ | OAuth {proof; _} | DPoP {proof} ->
6464+ proof
6565+ | _ ->
6666+ Errors.invalid_request "invalid DPoP header"
59676068let get_session_info identifier db =
6169 let%lwt actor =
···8492module Verifiers = struct
8593 open struct
8694 let parse_header req expected_type =
8787- match Dream.header req "authorization" with
9595+ match Dream.header req "Authorization" with
8896 | Some header -> (
8997 match String.split_on_char ' ' header with
9098 | [typ; token]
···95103 Error "invalid authorization header" )
96104 | None ->
97105 Error "missing authorization header"
106106+ end
981079999- let parse_basic req =
100100- match parse_header req "Basic" with
101101- | Ok token -> (
102102- match Base64.decode token with
103103- | Ok decoded -> (
104104- match Str.bounded_split (Str.regexp_string ":") decoded 2 with
105105- | [username; password] ->
106106- Ok (username, password)
107107- | _ ->
108108- Error "invalid basic authorization header" )
109109- | Error _ ->
108108+ let parse_basic req =
109109+ match parse_header req "Basic" with
110110+ | Ok token -> (
111111+ match Base64.decode token with
112112+ | Ok decoded -> (
113113+ match Str.bounded_split (Str.regexp_string ":") decoded 2 with
114114+ | [username; password] ->
115115+ Ok (username, password)
116116+ | _ ->
110117 Error "invalid basic authorization header" )
111118 | Error _ ->
112112- Error "invalid basic authorization header"
119119+ Error "invalid basic authorization header" )
120120+ | Error _ ->
121121+ Error "invalid basic authorization header"
113122114114- let parse_bearer req = parse_header req "Bearer"
115115- end
123123+ let parse_bearer req = parse_header req "Bearer"
124124+125125+ let parse_dpop req = parse_header req "DPoP"
116126117127 type ctx = {req: Dream.request; db: Data_store.t}
118128···122132 fun {req; _} ->
123133 match Dream.header req "authorization" with
124134 | Some _ ->
125125- Lwt.return_error @@ Errors.auth_required "Invalid authorization header"
135135+ Lwt.return_error @@ Errors.auth_required "invalid authorization header"
126136 | None ->
127137 Lwt.return_ok Unauthenticated
128138···134144 | "admin", p when p = Env.admin_password ->
135145 Lwt.return_ok Admin
136146 | _ ->
137137- Lwt.return_error @@ Errors.auth_required "Invalid credentials" )
147147+ Lwt.return_error @@ Errors.auth_required "invalid credentials" )
138148 | Error _ ->
139139- Lwt.return_error @@ Errors.auth_required "Invalid authorization header"
149149+ Lwt.return_error @@ Errors.auth_required "invalid authorization header"
140150141141- let access : verifier =
151151+ let bearer : verifier =
142152 fun {req; db} ->
143153 match parse_bearer req with
144154 | Ok jwt -> (
145145- match%lwt verify_bearer_jwt db jwt "com.atproto.access" with
146146- | Ok {sub= did; _} -> (
147147- match%lwt Data_store.get_actor_by_identifier did db with
148148- | Some {deactivated_at= None; _} ->
149149- Lwt.return_ok (Access {did})
150150- | Some {deactivated_at= Some _; _} ->
151151- Lwt.return_error
152152- @@ Errors.auth_required ~name:"AccountDeactivated"
153153- "Account is deactivated"
154154- | None ->
155155- Lwt.return_error @@ Errors.auth_required "Invalid credentials" )
156156- | Error _ ->
157157- Lwt.return_error @@ Errors.auth_required "Invalid credentials" )
155155+ match%lwt verify_bearer_jwt db jwt "com.atproto.access" with
156156+ | Ok {sub= did; _} -> (
157157+ match%lwt Data_store.get_actor_by_identifier did db with
158158+ | Some {deactivated_at= None; _} ->
159159+ Lwt.return_ok (Access {did})
160160+ | Some {deactivated_at= Some _; _} ->
161161+ Lwt.return_error
162162+ @@ Errors.auth_required ~name:"AccountDeactivated"
163163+ "account is deactivated"
164164+ | None ->
165165+ Lwt.return_error @@ Errors.auth_required "invalid credentials" )
166166+ | Error _ ->
167167+ Lwt.return_error @@ Errors.auth_required "invalid credentials" )
158168 | Error _ ->
159159- Lwt.return_error @@ Errors.auth_required "Invalid authorization header"
169169+ Lwt.return_error @@ Errors.auth_required "invalid authorization header"
170170+171171+ let dpop : verifier =
172172+ fun {req; _} ->
173173+ let dpop_header = Dream.header req "DPoP" in
174174+ match
175175+ Oauth.Dpop.verify_dpop_proof
176176+ ~mthd:(Dream.method_to_string @@ Dream.method_ req)
177177+ ~url:(Dream.target req) ~dpop_header ()
178178+ with
179179+ | Error "use_dpop_nonce" ->
180180+ Lwt.return_error @@ Errors.use_dpop_nonce ()
181181+ | Error e ->
182182+ Lwt.return_error @@ Errors.invalid_request ("dpop error: " ^ e)
183183+ | Ok proof ->
184184+ Lwt.return_ok (DPoP {proof})
185185+186186+ let oauth : verifier =
187187+ fun {req; db} ->
188188+ match parse_dpop req with
189189+ | Error e ->
190190+ Lwt.return_error @@ Errors.invalid_request ("dpop error: " ^ e)
191191+ | Ok token -> (
192192+ match%lwt dpop {req; db} with
193193+ | Error e ->
194194+ Lwt.return_error e
195195+ | Ok (DPoP {proof}) -> (
196196+ match Jwt.verify_jwt token Env.jwt_key with
197197+ | Error e ->
198198+ Lwt.return_error @@ Errors.auth_required e
199199+ | Ok (_header, claims) -> (
200200+ let open Yojson.Safe.Util in
201201+ try
202202+ let did = claims |> member "sub" |> to_string in
203203+ let exp = claims |> member "exp" |> to_int in
204204+ let jkt_claim =
205205+ claims |> member "cnf" |> member "jkt" |> to_string
206206+ in
207207+ let now = int_of_float (Unix.gettimeofday ()) in
208208+ if jkt_claim <> proof.jkt then
209209+ Lwt.return_error @@ Errors.auth_required "dpop key mismatch"
210210+ else if exp < now then
211211+ Lwt.return_error @@ Errors.auth_required "token expired"
212212+ else
213213+ let%lwt session =
214214+ try%lwt
215215+ let%lwt sess = get_session_info did db in
216216+ Lwt.return_ok sess
217217+ with _ ->
218218+ Lwt.return_error
219219+ @@ Errors.auth_required "invalid credentials"
220220+ in
221221+ match session with
222222+ | Ok {active= Some true; _} ->
223223+ Lwt.return_ok (OAuth {did; proof})
224224+ | Ok _ ->
225225+ Lwt.return_error
226226+ @@ Errors.auth_required ~name:"AccountDeactivated"
227227+ "account is deactivated"
228228+ | Error _ ->
229229+ Lwt.return_error
230230+ @@ Errors.auth_required "invalid credentials"
231231+ with _ ->
232232+ Lwt.return_error @@ Errors.auth_required "malformed JWT claims" )
233233+ )
234234+ | Ok _ ->
235235+ Lwt.return_error @@ Errors.auth_required "invalid credentials" )
160236161237 let refresh : verifier =
162238 fun {req; db} ->
163239 match parse_bearer req with
164240 | Ok jwt -> (
165165- match%lwt verify_bearer_jwt db jwt "com.atproto.refresh" with
166166- | Ok {sub= did; jti; _} -> (
167167- match%lwt Data_store.get_actor_by_identifier did db with
168168- | Some {deactivated_at= None; _} ->
169169- Lwt.return_ok (Refresh {did; jti})
170170- | Some {deactivated_at= Some _; _} ->
171171- Lwt.return_error
172172- @@ Errors.auth_required ~name:"AccountDeactivated"
173173- "Account is deactivated"
174174- | None ->
175175- Lwt.return_error @@ Errors.auth_required "Invalid credentials" )
176176- | Error "" | Error _ ->
177177- Lwt.return_error @@ Errors.auth_required "Invalid credentials" )
241241+ match%lwt verify_bearer_jwt db jwt "com.atproto.refresh" with
242242+ | Ok {sub= did; jti; _} -> (
243243+ match%lwt Data_store.get_actor_by_identifier did db with
244244+ | Some {deactivated_at= None; _} ->
245245+ Lwt.return_ok (Refresh {did; jti})
246246+ | Some {deactivated_at= Some _; _} ->
247247+ Lwt.return_error
248248+ @@ Errors.auth_required ~name:"AccountDeactivated"
249249+ "account is deactivated"
250250+ | None ->
251251+ Lwt.return_error @@ Errors.auth_required "invalid credentials" )
252252+ | Error "" | Error _ ->
253253+ Lwt.return_error @@ Errors.auth_required "invalid credentials" )
178254 | Error _ ->
179179- Lwt.return_error @@ Errors.auth_required "Invalid authorization header"
255255+ Lwt.return_error @@ Errors.auth_required "invalid authorization header"
180256181257 let authorization : verifier =
182258 fun ctx ->
···187263 | Some ("Basic" :: _) ->
188264 admin ctx
189265 | Some ("Bearer" :: _) ->
190190- access ctx
266266+ bearer ctx
267267+ | Some ("DPoP" :: _) ->
268268+ oauth ctx
191269 | _ ->
192270 Lwt.return_error
193271 @@ Errors.auth_required ~name:"InvalidToken"
194194- "Unexpected authorization type"
272272+ "unexpected authorization type"
195273196274 let any : verifier =
197275 fun ctx -> try authorization ctx with _ -> unauthenticated ctx
198276199199- type t = Unauthenticated | Admin | Access | Refresh | Authorization | Any
277277+ type t =
278278+ | Unauthenticated
279279+ | Admin
280280+ | Bearer
281281+ | DPoP
282282+ | OAuth
283283+ | Refresh
284284+ | Authorization
285285+ | Any
200286201287 let of_t = function
202288 | Unauthenticated ->
203289 unauthenticated
204290 | Admin ->
205291 admin
206206- | Access ->
207207- access
292292+ | Bearer ->
293293+ bearer
294294+ | DPoP ->
295295+ dpop
296296+ | OAuth ->
297297+ oauth
208298 | Refresh ->
209299 refresh
210300 | Authorization ->
+115-23
pegasus/lib/data_store.ml
···3636 created_at INTEGER NOT NULL,
3737 deactivated_at INTEGER
3838 )
3939- |sql}]
3939+ |sql}]
4040 () conn
4141 in
4242 let$! () =
···5252 [%rapper
5353 execute
5454 {sql| CREATE TABLE IF NOT EXISTS invite_codes (
5555- code TEXT PRIMARY KEY,
5656- did TEXT NOT NULL,
5757- remaining INTEGER NOT NULL
5858- )
5959- |sql}]
5555+ code TEXT PRIMARY KEY,
5656+ did TEXT NOT NULL,
5757+ remaining INTEGER NOT NULL
5858+ )
5959+ |sql}]
6060 () conn
6161 in
6262 let$! () =
6363 [%rapper
6464 execute
6565 {sql| CREATE TABLE IF NOT EXISTS firehose (
6666- seq INTEGER PRIMARY KEY,
6767- time INTEGER NOT NULL,
6868- t TEXT NOT NULL,
6969- data BLOB NOT NULL
7070- )
7171- |sql}]
6666+ seq INTEGER PRIMARY KEY,
6767+ time INTEGER NOT NULL,
6868+ t TEXT NOT NULL,
6969+ data BLOB NOT NULL
7070+ )
7171+ |sql}]
7272 () conn
7373 in
7474- [%rapper
7575- execute
7676- (* no need to store issued tokens, just revoked ones; stolen from millipds https://github.com/DavidBuchanan314/millipds/blob/8f89a01e7d367a2a46f379960e9ca50347dcce71/src/millipds/database.py#L253 *)
7777- {sql| CREATE TABLE IF NOT EXISTS revoked_tokens (
7878- did TEXT NOT NULL,
7979- jti TEXT NOT NULL,
8080- revoked_at INTEGER NOT NULL,
8181- PRIMARY KEY (did, jti)
8282- )
8383- |sql}]
8484- () conn
7474+ let$! () =
7575+ [%rapper
7676+ execute
7777+ (* no need to store issued tokens, just revoked ones; stolen from millipds https://github.com/DavidBuchanan314/millipds/blob/8f89a01e7d367a2a46f379960e9ca50347dcce71/src/millipds/database.py#L253 *)
7878+ {sql| CREATE TABLE IF NOT EXISTS revoked_tokens (
7979+ did TEXT NOT NULL,
8080+ jti TEXT NOT NULL,
8181+ revoked_at INTEGER NOT NULL,
8282+ PRIMARY KEY (did, jti)
8383+ )
8484+ |sql}]
8585+ () conn
8686+ in
8787+ let$! () =
8888+ [%rapper
8989+ execute
9090+ {sql| CREATE TABLE IF NOT EXISTS oauth_requests (
9191+ request_id TEXT PRIMARY KEY,
9292+ client_id TEXT NOT NULL,
9393+ request_data TEXT NOT NULL,
9494+ dpop_jkt TEXT,
9595+ expires_at INTEGER NOT NULL,
9696+ created_at INTEGER NOT NULL
9797+ )
9898+ |sql}]
9999+ () conn
100100+ in
101101+ let$! () =
102102+ [%rapper
103103+ execute
104104+ {sql| CREATE TABLE IF NOT EXISTS oauth_codes (
105105+ code TEXT PRIMARY KEY,
106106+ request_id TEXT NOT NULL REFERENCES oauth_requests(request_id) ON DELETE CASCADE,
107107+ authorized_by TEXT,
108108+ authorized_at INTEGER,
109109+ expires_at INTEGER NOT NULL,
110110+ used BOOLEAN DEFAULT FALSE
111111+ )
112112+ |sql}]
113113+ () conn
114114+ in
115115+ let$! () =
116116+ [%rapper
117117+ execute
118118+ {sql| CREATE TABLE IF NOT EXISTS oauth_tokens (
119119+ refresh_token TEXT UNIQUE NOT NULL,
120120+ client_id TEXT NOT NULL,
121121+ did TEXT NOT NULL,
122122+ dpop_jkt TEXT,
123123+ scope TEXT NOT NULL,
124124+ expires_at INTEGER NOT NULL
125125+ )
126126+ |sql}]
127127+ () conn
128128+ in
129129+ let$! () =
130130+ [%rapper
131131+ execute
132132+ {sql| CREATE INDEX IF NOT EXISTS oauth_requests_expires_idx ON oauth_requests(expires_at);
133133+ CREATE INDEX IF NOT EXISTS oauth_codes_expires_idx ON oauth_codes(expires_at);
134134+ CREATE INDEX IF NOT EXISTS oauth_tokens_refresh_idx ON oauth_tokens(refresh_token);
135135+ |sql}]
136136+ () conn
137137+ in
138138+ let$! () =
139139+ [%rapper
140140+ execute
141141+ {sql| CREATE TRIGGER IF NOT EXISTS cleanup_expired_oauth_requests
142142+ AFTER INSERT ON oauth_requests
143143+ BEGIN
144144+ DELETE FROM oauth_requests WHERE expires_at < unixepoch() * 1000;
145145+ END
146146+ |sql}
147147+ syntax_off]
148148+ () conn
149149+ in
150150+ let$! () =
151151+ [%rapper
152152+ execute
153153+ {sql| CREATE TRIGGER IF NOT EXISTS cleanup_expired_oauth_codes
154154+ AFTER INSERT ON oauth_codes
155155+ BEGIN
156156+ DELETE FROM oauth_codes WHERE expires_at < unixepoch() * 1000 OR used = 1;
157157+ END
158158+ |sql}
159159+ syntax_off]
160160+ () conn
161161+ in
162162+ let$! () =
163163+ [%rapper
164164+ execute
165165+ {sql| CREATE TRIGGER IF NOT EXISTS cleanup_expired_oauth_tokens
166166+ AFTER INSERT ON oauth_tokens
167167+ BEGIN
168168+ DELETE FROM oauth_tokens WHERE expires_at < unixepoch() * 1000;
169169+ END
170170+ |sql}
171171+ syntax_off]
172172+ () conn
173173+ in
174174+ Lwt.return_ok ()
8517586176 let create_actor =
87177 [%rapper
···221311type t = Util.caqti_pool
222312223313let connect ?create ?write () : t Lwt.t =
314314+ if create = Some true then
315315+ Util.mkfile_p Util.Constants.pegasus_db_filepath ~perm:0o644 ;
224316 Util.connect_sqlite ?create ?write Util.Constants.pegasus_db_location
225317226318let init conn : unit Lwt.t = Util.use_pool conn Queries.create_tables
···11open Cohttp_lwt
22-open Cohttp_lwt_unix
3243let did_regex =
54 Str.regexp {|^did:([a-z]+):([a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-])$|}
···1211 let uri =
1312 Uri.of_string ("https://" ^ handle ^ "/.well-known/atproto-did")
1413 in
1515- let%lwt {status; _}, body = Client.get uri in
1414+ let%lwt {status; _}, body = Util.http_get uri in
1615 match status with
1716 | `OK ->
1817 let%lwt did = Body.to_string body in
···164163 ~path:(Uri.pct_encode did) ()
165164 in
166165 let%lwt {status; _}, body =
167167- Client.get uri
166166+ Util.http_get uri
168167 ~headers:(Cohttp.Header.of_list [("Accept", "application/json")])
169168 in
170169 match status with
···186185 ~path:"/.well-known/did.json" ()
187186 in
188187 let%lwt {status; _}, body =
189189- Client.get uri
188188+ Util.http_get uri
190189 ~headers:(Cohttp.Header.of_list [("Accept", "application/json")])
191190 in
192191 match status with
+20-26
pegasus/lib/jwt.ml
···1919let b64_decode str =
2020 match Base64.decode ~pad:false ~alphabet:Base64.uri_safe_alphabet str with
2121 | Ok s ->
2222- Ok s
2222+ s
2323 | Error (`Msg e) ->
2424- Error e
2424+ failwith e
25252626let extract_signature_components signature =
2727 if Bytes.length signature <> 64 then failwith "expected 64 byte jwt signature"
···3030 let s = Bytes.sub signature 32 32 in
3131 (r, s)
32323333-let sign_jwt payload signing_key =
3333+let sign_jwt payload ?(typ = "JWT") signing_key =
3434 let _, (module Curve : Kleidos.CURVE) = signing_key in
3535 let alg =
3636 match Curve.name with
···5151 failwith "invalid curve"
5252 in
5353 let header_json =
5454- `Assoc [("alg", `String alg); ("crv", `String crv); ("typ", `String "JWT")]
5454+ `Assoc [("alg", `String alg); ("crv", `String crv); ("typ", `String typ)]
5555 in
5656 let encoded_header = header_json |> Yojson.Safe.to_string |> b64_encode in
5757 let encoded_payload = payload |> Yojson.Safe.to_string |> b64_encode in
···6565let decode_jwt jwt =
6666 match String.split_on_char '.' jwt with
6767 | [header_b64; payload_b64; _] -> (
6868- match (b64_decode header_b64, b64_decode payload_b64) with
6969- | Ok header_str, Ok payload_str -> (
7070- try
7171- let header = Yojson.Safe.from_string header_str in
7272- let payload = Yojson.Safe.from_string payload_str in
7373- Ok (header, payload)
7474- with _ -> Error "invalid json in jwt" )
7575- | Error e, _ | _, Error e ->
7676- Error e )
6868+ try
6969+ let header = Yojson.Safe.from_string (b64_decode header_b64) in
7070+ let payload = Yojson.Safe.from_string (b64_decode payload_b64) in
7171+ Ok (header, payload)
7272+ with _ -> Error "invalid jwt" )
7773 | _ ->
7874 Error "invalid jwt format"
79758076let verify_jwt jwt pubkey =
8177 match String.split_on_char '.' jwt with
8282- | [header_b64; payload_b64; signature_b64] -> (
8383- match b64_decode signature_b64 with
8484- | Error e ->
8585- Error e
8686- | Ok signature_str ->
8787- let signature = Bytes.of_string signature_str in
8888- let signing_input = header_b64 ^ "." ^ payload_b64 in
8989- let verified =
9090- Kleidos.verify ~pubkey ~msg:(Bytes.of_string signing_input) ~signature
9191- in
9292- if verified then decode_jwt jwt
9393- else Error "jwt signature verification failed" )
7878+ | [header_b64; payload_b64; signature_b64] ->
7979+ let signature = Bytes.of_string (b64_decode signature_b64) in
8080+ let signing_input = header_b64 ^ "." ^ payload_b64 in
8181+ let verified =
8282+ Kleidos.verify ~pubkey ~msg:(Bytes.of_string signing_input) ~signature
8383+ in
8484+ if verified then decode_jwt jwt
8585+ else Error "jwt signature verification failed"
9486 | _ ->
9587 Error "invalid jwt format"
9688···9890 let now_s = int_of_float (Unix.gettimeofday ()) in
9991 let access_exp = now_s + Defaults.access_token_exp in
10092 let refresh_exp = now_s + Defaults.refresh_token_exp in
101101- let jti = Uuidm.v4_gen (Random.get_state ()) () |> Uuidm.to_string in
9393+ let jti =
9494+ Uuidm.v4_gen (Random.State.make_self_init ()) () |> Uuidm.to_string
9595+ in
10296 let access_payload =
10397 symmetric_jwt_to_yojson
10498 { scope= "com.atproto.access"
+45
pegasus/lib/oauth/client.ml
···11+open Types
22+33+let fetch_client_metadata client_id : client_metadata Lwt.t =
44+ let%lwt {status; _}, res = Util.http_get (Uri.of_string client_id) in
55+ if status <> `OK then
66+ let%lwt () = Cohttp_lwt.Body.drain_body res in
77+ failwith
88+ (Printf.sprintf "client metadata not found; http %d"
99+ (Cohttp.Code.code_of_status status) )
1010+ else
1111+ let%lwt body = Cohttp_lwt.Body.to_string res in
1212+ let json = Yojson.Safe.from_string body in
1313+ let metadata =
1414+ match client_metadata_of_yojson json with
1515+ | Ok metadata ->
1616+ metadata
1717+ | Error err ->
1818+ failwith err
1919+ in
2020+ if metadata.client_id <> client_id then failwith "client_id mismatch"
2121+ else
2222+ let scopes = String.split_on_char ' ' metadata.scope in
2323+ if not (List.mem "atproto" scopes) then
2424+ failwith "scope must include 'atproto'"
2525+ else
2626+ List.iter
2727+ (function
2828+ | "authorization_code" | "refresh_token" ->
2929+ ()
3030+ | grant ->
3131+ failwith ("invalid grant type: " ^ grant) )
3232+ metadata.grant_types ;
3333+ List.iter
3434+ (fun uri ->
3535+ let u = Uri.of_string uri in
3636+ let host = Uri.host u in
3737+ match Uri.scheme u with
3838+ | Some "https" when host <> Some "localhost" ->
3939+ ()
4040+ | Some "http" when host = Some "127.0.0.1" || host = Some "[::1]" ->
4141+ ()
4242+ | _ ->
4343+ failwith ("invalid redirect_uri: " ^ uri) )
4444+ metadata.redirect_uris ;
4545+ Lwt.return metadata
···302302 did
303303 in
304304 let headers = Http.Header.init_with "Accept" "application/json" in
305305- let%lwt res, body = Client.get ~headers uri in
305305+ let%lwt res, body = Util.http_get ~headers uri in
306306 match res.status with
307307 | `OK ->
308308 let%lwt body = Body.to_string body in
+11-11
pegasus/lib/repository.ml
···180180 let%lwt map = get_map t in
181181 String_map.bindings map
182182 |> List.filter (fun (path, _) ->
183183- String.starts_with ~prefix:(path ^ "/") collection )
183183+ String.starts_with ~prefix:(path ^ "/") collection )
184184 |> Lwt_list.fold_left_s
185185 (fun acc (path, cid) ->
186186 match%lwt User_store.get_record t.db path with
···320320 let%lwt () =
321321 match old_cid with
322322 | Some _ -> (
323323- match%lwt User_store.get_record t.db path with
324324- | Some record ->
325325- let refs =
326326- Util.find_blob_refs record.value
327327- |> List.map (fun (r : Mist.Blob_ref.t) -> r.ref)
328328- in
329329- let%lwt () = User_store.clear_blob_refs t.db path refs in
330330- Lwt.return_unit
331331- | None ->
332332- Lwt.return_unit )
323323+ match%lwt User_store.get_record t.db path with
324324+ | Some record ->
325325+ let refs =
326326+ Util.find_blob_refs record.value
327327+ |> List.map (fun (r : Mist.Blob_ref.t) -> r.ref)
328328+ in
329329+ let%lwt () = User_store.clear_blob_refs t.db path refs in
330330+ Lwt.return_unit
331331+ | None ->
332332+ Lwt.return_unit )
333333 | None ->
334334 Lwt.return_unit
335335 in
+28-28
pegasus/lib/sequencer.ml
···330330 let blobs =
331331 j |> member "blobs" |> to_list
332332 |> List.filter_map (fun x ->
333333- match Cid.of_yojson x with Ok c -> Some c | _ -> None )
333333+ match Cid.of_yojson x with Ok c -> Some c | _ -> None )
334334 in
335335 let prev_data =
336336 match j |> member "prevData" with
···342342 let ops =
343343 j |> member "ops" |> to_list
344344 |> List.map (fun opj ->
345345- let action =
346346- match opj |> member "action" |> to_string with
347347- | "create" ->
348348- `Create
349349- | "update" ->
350350- `Update
351351- | "delete" ->
352352- `Delete
353353- | _ ->
354354- `Create
355355- in
356356- let path = opj |> member "path" |> to_string in
357357- let cid =
358358- match opj |> member "cid" with
359359- | `Null ->
360360- None
361361- | v -> (
362362- match Cid.of_yojson v with Ok c -> Some c | _ -> None )
363363- in
364364- let prev =
365365- match opj |> member "prev" with
366366- | `Null ->
367367- None
368368- | v -> (
369369- match Cid.of_yojson v with Ok c -> Some c | _ -> None )
370370- in
371371- {action; path; cid; prev} )
345345+ let action =
346346+ match opj |> member "action" |> to_string with
347347+ | "create" ->
348348+ `Create
349349+ | "update" ->
350350+ `Update
351351+ | "delete" ->
352352+ `Delete
353353+ | _ ->
354354+ `Create
355355+ in
356356+ let path = opj |> member "path" |> to_string in
357357+ let cid =
358358+ match opj |> member "cid" with
359359+ | `Null ->
360360+ None
361361+ | v -> (
362362+ match Cid.of_yojson v with Ok c -> Some c | _ -> None )
363363+ in
364364+ let prev =
365365+ match opj |> member "prev" with
366366+ | `Null ->
367367+ None
368368+ | v -> (
369369+ match Cid.of_yojson v with Ok c -> Some c | _ -> None )
370370+ in
371371+ {action; path; cid; prev} )
372372 in
373373 Ok
374374 { rebase