atproto_client 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +16 -7
- data/lib/atproto_client/client.rb +43 -8
- data/lib/atproto_client/dpop_handler.rb +6 -11
- data/lib/atproto_client/version.rb +1 -1
- data/lib/atproto_client.rb +0 -1
- metadata +2 -3
- data/lib/atproto_client/configuration.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 89742c30ed6ad23210c8e5eaecab1cabd26525419fe040bf1ca1f3a70ea29df7
|
4
|
+
data.tar.gz: 3b3854a97f5b74fb32e8e0cf98ad2566ab1ae652e64b543ff764286e0f0a8c8f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 88e12f5de1cb3e7075b42f07350ccccc57414e9e1293b76519b3e29a1682e166a1ec6708606de549ed22c6610add5c8176c40e776e08c1b2e64fb56f5861656a
|
7
|
+
data.tar.gz: d0a350fae5b3eded1590da41a072bf5e9b75641339b1a9e15ef8f58c828b974d67c0d88e463df6da2ea3a390340245e7de4f8f12945ecacb896133aa82d13233
|
data/README.md
CHANGED
@@ -3,6 +3,12 @@
|
|
3
3
|
Ruby client for the AT Protocol, with support for oauth/dpop authentication. It has been built and tested for bluesky but it should be agnostic of PDS. An omniauth strategy using this layer should appear soon.
|
4
4
|
The work is in progress but it should allready work and I'd be happy to have feedbacks.
|
5
5
|
|
6
|
+
### TODO
|
7
|
+
- [ ] Reject response without DPoP-Nonce header (to respect the spec)
|
8
|
+
- [ ] Try to reuse nonce better (currently we double each request to refresh it from the server each time)
|
9
|
+
- [ ] Give a better API (it changes at a hight rate currently)
|
10
|
+
|
11
|
+
|
6
12
|
## Installation
|
7
13
|
|
8
14
|
Add this line to your application's Gemfile:
|
@@ -14,22 +20,23 @@ gem 'atproto_client'
|
|
14
20
|
## Usage
|
15
21
|
|
16
22
|
```ruby
|
17
|
-
# Configure the client (optional)
|
18
|
-
AtProto.configure do |config|
|
19
|
-
config.base_url = "https://bsky.social" # default
|
20
|
-
end
|
21
23
|
|
22
24
|
# Initialize a client
|
23
|
-
client = AtProto::Client.new(
|
25
|
+
client = AtProto::Client.new(
|
26
|
+
access_token: access_token,
|
27
|
+
refresh_token: refresh_token,
|
28
|
+
private_key: private_key_used_for_access_token_creation,
|
29
|
+
refresh_token_url: "https://the-token-server.com" # optional, defaults do https://bsky.social
|
30
|
+
)
|
24
31
|
|
25
32
|
# Should be able to fetch collections
|
26
|
-
client.
|
33
|
+
client.request(
|
27
34
|
:get,
|
28
35
|
"#{PDS_URL}/xrpc/#{lexicon}",
|
29
36
|
params:{ repo: "did:therepodid", collection: "app.bsky.feed.post"}
|
30
37
|
)
|
31
38
|
|
32
|
-
# Also gives a handful DPOP handler for any request (here an
|
39
|
+
# Also gives a handful DPOP handler for any request (here an omniauth example)
|
33
40
|
dpop_handler = AtProto::DpopHandler.new(options.dpop_private_key)
|
34
41
|
response = @dpop_handler.make_request(
|
35
42
|
token_url,
|
@@ -38,6 +45,8 @@ response = @dpop_handler.make_request(
|
|
38
45
|
body: token_params
|
39
46
|
)
|
40
47
|
|
48
|
+
# you can then use the access_token from the response in conjunction with the same private_key
|
49
|
+
|
41
50
|
```
|
42
51
|
|
43
52
|
## Development
|
@@ -1,15 +1,49 @@
|
|
1
1
|
module AtProto
|
2
|
+
# The Client class handles authenticated HTTP requests to the AT Protocol services
|
3
|
+
# with DPoP token support and automatic token refresh capabilities.
|
4
|
+
#
|
5
|
+
# @attr_reader [String] access_token The current access token for authentication
|
6
|
+
# @attr_reader [String] refresh_token The current refresh token for renewing access
|
7
|
+
# @attr_reader [DpopHandler] dpop_handler The handler for DPoP token operations
|
2
8
|
class Client
|
3
9
|
attr_reader :access_token, :refresh_token, :dpop_handler
|
4
10
|
|
5
|
-
|
11
|
+
# Initializes a new AT Protocol client
|
12
|
+
#
|
13
|
+
# @param access_token [String] The initial access token for authentication
|
14
|
+
# @param refresh_token [String] The refresh token for renewing access tokens
|
15
|
+
# @param private_key [OpenSSL::PKey::EC] The EC private key used for DPoP token signing (required)
|
16
|
+
# @param refresh_token_url [String] The base URL for token refresh requests
|
17
|
+
#
|
18
|
+
# @raise [ArgumentError] If private_key is not provided or not an OpenSSL::PKey::EC instance
|
19
|
+
def initialize(access_token:, refresh_token:, private_key:, refresh_token_url: 'https://bsky.social')
|
6
20
|
@access_token = access_token
|
7
21
|
@refresh_token = refresh_token
|
8
|
-
@
|
22
|
+
@refresh_token_url = refresh_token_url
|
23
|
+
@dpop_handler = DpopHandler.new(private_key, access_token)
|
9
24
|
@token_mutex = Mutex.new
|
10
25
|
end
|
11
26
|
|
12
|
-
|
27
|
+
# Sets a new private key for DPoP token signing
|
28
|
+
#
|
29
|
+
# @param private_key [OpenSSL::PKey::EC] The EC private key to use for signing DPoP tokens (required)
|
30
|
+
# @raise [ArgumentError] If private_key is not an OpenSSL::PKey::EC instance
|
31
|
+
def private_key=(private_key)
|
32
|
+
@dpop_handler = @dpop_handler.new(private_key, @access_token)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Makes an authenticated HTTP request with automatic token refresh
|
36
|
+
#
|
37
|
+
# @param method [Symbol] The HTTP method to use (:get, :post, etc.)
|
38
|
+
# @param url [String] The URL to send the request to
|
39
|
+
# @param params [Hash] Optional query parameters
|
40
|
+
# @param body [Hash, nil] Optional request body
|
41
|
+
#
|
42
|
+
# @return [Net::HTTPResponse] The HTTP response
|
43
|
+
#
|
44
|
+
# @raise [TokenExpiredError] When token refresh fails
|
45
|
+
# @raise [RefreshTokenError] When unable to refresh the access token
|
46
|
+
def request(method, url, params: {}, body: nil)
|
13
47
|
retries = 0
|
14
48
|
begin
|
15
49
|
uri = URI(url)
|
@@ -31,10 +65,15 @@ module AtProto
|
|
31
65
|
|
32
66
|
private
|
33
67
|
|
68
|
+
# Refreshes the access token using the refresh token
|
69
|
+
#
|
70
|
+
# @private
|
71
|
+
#
|
72
|
+
# @raise [RefreshTokenError] When the token refresh request fails
|
34
73
|
def refresh_access_token!
|
35
74
|
@token_mutex.synchronize do
|
36
75
|
response = @dpop_handler.make_request(
|
37
|
-
"#{
|
76
|
+
"#{@refresh_token_url}/xrpc/com.atproto.server.refreshSession",
|
38
77
|
:post,
|
39
78
|
headers: {},
|
40
79
|
body: { refresh_token: @refresh_token }
|
@@ -49,9 +88,5 @@ module AtProto
|
|
49
88
|
@refresh_token = data['refresh_token']
|
50
89
|
end
|
51
90
|
end
|
52
|
-
|
53
|
-
def base_url
|
54
|
-
AtProto.configuration.base_url
|
55
|
-
end
|
56
91
|
end
|
57
92
|
end
|
@@ -63,7 +63,6 @@ module AtProto
|
|
63
63
|
OpenSSL::PKey::EC.generate('prime256v1').tap(&:check_key)
|
64
64
|
end
|
65
65
|
|
66
|
-
# Creates a DPoP token with the specified parameters, encoded by jwk
|
67
66
|
def create_dpop_token(http_method, target_uri, nonce = nil)
|
68
67
|
jwk = JWT::JWK.new(@private_key).export
|
69
68
|
payload = {
|
@@ -73,19 +72,15 @@ module AtProto
|
|
73
72
|
iat: Time.now.to_i,
|
74
73
|
exp: Time.now.to_i + 120
|
75
74
|
}
|
76
|
-
|
77
|
-
# Ajout du hachage du token d'accès si fourni
|
78
|
-
if @access_token
|
79
|
-
token_str = @access_token.to_s
|
80
|
-
sha256 = OpenSSL::Digest.new('SHA256')
|
81
|
-
hash_bytes = sha256.digest(token_str)
|
82
|
-
ath = Base64.urlsafe_encode64(hash_bytes, padding: false)
|
83
|
-
payload[:ath] = ath
|
84
|
-
end
|
85
|
-
|
75
|
+
payload[:ath] = generate_ath if @access_token
|
86
76
|
payload[:nonce] = nonce if nonce
|
87
77
|
|
88
78
|
JWT.encode(payload, @private_key, 'ES256', { typ: 'dpop+jwt', alg: 'ES256', jwk: jwk })
|
89
79
|
end
|
80
|
+
|
81
|
+
def generate_ath
|
82
|
+
hash_bytes = OpenSSL::Digest.new('SHA256').digest(@access_token)
|
83
|
+
Base64.urlsafe_encode64(hash_bytes, padding: false)
|
84
|
+
end
|
90
85
|
end
|
91
86
|
end
|
data/lib/atproto_client.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: atproto_client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- frabr
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-11-
|
11
|
+
date: 2024-11-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jwt
|
@@ -146,7 +146,6 @@ files:
|
|
146
146
|
- README.md
|
147
147
|
- lib/atproto_client.rb
|
148
148
|
- lib/atproto_client/client.rb
|
149
|
-
- lib/atproto_client/configuration.rb
|
150
149
|
- lib/atproto_client/dpop_handler.rb
|
151
150
|
- lib/atproto_client/request.rb
|
152
151
|
- lib/atproto_client/version.rb
|
@@ -1,19 +0,0 @@
|
|
1
|
-
module AtProto
|
2
|
-
class Configuration
|
3
|
-
attr_accessor :base_url
|
4
|
-
|
5
|
-
def initialize
|
6
|
-
@base_url = 'https://bsky.social'
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
|
-
class << self
|
11
|
-
def configuration
|
12
|
-
@configuration ||= Configuration.new
|
13
|
-
end
|
14
|
-
|
15
|
-
def configure
|
16
|
-
yield(configuration)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|