Packages

A small, transport-agnostic atproto client for Gleam: XRPC, identity, OAuth discovery, blobs, and repo CRUD.

Current section

Files

Jump to
atproto_client src atproto oauth core resource.gleam
Raw

src/atproto/oauth/core/resource.gleam

//// A DPoP-authenticated resource request as an `Effect`: attach
//// `Authorization: DPoP <token>` and a proof bound to the request's method,
//// URL, and access-token hash (`ath`), with the server-nonce retry. The `ath`
//// is a one-shot input the wrapper computes (a hash of the fixed access token),
//// so it comes in as a parameter; only the per-attempt proof is an effect.
import atproto/oauth/core/effect.{type Effect}
import atproto/oauth/core/transport
import atproto/xrpc
import gleam/bit_array
import gleam/http
import gleam/http/request.{type Request}
import gleam/http/response.{type Response}
import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/string
import gleam/uri
pub fn send(
req: Request(BitArray),
access_token access_token: String,
ath ath: String,
) -> Effect(Result(Response(BitArray), String)) {
let method = http.method_to_string(req.method) |> string.uppercase
let target = htu(req)
use first <- effect.then(send_once(
req,
access_token,
method,
target,
ath,
None,
))
case first {
Error(e) -> effect.done(Error(e))
Ok(first) ->
case transport.dpop_nonce_challenge(as_text(first)) {
Some(nonce) ->
send_once(req, access_token, method, target, ath, Some(nonce))
None -> effect.done(Ok(first))
}
}
}
// Only JSON bodies are sniffed for use_dpop_nonce; binary is never scanned.
fn as_text(resp: Response(BitArray)) -> Response(String) {
let json_body = case response.get_header(resp, "content-type") {
Ok(content_type) -> string.starts_with(content_type, "application/json")
Error(Nil) -> False
}
response.map(resp, fn(body) {
case json_body {
True -> bit_array.to_string(body) |> result.unwrap("")
False -> ""
}
})
}
fn send_once(
req: Request(BitArray),
access_token: String,
method: String,
target: String,
ath: String,
nonce: Option(String),
) -> Effect(Result(Response(BitArray), String)) {
use proof <- effect.then(effect.proof(
method:,
url: target,
nonce:,
ath: Some(ath),
))
case proof {
Error(e) -> effect.done(Error(e))
Ok(proof) ->
effect.fetch_bits(
req
|> request.set_header("authorization", "DPoP " <> access_token)
|> request.set_header("dpop", proof),
)
|> effect.map(result.map_error(_, xrpc.transport_error_to_string))
}
}
/// The DPoP `htu`: the request URI without query or fragment.
fn htu(req: Request(a)) -> String {
let u = request.to_uri(req)
uri.to_string(uri.Uri(..u, query: None, fragment: None))
}