preverus 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 25c4aebb9cfa7bd4d2c05599cdd9feca312e5920ca06e18b2baad0f7382355ce
4
+ data.tar.gz: 3620728bc7d32c3056dcb2ddf27e1fe63d8309abebfc18f69579a2ab93850e9e
5
+ SHA512:
6
+ metadata.gz: d97656d46e9618553da42d1e358b500a69141c99693bd588994a518cd900eaa3d1112751aa75f2230c1b84d6b9a76c340e9cd57dd0d130cff4545023b2addb2f
7
+ data.tar.gz: a09e2707363bf1db60207b82745c47ea4f39680c232561be79b810f7c20599c3317ab9cc37694ea131ba5975bfa338bf3ed599626249cfe39ff1304479694851
data/README.md ADDED
@@ -0,0 +1,324 @@
1
+ # Preverus Ruby
2
+
3
+ Ruby client for Preverus backend enforcement.
4
+
5
+ Use this gem with Rails, Rack, Sinatra, Hanami, or any Ruby backend that loads the hosted Preverus browser script and needs trusted server-side fraud decisions.
6
+
7
+ ## Install
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem "preverus"
13
+ ```
14
+
15
+ Then run:
16
+
17
+ ```bash
18
+ bundle install
19
+ ```
20
+
21
+ Requires Ruby 3.0+.
22
+
23
+ ## Browser And Server Flow
24
+
25
+ Add the hosted script to your layout:
26
+
27
+ ```html
28
+ <script
29
+ src="https://cdn.preverus.com/v1/preverus.js"
30
+ data-preverus-key="pk_live_xxx"
31
+ data-preverus-auto="true"
32
+ data-preverus-track-forms="true"
33
+ ></script>
34
+
35
+ <form method="POST" action="/register" data-preverus-action="signup">
36
+ <input name="email" type="email">
37
+ <button type="submit">Create account</button>
38
+ </form>
39
+ ```
40
+
41
+ Before submit, the script attaches:
42
+
43
+ ```text
44
+ preverus_fingerprint
45
+ preverus_visitor_id
46
+ preverus_risk_session_token
47
+ preverus_browser_session_event_id
48
+ ```
49
+
50
+ Your backend sends those values to Preverus with a private server key before approving sensitive actions.
51
+
52
+ ## Quick Start
53
+
54
+ ```ruby
55
+ require "preverus"
56
+
57
+ client = Preverus::Client.new(server_key: ENV.fetch("PREVERUS_SERVER_KEY"))
58
+
59
+ decision = client.decisions.evaluate(
60
+ {
61
+ event_type: "signup",
62
+ user_id: "acct_42",
63
+ ip: request.remote_ip,
64
+ risk_session_token: params[:preverus_risk_session_token],
65
+ fingerprint: params[:preverus_fingerprint],
66
+ metadata: {
67
+ email: params[:email],
68
+ browser_session_event_id: params[:preverus_browser_session_event_id]
69
+ }
70
+ },
71
+ visitor_id: params[:preverus_visitor_id],
72
+ idempotency_key: "signup:acct_42:request-id"
73
+ )
74
+
75
+ if decision.block?
76
+ head :forbidden
77
+ return
78
+ end
79
+
80
+ if decision.review?
81
+ redirect_to verify_path
82
+ return
83
+ end
84
+
85
+ # Continue signup.
86
+ ```
87
+
88
+ Prefer `risk_session_token` when available. It links the backend action to the browser session collected moments earlier.
89
+
90
+ ## Configuration
91
+
92
+ ```ruby
93
+ client = Preverus::Client.new(
94
+ server_key: ENV.fetch("PREVERUS_SERVER_KEY"),
95
+ endpoint: "https://api.preverus.com",
96
+ timeout: 1.5,
97
+ retries: 2,
98
+ retry_delay: 0.15,
99
+ max_retry_delay: 1.0
100
+ )
101
+ ```
102
+
103
+ The gem retries transient network failures and retryable statuses:
104
+
105
+ ```text
106
+ 408, 409, 425, 429, 500, 502, 503, 504
107
+ ```
108
+
109
+ It does not retry validation or authentication errors such as `400`, `401`, `403`, or `422`.
110
+
111
+ Use idempotency keys for retried POST requests.
112
+
113
+ ## Rails Setup
114
+
115
+ Create an initializer:
116
+
117
+ ```ruby
118
+ # config/initializers/preverus.rb
119
+ PREVERUS = Preverus::Client.new(
120
+ server_key: ENV.fetch("PREVERUS_SERVER_KEY"),
121
+ timeout: 1.5,
122
+ retries: 2
123
+ )
124
+ ```
125
+
126
+ Use it in a controller:
127
+
128
+ ```ruby
129
+ class RegistrationsController < ApplicationController
130
+ def create
131
+ decision = PREVERUS.decisions.evaluate(
132
+ {
133
+ event_type: "signup",
134
+ user_id: params[:user_id],
135
+ ip: request.remote_ip,
136
+ risk_session_token: params[:preverus_risk_session_token],
137
+ fingerprint: params[:preverus_fingerprint],
138
+ metadata: {
139
+ email: params[:email],
140
+ user_agent: request.user_agent,
141
+ browser_session_event_id: params[:preverus_browser_session_event_id]
142
+ }
143
+ },
144
+ visitor_id: params[:preverus_visitor_id],
145
+ idempotency_key: request.request_id
146
+ )
147
+
148
+ return head :forbidden if decision.block?
149
+ return redirect_to verify_path if decision.review?
150
+
151
+ # Continue registration.
152
+ end
153
+ end
154
+ ```
155
+
156
+ ## Sensitive Actions
157
+
158
+ Use decisions synchronously when your app needs an immediate allow/review/block answer:
159
+
160
+ ```ruby
161
+ decision = PREVERUS.decisions.evaluate(
162
+ {
163
+ event_type: "withdraw",
164
+ user_id: current_user.id,
165
+ ip: request.remote_ip,
166
+ risk_session_token: params[:preverus_risk_session_token],
167
+ metadata: {
168
+ payment_address: params[:payment_address]
169
+ }
170
+ },
171
+ visitor_id: params[:preverus_visitor_id],
172
+ idempotency_key: request.request_id
173
+ )
174
+
175
+ return head :forbidden if decision.block?
176
+ return redirect_to withdraw_review_path if decision.review?
177
+ ```
178
+
179
+ Good event names include:
180
+
181
+ ```text
182
+ signup
183
+ login
184
+ checkout
185
+ password_reset
186
+ payout
187
+ withdraw
188
+ payment_change
189
+ account_update
190
+ ```
191
+
192
+ ## Decisions
193
+
194
+ ```ruby
195
+ decision.recommended_action
196
+ decision.allow?
197
+ decision.review?
198
+ decision.block?
199
+ decision.to_h
200
+ ```
201
+
202
+ Recommended handling:
203
+
204
+ ```text
205
+ allow -> proceed
206
+ review -> step-up auth, hold, or manual review
207
+ block -> deny or hard challenge
208
+ ```
209
+
210
+ ## Events
211
+
212
+ Use events for non-blocking fraud telemetry:
213
+
214
+ ```ruby
215
+ event = PREVERUS.events.create(
216
+ {
217
+ event_type: "login",
218
+ user_id: "acct_42",
219
+ ip: "203.0.113.10",
220
+ fingerprint: "fp_hash",
221
+ metadata: { email: "person@example.com" }
222
+ },
223
+ visitor_id: "v_abc123"
224
+ )
225
+ ```
226
+
227
+ In Rails, send non-blocking events from Active Job or a background worker so customer requests do not wait for telemetry.
228
+
229
+ ## Lookups
230
+
231
+ ```ruby
232
+ visitor = PREVERUS.visitors.lookup(visitor_id: "v_abc123")
233
+ visitor = PREVERUS.visitors.lookup(fingerprint: "fp_hash")
234
+
235
+ metadata = PREVERUS.metadata.lookup("email", "person@example.com")
236
+ graph = PREVERUS.metadata.graph("v_abc123")
237
+ ```
238
+
239
+ Use lookups for investigation and context. Use `decisions.evaluate` for final enforcement.
240
+
241
+ ## Webhook Verification
242
+
243
+ ```ruby
244
+ class PreverusWebhooksController < ApplicationController
245
+ skip_before_action :verify_authenticity_token
246
+
247
+ def create
248
+ raw_body = request.raw_post
249
+ valid = PREVERUS.webhooks.verify(
250
+ raw_body: raw_body,
251
+ timestamp: request.headers["X-Fraud-Webhook-Timestamp"].to_s,
252
+ signature_header: request.headers["X-Fraud-Webhook-Signature"].to_s,
253
+ secret: ENV.fetch("PREVERUS_WEBHOOK_SECRET")
254
+ )
255
+
256
+ return head :bad_request unless valid
257
+
258
+ payload = JSON.parse(raw_body)
259
+
260
+ # Store payload["id"] or X-Fraud-Webhook-Id for idempotency.
261
+
262
+ head :no_content
263
+ end
264
+ end
265
+ ```
266
+
267
+ Webhook delivery is at-least-once. Dedupe by `X-Fraud-Webhook-Id` or payload `id`.
268
+
269
+ You can also verify, parse, and dispatch by event type:
270
+
271
+ ```ruby
272
+ event = PREVERUS.webhooks.construct_event(
273
+ raw_body: request.raw_post,
274
+ headers: request.headers,
275
+ secret: ENV.fetch("PREVERUS_WEBHOOK_SECRET")
276
+ )
277
+
278
+ return head :no_content if already_processed?(event.id)
279
+
280
+ PREVERUS.webhooks.dispatch(event, {
281
+ "decision.high_risk" => ->(event) { open_case(event.to_h) },
282
+ "*" => ->(event) { Rails.logger.info("Preverus webhook #{event.type}") }
283
+ })
284
+ ```
285
+
286
+ The gem verifies and parses the event, but your app should store processed event IDs in your database or cache.
287
+
288
+ ## Failure Handling
289
+
290
+ The Ruby core gem raises exceptions for API and network failures:
291
+
292
+ ```ruby
293
+ begin
294
+ decision = PREVERUS.decisions.evaluate(...)
295
+ rescue Preverus::ApiError => error
296
+ Rails.logger.warn("Preverus API error: #{error.status_code} #{error.error_code}")
297
+ rescue Preverus::NetworkError
298
+ # Apply your app's fail-open, fail-review, or fail-closed policy.
299
+ end
300
+ ```
301
+
302
+ For high-risk flows like withdrawals and payouts, a common policy is fail-review. For signup/login/checkout, many businesses prefer fail-open so the site keeps working during transient network failures.
303
+
304
+ Example fail-review wrapper:
305
+
306
+ ```ruby
307
+ def evaluate_or_review(input, **options)
308
+ PREVERUS.decisions.evaluate(input, **options)
309
+ rescue Preverus::Error => error
310
+ Rails.logger.warn("Preverus fallback review: #{error.class}")
311
+ Preverus::DecisionResult.fallback("review", error.class.name)
312
+ end
313
+ ```
314
+
315
+ ## Production Checklist
316
+
317
+ - Keep `PREVERUS_SERVER_KEY` private.
318
+ - Use a browser key only in templates/frontend code.
319
+ - Prefer `risk_session_token` when present.
320
+ - Include `visitor_id` through the client argument.
321
+ - Send your real customer account ID as `user_id`.
322
+ - Include IP and metadata such as email, phone, username, and payment address.
323
+ - Use idempotency keys for retried POST requests.
324
+ - Treat `review` as step-up/manual review, not as automatic allow.
@@ -0,0 +1,24 @@
1
+ require_relative "http_client"
2
+ require_relative "resources"
3
+
4
+ module Preverus
5
+ class Client
6
+ attr_reader :decisions, :events, :visitors, :metadata, :webhooks
7
+
8
+ def initialize(server_key:, endpoint: "https://api.preverus.com", timeout: 1.5, retries: 2, retry_delay: 0.15, max_retry_delay: 1.0)
9
+ http = HttpClient.new(
10
+ server_key: server_key,
11
+ endpoint: endpoint,
12
+ timeout: timeout,
13
+ retries: retries,
14
+ retry_delay: retry_delay,
15
+ max_retry_delay: max_retry_delay
16
+ )
17
+ @decisions = Decisions.new(http)
18
+ @events = Events.new(http)
19
+ @visitors = Visitors.new(http)
20
+ @metadata = Metadata.new(http)
21
+ @webhooks = Webhooks.new
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ module Preverus
2
+ class Error < StandardError; end
3
+
4
+ class NetworkError < Error; end
5
+
6
+ class ApiError < Error
7
+ attr_reader :status_code, :error_code, :response
8
+
9
+ def initialize(message, status_code: 0, error_code: "api_error", response: {})
10
+ super(message)
11
+ @status_code = status_code
12
+ @error_code = error_code
13
+ @response = response
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,91 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "openssl"
4
+ require "uri"
5
+ require_relative "errors"
6
+
7
+ module Preverus
8
+ class HttpClient
9
+ RETRYABLE_STATUSES = [408, 409, 425, 429, 500, 502, 503, 504].freeze
10
+
11
+ def initialize(server_key:, endpoint:, timeout:, retries:, retry_delay:, max_retry_delay:)
12
+ @server_key = server_key.to_s.strip
13
+ @endpoint = endpoint.to_s.sub(%r{/+$}, "")
14
+ @timeout = timeout
15
+ @retries = [retries.to_i, 0].max
16
+ @retry_delay = retry_delay
17
+ @max_retry_delay = max_retry_delay
18
+ end
19
+
20
+ def get(path, params = {}, headers = {})
21
+ query = URI.encode_www_form(params.reject { |_key, value| value.nil? || value == "" })
22
+ request("GET", query.empty? ? path : "#{path}?#{query}", nil, headers)
23
+ end
24
+
25
+ def post(path, body, headers = {})
26
+ request("POST", path, body, headers)
27
+ end
28
+
29
+ private
30
+
31
+ def request(method, path, body, headers)
32
+ raise ApiError.new("Missing Preverus server key.", error_code: "missing_server_key") if @server_key.empty?
33
+
34
+ attempts = @retries + 1
35
+ attempt = 1
36
+ begin
37
+ send_request(method, path, body, headers)
38
+ rescue NetworkError
39
+ raise if attempt >= attempts
40
+
41
+ sleep_before_retry(attempt)
42
+ attempt += 1
43
+ retry
44
+ rescue ApiError => error
45
+ raise if !RETRYABLE_STATUSES.include?(error.status_code) || attempt >= attempts
46
+
47
+ sleep_before_retry(attempt)
48
+ attempt += 1
49
+ retry
50
+ end
51
+ end
52
+
53
+ def send_request(method, path, body, headers)
54
+ uri = URI.parse("#{@endpoint}/#{path.sub(%r{^/+}, "")}")
55
+ http = Net::HTTP.new(uri.host, uri.port)
56
+ http.use_ssl = uri.scheme == "https"
57
+ http.open_timeout = @timeout
58
+ http.read_timeout = @timeout
59
+
60
+ request = method == "GET" ? Net::HTTP::Get.new(uri) : Net::HTTP::Post.new(uri)
61
+ request["Accept"] = "application/json"
62
+ request["Content-Type"] = "application/json"
63
+ request["User-Agent"] = "preverus-ruby/0.1"
64
+ request["X-API-Key"] = @server_key
65
+ headers.each { |key, value| request[key] = value if value && value != "" }
66
+ request.body = JSON.generate(body) if body
67
+
68
+ response = http.request(request)
69
+ decoded = response.body.to_s.empty? ? {} : JSON.parse(response.body)
70
+ status = response.code.to_i
71
+ raise api_error(status, decoded) unless status.between?(200, 299)
72
+
73
+ decoded
74
+ rescue JSON::ParserError
75
+ raise ApiError.new("Preverus returned invalid JSON.", error_code: "invalid_json")
76
+ rescue Timeout::Error, SocketError, SystemCallError, OpenSSL::SSL::SSLError => error
77
+ raise NetworkError, "Unable to connect to Preverus: #{error.message}"
78
+ end
79
+
80
+ def api_error(status, decoded)
81
+ message = decoded.is_a?(Hash) && decoded["message"].is_a?(String) ? decoded["message"] : "Preverus API error."
82
+ code = decoded.is_a?(Hash) && decoded["code"].is_a?(String) ? decoded["code"] : "api_error"
83
+ ApiError.new(message, status_code: status, error_code: code, response: decoded.is_a?(Hash) ? decoded : {})
84
+ end
85
+
86
+ def sleep_before_retry(attempt)
87
+ base = [@max_retry_delay, @retry_delay * (2**[attempt - 1, 0].max)].min
88
+ sleep(base + rand * (base / 2.0))
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,99 @@
1
+ require "openssl"
2
+ require "json"
3
+ require_relative "results"
4
+
5
+ module Preverus
6
+ class Decisions
7
+ def initialize(http)
8
+ @http = http
9
+ end
10
+
11
+ def evaluate(input, visitor_id: nil, idempotency_key: nil, headers: {})
12
+ request_headers = headers.dup
13
+ request_headers["X-Visitor-ID"] = visitor_id if visitor_id && visitor_id != ""
14
+ request_headers["X-Idempotency-Key"] = idempotency_key if idempotency_key && idempotency_key != ""
15
+ DecisionResult.new(@http.post("/v1/decision/evaluate", input, request_headers))
16
+ end
17
+ end
18
+
19
+ class Events
20
+ def initialize(http)
21
+ @http = http
22
+ end
23
+
24
+ def create(input, visitor_id: nil, idempotency_key: nil)
25
+ headers = {}
26
+ headers["X-Visitor-ID"] = visitor_id if visitor_id && visitor_id != ""
27
+ headers["X-Idempotency-Key"] = idempotency_key if idempotency_key && idempotency_key != ""
28
+ ApiResult.new(@http.post("/v1/events", input, headers))
29
+ end
30
+ end
31
+
32
+ class Visitors
33
+ def initialize(http)
34
+ @http = http
35
+ end
36
+
37
+ def lookup(visitor_id: nil, fingerprint: nil, ip: nil)
38
+ ApiResult.new(@http.get("/v1/score/visitors/lookup", { visitor_id: visitor_id, fingerprint: fingerprint, ip: ip }))
39
+ end
40
+ end
41
+
42
+ class Metadata
43
+ def initialize(http)
44
+ @http = http
45
+ end
46
+
47
+ def lookup(key, value, include_global: true, visitor_limit: 20)
48
+ ApiResult.new(@http.get("/v1/score/metadata/lookup", {
49
+ key: key,
50
+ value: value,
51
+ include_global: include_global ? "true" : "false",
52
+ visitor_limit: visitor_limit
53
+ }))
54
+ end
55
+
56
+ def graph(visitor_id, limit: 50)
57
+ ApiResult.new(@http.get("/v1/score/metadata/graph", { visitor_id: visitor_id, limit: limit }))
58
+ end
59
+ end
60
+
61
+ class Webhooks
62
+ def verify(raw_body:, timestamp:, signature_header:, secret:, tolerance_seconds: 300)
63
+ return false if timestamp.to_s !~ /\A\d+\z/ || signature_header.to_s.empty? || secret.to_s.empty?
64
+ return false if (Time.now.to_i - timestamp.to_i).abs > tolerance_seconds
65
+
66
+ expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}")
67
+ received = signature_header.to_s.sub(/\Av1=/, "").strip
68
+ secure_compare(expected, received)
69
+ end
70
+
71
+ def construct_event(raw_body:, headers:, secret:, tolerance_seconds: 300)
72
+ timestamp = headers["X-Fraud-Webhook-Timestamp"] || headers["x-fraud-webhook-timestamp"] || ""
73
+ signature = headers["X-Fraud-Webhook-Signature"] || headers["x-fraud-webhook-signature"] || ""
74
+ unless verify(raw_body: raw_body, timestamp: timestamp, signature_header: signature, secret: secret, tolerance_seconds: tolerance_seconds)
75
+ raise ArgumentError, "Invalid Preverus webhook signature."
76
+ end
77
+
78
+ payload = JSON.parse(raw_body)
79
+ raise ArgumentError, "Invalid Preverus webhook JSON payload." unless payload.is_a?(Hash)
80
+
81
+ WebhookEvent.new(payload, raw_body)
82
+ end
83
+
84
+ def dispatch(event, handlers)
85
+ handler = handlers[event.type] || handlers["*"]
86
+ return nil unless handler.respond_to?(:call)
87
+
88
+ handler.call(event)
89
+ end
90
+
91
+ private
92
+
93
+ def secure_compare(expected, received)
94
+ return false unless expected.bytesize == received.bytesize
95
+
96
+ expected.bytes.zip(received.bytes).reduce(0) { |memo, (left, right)| memo | (left ^ right) }.zero?
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,82 @@
1
+ module Preverus
2
+ class ApiResult
3
+ attr_reader :data
4
+
5
+ def initialize(data)
6
+ @data = data || {}
7
+ end
8
+
9
+ def [](key)
10
+ @data[key.to_s] || @data[key.to_sym]
11
+ end
12
+
13
+ def to_h
14
+ @data.dup
15
+ end
16
+ end
17
+
18
+ class DecisionResult < ApiResult
19
+ attr_reader :failure_reason
20
+
21
+ def initialize(data, fallback: false, failure_reason: nil)
22
+ super(data)
23
+ @fallback = fallback
24
+ @failure_reason = failure_reason
25
+ end
26
+
27
+ def self.fallback(action, reason)
28
+ new(
29
+ {
30
+ "recommended_action" => action,
31
+ "risk_tier" => action == "allow" ? "unknown" : "review",
32
+ "reasons" => [
33
+ {
34
+ "code" => "preverus_unavailable",
35
+ "severity" => action == "allow" ? "low" : "medium",
36
+ "message" => "Preverus was unavailable; fallback policy was applied."
37
+ }
38
+ ]
39
+ },
40
+ fallback: true,
41
+ failure_reason: reason
42
+ )
43
+ end
44
+
45
+ def recommended_action
46
+ self["recommended_action"] || self["action"] || "review"
47
+ end
48
+
49
+ def allow?
50
+ recommended_action == "allow"
51
+ end
52
+
53
+ def review?
54
+ recommended_action == "review"
55
+ end
56
+
57
+ def block?
58
+ ["block", "deny"].include?(recommended_action)
59
+ end
60
+
61
+ def fallback?
62
+ @fallback
63
+ end
64
+ end
65
+
66
+ class WebhookEvent < ApiResult
67
+ attr_reader :raw_body
68
+
69
+ def initialize(data, raw_body)
70
+ super(data)
71
+ @raw_body = raw_body
72
+ end
73
+
74
+ def id
75
+ self["id"].to_s
76
+ end
77
+
78
+ def type
79
+ self["type"].to_s
80
+ end
81
+ end
82
+ end
data/lib/preverus.rb ADDED
@@ -0,0 +1,6 @@
1
+ require_relative "preverus/client"
2
+ require_relative "preverus/errors"
3
+ require_relative "preverus/results"
4
+
5
+ module Preverus
6
+ end
data/preverus.gemspec ADDED
@@ -0,0 +1,16 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "preverus"
3
+ spec.version = "0.1.0"
4
+ spec.authors = ["Preverus"]
5
+ spec.summary = "Ruby client for Preverus fraud decisions, events, lookups, and webhooks."
6
+ spec.description = "Framework-agnostic Ruby client for Preverus backend fraud enforcement."
7
+ spec.homepage = "https://preverus.com"
8
+ spec.license = "MIT"
9
+ spec.required_ruby_version = ">= 3.0"
10
+ spec.files = Dir["lib/**/*.rb", "README.md", "LICENSE*", "preverus.gemspec"]
11
+ spec.require_paths = ["lib"]
12
+ spec.metadata = {
13
+ "source_code_uri" => "https://github.com/preverus/preverus-ruby",
14
+ "documentation_uri" => "https://preverus.com/docs"
15
+ }
16
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: preverus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Preverus
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Framework-agnostic Ruby client for Preverus backend fraud enforcement.
13
+ executables: []
14
+ extensions: []
15
+ extra_rdoc_files: []
16
+ files:
17
+ - README.md
18
+ - lib/preverus.rb
19
+ - lib/preverus/client.rb
20
+ - lib/preverus/errors.rb
21
+ - lib/preverus/http_client.rb
22
+ - lib/preverus/resources.rb
23
+ - lib/preverus/results.rb
24
+ - preverus.gemspec
25
+ homepage: https://preverus.com
26
+ licenses:
27
+ - MIT
28
+ metadata:
29
+ source_code_uri: https://github.com/preverus/preverus-ruby
30
+ documentation_uri: https://preverus.com/docs
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '3.0'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubygems_version: 4.0.10
46
+ specification_version: 4
47
+ summary: Ruby client for Preverus fraud decisions, events, lookups, and webhooks.
48
+ test_files: []