didkit 0.2.3 → 0.3.1

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: ae1ff103a9695991ae3e0e97072c2b10731e7c18e44cfa5f715995ef0631df4f
4
- data.tar.gz: b2bc822873a7804515d3ad06caa6c15e4177943668f6bcbb79f8c48fa87e4733
3
+ metadata.gz: b81757b37a0aa45a3ccefe6e8ebac777a283a6f8d3b827308f1f029b30abe0c8
4
+ data.tar.gz: beef9f4a9cc71c9edc46fce5e95df2b1c6a442c223826054eafed721d744b8ad
5
5
  SHA512:
6
- metadata.gz: 5e4bbd991480a0a98c13c514e52a65bdceafd1c1a4feb75d60db07b22af10f376edaaea89bd98122c94c902c718c241a01965abaa801433b2a48162bb86d6ec1
7
- data.tar.gz: 791481ed39520a72eb750e2d511f6017ca476a1386c15d6d6a667e2a170a7e63d0723e868b2d843c87d66d3d33306fcc6bd5cd3d359cbea8e213e6ba8a401f99
6
+ metadata.gz: 24c046d3566c3816c700e60936f9313b837df8403dc0435e67c317eabaf4e554c29357573a825d72a4795185d942c67c0fc408457a2f3c72e8569c1fc9c718ab
7
+ data.tar.gz: d0e4bb8224a6a0c5b6d62edfee27869fc7227229d3d95f482beb2c917ac0ea99c45fed7e8993299c8a2b69ef0a9d5f58e1d610e108f0f15aac50ffebb7599dfe
data/CHANGELOG.md CHANGED
@@ -1,3 +1,34 @@
1
+ ## [0.3.1] - 2025-12-19
2
+
3
+ - allow passing a DID string or object to `#resolve_handle` and just return that DID – so you can have a script that accepts either a handle or a DID, and passes the input to `DID.resolve_handle` without checking which one it is
4
+ - allow passing another DID object to `DID.new` and return a copy of that DID
5
+ - parse `seq` field in `PLCOperation` if included and expose it as a property
6
+ - fixed some errors on Rubies older than 3.2 due to missing `filter_map` and `URI#origin`
7
+ - `PLCOperation` verifies if the argument is a `Hash`
8
+
9
+ ## [0.3.0] - 2025-12-15
10
+
11
+ Breaking changes:
12
+
13
+ * removed `DID#is_known_by_relay?` – it doesn't work anymore, since relays are now non-archival and they expose almost no XRPC routes
14
+ * renamed a few handle-related methods:
15
+ - `get_validated_handle` -> `get_verified_handle`
16
+ - `pick_valid_handle` -> `first_verified_handle`
17
+
18
+ Also:
19
+
20
+ - added `DID#account_status` method, which checks `getRepoStatus` endpoint to tell if an account is active, deactivated, taken down etc.
21
+ - added `DID#account_active?` helper (`account_status == :active`)
22
+ - `DID#account_exists?` now calls `getRepoStatus` (via `account_status`, checking if it's not nil) instead of `getLatestCommit`
23
+ - added `DID#document` which keeps a memoized copy of the document
24
+ - added `pds_host` & `labeler_host` methods to `PLCOperation` and `Document`, which return the PDS/labeller address without the `https://`
25
+ - added `labeller_endpoint` & `labeller_host` aliases for the double-L enjoyers :]
26
+ - added `PLCOperation#cid`
27
+ - `PLCImporter` now removes duplicate operations at the edge of pages returned from the `/export` API
28
+ - rewritten some networking code – all classes now use `Net::HTTP` with consistent options instead of `open-uri`
29
+
30
+ Note: `PLCImporter` will be rewritten soon to add support for updated [plc.directory](https://plc.directory) APIs, so be prepared for some breaking changes there in v. 0.4.
31
+
1
32
  ## [0.2.3] - 2024-07-02
2
33
 
3
34
  - added a `DID#get_audit_log` method that fetches the PLC audit log for a DID
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The zlib License
2
2
 
3
- Copyright (c) 2023 Jakub Suder
3
+ Copyright (c) 2025 Jakub Suder
4
4
 
5
5
  This software is provided 'as-is', without any express or implied
6
6
  warranty. In no event will the authors be held liable for any damages
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  A small Ruby gem for handling Distributed Identifiers (DIDs) in Bluesky / AT Protocol.
4
4
 
5
5
  > [!NOTE]
6
- > ATProto Ruby gems collection: [skyfall](https://github.com/mackuba/skyfall) | [blue_factory](https://github.com/mackuba/blue_factory) | [minisky](https://github.com/mackuba/minisky) | [didkit](https://github.com/mackuba/didkit)
6
+ > Part of ATProto Ruby SDK: [ruby.sdk.blue](https://ruby.sdk.blue)
7
7
 
8
8
 
9
9
  ## What does it do
@@ -13,75 +13,99 @@ Accounts on Bluesky use identifiers like [did:plc:oio4hkxaop4ao4wz2pp3f4cr](http
13
13
 
14
14
  ## Installation
15
15
 
16
+ From the command line:
17
+
16
18
  gem install didkit
17
19
 
20
+ Or, add this to your `Gemfile`:
18
21
 
19
- ## Usage
22
+ gem 'didkit', '~> 0.3'
20
23
 
21
- Use the `DIDKit::Resolver` class to look up DIDs and handles.
22
24
 
23
- To look up a handle:
25
+ ## Usage
26
+
27
+ The simplest way to use the gem is through the `DIDKit::DID` class, aliased as just `DID`:
24
28
 
25
29
  ```rb
26
- resolver = DIDKit::Resolver.new
27
- resolver.resolve_handle('nytimes.com')
28
- # => #<DIDKit::DID:0x00000001035956b0 @did="did:plc:eclio37ymobqex2ncko63h4r", @type=:plc, @resolved_by=:dns>
30
+ did = DID.resolve_handle('jay.bsky.team')
31
+ # => #<DIDKit::DID:0x0... @did="did:plc:oky5czdrnfjpqslsw2a5iclo",
32
+ # @resolved_by=:dns, @type=:plc>
29
33
  ```
30
34
 
31
- This returns an object of `DIDKit::DID` class (aliased as just `DID`), which tells you:
35
+ This returns a `DID` object, which tells you:
32
36
 
33
37
  - the DID as a string (`#to_s` or `#did`)
34
38
  - the DID type (`#type`, `:plc` or `:web`)
35
39
  - if the handle was resolved via a DNS entry or a `.well-known` file (`#resolved_by`, `:dns` or `:http`)
36
40
 
37
- To go in the other direction – to find an assigned and verified handle given a DID – use `get_validated_handle` (pass DID as a string or an object):
41
+ To go in the other direction – to find an assigned and verified handle given a DID – create a `DID` from a DID string and call `get_verified_handle`:
38
42
 
39
43
  ```rb
40
- resolver.get_validated_handle('did:plc:ewvi7nxzyoun6zhxrhs64oiz')
41
- # => "atproto.com"
44
+ DID.new('did:plc:ewvi7nxzyoun6zhxrhs64oiz').get_verified_handle
45
+ # => "atproto.com"
42
46
  ```
43
47
 
44
- You can also load the DID document using `resolve_did`:
48
+ You can also load the DID JSON document using `#document`, which returns a `DIDKit::Document` (`DID` caches the document, so don't worry about calling this method multiple times):
45
49
 
46
50
  ```rb
47
- doc = resolver.resolve_did('did:plc:ragtjsm2j2vknwkz3zp4oxrd')
48
- # => #<DIDKit::Document:0x0000000105d751f8 @did=#<DIDKit::DID:...>, @json={...}>
51
+ did = DID.new('did:plc:ragtjsm2j2vknwkz3zp4oxrd')
49
52
 
50
- doc.handles
51
- # => ["pfrazee.com"]
53
+ did.document.handles
54
+ # => ["pfrazee.com"]
52
55
 
53
- doc.pds_endpoint
54
- # => "https://morel.us-east.host.bsky.network"
56
+ did.document.pds_host
57
+ # => "morel.us-east.host.bsky.network"
55
58
  ```
56
59
 
57
- There are also some helper methods in the `DID` class that create a `Resolver` for you to save you some typing:
60
+
61
+ ### Checking account status
62
+
63
+ `DIDKit::DID` also includes a few methods for checking the status of a given account (repo), which call the `com.atproto.sync.getRepoStatus` endpoint on the account's assigned PDS:
58
64
 
59
65
  ```rb
60
- did = DID.resolve_handle('jay.bsky.team')
61
- # => #<DIDKit::DID:0x000000010615ed28 @did="did:plc:oky5czdrnfjpqslsw2a5iclo", @type=:plc, @resolved_by=:dns>
66
+ did = DID.new('did:plc:ch7azdejgddtlijyzurfdihn')
67
+ did.account_status
68
+ # => :takendown
69
+ did.account_active?
70
+ # => false
71
+ did.account_exists?
72
+ # => true
73
+
74
+ did = DID.new('did:plc:44ybard66vv44zksje25o7dz')
75
+ did.account_status
76
+ # => :active
77
+ did.account_active?
78
+ # => true
79
+ ```
62
80
 
63
- did.to_s
64
- # => "did:plc:oky5czdrnfjpqslsw2a5iclo"
81
+ ### Configuration
65
82
 
66
- did.get_document
67
- # => #<DIDKit::Document:0x00000001066d4898 @did=#<DIDKit::DID:...>, @json={...}>
83
+ You can customize some things about the DID/handle lookups by using the `DIDKit::Resolver` class, which the methods in `DID` use behind the scenes.
68
84
 
69
- did.get_validated_handle
70
- # => "jay.bsky.team"
71
- ```
85
+ Currently available options include:
72
86
 
87
+ - `:nameserver` - override the nameserver used for DNS lookups, e.g. to use Google's or CloudFlare's DNS
88
+ - `:timeout` - change the connection/response timeout for HTTP requests (default: 15 s)
89
+ - `:max_redirects` - change allowed maximum number of redirects (default: 5)
73
90
 
74
- ### Configuration
91
+ Example:
92
+
93
+ ```rb
94
+ resolver = DIDKit::Resolver.new(nameserver: '8.8.8.8', timeout: 30)
75
95
 
76
- You can override the nameserver used for DNS lookups by setting the `nameserver` property in `Resolver`, e.g. to use Google's or CloudFlare's global DNS:
96
+ did = resolver.resolve_handle('nytimes.com')
97
+ # => #<DIDKit::DID:0x0... @did="did:plc:eclio37ymobqex2ncko63h4r",
98
+ # @resolved_by=:dns, @type=:plc>
77
99
 
78
- ```
79
- resolver.nameserver = '8.8.8.8'
80
- ```
100
+ resolver.resolve_did(did)
101
+ # => #<DIDKit::Document:0x0... @did=#<DIDKit::DID:...>, @json={...}>
81
102
 
103
+ resolver.get_verified_handle(did)
104
+ # => 'nytimes.com'
105
+ ```
82
106
 
83
107
  ## Credits
84
108
 
85
- Copyright © 2024 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/mackuba.eu)).
109
+ Copyright © 2025 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr)).
86
110
 
87
111
  The code is available under the terms of the [zlib license](https://choosealicense.com/licenses/zlib/) (permissive, similar to MIT).
data/lib/didkit/did.rb CHANGED
@@ -1,9 +1,14 @@
1
+ require 'json'
2
+ require 'uri'
3
+
1
4
  require_relative 'errors'
2
5
  require_relative 'requests'
3
6
  require_relative 'resolver'
4
7
 
5
8
  module DIDKit
6
9
  class DID
10
+ GENERIC_REGEXP = /\Adid\:\w+\:.+\z/
11
+
7
12
  include Requests
8
13
 
9
14
  def self.resolve_handle(handle)
@@ -13,9 +18,13 @@ module DIDKit
13
18
  attr_reader :type, :did, :resolved_by
14
19
 
15
20
  def initialize(did, resolved_by = nil)
16
- if did =~ /^did\:(\w+)\:/
21
+ if did.is_a?(DID)
22
+ did = did.to_s
23
+ end
24
+
25
+ if did =~ GENERIC_REGEXP
17
26
  @did = did
18
- @type = $1.to_sym
27
+ @type = did.split(':')[1].to_sym
19
28
  else
20
29
  raise DIDError.new("Invalid DID format")
21
30
  end
@@ -29,12 +38,16 @@ module DIDKit
29
38
 
30
39
  alias to_s did
31
40
 
41
+ def document
42
+ @document ||= get_document
43
+ end
44
+
32
45
  def get_document
33
46
  Resolver.new.resolve_did(self)
34
47
  end
35
48
 
36
- def get_validated_handle
37
- Resolver.new.get_validated_handle(self)
49
+ def get_verified_handle
50
+ Resolver.new.get_verified_handle(document)
38
51
  end
39
52
 
40
53
  def get_audit_log
@@ -49,32 +62,41 @@ module DIDKit
49
62
  did.gsub(/^did\:web\:/, '') if type == :web
50
63
  end
51
64
 
52
- def is_known_by_relay?(relay, options = {})
53
- relay_host = relay.include?('://') ? URI(relay).origin : "https://#{relay}"
54
- url = URI("#{relay_host}/xrpc/com.atproto.sync.getLatestCommit")
55
- url.query = URI.encode_www_form(:did => did)
65
+ def account_status(request_options = {})
66
+ doc = self.document
67
+ return nil if doc.pds_endpoint.nil?
68
+
69
+ pds_host = uri_origin(doc.pds_endpoint)
70
+ url = URI("#{pds_host}/xrpc/com.atproto.sync.getRepoStatus")
71
+ url.query = URI.encode_www_form(:did => @did)
56
72
 
57
- response = get_response(url, { timeout: 30, max_redirects: 5 }.merge(options))
73
+ response = get_response(url, request_options)
58
74
  status = response.code.to_i
59
75
  is_json = (response['Content-Type'] =~ /^application\/json(;.*)?$/)
60
76
 
61
- if status == 200
62
- true
77
+ if status == 200 && is_json
78
+ json = JSON.parse(response.body)
79
+
80
+ if json['active'] == true
81
+ :active
82
+ elsif json['active'] == false && json['status'].is_a?(String) && json['status'].length <= 100
83
+ json['status'].to_sym
84
+ else
85
+ raise APIError.new(response)
86
+ end
63
87
  elsif status == 400 && is_json && JSON.parse(response.body)['error'] == 'RepoNotFound'
64
- false
65
- elsif status == 404 && is_json && JSON.parse(response.body)['error']
66
- false
88
+ nil
67
89
  else
68
90
  raise APIError.new(response)
69
91
  end
70
92
  end
71
93
 
72
- def account_exists?
73
- doc = get_document
74
- return false if doc.pds_endpoint.nil?
94
+ def account_active?
95
+ account_status == :active
96
+ end
75
97
 
76
- pds_host = URI(doc.pds_endpoint).origin
77
- is_known_by_relay?(pds_host, timeout: 10)
98
+ def account_exists?
99
+ account_status != nil
78
100
  end
79
101
 
80
102
  def ==(other)
@@ -21,24 +21,30 @@ module DIDKit
21
21
  @did = did
22
22
  @json = json
23
23
 
24
- if service = json['service']
25
- raise FormatError, "Invalid service data" unless service.is_a?(Array) && service.all? { |x| x.is_a?(Hash) }
24
+ @services = parse_services(json['service'] || [])
25
+ @handles = parse_also_known_as(json['alsoKnownAs'] || [])
26
+ end
26
27
 
27
- @services = service.filter_map { |x|
28
- id, type, endpoint = x.values_at('id', 'type', 'serviceEndpoint')
29
- next unless id.is_a?(String) && id.start_with?('#') && type.is_a?(String) && endpoint.is_a?(String)
28
+ def get_verified_handle
29
+ Resolver.new.get_verified_handle(self)
30
+ end
30
31
 
31
- ServiceRecord.new(id.gsub(/^#/, ''), type, endpoint)
32
- }
33
- else
34
- @services = []
35
- end
32
+ private
36
33
 
37
- @handles = parse_also_known_as(json['alsoKnownAs'] || [])
38
- end
34
+ def parse_services(service_data)
35
+ raise FormatError, "Invalid service data" unless service_data.is_a?(Array) && service_data.all? { |x| x.is_a?(Hash) }
36
+
37
+ services = []
38
+
39
+ service_data.each do |x|
40
+ id, type, endpoint = x.values_at('id', 'type', 'serviceEndpoint')
41
+
42
+ if id.is_a?(String) && id.start_with?('#') && type.is_a?(String) && endpoint.is_a?(String)
43
+ services << ServiceRecord.new(id.gsub(/^#/, ''), type, endpoint)
44
+ end
45
+ end
39
46
 
40
- def get_validated_handle
41
- Resolver.new.pick_valid_handle(did, handles)
47
+ services
42
48
  end
43
49
  end
44
50
  end
@@ -1,14 +1,22 @@
1
1
  require 'json'
2
- require 'open-uri'
3
2
  require 'time'
3
+ require 'uri'
4
4
 
5
5
  require_relative 'plc_operation'
6
+ require_relative 'requests'
7
+
8
+ #
9
+ # NOTE: this class is pending a rewrite once new APIs are deployed to plc.directory.
10
+ # Things will change here in v. 0.4.
11
+ #
6
12
 
7
13
  module DIDKit
8
14
  class PLCImporter
9
15
  PLC_SERVICE = 'plc.directory'
10
16
  MAX_PAGE = 1000
11
17
 
18
+ include Requests
19
+
12
20
  attr_accessor :ignore_errors, :last_date, :error_handler
13
21
 
14
22
  def initialize(since: nil)
@@ -22,6 +30,8 @@ module DIDKit
22
30
  @last_date = Time.now
23
31
  @eof = true
24
32
  end
33
+
34
+ @last_page_cids = []
25
35
  end
26
36
 
27
37
  def plc_service
@@ -42,13 +52,13 @@ module DIDKit
42
52
  url = URI("https://#{plc_service}/export")
43
53
  url.query = URI.encode_www_form(args)
44
54
 
45
- data = URI.open(url).read
55
+ data = get_data(url, content_type: 'application/jsonlines')
46
56
  data.lines.map(&:strip).reject(&:empty?).map { |x| JSON.parse(x) }
47
57
  end
48
58
 
49
59
  def fetch_audit_log(did)
50
- response = URI.open("https://#{plc_service}/#{did}/log/audit").read
51
- JSON.parse(response).map { |j| PLCOperation.new(j) }
60
+ json = get_json("https://#{plc_service}/#{did}/log/audit", :content_type => :json)
61
+ json.map { |j| PLCOperation.new(j) }
52
62
  end
53
63
 
54
64
  def fetch_page
@@ -57,16 +67,22 @@ module DIDKit
57
67
  query = @last_date ? { :after => @last_date.utc.iso8601(6) } : {}
58
68
  rows = get_export(query)
59
69
 
60
- operations = rows.filter_map do |json|
70
+ operations = rows.filter_map { |json|
61
71
  begin
62
72
  PLCOperation.new(json)
63
73
  rescue PLCOperation::FormatError, AtHandles::FormatError, ServiceRecord::FormatError => e
64
74
  @error_handler ? @error_handler.call(e, json) : raise
65
75
  nil
66
76
  end
67
- end
77
+ }.reject { |op|
78
+ # when you pass the most recent op's timestamp to ?after, it will be returned as the first op again,
79
+ # so we need to use this CID list to filter it out (so pages will usually be 999 items long)
80
+
81
+ @last_page_cids.include?(op.cid)
82
+ }
68
83
 
69
84
  @last_date = operations.last&.created_at || request_time
85
+ @last_page_cids = Set.new(operations.map(&:cid))
70
86
  @eof = (rows.length < MAX_PAGE)
71
87
 
72
88
  operations
@@ -12,13 +12,20 @@ module DIDKit
12
12
  include AtHandles
13
13
  include Services
14
14
 
15
- attr_reader :json, :did, :created_at, :type, :handles, :services
15
+ attr_reader :json, :did, :cid, :seq, :created_at, :type, :handles, :services
16
16
 
17
17
  def initialize(json)
18
18
  @json = json
19
+ raise FormatError, "Expected argument to be a Hash, got a #{json.class}" unless @json.is_a?(Hash)
20
+
21
+ @seq = json['seq']
19
22
  @did = json['did']
20
23
  raise FormatError, "Missing DID: #{json}" if @did.nil?
21
- raise FormatError, "Invalid DID: #{@did}" unless @did.is_a?(String) && @did.start_with?('did:')
24
+ raise FormatError, "Invalid DID: #{@did.inspect}" unless @did.is_a?(String) && @did.start_with?('did:')
25
+
26
+ @cid = json['cid']
27
+ raise FormatError, "Missing CID: #{json}" if @cid.nil?
28
+ raise FormatError, "Invalid CID: #{@cid}" unless @cid.is_a?(String)
22
29
 
23
30
  timestamp = json['createdAt']
24
31
  raise FormatError, "Missing createdAt: #{json}" if timestamp.nil?
@@ -1,28 +1,86 @@
1
- module DIDKit::Requests
2
- def get_response(url, options = {})
3
- url = URI(url) unless url.is_a?(URI)
4
- request_options = { use_ssl: true }
5
-
6
- if timeout = options[:timeout]
7
- request_options[:open_timeout] = timeout
8
- request_options[:read_timeout] = timeout
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ require_relative 'errors'
6
+
7
+ module DIDKit
8
+ module Requests
9
+ def get_response(url, options = {})
10
+ url = URI(url) unless url.is_a?(URI)
11
+
12
+ timeout = options[:timeout] || 15
13
+
14
+ request_options = {
15
+ use_ssl: true,
16
+ open_timeout: timeout,
17
+ read_timeout: timeout
18
+ }
19
+
20
+ redirects = 0
21
+ visited_urls = []
22
+ max_redirects = options[:max_redirects] || 5
23
+
24
+ loop do
25
+ visited_urls << url
26
+
27
+ response = Net::HTTP.start(url.host, url.port, request_options) do |http|
28
+ request = Net::HTTP::Get.new(url)
29
+ http.request(request)
30
+ end
31
+
32
+ if response.is_a?(Net::HTTPRedirection) && redirects < max_redirects && (location = response['Location'])
33
+ url = URI(location.include?('://') ? location : (uri_origin(url) + location))
34
+
35
+ if visited_urls.include?(url)
36
+ return response
37
+ else
38
+ redirects += 1
39
+ end
40
+ else
41
+ return response
42
+ end
43
+ end
9
44
  end
10
45
 
11
- redirects = 0
12
- max_redirects = options[:max_redirects] || 0
46
+ def get_data(url, options = {})
47
+ content_type = options.delete(:content_type)
48
+ response = get_response(url, options)
13
49
 
14
- loop do
15
- response = Net::HTTP.start(url.host, url.port, request_options) do |http|
16
- request = Net::HTTP::Get.new(url)
17
- http.request(request)
50
+ if response.is_a?(Net::HTTPSuccess) && content_type_matches(response, content_type) && (data = response.body)
51
+ data
52
+ else
53
+ raise APIError.new(response)
18
54
  end
55
+ end
56
+
57
+ def get_json(url, options = {})
58
+ JSON.parse(get_data(url, options))
59
+ end
19
60
 
20
- if response.is_a?(Net::HTTPRedirection) && redirects < max_redirects && (location = response['Location'])
21
- url = URI(location.include?('://') ? location : (url.origin + location))
22
- redirects += 1
61
+ def content_type_matches(response, expected_type)
62
+ content_type = response['Content-Type']
63
+
64
+ case expected_type
65
+ when String
66
+ content_type == expected_type
67
+ when Regexp
68
+ content_type =~ expected_type
69
+ when :json
70
+ content_type =~ /^application\/json(;.*)?$/
71
+ when nil
72
+ true
23
73
  else
24
- return response
74
+ raise ArgumentError, "Invalid expected_type: #{expected_type.inspect}"
25
75
  end
26
76
  end
77
+
78
+ # backported from https://github.com/ruby/uri/pull/30/files for older Rubies
79
+ def uri_origin(uri)
80
+ uri = uri.is_a?(URI) ? uri : URI(uri)
81
+ authority = (uri.port == uri.default_port) ? uri.host : "#{uri.host}:#{uri.port}"
82
+
83
+ "#{uri.scheme}://#{authority}"
84
+ end
27
85
  end
28
86
  end
@@ -1,5 +1,3 @@
1
- require 'json'
2
- require 'open-uri'
3
1
  require 'net/http'
4
2
  require 'resolv'
5
3
 
@@ -10,7 +8,6 @@ require_relative 'requests'
10
8
  module DIDKit
11
9
  class Resolver
12
10
  RESERVED_DOMAINS = %w(alt arpa example internal invalid local localhost onion test)
13
- MAX_REDIRECTS = 5
14
11
 
15
12
  include Requests
16
13
 
@@ -18,9 +15,14 @@ module DIDKit
18
15
 
19
16
  def initialize(options = {})
20
17
  @nameserver = options[:nameserver]
18
+ @request_options = options.slice(:timeout, :max_redirects)
21
19
  end
22
20
 
23
21
  def resolve_handle(handle)
22
+ if handle.is_a?(DID) || handle =~ DID::GENERIC_REGEXP
23
+ return DID.new(handle)
24
+ end
25
+
24
26
  domain = handle.gsub(/^@/, '')
25
27
 
26
28
  return nil if RESERVED_DOMAINS.include?(domain.split('.').last)
@@ -50,7 +52,7 @@ module DIDKit
50
52
 
51
53
  def resolve_handle_by_well_known(domain)
52
54
  url = "https://#{domain}/.well-known/atproto-did"
53
- response = get_response(url, timeout: 10, max_redirects: MAX_REDIRECTS)
55
+ response = get_response(url, @request_options)
54
56
 
55
57
  if response.is_a?(Net::HTTPSuccess) && (text = response.body)
56
58
  return parse_did_from_well_known(text)
@@ -73,7 +75,7 @@ module DIDKit
73
75
 
74
76
  def parse_did_from_well_known(text)
75
77
  text = text.strip
76
- text.lines.length == 1 && text =~ /\Adid\:\w+\:.*\z/ ? text : nil
78
+ text.lines.length == 1 && text =~ DID::GENERIC_REGEXP ? text : nil
77
79
  end
78
80
 
79
81
  def resolve_did(did)
@@ -83,25 +85,23 @@ module DIDKit
83
85
  end
84
86
 
85
87
  def resolve_did_plc(did)
86
- url = "https://plc.directory/#{did}"
87
- json = JSON.parse(URI.open(url).read)
88
+ json = get_json("https://plc.directory/#{did}", content_type: /^application\/did\+ld\+json(;.+)?$/)
88
89
  Document.new(did, json)
89
90
  end
90
91
 
91
92
  def resolve_did_web(did)
92
- url = "https://#{did.web_domain}/.well-known/did.json"
93
- json = JSON.parse(URI.open(url).read)
93
+ json = get_json("https://#{did.web_domain}/.well-known/did.json")
94
94
  Document.new(did, json)
95
95
  end
96
96
 
97
- def get_validated_handle(did_or_doc)
98
- document = did_or_doc.is_a?(Document) ? did_or_doc : resolve_did(did_or_doc)
97
+ def get_verified_handle(subject)
98
+ document = subject.is_a?(Document) ? subject : resolve_did(subject)
99
99
 
100
- pick_valid_handle(document.did, document.handles)
100
+ first_verified_handle(document.did, document.handles)
101
101
  end
102
102
 
103
- def pick_valid_handle(did, handles)
104
- handles.detect { |h| resolve_handle(h) == did }
103
+ def first_verified_handle(did, handles)
104
+ handles.detect { |h| resolve_handle(h) == did.to_s }
105
105
  end
106
106
  end
107
107
  end
@@ -1,3 +1,5 @@
1
+ require 'uri'
2
+
1
3
  module DIDKit
2
4
  module Services
3
5
  def get_service(key, type)
@@ -11,5 +13,16 @@ module DIDKit
11
13
  def labeler_endpoint
12
14
  @labeler_endpoint ||= get_service('atproto_labeler', 'AtprotoLabeler')&.endpoint
13
15
  end
16
+
17
+ def pds_host
18
+ pds_endpoint&.then { |x| URI(x).host }
19
+ end
20
+
21
+ def labeler_host
22
+ labeler_endpoint&.then { |x| URI(x).host }
23
+ end
24
+
25
+ alias labeller_endpoint labeler_endpoint
26
+ alias labeller_host labeler_host
14
27
  end
15
28
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DIDKit
4
- VERSION = "0.2.3"
4
+ VERSION = "0.3.1"
5
5
  end
metadata CHANGED
@@ -1,16 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: didkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kuba Suder
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-07-02 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
- description:
14
12
  email:
15
13
  - jakub.suder@gmail.com
16
14
  executables: []
@@ -33,14 +31,13 @@ files:
33
31
  - lib/didkit/services.rb
34
32
  - lib/didkit/version.rb
35
33
  - sig/didkit.rbs
36
- homepage: https://github.com/mackuba/didkit
34
+ homepage: https://ruby.sdk.blue
37
35
  licenses:
38
36
  - Zlib
39
37
  metadata:
40
- bug_tracker_uri: https://github.com/mackuba/didkit/issues
41
- changelog_uri: https://github.com/mackuba/didkit/blob/master/CHANGELOG.md
42
- source_code_uri: https://github.com/mackuba/didkit
43
- post_install_message:
38
+ bug_tracker_uri: https://tangled.org/mackuba.eu/didkit/issues
39
+ changelog_uri: https://tangled.org/mackuba.eu/didkit/blob/master/CHANGELOG.md
40
+ source_code_uri: https://tangled.org/mackuba.eu/didkit
44
41
  rdoc_options: []
45
42
  require_paths:
46
43
  - lib
@@ -55,8 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
55
52
  - !ruby/object:Gem::Version
56
53
  version: '0'
57
54
  requirements: []
58
- rubygems_version: 3.4.10
59
- signing_key:
55
+ rubygems_version: 4.0.1
60
56
  specification_version: 4
61
57
  summary: A library for handling Distributed ID (DID) identifiers used in Bluesky AT
62
58
  Protocol