atproto_client 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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