vigilhq 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 +7 -0
- data/lib/vigil/client.rb +190 -0
- data/lib/vigil/errors.rb +40 -0
- data/lib/vigil/types.rb +24 -0
- data/lib/vigil.rb +9 -0
- metadata +75 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b773ff979e7c20af37f68c5a9ce3f625e58a397c80f186c8931e583be5fd9113
|
|
4
|
+
data.tar.gz: 0564f2a5d2c0cb5aeb23dccebf99c062c78261ac967f4b32f4e2d7c84efff33d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ff35ed4deb9f2907be7270a3eb79e81414a08ea2eb9af0c85b72f873d41664d2c872201c2e501bef7ee8550e9da54421366970a04f1febb2dadb17963b869bf6
|
|
7
|
+
data.tar.gz: 35b19c88297ac5561635bf2ef50716b1d6c0604abe7632c380d3fe2cd012fb420f60f0b62fea22a072ef19f3f78cb2fb7f53c5f0bbd235c874a91dfc4ac6434c
|
data/lib/vigil/client.rb
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
module Vigil
|
|
9
|
+
class Client
|
|
10
|
+
# Official Ruby client for the Vigil compliance screening API.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# client = Vigil::Client.new(api_key: "vgl_sk_live_...")
|
|
14
|
+
# result = client.screen("Osama bin Laden")
|
|
15
|
+
# puts result.status # "match"
|
|
16
|
+
# puts result.recommendation # "block"
|
|
17
|
+
|
|
18
|
+
attr_reader :api_key, :base_url
|
|
19
|
+
|
|
20
|
+
def initialize(api_key:, base_url: "https://api.vigilhq.dev", signing_secret: nil, timeout: 30)
|
|
21
|
+
@api_key = api_key
|
|
22
|
+
@base_url = base_url.chomp("/")
|
|
23
|
+
@signing_secret = signing_secret
|
|
24
|
+
@conn = Faraday.new(url: @base_url) do |f|
|
|
25
|
+
f.request :json
|
|
26
|
+
f.response :json
|
|
27
|
+
f.options.timeout = timeout
|
|
28
|
+
f.headers["Authorization"] = "Bearer #{api_key}"
|
|
29
|
+
f.headers["User-Agent"] = "vigil-ruby/#{VERSION}"
|
|
30
|
+
f.headers["Content-Type"] = "application/json"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# ── Core Screening ──────────────────────────────────
|
|
35
|
+
|
|
36
|
+
def screen(entity, context: "onboarding", entity_type: nil, date_of_birth: nil, nationality: nil, id_number: nil)
|
|
37
|
+
payload = { entity: entity, context: context }
|
|
38
|
+
payload[:entity_type] = entity_type if entity_type
|
|
39
|
+
payload[:date_of_birth] = date_of_birth if date_of_birth
|
|
40
|
+
payload[:nationality] = nationality if nationality
|
|
41
|
+
payload[:id_number] = id_number if id_number
|
|
42
|
+
|
|
43
|
+
data = post("/v1/screen", payload)
|
|
44
|
+
parse_screen_response(data)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get_screening(screening_id)
|
|
48
|
+
data = get("/v1/screen/#{screening_id}")
|
|
49
|
+
parse_screen_response(data)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def list_screenings(limit: 50, offset: 0, status: nil)
|
|
53
|
+
params = { limit: limit, offset: offset }
|
|
54
|
+
params[:status] = status if status
|
|
55
|
+
data = get("/v1/screenings", params)
|
|
56
|
+
results = data.is_a?(Array) ? data : (data["results"] || [])
|
|
57
|
+
results.map { |r| parse_screen_response(r) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# ── Batch Screening ─────────────────────────────────
|
|
61
|
+
|
|
62
|
+
def batch_screen(entities)
|
|
63
|
+
data = post("/v1/screen/batch", { entities: entities })
|
|
64
|
+
BatchJob.new(**symbolize(data))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def get_batch_job(job_id)
|
|
68
|
+
data = get("/v1/screen/batch/#{job_id}")
|
|
69
|
+
BatchJob.new(**symbolize(data))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# ── Monitoring ──────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def add_monitor(entity, **kwargs)
|
|
75
|
+
post("/v1/monitor/entities", { entity: entity, **kwargs })
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def list_monitors
|
|
79
|
+
get("/v1/monitor/entities")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def remove_monitor(monitor_id)
|
|
83
|
+
delete("/v1/monitor/entities/#{monitor_id}")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# ── Watchlist Info ──────────────────────────────────
|
|
87
|
+
|
|
88
|
+
def list_watchlists
|
|
89
|
+
data = get("/v1/lists")
|
|
90
|
+
sources = data.is_a?(Array) ? data : (data["sources"] || [])
|
|
91
|
+
sources.map { |s| WatchlistSource.new(**symbolize(s)) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# ── API Keys ────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
def create_key(name)
|
|
97
|
+
post("/v1/keys", { name: name })
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def list_keys
|
|
101
|
+
get("/v1/keys")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def revoke_key(key_id)
|
|
105
|
+
delete("/v1/keys/#{key_id}")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# ── Webhooks ────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
def create_webhook(url, events: nil)
|
|
111
|
+
payload = { url: url }
|
|
112
|
+
payload[:events] = events if events
|
|
113
|
+
post("/v1/webhooks", payload)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def list_webhooks
|
|
117
|
+
get("/v1/webhooks")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def delete_webhook(webhook_id)
|
|
121
|
+
delete("/v1/webhooks/#{webhook_id}")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def sign_headers(method, path, body = "")
|
|
127
|
+
return {} unless @signing_secret
|
|
128
|
+
|
|
129
|
+
ts = (Time.now.to_f * 1000).to_i.to_s
|
|
130
|
+
body_hash = OpenSSL::Digest::SHA256.hexdigest(body)
|
|
131
|
+
payload = "#{ts}.#{method.upcase}.#{path}.#{body_hash}"
|
|
132
|
+
sig = OpenSSL::HMAC.hexdigest("SHA256", @signing_secret, payload)
|
|
133
|
+
{ "X-Vigil-Timestamp" => ts, "X-Vigil-Signature" => sig }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def handle_error(response)
|
|
137
|
+
case response.status
|
|
138
|
+
when 401 then raise AuthError
|
|
139
|
+
when 403 then raise IPNotAllowedError
|
|
140
|
+
when 404 then raise NotFoundError
|
|
141
|
+
when 429
|
|
142
|
+
retry_after = (response.headers["retry-after"] || "60").to_i
|
|
143
|
+
raise RateLimitError.new(retry_after: retry_after)
|
|
144
|
+
when 400..599
|
|
145
|
+
body = response.body
|
|
146
|
+
msg = body.is_a?(Hash) ? body.dig("error", "message") || body.to_s : body.to_s
|
|
147
|
+
code = body.is_a?(Hash) ? body.dig("error", "code") || "UNKNOWN" : "UNKNOWN"
|
|
148
|
+
raise Error.new(msg, status_code: response.status, code: code)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def get(path, params = nil)
|
|
153
|
+
resp = @conn.get(path, params) do |req|
|
|
154
|
+
sign_headers("GET", path).each { |k, v| req.headers[k] = v }
|
|
155
|
+
end
|
|
156
|
+
handle_error(resp) if resp.status >= 400
|
|
157
|
+
resp.body
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def post(path, payload)
|
|
161
|
+
body = payload.to_json
|
|
162
|
+
resp = @conn.post(path) do |req|
|
|
163
|
+
req.body = body
|
|
164
|
+
sign_headers("POST", path, body).each { |k, v| req.headers[k] = v }
|
|
165
|
+
end
|
|
166
|
+
handle_error(resp) if resp.status >= 400
|
|
167
|
+
resp.body
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def delete(path)
|
|
171
|
+
resp = @conn.delete(path) do |req|
|
|
172
|
+
sign_headers("DELETE", path).each { |k, v| req.headers[k] = v }
|
|
173
|
+
end
|
|
174
|
+
handle_error(resp) if resp.status >= 400
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def parse_screen_response(data)
|
|
178
|
+
data = symbolize(data)
|
|
179
|
+
data[:sanctions] = SanctionsResult.new(**symbolize(data[:sanctions])) if data[:sanctions].is_a?(Hash)
|
|
180
|
+
data[:pep] = PepResult.new(**symbolize(data[:pep])) if data[:pep].is_a?(Hash)
|
|
181
|
+
data[:matches] = (data[:matches] || []).map { |m| MatchDetail.new(**symbolize(m)) }
|
|
182
|
+
ScreenResponse.new(**data)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def symbolize(hash)
|
|
186
|
+
return hash unless hash.is_a?(Hash)
|
|
187
|
+
hash.transform_keys(&:to_sym)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
data/lib/vigil/errors.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vigil
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
attr_reader :status_code, :code
|
|
6
|
+
|
|
7
|
+
def initialize(message, status_code: 0, code: "UNKNOWN")
|
|
8
|
+
@status_code = status_code
|
|
9
|
+
@code = code
|
|
10
|
+
super("[#{code}] #{message}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class AuthError < Error
|
|
15
|
+
def initialize(message = "Invalid or missing API key")
|
|
16
|
+
super(message, status_code: 401, code: "UNAUTHORIZED")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class RateLimitError < Error
|
|
21
|
+
attr_reader :retry_after
|
|
22
|
+
|
|
23
|
+
def initialize(message = "Rate limit exceeded", retry_after: 60)
|
|
24
|
+
@retry_after = retry_after
|
|
25
|
+
super(message, status_code: 429, code: "RATE_LIMIT_EXCEEDED")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class NotFoundError < Error
|
|
30
|
+
def initialize(message = "Resource not found")
|
|
31
|
+
super(message, status_code: 404, code: "NOT_FOUND")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class IPNotAllowedError < Error
|
|
36
|
+
def initialize(message = "IP not allowed")
|
|
37
|
+
super(message, status_code: 403, code: "IP_NOT_ALLOWED")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/vigil/types.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vigil
|
|
4
|
+
# Immutable struct-like classes for API responses
|
|
5
|
+
ScreenResponse = Struct.new(
|
|
6
|
+
:id, :status, :risk_score, :risk_level,
|
|
7
|
+
:sanctions, :pep, :matches, :recommendation,
|
|
8
|
+
:screening_time_ms, :created_at, :narrative, :adverse_media,
|
|
9
|
+
keyword_init: true
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
SanctionsResult = Struct.new(:match, :lists_checked, :last_updated, keyword_init: true)
|
|
13
|
+
PepResult = Struct.new(:match, keyword_init: true)
|
|
14
|
+
|
|
15
|
+
MatchDetail = Struct.new(
|
|
16
|
+
:entity_id, :source, :source_entity_id, :matched_name,
|
|
17
|
+
:entity_type, :score, :programs, :aliases,
|
|
18
|
+
:date_of_birth, :nationality, :match_reasons,
|
|
19
|
+
keyword_init: true
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
BatchJob = Struct.new(:job_id, :status, :total, :completed, :results, keyword_init: true)
|
|
23
|
+
WatchlistSource = Struct.new(:code, :name, :entity_count, :last_ingested_at, :is_active, keyword_init: true)
|
|
24
|
+
end
|
data/lib/vigil.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: vigilhq
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Vigil
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-02-15 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: json
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
description: Screen entities against global sanctions, PEP lists, and trust networks
|
|
41
|
+
with one API call.
|
|
42
|
+
email: sales@vigilhq.dev
|
|
43
|
+
executables: []
|
|
44
|
+
extensions: []
|
|
45
|
+
extra_rdoc_files: []
|
|
46
|
+
files:
|
|
47
|
+
- lib/vigil.rb
|
|
48
|
+
- lib/vigil/client.rb
|
|
49
|
+
- lib/vigil/errors.rb
|
|
50
|
+
- lib/vigil/types.rb
|
|
51
|
+
homepage: https://vigilhq.dev
|
|
52
|
+
licenses:
|
|
53
|
+
- MIT
|
|
54
|
+
metadata:
|
|
55
|
+
homepage_uri: https://vigilhq.dev
|
|
56
|
+
source_code_uri: https://github.com/ymatagne/vigil
|
|
57
|
+
documentation_uri: https://vigilhq.dev/docs
|
|
58
|
+
rdoc_options: []
|
|
59
|
+
require_paths:
|
|
60
|
+
- lib
|
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: 3.0.0
|
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '0'
|
|
71
|
+
requirements: []
|
|
72
|
+
rubygems_version: 3.6.2
|
|
73
|
+
specification_version: 4
|
|
74
|
+
summary: Official Ruby SDK for the Vigil compliance screening API
|
|
75
|
+
test_files: []
|