saro-dat 4.0.0 → 4.3.1
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/.idea/saro-dat.iml +2 -1
- data/PUBLISH.md +1 -1
- data/README.md +2 -2
- data/lib/saro/dat/dat_certificate.rb +29 -25
- data/lib/saro/dat/dat_cms_manager.rb +187 -0
- data/lib/saro/dat/dat_manager.rb +6 -3
- data/lib/saro/dat/signature.rb +8 -0
- data/lib/saro-dat.rb +1 -0
- data/saro-dat.gemspec +3 -2
- metadata +18 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56415a92654a73befef882b2ca36674095155444492b8e8c8db99ebb4c3cd7cb
|
|
4
|
+
data.tar.gz: 670ad624aa1372b2d33f1bae79b877b462970ec20e4a40d1419dd12b3fceb27a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '087cad97ce30b7d51e1e0ebed1c30749fd33c59e5cb7ccad1d197e21502f0d5e341fb713f937cd8315b0a56e5d94e1170a061d1967ce8778ec2e926ac7535b19'
|
|
7
|
+
data.tar.gz: a1c460e69d89564b6628d4b264d9f523133c350f0769caae6dff8b401822be8de59223889d9baf8cf7ed535acf15f1698852d60cc2e40da04a26dec4b587d7a4
|
data/.idea/saro-dat.iml
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
<orderEntry type="library" scope="PROVIDED" name="base64 (v0.3.0, rbenv: 4.0.5) [gem]" level="application" />
|
|
15
15
|
<orderEntry type="library" scope="PROVIDED" name="benchmark (v0.5.0, rbenv: 4.0.5) [gem]" level="application" />
|
|
16
16
|
<orderEntry type="library" scope="PROVIDED" name="bundler (v4.0.12, rbenv: 4.0.5) [gem]" level="application" />
|
|
17
|
-
<orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.3.
|
|
17
|
+
<orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.3.7, rbenv: 4.0.5) [gem]" level="application" />
|
|
18
|
+
<orderEntry type="library" scope="PROVIDED" name="logger (v1.7.0, rbenv: 4.0.5) [gem]" level="application" />
|
|
18
19
|
<orderEntry type="library" scope="PROVIDED" name="minitest (v5.27.0, rbenv: 4.0.5) [gem]" level="application" />
|
|
19
20
|
<orderEntry type="library" scope="PROVIDED" name="openssl (v4.0.2, rbenv: 4.0.5) [gem]" level="application" />
|
|
20
21
|
<orderEntry type="library" scope="PROVIDED" name="parallel (v2.1.0, rbenv: 4.0.5) [gem]" level="application" />
|
data/PUBLISH.md
CHANGED
data/README.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
### [DAT Run Online](https://dat.saro.me)
|
|
6
6
|
|
|
7
|
-
### [What is DAT](https://dat.saro.me
|
|
7
|
+
### [What is DAT](https://dat.saro.me/intro)
|
|
8
8
|
|
|
9
|
-
### [Example](https://dat.saro.me
|
|
9
|
+
### [Example](https://dat.saro.me/libs/gems-saro-dat)
|
|
10
10
|
|
|
11
11
|
## Support algorithm
|
|
12
12
|
### Signature
|
|
@@ -7,33 +7,33 @@ require_relative 'util'
|
|
|
7
7
|
module Saro
|
|
8
8
|
module Dat
|
|
9
9
|
class DatCertificate
|
|
10
|
-
attr_reader :cid, :signature_key, :crypto_key, :
|
|
10
|
+
attr_reader :cid, :signature_key, :crypto_key, :dat_issuance_start_seconds, :dat_issuance_end_seconds, :dat_ttl_seconds
|
|
11
11
|
|
|
12
|
-
def initialize(cid,
|
|
12
|
+
def initialize(cid, dat_issuance_start_seconds, dat_issuance_duration_seconds, dat_ttl_seconds, signature_key, crypto_key)
|
|
13
13
|
@cid = cid
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
@
|
|
14
|
+
@dat_issuance_start_seconds = dat_issuance_start_seconds
|
|
15
|
+
@dat_issuance_end_seconds = dat_issuance_start_seconds + dat_issuance_duration_seconds
|
|
16
|
+
@dat_ttl_seconds = dat_ttl_seconds
|
|
17
17
|
@signature_key = signature_key
|
|
18
18
|
@crypto_key = crypto_key
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def exports(verify_only = false)
|
|
22
22
|
cid_hex = @cid.to_s(16)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
dat_issuance_start_seconds = @dat_issuance_start_seconds.to_s
|
|
24
|
+
dat_issuance_duration_seconds = (@dat_issuance_end_seconds - @dat_issuance_start_seconds).to_s
|
|
25
|
+
dat_ttl_seconds = @dat_ttl_seconds.to_s
|
|
26
|
+
signature_algorithm = @signature_key.algorithm
|
|
27
|
+
crypto_algorithm = @crypto_key.algorithm
|
|
28
|
+
signature_key = @signature_key.exports(verify_only)
|
|
29
|
+
crypto_key = @crypto_key.exports
|
|
30
30
|
|
|
31
|
-
"#{cid_hex}.#{
|
|
31
|
+
"#{cid_hex}.#{dat_issuance_start_seconds}.#{dat_issuance_duration_seconds}.#{dat_ttl_seconds}.#{signature_algorithm}.#{crypto_algorithm}.#{signature_key}.#{crypto_key}"
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def self.generate(cid,
|
|
34
|
+
def self.generate(cid, dat_issuance_start_seconds, dat_issuance_duration_seconds, dat_ttl_seconds, signature_algorithm, crypto_algorithm)
|
|
35
35
|
new(
|
|
36
|
-
cid,
|
|
36
|
+
cid, dat_issuance_start_seconds, dat_issuance_duration_seconds, dat_ttl_seconds,
|
|
37
37
|
Saro::Dat::DatSignature.generate(signature_algorithm),
|
|
38
38
|
Saro::Dat::DatCrypto.generate(crypto_algorithm)
|
|
39
39
|
)
|
|
@@ -44,24 +44,24 @@ module Saro
|
|
|
44
44
|
raise ArgumentError, "Invalid Certificate format" if parts.length != 8
|
|
45
45
|
|
|
46
46
|
cid = parts[0].to_i(16)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
signature_key = Saro::Dat::DatSignature.imports(
|
|
53
|
-
crypto_key = Saro::Dat::DatCrypto.imports(
|
|
47
|
+
dat_issuance_start_seconds = parts[1].to_i
|
|
48
|
+
dat_issuance_duration_seconds = parts[2].to_i
|
|
49
|
+
dat_ttl_seconds = parts[3].to_i
|
|
50
|
+
signature_algorithm = parts[4]
|
|
51
|
+
crypto_algorithm = parts[5]
|
|
52
|
+
signature_key = Saro::Dat::DatSignature.imports(signature_algorithm, parts[6])
|
|
53
|
+
crypto_key = Saro::Dat::DatCrypto.imports(crypto_algorithm, parts[7])
|
|
54
54
|
|
|
55
|
-
new(cid,
|
|
55
|
+
new(cid, dat_issuance_start_seconds, dat_issuance_duration_seconds, dat_ttl_seconds, signature_key, crypto_key)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def issuable
|
|
59
59
|
now = Time.now.to_i
|
|
60
|
-
signable && @
|
|
60
|
+
signable && @dat_issuance_start_seconds <= now && now <= @dat_issuance_end_seconds
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def expired
|
|
64
|
-
Time.now.to_i > (@
|
|
64
|
+
Time.now.to_i > (@dat_issuance_end_seconds + @dat_ttl_seconds)
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
def signable
|
|
@@ -72,6 +72,10 @@ module Saro
|
|
|
72
72
|
@signature_key.pair
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
def support_verify_only
|
|
76
|
+
@signature_key.support_verify_only
|
|
77
|
+
end
|
|
78
|
+
|
|
75
79
|
# For Ruby conventions
|
|
76
80
|
alias_method :issuable?, :issuable
|
|
77
81
|
alias_method :expired?, :expired
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'logger'
|
|
6
|
+
require 'thread'
|
|
7
|
+
require_relative 'dat_manager'
|
|
8
|
+
require_relative 'dat'
|
|
9
|
+
|
|
10
|
+
module Saro
|
|
11
|
+
module Dat
|
|
12
|
+
class DatCmsManager
|
|
13
|
+
DAT_CMS_API_VERSION = "v1"
|
|
14
|
+
|
|
15
|
+
def initialize(uri:, token:, interval_seconds: 60, verify_only: false, dat_manager: nil)
|
|
16
|
+
@uri = uri
|
|
17
|
+
@token = token
|
|
18
|
+
@interval_seconds = interval_seconds
|
|
19
|
+
@verify_only = verify_only
|
|
20
|
+
@manager = dat_manager || DatManager.new
|
|
21
|
+
@version = 0
|
|
22
|
+
@lock = Mutex.new
|
|
23
|
+
@stopped = false
|
|
24
|
+
@logger = Logger.new($stdout)
|
|
25
|
+
@logger.level = Logger::DEBUG
|
|
26
|
+
|
|
27
|
+
sync
|
|
28
|
+
|
|
29
|
+
if @interval_seconds > 0
|
|
30
|
+
schedule_sync
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def stop
|
|
35
|
+
@lock.synchronize do
|
|
36
|
+
@stopped = true
|
|
37
|
+
@thread&.kill # 혹은 다른 방식으로 스레드 중지
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def sync
|
|
42
|
+
# non-blocking lock
|
|
43
|
+
unless @lock.try_lock
|
|
44
|
+
@logger.warn("Last request ignored (Duplicate request)")
|
|
45
|
+
return
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
begin
|
|
49
|
+
url = URI("#{@uri}?version=#{@version}")
|
|
50
|
+
request = Net::HTTP::Get.new(url)
|
|
51
|
+
request["Authorization"] = @token
|
|
52
|
+
|
|
53
|
+
response = Net::HTTP.start(url.host, url.port, use_ssl: url.scheme == 'https', open_timeout: 10, read_timeout: 10) do |http|
|
|
54
|
+
http.request(request)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if response.code != "200"
|
|
58
|
+
@logger.error("Response status error, status:#{response.code} in #{url}")
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
body = response.body
|
|
63
|
+
if body.nil? || body.empty?
|
|
64
|
+
@logger.debug("No new certificate: #{url}")
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
lines = body.split("\n", 2)
|
|
69
|
+
if lines.length < 2
|
|
70
|
+
if body.start_with?("\n")
|
|
71
|
+
@logger.error("Invalid response: #{url}")
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
@logger.debug("No new certificate: #{url}")
|
|
75
|
+
return
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
new_version_str = lines[0].strip
|
|
79
|
+
new_certificates = lines[1].strip
|
|
80
|
+
|
|
81
|
+
if new_version_str.empty?
|
|
82
|
+
@logger.error("Invalid version in response: #{url}")
|
|
83
|
+
return
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
begin
|
|
87
|
+
new_version = Integer(new_version_str)
|
|
88
|
+
renew_count = @manager.imports(new_certificates, clear: false)
|
|
89
|
+
@version = new_version
|
|
90
|
+
@logger.debug("Renewed #{renew_count} certificates for version #{new_version}: #{url}")
|
|
91
|
+
rescue ArgumentError => e
|
|
92
|
+
@logger.error("Failed to parse version or certificates: #{e.message}")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
@logger.error("[Exception] DAT CMS Sync #{@uri}: #{e.message}")
|
|
97
|
+
ensure
|
|
98
|
+
@lock.unlock
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def get_manager
|
|
103
|
+
@manager
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def issue(plain, secure)
|
|
107
|
+
@manager.issue(plain, secure)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def parse(dat)
|
|
111
|
+
@manager.parse(dat)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.builder
|
|
115
|
+
DatCmsManagerBuilder.new
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def schedule_sync
|
|
121
|
+
@thread = Thread.new do
|
|
122
|
+
loop do
|
|
123
|
+
sleep(@interval_seconds)
|
|
124
|
+
break if @stopped
|
|
125
|
+
run_sync_task
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def run_sync_task
|
|
131
|
+
sync
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
@logger.error("Error in sync task: #{e.message}")
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
class DatCmsManagerBuilder
|
|
138
|
+
def initialize
|
|
139
|
+
@uri = "http://localhost:8088"
|
|
140
|
+
@token = ""
|
|
141
|
+
@verify_only = false
|
|
142
|
+
@interval_seconds = 60
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def uri(uri)
|
|
146
|
+
@uri = uri.delete_suffix('/')
|
|
147
|
+
self
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def token(token)
|
|
151
|
+
@token = token
|
|
152
|
+
self
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def verify_only(verify_only)
|
|
156
|
+
@verify_only = verify_only
|
|
157
|
+
self
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def interval_seconds(interval_seconds)
|
|
161
|
+
@interval_seconds = interval_seconds
|
|
162
|
+
self
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def build
|
|
166
|
+
parsed = URI.parse(@uri)
|
|
167
|
+
|
|
168
|
+
if parsed.path && parsed.path != '' && parsed.path != '/'
|
|
169
|
+
raise ArgumentError, "uri must be path-less: #{@uri}"
|
|
170
|
+
end
|
|
171
|
+
if parsed.query
|
|
172
|
+
raise ArgumentError, "uri must be query-less: #{@uri}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
path = @verify_only ? "/v1/certs/verify-only" : "/v1/certs"
|
|
176
|
+
final_uri = "#{parsed.scheme}://#{parsed.host}:#{parsed.port}#{path}"
|
|
177
|
+
|
|
178
|
+
DatCmsManager.new(
|
|
179
|
+
uri: final_uri,
|
|
180
|
+
token: @token,
|
|
181
|
+
interval_seconds: @interval_seconds,
|
|
182
|
+
verify_only: @verify_only
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
data/lib/saro/dat/dat_manager.rb
CHANGED
|
@@ -17,9 +17,10 @@ module Saro
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def import_certificates(input_certs, clear: false)
|
|
20
|
+
renew_count = 0
|
|
20
21
|
@lock.with_write_lock do
|
|
21
22
|
certificates = clear ? [] : @certificates.dup
|
|
22
|
-
|
|
23
|
+
|
|
23
24
|
before_cids = Set.new(certificates.map(&:cid))
|
|
24
25
|
seen_cids = Set.new
|
|
25
26
|
|
|
@@ -30,9 +31,10 @@ module Saro
|
|
|
30
31
|
next if before_cids.include?(cert.cid)
|
|
31
32
|
|
|
32
33
|
certificates << cert
|
|
34
|
+
renew_count += 1
|
|
33
35
|
end
|
|
34
36
|
|
|
35
|
-
certificates.sort_by!(&:
|
|
37
|
+
certificates.sort_by!(&:dat_issuance_end_seconds)
|
|
36
38
|
|
|
37
39
|
# Find latest issuable certificate as issuer
|
|
38
40
|
issuer = certificates.reverse_each.find(&:issuable)
|
|
@@ -40,6 +42,7 @@ module Saro
|
|
|
40
42
|
@issuer = issuer
|
|
41
43
|
@certificates = certificates
|
|
42
44
|
end
|
|
45
|
+
renew_count
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
def imports(format_str, clear: false)
|
|
@@ -82,7 +85,7 @@ module Saro
|
|
|
82
85
|
|
|
83
86
|
def self._issue(cert, plain, secure)
|
|
84
87
|
now = Time.now.to_i
|
|
85
|
-
expire = now + cert.
|
|
88
|
+
expire = now + cert.dat_ttl_seconds
|
|
86
89
|
cid_hex = cert.cid.to_s(16)
|
|
87
90
|
|
|
88
91
|
plain_bytes = plain.is_a?(String) ? plain.encode('utf-8') : (plain || "".b)
|
data/lib/saro/dat/signature.rb
CHANGED
|
@@ -114,6 +114,10 @@ module Saro
|
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
def exports(verify_only = false)
|
|
117
|
+
if verify_only && !support_verify_only
|
|
118
|
+
raise ArgumentError, "#{config[:name]} does not supported verifying only key"
|
|
119
|
+
end
|
|
120
|
+
|
|
117
121
|
if @config[:name] == "HMAC"
|
|
118
122
|
Saro::Dat::Util.encode_base64_url_str(@verifying_key)
|
|
119
123
|
else
|
|
@@ -182,6 +186,10 @@ module Saro
|
|
|
182
186
|
@config[:name] == "ECDSA"
|
|
183
187
|
end
|
|
184
188
|
|
|
189
|
+
def support_verify_only
|
|
190
|
+
@config[:name] == "ECDSA"
|
|
191
|
+
end
|
|
192
|
+
|
|
185
193
|
private
|
|
186
194
|
|
|
187
195
|
def der_to_raw_signature(signature_der)
|
data/lib/saro-dat.rb
CHANGED
data/saro-dat.gemspec
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Gem::Specification.new do |spec|
|
|
4
4
|
spec.name = "saro-dat"
|
|
5
|
-
spec.version = "4.
|
|
5
|
+
spec.version = "4.3.1"
|
|
6
6
|
spec.authors = ["marker"]
|
|
7
7
|
spec.email = ["j@saro.me"]
|
|
8
8
|
|
|
9
9
|
spec.summary = "DAT (Data Access Token) Ruby implementation"
|
|
10
10
|
spec.description = "Ported from Python dat library"
|
|
11
|
-
spec.homepage = "https://dat.saro.me
|
|
11
|
+
spec.homepage = "https://dat.saro.me/libs/gems-saro-dat"
|
|
12
12
|
spec.license = "MIT"
|
|
13
13
|
spec.required_ruby_version = ">= 2.7.0"
|
|
14
14
|
|
|
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
|
|
|
30
30
|
spec.add_dependency "concurrent-ruby", "~> 1.3.6"
|
|
31
31
|
spec.add_dependency "openssl", "~> 4.0.2"
|
|
32
32
|
spec.add_dependency "base64"
|
|
33
|
+
spec.add_dependency "logger"
|
|
33
34
|
|
|
34
35
|
spec.add_development_dependency "minitest", "~> 5.0"
|
|
35
36
|
spec.add_development_dependency "benchmark"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: saro-dat
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- marker
|
|
@@ -51,6 +51,20 @@ dependencies:
|
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: logger
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
54
68
|
- !ruby/object:Gem::Dependency
|
|
55
69
|
name: minitest
|
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -113,15 +127,16 @@ files:
|
|
|
113
127
|
- lib/saro/dat/crypto.rb
|
|
114
128
|
- lib/saro/dat/dat.rb
|
|
115
129
|
- lib/saro/dat/dat_certificate.rb
|
|
130
|
+
- lib/saro/dat/dat_cms_manager.rb
|
|
116
131
|
- lib/saro/dat/dat_manager.rb
|
|
117
132
|
- lib/saro/dat/signature.rb
|
|
118
133
|
- lib/saro/dat/util.rb
|
|
119
134
|
- saro-dat.gemspec
|
|
120
|
-
homepage: https://dat.saro.me
|
|
135
|
+
homepage: https://dat.saro.me/libs/gems-saro-dat
|
|
121
136
|
licenses:
|
|
122
137
|
- MIT
|
|
123
138
|
metadata:
|
|
124
|
-
homepage_uri: https://dat.saro.me
|
|
139
|
+
homepage_uri: https://dat.saro.me/libs/gems-saro-dat
|
|
125
140
|
source_code_uri: https://github.com/saro-lab/dat-ruby
|
|
126
141
|
changelog_uri: https://github.com/saro-lab/dat-ruby/blob/main/CHANGELOG.md
|
|
127
142
|
keywords: dat, distributed, access, token, web, session, security, authentication
|