A library for handling DID identifiers used in Bluesky AT Protocol

unified parsing of services between Document/PLCOperation

+218 -52
+13 -1
lib/didkit/document.rb
··· 68 68 service_data.each do |x| 69 69 id, type, endpoint = x.values_at('id', 'type', 'serviceEndpoint') 70 70 71 - if id.is_a?(String) && id.start_with?('#') && type.is_a?(String) && endpoint.is_a?(String) 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 72 82 @services << ServiceRecord.new(id.gsub(/^#/, ''), type, endpoint) 83 + rescue FormatError => e 84 + # ignore services with invalid URIs 73 85 end 74 86 end 75 87 end
+21 -9
lib/didkit/plc_operation.rb
··· 77 77 @type = type.to_sym 78 78 return unless @type == :plc_operation 79 79 80 - services = operation['services'] 81 - raise FormatError, "Missing services key: #{json}" if services.nil? 82 - raise FormatError, "Invalid services data: #{services.inspect}" unless services.is_a?(Hash) 80 + raise FormatError, "Missing services key: #{json}" if operation['services'].nil? 81 + 82 + parse_services(operation['services']) 83 + parse_also_known_as(operation['alsoKnownAs']) 84 + end 85 + 86 + private 83 87 84 - @services = services.map { |k, x| 85 - type, endpoint = x.values_at('type', 'endpoint') 88 + def parse_services(service_data) 89 + raise FormatError, "Invalid services data: #{service_data.inspect}" unless service_data.is_a?(Hash) 90 + 91 + @services = [] 92 + 93 + service_data.each do |key, data| 94 + type, endpoint = data.values_at('type', 'endpoint') 86 95 87 96 raise FormatError, "Missing service type" unless type 88 97 raise FormatError, "Invalid service type: #{type.inspect}" unless type.is_a?(String) 98 + 89 99 raise FormatError, "Missing service endpoint" unless endpoint 90 100 raise FormatError, "Invalid service endpoint: #{endpoint.inspect}" unless endpoint.is_a?(String) 91 101 92 - ServiceRecord.new(k, type, endpoint) 93 - } 94 - 95 - parse_also_known_as(operation['alsoKnownAs']) 102 + begin 103 + @services << ServiceRecord.new(key, type, endpoint) 104 + rescue FormatError => e 105 + # ignore services with invalid URIs 106 + end 107 + end 96 108 end 97 109 end 98 110 end
+5
lib/didkit/service_record.rb
··· 26 26 # @param key [String] service identifier (without `#`) 27 27 # @param type [String] service type 28 28 # @param endpoint [String] service endpoint URL 29 + # @raise [ArgumentError] when the id starts with a `#` 29 30 # @raise [FormatError] when the endpoint is not a valid URI 30 31 31 32 def initialize(key, type, endpoint) ··· 33 34 uri = URI(endpoint) 34 35 rescue URI::Error 35 36 raise FormatError, "Invalid service endpoint: #{endpoint.inspect}" 37 + end 38 + 39 + if key.start_with?('#') 40 + raise ArgumentError, "Unexpected # in service id" 36 41 end 37 42 38 43 @key = key
+88 -26
spec/document_spec.rb
··· 39 39 let(:json) { base_json.dup.tap { |h| h.delete('id') }} 40 40 41 41 it 'should raise a format error' do 42 - expect { 43 - subject.new(did, json) 44 - }.to raise_error(DIDKit::FormatError) 42 + expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError) 45 43 end 46 44 end 47 45 ··· 49 47 let(:json) { base_json.merge('id' => 123) } 50 48 51 49 it 'should raise a format error' do 52 - expect { 53 - subject.new(did, json) 54 - }.to raise_error(DIDKit::FormatError) 50 + expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError) 55 51 end 56 52 end 57 53 ··· 59 55 let(:json) { base_json.merge('id' => 'did:plc:notmatching') } 60 56 61 57 it 'should raise a format error' do 62 - expect { 63 - subject.new(did, json) 64 - }.to raise_error(DIDKit::FormatError) 58 + expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError) 65 59 end 66 60 end 67 61 68 62 context 'when alsoKnownAs is not an array' do 69 63 let(:json) { base_json.merge('alsoKnownAs' => 'at://dholms.xyz') } 70 64 71 - it 'should raise an AtHandles format error' do 72 - expect { 73 - subject.new(did, json) 74 - }.to raise_error(DIDKit::FormatError) 65 + it 'should raise a format error' do 66 + expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError) 75 67 end 76 68 end 77 69 78 70 context 'when alsoKnownAs elements are not strings' do 79 71 let(:json) { base_json.merge('alsoKnownAs' => [666]) } 80 72 81 - it 'should raise an AtHandles format error' do 82 - expect { 83 - subject.new(did, json) 84 - }.to raise_error(DIDKit::FormatError) 73 + it 'should raise a format error' do 74 + expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError) 85 75 end 86 76 end 87 77 ··· 97 87 it 'should pick those starting with at:// and remove the prefixes' do 98 88 doc = subject.new(did, json) 99 89 doc.handles.should == ['dholms.xyz', 'other.handle'] 90 + end 91 + 92 + it 'should return all entries in #also_known_as' do 93 + op = subject.new(did, json) 94 + op.also_known_as.should == ['at://dholms.xyz', 'https://example.com', 'at://other.handle'] 100 95 end 101 96 end 102 97 ··· 104 99 let(:json) { base_json.merge('service' => 'not-an-array') } 105 100 106 101 it 'should raise a format error' do 107 - expect { 108 - subject.new(did, json) 109 - }.to raise_error(DIDKit::FormatError) 102 + expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError) 110 103 end 111 104 end 112 105 ··· 114 107 let(:json) { base_json.merge('service' => ['invalid']) } 115 108 116 109 it 'should raise a format error' do 117 - expect { 118 - subject.new(did, json) 119 - }.to raise_error(DIDKit::FormatError) 110 + expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError) 111 + end 112 + end 113 + 114 + context 'when service entry id is missing' do 115 + let(:json) { base_json.merge('service' => [service]) } 116 + let(:service) {{ 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' }} 117 + 118 + it 'should raise a format error' do 119 + expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError) 120 + end 121 + end 122 + 123 + context 'when service entry id is not a string' do 124 + let(:json) { base_json.merge('service' => [service]) } 125 + let(:service) {{ 'id' => 5, 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' }} 126 + 127 + it 'should raise a format error' do 128 + expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError) 129 + end 130 + end 131 + 132 + context 'when service entry id does not start with a #' do 133 + let(:json) { base_json.merge('service' => [service]) } 134 + let(:service) {{ 'id' => 'atproto_pds', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' }} 135 + 136 + it 'should *not* raise a format error' do 137 + expect { subject.new(did, json) }.to_not raise_error 138 + end 139 + end 140 + 141 + context 'when service entry type is missing' do 142 + let(:json) { base_json.merge('service' => [service]) } 143 + let(:service) {{ 'id' => '#atproto_pds', 'serviceEndpoint' => 'https://pds.dholms.xyz' }} 144 + 145 + it 'should raise a format error' do 146 + expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError) 147 + end 148 + end 149 + 150 + context 'when service entry type is not a string' do 151 + let(:json) { base_json.merge('service' => [service]) } 152 + let(:service) {{ 'id' => '#atproto_pds', 'type' => 7, 'serviceEndpoint' => 'https://pds.dholms.xyz' }} 153 + 154 + it 'should raise a format error' do 155 + expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError) 156 + end 157 + end 158 + 159 + context 'when service entry endpoint is missing' do 160 + let(:json) { base_json.merge('service' => [service]) } 161 + let(:service) {{ 'id' => '#atproto_pds', 'type' => 'AtprotoPersonalDataServer' }} 162 + 163 + it 'should raise a format error' do 164 + expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError) 165 + end 166 + end 167 + 168 + context 'when service entry endpoint is not a string' do 169 + let(:json) { base_json.merge('service' => [service]) } 170 + let(:service) {{ 'id' => '#atproto_pds', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => :somewhere }} 171 + 172 + it 'should raise a format error' do 173 + expect { subject.new(did, json) }.to raise_error(DIDKit::FormatError) 174 + end 175 + end 176 + 177 + context 'when service entry endpoint is not an URI' do 178 + let(:json) { base_json.merge('service' => [service]) } 179 + let(:service) {{ 'id' => '#atproto_pds', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => '<script>alert()</script>' }} 180 + 181 + it 'should *not* raise a format error' do 182 + expect { subject.new(did, json) }.to_not raise_error 120 183 end 121 184 end 122 185 ··· 124 187 let(:services) { 125 188 [ 126 189 { 'id' => '#atproto_pds', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' }, 127 - { 'id' => 'not_a_hash', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' }, 128 - { 'id' => '#wrong_type', 'type' => 123, 'serviceEndpoint' => 'https://pds.dholms.xyz' }, 129 - { 'id' => '#wrong_endpoint', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 123 }, 190 + { 'id' => 'missing_hash', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'https://pds.dholms.xyz' }, 191 + { 'id' => '#wrong_endpoint', 'type' => 'AtprotoPersonalDataServer', 'serviceEndpoint' => 'this is not a url' }, 130 192 { 'id' => '#lycan', 'type' => 'LycanService', 'serviceEndpoint' => 'https://lycan.feeds.blue' } 131 193 ] 132 194 }
+91 -16
spec/plc_operation_spec.rb
··· 157 157 context 'when alsoKnownAs is not an array' do 158 158 let(:json) { base_json.tap { |h| h['operation']['alsoKnownAs'] = 'at://dholms.xyz' }} 159 159 160 - it 'should raise an AtHandles format error' do 161 - expect { 162 - subject.new(json) 163 - }.to raise_error(DIDKit::FormatError) 160 + it 'should raise a format error' do 161 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 164 162 end 165 163 end 166 164 167 165 context 'when alsoKnownAs elements are not strings' do 168 166 let(:json) { base_json.tap { |h| h['operation']['alsoKnownAs'] = [666] }} 169 167 170 - it 'should raise an AtHandles format error' do 171 - expect { 172 - subject.new(json) 173 - }.to raise_error(DIDKit::FormatError) 168 + it 'should raise a format error' do 169 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 174 170 end 175 171 end 176 172 ··· 189 185 op = subject.new(json) 190 186 op.handles.should == ['dholms.xyz', 'other.handle'] 191 187 end 188 + 189 + it 'should return all entries in #also_known_as' do 190 + op = subject.new(json) 191 + op.also_known_as.should == ['at://dholms.xyz', 'https://example.com', 'at://other.handle'] 192 + end 192 193 end 193 194 194 195 context 'when services are missing' do ··· 217 218 end 218 219 end 219 220 220 - context 'when a service entry is missing fields' do 221 + context 'when a service entry type is missing' do 222 + let(:json) { 223 + base_json.tap { |h| 224 + h['operation']['services'] = { 225 + "atproto_pds" => { "endpoint" => "https://pds.dholms.xyz" } 226 + } 227 + } 228 + } 229 + 230 + it 'should raise a format error' do 231 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 232 + end 233 + end 234 + 235 + context 'when a service entry type is not a string' do 236 + let(:json) { 237 + base_json.tap { |h| 238 + h['operation']['services'] = { 239 + "atproto_pds" => { "type" => 77, "endpoint" => "https://pds.dholms.xyz" } 240 + } 241 + } 242 + } 243 + 244 + it 'should raise a format error' do 245 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 246 + end 247 + end 248 + 249 + context 'when a service entry endpoint is missing' do 250 + let(:json) { 251 + base_json.tap { |h| 252 + h['operation']['services'] = { 253 + "atproto_pds" => { "type" => "AtprotoPersonalDataServer" } 254 + } 255 + } 256 + } 257 + 258 + it 'should raise a format error' do 259 + expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 260 + end 261 + end 262 + 263 + context 'when a service entry endpoint is not a string' do 221 264 let(:json) { 222 265 base_json.tap { |h| 223 266 h['operation']['services'] = { 224 - "atproto_pds" => { 225 - "endpoint" => "https://pds.dholms.xyz" 226 - }, 227 - "atproto_labeler" => { 228 - "type" => "AtprotoLabeler", 229 - "endpoint" => "https://labeler.example.com" 230 - } 267 + "atproto_pds" => { "type" => "AtprotoPersonalDataServer", "endpoint" => { :host => 'localhost' }} 231 268 } 232 269 } 233 270 } 234 271 235 272 it 'should raise a format error' do 236 273 expect { subject.new(json) }.to raise_error(DIDKit::FormatError) 274 + end 275 + end 276 + 277 + context 'when a service entry endpoint is not an URI' do 278 + let(:json) { 279 + base_json.tap { |h| 280 + h['operation']['services'] = { 281 + "atproto_pds" => { "type" => "AtprotoPersonalDataServer", "endpoint" => "2 + 2" } 282 + } 283 + } 284 + } 285 + 286 + it 'should *not* raise a format error' do 287 + expect { subject.new(json) }.to_not raise_error 237 288 end 238 289 end 239 290 ··· 360 411 op.labeler_host.should be_nil 361 412 op.labeller_host.should be_nil 362 413 end 414 + end 415 + end 416 + 417 + context "when some services have endpoints that aren't valid URIs" do 418 + let(:json) { 419 + base_json.tap { |h| 420 + h['operation']['services'] = { 421 + "atproto_labeler" => { 422 + "type" => "AtprotoLabeler", 423 + "endpoint" => "bla bla bla" 424 + }, 425 + "custom_service" => { 426 + "type" => "OtherService", 427 + "endpoint" => "https://custom.example.com" 428 + } 429 + } 430 + } 431 + } 432 + 433 + it 'should only return valid services' do 434 + op = subject.new(json) 435 + 436 + op.services.length.should == 1 437 + op.services[0].type == 'OtherService' 363 438 end 364 439 end 365 440 end