tripwire-server 0.1.1 → 0.3.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +48 -13
  3. data/lib/tripwire/server/client.rb +171 -16
  4. data/lib/tripwire/server/crypto_support.rb +49 -0
  5. data/lib/tripwire/server/gate_delivery.rb +298 -0
  6. data/lib/tripwire/server/sealed_token.rb +2 -0
  7. data/lib/tripwire/server/version.rb +1 -1
  8. data/lib/tripwire/server.rb +12 -0
  9. data/spec/README.md +37 -6
  10. data/spec/fixtures/api/fingerprints/detail.json +70 -0
  11. data/spec/fixtures/api/fingerprints/list.json +37 -0
  12. data/spec/fixtures/api/gate/agent-token-verify.json +12 -0
  13. data/spec/fixtures/api/gate/login-session-consume.json +10 -0
  14. data/spec/fixtures/api/gate/login-session-create.json +12 -0
  15. data/spec/fixtures/api/gate/registry-detail.json +45 -0
  16. data/spec/fixtures/api/gate/registry-list.json +47 -0
  17. data/spec/fixtures/api/gate/service-create.json +49 -0
  18. data/spec/fixtures/api/gate/service-detail.json +49 -0
  19. data/spec/fixtures/api/gate/service-disable.json +49 -0
  20. data/spec/fixtures/api/gate/service-update.json +49 -0
  21. data/spec/fixtures/api/gate/services-list.json +51 -0
  22. data/spec/fixtures/api/gate/session-ack.json +10 -0
  23. data/spec/fixtures/api/gate/session-create.json +13 -0
  24. data/spec/fixtures/api/gate/session-poll.json +36 -0
  25. data/spec/fixtures/api/sessions/detail.json +405 -0
  26. data/spec/fixtures/api/sessions/list.json +36 -0
  27. data/spec/fixtures/api/teams/api-key-create.json +21 -0
  28. data/spec/fixtures/api/teams/api-key-list.json +26 -0
  29. data/spec/fixtures/api/teams/api-key-revoke.json +20 -0
  30. data/spec/fixtures/api/teams/api-key-rotate.json +21 -0
  31. data/spec/fixtures/api/teams/team-create.json +14 -0
  32. data/spec/fixtures/api/teams/team-update.json +14 -0
  33. data/spec/fixtures/api/teams/team.json +14 -0
  34. data/spec/fixtures/errors/invalid-api-key.json +3 -3
  35. data/spec/fixtures/errors/missing-api-key.json +2 -2
  36. data/spec/fixtures/errors/not-found.json +4 -4
  37. data/spec/fixtures/errors/validation-error.json +6 -7
  38. data/spec/fixtures/gate-delivery/approved-webhook-payload.valid.json +20 -0
  39. data/spec/fixtures/gate-delivery/delivery-request.json +9 -0
  40. data/spec/fixtures/gate-delivery/env-policy.json +40 -0
  41. data/spec/fixtures/gate-delivery/vector.v1.json +28 -0
  42. data/spec/fixtures/gate-delivery/webhook-signature.json +9 -0
  43. data/spec/fixtures/manifest.json +179 -0
  44. data/spec/fixtures/sealed-token/vector.v1.json +37 -24
  45. data/spec/openapi.json +4905 -779
  46. data/spec/sealed-token.md +36 -17
  47. metadata +36 -14
  48. data/spec/fixtures/public-api/fingerprints/detail.json +0 -40
  49. data/spec/fixtures/public-api/fingerprints/list.json +0 -31
  50. data/spec/fixtures/public-api/sessions/detail.json +0 -47
  51. data/spec/fixtures/public-api/sessions/list.json +0 -33
  52. data/spec/fixtures/public-api/teams/api-key-create.json +0 -18
  53. data/spec/fixtures/public-api/teams/api-key-list.json +0 -23
  54. data/spec/fixtures/public-api/teams/api-key-rotate.json +0 -18
  55. data/spec/fixtures/public-api/teams/team-create.json +0 -11
  56. data/spec/fixtures/public-api/teams/team-update.json +0 -11
  57. data/spec/fixtures/public-api/teams/team.json +0 -11
  58. /data/spec/fixtures/{public-api/teams/api-key-revoke.json → api/gate/agent-token-revoke.json} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29ad4de59013e7b0962b8444feb069a8b1b216831a40d46fa7dd9c3cb16371a8
4
- data.tar.gz: c6ccc706e11d7954c33e3a77797bb2711748cc99fb3144a6f46c2e588d0f8136
3
+ metadata.gz: 45e6986f12a591fdd4523c5564f2555d09c0729783e5d12998541e65652814d4
4
+ data.tar.gz: b7c767d8c99846ce42dfa6820dcf787a48cb36f0367ec302f636a55b706dfd0c
5
5
  SHA512:
6
- metadata.gz: 38a299b8107727875c39ebe0c0a3a97cf6d45396f484a49dbc02766cbc5bb1918308c75908959a53e1e4e04409ec545ba7d2cb795bea6ba85c5ec31a182372d7
7
- data.tar.gz: 0547b124aed5e0fba0e744279b63b312f9e0abee5114195ed452fd92bff6ad8cfa05e0543bcfd7c1c7ba678b0c4e5861be3f9e20451c7f43d8a578b4a4f910a2
6
+ metadata.gz: 6bb732f690f751fd371ffcaccec4895f762a06685703715c92e35a1cbdd35d87dca706d6fcb5a5c8763e8bd9e26ca003e758d747dc724bdba2c21547922ce032
7
+ data.tar.gz: fe3def014d6b619cad9e3a07a1fc05a276b415231b30538afbf49d832bb5436f644cc9918a02aa1bea1a9a0fdae0cbc67c141d8e2d181bfd60da3d4eb0cebf7b
data/README.md CHANGED
@@ -1,16 +1,18 @@
1
1
  # Tripwire Ruby Library
2
2
 
3
3
  ![Preview](https://img.shields.io/badge/status-preview-111827)
4
- ![Ruby 2.6+](https://img.shields.io/badge/ruby-2.6%2B-CC342D?logo=ruby&logoColor=white)
4
+ ![Ruby 3.3+](https://img.shields.io/badge/ruby-3.3%2B-CC342D?logo=ruby&logoColor=white)
5
5
  ![License: MIT](https://img.shields.io/badge/license-MIT-0f766e.svg)
6
6
 
7
- The Tripwire Ruby library provides convenient access to the Tripwire API from applications written in Ruby. It includes a client for Sessions, Fingerprints, Teams, Team API key management, and sealed token verification.
7
+ The Tripwire Ruby library provides convenient access to the Tripwire API from applications written in Ruby. It includes a client for Sessions, visitor fingerprints, Teams, Team API key management, sealed token verification, Gate, and Gate delivery/webhook helpers.
8
8
 
9
9
  The library also provides:
10
10
 
11
11
  - a fast configuration path using `TRIPWIRE_SECRET_KEY`
12
12
  - lazy helpers for cursor-based pagination
13
13
  - structured API errors and built-in sealed token verification
14
+ - public, bearer-token, and secret-key auth modes for Gate flows
15
+ - Gate delivery/webhook helpers
14
16
 
15
17
  ## Documentation
16
18
 
@@ -26,11 +28,11 @@ bundle add tripwire-server
26
28
 
27
29
  ## Requirements
28
30
 
29
- - Ruby 2.6+
31
+ - Ruby 3.3+
30
32
 
31
33
  ## Usage
32
34
 
33
- The library needs to be configured with your account's secret key. Set `TRIPWIRE_SECRET_KEY` in your environment or pass `secret_key` directly:
35
+ Use `TRIPWIRE_SECRET_KEY` or `secret_key:` for core detect APIs. For public or bearer-auth Gate flows, the client can also be created without a secret key:
34
36
 
35
37
  ```ruby
36
38
  require "tripwire/server"
@@ -38,7 +40,9 @@ require "tripwire/server"
38
40
  client = Tripwire::Server::Client.new(secret_key: "sk_live_...")
39
41
 
40
42
  page = client.sessions.list(verdict: "bot", limit: 25)
41
- session = client.sessions.get("sid_123")
43
+ session = client.sessions.get("sid_0123456789abcdefghjkmnpqrs")
44
+
45
+ puts "#{session[:decision][:automation_status]} #{session[:highlights].first&.fetch(:summary, nil)}"
42
46
  ```
43
47
 
44
48
  ### Sealed token verification
@@ -47,7 +51,7 @@ session = client.sessions.get("sid_123")
47
51
  result = Tripwire::Server.safe_verify_tripwire_token(sealed_token, "sk_live_...")
48
52
 
49
53
  if result[:ok]
50
- puts "#{result[:data][:verdict]} #{result[:data][:score]}"
54
+ puts "#{result[:data][:decision][:verdict]} #{result[:data][:decision][:risk_score]}"
51
55
  else
52
56
  warn result[:error].message
53
57
  end
@@ -57,22 +61,22 @@ end
57
61
 
58
62
  ```ruby
59
63
  client.sessions.iter(search: "signup").each do |session|
60
- puts "#{session[:id]} #{session[:latestResult][:verdict]}"
64
+ puts "#{session[:id]} #{session[:latest_decision][:verdict]}"
61
65
  end
62
66
  ```
63
67
 
64
- ### Fingerprints
68
+ ### Visitor fingerprints
65
69
 
66
70
  ```ruby
67
- fingerprint = client.fingerprints.get("vis_123")
71
+ fingerprint = client.fingerprints.get("vid_0123456789abcdefghjkmnpqrs")
68
72
  puts fingerprint[:id]
69
73
  ```
70
74
 
71
75
  ### Teams
72
76
 
73
77
  ```ruby
74
- team = client.teams.get("team_123")
75
- updated = client.teams.update("team_123", name: "New Name")
78
+ team = client.teams.get("team_0123456789abcdefghjkmnpqrs")
79
+ updated = client.teams.update("team_0123456789abcdefghjkmnpqrs", name: "New Name")
76
80
 
77
81
  puts updated[:name]
78
82
  ```
@@ -80,8 +84,39 @@ puts updated[:name]
80
84
  ### Team API keys
81
85
 
82
86
  ```ruby
83
- created = client.teams.api_keys.create("team_123", name: "Production")
84
- client.teams.api_keys.revoke("team_123", created[:id])
87
+ created = client.teams.api_keys.create("team_0123456789abcdefghjkmnpqrs", name: "Production", environment: "live")
88
+ client.teams.api_keys.revoke("team_0123456789abcdefghjkmnpqrs", created[:id])
89
+ ```
90
+
91
+ ### Gate APIs
92
+
93
+ ```ruby
94
+ delivery_key_pair = Tripwire::Server::GateDelivery.create_delivery_key_pair
95
+
96
+ services = client.gate.registry.list
97
+ session = client.gate.sessions.create(
98
+ service_id: "tripwire",
99
+ account_name: "my-project",
100
+ delivery: delivery_key_pair[:delivery]
101
+ )
102
+
103
+ puts "#{services.first[:id]} #{session[:consent_url]}"
104
+ ```
105
+
106
+ ### Gate delivery and webhook helpers
107
+
108
+ ```ruby
109
+ key_pair = Tripwire::Server::GateDelivery.create_delivery_key_pair
110
+ response = Tripwire::Server::GateDelivery.create_gate_approved_webhook_response(
111
+ delivery: key_pair[:delivery],
112
+ outputs: {
113
+ "TRIPWIRE_PUBLISHABLE_KEY" => "pk_live_...",
114
+ "TRIPWIRE_SECRET_KEY" => "sk_live_..."
115
+ }
116
+ )
117
+ payload = Tripwire::Server::GateDelivery.decrypt_gate_delivery_envelope(key_pair[:private_key], response[:encrypted_delivery])
118
+
119
+ puts payload[:outputs]["TRIPWIRE_SECRET_KEY"]
85
120
  ```
86
121
 
87
122
  ### Error handling
@@ -10,11 +10,9 @@ module Tripwire
10
10
  DEFAULT_TIMEOUT = 30
11
11
  SDK_CLIENT_HEADER = "tripwire-server-ruby/0.1.0".freeze
12
12
 
13
- attr_reader :sessions, :fingerprints, :teams, :timeout
13
+ attr_reader :sessions, :fingerprints, :teams, :gate, :timeout
14
14
 
15
15
  def initialize(secret_key: ENV["TRIPWIRE_SECRET_KEY"], base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, user_agent: nil, transport: nil)
16
- raise ConfigurationError, "Missing Tripwire secret key. Pass secret_key explicitly or set TRIPWIRE_SECRET_KEY." if secret_key.nil? || secret_key.empty?
17
-
18
16
  @secret_key = secret_key
19
17
  @base_url = base_url
20
18
  @timeout = timeout
@@ -24,17 +22,18 @@ module Tripwire
24
22
  @sessions = SessionsResource.new(self)
25
23
  @fingerprints = FingerprintsResource.new(self)
26
24
  @teams = TeamsResource.new(self)
25
+ @gate = GateResource.new(self)
27
26
  end
28
27
 
29
- def request_json(method, path, query: {}, body: nil, expect_content: true)
28
+ def request_json(method, path, query: {}, body: nil, expect_content: true, auth: { kind: :secret })
30
29
  url = build_url(path, query)
31
30
  headers = {
32
- "Authorization" => "Bearer #{@secret_key}",
33
31
  "Accept" => "application/json",
34
32
  "X-Tripwire-Client" => SDK_CLIENT_HEADER
35
33
  }
36
34
  headers["User-Agent"] = @user_agent if @user_agent
37
35
  headers["Content-Type"] = "application/json" if body
36
+ apply_auth_headers(headers, auth)
38
37
 
39
38
  status, response_headers, response_body =
40
39
  if @transport
@@ -54,9 +53,9 @@ module Tripwire
54
53
  status: status,
55
54
  code: error[:code] || "request.failed",
56
55
  message: error[:message] || response_body.to_s,
57
- request_id: request_id || error[:requestId],
58
- field_errors: details[:fieldErrors] || [],
59
- docs_url: error[:docsUrl],
56
+ request_id: request_id || error[:request_id],
57
+ field_errors: details[:fields] || [],
58
+ docs_url: error[:docs_url],
60
59
  body: payload
61
60
  )
62
61
  end
@@ -125,6 +124,24 @@ module Tripwire
125
124
  end
126
125
  end
127
126
  private :deep_symbolize
127
+
128
+ def apply_auth_headers(headers, auth)
129
+ kind = (auth[:kind] || :secret).to_sym
130
+ case kind
131
+ when :none
132
+ headers
133
+ when :bearer
134
+ token = auth[:token]
135
+ raise ConfigurationError, "Missing bearer token for this Tripwire request." if token.nil? || token.empty?
136
+
137
+ headers["Authorization"] = "Bearer #{token}"
138
+ else
139
+ raise ConfigurationError, "Missing Tripwire secret key. Pass secret_key explicitly or set TRIPWIRE_SECRET_KEY." if @secret_key.nil? || @secret_key.empty?
140
+
141
+ headers["Authorization"] = "Bearer #{@secret_key}"
142
+ end
143
+ end
144
+ private :apply_auth_headers
128
145
  end
129
146
 
130
147
  class BaseResource
@@ -138,8 +155,8 @@ module Tripwire
138
155
  ListResult.new(
139
156
  items: payload[:data],
140
157
  limit: payload.fetch(:pagination).fetch(:limit),
141
- has_more: payload.fetch(:pagination).fetch(:hasMore),
142
- next_cursor: payload.fetch(:pagination)[:nextCursor]
158
+ has_more: payload.fetch(:pagination).fetch(:has_more),
159
+ next_cursor: payload.fetch(:pagination)[:next_cursor]
143
160
  )
144
161
  end
145
162
  end
@@ -203,12 +220,12 @@ module Tripwire
203
220
  end
204
221
 
205
222
  class ApiKeysResource < BaseResource
206
- def create(team_id, name: nil, is_test: nil, allowed_origins: nil, rate_limit: nil)
223
+ def create(team_id, name: nil, environment: nil, allowed_origins: nil, rate_limit: nil)
207
224
  payload = @client.request_json("POST", "/v1/teams/#{CGI.escape(team_id)}/api-keys", body: compact({
208
225
  name: name,
209
- isTest: is_test,
210
- allowedOrigins: allowed_origins,
211
- rateLimit: rate_limit
226
+ environment: environment,
227
+ allowed_origins: allowed_origins,
228
+ rate_limit: rate_limit
212
229
  }))
213
230
  payload[:data]
214
231
  end
@@ -222,8 +239,7 @@ module Tripwire
222
239
  end
223
240
 
224
241
  def revoke(team_id, key_id)
225
- @client.request_json("DELETE", "/v1/teams/#{CGI.escape(team_id)}/api-keys/#{CGI.escape(key_id)}", expect_content: false)
226
- nil
242
+ @client.request_json("DELETE", "/v1/teams/#{CGI.escape(team_id)}/api-keys/#{CGI.escape(key_id)}")[:data]
227
243
  end
228
244
 
229
245
  def rotate(team_id, key_id)
@@ -261,5 +277,144 @@ module Tripwire
261
277
  }.reject { |_key, value| value.nil? })[:data]
262
278
  end
263
279
  end
280
+
281
+ class GateResource < BaseResource
282
+ attr_reader :registry, :services, :sessions, :login_sessions, :agent_tokens
283
+
284
+ def initialize(client)
285
+ super(client)
286
+ @registry = GateRegistryResource.new(client)
287
+ @services = GateServicesResource.new(client)
288
+ @sessions = GateSessionsResource.new(client)
289
+ @login_sessions = GateLoginSessionsResource.new(client)
290
+ @agent_tokens = GateAgentTokensResource.new(client)
291
+ end
292
+ end
293
+
294
+ class GateRegistryResource < BaseResource
295
+ def list
296
+ @client.request_json("GET", "/v1/gate/registry", auth: { kind: :none })[:data]
297
+ end
298
+
299
+ def get(service_id)
300
+ @client.request_json("GET", "/v1/gate/registry/#{CGI.escape(service_id)}", auth: { kind: :none })[:data]
301
+ end
302
+ end
303
+
304
+ class GateServicesResource < BaseResource
305
+ def list
306
+ @client.request_json("GET", "/v1/gate/services")[:data]
307
+ end
308
+
309
+ def get(service_id)
310
+ @client.request_json("GET", "/v1/gate/services/#{CGI.escape(service_id)}")[:data]
311
+ end
312
+
313
+ def create(id:, name:, description:, website:, webhook_url:, discoverable: nil, dashboard_login_url: nil, webhook_secret: nil, env_vars: nil, docs_url: nil, sdks: nil, branding: nil, consent: nil)
314
+ @client.request_json("POST", "/v1/gate/services", body: compact({
315
+ id: id,
316
+ discoverable: discoverable,
317
+ name: name,
318
+ description: description,
319
+ website: website,
320
+ dashboard_login_url: dashboard_login_url,
321
+ webhook_url: webhook_url,
322
+ webhook_secret: webhook_secret,
323
+ env_vars: env_vars,
324
+ docs_url: docs_url,
325
+ sdks: sdks,
326
+ branding: branding,
327
+ consent: consent
328
+ }))[:data]
329
+ end
330
+
331
+ def update(service_id, discoverable: nil, name: nil, description: nil, website: nil, dashboard_login_url: nil, webhook_url: nil, webhook_secret: nil, env_vars: nil, docs_url: nil, sdks: nil, branding: nil, consent: nil)
332
+ @client.request_json("PATCH", "/v1/gate/services/#{CGI.escape(service_id)}", body: compact({
333
+ discoverable: discoverable,
334
+ name: name,
335
+ description: description,
336
+ website: website,
337
+ dashboard_login_url: dashboard_login_url,
338
+ webhook_url: webhook_url,
339
+ webhook_secret: webhook_secret,
340
+ env_vars: env_vars,
341
+ docs_url: docs_url,
342
+ sdks: sdks,
343
+ branding: branding,
344
+ consent: consent
345
+ }))[:data]
346
+ end
347
+
348
+ def disable(service_id)
349
+ @client.request_json("DELETE", "/v1/gate/services/#{CGI.escape(service_id)}")[:data]
350
+ end
351
+
352
+ private
353
+
354
+ def compact(hash)
355
+ hash.reject { |_key, value| value.nil? }
356
+ end
357
+ end
358
+
359
+ class GateSessionsResource < BaseResource
360
+ def create(service_id:, account_name:, delivery:, metadata: nil)
361
+ body = {
362
+ service_id: service_id,
363
+ account_name: account_name,
364
+ delivery: delivery
365
+ }
366
+ body[:metadata] = metadata unless metadata.nil?
367
+
368
+ @client.request_json("POST", "/v1/gate/sessions", body: body, auth: { kind: :none })[:data]
369
+ end
370
+
371
+ def poll(gate_session_id, poll_token:)
372
+ @client.request_json(
373
+ "GET",
374
+ "/v1/gate/sessions/#{CGI.escape(gate_session_id)}",
375
+ auth: { kind: :bearer, token: poll_token }
376
+ )[:data]
377
+ end
378
+
379
+ def acknowledge(gate_session_id, poll_token:, ack_token:)
380
+ @client.request_json(
381
+ "POST",
382
+ "/v1/gate/sessions/#{CGI.escape(gate_session_id)}/ack",
383
+ body: { ack_token: ack_token },
384
+ auth: { kind: :bearer, token: poll_token }
385
+ )[:data]
386
+ end
387
+ end
388
+
389
+ class GateLoginSessionsResource < BaseResource
390
+ def create(service_id:, agent_token:)
391
+ @client.request_json(
392
+ "POST",
393
+ "/v1/gate/login-sessions",
394
+ body: { service_id: service_id },
395
+ auth: { kind: :bearer, token: agent_token }
396
+ )[:data]
397
+ end
398
+
399
+ def consume(code:)
400
+ @client.request_json("POST", "/v1/gate/login-sessions/consume", body: { code: code })[:data]
401
+ end
402
+ end
403
+
404
+ class GateAgentTokensResource < BaseResource
405
+ def verify(agent_token:)
406
+ @client.request_json("POST", "/v1/gate/agent-tokens/verify", body: { agent_token: agent_token })[:data]
407
+ end
408
+
409
+ def revoke(agent_token:)
410
+ @client.request_json(
411
+ "POST",
412
+ "/v1/gate/agent-tokens/revoke",
413
+ body: { agent_token: agent_token },
414
+ expect_content: false
415
+ )
416
+ nil
417
+ end
418
+ end
264
419
  end
265
420
  end
@@ -0,0 +1,49 @@
1
+ require "openssl"
2
+ require "rubygems"
3
+
4
+ module Tripwire
5
+ module Server
6
+ module CryptoSupport
7
+ MIN_SUPPORTED_RUBY_VERSION = Gem::Version.new("3.3.0")
8
+ UNSUPPORTED_RUNTIME_MESSAGE = "Tripwire Ruby cryptography helpers require Ruby 3.3+ with modern OpenSSL support.".freeze
9
+
10
+ module_function
11
+
12
+ def supported_runtime?
13
+ return @supported_runtime unless @supported_runtime.nil?
14
+
15
+ @supported_runtime = Gem::Version.new(RUBY_VERSION) >= MIN_SUPPORTED_RUBY_VERSION &&
16
+ OpenSSL::PKey.respond_to?(:generate_key) &&
17
+ defined?(OpenSSL::KDF) &&
18
+ OpenSSL::KDF.respond_to?(:hkdf) &&
19
+ aead_auth_data_supported?
20
+ end
21
+
22
+ def ensure_supported_runtime!
23
+ return if supported_runtime?
24
+
25
+ raise ConfigurationError, UNSUPPORTED_RUNTIME_MESSAGE
26
+ end
27
+
28
+ def minimum_supported_ruby_version
29
+ MIN_SUPPORTED_RUBY_VERSION
30
+ end
31
+
32
+ def unsupported_runtime_message
33
+ UNSUPPORTED_RUNTIME_MESSAGE
34
+ end
35
+
36
+ def aead_auth_data_supported?
37
+ cipher = OpenSSL::Cipher.new("aes-256-gcm")
38
+ cipher.encrypt
39
+ cipher.key = "\x00".b * 32
40
+ cipher.iv = "\x00".b * 12
41
+ cipher.auth_data = "".b
42
+ true
43
+ rescue StandardError
44
+ false
45
+ end
46
+ private_class_method :aead_auth_data_supported?
47
+ end
48
+ end
49
+ end