foil-server 0.3.3
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/LICENSE +21 -0
- data/README.md +154 -0
- data/lib/foil/server/client.rb +472 -0
- data/lib/foil/server/crypto_support.rb +49 -0
- data/lib/foil/server/errors.rb +21 -0
- data/lib/foil/server/gate_delivery.rb +325 -0
- data/lib/foil/server/sealed_token.rb +78 -0
- data/lib/foil/server/types.rb +5 -0
- data/lib/foil/server/version.rb +5 -0
- data/lib/foil/server.rb +31 -0
- data/spec/LICENSE +21 -0
- data/spec/README.md +160 -0
- data/spec/fixtures/api/fingerprints/detail.json +70 -0
- data/spec/fixtures/api/fingerprints/list.json +37 -0
- data/spec/fixtures/api/gate/agent-token-revoke.json +3 -0
- data/spec/fixtures/api/gate/agent-token-verify.json +12 -0
- data/spec/fixtures/api/gate/login-session-consume.json +10 -0
- data/spec/fixtures/api/gate/login-session-create.json +12 -0
- data/spec/fixtures/api/gate/registry-detail.json +45 -0
- data/spec/fixtures/api/gate/registry-list.json +47 -0
- data/spec/fixtures/api/gate/service-create.json +49 -0
- data/spec/fixtures/api/gate/service-detail.json +49 -0
- data/spec/fixtures/api/gate/service-disable.json +49 -0
- data/spec/fixtures/api/gate/service-update.json +49 -0
- data/spec/fixtures/api/gate/services-list.json +51 -0
- data/spec/fixtures/api/gate/session-ack.json +10 -0
- data/spec/fixtures/api/gate/session-create.json +13 -0
- data/spec/fixtures/api/gate/session-poll.json +36 -0
- data/spec/fixtures/api/organizations/api-key-create.json +27 -0
- data/spec/fixtures/api/organizations/api-key-list.json +31 -0
- data/spec/fixtures/api/organizations/api-key-revoke.json +25 -0
- data/spec/fixtures/api/organizations/api-key-rotate.json +27 -0
- data/spec/fixtures/api/organizations/api-key-update.json +29 -0
- data/spec/fixtures/api/organizations/organization-create.json +14 -0
- data/spec/fixtures/api/organizations/organization-update.json +14 -0
- data/spec/fixtures/api/organizations/organization.json +14 -0
- data/spec/fixtures/api/sessions/detail.json +434 -0
- data/spec/fixtures/api/sessions/list.json +36 -0
- data/spec/fixtures/errors/invalid-api-key.json +10 -0
- data/spec/fixtures/errors/missing-api-key.json +10 -0
- data/spec/fixtures/errors/not-found.json +10 -0
- data/spec/fixtures/errors/validation-error.json +20 -0
- data/spec/fixtures/gate-delivery/approved-webhook-payload.valid.json +19 -0
- data/spec/fixtures/gate-delivery/delivery-request.json +9 -0
- data/spec/fixtures/gate-delivery/env-policy.json +40 -0
- data/spec/fixtures/gate-delivery/vector.v1.json +28 -0
- data/spec/fixtures/gate-delivery/webhook-signature.json +9 -0
- data/spec/fixtures/manifest.json +185 -0
- data/spec/fixtures/sealed-token/invalid.json +4 -0
- data/spec/fixtures/sealed-token/vector.v1.json +54 -0
- data/spec/openapi.json +20482 -0
- data/spec/sealed-token.md +114 -0
- metadata +96 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cb419b8f30f0937555fa2b515c66f6bca1e8dc042ab72eee05396b53fe02d3a6
|
|
4
|
+
data.tar.gz: 1545b9bd453185c2656ca627adbe26f6e130d7d5da29491e30e780887e27415a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 216b749d89ea86f9c80b01bf0186a7916dda90718f81bb52eb7c7699c323776a8a9e647e1c6486a8499e3488fab26c4b0d01f8f71849b0325b05514887bbb923
|
|
7
|
+
data.tar.gz: 31c99e14cd9c805e1af6b66719c0eecc1c3ad1c42a59b205e7d969234e1ff473b9169d6fb3d9aeda5aac9805458118dfa2e079f82599f51d34054b0f8a0dcb5b
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ABXY Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Foil Ruby Library
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
The Foil Ruby library provides convenient access to the Foil API from applications written in Ruby. It includes a client for Sessions, visitor fingerprints, Organizations, Organization API key management, sealed token verification, Gate, and Gate delivery/webhook helpers.
|
|
8
|
+
|
|
9
|
+
The library also provides:
|
|
10
|
+
|
|
11
|
+
- a fast configuration path using `FOIL_SECRET_KEY`
|
|
12
|
+
- lazy helpers for cursor-based pagination
|
|
13
|
+
- structured API errors and built-in sealed token verification
|
|
14
|
+
- webhook endpoint management, test sends, and event delivery history
|
|
15
|
+
- public, bearer-token, and secret-key auth modes for Gate flows
|
|
16
|
+
- Gate delivery/webhook helpers
|
|
17
|
+
|
|
18
|
+
## Documentation
|
|
19
|
+
|
|
20
|
+
See the [Foil docs](https://usefoil.com/docs) and [API reference](https://usefoil.com/docs/api-reference/introduction).
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
You don't need this source code unless you want to modify the gem. If you just want to use the package, run:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bundle add foil-server
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Ruby 3.3+
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
Use `FOIL_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:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require "foil/server"
|
|
40
|
+
|
|
41
|
+
client = Foil::Server::Client.new(secret_key: "sk_live_...")
|
|
42
|
+
|
|
43
|
+
page = client.sessions.list(verdict: "bot", limit: 25)
|
|
44
|
+
session = client.sessions.get("sid_0123456789abcdefghjkmnpqrs")
|
|
45
|
+
|
|
46
|
+
puts "#{session[:decision][:automation_status]} #{session[:highlights].first&.fetch(:summary, nil)}"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Sealed token verification
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
result = Foil::Server.safe_verify_foil_token(sealed_token, "sk_live_...")
|
|
53
|
+
|
|
54
|
+
if result[:ok]
|
|
55
|
+
puts "#{result[:data][:decision][:verdict]} #{result[:data][:decision][:risk_score]}"
|
|
56
|
+
else
|
|
57
|
+
warn result[:error].message
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Pagination
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
client.sessions.iter(search: "signup").each do |session|
|
|
65
|
+
puts "#{session[:id]} #{session[:latest_decision][:verdict]}"
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Visitor fingerprints
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
fingerprint = client.fingerprints.get("vid_0123456789abcdefghjkmnpqrs")
|
|
73
|
+
puts fingerprint[:id]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Organizations
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
organization = client.organizations.get("org_0123456789abcdefghjkmnpqrs")
|
|
80
|
+
updated = client.organizations.update("org_0123456789abcdefghjkmnpqrs", name: "New Name")
|
|
81
|
+
|
|
82
|
+
puts updated[:name]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Organization API keys
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
created = client.organizations.api_keys.create("org_0123456789abcdefghjkmnpqrs", name: "Production", type: "secret", environment: "live")
|
|
89
|
+
client.organizations.api_keys.revoke("org_0123456789abcdefghjkmnpqrs", created[:id])
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Webhooks
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
endpoint = client.webhooks.create_endpoint(
|
|
96
|
+
"org_0123456789abcdefghjkmnpqrs",
|
|
97
|
+
name: "Production alerts",
|
|
98
|
+
url: "https://example.com/foil/webhook",
|
|
99
|
+
event_types: ["session.result.persisted", "gate.session.approved"]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
events = client.webhooks.list_events(
|
|
103
|
+
"org_0123456789abcdefghjkmnpqrs",
|
|
104
|
+
endpoint_id: endpoint[:id],
|
|
105
|
+
type: "session.result.persisted"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
puts events.items.first[:webhook_deliveries].first[:status]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Gate APIs
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
delivery_key_pair = Foil::Server::GateDelivery.create_delivery_key_pair
|
|
115
|
+
|
|
116
|
+
services = client.gate.registry.list
|
|
117
|
+
session = client.gate.sessions.create(
|
|
118
|
+
service_id: "foil",
|
|
119
|
+
account_name: "my-project",
|
|
120
|
+
delivery: delivery_key_pair[:delivery]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
puts "#{services.first[:id]} #{session[:consent_url]}"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Gate delivery and webhook helpers
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
key_pair = Foil::Server::GateDelivery.create_delivery_key_pair
|
|
130
|
+
response = Foil::Server::GateDelivery.create_gate_approved_webhook_response(
|
|
131
|
+
delivery: key_pair[:delivery],
|
|
132
|
+
outputs: {
|
|
133
|
+
"FOIL_PUBLISHABLE_KEY" => "pk_live_...",
|
|
134
|
+
"FOIL_SECRET_KEY" => "sk_live_..."
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
payload = Foil::Server::GateDelivery.decrypt_gate_delivery_envelope(key_pair[:private_key], response[:encrypted_delivery])
|
|
138
|
+
|
|
139
|
+
puts payload[:outputs]["FOIL_SECRET_KEY"]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Error handling
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
begin
|
|
146
|
+
client.sessions.list(limit: 999)
|
|
147
|
+
rescue Foil::Server::ApiError => error
|
|
148
|
+
warn "#{error.status} #{error.code} #{error.message}"
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Support
|
|
153
|
+
|
|
154
|
+
If you need help integrating Foil, start with [usefoil.com/docs](https://usefoil.com/docs).
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
require "cgi"
|
|
2
|
+
require "json"
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Foil
|
|
7
|
+
module Server
|
|
8
|
+
class Client
|
|
9
|
+
DEFAULT_BASE_URL = "https://api.usefoil.com".freeze
|
|
10
|
+
DEFAULT_TIMEOUT = 30
|
|
11
|
+
SDK_CLIENT_HEADER = "foil-server-ruby/0.1.0".freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :sessions, :fingerprints, :organizations, :gate, :webhooks, :timeout
|
|
14
|
+
|
|
15
|
+
def initialize(secret_key: ENV["FOIL_SECRET_KEY"], base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, user_agent: nil, transport: nil)
|
|
16
|
+
@secret_key = secret_key
|
|
17
|
+
@base_url = base_url
|
|
18
|
+
@timeout = timeout
|
|
19
|
+
@user_agent = user_agent
|
|
20
|
+
@transport = transport
|
|
21
|
+
|
|
22
|
+
@sessions = SessionsResource.new(self)
|
|
23
|
+
@fingerprints = FingerprintsResource.new(self)
|
|
24
|
+
@organizations = OrganizationsResource.new(self)
|
|
25
|
+
@gate = GateResource.new(self)
|
|
26
|
+
@webhooks = WebhooksResource.new(self)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def request_json(method, path, query: {}, body: nil, expect_content: true, auth: { kind: :secret })
|
|
30
|
+
url = build_url(path, query)
|
|
31
|
+
headers = {
|
|
32
|
+
"Accept" => "application/json",
|
|
33
|
+
"X-Foil-Client" => SDK_CLIENT_HEADER
|
|
34
|
+
}
|
|
35
|
+
headers["User-Agent"] = @user_agent if @user_agent
|
|
36
|
+
headers["Content-Type"] = "application/json" if body
|
|
37
|
+
apply_auth_headers(headers, auth)
|
|
38
|
+
|
|
39
|
+
status, response_headers, response_body =
|
|
40
|
+
if @transport
|
|
41
|
+
@transport.call(method: method, url: url.to_s, headers: headers, body: body.nil? ? nil : JSON.dump(body))
|
|
42
|
+
else
|
|
43
|
+
perform_http_request(method, url, headers, body)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
request_id = response_headers["x-request-id"] || response_headers["X-Request-Id"]
|
|
47
|
+
|
|
48
|
+
if status >= 400
|
|
49
|
+
payload = parse_json(response_body)
|
|
50
|
+
if payload[:error].is_a?(Hash)
|
|
51
|
+
error = payload[:error]
|
|
52
|
+
details = error[:details].is_a?(Hash) ? error[:details] : {}
|
|
53
|
+
raise ApiError.new(
|
|
54
|
+
status: status,
|
|
55
|
+
code: error[:code] || "request.failed",
|
|
56
|
+
message: error[:message] || response_body.to_s,
|
|
57
|
+
request_id: request_id || error[:request_id],
|
|
58
|
+
field_errors: details[:fields] || [],
|
|
59
|
+
docs_url: error[:docs_url],
|
|
60
|
+
body: payload
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
raise ApiError.new(status: status, code: "request.failed", message: response_body.to_s, request_id: request_id, body: payload)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
return {} unless expect_content
|
|
68
|
+
return {} if status == 204 || response_body.nil? || response_body.empty?
|
|
69
|
+
|
|
70
|
+
parse_json(response_body)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def perform_http_request(method, url, headers, body)
|
|
74
|
+
http = Net::HTTP.new(url.host, url.port)
|
|
75
|
+
http.use_ssl = (url.scheme == "https")
|
|
76
|
+
http.read_timeout = @timeout
|
|
77
|
+
http.open_timeout = @timeout
|
|
78
|
+
|
|
79
|
+
request_class = case method
|
|
80
|
+
when "GET" then Net::HTTP::Get
|
|
81
|
+
when "POST" then Net::HTTP::Post
|
|
82
|
+
when "PATCH" then Net::HTTP::Patch
|
|
83
|
+
when "DELETE" then Net::HTTP::Delete
|
|
84
|
+
else
|
|
85
|
+
raise ArgumentError, "Unsupported method #{method}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
request = request_class.new(url)
|
|
89
|
+
headers.each { |key, value| request[key] = value }
|
|
90
|
+
request.body = JSON.dump(body) if body
|
|
91
|
+
|
|
92
|
+
response = http.request(request)
|
|
93
|
+
[response.code.to_i, response.to_hash.transform_values { |value| Array(value).first }, response.body.to_s]
|
|
94
|
+
end
|
|
95
|
+
private :perform_http_request
|
|
96
|
+
|
|
97
|
+
def build_url(path, query)
|
|
98
|
+
url = URI.join(@base_url.end_with?("/") ? @base_url : "#{@base_url}/", path.sub(%r{\A/}, ""))
|
|
99
|
+
compact_query = query.each_with_object({}) do |(key, value), memo|
|
|
100
|
+
memo[key] = value unless value.nil? || value == ""
|
|
101
|
+
end
|
|
102
|
+
url.query = URI.encode_www_form(compact_query) unless compact_query.empty?
|
|
103
|
+
url
|
|
104
|
+
end
|
|
105
|
+
private :build_url
|
|
106
|
+
|
|
107
|
+
def parse_json(body)
|
|
108
|
+
data = JSON.parse(body)
|
|
109
|
+
deep_symbolize(data)
|
|
110
|
+
rescue JSON::ParserError
|
|
111
|
+
{}
|
|
112
|
+
end
|
|
113
|
+
private :parse_json
|
|
114
|
+
|
|
115
|
+
def deep_symbolize(value)
|
|
116
|
+
case value
|
|
117
|
+
when Array
|
|
118
|
+
value.map { |item| deep_symbolize(item) }
|
|
119
|
+
when Hash
|
|
120
|
+
value.each_with_object({}) do |(key, item), memo|
|
|
121
|
+
memo[key.to_sym] = deep_symbolize(item)
|
|
122
|
+
end
|
|
123
|
+
else
|
|
124
|
+
value
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
private :deep_symbolize
|
|
128
|
+
|
|
129
|
+
def apply_auth_headers(headers, auth)
|
|
130
|
+
kind = (auth[:kind] || :secret).to_sym
|
|
131
|
+
case kind
|
|
132
|
+
when :none
|
|
133
|
+
headers
|
|
134
|
+
when :bearer
|
|
135
|
+
token = auth[:token]
|
|
136
|
+
raise ConfigurationError, "Missing bearer token for this Foil request." if token.nil? || token.empty?
|
|
137
|
+
|
|
138
|
+
headers["Authorization"] = "Bearer #{token}"
|
|
139
|
+
else
|
|
140
|
+
raise ConfigurationError, "Missing Foil secret key. Pass secret_key explicitly or set FOIL_SECRET_KEY." if @secret_key.nil? || @secret_key.empty?
|
|
141
|
+
|
|
142
|
+
headers["Authorization"] = "Bearer #{@secret_key}"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
private :apply_auth_headers
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
class BaseResource
|
|
149
|
+
def initialize(client)
|
|
150
|
+
@client = client
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
def list_result(payload)
|
|
156
|
+
ListResult.new(
|
|
157
|
+
items: payload[:data],
|
|
158
|
+
limit: payload.fetch(:pagination).fetch(:limit),
|
|
159
|
+
has_more: payload.fetch(:pagination).fetch(:has_more),
|
|
160
|
+
next_cursor: payload.fetch(:pagination)[:next_cursor]
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
class SessionsResource < BaseResource
|
|
166
|
+
def list(limit: nil, cursor: nil, verdict: nil, search: nil)
|
|
167
|
+
payload = @client.request_json("GET", "/v1/sessions", query: {
|
|
168
|
+
limit: limit,
|
|
169
|
+
cursor: cursor,
|
|
170
|
+
verdict: verdict,
|
|
171
|
+
search: search
|
|
172
|
+
})
|
|
173
|
+
list_result(payload)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def get(session_id)
|
|
177
|
+
@client.request_json("GET", "/v1/sessions/#{CGI.escape(session_id)}")[:data]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def iter(limit: nil, verdict: nil, search: nil)
|
|
181
|
+
Enumerator.new do |yielder|
|
|
182
|
+
cursor = nil
|
|
183
|
+
loop do
|
|
184
|
+
page = list(limit: limit, cursor: cursor, verdict: verdict, search: search)
|
|
185
|
+
page.items.each { |item| yielder << item }
|
|
186
|
+
break unless page.has_more && page.next_cursor
|
|
187
|
+
|
|
188
|
+
cursor = page.next_cursor
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
class FingerprintsResource < BaseResource
|
|
195
|
+
def list(limit: nil, cursor: nil, search: nil, sort: nil)
|
|
196
|
+
payload = @client.request_json("GET", "/v1/fingerprints", query: {
|
|
197
|
+
limit: limit,
|
|
198
|
+
cursor: cursor,
|
|
199
|
+
search: search,
|
|
200
|
+
sort: sort
|
|
201
|
+
})
|
|
202
|
+
list_result(payload)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def get(visitor_id)
|
|
206
|
+
@client.request_json("GET", "/v1/fingerprints/#{CGI.escape(visitor_id)}")[:data]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def iter(limit: nil, search: nil, sort: nil)
|
|
210
|
+
Enumerator.new do |yielder|
|
|
211
|
+
cursor = nil
|
|
212
|
+
loop do
|
|
213
|
+
page = list(limit: limit, cursor: cursor, search: search, sort: sort)
|
|
214
|
+
page.items.each { |item| yielder << item }
|
|
215
|
+
break unless page.has_more && page.next_cursor
|
|
216
|
+
|
|
217
|
+
cursor = page.next_cursor
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
class ApiKeysResource < BaseResource
|
|
224
|
+
def create(organization_id, name:, type: nil, environment: nil, allowed_origins: nil, scopes: nil)
|
|
225
|
+
payload = @client.request_json("POST", "/v1/organizations/#{CGI.escape(organization_id)}/api-keys", body: compact({
|
|
226
|
+
name: name,
|
|
227
|
+
type: type,
|
|
228
|
+
environment: environment,
|
|
229
|
+
allowed_origins: allowed_origins,
|
|
230
|
+
scopes: scopes
|
|
231
|
+
}))
|
|
232
|
+
payload[:data]
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def list(organization_id, limit: nil, cursor: nil)
|
|
236
|
+
payload = @client.request_json("GET", "/v1/organizations/#{CGI.escape(organization_id)}/api-keys", query: {
|
|
237
|
+
limit: limit,
|
|
238
|
+
cursor: cursor
|
|
239
|
+
})
|
|
240
|
+
list_result(payload)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def update(organization_id, key_id, name: nil, allowed_origins: nil, scopes: nil)
|
|
244
|
+
@client.request_json("PATCH", "/v1/organizations/#{CGI.escape(organization_id)}/api-keys/#{CGI.escape(key_id)}", body: compact({
|
|
245
|
+
name: name,
|
|
246
|
+
allowed_origins: allowed_origins,
|
|
247
|
+
scopes: scopes
|
|
248
|
+
}))[:data]
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def revoke(organization_id, key_id)
|
|
252
|
+
@client.request_json("DELETE", "/v1/organizations/#{CGI.escape(organization_id)}/api-keys/#{CGI.escape(key_id)}")[:data]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def rotate(organization_id, key_id)
|
|
256
|
+
payload = @client.request_json("POST", "/v1/organizations/#{CGI.escape(organization_id)}/api-keys/#{CGI.escape(key_id)}/rotations")
|
|
257
|
+
payload[:data]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
private
|
|
261
|
+
|
|
262
|
+
def compact(hash)
|
|
263
|
+
hash.reject { |_key, value| value.nil? }
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
class OrganizationsResource < BaseResource
|
|
268
|
+
attr_reader :api_keys
|
|
269
|
+
|
|
270
|
+
def initialize(client)
|
|
271
|
+
super(client)
|
|
272
|
+
@api_keys = ApiKeysResource.new(client)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def create(name:, slug:)
|
|
276
|
+
@client.request_json("POST", "/v1/organizations", body: { name: name, slug: slug })[:data]
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def get(organization_id)
|
|
280
|
+
@client.request_json("GET", "/v1/organizations/#{CGI.escape(organization_id)}")[:data]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def update(organization_id, name: nil, status: nil)
|
|
284
|
+
@client.request_json("PATCH", "/v1/organizations/#{CGI.escape(organization_id)}", body: {
|
|
285
|
+
name: name,
|
|
286
|
+
status: status
|
|
287
|
+
}.reject { |_key, value| value.nil? })[:data]
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
class GateResource < BaseResource
|
|
292
|
+
attr_reader :registry, :services, :sessions, :login_sessions, :agent_tokens
|
|
293
|
+
|
|
294
|
+
def initialize(client)
|
|
295
|
+
super(client)
|
|
296
|
+
@registry = GateRegistryResource.new(client)
|
|
297
|
+
@services = GateServicesResource.new(client)
|
|
298
|
+
@sessions = GateSessionsResource.new(client)
|
|
299
|
+
@login_sessions = GateLoginSessionsResource.new(client)
|
|
300
|
+
@agent_tokens = GateAgentTokensResource.new(client)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
class GateRegistryResource < BaseResource
|
|
305
|
+
def list
|
|
306
|
+
@client.request_json("GET", "/v1/gate/registry", auth: { kind: :none })[:data]
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def get(service_id)
|
|
310
|
+
@client.request_json("GET", "/v1/gate/registry/#{CGI.escape(service_id)}", auth: { kind: :none })[:data]
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
class GateServicesResource < BaseResource
|
|
315
|
+
def list
|
|
316
|
+
@client.request_json("GET", "/v1/gate/services")[:data]
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def get(service_id)
|
|
320
|
+
@client.request_json("GET", "/v1/gate/services/#{CGI.escape(service_id)}")[:data]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def create(id:, name:, description:, website:, webhook_endpoint_id:, discoverable: nil, dashboard_login_url: nil, env_vars: nil, docs_url: nil, sdks: nil, branding: nil, consent: nil)
|
|
324
|
+
@client.request_json("POST", "/v1/gate/services", body: compact({
|
|
325
|
+
id: id,
|
|
326
|
+
discoverable: discoverable,
|
|
327
|
+
name: name,
|
|
328
|
+
description: description,
|
|
329
|
+
website: website,
|
|
330
|
+
dashboard_login_url: dashboard_login_url,
|
|
331
|
+
webhook_endpoint_id: webhook_endpoint_id,
|
|
332
|
+
env_vars: env_vars,
|
|
333
|
+
docs_url: docs_url,
|
|
334
|
+
sdks: sdks,
|
|
335
|
+
branding: branding,
|
|
336
|
+
consent: consent
|
|
337
|
+
}))[:data]
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def update(service_id, discoverable: nil, name: nil, description: nil, website: nil, dashboard_login_url: nil, webhook_endpoint_id: nil, env_vars: nil, docs_url: nil, sdks: nil, branding: nil, consent: nil)
|
|
341
|
+
@client.request_json("PATCH", "/v1/gate/services/#{CGI.escape(service_id)}", body: compact({
|
|
342
|
+
discoverable: discoverable,
|
|
343
|
+
name: name,
|
|
344
|
+
description: description,
|
|
345
|
+
website: website,
|
|
346
|
+
dashboard_login_url: dashboard_login_url,
|
|
347
|
+
webhook_endpoint_id: webhook_endpoint_id,
|
|
348
|
+
env_vars: env_vars,
|
|
349
|
+
docs_url: docs_url,
|
|
350
|
+
sdks: sdks,
|
|
351
|
+
branding: branding,
|
|
352
|
+
consent: consent
|
|
353
|
+
}))[:data]
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def disable(service_id)
|
|
357
|
+
@client.request_json("DELETE", "/v1/gate/services/#{CGI.escape(service_id)}")[:data]
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
private
|
|
361
|
+
|
|
362
|
+
def compact(hash)
|
|
363
|
+
hash.reject { |_key, value| value.nil? }
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
class WebhooksResource < BaseResource
|
|
368
|
+
def list_endpoints(organization_id)
|
|
369
|
+
payload = @client.request_json("GET", "/v1/organizations/#{CGI.escape(organization_id)}/webhooks/endpoints")
|
|
370
|
+
list_result(payload)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def create_endpoint(organization_id, name:, url:, event_types:)
|
|
374
|
+
@client.request_json("POST", "/v1/organizations/#{CGI.escape(organization_id)}/webhooks/endpoints", body: {
|
|
375
|
+
name: name,
|
|
376
|
+
url: url,
|
|
377
|
+
event_types: event_types
|
|
378
|
+
})[:data]
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def update_endpoint(organization_id, endpoint_id, **updates)
|
|
382
|
+
@client.request_json("PATCH", "/v1/organizations/#{CGI.escape(organization_id)}/webhooks/endpoints/#{CGI.escape(endpoint_id)}", body: updates)[:data]
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def disable_endpoint(organization_id, endpoint_id)
|
|
386
|
+
@client.request_json("DELETE", "/v1/organizations/#{CGI.escape(organization_id)}/webhooks/endpoints/#{CGI.escape(endpoint_id)}")[:data]
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def rotate_secret(organization_id, endpoint_id)
|
|
390
|
+
@client.request_json("POST", "/v1/organizations/#{CGI.escape(organization_id)}/webhooks/endpoints/#{CGI.escape(endpoint_id)}/rotations")[:data]
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def send_test(organization_id, endpoint_id)
|
|
394
|
+
@client.request_json("POST", "/v1/organizations/#{CGI.escape(organization_id)}/webhooks/endpoints/#{CGI.escape(endpoint_id)}/test")[:data]
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def list_events(organization_id, endpoint_id: nil, type: nil, limit: nil)
|
|
398
|
+
payload = @client.request_json("GET", "/v1/organizations/#{CGI.escape(organization_id)}/events", query: {
|
|
399
|
+
endpoint_id: endpoint_id,
|
|
400
|
+
type: type,
|
|
401
|
+
limit: limit
|
|
402
|
+
})
|
|
403
|
+
list_result(payload)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def retrieve_event(organization_id, event_id)
|
|
407
|
+
@client.request_json("GET", "/v1/organizations/#{CGI.escape(organization_id)}/events/#{CGI.escape(event_id)}")[:data]
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
class GateSessionsResource < BaseResource
|
|
412
|
+
def create(service_id:, account_name:, delivery:, metadata: nil)
|
|
413
|
+
body = {
|
|
414
|
+
service_id: service_id,
|
|
415
|
+
account_name: account_name,
|
|
416
|
+
delivery: delivery
|
|
417
|
+
}
|
|
418
|
+
body[:metadata] = metadata unless metadata.nil?
|
|
419
|
+
|
|
420
|
+
@client.request_json("POST", "/v1/gate/sessions", body: body, auth: { kind: :none })[:data]
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def poll(gate_session_id, poll_token:)
|
|
424
|
+
@client.request_json(
|
|
425
|
+
"GET",
|
|
426
|
+
"/v1/gate/sessions/#{CGI.escape(gate_session_id)}",
|
|
427
|
+
auth: { kind: :bearer, token: poll_token }
|
|
428
|
+
)[:data]
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def acknowledge(gate_session_id, poll_token:, ack_token:)
|
|
432
|
+
@client.request_json(
|
|
433
|
+
"POST",
|
|
434
|
+
"/v1/gate/sessions/#{CGI.escape(gate_session_id)}/ack",
|
|
435
|
+
body: { ack_token: ack_token },
|
|
436
|
+
auth: { kind: :bearer, token: poll_token }
|
|
437
|
+
)[:data]
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
class GateLoginSessionsResource < BaseResource
|
|
442
|
+
def create(service_id:, agent_token:)
|
|
443
|
+
@client.request_json(
|
|
444
|
+
"POST",
|
|
445
|
+
"/v1/gate/login-sessions",
|
|
446
|
+
body: { service_id: service_id },
|
|
447
|
+
auth: { kind: :bearer, token: agent_token }
|
|
448
|
+
)[:data]
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def consume(code:)
|
|
452
|
+
@client.request_json("POST", "/v1/gate/login-sessions/consume", body: { code: code })[:data]
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
class GateAgentTokensResource < BaseResource
|
|
457
|
+
def verify(agent_token:)
|
|
458
|
+
@client.request_json("POST", "/v1/gate/agent-tokens/verify", body: { agent_token: agent_token })[:data]
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def revoke(agent_token:)
|
|
462
|
+
@client.request_json(
|
|
463
|
+
"POST",
|
|
464
|
+
"/v1/gate/agent-tokens/revoke",
|
|
465
|
+
body: { agent_token: agent_token },
|
|
466
|
+
expect_content: false
|
|
467
|
+
)
|
|
468
|
+
nil
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require "openssl"
|
|
2
|
+
require "rubygems"
|
|
3
|
+
|
|
4
|
+
module Foil
|
|
5
|
+
module Server
|
|
6
|
+
module CryptoSupport
|
|
7
|
+
MIN_SUPPORTED_RUBY_VERSION = Gem::Version.new("3.3.0")
|
|
8
|
+
UNSUPPORTED_RUNTIME_MESSAGE = "Foil 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
|