A library for handling DID identifiers used in Bluesky AT Protocol

Compare changes

Choose any two refs to compare.

+392 -61
+1
.github/workflows/main.yml
··· 21 - '3.2' 22 - '3.3' 23 - '3.4' 24 25 steps: 26 - uses: actions/checkout@v3
··· 21 - '3.2' 22 - '3.3' 23 - '3.4' 24 + - '4.0' 25 26 steps: 27 - uses: actions/checkout@v3
+2
.gitignore
··· 1 .bundle 2 .DS_Store 3 .rspec_status 4 coverage 5 Gemfile.lock
··· 1 .bundle 2 .DS_Store 3 .rspec_status 4 + .yardoc 5 coverage 6 + doc 7 Gemfile.lock
+4
.yardopts
···
··· 1 + --protected 2 + --no-private 3 + --markup markdown 4 + --embed-mixin "DIDKit::Services"
+3
Gemfile
··· 9 gem 'rspec', '~> 3.0' 10 gem 'irb' 11 12 gem 'mocha' 13 gem 'simplecov' 14 gem 'webmock'
··· 9 gem 'rspec', '~> 3.0' 10 gem 'irb' 11 12 + gem 'rdoc' 13 + gem 'yard' 14 + 15 gem 'mocha' 16 gem 'simplecov' 17 gem 'webmock'
+1 -1
LICENSE.txt
··· 1 The zlib License 2 3 - Copyright (c) 2025 Jakub Suder 4 5 This software is provided 'as-is', without any express or implied 6 warranty. In no event will the authors be held liable for any damages
··· 1 The zlib License 2 3 + Copyright (c) 2026 Jakub Suder 4 5 This software is provided 'as-is', without any express or implied 6 warranty. In no event will the authors be held liable for any damages
+8 -4
README.md
··· 13 14 ## Installation 15 16 - From the command line: 17 18 - gem install didkit 19 20 - Or, add this to your `Gemfile`: 21 22 gem 'didkit', '~> 0.3' 23 ··· 106 107 ## Credits 108 109 - Copyright ยฉ 2025 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr)). 110 111 The code is available under the terms of the [zlib license](https://choosealicense.com/licenses/zlib/) (permissive, similar to MIT).
··· 13 14 ## Installation 15 16 + To use DIDKit, you need a reasonably new version of Ruby โ€“ it should run on Ruby 2.6 and above, although it's recommended to use a version that's still getting maintainance updates, i.e. currently 3.2+. A compatible version should be preinstalled on macOS Big Sur and above and on many Linux systems. Otherwise, you can install one using tools such as [RVM](https://rvm.io), [asdf](https://asdf-vm.com), [ruby-install](https://github.com/postmodern/ruby-install) or [ruby-build](https://github.com/rbenv/ruby-build), or `rpm` or `apt-get` on Linux (see more installation options on [ruby-lang.org](https://www.ruby-lang.org/en/downloads/)). 17 18 + To install the gem, run in the command line: 19 20 + [sudo] gem install didkit 21 + 22 + Or add this to your app's `Gemfile`: 23 24 gem 'didkit', '~> 0.3' 25 ··· 108 109 ## Credits 110 111 + Copyright ยฉ 2026 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr)). 112 113 The code is available under the terms of the [zlib license](https://choosealicense.com/licenses/zlib/) (permissive, similar to MIT). 114 + 115 + Bug reports and pull requests are welcome ๐Ÿ˜Ž
+9 -2
lib/didkit/at_handles.rb
··· 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)
··· 1 + require_relative 'errors' 2 + 3 module DIDKit 4 + 5 + # 6 + # @private 7 + # 8 + 9 module AtHandles 10 + 11 + private 12 13 def parse_also_known_as(aka) 14 raise FormatError, "Invalid alsoKnownAs: #{aka.inspect}" unless aka.is_a?(Array)
+87 -2
lib/didkit/did.rb
··· 6 require_relative 'resolver' 7 8 module DIDKit 9 class DID 10 GENERIC_REGEXP = /\Adid\:\w+\:.+\z/ 11 12 include Requests 13 14 def self.resolve_handle(handle) 15 Resolver.new.resolve_handle(handle) 16 end 17 18 - attr_reader :type, :did, :resolved_by 19 20 def initialize(did, resolved_by = nil) 21 if did.is_a?(DID) ··· 36 @resolved_by = resolved_by 37 end 38 39 - alias to_s did 40 41 def document 42 @document ||= get_document 43 end 44 45 def get_document 46 Resolver.new.resolve_did(self) 47 end 48 49 def get_verified_handle 50 Resolver.new.get_verified_handle(document) 51 end 52 53 def get_audit_log 54 if @type == :plc 55 PLCImporter.new.fetch_audit_log(self) ··· 58 end 59 end 60 61 def web_domain 62 did.gsub(/^did\:web\:/, '') if type == :web 63 end 64 65 def account_status(request_options = {}) 66 doc = self.document 67 return nil if doc.pds_endpoint.nil? ··· 91 end 92 end 93 94 def account_active? 95 account_status == :active 96 end 97 98 def account_exists? 99 account_status != nil 100 end 101 102 def ==(other) 103 if other.is_a?(String)
··· 6 require_relative 'resolver' 7 8 module DIDKit 9 + 10 + # 11 + # Represents a DID identifier (account on the ATProto network). This class serves as an entry 12 + # point to various lookup helpers. For convenience it can also be accessed as just `DID` without 13 + # the `DIDKit::` prefix. 14 + # 15 + # @example Resolving a handle 16 + # did = DID.resolve_handle('bsky.app') 17 + # 18 + 19 class DID 20 GENERIC_REGEXP = /\Adid\:\w+\:.+\z/ 21 22 include Requests 23 24 + # Resolve a handle into a DID. Looks up the given ATProto domain handle using the DNS TXT method 25 + # and the HTTP .well-known method and returns a DID if one is assigned using either of the methods. 26 + # 27 + # If a DID string or a {DID} object is passed, it simply returns that DID, so you can use this 28 + # method to pass it an input string from the user which can be a DID or handle, without having to 29 + # check which one it is. 30 + # 31 + # @param handle [String, DID] a domain handle (may start with an `@`) or a DID string 32 + # @return [DID, nil] resolved DID if found, nil otherwise 33 + 34 def self.resolve_handle(handle) 35 Resolver.new.resolve_handle(handle) 36 end 37 38 + # @return [Symbol] DID type (`:plc` or `:web`) 39 + attr_reader :type 40 + 41 + # @return [String] DID identifier string 42 + attr_reader :did 43 + 44 + # @return [Symbol, nil] `:dns` or `:http` if the DID was looked up using one of those methods 45 + attr_reader :resolved_by 46 + 47 + alias to_s did 48 + 49 + 50 + # Create a DID object from a DID string. 51 + # 52 + # @param did [String, DID] DID string or another DID object 53 + # @param resolved_by [Symbol, nil] optionally, how the DID was looked up (`:dns` or `:http`) 54 + # @raise [DIDError] when the DID format or type is invalid 55 56 def initialize(did, resolved_by = nil) 57 if did.is_a?(DID) ··· 72 @resolved_by = resolved_by 73 end 74 75 + # Returns or looks up the DID document with the DID's identity details from an appropriate source. 76 + # This method caches the document in a local variable if it's called again. 77 + # 78 + # @return [Document] resolved DID document 79 80 def document 81 @document ||= get_document 82 end 83 84 + # Looks up the DID document with the DID's identity details from an appropriate source. 85 + # @return [Document] resolved DID document 86 + 87 def get_document 88 Resolver.new.resolve_did(self) 89 end 90 91 + # Returns the first verified handle assigned to this DID. 92 + # 93 + # Looks up the domain handles assigned to this DID in its DID document, checks if they are 94 + # verified (i.e. assigned correctly to this DID using DNS TXT or .well-known) and returns 95 + # the first handle that validates correctly, or nil if none matches. 96 + # 97 + # @return [String, nil] verified handle domain, if found 98 + 99 def get_verified_handle 100 Resolver.new.get_verified_handle(document) 101 end 102 103 + # Fetches the PLC audit log (list of all previous operations) for a did:plc DID. 104 + # 105 + # @return [Array<PLCOperation>] list of PLC operations in the audit log 106 + # @raise [DIDError] when the DID is not a did:plc 107 + 108 def get_audit_log 109 if @type == :plc 110 PLCImporter.new.fetch_audit_log(self) ··· 113 end 114 end 115 116 + # Returns the domain portion of a did:web identifier. 117 + # 118 + # @return [String, nil] DID domain if the DID is a did:web, nil for did:plc 119 + 120 def web_domain 121 did.gsub(/^did\:web\:/, '') if type == :web 122 end 123 124 + # Checks the status of the account/repo on its own PDS using the `getRepoStatus` endpoint. 125 + # 126 + # @param request_options [Hash] request options to override 127 + # @option request_options [Integer] :timeout request timeout (default: 15) 128 + # @option request_options [Integer] :max_redirects maximum number of redirects to follow (default: 5) 129 + # 130 + # @return [Symbol, nil] `:active`, or returned inactive status, or `nil` if account is not found 131 + # @raise [APIError] when the response is invalid 132 + 133 def account_status(request_options = {}) 134 doc = self.document 135 return nil if doc.pds_endpoint.nil? ··· 159 end 160 end 161 162 + # Checks if the account is seen as active on its own PDS, using the `getRepoStatus` endpoint. 163 + # This is a helper which calls the {#account_status} method and checks if the status is `:active`. 164 + # 165 + # @return [Boolean] true if the returned status is active 166 + # @raise [APIError] when the response is invalid 167 + 168 def account_active? 169 account_status == :active 170 end 171 172 + # Checks if the account exists its own PDS, using the `getRepoStatus` endpoint. 173 + # This is a helper which calls the {#account_status} method and checks if the repo is found at all. 174 + # 175 + # @return [Boolean] true if the returned status is valid, false if repo is not found 176 + # @raise [APIError] when the response is invalid 177 + 178 def account_exists? 179 account_status != nil 180 end 181 + 182 + # Compares the DID to another DID object or string. 183 + # 184 + # @param other [DID, String] other DID to compare with 185 + # @return [Boolean] true if it's the same DID 186 187 def ==(other) 188 if other.is_a?(String)
+39 -4
lib/didkit/document.rb
··· 1 require_relative 'at_handles' 2 require_relative 'resolver' 3 require_relative 'service_record' 4 require_relative 'services' 5 6 module DIDKit 7 class Document 8 - class FormatError < StandardError 9 - end 10 - 11 include AtHandles 12 include Services 13 14 - attr_reader :json, :did, :handles, :services 15 16 def initialize(did, json) 17 raise FormatError, "Missing id field" if json['id'].nil? ··· 25 @handles = parse_also_known_as(json['alsoKnownAs'] || []) 26 end 27 28 def get_verified_handle 29 Resolver.new.get_verified_handle(self) 30 end 31 32 private 33
··· 1 require_relative 'at_handles' 2 + require_relative 'errors' 3 require_relative 'resolver' 4 require_relative 'service_record' 5 require_relative 'services' 6 7 module DIDKit 8 + 9 + # 10 + # Parsed DID document from a JSON file loaded from [plc.directory](https://plc.directory) or a did:web domain. 11 + # 12 + # Use {DID#document} or {Resolver#resolve_did} to fetch a DID document and return this object. 13 + # 14 + 15 class Document 16 include AtHandles 17 include Services 18 19 + # @return [Hash] the complete JSON data of the DID document 20 + attr_reader :json 21 + 22 + # @return [DID] the DID that this document describes 23 + attr_reader :did 24 + 25 + # Returns a list of handles assigned to this DID in its DID document. 26 + # 27 + # Note: the handles aren't guaranteed to be verified (validated in the other direction). 28 + # Use {#get_verified_handle} to find a handle that is correctly verified. 29 + # 30 + # @return [Array<String>] 31 + attr_reader :handles 32 + 33 + # @return [Array<ServiceRecords>] service records like PDS details assigned to the DID 34 + attr_reader :services 35 + 36 + # Creates a DID document object. 37 + # 38 + # @param did [DID] DID object 39 + # @param json [Hash] DID document JSON 40 + # @raise [FormatError] when required fields are missing or invalid. 41 42 def initialize(did, json) 43 raise FormatError, "Missing id field" if json['id'].nil? ··· 51 @handles = parse_also_known_as(json['alsoKnownAs'] || []) 52 end 53 54 + # Returns the first verified handle assigned to the DID. 55 + # 56 + # Looks up the domain handles assigned to this DID in the DID document, checks if they are 57 + # verified (i.e. assigned correctly to this DID using DNS TXT or .well-known) and returns 58 + # the first handle that validates correctly, or nil if none matches. 59 + # 60 + # @return [String, nil] verified handle domain, if found 61 + 62 def get_verified_handle 63 Resolver.new.get_verified_handle(self) 64 end 65 + 66 67 private 68
+20 -2
lib/didkit/errors.rb
··· 1 module DIDKit 2 - class DIDError < StandardError 3 - end 4 5 class APIError < StandardError 6 attr_reader :response 7 8 def initialize(response) 9 @response = response 10 super("APIError: #{response}") 11 end 12 13 def status 14 response.code.to_i 15 end 16 17 def body 18 response.body 19 end 20 end 21 end
··· 1 module DIDKit 2 3 + # 4 + # Raised when an HTTP request returns a response with an error status. 5 + # 6 class APIError < StandardError 7 + 8 + # @return [Net::HTTPResponse] the returned HTTP response 9 attr_reader :response 10 11 + # @param response [Net::HTTPResponse] the returned HTTP response 12 def initialize(response) 13 @response = response 14 super("APIError: #{response}") 15 end 16 17 + # @return [Integer] HTTP status code 18 def status 19 response.code.to_i 20 end 21 22 + # @return [String] HTTP response body 23 def body 24 response.body 25 end 26 + end 27 + 28 + # 29 + # Raised when a string is not a valid DID or not of the right type. 30 + # 31 + class DIDError < StandardError 32 + end 33 + 34 + # 35 + # Raised when the loaded data has some missing or invalid fields. 36 + # 37 + class FormatError < StandardError 38 end 39 end
+44 -4
lib/didkit/plc_operation.rb
··· 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, :cid, :seq, :created_at, :type, :handles, :services 16 17 def initialize(json) 18 @json = json
··· 1 require 'time' 2 3 require_relative 'at_handles' 4 + require_relative 'errors' 5 require_relative 'service_record' 6 require_relative 'services' 7 8 module DIDKit 9 10 + # 11 + # Represents a single operation of changing a specific DID's data in the [plc.directory](https://plc.directory) 12 + # (e.g. changing assigned handles or migrating to a different PDS). 13 + # 14 + 15 + class PLCOperation 16 include AtHandles 17 include Services 18 19 + # @return [Hash] the JSON from which the operation is parsed 20 + attr_reader :json 21 + 22 + # @return [String] the DID which the operation concerns 23 + attr_reader :did 24 + 25 + # @return [String] CID (Content Identifier) of the operation 26 + attr_reader :cid 27 + 28 + # Returns a sequential number of the operation (only used in the new export API). 29 + # @return [Integer, nil] sequential number of the operation 30 + attr_reader :seq 31 + 32 + # @return [Time] time when the operation was created 33 + attr_reader :created_at 34 + 35 + # Returns the `type` field of the operation (usually `"plc_operation"`). 36 + # @return [String] the operation type 37 + attr_reader :type 38 + 39 + # Returns a list of handles assigned to the DID in this operation. 40 + # 41 + # Note: the handles aren't guaranteed to be verified (validated in the other direction). 42 + # Use {DID#get_verified_handle} or {Document#get_verified_handle} to find a handle that is 43 + # correctly verified. 44 + # 45 + # @return [Array<String>] 46 + attr_reader :handles 47 + 48 + # @return [Array<ServiceRecords>] service records like PDS details assigned to the DID 49 + attr_reader :services 50 + 51 + 52 + # Creates a PLCOperation object. 53 + # 54 + # @param json [Hash] operation JSON 55 + # @raise [FormatError] when required fields are missing or invalid 56 57 def initialize(json) 58 @json = json
+8
lib/didkit/requests.rb
··· 5 require_relative 'errors' 6 7 module DIDKit 8 module Requests 9 def get_response(url, options = {}) 10 url = URI(url) unless url.is_a?(URI) 11
··· 5 require_relative 'errors' 6 7 module DIDKit 8 + 9 + # 10 + # @private 11 + # 12 + 13 module Requests 14 + 15 + private 16 + 17 def get_response(url, options = {}) 18 url = URI(url) unless url.is_a?(URI) 19
+82 -16
lib/didkit/resolver.rb
··· 6 require_relative 'requests' 7 8 module DIDKit 9 class Resolver 10 RESERVED_DOMAINS = %w(alt arpa example internal invalid local localhost onion test) 11 12 include Requests 13 14 attr_accessor :nameserver 15 16 def initialize(options = {}) 17 @nameserver = options[:nameserver] 18 @request_options = options.slice(:timeout, :max_redirects) 19 end 20 21 def resolve_handle(handle) 22 if handle.is_a?(DID) || handle =~ DID::GENERIC_REGEXP 23 return DID.new(handle) ··· 36 end 37 end 38 39 def resolve_handle_by_dns(domain) 40 dns_records = Resolv::DNS.open(resolv_options) do |d| 41 d.getresources("_atproto.#{domain}", Resolv::DNS::Resource::IN::TXT) ··· 50 nil 51 end 52 53 def resolve_handle_by_well_known(domain) 54 url = "https://#{domain}/.well-known/atproto-did" 55 response = get_response(url, @request_options) ··· 63 nil 64 end 65 66 def resolv_options 67 options = Resolv::DNS::Config.default_config_hash.dup 68 options[:nameserver] = nameserver if nameserver ··· 78 text.lines.length == 1 && text =~ DID::GENERIC_REGEXP ? text : nil 79 end 80 81 - def resolve_did(did) 82 - did = DID.new(did) if did.is_a?(String) 83 - 84 - did.type == :plc ? resolve_did_plc(did) : resolve_did_web(did) 85 - end 86 - 87 def resolve_did_plc(did) 88 json = get_json("https://plc.directory/#{did}", content_type: /^application\/did\+ld\+json(;.+)?$/) 89 Document.new(did, json) ··· 92 def resolve_did_web(did) 93 json = get_json("https://#{did.web_domain}/.well-known/did.json") 94 Document.new(did, json) 95 - end 96 - 97 - def get_verified_handle(subject) 98 - document = subject.is_a?(Document) ? subject : resolve_did(subject) 99 - 100 - first_verified_handle(document.did, document.handles) 101 - end 102 - 103 - def first_verified_handle(did, handles) 104 - handles.detect { |h| resolve_handle(h) == did.to_s } 105 end 106 end 107 end
··· 6 require_relative 'requests' 7 8 module DIDKit 9 + 10 + # 11 + # A class which manages resolving of handles to DIDs and DIDs to DID documents. 12 + # 13 + 14 class Resolver 15 + # These TLDs are not allowed in ATProto handles, so the resolver returns nil for them 16 + # without trying to look them up. 17 RESERVED_DOMAINS = %w(alt arpa example internal invalid local localhost onion test) 18 19 include Requests 20 21 + # @return [String, Array<String>] custom DNS nameserver(s) to use for DNS TXT lookups 22 attr_accessor :nameserver 23 + 24 + # @param options [Hash] resolver options 25 + # @option options [String, Array<String>] :nameserver custom DNS nameserver(s) to use (IP or an array of IPs) 26 + # @option options [Integer] :timeout request timeout in seconds (default: 15) 27 + # @option options [Integer] :max_redirects maximum number of redirects to follow (default: 5) 28 29 def initialize(options = {}) 30 @nameserver = options[:nameserver] 31 @request_options = options.slice(:timeout, :max_redirects) 32 end 33 34 + # Resolve a handle into a DID. Looks up the given ATProto domain handle using the DNS TXT method 35 + # and the HTTP .well-known method and returns a DID if one is assigned using either of the methods. 36 + # 37 + # If a DID string or a {DID} object is passed, it simply returns that DID, so you can use this 38 + # method to pass it an input string from the user which can be a DID or handle, without having to 39 + # check which one it is. 40 + # 41 + # @param handle [String, DID] a domain handle (may start with an `@`) or a DID string 42 + # @return [DID, nil] resolved DID if found, nil otherwise 43 + 44 def resolve_handle(handle) 45 if handle.is_a?(DID) || handle =~ DID::GENERIC_REGEXP 46 return DID.new(handle) ··· 59 end 60 end 61 62 + # Tries to resolve a handle into DID using the DNS TXT method. 63 + # 64 + # Checks the DNS records for a given domain for an entry `_atproto.#{domain}` whose value is 65 + # a correct DID string. 66 + # 67 + # @param domain [String] a domain handle to look up 68 + # @return [String, nil] resolved DID if found, nil otherwise 69 + 70 def resolve_handle_by_dns(domain) 71 dns_records = Resolv::DNS.open(resolv_options) do |d| 72 d.getresources("_atproto.#{domain}", Resolv::DNS::Resource::IN::TXT) ··· 81 nil 82 end 83 84 + # Tries to resolve a handle into DID using the HTTP .well-known method. 85 + # 86 + # Checks the `/.well-known/atproto-did` endpoint on the given domain to see if it returns 87 + # a text file that contains a correct DID string. 88 + # 89 + # @param domain [String] a domain handle to look up 90 + # @return [String, nil] resolved DID if found, nil otherwise 91 + 92 def resolve_handle_by_well_known(domain) 93 url = "https://#{domain}/.well-known/atproto-did" 94 response = get_response(url, @request_options) ··· 102 nil 103 end 104 105 + # Resolve a DID to a DID document. 106 + # 107 + # Looks up the DID document with the DID's identity details from an appropriate source, i.e. either 108 + # [plc.directory](https://plc.directory) for did:plc DIDs, or the did:web's domain for did:web DIDs. 109 + # 110 + # @param did [String, DID] DID string or object 111 + # @return [Document] resolved DID document 112 + # @raise [APIError] if an incorrect response is returned 113 + 114 + def resolve_did(did) 115 + did = DID.new(did) if did.is_a?(String) 116 + 117 + did.type == :plc ? resolve_did_plc(did) : resolve_did_web(did) 118 + end 119 + 120 + # Returns the first verified handle assigned to the given DID. 121 + # 122 + # Looks up the domain handles assigned to the DID in the DID document, checks if they are 123 + # verified (i.e. assigned correctly to this DID using DNS TXT or .well-known) and returns 124 + # the first handle that validates correctly, or nil if none matches. 125 + # 126 + # @param subject [String, DID, Document] a DID or its DID document 127 + # @return [String, nil] verified handle domain, if found 128 + 129 + def get_verified_handle(subject) 130 + document = subject.is_a?(Document) ? subject : resolve_did(subject) 131 + 132 + first_verified_handle(document.did, document.handles) 133 + end 134 + 135 + # Returns the first handle from the list that resolves back to the given DID. 136 + # 137 + # @param did [DID, String] DID to verify the handles against 138 + # @param handles [Array<String>] handles to check 139 + # @return [String, nil] a verified handle, if found 140 + 141 + def first_verified_handle(did, handles) 142 + handles.detect { |h| resolve_handle(h) == did.to_s } 143 + end 144 + 145 + 146 + private 147 + 148 def resolv_options 149 options = Resolv::DNS::Config.default_config_hash.dup 150 options[:nameserver] = nameserver if nameserver ··· 160 text.lines.length == 1 && text =~ DID::GENERIC_REGEXP ? text : nil 161 end 162 163 def resolve_did_plc(did) 164 json = get_json("https://plc.directory/#{did}", content_type: /^application\/did\+ld\+json(;.+)?$/) 165 Document.new(did, json) ··· 168 def resolve_did_web(did) 169 json = get_json("https://#{did.web_domain}/.well-known/did.json") 170 Document.new(did, json) 171 end 172 end 173 end
+21 -3
lib/didkit/service_record.rb
··· 2 require_relative 'errors' 3 4 module DIDKit 5 class ServiceRecord 6 - class FormatError < StandardError 7 - end 8 9 - attr_reader :key, :type, :endpoint 10 11 def initialize(key, type, endpoint) 12 begin
··· 2 require_relative 'errors' 3 4 module DIDKit 5 + 6 + # A parsed service record from either a DID document's `service` field or a PLC directory 7 + # operation's `services` field. 8 + 9 class ServiceRecord 10 11 + # Returns the service's identifier (without `#`), like "atproto_pds". 12 + # @return [String] service's identifier 13 + attr_reader :key 14 + 15 + # Returns the service's type field, like "AtprotoPersonalDataServer". 16 + # @return [String] service's type 17 + attr_reader :type 18 + 19 + # @return [String] service's endpoint URL 20 + attr_reader :endpoint 21 + 22 + # Create a service record from DID document fields. 23 + # 24 + # @param key [String] service identifier (without `#`) 25 + # @param type [String] service type 26 + # @param endpoint [String] service endpoint URL 27 + # @raise [FormatError] when the endpoint is not a valid URI 28 29 def initialize(key, type, endpoint) 30 begin
+40
lib/didkit/services.rb
··· 1 require 'uri' 2 3 module DIDKit 4 module Services 5 def get_service(key, type) 6 @services&.detect { |s| s.key == key && s.type == type } 7 end 8 9 def pds_endpoint 10 @pds_endpoint ||= get_service('atproto_pds', 'AtprotoPersonalDataServer')&.endpoint 11 end 12 13 def labeler_endpoint 14 @labeler_endpoint ||= get_service('atproto_labeler', 'AtprotoLabeler')&.endpoint 15 end 16 17 def pds_host 18 pds_endpoint&.then { |x| URI(x).host } 19 end 20 21 def labeler_host 22 labeler_endpoint&.then { |x| URI(x).host }
··· 1 require 'uri' 2 3 module DIDKit 4 + 5 + # 6 + # @api private 7 + # 8 + 9 module Services 10 + 11 + # Finds a service entry matching the given key and type. 12 + # 13 + # @api public 14 + # @param key [String] service key in the DID document 15 + # @param type [String] service type identifier 16 + # @return [ServiceRecord, nil] matching service record, if found 17 + 18 def get_service(key, type) 19 @services&.detect { |s| s.key == key && s.type == type } 20 end 21 22 + # Returns the PDS service endpoint, if present. 23 + # 24 + # If the DID has an `#atproto_pds` service declared in its `service` section, 25 + # returns the URL in its `serviceEndpoint` field. In other words, this is the URL 26 + # of the PDS assigned to a given user, which stores the user's account and repo. 27 + # 28 + # @api public 29 + # @return [String, nil] PDS service endpoint URL 30 + 31 def pds_endpoint 32 @pds_endpoint ||= get_service('atproto_pds', 'AtprotoPersonalDataServer')&.endpoint 33 end 34 + 35 + # Returns the labeler service endpoint, if present. 36 + # 37 + # If the DID has an `#atproto_labeler` service declared in its `service` section, 38 + # returns the URL in its `serviceEndpoint` field. 39 + # 40 + # @api public 41 + # @return [String, nil] labeler service endpoint URL 42 43 def labeler_endpoint 44 @labeler_endpoint ||= get_service('atproto_labeler', 'AtprotoLabeler')&.endpoint 45 end 46 47 + # Returns the hostname of the PDS service, if present. 48 + # 49 + # @api public 50 + # @return [String, nil] hostname of the PDS endpoint URL 51 + 52 def pds_host 53 pds_endpoint&.then { |x| URI(x).host } 54 end 55 + 56 + # Returns the hostname of the labeler service, if present. 57 + # 58 + # @api public 59 + # @return [String, nil] hostname of the labeler endpoint URL 60 61 def labeler_host 62 labeler_endpoint&.then { |x| URI(x).host }
+7 -7
spec/document_spec.rb
··· 41 it 'should raise a format error' do 42 expect { 43 subject.new(did, json) 44 - }.to raise_error(DIDKit::Document::FormatError) 45 end 46 end 47 ··· 51 it 'should raise a format error' do 52 expect { 53 subject.new(did, json) 54 - }.to raise_error(DIDKit::Document::FormatError) 55 end 56 end 57 ··· 61 it 'should raise a format error' do 62 expect { 63 subject.new(did, json) 64 - }.to raise_error(DIDKit::Document::FormatError) 65 end 66 end 67 ··· 71 it 'should raise an AtHandles format error' do 72 expect { 73 subject.new(did, json) 74 - }.to raise_error(DIDKit::AtHandles::FormatError) 75 end 76 end 77 ··· 81 it 'should raise an AtHandles format error' do 82 expect { 83 subject.new(did, json) 84 - }.to raise_error(DIDKit::AtHandles::FormatError) 85 end 86 end 87 ··· 106 it 'should raise a format error' do 107 expect { 108 subject.new(did, json) 109 - }.to raise_error(DIDKit::Document::FormatError) 110 end 111 end 112 ··· 116 it 'should raise a format error' do 117 expect { 118 subject.new(did, json) 119 - }.to raise_error(DIDKit::Document::FormatError) 120 end 121 end 122
··· 41 it 'should raise a format error' do 42 expect { 43 subject.new(did, json) 44 + }.to raise_error(DIDKit::FormatError) 45 end 46 end 47 ··· 51 it 'should raise a format error' do 52 expect { 53 subject.new(did, json) 54 + }.to raise_error(DIDKit::FormatError) 55 end 56 end 57 ··· 61 it 'should raise a format error' do 62 expect { 63 subject.new(did, json) 64 + }.to raise_error(DIDKit::FormatError) 65 end 66 end 67 ··· 71 it 'should raise an AtHandles format error' do 72 expect { 73 subject.new(did, json) 74 + }.to raise_error(DIDKit::FormatError) 75 end 76 end 77 ··· 81 it 'should raise an AtHandles format error' do 82 expect { 83 subject.new(did, json) 84 + }.to raise_error(DIDKit::FormatError) 85 end 86 end 87 ··· 106 it 'should raise a format error' do 107 expect { 108 subject.new(did, json) 109 + }.to raise_error(DIDKit::FormatError) 110 end 111 end 112 ··· 116 it 'should raise a format error' do 117 expect { 118 subject.new(did, json) 119 + }.to raise_error(DIDKit::FormatError) 120 end 121 end 122
+16 -16
spec/plc_operation_spec.rb
··· 27 let(:json) { [base_json] } 28 29 it 'should raise a format error' do 30 - expect { subject.new(json) }.to raise_error(DIDKit::PLCOperation::FormatError) 31 end 32 end 33 ··· 35 let(:json) { base_json.tap { |h| h.delete('did') }} 36 37 it 'should raise a format error' do 38 - expect { subject.new(json) }.to raise_error(DIDKit::PLCOperation::FormatError) 39 end 40 end 41 ··· 43 let(:json) { base_json.merge('did' => 123) } 44 45 it 'should raise a format error' do 46 - expect { subject.new(json) }.to raise_error(DIDKit::PLCOperation::FormatError) 47 end 48 end 49 ··· 51 let(:json) { base_json.merge('did' => 'foobar') } 52 53 it 'should raise a format error' do 54 - expect { subject.new(json) }.to raise_error(DIDKit::PLCOperation::FormatError) 55 end 56 end 57 ··· 59 let(:json) { base_json.tap { |h| h.delete('cid') }} 60 61 it 'should raise a format error' do 62 - expect { subject.new(json) }.to raise_error(DIDKit::PLCOperation::FormatError) 63 end 64 end 65 ··· 67 let(:json) { base_json.merge('cid' => 700) } 68 69 it 'should raise a format error' do 70 - expect { subject.new(json) }.to raise_error(DIDKit::PLCOperation::FormatError) 71 end 72 end 73 ··· 75 let(:json) { base_json.tap { |h| h.delete('createdAt') }} 76 77 it 'should raise a format error' do 78 - expect { subject.new(json) }.to raise_error(DIDKit::PLCOperation::FormatError) 79 end 80 end 81 ··· 83 let(:json) { base_json.merge('createdAt' => 123) } 84 85 it 'should raise a format error' do 86 - expect { subject.new(json) }.to raise_error(DIDKit::PLCOperation::FormatError) 87 end 88 end 89 ··· 91 let(:json) { base_json.tap { |h| h.delete('operation') }} 92 93 it 'should raise a format error' do 94 - expect { subject.new(json) }.to raise_error(DIDKit::PLCOperation::FormatError) 95 end 96 end 97 ··· 99 let(:json) { base_json.merge('operation' => 'invalid') } 100 101 it 'should raise a format error' do 102 - expect { subject.new(json) }.to raise_error(DIDKit::PLCOperation::FormatError) 103 end 104 end 105 ··· 107 let(:json) { base_json.tap { |h| h['operation'].delete('type') }} 108 109 it 'should raise a format error' do 110 - expect { subject.new(json) }.to raise_error(DIDKit::PLCOperation::FormatError) 111 end 112 end 113 ··· 152 it 'should raise an AtHandles format error' do 153 expect { 154 subject.new(json) 155 - }.to raise_error(DIDKit::AtHandles::FormatError) 156 end 157 end 158 ··· 162 it 'should raise an AtHandles format error' do 163 expect { 164 subject.new(json) 165 - }.to raise_error(DIDKit::AtHandles::FormatError) 166 end 167 end 168 ··· 187 let(:json) { base_json.tap { |h| h['operation'].delete('services') }} 188 189 it 'should raise a format error' do 190 - expect { subject.new(json) }.to raise_error(DIDKit::PLCOperation::FormatError) 191 end 192 end 193 ··· 205 } 206 207 it 'should raise a format error' do 208 - expect { subject.new(json) }.to raise_error(DIDKit::PLCOperation::FormatError) 209 end 210 end 211 ··· 225 } 226 227 it 'should raise a format error' do 228 - expect { subject.new(json) }.to raise_error(DIDKit::PLCOperation::FormatError) 229 end 230 end 231
··· 27 let(:json) { [base_json] } 28 29 it 'should raise a format error' do 30 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 31 end 32 end 33 ··· 35 let(:json) { base_json.tap { |h| h.delete('did') }} 36 37 it 'should raise a format error' do 38 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 39 end 40 end 41 ··· 43 let(:json) { base_json.merge('did' => 123) } 44 45 it 'should raise a format error' do 46 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 47 end 48 end 49 ··· 51 let(:json) { base_json.merge('did' => 'foobar') } 52 53 it 'should raise a format error' do 54 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 55 end 56 end 57 ··· 59 let(:json) { base_json.tap { |h| h.delete('cid') }} 60 61 it 'should raise a format error' do 62 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 63 end 64 end 65 ··· 67 let(:json) { base_json.merge('cid' => 700) } 68 69 it 'should raise a format error' do 70 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 71 end 72 end 73 ··· 75 let(:json) { base_json.tap { |h| h.delete('createdAt') }} 76 77 it 'should raise a format error' do 78 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 79 end 80 end 81 ··· 83 let(:json) { base_json.merge('createdAt' => 123) } 84 85 it 'should raise a format error' do 86 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 87 end 88 end 89 ··· 91 let(:json) { base_json.tap { |h| h.delete('operation') }} 92 93 it 'should raise a format error' do 94 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 95 end 96 end 97 ··· 99 let(:json) { base_json.merge('operation' => 'invalid') } 100 101 it 'should raise a format error' do 102 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 103 end 104 end 105 ··· 107 let(:json) { base_json.tap { |h| h['operation'].delete('type') }} 108 109 it 'should raise a format error' do 110 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 111 end 112 end 113 ··· 152 it 'should raise an AtHandles format error' do 153 expect { 154 subject.new(json) 155 + }.to raise_error(DIDKit::FormatError) 156 end 157 end 158 ··· 162 it 'should raise an AtHandles format error' do 163 expect { 164 subject.new(json) 165 + }.to raise_error(DIDKit::FormatError) 166 end 167 end 168 ··· 187 let(:json) { base_json.tap { |h| h['operation'].delete('services') }} 188 189 it 'should raise a format error' do 190 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 191 end 192 end 193 ··· 205 } 206 207 it 'should raise a format error' do 208 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 209 end 210 end 211 ··· 225 } 226 227 it 'should raise a format error' do 228 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 229 end 230 end 231