didkit 0.0.4 → 0.1.0
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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +67 -5
- data/lib/didkit/document.rb +3 -1
- data/lib/didkit/plc_importer.rb +45 -0
- data/lib/didkit/plc_operation.rb +56 -0
- data/lib/didkit/resolver.rb +42 -9
- data/lib/didkit/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7744ae1dc9ba4ca678d641139ffa94931130ee7f91fa93d3ee89e3d6e196ba8d
|
4
|
+
data.tar.gz: 4111e824027b646aa8b3a21fb3032dfdba26019b6fecaf3f40f2120dfa417e52
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
#
|
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
|
-
|
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 ©
|
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).
|
data/lib/didkit/document.rb
CHANGED
@@ -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.
|
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
|
data/lib/didkit/resolver.rb
CHANGED
@@ -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|
|
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
|
-
|
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
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
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.
|
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
|
data/lib/didkit/version.rb
CHANGED
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
|
+
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-
|
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
|