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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7a4ebcd65dd0bb01a1dfd790dafaa6db2780e704558b8da171eae1db45053d5
4
- data.tar.gz: 27bd5afc5f6bec8207f07b152092337c028abfee26cf0684f5c04d1352241b03
3
+ metadata.gz: 505c6714968219d931f33b5bf4eb42f90f12b6c4391227b4257a5b4063491128
4
+ data.tar.gz: 3e877922065e457bb1fe706db1259b8ddd741cacdc7faa7f17ea3adeb4b23faa
5
5
  SHA512:
6
- metadata.gz: 5cfcbe474eb92088fef122b10df8776b99589a5aa8f1725c5303305cc5ece4696d291f82931e691c92d7b3eb16b0f874efc0fb8bfbfc01f4267defa4e3cc28e4
7
- data.tar.gz: 984039e433ef24d7499c70c5bcd336c4f826195b59b3143a03ca131741146c18fdae3b9088a201b3556169871b8ddd93062e7dfcc7292db4294fb45e464b9e17
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
+ [![Gem Version](https://badge.fury.io/rb/didkit.svg?icon=si%3Arubygems&icon_color=%23ff6251)](https://rubygems.org/gems/didkit) [![YARD Docs](http://img.shields.io/badge/yard-docs-blue.svg)](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` - override the nameserver used for DNS lookups, e.g. to use Google's or CloudFlare's DNS
90
- - `:timeout` - change the connection/response timeout for HTTP requests (default: 15 s)
91
- - `:max_redirects` - change allowed maximum number of redirects (default: 5)
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)).
@@ -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
- aka.select { |x| x =~ %r(\Aat://[^/]+\z) }.map { |x| x.gsub('at://', '') }
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
- else
155
- raise APIError.new(response)
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.
@@ -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
- @services = parse_services(json['service'] || [])
53
- @handles = parse_also_known_as(json['alsoKnownAs'] || [])
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
- if id.is_a?(String) && id.start_with?('#') && type.is_a?(String) && endpoint.is_a?(String)
80
- services << ServiceRecord.new(id.gsub(/^#/, ''), type, endpoint)
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
@@ -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.4.
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 :ignore_errors, :last_date, :error_handler
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
- end
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.filter_map { |json|
73
- begin
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
- @last_date = operations.last&.created_at || request_time
87
- @last_page_cids = Set.new(operations.map(&:cid))
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
@@ -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
- services = operation['services']
89
- raise FormatError, "Missing services key: #{json}" if services.nil?
90
- raise FormatError, "Invalid services data: #{services}" unless services.is_a?(Hash)
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
- @services = services.map { |k, x|
93
- type, endpoint = x.values_at('type', 'endpoint')
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
- ServiceRecord.new(k, type, endpoint)
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
- @handles = parse_also_known_as(operation['alsoKnownAs'])
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
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DIDKit
4
- VERSION = "0.3.2"
4
+ VERSION = "0.4.0"
5
5
  end
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.3.2
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.3
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