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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cbbb4053b1b25b1d1d3d37a6ae736570dd8d1bd097d69fde6d7868015fa45ac1
4
- data.tar.gz: 0a8cd5beac57a16b965aa7d5487a4896887d820ed012e7526091e28054111c1a
3
+ metadata.gz: 89742c30ed6ad23210c8e5eaecab1cabd26525419fe040bf1ca1f3a70ea29df7
4
+ data.tar.gz: 3b3854a97f5b74fb32e8e0cf98ad2566ab1ae652e64b543ff764286e0f0a8c8f
5
5
  SHA512:
6
- metadata.gz: eac347a2e5d30aa247d1ae27c16aa5df76ee581945c0af5c3cb1fe31575332687b662a9105470ed5634c8912b041b83ed36f0fd5ca6248d44fde387918c0ba87
7
- data.tar.gz: 1711570172c8f61c617784c886386b6f2afe7b71c4435e1e353bab7314e1ae3651c64a661526f27915bfa9494e52c897e913c2bf804b8d01bb826d06280da006
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(access_token, refresh_token)
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.make_api_request(
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 oauth example)
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
- def initialize(access_token, refresh_token, dpop_handler = nil)
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
- @dpop_handler = dpop_handler || DpopHandler.new
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
- def make_api_request(method, url, params: {}, body: nil)
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
- "#{base_url}/xrpc/com.atproto.server.refreshSession",
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
@@ -1,3 +1,3 @@
1
1
  module AtProto
2
- VERSION = '0.1.2'
2
+ VERSION = '0.1.3'
3
3
  end
@@ -19,7 +19,6 @@ module AtProto
19
19
  class APIError < Error; end
20
20
  end
21
21
 
22
- require 'atproto_client/configuration'
23
22
  require 'atproto_client/client'
24
23
  require 'atproto_client/dpop_handler'
25
24
  require 'atproto_client/request'
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.2
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-24 00:00:00.000000000 Z
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