didkit 0.0.4 → 0.1.0

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: 76dd4f3787cf1cc288015511eb35ea6d41ef4c390f85ca67854617fe7c46d710
4
- data.tar.gz: 40ea426b53fa3176dccd4c538cbd40050a3883bb6632af2b45ca1a6f49ac84b7
3
+ metadata.gz: 7744ae1dc9ba4ca678d641139ffa94931130ee7f91fa93d3ee89e3d6e196ba8d
4
+ data.tar.gz: 4111e824027b646aa8b3a21fb3032dfdba26019b6fecaf3f40f2120dfa417e52
5
5
  SHA512:
6
- metadata.gz: a26c81e89e2397bb7690149e143a0de6c9e9635a993e8cd13d572d76b21b38ce37628eb3c3ebc4ea9ffc20b3807f619acb46612a76af9f816b336315a10f7aab
7
- data.tar.gz: 89b4d9eb1ec2b7f5207587cde9b0a2b2db8a5912a92dc1ef996f0bac493996aae8b1340fc673838c404ac28d4ab191be6e7ce3a984f35a83402b9ef489feea8b
6
+ metadata.gz: ed96e325c2d0e8152b1db1f6a46e34afdbec049e0442f2298a89a2bbd07afb96e3ca166fca40adf1ff90d0da02997b825addd20163eb858c6074771d9ea229ce
7
+ data.tar.gz: 9237183a427ad4f5380504334abcfb16a965b0e95ce39693e9aa8d58d24c6acc6c00d7150bd86be18332e02da3cbd379eb516ccb60716a3b09bf11efc155c26c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [0.1.0] - 2024-03-12
2
+
3
+ - rejecting handles from disallowed domains like `.arpa` or `.test`
4
+ - validating handles with the `.well-known` file having a trailing newline
5
+ - validating handles with `.well-known` address returning a redirect
6
+ - added `#pick_valid_handle` helper
7
+ - allow overriding the nameserver for `Resolv::DNS`
8
+ - other bug fixes
9
+
1
10
  ## [0.0.4] - 2024-03-07
2
11
 
3
12
  - extracted resolving code from `DID` to a new `Resolver` class (`DID` has helper methods to call the resolver)
data/README.md CHANGED
@@ -1,13 +1,11 @@
1
- # DidKit
1
+ # DIDKit
2
2
 
3
3
  A small Ruby gem for handling Distributed Identifiers (DIDs) in Bluesky / AT Protocol
4
4
 
5
5
 
6
6
  ## What does it do
7
7
 
8
- **TODO** - not much yet :)
9
-
10
- See the [did.rb](https://github.com/mackuba/didkit/blob/master/lib/didkit/did.rb) file for now.
8
+ Accounts on Bluesky use identifiers like [did:plc:oio4hkxaop4ao4wz2pp3f4cr](https://plc.directory/did:plc:oio4hkxaop4ao4wz2pp3f4cr) as unique IDs, and they also have assigned human-readable handles like [@mackuba.eu](https://bsky.app/profile/mackuba.eu), which are verified either through a DNS TXT entry or a `/.well-known/atproto-did` file. This library allows you to look up any account's assigned handle using a DID string or vice versa, load the account's DID JSON document that specifies the handles and the PDS server hosting user's repo, and check if the assigned handle verifies correctly.
11
9
 
12
10
 
13
11
  ## Installation
@@ -15,8 +13,72 @@ See the [did.rb](https://github.com/mackuba/didkit/blob/master/lib/didkit/did.rb
15
13
  gem install didkit
16
14
 
17
15
 
16
+ ## Usage
17
+
18
+ Use the `DIDKit::Resolver` class to look up DIDs and handles.
19
+
20
+ To look up a handle:
21
+
22
+ ```rb
23
+ resolver = DIDKit::Resolver.new
24
+ resolver.resolve_handle('nytimes.com')
25
+ # => #<DIDKit::DID:0x00000001035956b0 @did="did:plc:eclio37ymobqex2ncko63h4r", @type=:plc, @resolved_by=:dns>
26
+ ```
27
+
28
+ This returns an object of `DIDKit::DID` class (aliased as just `DID`), which tells you:
29
+
30
+ - the DID as a string (`#to_s` or `#did`)
31
+ - the DID type (`#type`, `:plc` or `:web`)
32
+ - if the handle was resolved via a DNS entry or a `.well-known` file (`#resolved_by`, `:dns` or `:http`)
33
+
34
+ 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):
35
+
36
+ ```rb
37
+ resolver.get_validated_handle('did:plc:ewvi7nxzyoun6zhxrhs64oiz')
38
+ # => "atproto.com"
39
+ ```
40
+
41
+ You can also load the DID document using `resolve_did`:
42
+
43
+ ```rb
44
+ doc = resolver.resolve_did('did:plc:ragtjsm2j2vknwkz3zp4oxrd')
45
+ # => #<DIDKit::Document:0x0000000105d751f8 @did=#<DIDKit::DID:...>, @json={...}>
46
+
47
+ doc.handles
48
+ # => ["pfrazee.com"]
49
+
50
+ doc.pds_endpoint
51
+ # => "https://morel.us-east.host.bsky.network"
52
+ ```
53
+
54
+ There are also some helper methods in the `DID` class that create a `Resolver` for you to save you some typing:
55
+
56
+ ```rb
57
+ did = DID.resolve_handle('jay.bsky.team')
58
+ # => #<DIDKit::DID:0x000000010615ed28 @did="did:plc:oky5czdrnfjpqslsw2a5iclo", @type=:plc, @resolved_by=:dns>
59
+
60
+ did.to_s
61
+ # => "did:plc:oky5czdrnfjpqslsw2a5iclo"
62
+
63
+ did.get_document
64
+ # => #<DIDKit::Document:0x00000001066d4898 @did=#<DIDKit::DID:...>, @json={...}>
65
+
66
+ did.get_validated_handle
67
+ # => "jay.bsky.team"
68
+ ```
69
+
70
+
71
+ ### Configuration
72
+
73
+ 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:
74
+
75
+ ```
76
+ resolver.nameserver = '8.8.8.8'
77
+ ```
78
+
79
+
18
80
  ## Credits
19
81
 
20
- Copyright © 2023 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/mackuba.eu)).
82
+ Copyright © 2024 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/mackuba.eu)).
21
83
 
22
84
  The code is available under the terms of the [zlib license](https://choosealicense.com/licenses/zlib/) (permissive, similar to MIT).
@@ -1,3 +1,5 @@
1
+ require_relative 'resolver'
2
+
1
3
  module DIDKit
2
4
  class Document
3
5
  class FormatError < StandardError
@@ -39,7 +41,7 @@ module DIDKit
39
41
  end
40
42
 
41
43
  def get_validated_handle
42
- Resolver.new.get_validated_handle(self)
44
+ Resolver.new.pick_valid_handle(did, handles)
43
45
  end
44
46
  end
45
47
  end
@@ -0,0 +1,45 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'open-uri'
4
+ require 'time'
5
+
6
+ require_relative 'plc_operation'
7
+
8
+ module DIDKit
9
+ class PLCImporter
10
+ PLC_SERVICE = 'plc.directory'
11
+
12
+ attr_accessor :ignore_errors
13
+
14
+ def initialize(last_date = Time.now)
15
+ @last_date = last_date
16
+ @ignore_errors = false
17
+ end
18
+
19
+ def fetch
20
+ url = URI("https://#{PLC_SERVICE}/export")
21
+ url.query = URI.encode_www_form(:after => @last_date.utc.iso8601(6)) if @last_date
22
+ request_time = Time.now
23
+
24
+ data = URI.open(url).read
25
+ rows = data.lines.map(&:strip).reject(&:empty?).map { |x| JSON.parse(x) }
26
+
27
+ operations = rows.filter_map do |json|
28
+ begin
29
+ PLCOperation.new(json)
30
+ rescue PLCOperation::FormatError => e
31
+ ignore_errors ? nil : raise
32
+ end
33
+ end
34
+
35
+ block.call(operations)
36
+
37
+ if rows.length == 1000
38
+ @last_date = operations.last.created_at || request_time
39
+ run_update
40
+ else
41
+ @last_date = request_time
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,56 @@
1
+ require 'time'
2
+
3
+ module DIDKit
4
+ class PLCOperation
5
+ class FormatError < StandardError
6
+ end
7
+
8
+ attr_reader :did, :created_at, :type, :pds_endpoint, :handles
9
+
10
+ def initialize(json)
11
+ @did = json['did']
12
+ raise FormatError, "Missing DID" if @did.nil?
13
+ raise FormatError, "Invalid DID" unless @did.is_a?(String) && @did.start_with?('did:')
14
+
15
+ timestamp = json['createdAt']
16
+ raise FormatError, "Missing createdAt" if timestamp.nil?
17
+ raise FormatError, "Invalid createdAt" unless timestamp.is_a?(String)
18
+
19
+ @created_at = Time.parse(timestamp)
20
+
21
+ operation = json['operation']
22
+ raise FormatError, "Missing operation key" if operation.nil?
23
+ raise FormatError, "Invalid operation data" unless operation.is_a?(Hash)
24
+
25
+ type = operation['type']
26
+ raise FormatError, "Missing type" if type.nil?
27
+
28
+ @type = type.to_sym
29
+ return unless @type == :plc_operation
30
+
31
+ services = operation['services']
32
+ raise FormatError, "Missing services key" if services.nil?
33
+ raise FormatError, "Invalid services data" unless services.is_a?(Hash)
34
+
35
+ if pds = services['atproto_pds']
36
+ raise FormatError, "Invalid PDS data" unless pds.is_a?(Hash)
37
+ raise FormatError, "Missing PDS type" unless pds['type']
38
+ raise FormatError, "Invalid PDS type" unless pds['type'] == 'AtprotoPersonalDataServer'
39
+ raise FormatError, "Missing PDS endpoint" unless pds['endpoint']
40
+ raise FormatError, "Invalid PDS endpoint" unless pds['endpoint'].is_a?(String) && pds['endpoint'] =~ %r(://)
41
+
42
+ @pds_endpoint = pds['endpoint']
43
+ end
44
+
45
+ if aka = operation['alsoKnownAs']
46
+ raise FormatError, "Invalid alsoKnownAs" unless aka.is_a?(Array)
47
+ raise FormatError, "Invalid alsoKnownAs" unless aka.all? { |x| x.is_a?(String) }
48
+ raise FormatError, "Invalid alsoKnownAs" unless aka.all? { |x| x =~ %r(\Aat://[^/]+\z) }
49
+
50
+ @handles = aka.map { |x| x.gsub('at://', '') }
51
+ else
52
+ @handles = []
53
+ end
54
+ end
55
+ end
56
+ end
@@ -8,9 +8,16 @@ require_relative 'document'
8
8
 
9
9
  module DIDKit
10
10
  class Resolver
11
+ RESERVED_DOMAINS = %w(alt arpa example internal invalid local localhost onion test)
12
+ MAX_REDIRECTS = 5
13
+
14
+ attr_accessor :nameserver
15
+
11
16
  def resolve_handle(handle)
12
17
  domain = handle.gsub(/^@/, '')
13
18
 
19
+ return nil if RESERVED_DOMAINS.include?(domain.split('.').last)
20
+
14
21
  if dns_did = resolve_handle_by_dns(domain)
15
22
  DID.new(dns_did, :dns)
16
23
  elsif http_did = resolve_handle_by_well_known(domain)
@@ -21,13 +28,13 @@ module DIDKit
21
28
  end
22
29
 
23
30
  def resolve_handle_by_dns(domain)
24
- dns_records = Resolv::DNS.open { |d| d.getresources("_atproto.#{domain}", Resolv::DNS::Resource::IN::TXT) }
31
+ dns_records = Resolv::DNS.open(resolv_options) { |d|
32
+ d.getresources("_atproto.#{domain}", Resolv::DNS::Resource::IN::TXT)
33
+ }
25
34
 
26
35
  if record = dns_records.first
27
36
  if string = record.strings.first
28
- if string =~ /^did\=(did\:\w+\:.*)$/
29
- return $1
30
- end
37
+ return parse_did_from_dns(string)
31
38
  end
32
39
  end
33
40
 
@@ -35,7 +42,11 @@ module DIDKit
35
42
  end
36
43
 
37
44
  def resolve_handle_by_well_known(domain)
38
- url = URI("https://#{domain}/.well-known/atproto-did")
45
+ resolve_handle_from_url("https://#{domain}/.well-known/atproto-did")
46
+ end
47
+
48
+ def resolve_handle_from_url(url, redirects = 0)
49
+ url = URI(url) unless url.is_a?(URI)
39
50
 
40
51
  response = Net::HTTP.start(url.host, url.port, use_ssl: true, open_timeout: 10, read_timeout: 10) do |http|
41
52
  request = Net::HTTP::Get.new(url)
@@ -44,9 +55,12 @@ module DIDKit
44
55
 
45
56
  if response.is_a?(Net::HTTPSuccess)
46
57
  if text = response.body
47
- if text.lines.length == 1 && text.start_with?('did:')
48
- return text
49
- end
58
+ return parse_did_from_well_known(text)
59
+ end
60
+ elsif response.is_a?(Net::HTTPRedirection) && redirects < MAX_REDIRECTS
61
+ if location = response['Location']
62
+ target_url = location.include?('://') ? location : (url.origin + location)
63
+ return resolve_handle_from_url(target_url, redirects + 1)
50
64
  end
51
65
  end
52
66
 
@@ -55,6 +69,21 @@ module DIDKit
55
69
  nil
56
70
  end
57
71
 
72
+ def resolv_options
73
+ options = Resolv::DNS::Config.default_config_hash.dup
74
+ options[:nameserver] = nameserver if nameserver
75
+ options
76
+ end
77
+
78
+ def parse_did_from_dns(txt)
79
+ txt =~ /\Adid\=(did\:\w+\:.*)\z/ ? $1 : nil
80
+ end
81
+
82
+ def parse_did_from_well_known(text)
83
+ text = text.strip
84
+ text.lines.length == 1 && text =~ /\Adid\:\w+\:.*\z/ ? text : nil
85
+ end
86
+
58
87
  def resolve_did(did)
59
88
  did = DID.new(did) if did.is_a?(String)
60
89
 
@@ -76,7 +105,11 @@ module DIDKit
76
105
  def get_validated_handle(did_or_doc)
77
106
  document = did_or_doc.is_a?(Document) ? did_or_doc : resolve_did(did_or_doc)
78
107
 
79
- document.handles.detect { |h| resolve_handle(h) == document.did }
108
+ pick_valid_handle(document.did, document.handles)
109
+ end
110
+
111
+ def pick_valid_handle(did, handles)
112
+ handles.detect { |h| resolve_handle(h) == did }
80
113
  end
81
114
  end
82
115
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DIDKit
4
- VERSION = "0.0.4"
4
+ VERSION = "0.1.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: didkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.1.0
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-07 00:00:00.000000000 Z
11
+ date: 2024-03-12 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -24,6 +24,8 @@ files:
24
24
  - lib/didkit/did.rb
25
25
  - lib/didkit/document.rb
26
26
  - lib/didkit/errors.rb
27
+ - lib/didkit/plc_importer.rb
28
+ - lib/didkit/plc_operation.rb
27
29
  - lib/didkit/resolver.rb
28
30
  - lib/didkit/version.rb
29
31
  - sig/didkit.rbs