didkit 0.3.2 → 0.4.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 +18 -1
- data/README.md +12 -3
- data/lib/didkit/at_handles.rb +23 -2
- data/lib/didkit/did.rb +10 -8
- data/lib/didkit/document.rb +17 -15
- data/lib/didkit/plc_importer.rb +15 -31
- data/lib/didkit/plc_operation.rb +52 -18
- data/lib/didkit/services.rb +4 -0
- data/lib/didkit/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 505c6714968219d931f33b5bf4eb42f90f12b6c4391227b4257a5b4063491128
|
|
4
|
+
data.tar.gz: 3e877922065e457bb1fe706db1259b8ddd741cacdc7faa7f17ea3adeb4b23faa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 587c899080bdb947261b54b2bbe2b3263567cb9aecca682086149d3a0ca4692a443a217b42520caca3e24ec246a5e3a0ea058d86c903cadb13075c2ebbac0291
|
|
7
|
+
data.tar.gz: c76d96e9fda51a31d87a7e15f93549e9594d8651d7668d77a38b9fe0bff09631d78a2d3a34f0afbd6cd9506a0208e9ece7617ebf1128fca7d1ab7d27896a5ea3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
+
## [0.4.0] - 2026-07-02
|
|
2
|
+
|
|
3
|
+
- added `#also_known_as` separate from `#handles` in `Document` and `PLCOperation`, which stores the original array of strings as listed in the JSON, not filtered and not stripped of the `at://` prefix
|
|
4
|
+
- added `PLCOperation#nullified?`
|
|
5
|
+
- added support for parsing `handle` and `service` fields from legacy (2022-23 era) `:create` PLC ops in `PLCOperation`
|
|
6
|
+
- `PLCImporter` can be configured with a custom hostname of a PLC mirror to use instead of [plc.directory](https://plc.directory)
|
|
7
|
+
- `PLCImporter` will no longer update cursor to local request time if an empty page is received, but will instead leave the cursor as is – local time might have some offset vs. server time if the clock isn't set right, and this could lead to skipped operations
|
|
8
|
+
- in `DID#account_status` / `account_active?` / `account_exists?`, accept `getRepoStatus` endpoint returning responses with status 404 instead of 400 (since some PDS implementations do it this way)
|
|
9
|
+
- tweaked parsing of services in `Document` and `PLCOperation`:
|
|
10
|
+
1. `Document` raises format errors if the fields are missing or not of expected type (since this shouldn't happen)
|
|
11
|
+
2. `Document` & `PLCOperation` silently ignore the service if its endpoint is not a valid URI (since this does happen)
|
|
12
|
+
3. Removed `ignore_errors` / `error_handler` from `PLCImporter`, since because of 2) no expected errors are emitted outside `PLCOperation`
|
|
13
|
+
|
|
14
|
+
## [0.3.3] - 2026-02-22
|
|
15
|
+
|
|
16
|
+
- fixed old `FormatError` classes mentioned in `PLCImporter`
|
|
17
|
+
|
|
1
18
|
## [0.3.2] - 2026-02-15
|
|
2
19
|
|
|
3
|
-
- added YARD API documentation
|
|
20
|
+
- added [YARD API documentation](https://rubydoc.info/gems/didkit/)
|
|
4
21
|
- marked some helper methods in `requests.rb`, `at_handles.rb` and `DIDKit::Resolver` as private
|
|
5
22
|
- merged all "FormatErrors" into one `DIDKit::FormatError`
|
|
6
23
|
- added `frozen_string_literal` directive everywhere to minimize garbage collection
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# DIDKit 🪪
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/didkit) [](https://rubydoc.info/gems/didkit)
|
|
4
|
+
|
|
3
5
|
A small Ruby gem for handling Distributed Identifiers (DIDs) in Bluesky / AT Protocol.
|
|
4
6
|
|
|
5
7
|
> [!NOTE]
|
|
@@ -86,9 +88,9 @@ You can customize some things about the DID/handle lookups by using the `DIDKit:
|
|
|
86
88
|
|
|
87
89
|
Currently available options include:
|
|
88
90
|
|
|
89
|
-
- `:nameserver`
|
|
90
|
-
- `:timeout`
|
|
91
|
-
- `:max_redirects`
|
|
91
|
+
- `:nameserver` – override the nameserver used for DNS lookups, e.g. to use Google's or CloudFlare's DNS
|
|
92
|
+
- `:timeout` – change the connection/response timeout for HTTP requests (default: 15 s)
|
|
93
|
+
- `:max_redirects` – change allowed maximum number of redirects (default: 5)
|
|
92
94
|
|
|
93
95
|
Example:
|
|
94
96
|
|
|
@@ -106,6 +108,13 @@ resolver.get_verified_handle(did)
|
|
|
106
108
|
# => 'nytimes.com'
|
|
107
109
|
```
|
|
108
110
|
|
|
111
|
+
## Other resources
|
|
112
|
+
|
|
113
|
+
- [YARD API documentation](https://rubydoc.info/gems/didkit) at rubydoc.info
|
|
114
|
+
- [ruby.sdk.blue](https://ruby.sdk.blue)
|
|
115
|
+
- [Example scripts](https://ruby.sdk.blue/examples/)
|
|
116
|
+
|
|
117
|
+
|
|
109
118
|
## Credits
|
|
110
119
|
|
|
111
120
|
Copyright © 2026 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr)).
|
data/lib/didkit/at_handles.rb
CHANGED
|
@@ -5,18 +5,39 @@ require_relative 'errors'
|
|
|
5
5
|
module DIDKit
|
|
6
6
|
|
|
7
7
|
#
|
|
8
|
-
# @private
|
|
8
|
+
# @api private
|
|
9
9
|
#
|
|
10
10
|
|
|
11
11
|
module AtHandles
|
|
12
12
|
|
|
13
|
+
# Returns a list of ATProto handles assigned to this DID in its DID document.
|
|
14
|
+
#
|
|
15
|
+
# Note: the handles aren't guaranteed to be verified (validated in the other direction).
|
|
16
|
+
# Use {DID#get_verified_handle} to find a handle that is correctly verified.
|
|
17
|
+
#
|
|
18
|
+
# @api public
|
|
19
|
+
# @return [Array<String>]
|
|
20
|
+
|
|
21
|
+
attr_reader :handles
|
|
22
|
+
|
|
23
|
+
# Returns a list of all identifiers assigned to this DID in its DID document through the
|
|
24
|
+
# `alsoKnownAs` field. This includes ATProto handles (in the format `at://example.com`) and
|
|
25
|
+
# potentially other URIs like `http` URLs (e.g. for Bridgy accounts), and even (technically
|
|
26
|
+
# invalid) non-URI strings. Use {#handles} to get just the ATProto handles.
|
|
27
|
+
#
|
|
28
|
+
# @api public
|
|
29
|
+
# @return [Array<String>]
|
|
30
|
+
|
|
31
|
+
attr_reader :also_known_as
|
|
32
|
+
|
|
13
33
|
private
|
|
14
34
|
|
|
15
35
|
def parse_also_known_as(aka)
|
|
16
36
|
raise FormatError, "Invalid alsoKnownAs: #{aka.inspect}" unless aka.is_a?(Array)
|
|
17
37
|
raise FormatError, "Invalid alsoKnownAs: #{aka.inspect}" unless aka.all? { |x| x.is_a?(String) }
|
|
18
38
|
|
|
19
|
-
|
|
39
|
+
@also_known_as = aka
|
|
40
|
+
@handles = aka.select { |x| x =~ %r(\Aat://[^/]+\z) }.map { |x| x.gsub('at://', '') }
|
|
20
41
|
end
|
|
21
42
|
end
|
|
22
43
|
end
|
data/lib/didkit/did.rb
CHANGED
|
@@ -148,17 +148,19 @@ module DIDKit
|
|
|
148
148
|
json = JSON.parse(response.body)
|
|
149
149
|
|
|
150
150
|
if json['active'] == true
|
|
151
|
-
:active
|
|
151
|
+
return :active
|
|
152
152
|
elsif json['active'] == false && json['status'].is_a?(String) && json['status'].length <= 100
|
|
153
|
-
json['status'].to_sym
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
return json['status'].to_sym
|
|
154
|
+
end
|
|
155
|
+
elsif (status == 400 || status == 404) && is_json
|
|
156
|
+
json = JSON.parse(response.body)
|
|
157
|
+
|
|
158
|
+
if json['error'] == 'RepoNotFound'
|
|
159
|
+
return nil
|
|
156
160
|
end
|
|
157
|
-
elsif status == 400 && is_json && JSON.parse(response.body)['error'] == 'RepoNotFound'
|
|
158
|
-
nil
|
|
159
|
-
else
|
|
160
|
-
raise APIError.new(response)
|
|
161
161
|
end
|
|
162
|
+
|
|
163
|
+
raise APIError.new(response)
|
|
162
164
|
end
|
|
163
165
|
|
|
164
166
|
# Checks if the account is seen as active on its own PDS, using the `getRepoStatus` endpoint.
|
data/lib/didkit/document.rb
CHANGED
|
@@ -24,14 +24,6 @@ module DIDKit
|
|
|
24
24
|
# @return [DID] the DID that this document describes
|
|
25
25
|
attr_reader :did
|
|
26
26
|
|
|
27
|
-
# Returns a list of handles assigned to this DID in its DID document.
|
|
28
|
-
#
|
|
29
|
-
# Note: the handles aren't guaranteed to be verified (validated in the other direction).
|
|
30
|
-
# Use {#get_verified_handle} to find a handle that is correctly verified.
|
|
31
|
-
#
|
|
32
|
-
# @return [Array<String>]
|
|
33
|
-
attr_reader :handles
|
|
34
|
-
|
|
35
27
|
# @return [Array<ServiceRecords>] service records like PDS details assigned to the DID
|
|
36
28
|
attr_reader :services
|
|
37
29
|
|
|
@@ -49,8 +41,8 @@ module DIDKit
|
|
|
49
41
|
@did = did
|
|
50
42
|
@json = json
|
|
51
43
|
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
parse_services(json['service'] || [])
|
|
45
|
+
parse_also_known_as(json['alsoKnownAs'] || [])
|
|
54
46
|
end
|
|
55
47
|
|
|
56
48
|
# Returns the first verified handle assigned to the DID.
|
|
@@ -71,17 +63,27 @@ module DIDKit
|
|
|
71
63
|
def parse_services(service_data)
|
|
72
64
|
raise FormatError, "Invalid service data" unless service_data.is_a?(Array) && service_data.all? { |x| x.is_a?(Hash) }
|
|
73
65
|
|
|
74
|
-
services = []
|
|
66
|
+
@services = []
|
|
75
67
|
|
|
76
68
|
service_data.each do |x|
|
|
77
69
|
id, type, endpoint = x.values_at('id', 'type', 'serviceEndpoint')
|
|
78
70
|
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
raise FormatError, "Missing service id" unless id
|
|
72
|
+
raise FormatError, "Invalid service id: #{id.inspect}" unless id.is_a?(String)
|
|
73
|
+
next if !id.start_with?('#')
|
|
74
|
+
|
|
75
|
+
raise FormatError, "Missing service type" unless type
|
|
76
|
+
raise FormatError, "Invalid service type: #{type.inspect}" unless type.is_a?(String)
|
|
77
|
+
|
|
78
|
+
raise FormatError, "Missing service endpoint" unless endpoint
|
|
79
|
+
raise FormatError, "Invalid service endpoint: #{endpoint.inspect}" unless endpoint.is_a?(String)
|
|
80
|
+
|
|
81
|
+
begin
|
|
82
|
+
@services << ServiceRecord.new(id.gsub(/^#/, ''), type, endpoint)
|
|
83
|
+
rescue FormatError => e
|
|
84
|
+
# ignore services with invalid URIs
|
|
81
85
|
end
|
|
82
86
|
end
|
|
83
|
-
|
|
84
|
-
services
|
|
85
87
|
end
|
|
86
88
|
end
|
|
87
89
|
end
|
data/lib/didkit/plc_importer.rb
CHANGED
|
@@ -4,12 +4,13 @@ require 'json'
|
|
|
4
4
|
require 'time'
|
|
5
5
|
require 'uri'
|
|
6
6
|
|
|
7
|
+
require_relative 'errors'
|
|
7
8
|
require_relative 'plc_operation'
|
|
8
9
|
require_relative 'requests'
|
|
9
10
|
|
|
10
11
|
#
|
|
11
12
|
# NOTE: this class is pending a rewrite once new APIs are deployed to plc.directory.
|
|
12
|
-
# Things will change here in v. 0.
|
|
13
|
+
# Things will change here in v. 0.5.
|
|
13
14
|
#
|
|
14
15
|
|
|
15
16
|
module DIDKit
|
|
@@ -19,9 +20,9 @@ module DIDKit
|
|
|
19
20
|
|
|
20
21
|
include Requests
|
|
21
22
|
|
|
22
|
-
attr_accessor :
|
|
23
|
+
attr_accessor :last_date
|
|
23
24
|
|
|
24
|
-
def initialize(since: nil)
|
|
25
|
+
def initialize(since: nil, service: nil)
|
|
25
26
|
if since.to_s == 'beginning'
|
|
26
27
|
@last_date = nil
|
|
27
28
|
elsif since.is_a?(String)
|
|
@@ -34,24 +35,11 @@ module DIDKit
|
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
@last_page_cids = []
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def plc_service
|
|
40
|
-
PLC_SERVICE
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def ignore_errors=(val)
|
|
44
|
-
@ignore_errors = val
|
|
45
|
-
|
|
46
|
-
if val
|
|
47
|
-
@error_handler = proc { |e, j| "(ignore error)" }
|
|
48
|
-
else
|
|
49
|
-
@error_handler = nil
|
|
50
|
-
end
|
|
38
|
+
@plc_service = service || PLC_SERVICE
|
|
51
39
|
end
|
|
52
40
|
|
|
53
41
|
def get_export(args = {})
|
|
54
|
-
url = URI("https://#{plc_service}/export")
|
|
42
|
+
url = URI("https://#{@plc_service}/export")
|
|
55
43
|
url.query = URI.encode_www_form(args)
|
|
56
44
|
|
|
57
45
|
data = get_data(url, content_type: 'application/jsonlines')
|
|
@@ -59,23 +47,16 @@ module DIDKit
|
|
|
59
47
|
end
|
|
60
48
|
|
|
61
49
|
def fetch_audit_log(did)
|
|
62
|
-
json = get_json("https://#{plc_service}/#{did}/log/audit", :content_type => :json)
|
|
50
|
+
json = get_json("https://#{@plc_service}/#{did}/log/audit", :content_type => :json)
|
|
63
51
|
json.map { |j| PLCOperation.new(j) }
|
|
64
|
-
end
|
|
52
|
+
end
|
|
65
53
|
|
|
66
54
|
def fetch_page
|
|
67
|
-
request_time = Time.now
|
|
68
|
-
|
|
69
55
|
query = @last_date ? { :after => @last_date.utc.iso8601(6) } : {}
|
|
70
56
|
rows = get_export(query)
|
|
71
57
|
|
|
72
|
-
operations = rows.
|
|
73
|
-
|
|
74
|
-
PLCOperation.new(json)
|
|
75
|
-
rescue PLCOperation::FormatError, AtHandles::FormatError, ServiceRecord::FormatError => e
|
|
76
|
-
@error_handler ? @error_handler.call(e, json) : raise
|
|
77
|
-
nil
|
|
78
|
-
end
|
|
58
|
+
operations = rows.map { |json|
|
|
59
|
+
PLCOperation.new(json)
|
|
79
60
|
}.reject { |op|
|
|
80
61
|
# when you pass the most recent op's timestamp to ?after, it will be returned as the first op again,
|
|
81
62
|
# so we need to use this CID list to filter it out (so pages will usually be 999 items long)
|
|
@@ -83,8 +64,11 @@ module DIDKit
|
|
|
83
64
|
@last_page_cids.include?(op.cid)
|
|
84
65
|
}
|
|
85
66
|
|
|
86
|
-
|
|
87
|
-
|
|
67
|
+
unless operations.empty?
|
|
68
|
+
@last_date = operations.last.created_at
|
|
69
|
+
@last_page_cids = Set.new(operations.map(&:cid))
|
|
70
|
+
end
|
|
71
|
+
|
|
88
72
|
@eof = (rows.length < MAX_PAGE)
|
|
89
73
|
|
|
90
74
|
operations
|
data/lib/didkit/plc_operation.rb
CHANGED
|
@@ -38,15 +38,6 @@ module DIDKit
|
|
|
38
38
|
# @return [String] the operation type
|
|
39
39
|
attr_reader :type
|
|
40
40
|
|
|
41
|
-
# Returns a list of handles assigned to the DID in this operation.
|
|
42
|
-
#
|
|
43
|
-
# Note: the handles aren't guaranteed to be verified (validated in the other direction).
|
|
44
|
-
# Use {DID#get_verified_handle} or {Document#get_verified_handle} to find a handle that is
|
|
45
|
-
# correctly verified.
|
|
46
|
-
#
|
|
47
|
-
# @return [Array<String>]
|
|
48
|
-
attr_reader :handles
|
|
49
|
-
|
|
50
41
|
# @return [Array<ServiceRecords>] service records like PDS details assigned to the DID
|
|
51
42
|
attr_reader :services
|
|
52
43
|
|
|
@@ -81,26 +72,69 @@ module DIDKit
|
|
|
81
72
|
|
|
82
73
|
type = operation['type']
|
|
83
74
|
raise FormatError, "Missing operation type: #{json}" if type.nil?
|
|
75
|
+
raise FormatError, "Invalid operation type: #{type.inspect}" unless type.is_a?(String)
|
|
84
76
|
|
|
85
77
|
@type = type.to_sym
|
|
86
|
-
return unless @type == :plc_operation
|
|
87
78
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
79
|
+
case @type
|
|
80
|
+
when :plc_operation
|
|
81
|
+
raise FormatError, "Missing services key: #{json}" if operation['services'].nil?
|
|
82
|
+
|
|
83
|
+
parse_services(operation['services'])
|
|
84
|
+
parse_also_known_as(operation['alsoKnownAs'])
|
|
85
|
+
when :create
|
|
86
|
+
parse_legacy_ops(operation)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @return [Boolean] if the operation has been nullified through a rotation operation
|
|
91
|
+
def nullified?
|
|
92
|
+
@json['nullified'] == true
|
|
93
|
+
end
|
|
94
|
+
|
|
91
95
|
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def parse_services(service_data)
|
|
99
|
+
raise FormatError, "Invalid services data: #{service_data.inspect}" unless service_data.is_a?(Hash)
|
|
100
|
+
|
|
101
|
+
@services = []
|
|
102
|
+
|
|
103
|
+
service_data.each do |key, data|
|
|
104
|
+
type, endpoint = data.values_at('type', 'endpoint')
|
|
94
105
|
|
|
95
106
|
raise FormatError, "Missing service type" unless type
|
|
96
107
|
raise FormatError, "Invalid service type: #{type.inspect}" unless type.is_a?(String)
|
|
108
|
+
|
|
97
109
|
raise FormatError, "Missing service endpoint" unless endpoint
|
|
98
110
|
raise FormatError, "Invalid service endpoint: #{endpoint.inspect}" unless endpoint.is_a?(String)
|
|
99
111
|
|
|
100
|
-
|
|
101
|
-
|
|
112
|
+
begin
|
|
113
|
+
@services << ServiceRecord.new(key, type, endpoint)
|
|
114
|
+
rescue FormatError => e
|
|
115
|
+
# ignore services with invalid URIs
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def parse_legacy_ops(operation)
|
|
121
|
+
@handles = []
|
|
122
|
+
@services = []
|
|
123
|
+
|
|
124
|
+
if handle = operation['handle']
|
|
125
|
+
raise FormatError, "Handle should be a string" unless handle.is_a?(String)
|
|
126
|
+
@handles << handle
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
if service = operation['service']
|
|
130
|
+
raise FormatError, "Service should be a string" unless service.is_a?(String)
|
|
102
131
|
|
|
103
|
-
|
|
132
|
+
begin
|
|
133
|
+
@services << ServiceRecord.new('atproto_pds', 'AtprotoPersonalDataServer', service)
|
|
134
|
+
rescue FormatError
|
|
135
|
+
# ignore services with invalid URIs
|
|
136
|
+
end
|
|
137
|
+
end
|
|
104
138
|
end
|
|
105
139
|
end
|
|
106
140
|
end
|
data/lib/didkit/services.rb
CHANGED
|
@@ -48,6 +48,8 @@ module DIDKit
|
|
|
48
48
|
|
|
49
49
|
# Returns the hostname of the PDS service, if present.
|
|
50
50
|
#
|
|
51
|
+
# If the endpoint URL includes a port number, it is excluded.
|
|
52
|
+
#
|
|
51
53
|
# @api public
|
|
52
54
|
# @return [String, nil] hostname of the PDS endpoint URL
|
|
53
55
|
|
|
@@ -57,6 +59,8 @@ module DIDKit
|
|
|
57
59
|
|
|
58
60
|
# Returns the hostname of the labeler service, if present.
|
|
59
61
|
#
|
|
62
|
+
# If the endpoint URL includes a port number, it is excluded.
|
|
63
|
+
#
|
|
60
64
|
# @api public
|
|
61
65
|
# @return [String, nil] hostname of the labeler endpoint URL
|
|
62
66
|
|
data/lib/didkit/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: didkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kuba Suder
|
|
@@ -52,7 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0'
|
|
54
54
|
requirements: []
|
|
55
|
-
rubygems_version: 4.0.
|
|
55
|
+
rubygems_version: 4.0.15
|
|
56
56
|
specification_version: 4
|
|
57
57
|
summary: A library for handling Distributed ID (DID) identifiers used in Bluesky AT
|
|
58
58
|
Protocol
|