NAME

Mojo::ATProto::OAuth::ResourceClient - authenticated XRPC requests against a session's own PDS

SYNOPSIS

use Mojo::ATProto::OAuth qw//;
use Mojo::ATProto::OAuth::ResourceClient qw//;
use feature 'try'; 

my $oauth  = Mojo::ATProto::OAuth->new(..., store => 'Pg');
my $client = Mojo::ATProto::OAuth::ResourceClient->new(oauth => $oauth);

# authenticated GET
my $profile = $client->request($did, $session_id, 'get', '/xrpc/app.bsky.actor.getProfile?actor=' . $did);

# authenticated POST, e.g. a putRecord with optimistic-concurrency swapRecord
try { 
    my $result = $client->request($did, $session_id, 'post', '/xrpc/com.atproto.repo.putRecord', {
        repo       => $did,
        collection => 'app.bsky.feed.post',
        rkey       => $rkey,
        record     => $record,
        swapRecord => $prior_cid,
    });
} catch($ex) {
    die $err unless $ex =~ /xrpc_error=InvalidSwap/; # re-throw the exception if it isn't an InvalidSwap
    # ... retry with a fresh $prior_cid ...
}

# non-blocking counterpart
$client->request_p($did, $session_id, 'get', '/xrpc/app.bsky.actor.getProfile?actor=' . $did)
    ->then(sub ($profile) { ... })
    ->catch(sub ($err) { ... });

DESCRIPTION

Mojo::ATProto::OAuth itself only handles the OAuth handshake (PAR, token exchange, refresh) and gives you a durable store - it has no opinion on what you do with a session afterwards. This class provides the "make an authenticated request against the resource server (the user's own PDS) with a stored session" logic. Given a $did/$session_id pair, it loads the session from the same store the $oauth instance uses, signs a DPoP proof, sends the request, and transparently handles the two ways a PDS can reject an otherwise-valid request:

  • DPoP nonce rotation (RFC 9449) - a 401 accompanied by a fresh DPoP-Nonce response header means "retry with this nonce", not a real auth failure. The resource server can also rotate the nonce on a successful response - this is persisted back to store either way, so the next call (from any session, any process) starts from the freshest known nonce.

  • access token expiry - a 401 with no fresh nonce means the access token itself needs refreshing; this calls the $oauth instance's own refresh_tokens(_p) (which persists the refreshed session itself) and retries once more.

Both of these are retried at most once (matching Mojo::ATProto::OAuth's own _post_dpop_retry/_post_dpop_retry_p 2-attempt cap), so a persistently-failing session dies cleanly instead of looping.

ATTRIBUTES

oauth

(Required.) The Mojo::ATProto::OAuth instance to operate against - its store is used to load/save sessions, and its refresh_tokens/refresh_tokens_p are called on access-token expiry.

ua

A Mojo::UserAgent instance used for every HTTP request this class makes. Defaults to $oauth->ua, so this class shares the same client (and its configured timeout) unless overridden.

log

A Mojo::Log instance for debug logging. Defaults to $oauth->log.

METHODS

request / request_p

my $json = $client->request($did, $session_id, $method, $path, $body);

Sends an authenticated XRPC request against the session's own PDS (host_url). $method is a lowercase Mojo::UserAgent verb (get, post, ...); $path is the XRPC path, including any query string, appended to host_url; $body (optional) is sent as a JSON request body. Returns the decoded JSON response.

request dies (request_p rejects) with a newline-terminated message on a non-2xx response that isn't recovered by a nonce/refresh retry. The message includes the XRPC response's machine-readable error field when present (as xrpc_error=value) alongside the human-readable message, so a caller can distinguish error kinds (e.g. a com.atproto.repo.putRecord swapRecord conflict surfacing as xrpc_error=InvalidSwap) by matching against the die message, without needing a blessed exception type.

SEE ALSO

Mojo::ATProto::OAuth, Mojo::ATProto::OAuth::DPoP