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 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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "vigil/client"
4
+ require_relative "vigil/types"
5
+ require_relative "vigil/errors"
6
+
7
+ module Vigil
8
+ VERSION = "0.1.0"
9
+ end
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: []