didkit 0.2.0 → 0.2.2

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: 0170f70f452400a399264cccd61a96a0ef96d570caca53547b2794eb8067d4bd
4
- data.tar.gz: 68197fe06b766fbe69d09d6d85b8abf59467f09560cb600b9af34bb900efaf6b
3
+ metadata.gz: fd7709e2b0973a44b71e939798901a6fd74bc9fa25e284958948fc4fe22de51e
4
+ data.tar.gz: b38397ddc0244f9035719dba7eb0fdcd844feb549e9628462da16b4aba662cf4
5
5
  SHA512:
6
- metadata.gz: 367f5acd94953397d5ca72edec0a43f2a96e8dfef3615d16210edc203abe14b1875c1e78316efe351e223c881cbc80d2fd6cf7314753c78caea6c9ccb7ed78d0
7
- data.tar.gz: 15ade27998fbc1881f28bc07fe7d625f310e686be7f101508460db48df7ca263adfc6731bf9f8bb935363fb23e51f33b1f560183595ba624d1fd7ad4e8dcf00f
6
+ metadata.gz: 2d5ddb2ce60022530ffd59879d5734d4300a091eb78db13028ae10a55d8ee83981ba6d20a7c933c2b6249245f40adf7458d0687f7349fa042915445e260b2e3a
7
+ data.tar.gz: 9a8c796fee2372323a405ec8962671438cbd7263b1265f31eb45a324dfcab7e33557dbf74b1f4b87fed0492467965ef7222569ee7fad2511efb41b7b52d33ed9
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [0.2.2] - 2024-04-01
2
+
3
+ - added helpers for checking if a DID is known by (federated with) a relay or if the repo exists on its assigned PDS
4
+
5
+ ## [0.2.1] - 2024-03-26
6
+
7
+ - tweaked validations in `Document` and `PLCOperation` to make them more aligned with what might be expected
8
+ - added Ruby stdlib dependencies explicitly to the gemspec
9
+
1
10
  ## [0.2.0] - 2024-03-19
2
11
 
3
12
  - added `PLCImporter` class, which lets you import operations from PLC in pages of 1000 through the "export" API
@@ -6,9 +6,8 @@ module DIDKit
6
6
  def parse_also_known_as(aka)
7
7
  raise FormatError, "Invalid alsoKnownAs: #{aka.inspect}" unless aka.is_a?(Array)
8
8
  raise FormatError, "Invalid alsoKnownAs: #{aka.inspect}" unless aka.all? { |x| x.is_a?(String) }
9
- raise FormatError, "Invalid alsoKnownAs: #{aka.inspect}" unless aka.all? { |x| x =~ %r(\Aat://[^/]+\z) }
10
9
 
11
- aka.map { |x| x.gsub('at://', '') }
10
+ aka.select { |x| x =~ %r(\Aat://[^/]+\z) }.map { |x| x.gsub('at://', '') }
12
11
  end
13
12
  end
14
13
  end
data/lib/didkit/did.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  require_relative 'errors'
2
+ require_relative 'requests'
2
3
  require_relative 'resolver'
3
4
 
4
5
  module DIDKit
5
6
  class DID
7
+ include Requests
8
+
6
9
  def self.resolve_handle(handle)
7
10
  Resolver.new.resolve_handle(handle)
8
11
  end
@@ -38,6 +41,34 @@ module DIDKit
38
41
  did.gsub(/^did\:web\:/, '') if type == :web
39
42
  end
40
43
 
44
+ def is_known_by_relay?(relay, options = {})
45
+ relay_host = relay.include?('://') ? URI(relay).origin : "https://#{relay}"
46
+ url = URI("#{relay_host}/xrpc/com.atproto.sync.getLatestCommit")
47
+ url.query = URI.encode_www_form(:did => did)
48
+
49
+ response = get_response(url, { timeout: 30, max_redirects: 5 }.merge(options))
50
+ status = response.code.to_i
51
+ is_json = (response['Content-Type'] =~ /^application\/json(;.*)?$/)
52
+
53
+ if status == 200
54
+ true
55
+ elsif status == 400 && is_json && JSON.parse(response.body)['error'] == 'RepoNotFound'
56
+ false
57
+ elsif status == 404 && is_json && JSON.parse(response.body)['error']
58
+ false
59
+ else
60
+ raise APIError.new(response)
61
+ end
62
+ end
63
+
64
+ def account_exists?
65
+ doc = get_document
66
+ return false if doc.pds_endpoint.nil?
67
+
68
+ pds_host = URI(doc.pds_endpoint).origin
69
+ is_known_by_relay?(pds_host, timeout: 10)
70
+ end
71
+
41
72
  def ==(other)
42
73
  if other.is_a?(String)
43
74
  self.did == other
@@ -21,22 +21,18 @@ module DIDKit
21
21
  @did = did
22
22
  @json = json
23
23
 
24
- service = json['service']
25
- raise FormatError, "Missing service key" if service.nil?
26
- raise FormatError, "Invalid service data" unless service.is_a?(Array) && service.all? { |x| x.is_a?(Hash) }
27
-
28
- @services = service.map { |x|
29
- id, type, endpoint = x.values_at('id', 'type', 'serviceEndpoint')
30
-
31
- raise FormatError, "Missing service id" unless id
32
- raise FormatError, "Invalid service id: #{id.inspect}" unless id.is_a?(String) && id.start_with?('#')
33
- raise FormatError, "Missing service type" unless type
34
- raise FormatError, "Invalid service type: #{type.inspect}" unless type.is_a?(String)
35
- raise FormatError, "Missing service endpoint" unless endpoint
36
- raise FormatError, "Invalid service endpoint: #{endpoint.inspect}" unless endpoint.is_a?(String)
37
-
38
- ServiceRecord.new(id.gsub(/^#/, ''), type, endpoint)
39
- }
24
+ if service = json['service']
25
+ raise FormatError, "Invalid service data" unless service.is_a?(Array) && service.all? { |x| x.is_a?(Hash) }
26
+
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)
30
+
31
+ ServiceRecord.new(id.gsub(/^#/, ''), type, endpoint)
32
+ }
33
+ else
34
+ @services = []
35
+ end
40
36
 
41
37
  @handles = parse_also_known_as(json['alsoKnownAs'] || [])
42
38
  end
data/lib/didkit/errors.rb CHANGED
@@ -1,4 +1,21 @@
1
1
  module DIDKit
2
2
  class DIDError < StandardError
3
3
  end
4
+
5
+ class APIError < StandardError
6
+ attr_reader :response
7
+
8
+ def initialize(response)
9
+ @response = response
10
+ super("APIError: #{response}")
11
+ end
12
+
13
+ def status
14
+ response.code.to_i
15
+ end
16
+
17
+ def body
18
+ response.body
19
+ end
20
+ end
4
21
  end
@@ -51,7 +51,7 @@ module DIDKit
51
51
  ServiceRecord.new(k, type, endpoint)
52
52
  }
53
53
 
54
- @handles = parse_also_known_as(operation['alsoKnownAs'] || [])
54
+ @handles = parse_also_known_as(operation['alsoKnownAs'])
55
55
  end
56
56
  end
57
57
  end
@@ -0,0 +1,28 @@
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
9
+ end
10
+
11
+ redirects = 0
12
+ max_redirects = options[:max_redirects] || 0
13
+
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)
18
+ end
19
+
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
23
+ else
24
+ return response
25
+ end
26
+ end
27
+ end
28
+ end
@@ -5,12 +5,15 @@ require 'resolv'
5
5
 
6
6
  require_relative 'did'
7
7
  require_relative 'document'
8
+ require_relative 'requests'
8
9
 
9
10
  module DIDKit
10
11
  class Resolver
11
12
  RESERVED_DOMAINS = %w(alt arpa example internal invalid local localhost onion test)
12
13
  MAX_REDIRECTS = 5
13
14
 
15
+ include Requests
16
+
14
17
  attr_accessor :nameserver
15
18
 
16
19
  def initialize(options = {})
@@ -32,9 +35,9 @@ module DIDKit
32
35
  end
33
36
 
34
37
  def resolve_handle_by_dns(domain)
35
- dns_records = Resolv::DNS.open(resolv_options) { |d|
38
+ dns_records = Resolv::DNS.open(resolv_options) do |d|
36
39
  d.getresources("_atproto.#{domain}", Resolv::DNS::Resource::IN::TXT)
37
- }
40
+ end
38
41
 
39
42
  if record = dns_records.first
40
43
  if string = record.strings.first
@@ -46,26 +49,11 @@ module DIDKit
46
49
  end
47
50
 
48
51
  def resolve_handle_by_well_known(domain)
49
- resolve_handle_from_url("https://#{domain}/.well-known/atproto-did")
50
- end
51
-
52
- def resolve_handle_from_url(url, redirects = 0)
53
- url = URI(url) unless url.is_a?(URI)
52
+ url = "https://#{domain}/.well-known/atproto-did"
53
+ response = get_response(url, timeout: 10, max_redirects: MAX_REDIRECTS)
54
54
 
55
- response = Net::HTTP.start(url.host, url.port, use_ssl: true, open_timeout: 10, read_timeout: 10) do |http|
56
- request = Net::HTTP::Get.new(url)
57
- http.request(request)
58
- end
59
-
60
- if response.is_a?(Net::HTTPSuccess)
61
- if text = response.body
62
- return parse_did_from_well_known(text)
63
- end
64
- elsif response.is_a?(Net::HTTPRedirection) && redirects < MAX_REDIRECTS
65
- if location = response['Location']
66
- target_url = location.include?('://') ? location : (url.origin + location)
67
- return resolve_handle_from_url(target_url, redirects + 1)
68
- end
55
+ if response.is_a?(Net::HTTPSuccess) && (text = response.body)
56
+ return parse_did_from_well_known(text)
69
57
  end
70
58
 
71
59
  nil
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DIDKit
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
metadata CHANGED
@@ -1,15 +1,99 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: didkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kuba Suder
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-19 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2024-03-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-http
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: open-uri
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: resolv
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: time
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.3'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: uri
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.13'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.13'
13
97
  description:
14
98
  email:
15
99
  - jakub.suder@gmail.com
@@ -27,6 +111,7 @@ files:
27
111
  - lib/didkit/errors.rb
28
112
  - lib/didkit/plc_importer.rb
29
113
  - lib/didkit/plc_operation.rb
114
+ - lib/didkit/requests.rb
30
115
  - lib/didkit/resolver.rb
31
116
  - lib/didkit/service_record.rb
32
117
  - lib/didkit/services.rb