certynix 1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 206f0b9118f06cfe87e16926a83cca66fae4186fd19bf87926e4fdb54b412db9
4
+ data.tar.gz: a48a96dbfcb73d9ac8e23cc4a1a85da97ff9a44bb3742cd942ca4529da7e2aff
5
+ SHA512:
6
+ metadata.gz: 9c1d9204cc01e24bb2a46d1a9dd7f4dafa501264988bf0265da3d6dff68e9e947480654efa5540c316bb7ed077d99c80a3997aa04ffd968ca4b375e28fc6168b
7
+ data.tar.gz: 3b773469de280162e98b3d8e01d949a024ac39c43d35b2a24fb617060c7122c88676a57a4ab2fe124f87a61022576c153c0288071ffd7162c10ae03c0a7ce101
data/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## [1.0.0] - 2026-03-15
4
+
5
+ ### Added
6
+ - Initial release of `certynix` gem
7
+ - Ruby 3.1+ with keyword arguments, `Data.define` patterns
8
+ - `Assets`: `register`, `register_batch`, `get`, `list` (Enumerable/lazy), `delete`
9
+ - `Verify`: `by_hash`, `by_asset_id`, `by_hash_post` (public, no auth)
10
+ - `Webhooks`: `create`, `list`, `update`, `delete`, `list_deliveries`
11
+ - `Alerts`: `list`
12
+ - `ApiKeys`: `create`, `list`, `revoke`
13
+ - `AuditLogs`: `list`
14
+ - `TrustScore`: `get`
15
+ - `Certynix::Webhooks.validate_signature` — uses `OpenSSL.secure_compare` (constant-time)
16
+ - Anti-replay protection (5-minute timestamp tolerance)
17
+ - `Paginator` with `Enumerable` — supports `.lazy`, `.first(n)`, `.select`, `.map`
18
+ - Faraday-based HTTP with automatic retry (faraday-retry)
19
+ - Automatic sandbox detection via `cnx_test_sk_` prefix
20
+ - API key never exposed in error messages — `mask_api_key` helper
data/README.md ADDED
@@ -0,0 +1,316 @@
1
+ # certynix
2
+
3
+ Official Ruby SDK for the [Certynix](https://certynix.com) Trust Infrastructure API.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'certynix'
11
+ ```
12
+
13
+ Then run:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ Or install directly:
20
+
21
+ ```bash
22
+ gem install certynix
23
+ ```
24
+
25
+ Requires **Ruby 3.1+**.
26
+
27
+ ## Quick Start
28
+
29
+ ```ruby
30
+ require 'certynix'
31
+
32
+ client = Certynix::Client.new(ENV['CERTYNIX_API_KEY']) # cnx_live_sk_... or cnx_test_sk_...
33
+
34
+ # Register an asset by SHA-256 hash
35
+ asset = client.assets.register(
36
+ hash_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
37
+ filename: 'contract-2024.pdf'
38
+ )
39
+ puts "#{asset.id} — #{asset.status}"
40
+ puts asset.is_first_registrant ? 'First registrant!' : 'Already registered'
41
+
42
+ # Public verification (no auth required)
43
+ result = client.verify.by_hash(asset.hash)
44
+ puts result.match ? 'Verified' : 'Not found'
45
+ puts result.first_registrant.organization_name
46
+ ```
47
+
48
+ ## Authentication
49
+
50
+ ```ruby
51
+ # Production
52
+ client = Certynix::Client.new('cnx_live_sk_...')
53
+
54
+ # Sandbox (auto-detected from key prefix cnx_test_sk_)
55
+ client = Certynix::Client.new('cnx_test_sk_...')
56
+ # Automatically uses https://sandbox.certynix.com
57
+
58
+ # Custom options
59
+ client = Certynix::Client.new(
60
+ 'cnx_live_sk_...',
61
+ base_url: 'https://api.staging.certynix.com',
62
+ timeout: 30,
63
+ max_retries: 3
64
+ )
65
+ ```
66
+
67
+ ## Resources
68
+
69
+ ### Assets
70
+
71
+ ```ruby
72
+ # Register by hash
73
+ asset = client.assets.register(
74
+ hash_sha256: 'abc123...',
75
+ filename: 'document.pdf',
76
+ source_url: 'https://example.com/document.pdf',
77
+ metadata: { author: 'John Doe' }
78
+ )
79
+
80
+ # Register by URL (Certynix downloads and hashes)
81
+ asset = client.assets.register(
82
+ source_url: 'https://example.com/document.pdf',
83
+ filename: 'document.pdf'
84
+ )
85
+
86
+ # Register batch (up to 1,000 assets)
87
+ batch = client.assets.register_batch(
88
+ assets: [
89
+ { hash_sha256: 'abc...', filename: 'file1.pdf' },
90
+ { hash_sha256: 'def...', filename: 'file2.pdf' }
91
+ ]
92
+ )
93
+ puts "#{batch.batch_id} — #{batch.status}"
94
+
95
+ # Get by ID
96
+ asset = client.assets.get('ast_abc123')
97
+
98
+ # List (Enumerable — all pages iterated automatically)
99
+ client.assets.list(limit: 50).each do |asset|
100
+ puts "#{asset.id} — #{asset.status}"
101
+ end
102
+
103
+ # List with filters
104
+ verified = client.assets.list(status: 'verified').to_a
105
+
106
+ # Lazy with limit
107
+ first_10 = client.assets.list.lazy.first(10)
108
+
109
+ # Delete (soft delete — history preserved)
110
+ client.assets.delete('ast_abc123')
111
+ ```
112
+
113
+ ### Verification (public — no API key required)
114
+
115
+ ```ruby
116
+ # Verify by SHA-256 hash
117
+ result = client.verify.by_hash(
118
+ 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
119
+ )
120
+ puts result.match ? 'Verified' : 'Not found'
121
+ puts result.first_registrant.organization_name
122
+
123
+ # Verify by asset ID
124
+ result = client.verify.by_asset_id('ast_abc123')
125
+
126
+ # Verify by URL
127
+ result = client.verify.by_url('https://example.com/document.pdf')
128
+ ```
129
+
130
+ ### Webhooks
131
+
132
+ ```ruby
133
+ # Create webhook
134
+ webhook = client.webhooks.create(
135
+ url: 'https://example.com/webhook',
136
+ events: ['asset.created', 'asset.verified', 'exposure.alert.created']
137
+ )
138
+ # Store webhook.signing_secret securely — shown only once!
139
+
140
+ # List
141
+ client.webhooks.list.each do |webhook|
142
+ puts "#{webhook.id} — #{webhook.url}"
143
+ end
144
+
145
+ # Update
146
+ webhook = client.webhooks.update('wh_abc123', active: false)
147
+
148
+ # Delete
149
+ client.webhooks.delete('wh_abc123')
150
+
151
+ # Validate signature (in your webhook handler)
152
+ begin
153
+ event = Certynix::Webhooks.validate_signature(
154
+ raw_body: request.body.read,
155
+ signature: request.headers['X-Certynix-Signature'],
156
+ secret: ENV['CERTYNIX_WEBHOOK_SECRET']
157
+ )
158
+ puts event.type
159
+ puts event.payload.inspect
160
+ rescue Certynix::WebhookSignatureError => e
161
+ render plain: 'Invalid signature', status: :bad_request
162
+ rescue Certynix::WebhookReplayError => e
163
+ render plain: 'Replay attack detected', status: :bad_request
164
+ end
165
+ ```
166
+
167
+ ### Exposure Alerts
168
+
169
+ ```ruby
170
+ # List active alerts
171
+ client.alerts.list(resolved: false).each do |alert|
172
+ puts "[#{alert.severity}] #{alert.description}"
173
+ end
174
+ ```
175
+
176
+ ### API Keys
177
+
178
+ ```ruby
179
+ # Create — value shown only once!
180
+ key = client.api_keys.create(name: 'Production App')
181
+ puts key.key_value # cnx_live_sk_... — store immediately!
182
+
183
+ # List
184
+ client.api_keys.list.each do |key|
185
+ puts "#{key.name} — #{key.prefix}"
186
+ end
187
+
188
+ # Revoke
189
+ client.api_keys.revoke('key_abc123')
190
+ ```
191
+
192
+ ### Audit Logs
193
+
194
+ ```ruby
195
+ client.audit_logs.list(limit: 50).each do |log|
196
+ puts "#{log.action} by #{log.actor_id} at #{log.created_at}"
197
+ end
198
+
199
+ # Filter by action
200
+ client.audit_logs.list(action: 'asset.created').each do |log|
201
+ puts log.resource_id
202
+ end
203
+ ```
204
+
205
+ ### Trust Score
206
+
207
+ ```ruby
208
+ score = client.trust_score.get
209
+ puts "Score: #{score.score}/100"
210
+ puts "Identity: #{score.components.identity}"
211
+ puts "Security: #{score.components.security}"
212
+ puts "Behavior: #{score.components.behavior}"
213
+ puts "Assets: #{score.components.assets}"
214
+
215
+ score.penalties.each do |penalty|
216
+ puts "Penalty: #{penalty.description} (-#{penalty.points})"
217
+ end
218
+ ```
219
+
220
+ ## Error Handling
221
+
222
+ ```ruby
223
+ require 'certynix'
224
+
225
+ begin
226
+ asset = client.assets.get('ast_notfound')
227
+ rescue Certynix::NotFoundError => e
228
+ puts "Not found: #{e.message}"
229
+ puts "Code: #{e.code}"
230
+ puts "Request ID: #{e.request_id}"
231
+ rescue Certynix::RateLimitError => e
232
+ puts "Rate limited. Retry after: #{e.retry_after}s"
233
+ rescue Certynix::AuthenticationError => e
234
+ puts "Invalid API key"
235
+ rescue Certynix::ServerError => e
236
+ puts "Server error: #{e.message}"
237
+ rescue Certynix::NetworkError => e
238
+ puts "Network error: #{e.message}"
239
+ end
240
+ ```
241
+
242
+ ## Pagination
243
+
244
+ All `list` methods return a `Paginator` that includes Ruby's `Enumerable`:
245
+
246
+ ```ruby
247
+ # Iterate all pages automatically
248
+ client.assets.list(limit: 100).each do |asset|
249
+ puts asset.id
250
+ end
251
+
252
+ # Collect all into array
253
+ assets = client.assets.list.to_a
254
+
255
+ # Lazy enumeration (stops fetching when you stop iterating)
256
+ first_10 = client.assets.list.lazy.first(10)
257
+
258
+ # Map, select, reduce
259
+ verified_ids = client.assets.list
260
+ .select { |a| a.status == 'verified' }
261
+ .map(&:id)
262
+ ```
263
+
264
+ ## Retry Policy
265
+
266
+ The SDK automatically retries on transient errors (via `faraday-retry`):
267
+
268
+ | Scenario | Retried |
269
+ |---|---|
270
+ | `429 Too Many Requests` | Yes — respects `Retry-After` |
271
+ | `500`, `502`, `503`, `504` | Yes — exponential backoff + jitter |
272
+ | Network errors | Yes |
273
+ | `400`, `401`, `403`, `404`, `409` | No |
274
+
275
+ Default: 3 retries, max 60s backoff.
276
+
277
+ ## Webhook Signature Validation
278
+
279
+ ```ruby
280
+ # Rails example
281
+ class WebhooksController < ApplicationController
282
+ skip_before_action :verify_authenticity_token
283
+
284
+ def certynix
285
+ event = Certynix::Webhooks.validate_signature(
286
+ raw_body: request.body.read,
287
+ signature: request.headers['X-Certynix-Signature'],
288
+ secret: Rails.application.credentials.certynix_webhook_secret
289
+ )
290
+
291
+ case event.type
292
+ when 'asset.created'
293
+ AssetCreatedJob.perform_later(event.payload)
294
+ when 'asset.verified'
295
+ AssetVerifiedJob.perform_later(event.payload)
296
+ when 'exposure.alert.created'
297
+ AlertCreatedJob.perform_later(event.payload)
298
+ end
299
+
300
+ head :ok
301
+ rescue Certynix::WebhookSignatureError, Certynix::WebhookReplayError
302
+ head :bad_request
303
+ end
304
+ end
305
+ ```
306
+
307
+ ## Testing
308
+
309
+ ```bash
310
+ bundle install
311
+ bundle exec rspec
312
+ ```
313
+
314
+ ## License
315
+
316
+ MIT
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Certynix
4
+ class Client
5
+ attr_reader :assets, :verify, :webhooks, :api_keys, :alerts, :audit_logs, :trust_score
6
+
7
+ def initialize(api_key:, base_url: nil, timeout: 30, max_retries: 3, access_token: nil)
8
+ config = Config.new(
9
+ api_key: api_key,
10
+ base_url: base_url,
11
+ timeout: timeout,
12
+ max_retries: max_retries,
13
+ access_token: access_token,
14
+ )
15
+
16
+ http = HttpClient.new(config)
17
+
18
+ @assets = Resources::Assets.new(http)
19
+ @verify = Resources::Verify.new(http)
20
+ @webhooks = Resources::Webhooks.new(http)
21
+ @api_keys = Resources::ApiKeys.new(http)
22
+ @alerts = Resources::Alerts.new(http)
23
+ @audit_logs = Resources::AuditLogs.new(http)
24
+ @trust_score = Resources::TrustScore.new(http)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Certynix
4
+ class Config
5
+ API_KEY_REGEX = /\Acnx_(live|test)_sk_[a-zA-Z0-9]{32,}\z/
6
+ PRODUCTION_URL = 'https://api.certynix.com'
7
+ SANDBOX_URL = 'https://sandbox.certynix.com'
8
+
9
+ attr_reader :api_key, :base_url, :timeout, :max_retries, :access_token, :sandbox
10
+
11
+ def initialize(api_key:, base_url: nil, timeout: 30, max_retries: 3, access_token: nil)
12
+ raise ConfigurationError, 'api_key is required' if api_key.nil? || api_key.empty?
13
+ unless api_key.match?(API_KEY_REGEX)
14
+ # NUNCA incluir a API key na mensagem de erro
15
+ raise ConfigurationError, 'Invalid API key format. Expected cnx_live_sk_... or cnx_test_sk_...'
16
+ end
17
+
18
+ @api_key = api_key
19
+ @sandbox = api_key.start_with?('cnx_test_')
20
+ @base_url = base_url || (@sandbox ? SANDBOX_URL : PRODUCTION_URL)
21
+ @timeout = timeout
22
+ @max_retries = max_retries
23
+ @access_token = access_token
24
+ end
25
+
26
+ alias sandbox? sandbox
27
+
28
+ # Mascara a API key para logs — nunca expõe o valor completo
29
+ def mask_api_key
30
+ return '***' if api_key.length <= 12
31
+ "#{api_key[0..11]}***"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Certynix
4
+ # Erro base para todos os erros do SDK Certynix.
5
+ class Error < StandardError
6
+ attr_reader :code, :request_id, :status_code
7
+
8
+ def initialize(message:, code:, request_id: nil, status_code: 0)
9
+ super(message)
10
+ @code = code
11
+ @request_id = request_id
12
+ @status_code = status_code
13
+ end
14
+ end
15
+
16
+ # API key inválida, configuração incorreta
17
+ class ConfigurationError < Error
18
+ def initialize(message)
19
+ super(message: message, code: 'CONFIGURATION_ERROR', status_code: 0)
20
+ end
21
+ end
22
+
23
+ # HTTP 401
24
+ class AuthenticationError < Error
25
+ def initialize(message:, code:, request_id: nil)
26
+ super(message: message, code: code, request_id: request_id, status_code: 401)
27
+ end
28
+ end
29
+
30
+ # HTTP 403
31
+ class PermissionError < Error
32
+ def initialize(message:, code:, request_id: nil)
33
+ super(message: message, code: code, request_id: request_id, status_code: 403)
34
+ end
35
+ end
36
+
37
+ # HTTP 404
38
+ class NotFoundError < Error
39
+ def initialize(message:, code:, request_id: nil)
40
+ super(message: message, code: code, request_id: request_id, status_code: 404)
41
+ end
42
+ end
43
+
44
+ # HTTP 409
45
+ class ConflictError < Error
46
+ def initialize(message:, code:, request_id: nil)
47
+ super(message: message, code: code, request_id: request_id, status_code: 409)
48
+ end
49
+ end
50
+
51
+ # HTTP 400
52
+ class ValidationError < Error
53
+ def initialize(message:, code:, request_id: nil)
54
+ super(message: message, code: code, request_id: request_id, status_code: 400)
55
+ end
56
+ end
57
+
58
+ # HTTP 429
59
+ class RateLimitError < Error
60
+ attr_reader :retry_after
61
+
62
+ def initialize(message:, code:, request_id: nil, retry_after: 0)
63
+ super(message: message, code: code, request_id: request_id, status_code: 429)
64
+ @retry_after = retry_after
65
+ end
66
+ end
67
+
68
+ # HTTP 5xx
69
+ class ServerError < Error
70
+ def initialize(message:, code:, request_id: nil, status_code: 500)
71
+ super(message: message, code: code, request_id: request_id, status_code: status_code)
72
+ end
73
+ end
74
+
75
+ # Timeout, DNS, conexão recusada
76
+ class NetworkError < Error
77
+ def initialize(message, cause: nil)
78
+ super(message: message, code: 'NETWORK_ERROR', status_code: 0)
79
+ @cause = cause
80
+ end
81
+ end
82
+
83
+ # HMAC inválido
84
+ class WebhookSignatureError < Error
85
+ def initialize(message)
86
+ super(message: message, code: 'INVALID_WEBHOOK_SIGNATURE', status_code: 0)
87
+ end
88
+ end
89
+
90
+ # Timestamp fora do prazo
91
+ class WebhookReplayError < Error
92
+ def initialize(message)
93
+ super(message: message, code: 'WEBHOOK_REPLAY_ATTACK', status_code: 0)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday/retry'
5
+ require 'securerandom'
6
+ require 'json'
7
+
8
+ module Certynix
9
+ class HttpClient
10
+ SDK_VERSION = Certynix::VERSION
11
+
12
+ def initialize(config)
13
+ @config = config
14
+ @conn = build_connection
15
+ end
16
+
17
+ def get(path, params = {})
18
+ request(:get, path, params: compact_params(params))
19
+ end
20
+
21
+ def post(path, body = nil)
22
+ request(:post, path, body: body)
23
+ end
24
+
25
+ def put(path, body = nil)
26
+ request(:put, path, body: body)
27
+ end
28
+
29
+ def delete(path)
30
+ request(:delete, path)
31
+ nil
32
+ end
33
+
34
+ # GET sem autenticação — para endpoints públicos (verify)
35
+ def get_public(path, params = {})
36
+ request(:get, path, params: compact_params(params), public: true)
37
+ end
38
+
39
+ def post_public(path, body = nil)
40
+ request(:post, path, body: body, public: true)
41
+ end
42
+
43
+ private
44
+
45
+ def build_connection
46
+ Faraday.new(url: @config.base_url) do |f|
47
+ f.options.timeout = @config.timeout
48
+ f.options.open_timeout = 10
49
+
50
+ f.request :retry,
51
+ max: @config.max_retries,
52
+ interval: 1.0,
53
+ interval_randomness: 0.5,
54
+ backoff_factor: 2,
55
+ exceptions: [Faraday::TimeoutError, Faraday::ConnectionFailed],
56
+ retry_statuses: [429, 500, 502, 503, 504]
57
+
58
+ f.request :json
59
+ f.response :raise_error
60
+ f.adapter Faraday.default_adapter
61
+ end
62
+ end
63
+
64
+ def request(method, path, params: {}, body: nil, public: false)
65
+ headers = build_headers(public_request: public)
66
+ request_id = headers['X-Request-ID']
67
+
68
+ response = @conn.public_send(method, path) do |req|
69
+ req.headers.merge!(headers)
70
+ req.params.merge!(params) if params.any?
71
+ req.body = body.to_json if body && %i[post put patch].include?(method)
72
+ end
73
+
74
+ parse_response(response.body)
75
+ rescue Faraday::ClientError => e
76
+ handle_error(e, request_id)
77
+ rescue Faraday::ServerError => e
78
+ handle_error(e, request_id)
79
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
80
+ raise NetworkError.new(e.message, cause: e)
81
+ end
82
+
83
+ def handle_error(err, request_id)
84
+ status = err.response&.dig(:status) || 0
85
+ body = err.response&.dig(:body) || '{}'
86
+
87
+ begin
88
+ parsed = JSON.parse(body, symbolize_names: true)
89
+ code = parsed.dig(:error, :code) || 'INTERNAL_ERROR'
90
+ message = parsed.dig(:error, :message) || "HTTP #{status}"
91
+ req_id = parsed.dig(:error, :request_id) || request_id
92
+ rescue JSON::ParserError
93
+ code = 'INTERNAL_ERROR'
94
+ message = "HTTP #{status}"
95
+ req_id = request_id
96
+ end
97
+
98
+ retry_after = err.response&.dig(:headers, 'retry-after').to_i
99
+
100
+ raise map_error(status, code, message, req_id, retry_after)
101
+ end
102
+
103
+ def map_error(status, code, message, request_id, retry_after = 0)
104
+ case status
105
+ when 400 then ValidationError.new(message: message, code: code, request_id: request_id)
106
+ when 401 then AuthenticationError.new(message: message, code: code, request_id: request_id)
107
+ when 403 then PermissionError.new(message: message, code: code, request_id: request_id)
108
+ when 404 then NotFoundError.new(message: message, code: code, request_id: request_id)
109
+ when 409 then ConflictError.new(message: message, code: code, request_id: request_id)
110
+ when 429 then RateLimitError.new(message: message, code: code, request_id: request_id, retry_after: retry_after)
111
+ else ServerError.new(message: message, code: code, request_id: request_id, status_code: status)
112
+ end
113
+ end
114
+
115
+ def build_headers(public_request: false)
116
+ headers = {
117
+ 'Accept' => 'application/json',
118
+ 'Content-Type' => 'application/json',
119
+ 'User-Agent' => "certynix-ruby/#{SDK_VERSION} (ruby/#{RUBY_VERSION})",
120
+ 'X-Request-ID' => SecureRandom.uuid,
121
+ }
122
+ headers['x-api-key'] = @config.api_key unless public_request
123
+ if !public_request && @config.access_token
124
+ headers['Authorization'] = "Bearer #{@config.access_token}"
125
+ end
126
+ headers
127
+ end
128
+
129
+ def compact_params(params)
130
+ params.reject { |_, v| v.nil? }
131
+ end
132
+
133
+ def parse_response(body)
134
+ return {} if body.nil? || body.empty?
135
+ JSON.parse(body, symbolize_names: true)
136
+ rescue JSON::ParserError
137
+ {}
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Certynix
4
+ module Models
5
+ # Paginador automático com suporte completo ao módulo Enumerable.
6
+ #
7
+ # @example Iterar todos os assets
8
+ # client.assets.list.each { |a| puts a[:id] }
9
+ #
10
+ # @example Lazy — pegar apenas os 10 primeiros
11
+ # client.assets.list.lazy.first(10)
12
+ #
13
+ # @example Usar métodos Enumerable
14
+ # verified = client.assets.list.select { |a| a[:status] == 'verified' }
15
+ class Paginator
16
+ include Enumerable
17
+
18
+ def initialize(http:, path:, params: {})
19
+ @http = http
20
+ @path = path
21
+ @params = params
22
+ end
23
+
24
+ def each
25
+ cursor = nil
26
+
27
+ loop do
28
+ query = cursor ? @params.merge(cursor: cursor) : @params
29
+ response = @http.get(@path, query)
30
+
31
+ data = response[:data] || []
32
+ pagination = response[:pagination] || {}
33
+
34
+ data.each { |item| yield item }
35
+
36
+ break unless pagination[:has_more]
37
+ cursor = pagination[:next_cursor]
38
+ break if cursor.nil? || cursor.empty?
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Certynix
4
+ module Resources
5
+ class Alerts
6
+ def initialize(http)
7
+ @http = http
8
+ end
9
+
10
+ def list(**params)
11
+ Models::Paginator.new(http: @http, path: '/v1/alerts', params: params)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Certynix
4
+ module Resources
5
+ class ApiKeys
6
+ def initialize(http)
7
+ @http = http
8
+ end
9
+
10
+ def create(name:)
11
+ @http.post('/v1/api-keys', { name: name })
12
+ end
13
+
14
+ def list(**params)
15
+ Models::Paginator.new(http: @http, path: '/v1/api-keys', params: params)
16
+ end
17
+
18
+ def revoke(id)
19
+ @http.delete("/v1/api-keys/#{URI.encode_www_form_component(id)}")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Certynix
4
+ module Resources
5
+ class Assets
6
+ def initialize(http)
7
+ @http = http
8
+ end
9
+
10
+ # Registra um asset por hash SHA-256, URL ou arquivo.
11
+ def register(hash_sha256: nil, url: nil, file: nil, filename: nil, mime_type: nil, file_size: nil)
12
+ body = {}
13
+ if file
14
+ # Upload — usar multipart (simplificado: enviar como hash com file_data)
15
+ body[:file] = file
16
+ body[:filename] = filename if filename
17
+ body[:mime_type] = mime_type if mime_type
18
+ elsif hash_sha256
19
+ body[:hash_sha256] = hash_sha256
20
+ body[:filename] = filename if filename
21
+ body[:mime_type] = mime_type if mime_type
22
+ body[:file_size] = file_size if file_size
23
+ elsif url
24
+ body[:url] = url
25
+ body[:filename] = filename if filename
26
+ end
27
+ @http.post('/v1/assets', body)
28
+ end
29
+
30
+ # Registra um lote de assets.
31
+ def register_batch(assets:)
32
+ @http.post('/v1/assets/batch', { assets: assets })
33
+ end
34
+
35
+ # Busca um asset por ID.
36
+ def get(id)
37
+ @http.get("/v1/assets/#{URI.encode_www_form_component(id)}")
38
+ end
39
+
40
+ # Lista assets com paginação automática via Enumerable.
41
+ #
42
+ # @example
43
+ # client.assets.list.each { |a| puts a[:id] }
44
+ # client.assets.list.lazy.first(10)
45
+ def list(**params)
46
+ Models::Paginator.new(http: @http, path: '/v1/assets', params: params)
47
+ end
48
+
49
+ # Remove um asset (soft delete).
50
+ def delete(id)
51
+ @http.delete("/v1/assets/#{URI.encode_www_form_component(id)}")
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Certynix
4
+ module Resources
5
+ class AuditLogs
6
+ def initialize(http)
7
+ @http = http
8
+ end
9
+
10
+ def list(**params)
11
+ Models::Paginator.new(http: @http, path: '/v1/audit', params: params)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Certynix
4
+ module Resources
5
+ class TrustScore
6
+ def initialize(http)
7
+ @http = http
8
+ end
9
+
10
+ def get
11
+ @http.get('/v1/trust-score')
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Certynix
4
+ module Resources
5
+ class Verify
6
+ def initialize(http)
7
+ @http = http
8
+ end
9
+
10
+ # Verificação pública por hash SHA-256 — sem autenticação.
11
+ def by_hash(hash)
12
+ @http.get_public("/v1/verify/#{URI.encode_www_form_component(hash)}")
13
+ end
14
+
15
+ # Verificação pública por asset ID — sem autenticação.
16
+ def by_asset_id(id)
17
+ @http.get_public("/v1/verify/#{URI.encode_www_form_component(id)}")
18
+ end
19
+
20
+ # Verificação pública por hash via POST — sem autenticação.
21
+ def by_hash_post(hash)
22
+ @http.post_public('/v1/verify', { hash_sha256: hash })
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Certynix
4
+ module Resources
5
+ class Webhooks
6
+ def initialize(http)
7
+ @http = http
8
+ end
9
+
10
+ def create(url:, events: nil)
11
+ body = { url: url }
12
+ body[:events] = events if events
13
+ @http.post('/v1/webhooks', body)
14
+ end
15
+
16
+ def list(**params)
17
+ Models::Paginator.new(http: @http, path: '/v1/webhooks', params: params)
18
+ end
19
+
20
+ def update(id, url: nil, events: nil, active: nil)
21
+ body = {}
22
+ body[:url] = url if url
23
+ body[:events] = events if events
24
+ body[:active] = active unless active.nil?
25
+ @http.put("/v1/webhooks/#{URI.encode_www_form_component(id)}", body)
26
+ end
27
+
28
+ def delete(id)
29
+ @http.delete("/v1/webhooks/#{URI.encode_www_form_component(id)}")
30
+ end
31
+
32
+ def list_deliveries(id, **params)
33
+ Models::Paginator.new(
34
+ http: @http,
35
+ path: "/v1/webhooks/#{URI.encode_www_form_component(id)}/deliveries",
36
+ params: params
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Certynix
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'json'
5
+
6
+ module Certynix
7
+ # Utilitários para validação de assinatura de webhooks Certynix.
8
+ module Webhooks
9
+ TOLERANCE_SECONDS = 300 # 5 minutos
10
+
11
+ # Valida a assinatura HMAC-SHA256 de um delivery de webhook.
12
+ #
13
+ # CRÍTICO: raw_body deve ser o body bruto ANTES de qualquer JSON.parse.
14
+ # Usa OpenSSL.secure_compare (Ruby 2.7+) para constant-time comparison.
15
+ #
16
+ # @param raw_body [String] body bruto do request
17
+ # @param signature [String] valor do header X-Certynix-Signature
18
+ # @param secret [String] signing secret do webhook
19
+ # @return [Hash] { type:, payload:, timestamp: }
20
+ # @raise [WebhookSignatureError] se a assinatura for inválida
21
+ # @raise [WebhookReplayError] se o timestamp for > 5 minutos
22
+ def self.validate_signature(raw_body:, signature:, secret:)
23
+ # 1. Parse: "t=timestamp,v1=hash"
24
+ timestamp = nil
25
+ hash = nil
26
+
27
+ signature.split(',').each do |part|
28
+ timestamp = part[2..] if part.start_with?('t=')
29
+ hash = part[3..] if part.start_with?('v1=')
30
+ end
31
+
32
+ if timestamp.nil? || timestamp.empty? || hash.nil? || hash.empty?
33
+ raise WebhookSignatureError, 'Invalid signature format: expected t=timestamp,v1=hash'
34
+ end
35
+
36
+ # 2. Anti-replay
37
+ ts = timestamp.to_i
38
+ diff = (Time.now.to_i - ts).abs
39
+ if diff > TOLERANCE_SECONDS
40
+ raise WebhookReplayError, "Webhook timestamp is #{diff}s old — exceeds #{TOLERANCE_SECONDS}s tolerance"
41
+ end
42
+
43
+ # 3. Calcular HMAC-SHA256("{timestamp}.{raw_body}")
44
+ expected = OpenSSL::HMAC.hexdigest('SHA256', secret, "#{timestamp}.#{raw_body}")
45
+
46
+ # 4. Constant-time comparison
47
+ unless OpenSSL.secure_compare(expected, hash)
48
+ raise WebhookSignatureError, 'Webhook signature mismatch'
49
+ end
50
+
51
+ # 5. Parse payload
52
+ begin
53
+ payload = JSON.parse(raw_body, symbolize_names: true)
54
+ rescue JSON::ParserError
55
+ raise WebhookSignatureError, 'Failed to parse webhook payload as JSON'
56
+ end
57
+
58
+ {
59
+ type: payload[:type].to_s,
60
+ payload: payload,
61
+ timestamp: ts,
62
+ }
63
+ end
64
+ end
65
+ end
data/lib/certynix.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'certynix/version'
4
+ require 'certynix/errors'
5
+ require 'certynix/config'
6
+ require 'certynix/http_client'
7
+ require 'certynix/webhooks'
8
+ require 'certynix/models/paginator'
9
+ require 'certynix/resources/assets'
10
+ require 'certynix/resources/verify'
11
+ require 'certynix/resources/webhooks'
12
+ require 'certynix/resources/alerts'
13
+ require 'certynix/resources/api_keys'
14
+ require 'certynix/resources/audit_logs'
15
+ require 'certynix/resources/trust_score'
16
+ require 'certynix/client'
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: certynix
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Certynix
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday-retry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: webmock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.23'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.23'
69
+ - !ruby/object:Gem::Dependency
70
+ name: vcr
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '6.2'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '6.2'
83
+ description: Register, certify and verify digital assets with Certynix Trust Infrastructure
84
+ email:
85
+ - sdk@certynix.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - CHANGELOG.md
91
+ - README.md
92
+ - lib/certynix.rb
93
+ - lib/certynix/client.rb
94
+ - lib/certynix/config.rb
95
+ - lib/certynix/errors.rb
96
+ - lib/certynix/http_client.rb
97
+ - lib/certynix/models/paginator.rb
98
+ - lib/certynix/resources/alerts.rb
99
+ - lib/certynix/resources/api_keys.rb
100
+ - lib/certynix/resources/assets.rb
101
+ - lib/certynix/resources/audit_logs.rb
102
+ - lib/certynix/resources/trust_score.rb
103
+ - lib/certynix/resources/verify.rb
104
+ - lib/certynix/resources/webhooks.rb
105
+ - lib/certynix/version.rb
106
+ - lib/certynix/webhooks.rb
107
+ homepage: https://certynix.com
108
+ licenses:
109
+ - Proprietary
110
+ metadata: {}
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: 3.1.0
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubygems_version: 3.5.22
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Official Ruby SDK for Certynix Trust Infrastructure API
130
+ test_files: []