touchque 5.0.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 +7 -0
- data/CHANGELOG.md +105 -0
- data/LICENSE +21 -0
- data/README.md +383 -0
- data/lib/touchque/agent.rb +211 -0
- data/lib/touchque/agent_card.rb +159 -0
- data/lib/touchque/approvals.rb +214 -0
- data/lib/touchque/client.rb +81 -0
- data/lib/touchque/code_reply.rb +53 -0
- data/lib/touchque/cross_app_access.rb +204 -0
- data/lib/touchque/crypto.rb +215 -0
- data/lib/touchque/errors.rb +246 -0
- data/lib/touchque/flow.rb +362 -0
- data/lib/touchque/hosted_login.rb +401 -0
- data/lib/touchque/http.rb +127 -0
- data/lib/touchque/login_session.rb +86 -0
- data/lib/touchque/org_agent.rb +398 -0
- data/lib/touchque/rack/relay.rb +75 -0
- data/lib/touchque/rails/guard.rb +132 -0
- data/lib/touchque/rails/hosted_login.rb +143 -0
- data/lib/touchque/rails/sessions_controller.rb +12 -0
- data/lib/touchque/railtie.rb +34 -0
- data/lib/touchque/relay.rb +533 -0
- data/lib/touchque/resources/actions.rb +212 -0
- data/lib/touchque/resources/agents.rb +251 -0
- data/lib/touchque/resources/device_risk.rb +130 -0
- data/lib/touchque/resources/identity.rb +331 -0
- data/lib/touchque/resources/login.rb +325 -0
- data/lib/touchque/resources/mobile.rb +115 -0
- data/lib/touchque/resources/people.rb +237 -0
- data/lib/touchque/resources/qr_pairings.rb +82 -0
- data/lib/touchque/resources/safe_guard.rb +87 -0
- data/lib/touchque/resources/webhook.rb +177 -0
- data/lib/touchque/safe_approval.rb +283 -0
- data/lib/touchque/security_events.rb +162 -0
- data/lib/touchque/util.rb +260 -0
- data/lib/touchque/version.rb +5 -0
- data/lib/touchque.rb +124 -0
- metadata +83 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Touchque
|
|
9
|
+
# Small helpers shared by the SDK (no third-party gems).
|
|
10
|
+
module Util
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def b64url(bytes)
|
|
14
|
+
[bytes].pack("m0").tr("+/", "-_").delete("=")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def b64url_decode(text)
|
|
18
|
+
s = text.to_s.tr("-_", "+/")
|
|
19
|
+
s += "=" * ((4 - s.length % 4) % 4)
|
|
20
|
+
s.unpack1("m0")
|
|
21
|
+
rescue ArgumentError
|
|
22
|
+
raise ArgumentError, "malformed base64url"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def b64_decode(text)
|
|
26
|
+
text.to_s.unpack1("m0")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def sha256(data)
|
|
30
|
+
OpenSSL::Digest::SHA256.digest(data)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def sha256_hex(data)
|
|
34
|
+
OpenSSL::Digest::SHA256.hexdigest(data)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def hmac_hex(key, data)
|
|
38
|
+
OpenSSL::HMAC.hexdigest("SHA256", key, data)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def hmac(key, data)
|
|
42
|
+
OpenSSL::HMAC.digest("SHA256", key, data)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Constant-time string comparison.
|
|
46
|
+
def secure_compare(a, b)
|
|
47
|
+
a = a.to_s.b
|
|
48
|
+
b = b.to_s.b
|
|
49
|
+
return false unless a.bytesize == b.bytesize
|
|
50
|
+
|
|
51
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def random_hex(bytes = 16)
|
|
55
|
+
SecureRandom.hex(bytes)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A path segment, percent-encoded (encodeURIComponent / rawurlencode).
|
|
59
|
+
def seg(value)
|
|
60
|
+
URI.encode_www_form_component(value.to_s).gsub("+", "%20")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# JSON text, compact, non-ASCII kept as is (like JSON.stringify).
|
|
64
|
+
def json(value)
|
|
65
|
+
JSON.generate(value)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse_json(text)
|
|
69
|
+
return nil if text.nil? || text.strip.empty?
|
|
70
|
+
|
|
71
|
+
JSON.parse(text)
|
|
72
|
+
rescue JSON::ParserError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Keys as strings, deeply (symbol keys from Ruby callers become the API's names).
|
|
77
|
+
def stringify(value)
|
|
78
|
+
case value
|
|
79
|
+
when Hash then value.each_with_object({}) { |(k, v), h| h[k.to_s] = stringify(v) }
|
|
80
|
+
when Array then value.map { |v| stringify(v) }
|
|
81
|
+
when Symbol then value.to_s
|
|
82
|
+
else value
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A hash without nil values (and, for strings, without empty ones).
|
|
87
|
+
def compact(hash)
|
|
88
|
+
hash.reject { |_, v| v.nil? || (v.is_a?(String) && v.empty?) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The bytes JSON.stringify would write for a string (quotes included).
|
|
92
|
+
def js_string(str)
|
|
93
|
+
out = +'"'
|
|
94
|
+
str.to_s.each_char do |c|
|
|
95
|
+
out << case c
|
|
96
|
+
when '"' then '\\"'
|
|
97
|
+
when "\\" then "\\\\"
|
|
98
|
+
when "\b" then "\\b"
|
|
99
|
+
when "\f" then "\\f"
|
|
100
|
+
when "\n" then "\\n"
|
|
101
|
+
when "\r" then "\\r"
|
|
102
|
+
when "\t" then "\\t"
|
|
103
|
+
else
|
|
104
|
+
c.ord < 0x20 ? format("\\u%04x", c.ord) : c
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
out << '"'
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# JSON text without insignificant whitespace; strings and numbers kept byte for byte.
|
|
111
|
+
def compact_json(text)
|
|
112
|
+
out = +""
|
|
113
|
+
in_string = false
|
|
114
|
+
escaped = false
|
|
115
|
+
text.each_char do |c|
|
|
116
|
+
if in_string
|
|
117
|
+
out << c
|
|
118
|
+
if escaped
|
|
119
|
+
escaped = false
|
|
120
|
+
elsif c == "\\"
|
|
121
|
+
escaped = true
|
|
122
|
+
elsif c == '"'
|
|
123
|
+
in_string = false
|
|
124
|
+
end
|
|
125
|
+
elsif c == '"'
|
|
126
|
+
in_string = true
|
|
127
|
+
out << c
|
|
128
|
+
elsif !" \t\n\r".include?(c)
|
|
129
|
+
out << c
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
out
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The members of a top-level JSON object as [key, raw value text] pairs, in order.
|
|
136
|
+
def top_level_members(text)
|
|
137
|
+
s = text.strip
|
|
138
|
+
raise JSON::ParserError, "not an object" unless s.start_with?("{")
|
|
139
|
+
|
|
140
|
+
members = []
|
|
141
|
+
i = 1
|
|
142
|
+
loop do
|
|
143
|
+
i = skip_ws(s, i)
|
|
144
|
+
break if s[i] == "}"
|
|
145
|
+
|
|
146
|
+
key_end = scan_string(s, i)
|
|
147
|
+
key = JSON.parse("[#{s[i...key_end]}]")[0]
|
|
148
|
+
i = skip_ws(s, key_end)
|
|
149
|
+
raise JSON::ParserError, "expected :" unless s[i] == ":"
|
|
150
|
+
|
|
151
|
+
i = skip_ws(s, i + 1)
|
|
152
|
+
value_end = scan_value(s, i)
|
|
153
|
+
members << [key, s[i...value_end]]
|
|
154
|
+
i = skip_ws(s, value_end)
|
|
155
|
+
if s[i] == ","
|
|
156
|
+
i += 1
|
|
157
|
+
elsif s[i] == "}"
|
|
158
|
+
break
|
|
159
|
+
else
|
|
160
|
+
raise JSON::ParserError, "expected , or }"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
members
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def skip_ws(s, i)
|
|
167
|
+
i += 1 while i < s.length && " \t\n\r".include?(s[i])
|
|
168
|
+
i
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def scan_string(s, i)
|
|
172
|
+
raise JSON::ParserError, "expected a string" unless s[i] == '"'
|
|
173
|
+
|
|
174
|
+
i += 1
|
|
175
|
+
while i < s.length
|
|
176
|
+
case s[i]
|
|
177
|
+
when "\\" then i += 2
|
|
178
|
+
when '"' then return i + 1
|
|
179
|
+
else i += 1
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
raise JSON::ParserError, "unterminated string"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def scan_value(s, i)
|
|
186
|
+
case s[i]
|
|
187
|
+
when '"' then scan_string(s, i)
|
|
188
|
+
when "{", "["
|
|
189
|
+
depth = 0
|
|
190
|
+
while i < s.length
|
|
191
|
+
case s[i]
|
|
192
|
+
when '"'
|
|
193
|
+
i = scan_string(s, i)
|
|
194
|
+
next
|
|
195
|
+
when "{", "[" then depth += 1
|
|
196
|
+
when "}", "]"
|
|
197
|
+
depth -= 1
|
|
198
|
+
return i + 1 if depth.zero?
|
|
199
|
+
end
|
|
200
|
+
i += 1
|
|
201
|
+
end
|
|
202
|
+
raise JSON::ParserError, "unterminated value"
|
|
203
|
+
else
|
|
204
|
+
j = i
|
|
205
|
+
j += 1 while j < s.length && !",}".include?(s[j])
|
|
206
|
+
j -= 1 while j > i && " \t\n\r".include?(s[j - 1])
|
|
207
|
+
j
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# True for https, and for plaintext http only on this machine (localhost, 127.0.0.1,
|
|
212
|
+
# ::1, *.localhost: local development).
|
|
213
|
+
def https_or_loopback?(url)
|
|
214
|
+
uri = URI.parse(url.to_s)
|
|
215
|
+
return true if uri.scheme == "https"
|
|
216
|
+
|
|
217
|
+
host = uri.hostname.to_s.downcase
|
|
218
|
+
uri.scheme == "http" && (%w[localhost 127.0.0.1 ::1].include?(host) || host.end_with?(".localhost"))
|
|
219
|
+
rescue URI::InvalidURIError
|
|
220
|
+
false
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Allow plaintext http only for localhost / loopback (local development).
|
|
224
|
+
def assert_safe_url!(url, name)
|
|
225
|
+
uri = URI.parse(url.to_s)
|
|
226
|
+
return if uri.scheme == "https"
|
|
227
|
+
|
|
228
|
+
if uri.scheme == "http"
|
|
229
|
+
return if %w[localhost 127.0.0.1 ::1 [::1]].include?(uri.host.to_s.downcase)
|
|
230
|
+
|
|
231
|
+
raise ConfigError, "Refusing a plaintext http:// #{name} for \"#{uri.host}\". The API key and request signature would be sent in the clear: use https://."
|
|
232
|
+
end
|
|
233
|
+
raise ConfigError, "#{name} must be http(s): #{url}"
|
|
234
|
+
rescue URI::InvalidURIError
|
|
235
|
+
raise ConfigError, "#{name} is not a valid URL: #{url}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def now
|
|
239
|
+
Time.now.to_i
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def now_ms
|
|
243
|
+
(Time.now.to_f * 1000).to_i
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Unix seconds of an ISO 8601 timestamp, or nil.
|
|
247
|
+
def parse_time(value)
|
|
248
|
+
return nil unless value.is_a?(String) && !value.empty?
|
|
249
|
+
|
|
250
|
+
require "time"
|
|
251
|
+
Time.iso8601(value).to_f
|
|
252
|
+
rescue ArgumentError
|
|
253
|
+
begin
|
|
254
|
+
Time.parse(value).to_f
|
|
255
|
+
rescue ArgumentError
|
|
256
|
+
nil
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
data/lib/touchque.rb
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "touchque/version"
|
|
4
|
+
require_relative "touchque/errors"
|
|
5
|
+
require_relative "touchque/util"
|
|
6
|
+
require_relative "touchque/http"
|
|
7
|
+
require_relative "touchque/crypto"
|
|
8
|
+
require_relative "touchque/approvals"
|
|
9
|
+
require_relative "touchque/code_reply"
|
|
10
|
+
require_relative "touchque/safe_approval"
|
|
11
|
+
require_relative "touchque/resources/login"
|
|
12
|
+
require_relative "touchque/resources/actions"
|
|
13
|
+
require_relative "touchque/resources/people"
|
|
14
|
+
require_relative "touchque/resources/agents"
|
|
15
|
+
require_relative "touchque/resources/identity"
|
|
16
|
+
require_relative "touchque/resources/mobile"
|
|
17
|
+
require_relative "touchque/resources/qr_pairings"
|
|
18
|
+
require_relative "touchque/resources/device_risk"
|
|
19
|
+
require_relative "touchque/resources/safe_guard"
|
|
20
|
+
require_relative "touchque/resources/webhook"
|
|
21
|
+
require_relative "touchque/flow"
|
|
22
|
+
require_relative "touchque/client"
|
|
23
|
+
require_relative "touchque/relay"
|
|
24
|
+
require_relative "touchque/agent"
|
|
25
|
+
require_relative "touchque/org_agent"
|
|
26
|
+
require_relative "touchque/cross_app_access"
|
|
27
|
+
require_relative "touchque/security_events"
|
|
28
|
+
require_relative "touchque/agent_card"
|
|
29
|
+
require_relative "touchque/hosted_login"
|
|
30
|
+
require_relative "touchque/login_session"
|
|
31
|
+
|
|
32
|
+
# Touchque: push approvals with number matching, passkeys, silent sign-ins, offline
|
|
33
|
+
# codes, verified webhooks, Touchque Actions, AI agent approvals, Touchque Login and the
|
|
34
|
+
# server side of the Mobile SDK.
|
|
35
|
+
#
|
|
36
|
+
# # TQ_API_KEY=tq_... TQ_API_SECRET=...
|
|
37
|
+
# step = Touchque.client.start("SEND_MONEY", "jane@acme.com", details: { "Amount" => "250 EUR" })
|
|
38
|
+
module Touchque
|
|
39
|
+
@lock = Mutex.new
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
attr_writer :client, :replay_cache, :hosted_login, :login_secret
|
|
43
|
+
|
|
44
|
+
# Where the SDK reports its own problems: a misconfigured guard, a failed on_challenge,
|
|
45
|
+
# a relay without get_login_user. nil (the default) logs nothing; set any Logger
|
|
46
|
+
# (Touchque.logger = Rails.logger). Lines carry an error's class and message, never
|
|
47
|
+
# request data.
|
|
48
|
+
attr_accessor :logger
|
|
49
|
+
|
|
50
|
+
# @api private: one of the SDK's own problems (:warn or :error) to Touchque.logger, if any.
|
|
51
|
+
# A logger that raises never breaks the request.
|
|
52
|
+
def log(level, message)
|
|
53
|
+
logger&.public_send(level, message)
|
|
54
|
+
rescue StandardError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Options for Touchque.client (api_key:, api_secret:, base_url:, timeout:); anything
|
|
59
|
+
# left out comes from the environment.
|
|
60
|
+
def configure(**options)
|
|
61
|
+
@lock.synchronize do
|
|
62
|
+
@client_options = options
|
|
63
|
+
@client = nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The shared client, built on first use from configure or the environment.
|
|
68
|
+
def client
|
|
69
|
+
@client || @lock.synchronize { @client ||= Client.new(**(@client_options || {})) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Webhook jtis already accepted (in-process by default; set one over Redis when you
|
|
73
|
+
# run several processes: anything answering check_and_set(jti, ttl_seconds)).
|
|
74
|
+
def replay_cache
|
|
75
|
+
@replay_cache || @lock.synchronize { @replay_cache ||= MemoryReplayCache.new }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Touchque Login settings: domain:, client_id:, client_secret:, scope:, audience:,
|
|
79
|
+
# redirect_uri:, callback_path:, login_path:, post_logout_redirect_uri:, session_max_age:.
|
|
80
|
+
def login_options=(options)
|
|
81
|
+
@login_options = Util.stringify(options || {}).transform_keys(&:to_sym)
|
|
82
|
+
@hosted_login = nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def login_options
|
|
86
|
+
@login_options || {}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The Touchque Login client of login_options.
|
|
90
|
+
def hosted_login
|
|
91
|
+
@hosted_login || @lock.synchronize do
|
|
92
|
+
@hosted_login ||= begin
|
|
93
|
+
o = login_options
|
|
94
|
+
raise ConfigError, "Set config.touchque.login (domain:, client_id:) or Touchque.login_options" if o[:domain].to_s.empty?
|
|
95
|
+
|
|
96
|
+
HostedLogin.new(domain: o[:domain], client_id: o[:client_id], client_secret: o[:client_secret],
|
|
97
|
+
scope: o[:scope] || "openid profile email", audience: o[:audience],
|
|
98
|
+
redirect_uri: o[:redirect_uri].to_s.match?(%r{\Ahttps?://}) ? o[:redirect_uri] : nil)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The secret that encrypts the Touchque Login cookies (at least 32 characters).
|
|
104
|
+
def login_secret
|
|
105
|
+
secret = @login_secret.respond_to?(:call) ? @login_secret.call : @login_secret
|
|
106
|
+
raise ConfigError, "Set Touchque.login_secret (32+ characters) for the Touchque Login cookies" if secret.to_s.length < 32
|
|
107
|
+
|
|
108
|
+
secret
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The relay as a Rack app (Sinatra, plain Rack), loaded on first use: Rack is not a
|
|
113
|
+
# dependency of the gem.
|
|
114
|
+
module Rack
|
|
115
|
+
autoload :Relay, File.expand_path("touchque/rack/relay", __dir__)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
if defined?(Rails::Railtie)
|
|
120
|
+
require_relative "touchque/rack/relay"
|
|
121
|
+
require_relative "touchque/rails/guard"
|
|
122
|
+
require_relative "touchque/rails/hosted_login"
|
|
123
|
+
require_relative "touchque/railtie"
|
|
124
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: touchque
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 5.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Touchque
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Server SDK for Touchque Authenticator and Touchque Login, with Rails
|
|
13
|
+
integration (Railtie, controller concerns) and a Rack relay for @touchque/web.
|
|
14
|
+
email:
|
|
15
|
+
- developer@touchque.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- lib/touchque.rb
|
|
24
|
+
- lib/touchque/agent.rb
|
|
25
|
+
- lib/touchque/agent_card.rb
|
|
26
|
+
- lib/touchque/approvals.rb
|
|
27
|
+
- lib/touchque/client.rb
|
|
28
|
+
- lib/touchque/code_reply.rb
|
|
29
|
+
- lib/touchque/cross_app_access.rb
|
|
30
|
+
- lib/touchque/crypto.rb
|
|
31
|
+
- lib/touchque/errors.rb
|
|
32
|
+
- lib/touchque/flow.rb
|
|
33
|
+
- lib/touchque/hosted_login.rb
|
|
34
|
+
- lib/touchque/http.rb
|
|
35
|
+
- lib/touchque/login_session.rb
|
|
36
|
+
- lib/touchque/org_agent.rb
|
|
37
|
+
- lib/touchque/rack/relay.rb
|
|
38
|
+
- lib/touchque/rails/guard.rb
|
|
39
|
+
- lib/touchque/rails/hosted_login.rb
|
|
40
|
+
- lib/touchque/rails/sessions_controller.rb
|
|
41
|
+
- lib/touchque/railtie.rb
|
|
42
|
+
- lib/touchque/relay.rb
|
|
43
|
+
- lib/touchque/resources/actions.rb
|
|
44
|
+
- lib/touchque/resources/agents.rb
|
|
45
|
+
- lib/touchque/resources/device_risk.rb
|
|
46
|
+
- lib/touchque/resources/identity.rb
|
|
47
|
+
- lib/touchque/resources/login.rb
|
|
48
|
+
- lib/touchque/resources/mobile.rb
|
|
49
|
+
- lib/touchque/resources/people.rb
|
|
50
|
+
- lib/touchque/resources/qr_pairings.rb
|
|
51
|
+
- lib/touchque/resources/safe_guard.rb
|
|
52
|
+
- lib/touchque/resources/webhook.rb
|
|
53
|
+
- lib/touchque/safe_approval.rb
|
|
54
|
+
- lib/touchque/security_events.rb
|
|
55
|
+
- lib/touchque/util.rb
|
|
56
|
+
- lib/touchque/version.rb
|
|
57
|
+
homepage: https://touchque.com
|
|
58
|
+
licenses:
|
|
59
|
+
- MIT
|
|
60
|
+
metadata:
|
|
61
|
+
source_code_uri: https://github.com/Touchque/touchque-ruby
|
|
62
|
+
changelog_uri: https://github.com/Touchque/touchque-ruby/blob/main/CHANGELOG.md
|
|
63
|
+
documentation_uri: https://support.touchque.com
|
|
64
|
+
rubygems_mfa_required: 'true'
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '3.1'
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 4.0.23
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: 'Official Ruby SDK for Touchque: push approvals with number matching, passkeys,
|
|
82
|
+
silent sign-ins, webhooks, Actions, AI agents and Touchque Login.'
|
|
83
|
+
test_files: []
|