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. Only the nonce itself is written (via the store's update_session), never the rest of the session this request started with, so it can't overwrite tokens another process has refreshed in the meantime.

  • 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 is safe to call from several processes sharing one session at once - see there) 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