view release on metacpan or search on metacpan
$at->oauth_callback( $code, $state );
```
See the demonstration scripts `eg/bsky_oauth.pl` and `eg/mojo_oauth.pl` for both a CLI and web based examples.
Once authenticated, you should store your session data securely so you can resume it later without requiring the user
to log in again.
### Resuming an OAuth Session
You need to store the tokens, the DPoP key, and the PDS endpoint. The `_raw` method on the session object provides a
simple hash for this purpose:
```perl
# After login, save the session
my $data = $at->session->_raw;
# ... store $data securely ...
# Later, resume the session
$at->resume(
$data->{accessJwt},
supports [Mojo::UserAgent](https://metacpan.org/pod/Mojo%3A%3AUserAgent) so you should usually use [Mojo::IOLoop](https://metacpan.org/pod/Mojo%3A%3AIOLoop):
```perl
use Mojo::IOLoop;
# ... setup firehose ...
Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
```
# Lexicon Caching
The AT Protocol defines its API endpoints using "Lexicons" (JSON schemas). This library uses these schemas to
automatically coerce API responses into Perl objects.
## How it works
When you call a method like `app.bsky.actor.getProfile`, the library:
- 1. **Checks user-provided paths:** It looks in any directories passed to `lexicon_paths`.
- 2. **Checks local storage:** It looks for the schema in the distribution's `share` directory.
- 3. **Checks user cache:** It looks in `~/.cache/atproto/lexicons/`.
- 4. **Downloads if missing:** If not found, it automatically downloads the schema from the
Returns the DID of the authenticated user.
## `peer_id_for_did( $did )`
Resolves an AT Protocol DID to a libp2p PeerID. This is used to discover the user's data on the P2P network.
## `get_repo_head( $did )`
Retrieves the current MST (Merkle Search Tree) root CID for a user's repository via the `com.atproto.sync.getHead`
endpoint.
## `get_block( $cid_str, [ $target_peer_id ] )`
Retrieves a raw block by its CID. If an `ipfs_node` was provided to the constructor, this method will:
- Check the local blockstore.
- Attempt to fetch the block via Bitswap from the provided `$target_peer_id`.
- Fall back to the centralized PDS via HTTP if the block is not found in the P2P network.
Returns a [Future](https://metacpan.org/pod/Future) that resolves to the block data.
code_verifier => $code_verifier,
state => $state,
redirect_uri => $redirect_uri,
client_id => $client_id,
handle => $handle,
scope => $scope
};
# Prepare UA for DPoP
$http->set_tokens( undef, undef, 'DPoP', $self->_get_dpop_key() );
my $par_endpoint = $discovery->{metadata}{pushed_authorization_request_endpoint};
my $par_content = {
client_id => $client_id,
response_type => 'code',
code_challenge => $code_challenge,
code_challenge_method => 'S256',
redirect_uri => $redirect_uri,
state => $state,
scope => $scope,
aud => $discovery->{pds},
};
say '[DEBUG] [At] PAR request: ' . JSON::PP->new->ascii->encode($par_content) if $ENV{DEBUG};
my ($par_res) = $http->post(
$par_endpoint => {
headers => { DPoP => $http->_generate_dpop_proof( $par_endpoint, 'POST', 1 ) },
encoding => 'form',
content => $par_content,
skip_ath => 1
}
);
die 'PAR failed: ' . ( $par_res . '' ) if builtin::blessed $par_res;
say '[DEBUG] [At] PAR response: ' . JSON::PP->new->ascii->encode($par_res) if $ENV{DEBUG};
my $auth_uri = URI->new( $discovery->{metadata}{authorization_endpoint} );
$auth_uri->query_form( client_id => $client_id, request_uri => $par_res->{request_uri} );
return $auth_uri->as_string;
}
method oauth_callback ( $code, $state ) {
die 'OAuth state mismatch' unless $oauth_state && $state eq $oauth_state->{state};
my $token_endpoint = $oauth_state->{discovery}{metadata}{token_endpoint};
my $key = $self->_get_dpop_key();
my ($token_res) = $http->post(
$token_endpoint => {
headers => { DPoP => $http->_generate_dpop_proof( $token_endpoint, 'POST', 1 ) },
encoding => 'form',
content => {
grant_type => 'authorization_code',
code => $code,
client_id => $oauth_state->{client_id},
redirect_uri => $oauth_state->{redirect_uri},
code_verifier => $oauth_state->{code_verifier},
aud => $oauth_state->{discovery}{pds}
},
skip_ath => 1
pds => $oauth_state->{discovery}{pds}
);
$self->set_host( $oauth_state->{discovery}{pds} );
$http->set_tokens( $token_res->{access_token}, $token_res->{refresh_token}, 'DPoP', $key );
}
method oauth_refresh() {
return unless $session && $session->refreshJwt && $session->token_type eq 'DPoP';
my $discovery = $self->oauth_discover( $session->handle );
return unless $discovery;
my $token_endpoint = $discovery->{metadata}{token_endpoint};
my $key = $self->_get_dpop_key();
my $refresh_content = {
grant_type => 'refresh_token',
refresh_token => $session->refreshJwt,
client_id => $session->client_id // '',
aud => $discovery->{pds},
};
say '[DEBUG] [At] Refresh request: ' . JSON::PP->new->ascii->encode($refresh_content) if $ENV{DEBUG};
my ($token_res) = $http->post(
$token_endpoint => {
headers => { DPoP => $http->_generate_dpop_proof( $token_endpoint, 'POST', 1 ) },
encoding => 'form',
content => $refresh_content,
skip_ath => 1
}
);
die 'Refresh failed: ' . ( $token_res . '' ) if builtin::blessed $token_res;
$session = At::Protocol::Session->new(
did => $token_res->{sub},
accessJwt => $token_res->{access_token},
refreshJwt => $token_res->{refresh_token},
return $self->_get_block_http( $cid_str, $did );
}
);
}
}
return $self->_get_block_http( $cid_str, $did );
}
method _get_block_http ( $cid_str, $did ) {
# If no DID provided, we can't fallback to sync endpoints
return Future->done(undef) unless $did;
#~ say "[HTTP] Fetching block $cid_str for $did via com.atproto.sync.getBlocks...";
# com.atproto.sync.getBlocks returns a CAR file
return Future->call(
sub {
my $car_data = $self->get( 'com.atproto.sync.getBlocks' => { did => $did, cids => [$cid_str] } );
return undef unless $car_data;
require Archive::CAR;
require Archive::CAR::v1;
See the demonstration scripts C<eg/bsky_oauth.pl> and C<eg/mojo_oauth.pl> for both a CLI and web based examples.
=back
Once authenticated, you should store your session data securely so you can resume it later without requiring the user
to log in again.
=head3 Resuming an OAuth Session
You need to store the tokens, the DPoP key, and the PDS endpoint. The C<_raw> method on the session object provides a
simple hash for this purpose:
# After login, save the session
my $data = $at->session->_raw;
# ... store $data securely ...
# Later, resume the session
$at->resume(
$data->{accessJwt},
$data->{refreshJwt},
B<Note:> The Firehose requires L<Codec::CBOR> and an async event loop to keep the connection alive. Currently, At.pm
supports L<Mojo::UserAgent> so you should usually use L<Mojo::IOLoop>:
use Mojo::IOLoop;
# ... setup firehose ...
Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
=head1 Lexicon Caching
The AT Protocol defines its API endpoints using "Lexicons" (JSON schemas). This library uses these schemas to
automatically coerce API responses into Perl objects.
=head2 How it works
When you call a method like C<app.bsky.actor.getProfile>, the library:
=over
=item 1. B<Checks user-provided paths:> It looks in any directories passed to C<lexicon_paths>.
Returns the DID of the authenticated user.
=head2 C<peer_id_for_did( $did )>
Resolves an AT Protocol DID to a libp2p PeerID. This is used to discover the user's data on the P2P network.
=head2 C<get_repo_head( $did )>
Retrieves the current MST (Merkle Search Tree) root CID for a user's repository via the C<com.atproto.sync.getHead>
endpoint.
=head2 C<get_block( $cid_str, [ $target_peer_id ] )>
Retrieves a raw block by its CID. If an C<ipfs_node> was provided to the constructor, this method will:
=over
=item Check the local blockstore.
=item Attempt to fetch the block via Bitswap from the provided C<$target_peer_id>.
lib/At/Protocol/NSID.pm view on Meta::CPAN
use At::Protocol::NSID qw[:all];
try {
ensureValidNSID( 'net.users.bob.ping' );
}
catch($err) {
...; # do something about it
}
=head1 DESCRIPTION
Namespaced Identifiers (NSIDs) are used to reference Lexicon schemas for records, XRPC endpoints, and more.
The basic structure and semantics of an NSID are a fully-qualified hostname in Reverse Domain-Name Order, followed by a
simple name. The hostname part is the B<domain authority>, and the final segment is the B<name>.
This package aims to validate them.
=head1 Functions
You may import functions by name or with the C<:all> tag.
share/lexicons/app/bsky/contact/sendNotification.json view on Meta::CPAN
{
"lexicon": 1,
"id": "app.bsky.contact.sendNotification",
"defs": {
"main": {
"type": "procedure",
"description": "System endpoint to send notifications related to contact imports. Requires role authentication.",
"input": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["from", "to"],
"properties": {
"from": {
"description": "The DID of who this notification comes from.",
"type": "string",
"format": "did"
share/lexicons/app/bsky/feed/searchPosts.json view on Meta::CPAN
{
"lexicon": 1,
"id": "app.bsky.feed.searchPosts",
"defs": {
"main": {
"type": "query",
"description": "Find posts matching search criteria, returning views of those posts. Note that this API endpoint may require authentication (eg, not public) for some service providers and implementations.",
"parameters": {
"type": "params",
"required": ["q"],
"properties": {
"q": {
"type": "string",
"description": "Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended."
},
"sort": {
"type": "string",
share/lexicons/app/bsky/unspecced/getPostThreadOtherV2.json view on Meta::CPAN
{
"lexicon": 1,
"id": "app.bsky.unspecced.getPostThreadOtherV2",
"defs": {
"main": {
"type": "query",
"description": "(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get additional posts under a thread e.g. replies hidden by threadgate. B...
"parameters": {
"type": "params",
"required": ["anchor"],
"properties": {
"anchor": {
"type": "string",
"format": "at-uri",
"description": "Reference (AT-URI) to post record. This is the anchor post."
}
}
share/lexicons/app/bsky/unspecced/getPostThreadV2.json view on Meta::CPAN
{
"lexicon": 1,
"id": "app.bsky.unspecced.getPostThreadV2",
"defs": {
"main": {
"type": "query",
"description": "(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get posts in a thread. It is based in an anchor post at any depth of the...
"parameters": {
"type": "params",
"required": ["anchor"],
"properties": {
"anchor": {
"type": "string",
"format": "at-uri",
"description": "Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post."
},
"above": {
share/lexicons/app/bsky/unspecced/getPostThreadV2.json view on Meta::CPAN
"type": "ref",
"ref": "#threadItem"
}
},
"threadgate": {
"type": "ref",
"ref": "app.bsky.feed.defs#threadgateView"
},
"hasOtherReplies": {
"type": "boolean",
"description": "Whether this thread has additional replies. If true, a call can be made to the `getPostThreadOtherV2` endpoint to retrieve them."
}
}
}
}
},
"threadItem": {
"type": "object",
"required": ["uri", "depth", "value"],
"properties": {
"uri": {
share/lexicons/com/atproto/label/queryLabels.json view on Meta::CPAN
{
"lexicon": 1,
"id": "com.atproto.label.queryLabels",
"defs": {
"main": {
"type": "query",
"description": "Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.",
"parameters": {
"type": "params",
"required": ["uriPatterns"],
"properties": {
"uriPatterns": {
"type": "array",
"items": { "type": "string" },
"description": "List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI."
},
"sources": {
share/lexicons/com/atproto/label/subscribeLabels.json view on Meta::CPAN
{
"lexicon": 1,
"id": "com.atproto.label.subscribeLabels",
"defs": {
"main": {
"type": "subscription",
"description": "Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream.",
"parameters": {
"type": "params",
"properties": {
"cursor": {
"type": "integer",
"description": "The last known event seq number to backfill from."
}
}
},
"message": {
share/lexicons/com/atproto/server/reserveSigningKey.json view on Meta::CPAN
{
"lexicon": 1,
"id": "com.atproto.server.reserveSigningKey",
"defs": {
"main": {
"type": "procedure",
"description": "Reserve a repo signing key, for use with account creation. Necessary so that a DID PLC update operation can be constructed during an account migraiton. Public and does not require auth; implemented by PDS. NOTE: this endpoint ma...
"input": {
"encoding": "application/json",
"schema": {
"type": "object",
"properties": {
"did": {
"type": "string",
"format": "did",
"description": "The DID to reserve a key for."
}
share/lexicons/com/atproto/sync/subscribeRepos.json view on Meta::CPAN
{
"lexicon": 1,
"id": "com.atproto.sync.subscribeRepos",
"defs": {
"main": {
"type": "subscription",
"description": "Repository event stream, aka Firehose endpoint. Outputs repo commits with diff data, and identity update events, for all repositories on the current server. See the atproto specifications for details around stream sequencing, re...
"parameters": {
"type": "params",
"properties": {
"cursor": {
"type": "integer",
"description": "The last known event seq number to backfill from."
}
}
},
"message": {
share/lexicons/com/atproto/sync/subscribeRepos.json view on Meta::CPAN
},
"time": {
"type": "string",
"format": "datetime",
"description": "Timestamp of when this message was originally broadcast."
}
}
},
"identity": {
"type": "object",
"description": "Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache.",
"required": ["seq", "did", "time"],
"properties": {
"seq": { "type": "integer" },
"did": { "type": "string", "format": "did" },
"time": { "type": "string", "format": "datetime" },
"handle": {
"type": "string",
"format": "handle",
"description": "The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in...
}