didkit 0.0.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +67 -5
- data/lib/didkit/at_handles.rb +14 -0
- data/lib/didkit/document.rb +24 -21
- data/lib/didkit/plc_importer.rb +73 -0
- data/lib/didkit/plc_operation.rb +57 -0
- data/lib/didkit/resolver.rb +46 -9
- data/lib/didkit/service_record.rb +20 -0
- data/lib/didkit/services.rb +15 -0
- data/lib/didkit/version.rb +1 -1
- data/lib/didkit.rb +3 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0170f70f452400a399264cccd61a96a0ef96d570caca53547b2794eb8067d4bd
|
4
|
+
data.tar.gz: 68197fe06b766fbe69d09d6d85b8abf59467f09560cb600b9af34bb900efaf6b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 367f5acd94953397d5ca72edec0a43f2a96e8dfef3615d16210edc203abe14b1875c1e78316efe351e223c881cbc80d2fd6cf7314753c78caea6c9ccb7ed78d0
|
7
|
+
data.tar.gz: 15ade27998fbc1881f28bc07fe7d625f310e686be7f101508460db48df7ca263adfc6731bf9f8bb935363fb23e51f33b1f560183595ba624d1fd7ad4e8dcf00f
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
## [0.2.0] - 2024-03-19
|
2
|
+
|
3
|
+
- added `PLCImporter` class, which lets you import operations from PLC in pages of 1000 through the "export" API
|
4
|
+
- implemented parsing of all services from DID doc & operations, not only `atproto_pds` (specifically labeller endpoints)
|
5
|
+
- allow setting the nameserver in `Resolver` initializer
|
6
|
+
|
7
|
+
## [0.1.0] - 2024-03-12
|
8
|
+
|
9
|
+
- rejecting handles from disallowed domains like `.arpa` or `.test`
|
10
|
+
- validating handles with the `.well-known` file having a trailing newline
|
11
|
+
- validating handles with `.well-known` address returning a redirect
|
12
|
+
- added `#pick_valid_handle` helper
|
13
|
+
- allow overriding the nameserver for `Resolv::DNS`
|
14
|
+
- other bug fixes
|
15
|
+
|
1
16
|
## [0.0.4] - 2024-03-07
|
2
17
|
|
3
18
|
- 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).
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module DIDKit
|
2
|
+
module AtHandles
|
3
|
+
class FormatError < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
def parse_also_known_as(aka)
|
7
|
+
raise FormatError, "Invalid alsoKnownAs: #{aka.inspect}" unless aka.is_a?(Array)
|
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
|
+
|
11
|
+
aka.map { |x| x.gsub('at://', '') }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/didkit/document.rb
CHANGED
@@ -1,9 +1,17 @@
|
|
1
|
+
require_relative 'at_handles'
|
2
|
+
require_relative 'resolver'
|
3
|
+
require_relative 'service_record'
|
4
|
+
require_relative 'services'
|
5
|
+
|
1
6
|
module DIDKit
|
2
7
|
class Document
|
3
8
|
class FormatError < StandardError
|
4
9
|
end
|
5
10
|
|
6
|
-
|
11
|
+
include AtHandles
|
12
|
+
include Services
|
13
|
+
|
14
|
+
attr_reader :json, :did, :handles, :services
|
7
15
|
|
8
16
|
def initialize(did, json)
|
9
17
|
raise FormatError, "Missing id field" if json['id'].nil?
|
@@ -17,29 +25,24 @@ module DIDKit
|
|
17
25
|
raise FormatError, "Missing service key" if service.nil?
|
18
26
|
raise FormatError, "Invalid service data" unless service.is_a?(Array) && service.all? { |x| x.is_a?(Hash) }
|
19
27
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
raise FormatError, "Missing
|
24
|
-
raise FormatError, "Invalid
|
25
|
-
raise FormatError, "
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
@handles = aka.map { |x| x.gsub('at://', '') }
|
36
|
-
else
|
37
|
-
@handles = []
|
38
|
-
end
|
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
|
+
}
|
40
|
+
|
41
|
+
@handles = parse_also_known_as(json['alsoKnownAs'] || [])
|
39
42
|
end
|
40
43
|
|
41
44
|
def get_validated_handle
|
42
|
-
Resolver.new.
|
45
|
+
Resolver.new.pick_valid_handle(did, handles)
|
43
46
|
end
|
44
47
|
end
|
45
48
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
require_relative 'plc_operation'
|
6
|
+
|
7
|
+
module DIDKit
|
8
|
+
class PLCImporter
|
9
|
+
PLC_SERVICE = 'plc.directory'
|
10
|
+
MAX_PAGE = 1000
|
11
|
+
|
12
|
+
attr_accessor :ignore_errors, :last_date
|
13
|
+
|
14
|
+
def initialize(since: nil)
|
15
|
+
if since.to_s == 'beginning'
|
16
|
+
@last_date = nil
|
17
|
+
elsif since.is_a?(String)
|
18
|
+
@last_date = Time.parse(since)
|
19
|
+
elsif since
|
20
|
+
@last_date = since
|
21
|
+
else
|
22
|
+
@last_date = Time.now
|
23
|
+
@eof = true
|
24
|
+
end
|
25
|
+
|
26
|
+
@ignore_errors = false
|
27
|
+
end
|
28
|
+
|
29
|
+
def plc_service
|
30
|
+
PLC_SERVICE
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_export(args = {})
|
34
|
+
url = URI("https://#{plc_service}/export")
|
35
|
+
url.query = URI.encode_www_form(args)
|
36
|
+
|
37
|
+
data = URI.open(url).read
|
38
|
+
data.lines.map(&:strip).reject(&:empty?).map { |x| JSON.parse(x) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def fetch_page
|
42
|
+
request_time = Time.now
|
43
|
+
|
44
|
+
query = @last_date ? { :after => @last_date.utc.iso8601(6) } : {}
|
45
|
+
rows = get_export(query)
|
46
|
+
|
47
|
+
operations = rows.filter_map do |json|
|
48
|
+
begin
|
49
|
+
PLCOperation.new(json)
|
50
|
+
rescue PLCOperation::FormatError => e
|
51
|
+
ignore_errors ? nil : raise
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
@last_date = operations.last&.created_at || request_time
|
56
|
+
@eof = (rows.length < MAX_PAGE)
|
57
|
+
|
58
|
+
operations
|
59
|
+
end
|
60
|
+
|
61
|
+
def fetch(&block)
|
62
|
+
loop do
|
63
|
+
operations = fetch_page
|
64
|
+
block.call(operations)
|
65
|
+
break if eof?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def eof?
|
70
|
+
!!@eof
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
require_relative 'at_handles'
|
4
|
+
require_relative 'service_record'
|
5
|
+
require_relative 'services'
|
6
|
+
|
7
|
+
module DIDKit
|
8
|
+
class PLCOperation
|
9
|
+
class FormatError < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
include AtHandles
|
13
|
+
include Services
|
14
|
+
|
15
|
+
attr_reader :json, :did, :created_at, :type, :handles, :services
|
16
|
+
|
17
|
+
def initialize(json)
|
18
|
+
@json = json
|
19
|
+
@did = json['did']
|
20
|
+
raise FormatError, "Missing DID: #{json}" if @did.nil?
|
21
|
+
raise FormatError, "Invalid DID: #{@did}" unless @did.is_a?(String) && @did.start_with?('did:')
|
22
|
+
|
23
|
+
timestamp = json['createdAt']
|
24
|
+
raise FormatError, "Missing createdAt: #{json}" if timestamp.nil?
|
25
|
+
raise FormatError, "Invalid createdAt: #{timestamp.inspect}" unless timestamp.is_a?(String)
|
26
|
+
|
27
|
+
@created_at = Time.parse(timestamp)
|
28
|
+
|
29
|
+
operation = json['operation']
|
30
|
+
raise FormatError, "Missing operation key: #{json}" if operation.nil?
|
31
|
+
raise FormatError, "Invalid operation data: #{operation.inspect}" unless operation.is_a?(Hash)
|
32
|
+
|
33
|
+
type = operation['type']
|
34
|
+
raise FormatError, "Missing operation type: #{json}" if type.nil?
|
35
|
+
|
36
|
+
@type = type.to_sym
|
37
|
+
return unless @type == :plc_operation
|
38
|
+
|
39
|
+
services = operation['services']
|
40
|
+
raise FormatError, "Missing services key: #{json}" if services.nil?
|
41
|
+
raise FormatError, "Invalid services data: #{services}" unless services.is_a?(Hash)
|
42
|
+
|
43
|
+
@services = services.map { |k, x|
|
44
|
+
type, endpoint = x.values_at('type', 'endpoint')
|
45
|
+
|
46
|
+
raise FormatError, "Missing service type" unless type
|
47
|
+
raise FormatError, "Invalid service type: #{type.inspect}" unless type.is_a?(String)
|
48
|
+
raise FormatError, "Missing service endpoint" unless endpoint
|
49
|
+
raise FormatError, "Invalid service endpoint: #{endpoint.inspect}" unless endpoint.is_a?(String)
|
50
|
+
|
51
|
+
ServiceRecord.new(k, type, endpoint)
|
52
|
+
}
|
53
|
+
|
54
|
+
@handles = parse_also_known_as(operation['alsoKnownAs'] || [])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/didkit/resolver.rb
CHANGED
@@ -8,9 +8,20 @@ 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
|
+
|
16
|
+
def initialize(options = {})
|
17
|
+
@nameserver = options[:nameserver]
|
18
|
+
end
|
19
|
+
|
11
20
|
def resolve_handle(handle)
|
12
21
|
domain = handle.gsub(/^@/, '')
|
13
22
|
|
23
|
+
return nil if RESERVED_DOMAINS.include?(domain.split('.').last)
|
24
|
+
|
14
25
|
if dns_did = resolve_handle_by_dns(domain)
|
15
26
|
DID.new(dns_did, :dns)
|
16
27
|
elsif http_did = resolve_handle_by_well_known(domain)
|
@@ -21,13 +32,13 @@ module DIDKit
|
|
21
32
|
end
|
22
33
|
|
23
34
|
def resolve_handle_by_dns(domain)
|
24
|
-
dns_records = Resolv::DNS.open { |d|
|
35
|
+
dns_records = Resolv::DNS.open(resolv_options) { |d|
|
36
|
+
d.getresources("_atproto.#{domain}", Resolv::DNS::Resource::IN::TXT)
|
37
|
+
}
|
25
38
|
|
26
39
|
if record = dns_records.first
|
27
40
|
if string = record.strings.first
|
28
|
-
|
29
|
-
return $1
|
30
|
-
end
|
41
|
+
return parse_did_from_dns(string)
|
31
42
|
end
|
32
43
|
end
|
33
44
|
|
@@ -35,7 +46,11 @@ module DIDKit
|
|
35
46
|
end
|
36
47
|
|
37
48
|
def resolve_handle_by_well_known(domain)
|
38
|
-
|
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)
|
39
54
|
|
40
55
|
response = Net::HTTP.start(url.host, url.port, use_ssl: true, open_timeout: 10, read_timeout: 10) do |http|
|
41
56
|
request = Net::HTTP::Get.new(url)
|
@@ -44,9 +59,12 @@ module DIDKit
|
|
44
59
|
|
45
60
|
if response.is_a?(Net::HTTPSuccess)
|
46
61
|
if text = response.body
|
47
|
-
|
48
|
-
|
49
|
-
|
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)
|
50
68
|
end
|
51
69
|
end
|
52
70
|
|
@@ -55,6 +73,21 @@ module DIDKit
|
|
55
73
|
nil
|
56
74
|
end
|
57
75
|
|
76
|
+
def resolv_options
|
77
|
+
options = Resolv::DNS::Config.default_config_hash.dup
|
78
|
+
options[:nameserver] = nameserver if nameserver
|
79
|
+
options
|
80
|
+
end
|
81
|
+
|
82
|
+
def parse_did_from_dns(txt)
|
83
|
+
txt =~ /\Adid\=(did\:\w+\:.*)\z/ ? $1 : nil
|
84
|
+
end
|
85
|
+
|
86
|
+
def parse_did_from_well_known(text)
|
87
|
+
text = text.strip
|
88
|
+
text.lines.length == 1 && text =~ /\Adid\:\w+\:.*\z/ ? text : nil
|
89
|
+
end
|
90
|
+
|
58
91
|
def resolve_did(did)
|
59
92
|
did = DID.new(did) if did.is_a?(String)
|
60
93
|
|
@@ -76,7 +109,11 @@ module DIDKit
|
|
76
109
|
def get_validated_handle(did_or_doc)
|
77
110
|
document = did_or_doc.is_a?(Document) ? did_or_doc : resolve_did(did_or_doc)
|
78
111
|
|
79
|
-
document.
|
112
|
+
pick_valid_handle(document.did, document.handles)
|
113
|
+
end
|
114
|
+
|
115
|
+
def pick_valid_handle(did, handles)
|
116
|
+
handles.detect { |h| resolve_handle(h) == did }
|
80
117
|
end
|
81
118
|
end
|
82
119
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require_relative 'errors'
|
3
|
+
|
4
|
+
module DIDKit
|
5
|
+
class ServiceRecord
|
6
|
+
attr_reader :key, :type, :endpoint
|
7
|
+
|
8
|
+
def initialize(key, type, endpoint)
|
9
|
+
begin
|
10
|
+
uri = URI(endpoint)
|
11
|
+
rescue URI::Error
|
12
|
+
raise FormatError, "Invalid service endpoint: #{endpoint.inspect}"
|
13
|
+
end
|
14
|
+
|
15
|
+
@key = key
|
16
|
+
@type = type
|
17
|
+
@endpoint = endpoint
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module DIDKit
|
2
|
+
module Services
|
3
|
+
def get_service(key, type)
|
4
|
+
@services&.detect { |s| s.key == key && s.type == type }
|
5
|
+
end
|
6
|
+
|
7
|
+
def pds_endpoint
|
8
|
+
@pds_endpoint ||= get_service('atproto_pds', 'AtprotoPersonalDataServer')&.endpoint
|
9
|
+
end
|
10
|
+
|
11
|
+
def labeler_endpoint
|
12
|
+
@labeler_endpoint ||= get_service('atproto_labeler', 'AtprotoLabeler')&.endpoint
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/didkit/version.rb
CHANGED
data/lib/didkit.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.2.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-19 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -21,10 +21,15 @@ files:
|
|
21
21
|
- LICENSE.txt
|
22
22
|
- README.md
|
23
23
|
- lib/didkit.rb
|
24
|
+
- lib/didkit/at_handles.rb
|
24
25
|
- lib/didkit/did.rb
|
25
26
|
- lib/didkit/document.rb
|
26
27
|
- lib/didkit/errors.rb
|
28
|
+
- lib/didkit/plc_importer.rb
|
29
|
+
- lib/didkit/plc_operation.rb
|
27
30
|
- lib/didkit/resolver.rb
|
31
|
+
- lib/didkit/service_record.rb
|
32
|
+
- lib/didkit/services.rb
|
28
33
|
- lib/didkit/version.rb
|
29
34
|
- sig/didkit.rbs
|
30
35
|
homepage: https://github.com/mackuba/didkit
|