didkit 0.3.3 → 0.5.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 +46 -2
- data/README.md +35 -5
- data/SECURITY.md +16 -0
- data/lib/didkit/at_handles.rb +34 -2
- data/lib/didkit/constants.rb +31 -0
- data/lib/didkit/did.rb +86 -30
- data/lib/didkit/document.rb +70 -22
- data/lib/didkit/errors.rb +6 -0
- data/lib/didkit/global_did.rb +5 -0
- data/lib/didkit/plc_importer.rb +18 -32
- data/lib/didkit/plc_operation.rb +85 -20
- data/lib/didkit/public_key.rb +116 -0
- data/lib/didkit/requests.rb +8 -11
- data/lib/didkit/resolver.rb +111 -43
- data/lib/didkit/services.rb +34 -4
- data/lib/didkit/version.rb +1 -1
- data/lib/didkit.rb +2 -2
- metadata +22 -4
- data/sig/didkit.rbs +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf656ddc5c0b21d616657dc8f7d310c1f5aba7b77828542a1871c05d177ecbc0
|
|
4
|
+
data.tar.gz: b23f42ec6c434fa1ba7867976310fac7cfa006c0d093ef105141c0cd5c8b8543
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c0227bd5bad67bba3086ac4cc3f766d8976cfd0f4d6a882039676b9792a9c1bcb8a9dd69015918b7c5100613d0689a3460fa0350a25c0ab63a1c54b7e3df7c22
|
|
7
|
+
data.tar.gz: '017428fc8d8cae144b57029f1547615788ab9fb097ab45403803e94e61954c72511cdf68412c053da40437ba6bdd0f3be86990146dcd142389167ebd4febd84e'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
+
## [0.5.0] - 2026-10-26
|
|
2
|
+
|
|
3
|
+
Breaking changes:
|
|
4
|
+
|
|
5
|
+
- the [ATProto DID spec](https://atproto.com/specs/did#did-documents) now says that if there are multiple handles in a document, only the first one should be checked bi-directionally and the rest should be skipped:
|
|
6
|
+
|
|
7
|
+
> The first syntactically valid handle found in the ordered list is treated as the claimed handle, even if it fails to resolve bi-directionally. Any other handle URIs should be ignored.
|
|
8
|
+
|
|
9
|
+
- the `get_verified_handle` algorithm has been updated to match this behavior
|
|
10
|
+
- `Resolver#first_verified_handle` method has been removed
|
|
11
|
+
- `DIDKit::PLCImporter::PLC_SERVICE` was moved to `DIDKit::DEFAULT_PLC_DIRECTORY`
|
|
12
|
+
- `DIDKit::DID::GENERIC_REGEXP` was moved to `DIDKit::GENERIC_DID_REGEXP`
|
|
13
|
+
- `DIDKit::Resolver#nameserver` has been changed to be read-only
|
|
14
|
+
- `DIDKit::Resolver#resolve_handle` and `DID.resolve_handle` now verify the resolve handle bi-directionally, checking also if the matched DID has that handle assigned too; pass `verify: false` to skip this (default is true). This call can now raise an error if the DID document cannot be fetched.
|
|
15
|
+
|
|
16
|
+
Added:
|
|
17
|
+
|
|
18
|
+
- added support for decoding public (signing) keys – `Document#signing_key`, `PLCOperation#signing_key` and `DIDKit::PublicKey` class
|
|
19
|
+
- `DIDKit::Resolver` now accepts a `:plc_directory` option that lets you use a PLC mirror server instead of `https://plc.directory`
|
|
20
|
+
- added a shared global `DIDKit.default_resolver`, used internally by `DID` & `Document` – you can reassign it to one configured with a custom DNS nameserver or PLC mirror
|
|
21
|
+
- added support for PLC servers returning `application/did+json` content type instead of `application/did+ld+json` (the spec allows it and the replica server implementation does that)
|
|
22
|
+
- added support for PLC servers returning services with IDs in the absolute format `did:...#id` instead of just `#id`
|
|
23
|
+
- added `DID#did_web_host`, which returns the did:web host URL together with `http(s)://` and a port if included (only for `localhost`), and raises a `DIDError` if the DID is not a did:web; `#web_domain` renamed to `#did_web_domain` and kept with the old behavior, `#web_domain` added as an alias
|
|
24
|
+
|
|
25
|
+
Other:
|
|
26
|
+
|
|
27
|
+
- more detailed validation of DID & handle strings and for service endpoint URLs
|
|
28
|
+
- fixed DNS-based handle verification if there are multiple text chunks in a TXT record or multiple TXT records
|
|
29
|
+
- make sure that handle verification is case-insensitive
|
|
30
|
+
- the global `DID` alias is not reassigned if something else already defines it before DIDKit is loaded
|
|
31
|
+
- fixed redirects to relative URLs, and disallowed redirects to non-HTTP URLs and from HTTPS to HTTP
|
|
32
|
+
- fixed `:max_redirects` & `:timeout` options not being used when loading a DID document
|
|
33
|
+
|
|
34
|
+
## [0.4.0] - 2026-07-02
|
|
35
|
+
|
|
36
|
+
- 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
|
|
37
|
+
- added `PLCOperation#nullified?`
|
|
38
|
+
- added support for parsing `handle` and `service` fields from legacy (2022-23 era) `:create` PLC ops in `PLCOperation`
|
|
39
|
+
- `PLCImporter` can be configured with a custom hostname of a PLC mirror to use instead of [plc.directory](https://plc.directory)
|
|
40
|
+
- `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
|
|
41
|
+
- 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)
|
|
42
|
+
- tweaked parsing of services in `Document` and `PLCOperation`:
|
|
43
|
+
1. `Document` raises format errors if the fields are missing or not of expected type (since this shouldn't happen)
|
|
44
|
+
2. `Document` & `PLCOperation` silently ignore the service if its endpoint is not a valid URI (since this does happen)
|
|
45
|
+
3. Removed `ignore_errors` / `error_handler` from `PLCImporter`, since because of 2) no expected errors are emitted outside `PLCOperation`
|
|
46
|
+
|
|
1
47
|
## [0.3.3] - 2026-02-22
|
|
2
48
|
|
|
3
49
|
- fixed old `FormatError` classes mentioned in `PLCImporter`
|
|
@@ -38,8 +84,6 @@ Also:
|
|
|
38
84
|
- `PLCImporter` now removes duplicate operations at the edge of pages returned from the `/export` API
|
|
39
85
|
- rewritten some networking code – all classes now use `Net::HTTP` with consistent options instead of `open-uri`
|
|
40
86
|
|
|
41
|
-
Note: `PLCImporter` will be rewritten soon to add support for updated [plc.directory](https://plc.directory) APIs, so be prepared for some breaking changes there in v. 0.4.
|
|
42
|
-
|
|
43
87
|
## [0.2.3] - 2024-07-02
|
|
44
88
|
|
|
45
89
|
- added a `DID#get_audit_log` method that fetches the PLC audit log for a DID
|
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]
|
|
@@ -13,7 +15,7 @@ Accounts on Bluesky use identifiers like [did:plc:oio4hkxaop4ao4wz2pp3f4cr](http
|
|
|
13
15
|
|
|
14
16
|
## Installation
|
|
15
17
|
|
|
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.
|
|
18
|
+
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.3+. 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
19
|
|
|
18
20
|
To install the gem, run in the command line:
|
|
19
21
|
|
|
@@ -21,7 +23,7 @@ To install the gem, run in the command line:
|
|
|
21
23
|
|
|
22
24
|
Or add this to your app's `Gemfile`:
|
|
23
25
|
|
|
24
|
-
gem 'didkit', '~> 0.
|
|
26
|
+
gem 'didkit', '~> 0.5'
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
## Usage
|
|
@@ -40,6 +42,8 @@ This returns a `DID` object, which tells you:
|
|
|
40
42
|
- the DID type (`#type`, `:plc` or `:web`)
|
|
41
43
|
- if the handle was resolved via a DNS entry or a `.well-known` file (`#resolved_by`, `:dns` or `:http`)
|
|
42
44
|
|
|
45
|
+
The handle is automatically verified bi-directionally, i.e. the resolver loads the DID document for e.g. `did:plc:oky5czdrnfjpqslsw2a5iclo` and checks if the handle `jay.bsky.team` is listed there under `alsoKnownAs`. If you want to skip that second check, pass `verify: false` to `#resolve_handle`.
|
|
46
|
+
|
|
43
47
|
To go in the other direction – to find an assigned and verified handle given a DID – create a `DID` from a DID string and call `get_verified_handle`:
|
|
44
48
|
|
|
45
49
|
```rb
|
|
@@ -86,9 +90,10 @@ You can customize some things about the DID/handle lookups by using the `DIDKit:
|
|
|
86
90
|
|
|
87
91
|
Currently available options include:
|
|
88
92
|
|
|
89
|
-
- `:nameserver`
|
|
90
|
-
- `:
|
|
91
|
-
- `:
|
|
93
|
+
- `:nameserver` – override the nameserver used for DNS lookups, e.g. to use Google's or CloudFlare's DNS
|
|
94
|
+
- `:plc_directory` – hostname or URL of a PLC directory mirror to use instead of `plc.directory`
|
|
95
|
+
- `:timeout` – change the connection/response timeout for HTTP requests (default: 15 s)
|
|
96
|
+
- `:max_redirects` – change allowed maximum number of redirects (default: 5)
|
|
92
97
|
|
|
93
98
|
Example:
|
|
94
99
|
|
|
@@ -106,6 +111,31 @@ resolver.get_verified_handle(did)
|
|
|
106
111
|
# => 'nytimes.com'
|
|
107
112
|
```
|
|
108
113
|
|
|
114
|
+
`DID` and `Document` use a shared global resolver instance at `DIDKit.default_resolver`, which you can overwrite with one using a custom config:
|
|
115
|
+
|
|
116
|
+
```rb
|
|
117
|
+
DIDKit.default_resolver = DIDKit::Resolver.new(plc_directory: 'plc.eurosky.network')
|
|
118
|
+
|
|
119
|
+
did = DID.resolve_handle('mackuba.eu')
|
|
120
|
+
puts did.document.pds_host
|
|
121
|
+
# uses plc.eurosky.network to look up DID info
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
More info in the [YARD API docs](https://rubydoc.info/gems/didkit).
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
### Global DID alias
|
|
128
|
+
|
|
129
|
+
As mentioned earlier, DIDKit defines a global alias `DID` as a shorthand for `DIDKit::DID`. If this happens to conflict with another constant defined by your app or a different dependency, make sure to `require 'didkit'` *after* that constant is already defined, or alternatively, require e.g. `'didkit/did'` directly, which skips the `global_did.rb` file which defines the global `DID`.
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
## Other resources
|
|
133
|
+
|
|
134
|
+
- [YARD API documentation](https://rubydoc.info/gems/didkit) at rubydoc.info
|
|
135
|
+
- [ruby.sdk.blue](https://ruby.sdk.blue)
|
|
136
|
+
- [Example scripts](https://ruby.sdk.blue/examples/)
|
|
137
|
+
|
|
138
|
+
|
|
109
139
|
## Credits
|
|
110
140
|
|
|
111
141
|
Copyright © 2026 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr)).
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## How To Report a Vulnerability
|
|
4
|
+
|
|
5
|
+
If you find a potential vulnerability in one of my projects, it's recommended not to report it through a standard publicly-readable issue report, in order to avoid giving possible clues to any potential attackers who could abuse it before it's fixed (see: ["Just a rumour of a bug is enough to find a security exploit these days"](https://anil.recoil.org/notes/rumour-is-the-exploit)).
|
|
6
|
+
|
|
7
|
+
You can use one of these to contact me instead:
|
|
8
|
+
|
|
9
|
+
- [@mackuba.eu](https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr) on Bluesky (my DMs should be open, or mention me discretely with a request for contact)
|
|
10
|
+
- [@mackuba@martianbase.net](https://martianbase.net/@mackuba) on Mastodon (my personal instance)
|
|
11
|
+
- email: mackuba `@` protonmail.ch (optionally with [this PGP key](https://mail-api.proton.me/pks/lookup?op=get&search=mackuba@protonmail.ch))
|
|
12
|
+
- ["Report a vulnerability"](https://github.com/mackuba/didkit/security/advisories/new) form on GitHub
|
|
13
|
+
|
|
14
|
+
I do not check my Twitter/X or LinkedIn accounts regularly.
|
|
15
|
+
|
|
16
|
+
Note: please don't submit AI-reported issues that have not been verified manually :)
|
data/lib/didkit/at_handles.rb
CHANGED
|
@@ -1,22 +1,54 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'constants'
|
|
3
4
|
require_relative 'errors'
|
|
4
5
|
|
|
5
6
|
module DIDKit
|
|
6
7
|
|
|
7
8
|
#
|
|
8
|
-
# @private
|
|
9
|
+
# @api private
|
|
9
10
|
#
|
|
10
11
|
|
|
11
12
|
module AtHandles
|
|
12
13
|
|
|
14
|
+
# Returns a list of ATProto handles assigned to this DID in its DID document.
|
|
15
|
+
#
|
|
16
|
+
# Note: the handles aren't guaranteed to be verified (validated in the other direction).
|
|
17
|
+
# Use {DID#get_verified_handle} to get a handle that is correctly verified.
|
|
18
|
+
#
|
|
19
|
+
# According to the current [ATProto DID spec](https://atproto.com/specs/did#did-documents),
|
|
20
|
+
# if a DID has more than one handle assigned, only the first one on the list should be
|
|
21
|
+
# considered and the rest should be ignored.
|
|
22
|
+
#
|
|
23
|
+
# @api public
|
|
24
|
+
# @return [Array<String>]
|
|
25
|
+
|
|
26
|
+
attr_reader :handles
|
|
27
|
+
|
|
28
|
+
# Returns a list of all identifiers assigned to this DID in its DID document through the
|
|
29
|
+
# `alsoKnownAs` field. This includes ATProto handles (in the format `at://example.com`) and
|
|
30
|
+
# potentially other URIs like `http` URLs (e.g. for Bridgy accounts), and even (technically
|
|
31
|
+
# invalid) non-URI strings. Use {#handles} to get just the ATProto handles.
|
|
32
|
+
#
|
|
33
|
+
# @api public
|
|
34
|
+
# @return [Array<String>]
|
|
35
|
+
|
|
36
|
+
attr_reader :also_known_as
|
|
37
|
+
|
|
13
38
|
private
|
|
14
39
|
|
|
15
40
|
def parse_also_known_as(aka)
|
|
16
41
|
raise FormatError, "Invalid alsoKnownAs: #{aka.inspect}" unless aka.is_a?(Array)
|
|
17
42
|
raise FormatError, "Invalid alsoKnownAs: #{aka.inspect}" unless aka.all? { |x| x.is_a?(String) }
|
|
18
43
|
|
|
19
|
-
|
|
44
|
+
@also_known_as = aka
|
|
45
|
+
@handles = extract_handles_from_aka(aka)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def extract_handles_from_aka(aka)
|
|
49
|
+
handles = aka.select { |x| x.start_with?('at://') }.map { |x| x.delete_prefix('at://') }
|
|
50
|
+
|
|
51
|
+
handles.select { |h| h =~ DIDKit::HANDLE_REGEXP && h.length <= DIDKit::MAX_HANDLE_LENGTH }
|
|
20
52
|
end
|
|
21
53
|
end
|
|
22
54
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DIDKit
|
|
4
|
+
# URL of the standard PLC directory service.
|
|
5
|
+
DEFAULT_PLC_DIRECTORY = 'https://plc.directory'
|
|
6
|
+
|
|
7
|
+
DOMAIN_SEGMENT = /[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?/
|
|
8
|
+
LAST_DOMAIN_SEGMENT = /[a-zA-Z]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?/
|
|
9
|
+
private_constant :DOMAIN_SEGMENT, :LAST_DOMAIN_SEGMENT
|
|
10
|
+
|
|
11
|
+
# Pattern that matches any valid ATProto handle (without @)
|
|
12
|
+
HANDLE_REGEXP = /\A(#{DOMAIN_SEGMENT}\.)+#{LAST_DOMAIN_SEGMENT}\z/
|
|
13
|
+
|
|
14
|
+
# Pattern that matches any valid did:plc DID
|
|
15
|
+
DID_PLC_REGEXP = /\Adid\:plc\:[abcdefghijklmnopqrstuvwxyz234567]{24}\z/
|
|
16
|
+
|
|
17
|
+
# Pattern that matches a percent-decoded did:web DID
|
|
18
|
+
DID_WEB_REGEXP = /\Adid\:web\:((#{DOMAIN_SEGMENT}\.)+#{LAST_DOMAIN_SEGMENT}|localhost(:[1-9][0-9]{0,4})?)\z/
|
|
19
|
+
|
|
20
|
+
# Pattern that matches any valid DID (including methods unsupported in ATProto)
|
|
21
|
+
GENERIC_DID_REGEXP = /\Adid:[a-z]+:[a-zA-Z0-9_\.\:\%\-]*[a-zA-Z0-9_\.\-]\z/
|
|
22
|
+
|
|
23
|
+
# Maximum allowed DID length in ATProto
|
|
24
|
+
MAX_DID_LENGTH = 2048
|
|
25
|
+
|
|
26
|
+
# Maximum allowed handle length in ATProto
|
|
27
|
+
MAX_HANDLE_LENGTH = 253
|
|
28
|
+
|
|
29
|
+
# TLDs that are not allowed in ATProto handles
|
|
30
|
+
RESERVED_DOMAINS = %w(alt arpa example internal invalid local localhost onion test)
|
|
31
|
+
end
|
data/lib/didkit/did.rb
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'uri'
|
|
5
5
|
|
|
6
|
+
require_relative 'constants'
|
|
6
7
|
require_relative 'errors'
|
|
8
|
+
require_relative 'plc_importer'
|
|
7
9
|
require_relative 'requests'
|
|
8
10
|
require_relative 'resolver'
|
|
9
11
|
|
|
@@ -19,8 +21,6 @@ module DIDKit
|
|
|
19
21
|
#
|
|
20
22
|
|
|
21
23
|
class DID
|
|
22
|
-
GENERIC_REGEXP = /\Adid\:\w+\:.+\z/
|
|
23
|
-
|
|
24
24
|
include Requests
|
|
25
25
|
|
|
26
26
|
# Resolve a handle into a DID. Looks up the given ATProto domain handle using the DNS TXT method
|
|
@@ -31,10 +31,13 @@ module DIDKit
|
|
|
31
31
|
# check which one it is.
|
|
32
32
|
#
|
|
33
33
|
# @param handle [String, DID] a domain handle (may start with an `@`) or a DID string
|
|
34
|
+
# @param verify [Boolean] require the handle to be assigned to the DID in the DID document (default: true)
|
|
34
35
|
# @return [DID, nil] resolved DID if found, nil otherwise
|
|
36
|
+
# @raise [APIError] if verification is enabled and the DID document request is unsuccessful
|
|
37
|
+
# @raise [FormatError] if verification is enabled and the DID document has invalid data
|
|
35
38
|
|
|
36
|
-
def self.resolve_handle(handle)
|
|
37
|
-
|
|
39
|
+
def self.resolve_handle(handle, verify: true)
|
|
40
|
+
DIDKit.default_resolver.resolve_handle(handle, verify: verify)
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
# @return [Symbol] DID type (`:plc` or `:web`)
|
|
@@ -60,14 +63,18 @@ module DIDKit
|
|
|
60
63
|
did = did.to_s
|
|
61
64
|
end
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
raise ArgumentError.new("did should be a String or a DID object") unless did.is_a?(String)
|
|
67
|
+
raise DIDError.new("Invalid DID format") unless did =~ DIDKit::GENERIC_DID_REGEXP
|
|
68
|
+
raise DIDError.new("DID string too long") if did.length > DIDKit::MAX_DID_LENGTH
|
|
69
|
+
|
|
70
|
+
@did = did
|
|
71
|
+
@type = did.split(':')[1].to_sym
|
|
69
72
|
|
|
70
|
-
if @type
|
|
73
|
+
if @type == :plc
|
|
74
|
+
validate_did_plc(did)
|
|
75
|
+
elsif @type == :web
|
|
76
|
+
validate_did_web(did)
|
|
77
|
+
else
|
|
71
78
|
raise DIDError.new("Unrecognized DID type: #{@type}")
|
|
72
79
|
end
|
|
73
80
|
|
|
@@ -87,19 +94,19 @@ module DIDKit
|
|
|
87
94
|
# @return [Document] resolved DID document
|
|
88
95
|
|
|
89
96
|
def get_document
|
|
90
|
-
|
|
97
|
+
DIDKit.default_resolver.resolve_did(self)
|
|
91
98
|
end
|
|
92
99
|
|
|
93
|
-
# Returns the
|
|
100
|
+
# Returns the current primary handle assigned to the DID, if it verifies correctly.
|
|
94
101
|
#
|
|
95
|
-
# Looks up the
|
|
96
|
-
#
|
|
97
|
-
#
|
|
102
|
+
# Looks up the first syntactically valid handle in the DID document and checks if it is assigned
|
|
103
|
+
# correctly to this DID using DNS TXT or .well-known. Any later handles are ignored, even if the
|
|
104
|
+
# first handle fails verification. Returns nil if the primary handle cannot be verified.
|
|
98
105
|
#
|
|
99
106
|
# @return [String, nil] verified handle domain, if found
|
|
100
107
|
|
|
101
108
|
def get_verified_handle
|
|
102
|
-
|
|
109
|
+
DIDKit.default_resolver.get_verified_handle(document)
|
|
103
110
|
end
|
|
104
111
|
|
|
105
112
|
# Fetches the PLC audit log (list of all previous operations) for a did:plc DID.
|
|
@@ -115,12 +122,31 @@ module DIDKit
|
|
|
115
122
|
end
|
|
116
123
|
end
|
|
117
124
|
|
|
118
|
-
# Returns the domain portion of a did:web identifier.
|
|
125
|
+
# Returns the domain portion of a did:web identifier. If the DID uses `localhost` with port as the hostname,
|
|
126
|
+
# the port part is skipped.
|
|
119
127
|
#
|
|
120
128
|
# @return [String, nil] DID domain if the DID is a did:web, nil for did:plc
|
|
121
129
|
|
|
122
|
-
def
|
|
123
|
-
|
|
130
|
+
def did_web_domain
|
|
131
|
+
return nil unless type == :web
|
|
132
|
+
|
|
133
|
+
@did_web_host.gsub(/:.*/, '')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
alias web_domain did_web_domain
|
|
137
|
+
|
|
138
|
+
# Returns the URL of the host that hosts a did:web identity, including the http/https scheme
|
|
139
|
+
# and the port if included.
|
|
140
|
+
#
|
|
141
|
+
# @return [String] root URL of the host referenced by the did:web domain
|
|
142
|
+
# @raise [DIDError] when the DID is not a did:web
|
|
143
|
+
|
|
144
|
+
def did_web_host
|
|
145
|
+
raise DIDError, "DID is not a did:web" unless type == :web
|
|
146
|
+
|
|
147
|
+
protocol = (@did_web_host =~ /^localhost(\:.*|$)/) ? 'http' : 'https'
|
|
148
|
+
|
|
149
|
+
"#{protocol}://#{@did_web_host}"
|
|
124
150
|
end
|
|
125
151
|
|
|
126
152
|
# Checks the status of the account/repo on its own PDS using the `getRepoStatus` endpoint.
|
|
@@ -136,8 +162,7 @@ module DIDKit
|
|
|
136
162
|
doc = self.document
|
|
137
163
|
return nil if doc.pds_endpoint.nil?
|
|
138
164
|
|
|
139
|
-
|
|
140
|
-
url = URI("#{pds_host}/xrpc/com.atproto.sync.getRepoStatus")
|
|
165
|
+
url = URI.join(doc.pds_endpoint, "/xrpc/com.atproto.sync.getRepoStatus")
|
|
141
166
|
url.query = URI.encode_www_form(:did => @did)
|
|
142
167
|
|
|
143
168
|
response = get_response(url, request_options)
|
|
@@ -148,17 +173,19 @@ module DIDKit
|
|
|
148
173
|
json = JSON.parse(response.body)
|
|
149
174
|
|
|
150
175
|
if json['active'] == true
|
|
151
|
-
:active
|
|
176
|
+
return :active
|
|
152
177
|
elsif json['active'] == false && json['status'].is_a?(String) && json['status'].length <= 100
|
|
153
|
-
json['status'].to_sym
|
|
154
|
-
|
|
155
|
-
|
|
178
|
+
return json['status'].to_sym
|
|
179
|
+
end
|
|
180
|
+
elsif (status == 400 || status == 404) && is_json
|
|
181
|
+
json = JSON.parse(response.body)
|
|
182
|
+
|
|
183
|
+
if json['error'] == 'RepoNotFound'
|
|
184
|
+
return nil
|
|
156
185
|
end
|
|
157
|
-
elsif status == 400 && is_json && JSON.parse(response.body)['error'] == 'RepoNotFound'
|
|
158
|
-
nil
|
|
159
|
-
else
|
|
160
|
-
raise APIError.new(response)
|
|
161
186
|
end
|
|
187
|
+
|
|
188
|
+
raise APIError.new(response)
|
|
162
189
|
end
|
|
163
190
|
|
|
164
191
|
# Checks if the account is seen as active on its own PDS, using the `getRepoStatus` endpoint.
|
|
@@ -195,5 +222,34 @@ module DIDKit
|
|
|
195
222
|
false
|
|
196
223
|
end
|
|
197
224
|
end
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
def validate_did_plc(did)
|
|
230
|
+
raise DIDError.new("Invalid did:plc") unless did =~ DIDKit::DID_PLC_REGEXP
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def validate_did_web(did)
|
|
234
|
+
identifier_part = did.delete_prefix('did:web:')
|
|
235
|
+
|
|
236
|
+
# literal colons delimit paths in did:web - a port separator must be percent-encoded
|
|
237
|
+
raise DIDError.new("Invalid did:web") if identifier_part.include?(':')
|
|
238
|
+
|
|
239
|
+
decoded_host = URI::DEFAULT_PARSER.unescape(identifier_part).b
|
|
240
|
+
raise DIDError.new("Invalid did:web") unless "did:web:#{decoded_host}" =~ DIDKit::DID_WEB_REGEXP
|
|
241
|
+
|
|
242
|
+
if decoded_host.include?(':')
|
|
243
|
+
hostname, port = decoded_host.split(':')
|
|
244
|
+
raise DIDError.new("Invalid did:web server port") unless port.to_i.between?(1, 65535)
|
|
245
|
+
else
|
|
246
|
+
hostname = decoded_host
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
tld = hostname.split('.').last.downcase
|
|
250
|
+
raise DIDError.new("Invalid did:web TLD: #{tld}") if RESERVED_DOMAINS.include?(tld) && hostname != 'localhost'
|
|
251
|
+
|
|
252
|
+
@did_web_host = decoded_host
|
|
253
|
+
end
|
|
198
254
|
end
|
|
199
255
|
end
|
data/lib/didkit/document.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'at_handles'
|
|
4
4
|
require_relative 'errors'
|
|
5
|
+
require_relative 'public_key'
|
|
5
6
|
require_relative 'resolver'
|
|
6
7
|
require_relative 'service_record'
|
|
7
8
|
require_relative 'services'
|
|
@@ -9,30 +10,29 @@ require_relative 'services'
|
|
|
9
10
|
module DIDKit
|
|
10
11
|
|
|
11
12
|
#
|
|
12
|
-
# Parsed DID document from a JSON file loaded from [plc.directory](https://plc.directory)
|
|
13
|
+
# Parsed DID document from a JSON file loaded from the PLC directory ([plc.directory](https://plc.directory)
|
|
14
|
+
# or a mirror) or from a did:web domain.
|
|
13
15
|
#
|
|
14
16
|
# Use {DID#document} or {Resolver#resolve_did} to fetch a DID document and return this object.
|
|
15
17
|
#
|
|
18
|
+
# Related specifications:
|
|
19
|
+
# - [ATProto DID](https://atproto.com/specs/did)
|
|
20
|
+
#
|
|
16
21
|
|
|
17
22
|
class Document
|
|
18
23
|
include AtHandles
|
|
19
24
|
include Services
|
|
20
25
|
|
|
26
|
+
SERVICE_REGEXP = /\A(did:[^#]+)?#[^#]+\z/
|
|
27
|
+
private_constant :SERVICE_REGEXP
|
|
28
|
+
|
|
21
29
|
# @return [Hash] the complete JSON data of the DID document
|
|
22
30
|
attr_reader :json
|
|
23
31
|
|
|
24
32
|
# @return [DID] the DID that this document describes
|
|
25
33
|
attr_reader :did
|
|
26
34
|
|
|
27
|
-
#
|
|
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
|
-
# @return [Array<ServiceRecords>] service records like PDS details assigned to the DID
|
|
35
|
+
# @return [Array<ServiceRecord>] service records like PDS details assigned to the DID
|
|
36
36
|
attr_reader :services
|
|
37
37
|
|
|
38
38
|
# Creates a DID document object.
|
|
@@ -42,6 +42,9 @@ module DIDKit
|
|
|
42
42
|
# @raise [FormatError] when required fields are missing or invalid.
|
|
43
43
|
|
|
44
44
|
def initialize(did, json)
|
|
45
|
+
raise ArgumentError, "did should be a DID object" unless did.is_a?(DID)
|
|
46
|
+
raise ArgumentError, "json should be a Hash" unless json.is_a?(Hash)
|
|
47
|
+
|
|
45
48
|
raise FormatError, "Missing id field" if json['id'].nil?
|
|
46
49
|
raise FormatError, "Invalid id field" unless json['id'].is_a?(String)
|
|
47
50
|
raise FormatError, "id field doesn't match expected DID" unless json['id'] == did.to_s
|
|
@@ -49,20 +52,46 @@ module DIDKit
|
|
|
49
52
|
@did = did
|
|
50
53
|
@json = json
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
parse_services(json['service'] || [])
|
|
56
|
+
parse_also_known_as(json['alsoKnownAs'] || [])
|
|
54
57
|
end
|
|
55
58
|
|
|
56
|
-
# Returns the
|
|
59
|
+
# Returns the current primary handle assigned to the DID, if it verifies correctly.
|
|
57
60
|
#
|
|
58
|
-
# Looks up the
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
+
# Looks up the first syntactically valid handle in the DID document and checks if it is assigned
|
|
62
|
+
# correctly to this DID using DNS TXT or .well-known. Any later handles are ignored, even if the
|
|
63
|
+
# first handle fails verification. Returns nil if the primary handle cannot be verified.
|
|
61
64
|
#
|
|
62
65
|
# @return [String, nil] verified handle domain, if found
|
|
63
66
|
|
|
64
67
|
def get_verified_handle
|
|
65
|
-
|
|
68
|
+
DIDKit.default_resolver.get_verified_handle(self)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Returns the account's public signing key parsed from the DID document's `verificationMethod`.
|
|
72
|
+
#
|
|
73
|
+
# @return [PublicKey, nil] decoded signing key wrapped in {PublicKey}, or nil if no matching verification
|
|
74
|
+
# method is found
|
|
75
|
+
# @raise [FormatError] when `verificationMethod` or one of its entries has an invalid type
|
|
76
|
+
# @raise [ArgumentError] when selected key data is not a string
|
|
77
|
+
# @raise [PublicKeyError] when the key is invalid, incorrectly encoded, or has unsupported type
|
|
78
|
+
|
|
79
|
+
def signing_key
|
|
80
|
+
keys = @json['verificationMethod']
|
|
81
|
+
return nil if keys.nil?
|
|
82
|
+
|
|
83
|
+
raise FormatError, "verificationMethod is not an array: #{keys.inspect}" unless keys.is_a?(Array)
|
|
84
|
+
|
|
85
|
+
keys.each do |data|
|
|
86
|
+
raise FormatError, "verificationMethod item is invalid: #{data.inspect}" unless data.is_a?(Hash)
|
|
87
|
+
|
|
88
|
+
next unless data['id'] == "#{@did}#atproto" || data['id'] == "#atproto"
|
|
89
|
+
next unless data['controller'] == @did.to_s && data['type'] == 'Multikey' && !data['publicKeyMultibase'].nil?
|
|
90
|
+
|
|
91
|
+
return PublicKey.new(data['publicKeyMultibase'])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
nil
|
|
66
95
|
end
|
|
67
96
|
|
|
68
97
|
|
|
@@ -71,17 +100,36 @@ module DIDKit
|
|
|
71
100
|
def parse_services(service_data)
|
|
72
101
|
raise FormatError, "Invalid service data" unless service_data.is_a?(Array) && service_data.all? { |x| x.is_a?(Hash) }
|
|
73
102
|
|
|
74
|
-
services = []
|
|
103
|
+
@services = []
|
|
75
104
|
|
|
76
105
|
service_data.each do |x|
|
|
77
106
|
id, type, endpoint = x.values_at('id', 'type', 'serviceEndpoint')
|
|
78
107
|
|
|
79
|
-
|
|
80
|
-
|
|
108
|
+
raise FormatError, "Missing service id" unless id
|
|
109
|
+
raise FormatError, "Invalid service id: #{id.inspect}" unless id.is_a?(String)
|
|
110
|
+
next unless id =~ SERVICE_REGEXP
|
|
111
|
+
|
|
112
|
+
if id.start_with?('did:')
|
|
113
|
+
did, rest = id.split('#')
|
|
114
|
+
next unless did == @did.to_s
|
|
115
|
+
|
|
116
|
+
id = rest
|
|
117
|
+
else
|
|
118
|
+
id = id.gsub(/^#/, '')
|
|
81
119
|
end
|
|
82
|
-
end
|
|
83
120
|
|
|
84
|
-
|
|
121
|
+
raise FormatError, "Missing service type" unless type
|
|
122
|
+
raise FormatError, "Invalid service type: #{type.inspect}" unless type.is_a?(String)
|
|
123
|
+
|
|
124
|
+
raise FormatError, "Missing service endpoint" unless endpoint
|
|
125
|
+
raise FormatError, "Invalid service endpoint: #{endpoint.inspect}" unless endpoint.is_a?(String)
|
|
126
|
+
|
|
127
|
+
begin
|
|
128
|
+
@services << ServiceRecord.new(id, type, endpoint)
|
|
129
|
+
rescue FormatError => e
|
|
130
|
+
# ignore services with invalid URIs
|
|
131
|
+
end
|
|
132
|
+
end
|
|
85
133
|
end
|
|
86
134
|
end
|
|
87
135
|
end
|
data/lib/didkit/errors.rb
CHANGED