didkit 0.0.4 → 0.2.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 +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
|