From 545ca504010097f42240dd0ba09de823ecb54db0 Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Mon, 20 Jul 2026 14:16:13 -0400 Subject: [PATCH] fmt --- src/atproto_records.gleam | 5 +- src/entwine.gleam | 14 ++-- src/oauth.gleam | 147 ++++++++++++++++---------------------- src/oauth_callback.gleam | 5 +- test/entwine_test.gleam | 4 +- 5 files changed, 77 insertions(+), 98 deletions(-) diff --git a/src/atproto_records.gleam b/src/atproto_records.gleam index 1633de1..17ab0bb 100644 --- a/src/atproto_records.gleam +++ b/src/atproto_records.gleam @@ -48,10 +48,7 @@ pub fn create_record( ) let request = request - |> request.set_header( - "authorization", - "DPoP " <> credential.access_token, - ) + |> request.set_header("authorization", "DPoP " <> credential.access_token) |> request.set_header("dpop", dpop_proof) case httpc.send(request) { diff --git a/src/entwine.gleam b/src/entwine.gleam index ca92a73..e9f06c7 100644 --- a/src/entwine.gleam +++ b/src/entwine.gleam @@ -13,7 +13,9 @@ import oauth import tangled const max_concurrent_profile_checks = 40 + const oauth_scope = "atproto repo:sh.tangled.graph.follow" + const oauth_redirect_uri = "http://127.0.0.1:8080/callback" @external(erlang, "entwine_ffi", "prompt") @@ -27,7 +29,11 @@ fn oauth_client() -> oauth.OAuthClient { #("scope", oauth_scope), ]) - oauth.OAuthClient(client_id:, redirect_uri: oauth_redirect_uri, scope: oauth_scope) + oauth.OAuthClient( + client_id:, + redirect_uri: oauth_redirect_uri, + scope: oauth_scope, + ) } pub fn find_accounts_to_follow( @@ -135,11 +141,7 @@ pub fn main() -> Nil { io.println("Open the authorization URL in your browser to continue.") let credential = - oauth.authenticate( - "https://" <> pds, - at_did.to_string(did), - oauth_client(), - ) + oauth.authenticate("https://" <> pds, at_did.to_string(did), oauth_client()) io.println("Authenticated successfully as " <> credential.subject <> ".") let bsky_follows = bsky.get_bsky_follows(did, pds) diff --git a/src/oauth.gleam b/src/oauth.gleam index 5e6c8e8..2cc7c6b 100644 --- a/src/oauth.gleam +++ b/src/oauth.gleam @@ -1,21 +1,21 @@ +import gleam/bit_array import gleam/dict import gleam/dynamic/decode import gleam/erlang/process +import gleam/http import gleam/http/request import gleam/http/response -import gleam/http import gleam/httpc import gleam/json import gleam/list import gleam/option.{type Option} import gleam/string -import gleam/uri import gleam/time/timestamp -import gleam/bit_array +import gleam/uri +import gose import gose/jose/jwk import gose/jose/jws -import gose import kryptos/crypto import kryptos/ec @@ -48,11 +48,7 @@ pub type OAuthSession { } pub type ParResponse { - ParResponse( - request_uri: String, - expires_in: Int, - dpop_nonce: Option(String), - ) + ParResponse(request_uri: String, expires_in: Int, dpop_nonce: Option(String)) } pub type TokenResponse { @@ -80,8 +76,6 @@ pub type OAuthCredential { ) } - - pub fn with_dpop_nonce( credential: OAuthCredential, dpop_nonce: Option(String), @@ -91,7 +85,7 @@ pub fn with_dpop_nonce( fn random_string() -> String { crypto.random_bytes(32) - |> bit_array.base64_url_encode(False) + |> bit_array.base64_url_encode(False) } pub fn get_auth_server(pds: String) -> String { @@ -142,15 +136,13 @@ pub fn get_oauth_server_metadata(auth_server: String) -> OAuthServerMetadata { decode.list(decode.string), ) - decode.success( - OAuthServerMetadata( - issuer: issuer, - authorization_endpoint: authoriization_endpoint, - token_endpoint: token_endpoint, - pushed_authorization_request_endpoint: par_endpoint, - dpop_signing_alg_values_supported: dpop_algorithms, - ) - ) + decode.success(OAuthServerMetadata( + issuer: issuer, + authorization_endpoint: authoriization_endpoint, + token_endpoint: token_endpoint, + pushed_authorization_request_endpoint: par_endpoint, + dpop_signing_alg_values_supported: dpop_algorithms, + )) } let assert Ok(metadata) = json.parse(metadata_response.body, metadata_decoder) @@ -162,10 +154,10 @@ pub fn create_pkce() -> #(String, String) { let assert Ok(h) = hash.new(hash.Sha256) let challenge = - h - |> hash.update(<>) - |> hash.final() - |> bit_array.base64_url_encode(False) + h + |> hash.update(<>) + |> hash.final() + |> bit_array.base64_url_encode(False) #(verifier, challenge) } @@ -174,13 +166,19 @@ pub fn create_dpop_key() -> gose.Key(String) { gose.generate_ec(ec.P256) } -pub fn create_dpop_proof(key: gose.Key(String), method: String, url: String, nonce: Option(String), access_token: Option(String)) -> String { +pub fn create_dpop_proof( + key: gose.Key(String), + method: String, + url: String, + nonce: Option(String), + access_token: Option(String), +) -> String { let jti = random_string() - let #(iat, _) = timestamp.system_time() + let #(iat, _) = + timestamp.system_time() |> timestamp.to_unix_seconds_and_nanoseconds() - let payload = - [ + let payload = [ #("jti", json.string(jti)), #("htm", json.string(method)), #("htu", json.string(url)), @@ -190,8 +188,10 @@ pub fn create_dpop_proof(key: gose.Key(String), method: String, url: String, non let payload = case nonce { option.None -> payload - option.Some(nonce_value) -> - [#("nonce", json.string(nonce_value)), ..payload] + option.Some(nonce_value) -> [ + #("nonce", json.string(nonce_value)), + ..payload + ] } let payload = case access_token { @@ -224,21 +224,23 @@ pub fn create_dpop_proof(key: gose.Key(String), method: String, url: String, non |> jws.with_typ("dpop+jwt") |> jws.with_header("jwk", public_jwk) - let assert Ok(signed) = - jws.sign(unsigned, key: key, payload: payload_bits) + let assert Ok(signed) = jws.sign(unsigned, key: key, payload: payload_bits) let assert Ok(dpop_proof) = jws.serialize_compact(signed) dpop_proof } -pub fn push_authorization_request(par_endpoint: String, client_id: String, redirect_uri: String, scope: String, state: String, code_challenge: String, dpop_key: gose.Key(String)) -> ParResponse { - let dpop_proof = create_dpop_proof( - dpop_key, - "POST", - par_endpoint, - option.None, - option.None, - ) +pub fn push_authorization_request( + par_endpoint: String, + client_id: String, + redirect_uri: String, + scope: String, + state: String, + code_challenge: String, + dpop_key: gose.Key(String), +) -> ParResponse { + let dpop_proof = + create_dpop_proof(dpop_key, "POST", par_endpoint, option.None, option.None) let form_body = uri.query_to_string([ @@ -255,18 +257,11 @@ pub fn push_authorization_request(par_endpoint: String, client_id: String, redir let assert Ok(par_request) = request.from_uri(par_uri) let par_request = - request.Request( - ..par_request, - method: http.Post, - body: form_body, - ) + request.Request(..par_request, method: http.Post, body: form_body) let par_request = par_request - |> request.set_header( - "content-type", - "application/x-www-form-urlencoded", - ) + |> request.set_header("content-type", "application/x-www-form-urlencoded") |> request.set_header("dpop", dpop_proof) let assert Ok(par_response) = httpc.send(par_request) @@ -278,7 +273,8 @@ pub fn push_authorization_request(par_endpoint: String, client_id: String, redir decode.success(#(request_uri, expires_in)) } - let assert Ok(#(request_uri, expires_in)) = json.parse(par_response.body, par_response_decoder) + let assert Ok(#(request_uri, expires_in)) = + json.parse(par_response.body, par_response_decoder) let dpop_nonce = response.get_header(par_response, "dpop-nonce") @@ -287,8 +283,6 @@ pub fn push_authorization_request(par_endpoint: String, client_id: String, redir ParResponse(request_uri:, expires_in:, dpop_nonce:) } - - pub fn build_authorization_url( authorization_endpoint: String, client_id: String, @@ -316,7 +310,7 @@ pub fn get_oauth_session( state: String, ) -> Option(OAuthSession) { dict.get(sessions, state) - |> option.from_result + |> option.from_result } pub fn parse_callback_url(callback_url: String) -> #(String, String, String) { @@ -341,13 +335,7 @@ pub fn exchange_code_for_tokens( dpop_nonce: Option(String), ) -> TokenResponse { let dpop_proof = - create_dpop_proof( - dpop_key, - "POST", - token_endpoint, - dpop_nonce, - option.None, - ) + create_dpop_proof(dpop_key, "POST", token_endpoint, dpop_nonce, option.None) let form_body = uri.query_to_string([ @@ -362,18 +350,11 @@ pub fn exchange_code_for_tokens( let assert Ok(token_request) = request.from_uri(token_uri) let token_request = - request.Request( - ..token_request, - method: http.Post, - body: form_body, - ) + request.Request(..token_request, method: http.Post, body: form_body) let token_request = - token_request - |> request.set_header( - "content-type", - "application/x-www-form-urlencoded", - ) + token_request + |> request.set_header("content-type", "application/x-www-form-urlencoded") |> request.set_header("dpop", dpop_proof) let assert Ok(token_response) = httpc.send(token_request) @@ -382,7 +363,6 @@ pub fn exchange_code_for_tokens( response.get_header(token_response, "dpop-nonce") |> option.from_result - let token_response_decoder = { use access_token <- decode.field("access_token", decode.string) use token_type <- decode.field("token_type", decode.string) @@ -395,17 +375,15 @@ pub fn exchange_code_for_tokens( use scope <- decode.field("scope", decode.string) use sub <- decode.field("sub", decode.string) - decode.success( - TokenResponse( - access_token:, - token_type:, - expires_in:, - refresh_token:, - scope:, - sub:, - dpop_nonce:, - ), - ) + decode.success(TokenResponse( + access_token:, + token_type:, + expires_in:, + refresh_token:, + scope:, + sub:, + dpop_nonce:, + )) } let assert Ok(tokens) = @@ -461,8 +439,7 @@ pub fn authenticate( let oauth_callback.Callback(code:, state: returned_state, issuer:) = process.receive_forever(callback_messages) - let assert option.Some(session) = - get_oauth_session(sessions, returned_state) + let assert option.Some(session) = get_oauth_session(sessions, returned_state) let assert True = issuer == session.issuer let tokens = diff --git a/src/oauth_callback.gleam b/src/oauth_callback.gleam index 778992d..b60cb8f 100644 --- a/src/oauth_callback.gleam +++ b/src/oauth_callback.gleam @@ -57,7 +57,10 @@ fn parse_callback(request: Request(mist.Connection)) -> Result(Callback, Nil) { } } -fn html_response(status: Int, body: String) -> response.Response(mist.ResponseData) { +fn html_response( + status: Int, + body: String, +) -> response.Response(mist.ResponseData) { response.new(status) |> response.set_header("content-type", "text/html; charset=utf-8") |> response.set_body(mist.Bytes(bytes_tree.from_string(body))) diff --git a/test/entwine_test.gleam b/test/entwine_test.gleam index d37e1f6..b5a7a65 100644 --- a/test/entwine_test.gleam +++ b/test/entwine_test.gleam @@ -30,8 +30,8 @@ pub fn build_authorization_url_test() { "urn:example:request-123", ) - assert authorization_url == - "https://pds.cute.haus/oauth/authorize?client_id=http%3A%2F%2Flocalhost%3Fscope%3Datproto&request_uri=urn%3Aexample%3Arequest-123" + assert authorization_url + == "https://pds.cute.haus/oauth/authorize?client_id=http%3A%2F%2Flocalhost%3Fscope%3Datproto&request_uri=urn%3Aexample%3Arequest-123" } pub fn create_pkce_returns_matching_challenge_test() { -- 2.51.2