velix-sdk 0.1.0.pre.alpha1
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 +29 -0
- data/README.md +170 -0
- data/lib/velix/client.rb +131 -0
- data/lib/velix/config.rb +9 -0
- data/lib/velix/error.rb +19 -0
- data/lib/velix/modules/checkin.rb +55 -0
- data/lib/velix/modules/events.rb +52 -0
- data/lib/velix/modules/lgpd.rb +20 -0
- data/lib/velix/modules/me.rb +27 -0
- data/lib/velix/modules/onboarding.rb +41 -0
- data/lib/velix/modules/time.rb +29 -0
- data/lib/velix/retry.rb +36 -0
- data/lib/velix.rb +9 -0
- metadata +60 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 40da25499d3ae666cd66289c246ee35de7f530b7531cf7a6c4573951dd44b10a
|
|
4
|
+
data.tar.gz: 15670d812a8dc1a27e8eaf53aa34619d93aee1b72c7f0a0936ef25550beb9ce3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 97aa391c0f9fbf9f8d46af5503c48f8fc0571b73deea523f63056f7dec0fe3111a989dcdff421b9a5b5841ef2f8f72292ee3735a3d588041106ffb716c39223d
|
|
7
|
+
data.tar.gz: d3006602956f427a885e59e378e4a93885ce4f3147ea15ac99267797182f256769260bc2e4c25bb718e5adb9298904a6fe497c76a7109a95207b17849f0186d3
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Todas as mudanças notáveis deste projeto são documentadas neste arquivo.
|
|
4
|
+
|
|
5
|
+
O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/1.0.0/)
|
|
6
|
+
e este projeto adere a [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Retry deixou de reenviar cegamente qualquer `5xx`. Requisições não
|
|
13
|
+
idempotentes (`POST`, `PATCH`, `PUT`, `DELETE`) só são retentadas quando o
|
|
14
|
+
status é `429` ou `503` (`RETRYABLE_STATUSES`), casos em que o servidor
|
|
15
|
+
garante que a requisição original não foi processada. Isso elimina o risco
|
|
16
|
+
de duplicar `checkin`/`enroll` em erro `500`.
|
|
17
|
+
- Erros de parse do corpo da resposta deixaram de ser engolidos
|
|
18
|
+
silenciosamente (`JSON.parse rescue {}`). Agora um corpo malformado
|
|
19
|
+
propaga um `Velix::VelixError` estruturado em vez de virar `{}` e esconder
|
|
20
|
+
a mensagem de erro original.
|
|
21
|
+
|
|
22
|
+
## [0.1.0.pre.alpha1] - pré-release
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Cliente HTTP baseado em `Net::HTTP` (stdlib, zero dependências de runtime).
|
|
27
|
+
- Autenticação via header `x-api-key`.
|
|
28
|
+
- Módulos `checkin`, `persons`, `events`, `tenants`.
|
|
29
|
+
- Retry com backoff exponencial para status retentáveis.
|
data/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# velix-sdk — Ruby SDK 
|
|
2
|
+
|
|
3
|
+
> ⚠️ **Alpha / pre-release.** This SDK targets the real API-key-protected surface of the
|
|
4
|
+
> VELIX backend (`/v1/api/*`, see `public-api.yaml` task #593). Only six endpoints exist
|
|
5
|
+
> today; everything else (Velix Time included) is intentionally not implemented. Do not
|
|
6
|
+
> use in production integrations yet.
|
|
7
|
+
|
|
8
|
+
Official Ruby SDK for the VELIX Biometrics platform — facial access control B2B SaaS.
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
- Ruby 3.1+
|
|
13
|
+
- Zero runtime dependencies (uses stdlib `net/http`)
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add to your `Gemfile`:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "velix-sdk", "~> 0.1.0.pre"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bundle install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or install directly:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
gem install velix-sdk
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require "velix"
|
|
39
|
+
|
|
40
|
+
client = Velix::Client.new(
|
|
41
|
+
api_url: ENV["VELIX_API_URL"],
|
|
42
|
+
api_key: ENV["VELIX_API_KEY"]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
result = client.checkin.identify(image_base64: frame_base64)
|
|
46
|
+
puts result.matched ? "GRANTED" : "DENIED"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Auth is sent as `x-api-key: vlx_<hex>` on every request (the API also accepts
|
|
50
|
+
`Authorization: Bearer vlx_<hex>` as an alternative, but the SDK always uses the
|
|
51
|
+
`x-api-key` header).
|
|
52
|
+
|
|
53
|
+
## Environment Variables
|
|
54
|
+
|
|
55
|
+
| Variable | Required | Description |
|
|
56
|
+
|----------|----------|-------------|
|
|
57
|
+
| `VELIX_API_URL` | Yes | API base URL (`https://api.velixbiometrics.com`) |
|
|
58
|
+
| `VELIX_API_KEY` | Yes | Integrator API key (`vlx_...`) |
|
|
59
|
+
|
|
60
|
+
## Modules
|
|
61
|
+
|
|
62
|
+
The API-key surface has exactly six real endpoints, one per method below. There is no
|
|
63
|
+
list/update/delete for persons, events, or tenants under `/v1/api/*` — do not expect
|
|
64
|
+
those methods.
|
|
65
|
+
|
|
66
|
+
| Module | Method | Endpoint | Scope |
|
|
67
|
+
|--------|--------|----------|-------|
|
|
68
|
+
| `client.onboarding` | `create()` | `POST /v1/api/onboarding` | `onboarding:write` |
|
|
69
|
+
| `client.checkin` | `identify()` | `POST /v1/api/checkin/identify` | `checkin:write` |
|
|
70
|
+
| `client.lgpd` | `create_deletion_request()` | `POST /v1/api/deletion-request` | `lgpd:write` |
|
|
71
|
+
| `client.me` | `find()` | `GET /v1/api/me/{personId}` | `me:read` |
|
|
72
|
+
| `client.events` | `create_guest()` | `POST /v1/api/events/{id}/guests` | `events:write` |
|
|
73
|
+
| `client.events` | `get_guest()` | `GET /v1/api/events/{id}/guests/{guestId}` | `events:read` |
|
|
74
|
+
|
|
75
|
+
`client.time` exists only as a stub that raises `NotImplementedError` — Velix Time has
|
|
76
|
+
no endpoint under `/v1/api/*` yet (see spec note "Velix Time — COBERTURA PARCIAL").
|
|
77
|
+
|
|
78
|
+
## Onboarding Module
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
result = client.onboarding.create(
|
|
82
|
+
name: "João Silva",
|
|
83
|
+
frames: [frame1_base64, frame2_base64, frame3_base64], # min 1, tenant-configured minimum
|
|
84
|
+
email: "joao@company.com",
|
|
85
|
+
external_id: "EMP-001" # upserts by external key when provided
|
|
86
|
+
)
|
|
87
|
+
# result.person_id => "uuid"
|
|
88
|
+
# result.identity_id => "uuid"
|
|
89
|
+
# result.enrolled => true
|
|
90
|
+
# result.frames_processed => 3
|
|
91
|
+
# result.frames_results => [...]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Checkin Module
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
result = client.checkin.identify(
|
|
98
|
+
image_base64: frame_base64,
|
|
99
|
+
top_k: 3,
|
|
100
|
+
liveness: {
|
|
101
|
+
token: challenge_token, # from GET /v1/public/checkin/{tenantSlug}/liveness/challenge
|
|
102
|
+
samples: [{ action: "center", image_base64: sample_base64 }]
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
# result.matched => true
|
|
106
|
+
# result.person_id => "uuid"
|
|
107
|
+
# result.quality_score => 0.92
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Liveness score is never returned by the API — only `matched`/`quality_score` are
|
|
111
|
+
exposed, by design.
|
|
112
|
+
|
|
113
|
+
## LGPD Module
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
result = client.lgpd.create_deletion_request(person_id: "uuid")
|
|
117
|
+
# result.protocol_number => "PROTO-123"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Me Module
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
person = client.me.find("uuid")
|
|
124
|
+
# person.id, person.name, person.email, person.phone, person.photo_url, person.created_at
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Events Module
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
guest = client.events.create_guest("event-uuid", name: "Ana", email: "ana@empresa.com")
|
|
131
|
+
# guest.id, guest.event_id, guest.status, guest.category_id
|
|
132
|
+
|
|
133
|
+
guest = client.events.get_guest("event-uuid", "guest-uuid")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Error Handling
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
begin
|
|
140
|
+
result = client.checkin.identify(image_base64: frame)
|
|
141
|
+
rescue Velix::AuthError
|
|
142
|
+
puts "Invalid API key"
|
|
143
|
+
rescue Velix::BiometricError => e
|
|
144
|
+
puts "Face not recognized: #{e.message}"
|
|
145
|
+
rescue Velix::RateLimitError => e
|
|
146
|
+
puts "Rate limit: #{e.message}"
|
|
147
|
+
rescue Velix::VelixError => e
|
|
148
|
+
puts "HTTP #{e.status}: #{e.message}"
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Running Tests
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
bundle exec rspec # all tests
|
|
156
|
+
bundle exec rspec spec/checkin_spec.rb # single file
|
|
157
|
+
bundle exec rspec --format documentation # verbose
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Local Development
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
git clone <repo>
|
|
164
|
+
bundle install
|
|
165
|
+
bundle exec rspec
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Get an API Key
|
|
169
|
+
|
|
170
|
+
Access the dashboard at **velixbiometrics.com** → Settings → API Keys → New Key.
|
data/lib/velix/client.rb
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
require_relative "config"
|
|
8
|
+
require_relative "error"
|
|
9
|
+
require_relative "retry"
|
|
10
|
+
require_relative "modules/onboarding"
|
|
11
|
+
require_relative "modules/checkin"
|
|
12
|
+
require_relative "modules/lgpd"
|
|
13
|
+
require_relative "modules/me"
|
|
14
|
+
require_relative "modules/events"
|
|
15
|
+
require_relative "modules/time"
|
|
16
|
+
|
|
17
|
+
module Velix
|
|
18
|
+
class Client
|
|
19
|
+
include Velix::Retry
|
|
20
|
+
|
|
21
|
+
USER_AGENT = "velix-ruby-sdk/0.1.0.pre.alpha1"
|
|
22
|
+
|
|
23
|
+
def initialize(api_url:, api_key:, timeout: 30, max_retries: 3)
|
|
24
|
+
@config = Config.new(api_url: api_url, api_key: api_key,
|
|
25
|
+
timeout: timeout, max_retries: max_retries)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def onboarding = @onboarding ||= Modules::Onboarding.new(self)
|
|
29
|
+
def checkin = @checkin ||= Modules::Checkin.new(self)
|
|
30
|
+
def lgpd = @lgpd ||= Modules::Lgpd.new(self)
|
|
31
|
+
def me = @me ||= Modules::Me.new(self)
|
|
32
|
+
def events = @events ||= Modules::Events.new(self)
|
|
33
|
+
|
|
34
|
+
# Velix Time has no endpoint under /v1/api/* yet — see modules/time.rb.
|
|
35
|
+
def time = @time ||= Modules::Time.new(self)
|
|
36
|
+
|
|
37
|
+
def get(path, params = {})
|
|
38
|
+
uri = build_uri(path, params)
|
|
39
|
+
request(Net::HTTP::Get.new(uri))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def post(path, body = {})
|
|
43
|
+
uri = build_uri(path)
|
|
44
|
+
req = Net::HTTP::Post.new(uri)
|
|
45
|
+
req.body = body.to_json
|
|
46
|
+
request(req)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def put(path, body = {})
|
|
50
|
+
uri = build_uri(path)
|
|
51
|
+
req = Net::HTTP::Put.new(uri)
|
|
52
|
+
req.body = body.to_json
|
|
53
|
+
request(req)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def patch(path, body = {})
|
|
57
|
+
uri = build_uri(path)
|
|
58
|
+
req = Net::HTTP::Patch.new(uri)
|
|
59
|
+
req.body = body.to_json
|
|
60
|
+
request(req)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def delete(path)
|
|
64
|
+
uri = build_uri(path)
|
|
65
|
+
request(Net::HTTP::Delete.new(uri))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def build_uri(path, params = {})
|
|
71
|
+
uri = URI.join(@config.api_url, path)
|
|
72
|
+
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
73
|
+
uri
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def request(req)
|
|
77
|
+
req["x-api-key"] = @config.api_key
|
|
78
|
+
req["Content-Type"] = "application/json"
|
|
79
|
+
req["Accept"] = "application/json"
|
|
80
|
+
req["User-Agent"] = USER_AGENT
|
|
81
|
+
|
|
82
|
+
with_retry(max_retries: @config.max_retries, method: req.method) do
|
|
83
|
+
uri = req.uri
|
|
84
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
85
|
+
http.use_ssl = uri.scheme == "https"
|
|
86
|
+
http.open_timeout = @config.timeout
|
|
87
|
+
http.read_timeout = @config.timeout
|
|
88
|
+
|
|
89
|
+
response = http.request(req)
|
|
90
|
+
handle_response(response)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def handle_response(response)
|
|
95
|
+
body = parse_body(response.body)
|
|
96
|
+
# Envelope de erro real: {"success":false,"error":{"code":"...","message":"..."}}.
|
|
97
|
+
# body["message"] no nível raiz não existe — ficava sempre no fallback
|
|
98
|
+
# genérico, mascarando a mensagem real da API.
|
|
99
|
+
message = body.is_a?(Hash) ? body.dig("error", "message") || body["message"] : nil
|
|
100
|
+
|
|
101
|
+
case response.code.to_i
|
|
102
|
+
when 200, 201, 204
|
|
103
|
+
body.is_a?(Hash) && body.key?("data") ? body["data"] : body
|
|
104
|
+
when 401, 403
|
|
105
|
+
raise AuthError.new(message || "Unauthorized", status: response.code.to_i)
|
|
106
|
+
when 404
|
|
107
|
+
raise NotFoundError.new(message || "Not found", status: 404)
|
|
108
|
+
when 422
|
|
109
|
+
raise BiometricError.new(message || "Unprocessable", status: 422)
|
|
110
|
+
when 429
|
|
111
|
+
raise RateLimitError.new(message || "Rate limit exceeded", status: 429)
|
|
112
|
+
when 500..599
|
|
113
|
+
raise ServerError.new(message || "Server error", status: response.code.to_i)
|
|
114
|
+
else
|
|
115
|
+
raise VelixError.new(message || "Unexpected error", status: response.code.to_i)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Parse errors are never swallowed: a malformed body on an error
|
|
120
|
+
# response must still surface as a structured VelixError instead of
|
|
121
|
+
# silently becoming an empty hash (which hides the real failure and
|
|
122
|
+
# nils out the error message).
|
|
123
|
+
def parse_body(raw)
|
|
124
|
+
return {} if raw.nil? || raw.empty?
|
|
125
|
+
|
|
126
|
+
JSON.parse(raw)
|
|
127
|
+
rescue JSON::ParserError => e
|
|
128
|
+
raise VelixError.new("Failed to parse response body: #{e.message}", status: nil, code: "invalid_json")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
data/lib/velix/config.rb
ADDED
data/lib/velix/error.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Velix
|
|
4
|
+
class VelixError < StandardError
|
|
5
|
+
attr_reader :status, :code
|
|
6
|
+
|
|
7
|
+
def initialize(message, status: nil, code: nil)
|
|
8
|
+
super(message)
|
|
9
|
+
@status = status
|
|
10
|
+
@code = code
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class AuthError < VelixError; end
|
|
15
|
+
class NotFoundError < VelixError; end
|
|
16
|
+
class RateLimitError < VelixError; end
|
|
17
|
+
class BiometricError < VelixError; end
|
|
18
|
+
class ServerError < VelixError; end
|
|
19
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Velix
|
|
4
|
+
module Modules
|
|
5
|
+
# POST /v1/api/checkin/identify — scope checkin:write
|
|
6
|
+
#
|
|
7
|
+
# Liveness score is never returned by the API by design (security rule) —
|
|
8
|
+
# this endpoint's response only ever reports `matched`, never a raw
|
|
9
|
+
# liveness/confidence score.
|
|
10
|
+
class Checkin
|
|
11
|
+
Result = Data.define(:matched, :person_id, :quality_score, :message)
|
|
12
|
+
|
|
13
|
+
def initialize(client)
|
|
14
|
+
@client = client
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# image_base64: main frame, required
|
|
18
|
+
# images: optional array of extra base64 frames
|
|
19
|
+
# top_k: optional integer 1..10
|
|
20
|
+
# liveness: optional { token:, samples: [{ action:, image_base64: }] }
|
|
21
|
+
# location: optional { latitude:, longitude:, accuracy: }
|
|
22
|
+
def identify(image_base64:, images: nil, top_k: nil, liveness: nil, location: nil)
|
|
23
|
+
body = { imageBase64: image_base64 }
|
|
24
|
+
body[:images] = images if images
|
|
25
|
+
body[:topK] = top_k if top_k
|
|
26
|
+
body[:liveness] = serialize_liveness(liveness) if liveness
|
|
27
|
+
body[:location] = location if location
|
|
28
|
+
|
|
29
|
+
resp = @client.post("/v1/api/checkin/identify", body)
|
|
30
|
+
|
|
31
|
+
Result.new(
|
|
32
|
+
matched: resp["matched"],
|
|
33
|
+
person_id: resp["person_id"],
|
|
34
|
+
quality_score: resp["quality_score"],
|
|
35
|
+
message: resp["message"]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Maps idiomatic Ruby keys (token:, samples: [{ action:, image_base64: }])
|
|
42
|
+
# onto the wire's literal camelCase field names (token, samples[].action,
|
|
43
|
+
# samples[].imageBase64) as defined by LivenessBlock/LivenessSample in
|
|
44
|
+
# the spec.
|
|
45
|
+
def serialize_liveness(liveness)
|
|
46
|
+
{
|
|
47
|
+
token: liveness[:token],
|
|
48
|
+
samples: Array(liveness[:samples]).map do |sample|
|
|
49
|
+
{ action: sample[:action], imageBase64: sample[:image_base64] || sample[:imageBase64] }
|
|
50
|
+
end
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Velix
|
|
4
|
+
module Modules
|
|
5
|
+
# /v1/api/events/{id}/guests — Velix Events guest endpoints.
|
|
6
|
+
#
|
|
7
|
+
# Only two operations exist in the real API: creating a guest
|
|
8
|
+
# (events:write) and reading one back (events:read). There is no
|
|
9
|
+
# list/find/create/update/delete-event or update-config endpoint under
|
|
10
|
+
# the API-key surface — do not add methods for those without a
|
|
11
|
+
# corresponding spec entry.
|
|
12
|
+
class Events
|
|
13
|
+
Guest = Data.define(:id, :event_id, :name, :email, :status, :category_id)
|
|
14
|
+
|
|
15
|
+
def initialize(client)
|
|
16
|
+
@client = client
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# cpf, phone, birth_date, category_id, companion_of are optional.
|
|
20
|
+
def create_guest(event_id, name:, email:, cpf: nil, phone: nil, birth_date: nil,
|
|
21
|
+
category_id: nil, companion_of: nil)
|
|
22
|
+
body = { name: name, email: email }
|
|
23
|
+
body[:cpf] = cpf if cpf
|
|
24
|
+
body[:phone] = phone if phone
|
|
25
|
+
body[:birthDate] = birth_date if birth_date
|
|
26
|
+
body[:categoryId] = category_id if category_id
|
|
27
|
+
body[:companionOf] = companion_of if companion_of
|
|
28
|
+
|
|
29
|
+
resp = @client.post("/v1/api/events/#{event_id}/guests", body)
|
|
30
|
+
build_guest(resp)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get_guest(event_id, guest_id)
|
|
34
|
+
resp = @client.get("/v1/api/events/#{event_id}/guests/#{guest_id}")
|
|
35
|
+
build_guest(resp)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def build_guest(resp)
|
|
41
|
+
Guest.new(
|
|
42
|
+
id: resp["id"],
|
|
43
|
+
event_id: resp["eventId"],
|
|
44
|
+
name: resp["name"],
|
|
45
|
+
email: resp["email"],
|
|
46
|
+
status: resp["status"],
|
|
47
|
+
category_id: resp["categoryId"]
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Velix
|
|
4
|
+
module Modules
|
|
5
|
+
# POST /v1/api/deletion-request — scope lgpd:write
|
|
6
|
+
class Lgpd
|
|
7
|
+
Result = Data.define(:protocol_number, :message)
|
|
8
|
+
|
|
9
|
+
def initialize(client)
|
|
10
|
+
@client = client
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create_deletion_request(person_id:)
|
|
14
|
+
resp = @client.post("/v1/api/deletion-request", { person_id: person_id })
|
|
15
|
+
|
|
16
|
+
Result.new(protocol_number: resp["protocol_number"], message: resp["message"])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Velix
|
|
4
|
+
module Modules
|
|
5
|
+
# GET /v1/api/me/{personId} — scope me:read
|
|
6
|
+
class Me
|
|
7
|
+
Result = Data.define(:id, :name, :email, :phone, :photo_url, :created_at)
|
|
8
|
+
|
|
9
|
+
def initialize(client)
|
|
10
|
+
@client = client
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def find(person_id)
|
|
14
|
+
resp = @client.get("/v1/api/me/#{person_id}")
|
|
15
|
+
|
|
16
|
+
Result.new(
|
|
17
|
+
id: resp["id"],
|
|
18
|
+
name: resp["name"],
|
|
19
|
+
email: resp["email"],
|
|
20
|
+
phone: resp["phone"],
|
|
21
|
+
photo_url: resp["photo_url"],
|
|
22
|
+
created_at: resp["created_at"]
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Velix
|
|
4
|
+
module Modules
|
|
5
|
+
# POST /v1/api/onboarding — scope onboarding:write
|
|
6
|
+
class Onboarding
|
|
7
|
+
Result = Data.define(:person_id, :identity_id, :enrolled, :frames_processed,
|
|
8
|
+
:frames_results, :embedding_id, :message)
|
|
9
|
+
|
|
10
|
+
def initialize(client)
|
|
11
|
+
@client = client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# frames: array of base64 jpeg strings (no data URI prefix), min 1
|
|
15
|
+
def create(name:, frames:, email: nil, phone: nil, document: nil, document_type: nil,
|
|
16
|
+
external_id: nil, metadata: nil, role: nil, access_groups: nil)
|
|
17
|
+
body = { name: name, frames: frames }
|
|
18
|
+
body[:email] = email if email
|
|
19
|
+
body[:phone] = phone if phone
|
|
20
|
+
body[:document] = document if document
|
|
21
|
+
body[:document_type] = document_type if document_type
|
|
22
|
+
body[:external_id] = external_id if external_id
|
|
23
|
+
body[:metadata] = metadata if metadata
|
|
24
|
+
body[:role] = role if role
|
|
25
|
+
body[:access_groups] = access_groups if access_groups
|
|
26
|
+
|
|
27
|
+
resp = @client.post("/v1/api/onboarding", body)
|
|
28
|
+
|
|
29
|
+
Result.new(
|
|
30
|
+
person_id: resp["person_id"],
|
|
31
|
+
identity_id: resp["identity_id"],
|
|
32
|
+
enrolled: resp["enrolled"],
|
|
33
|
+
frames_processed: resp["frames_processed"],
|
|
34
|
+
frames_results: resp["frames_results"],
|
|
35
|
+
embedding_id: resp["embedding_id"],
|
|
36
|
+
message: resp["message"]
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Velix
|
|
4
|
+
module Modules
|
|
5
|
+
# Velix Time (ponto/punch) has NO endpoint exposed under the API-key
|
|
6
|
+
# surface (`/v1/api/*`) as of the current public-api.yaml spec. The
|
|
7
|
+
# `time:read`/`time:write` scopes exist on ApplicationApiKey but no
|
|
8
|
+
# controller is mounted for them yet (see spec note "Velix Time —
|
|
9
|
+
# COBERTURA PARCIAL"). Do not implement calls against guessed endpoints.
|
|
10
|
+
#
|
|
11
|
+
# This stub exists only so that any accidental `client.time` call fails
|
|
12
|
+
# loudly instead of silently hitting a nonexistent/wrong endpoint.
|
|
13
|
+
class Time
|
|
14
|
+
def initialize(client)
|
|
15
|
+
@client = client
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def method_missing(name, *)
|
|
19
|
+
raise NotImplementedError,
|
|
20
|
+
"Velix::Modules::Time##{name} is not implemented — Velix Time has no endpoint " \
|
|
21
|
+
"under /v1/api/* yet (see public-api.yaml, task #616 follow-up)."
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def respond_to_missing?(*)
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/velix/retry.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Velix
|
|
4
|
+
module Retry
|
|
5
|
+
RETRYABLE_STATUSES = [429, 503].freeze
|
|
6
|
+
IDEMPOTENT_METHODS = %w[GET].freeze
|
|
7
|
+
|
|
8
|
+
# Retries are only safe for:
|
|
9
|
+
# - idempotent HTTP methods (GET), regardless of status
|
|
10
|
+
# - RETRYABLE_STATUSES (429/503), where the server guarantees the
|
|
11
|
+
# request was not processed, even for non-idempotent methods (POST)
|
|
12
|
+
#
|
|
13
|
+
# Any other 5xx on a non-idempotent method (POST/PATCH/PUT/DELETE) is
|
|
14
|
+
# raised immediately — retrying could duplicate a checkin/enroll.
|
|
15
|
+
def with_retry(max_retries:, method: "GET")
|
|
16
|
+
attempts = 0
|
|
17
|
+
begin
|
|
18
|
+
attempts += 1
|
|
19
|
+
yield
|
|
20
|
+
rescue RateLimitError, ServerError => e
|
|
21
|
+
raise unless retryable?(method, e.status)
|
|
22
|
+
raise if attempts >= max_retries
|
|
23
|
+
|
|
24
|
+
wait = (2**(attempts - 1)) + rand
|
|
25
|
+
sleep(wait)
|
|
26
|
+
retry
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def retryable?(method, status)
|
|
33
|
+
IDEMPOTENT_METHODS.include?(method.to_s.upcase) || RETRYABLE_STATUSES.include?(status)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/velix.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: velix-sdk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.pre.alpha1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Velix Biometrics
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-03 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Integre controle de acesso biométrico VELIX em aplicações Ruby.
|
|
14
|
+
email:
|
|
15
|
+
- dev@velixbiometrics.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/velix.rb
|
|
23
|
+
- lib/velix/client.rb
|
|
24
|
+
- lib/velix/config.rb
|
|
25
|
+
- lib/velix/error.rb
|
|
26
|
+
- lib/velix/modules/checkin.rb
|
|
27
|
+
- lib/velix/modules/events.rb
|
|
28
|
+
- lib/velix/modules/lgpd.rb
|
|
29
|
+
- lib/velix/modules/me.rb
|
|
30
|
+
- lib/velix/modules/onboarding.rb
|
|
31
|
+
- lib/velix/modules/time.rb
|
|
32
|
+
- lib/velix/retry.rb
|
|
33
|
+
homepage: https://velixbiometrics.com
|
|
34
|
+
licenses:
|
|
35
|
+
- MIT
|
|
36
|
+
metadata:
|
|
37
|
+
homepage_uri: https://velixbiometrics.com
|
|
38
|
+
source_code_uri: https://github.com/VELIX-Biometrics/sdk-velix-ruby
|
|
39
|
+
changelog_uri: https://github.com/VELIX-Biometrics/sdk-velix-ruby/blob/main/CHANGELOG.md
|
|
40
|
+
rubygems_mfa_required: 'true'
|
|
41
|
+
post_install_message:
|
|
42
|
+
rdoc_options: []
|
|
43
|
+
require_paths:
|
|
44
|
+
- lib
|
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: 3.1.0
|
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 1.3.1
|
|
55
|
+
requirements: []
|
|
56
|
+
rubygems_version: 3.0.3.1
|
|
57
|
+
signing_key:
|
|
58
|
+
specification_version: 4
|
|
59
|
+
summary: SDK oficial do VELIX para Ruby
|
|
60
|
+
test_files: []
|