foil-server 0.3.5 → 0.4.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -29
  3. data/lib/foil/server/client.rb +6 -155
  4. data/lib/foil/server/version.rb +1 -1
  5. data/lib/foil/server/webhooks.rb +71 -0
  6. data/lib/foil/server.rb +8 -6
  7. data/spec/README.md +14 -28
  8. data/spec/fixtures/api/organizations/api-key-create.json +1 -0
  9. data/spec/fixtures/api/organizations/api-key-list.json +1 -0
  10. data/spec/fixtures/api/organizations/api-key-revoke.json +1 -0
  11. data/spec/fixtures/api/organizations/api-key-rotate.json +1 -0
  12. data/spec/fixtures/api/organizations/api-key-update.json +1 -0
  13. data/spec/fixtures/api/sessions/detail.json +8 -1
  14. data/spec/fixtures/manifest.json +0 -85
  15. data/spec/fixtures/webhooks/signature.json +9 -0
  16. data/spec/openapi.json +1683 -6333
  17. metadata +4 -22
  18. data/lib/foil/server/gate_delivery.rb +0 -332
  19. data/spec/fixtures/api/gate/agent-token-revoke.json +0 -3
  20. data/spec/fixtures/api/gate/agent-token-verify.json +0 -12
  21. data/spec/fixtures/api/gate/login-session-consume.json +0 -10
  22. data/spec/fixtures/api/gate/login-session-create.json +0 -12
  23. data/spec/fixtures/api/gate/registry-detail.json +0 -45
  24. data/spec/fixtures/api/gate/registry-list.json +0 -47
  25. data/spec/fixtures/api/gate/service-create.json +0 -49
  26. data/spec/fixtures/api/gate/service-detail.json +0 -49
  27. data/spec/fixtures/api/gate/service-disable.json +0 -49
  28. data/spec/fixtures/api/gate/service-update.json +0 -49
  29. data/spec/fixtures/api/gate/services-list.json +0 -51
  30. data/spec/fixtures/api/gate/session-ack.json +0 -10
  31. data/spec/fixtures/api/gate/session-create.json +0 -13
  32. data/spec/fixtures/api/gate/session-poll.json +0 -36
  33. data/spec/fixtures/gate-delivery/approved-webhook-payload.valid.json +0 -19
  34. data/spec/fixtures/gate-delivery/delivery-request.json +0 -9
  35. data/spec/fixtures/gate-delivery/env-policy.json +0 -40
  36. data/spec/fixtures/gate-delivery/vector.v1.json +0 -28
  37. data/spec/fixtures/gate-delivery/webhook-signature.json +0 -9
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foil-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ABXY Labs
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-20 00:00:00.000000000 Z
11
+ date: 2026-10-05 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Customer-facing Ruby SDK for Foil Sessions, Fingerprints, Organizations,
14
14
  and sealed token verification.
@@ -24,28 +24,14 @@ files:
24
24
  - lib/foil/server/client.rb
25
25
  - lib/foil/server/crypto_support.rb
26
26
  - lib/foil/server/errors.rb
27
- - lib/foil/server/gate_delivery.rb
28
27
  - lib/foil/server/sealed_token.rb
29
28
  - lib/foil/server/types.rb
30
29
  - lib/foil/server/version.rb
30
+ - lib/foil/server/webhooks.rb
31
31
  - spec/LICENSE
32
32
  - spec/README.md
33
33
  - spec/fixtures/api/fingerprints/detail.json
34
34
  - spec/fixtures/api/fingerprints/list.json
35
- - spec/fixtures/api/gate/agent-token-revoke.json
36
- - spec/fixtures/api/gate/agent-token-verify.json
37
- - spec/fixtures/api/gate/login-session-consume.json
38
- - spec/fixtures/api/gate/login-session-create.json
39
- - spec/fixtures/api/gate/registry-detail.json
40
- - spec/fixtures/api/gate/registry-list.json
41
- - spec/fixtures/api/gate/service-create.json
42
- - spec/fixtures/api/gate/service-detail.json
43
- - spec/fixtures/api/gate/service-disable.json
44
- - spec/fixtures/api/gate/service-update.json
45
- - spec/fixtures/api/gate/services-list.json
46
- - spec/fixtures/api/gate/session-ack.json
47
- - spec/fixtures/api/gate/session-create.json
48
- - spec/fixtures/api/gate/session-poll.json
49
35
  - spec/fixtures/api/organizations/api-key-create.json
50
36
  - spec/fixtures/api/organizations/api-key-list.json
51
37
  - spec/fixtures/api/organizations/api-key-revoke.json
@@ -60,15 +46,11 @@ files:
60
46
  - spec/fixtures/errors/missing-api-key.json
61
47
  - spec/fixtures/errors/not-found.json
62
48
  - spec/fixtures/errors/validation-error.json
63
- - spec/fixtures/gate-delivery/approved-webhook-payload.valid.json
64
- - spec/fixtures/gate-delivery/delivery-request.json
65
- - spec/fixtures/gate-delivery/env-policy.json
66
- - spec/fixtures/gate-delivery/vector.v1.json
67
- - spec/fixtures/gate-delivery/webhook-signature.json
68
49
  - spec/fixtures/manifest.json
69
50
  - spec/fixtures/sealed-token/invalid.json
70
51
  - spec/fixtures/sealed-token/vector.v1.json
71
52
  - spec/fixtures/sealed-token/vector.v2.json
53
+ - spec/fixtures/webhooks/signature.json
72
54
  - spec/openapi.json
73
55
  - spec/sealed-token.md
74
56
  homepage: https://github.com/abxy-labs/foil-server-ruby
@@ -1,332 +0,0 @@
1
- require "base64"
2
- require "digest"
3
- require "json"
4
- require "openssl"
5
-
6
- module Foil
7
- module Server
8
- module GateDelivery
9
- GATE_DELIVERY_VERSION = 1
10
- GATE_DELIVERY_ALGORITHM = "x25519-hkdf-sha256/aes-256-gcm"
11
- GATE_AGENT_TOKEN_ENV_SUFFIX = "_GATE_AGENT_TOKEN"
12
- BLOCKED_GATE_ENV_VAR_KEYS = %w[
13
- BASH_ENV
14
- BROWSER
15
- CDPATH
16
- DYLD_INSERT_LIBRARIES
17
- DYLD_LIBRARY_PATH
18
- EDITOR
19
- ENV
20
- GIT_ASKPASS
21
- GIT_SSH_COMMAND
22
- HOME
23
- LD_LIBRARY_PATH
24
- LD_PRELOAD
25
- NODE_OPTIONS
26
- NODE_PATH
27
- PATH
28
- PERL5OPT
29
- PERLLIB
30
- PROMPT_COMMAND
31
- PYTHONHOME
32
- PYTHONPATH
33
- PYTHONSTARTUP
34
- RUBYLIB
35
- RUBYOPT
36
- SHELLOPTS
37
- SSH_ASKPASS
38
- VISUAL
39
- XDG_CONFIG_HOME
40
- ].freeze
41
- BLOCKED_GATE_ENV_VAR_PREFIXES = %w[NPM_CONFIG_ BUN_CONFIG_ GIT_CONFIG_].freeze
42
- WEBHOOK_EVENT_TYPES = %w[
43
- session.fingerprint.calculated
44
- session.result.persisted
45
- gate.session.approved
46
- webhook.test
47
- ].freeze
48
- GATE_DELIVERY_HKDF_INFO = "foil-gate-delivery:v1".b.freeze
49
- X25519_SPKI_PREFIX = ["302a300506032b656e032100"].pack("H*").freeze
50
-
51
- module_function
52
-
53
- def derive_gate_agent_token_env_key(service_id)
54
- normalized = service_id.to_s.strip.gsub(/[^A-Za-z0-9]+/, "_").gsub(/^_+|_+$/, "").gsub(/_+/, "_").upcase
55
- raise ArgumentError, "service_id is required to derive a Gate agent token env key" if normalized.empty?
56
-
57
- return "FOIL#{GATE_AGENT_TOKEN_ENV_SUFFIX}" if ["FOIL", "FOIL"].include?(normalized)
58
-
59
- "#{normalized}#{GATE_AGENT_TOKEN_ENV_SUFFIX}"
60
- end
61
-
62
- def is_gate_managed_env_var_key(key)
63
- key == "FOIL_AGENT_TOKEN" || key.to_s.end_with?(GATE_AGENT_TOKEN_ENV_SUFFIX)
64
- end
65
-
66
- def is_blocked_gate_env_var_key(key)
67
- normalized = key.to_s.strip.upcase
68
- BLOCKED_GATE_ENV_VAR_KEYS.include?(normalized) || BLOCKED_GATE_ENV_VAR_PREFIXES.any? { |prefix| normalized.start_with?(prefix) }
69
- end
70
-
71
- def raw_x25519_public_key_from_key_object(public_key)
72
- if public_key.respond_to?(:raw_public_key)
73
- raw_public_key = public_key.raw_public_key
74
- raise ArgumentError, "X25519 public key must be 32 bytes" unless raw_public_key.bytesize == 32
75
-
76
- return raw_public_key
77
- end
78
-
79
- der = public_key.public_to_der
80
- raise ArgumentError, "Unexpected X25519 public key encoding" unless der.bytesize == X25519_SPKI_PREFIX.bytesize + 32
81
- raise ArgumentError, "Unexpected X25519 public key prefix" unless der.byteslice(0, X25519_SPKI_PREFIX.bytesize) == X25519_SPKI_PREFIX
82
-
83
- der.byteslice(X25519_SPKI_PREFIX.bytesize, 32)
84
- end
85
-
86
- def key_id_for_raw_x25519_public_key(raw_public_key)
87
- raise ArgumentError, "X25519 public key must be 32 bytes" unless raw_public_key.bytesize == 32
88
-
89
- Base64.urlsafe_encode64(Digest::SHA256.digest(raw_public_key), padding: false)
90
- end
91
-
92
- def create_delivery_key_pair
93
- CryptoSupport.ensure_supported_runtime!
94
- private_key = OpenSSL::PKey.generate_key("X25519")
95
- raw_public_key = raw_x25519_public_key_from_key_object(private_key)
96
- {
97
- delivery: {
98
- version: GATE_DELIVERY_VERSION,
99
- algorithm: GATE_DELIVERY_ALGORITHM,
100
- key_id: key_id_for_raw_x25519_public_key(raw_public_key),
101
- public_key: Base64.urlsafe_encode64(raw_public_key, padding: false)
102
- },
103
- private_key: private_key
104
- }
105
- end
106
-
107
- def export_delivery_private_key_pkcs8(private_key)
108
- CryptoSupport.ensure_supported_runtime!
109
- Base64.urlsafe_encode64(private_key.private_to_der, padding: false)
110
- end
111
-
112
- def import_delivery_private_key_pkcs8(value)
113
- CryptoSupport.ensure_supported_runtime!
114
- OpenSSL::PKey.read(b64url_decode(value, "delivery.private_key_pkcs8"))
115
- end
116
-
117
- def validate_gate_delivery_request(value)
118
- candidate = symbolize(value)
119
- raise ArgumentError, "delivery.version must be 1" unless candidate[:version] == GATE_DELIVERY_VERSION
120
- raise ArgumentError, "delivery.algorithm must be #{GATE_DELIVERY_ALGORITHM}" unless candidate[:algorithm] == GATE_DELIVERY_ALGORITHM
121
- raise ArgumentError, "delivery.public_key is required" if candidate[:public_key].to_s.empty?
122
- raise ArgumentError, "delivery.key_id is required" if candidate[:key_id].to_s.empty?
123
-
124
- raw_public_key = b64url_decode(candidate[:public_key], "delivery.public_key")
125
- raise ArgumentError, "delivery.public_key must be a raw X25519 public key" unless raw_public_key.bytesize == 32
126
- raise ArgumentError, "delivery.key_id does not match delivery.public_key" unless key_id_for_raw_x25519_public_key(raw_public_key) == candidate[:key_id]
127
-
128
- {
129
- version: GATE_DELIVERY_VERSION,
130
- algorithm: GATE_DELIVERY_ALGORITHM,
131
- key_id: candidate[:key_id],
132
- public_key: candidate[:public_key]
133
- }
134
- end
135
-
136
- def create_encrypted_delivery_response(input)
137
- {
138
- encrypted_delivery: encrypt_gate_delivery_payload(
139
- input.fetch(:delivery),
140
- {
141
- version: GATE_DELIVERY_VERSION,
142
- outputs: input.fetch(:outputs)
143
- }
144
- )
145
- }
146
- end
147
-
148
- def create_gate_approved_webhook_response(input)
149
- create_encrypted_delivery_response(input)
150
- end
151
-
152
- def validate_encrypted_gate_delivery_envelope(value)
153
- candidate = symbolize(value)
154
- raise ArgumentError, "encrypted_delivery.version must be 1" unless candidate[:version] == GATE_DELIVERY_VERSION
155
- raise ArgumentError, "encrypted_delivery.algorithm must be #{GATE_DELIVERY_ALGORITHM}" unless candidate[:algorithm] == GATE_DELIVERY_ALGORITHM
156
- %i[key_id ephemeral_public_key salt iv ciphertext tag].each do |field|
157
- raise ArgumentError, "encrypted_delivery.#{field} is required" if candidate[field].to_s.empty?
158
- end
159
- raise ArgumentError, "encrypted_delivery.ephemeral_public_key must be 32 bytes" unless b64url_decode(candidate[:ephemeral_public_key], "encrypted_delivery.ephemeral_public_key").bytesize == 32
160
- raise ArgumentError, "encrypted_delivery.salt must be 32 bytes" unless b64url_decode(candidate[:salt], "encrypted_delivery.salt").bytesize == 32
161
- raise ArgumentError, "encrypted_delivery.iv must be 12 bytes" unless b64url_decode(candidate[:iv], "encrypted_delivery.iv").bytesize == 12
162
- raise ArgumentError, "encrypted_delivery.tag must be 16 bytes" unless b64url_decode(candidate[:tag], "encrypted_delivery.tag").bytesize == 16
163
-
164
- candidate
165
- end
166
-
167
- def encrypt_gate_delivery_payload(delivery, payload)
168
- CryptoSupport.ensure_supported_runtime!
169
- validated_delivery = validate_gate_delivery_request(delivery)
170
- payload = symbolize(payload)
171
- raise ArgumentError, "Gate delivery payload version must be 1" unless payload[:version] == GATE_DELIVERY_VERSION
172
-
173
- recipient_public_key = OpenSSL::PKey.read(X25519_SPKI_PREFIX + b64url_decode(validated_delivery[:public_key], "delivery.public_key"))
174
- ephemeral_private_key = OpenSSL::PKey.generate_key("X25519")
175
- shared_secret = ephemeral_private_key.derive(recipient_public_key)
176
- salt = OpenSSL::Random.random_bytes(32)
177
- iv = OpenSSL::Random.random_bytes(12)
178
- key = OpenSSL::KDF.hkdf(shared_secret, salt: salt, info: GATE_DELIVERY_HKDF_INFO, length: 32, hash: "SHA256")
179
-
180
- cipher = OpenSSL::Cipher.new("aes-256-gcm")
181
- cipher.encrypt
182
- cipher.key = key
183
- cipher.iv = iv
184
- ciphertext = cipher.update(JSON.generate(compact_payload(payload))) + cipher.final
185
- tag = cipher.auth_tag
186
-
187
- {
188
- version: GATE_DELIVERY_VERSION,
189
- algorithm: GATE_DELIVERY_ALGORITHM,
190
- key_id: validated_delivery[:key_id],
191
- ephemeral_public_key: Base64.urlsafe_encode64(raw_x25519_public_key_from_key_object(ephemeral_private_key), padding: false),
192
- salt: Base64.urlsafe_encode64(salt, padding: false),
193
- iv: Base64.urlsafe_encode64(iv, padding: false),
194
- ciphertext: Base64.urlsafe_encode64(ciphertext, padding: false),
195
- tag: Base64.urlsafe_encode64(tag, padding: false)
196
- }
197
- end
198
-
199
- def decrypt_gate_delivery_envelope(private_key, envelope)
200
- CryptoSupport.ensure_supported_runtime!
201
- validated = validate_encrypted_gate_delivery_envelope(envelope)
202
- shared_secret = private_key.derive(
203
- OpenSSL::PKey.read(X25519_SPKI_PREFIX + b64url_decode(validated[:ephemeral_public_key], "encrypted_delivery.ephemeral_public_key"))
204
- )
205
- key = OpenSSL::KDF.hkdf(
206
- shared_secret,
207
- salt: b64url_decode(validated[:salt], "encrypted_delivery.salt"),
208
- info: GATE_DELIVERY_HKDF_INFO,
209
- length: 32,
210
- hash: "SHA256"
211
- )
212
- cipher = OpenSSL::Cipher.new("aes-256-gcm")
213
- cipher.decrypt
214
- cipher.key = key
215
- cipher.iv = b64url_decode(validated[:iv], "encrypted_delivery.iv")
216
- cipher.auth_tag = b64url_decode(validated[:tag], "encrypted_delivery.tag")
217
- cipher.auth_data = ""
218
- plaintext = cipher.update(b64url_decode(validated[:ciphertext], "encrypted_delivery.ciphertext")) + cipher.final
219
- payload = JSON.parse(plaintext)
220
- raise ArgumentError, "encrypted_delivery payload must be an object" unless payload.is_a?(Hash)
221
-
222
- symbolize(payload).tap do |candidate|
223
- raise ArgumentError, "encrypted_delivery payload version must be 1" unless candidate[:version] == GATE_DELIVERY_VERSION
224
- raise ArgumentError, "encrypted_delivery payload outputs must be an object" unless candidate[:outputs].is_a?(Hash)
225
- candidate[:outputs].each do |key_name, item|
226
- raise ArgumentError, "encrypted_delivery output #{key_name} must be a string" unless item.is_a?(String)
227
- end
228
- end
229
- rescue JSON::ParserError
230
- raise ArgumentError, "encrypted_delivery decrypted to invalid JSON"
231
- end
232
-
233
- def validate_gate_approved_webhook_payload(value)
234
- candidate = symbolize(value)
235
- raise ArgumentError, "webhook payload must not include event; use the webhook event envelope type" if candidate.key?(:event)
236
- raise ArgumentError, "service_id is required" if candidate[:service_id].to_s.empty?
237
- raise ArgumentError, "gate_session_id is required" if candidate[:gate_session_id].to_s.empty?
238
- raise ArgumentError, "gate_account_id is required" if candidate[:gate_account_id].to_s.empty?
239
- raise ArgumentError, "account_name is required" if candidate[:account_name].to_s.empty?
240
- raise ArgumentError, "metadata must be an object or null" unless candidate[:metadata].nil? || candidate[:metadata].is_a?(Hash)
241
- raise ArgumentError, "foil must be an object" unless candidate[:foil].is_a?(Hash)
242
- verdict = candidate[:foil][:verdict]
243
- raise ArgumentError, "foil.verdict is invalid" unless %w[bot human inconclusive].include?(verdict)
244
- score = candidate[:foil][:score]
245
- raise ArgumentError, "foil.score must be a number or null" unless score.nil? || score.is_a?(Numeric)
246
-
247
- {
248
- service_id: candidate[:service_id],
249
- gate_session_id: candidate[:gate_session_id],
250
- gate_account_id: candidate[:gate_account_id],
251
- account_name: candidate[:account_name],
252
- metadata: candidate[:metadata]&.dup,
253
- foil: {
254
- verdict: verdict,
255
- score: score
256
- },
257
- delivery: validate_gate_delivery_request(candidate[:delivery])
258
- }
259
- end
260
-
261
- def verify_gate_webhook_signature(secret:, timestamp:, raw_body:, signature:, max_age_seconds: 300, now_seconds: nil)
262
- parsed_timestamp = Integer(timestamp)
263
- current = now_seconds || Time.now.to_i
264
- return false if (current - parsed_timestamp).abs > max_age_seconds
265
-
266
- expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}")
267
- secure_compare(expected, signature)
268
- rescue ArgumentError
269
- false
270
- end
271
-
272
- def parse_webhook_event(raw_body)
273
- envelope = symbolize(JSON.parse(raw_body))
274
- raise ArgumentError, "webhook event object must be webhook_event" unless envelope[:object] == "webhook_event"
275
- raise ArgumentError, "webhook event id is required" if envelope[:id].to_s.empty?
276
- raise ArgumentError, "webhook event type is required" if envelope[:type].to_s.empty?
277
- raise ArgumentError, "unsupported webhook event type: #{envelope[:type]}" unless WEBHOOK_EVENT_TYPES.include?(envelope[:type])
278
- raise ArgumentError, "webhook event created timestamp is required" if envelope[:created].to_s.empty?
279
- raise ArgumentError, "webhook event data must be an object" unless envelope[:data].is_a?(Hash)
280
-
281
- envelope[:data] = validate_gate_approved_webhook_payload(envelope[:data]) if envelope[:type] == "gate.session.approved"
282
- envelope
283
- end
284
-
285
- def verify_and_parse_webhook_event(secret:, timestamp:, raw_body:, signature:, max_age_seconds: 300, now_seconds: nil)
286
- unless verify_gate_webhook_signature(secret: secret, timestamp: timestamp, raw_body: raw_body, signature: signature, max_age_seconds: max_age_seconds, now_seconds: now_seconds)
287
- raise ArgumentError, "Invalid Foil webhook signature"
288
- end
289
- parse_webhook_event(raw_body)
290
- end
291
-
292
- def symbolize(value)
293
- case value
294
- when Array
295
- value.map { |item| symbolize(item) }
296
- when Hash
297
- value.each_with_object({}) do |(key, item), memo|
298
- memo[key.to_sym] = symbolize(item)
299
- end
300
- else
301
- value
302
- end
303
- end
304
- private_class_method :symbolize
305
-
306
- def compact_payload(payload)
307
- {
308
- version: payload[:version],
309
- outputs: payload[:outputs],
310
- **(payload[:ack_token] ? { ack_token: payload[:ack_token] } : {})
311
- }
312
- end
313
- private_class_method :compact_payload
314
-
315
- def b64url_decode(value, label)
316
- Base64.urlsafe_decode64(value.to_s)
317
- rescue ArgumentError => error
318
- raise ArgumentError, "Invalid #{label}: #{error.message}"
319
- end
320
- private_class_method :b64url_decode
321
-
322
- def secure_compare(left, right)
323
- return false unless left.bytesize == right.bytesize
324
-
325
- result = 0
326
- left.bytes.zip(right.bytes) { |a, b| result |= a ^ b }
327
- result.zero?
328
- end
329
- private_class_method :secure_compare
330
- end
331
- end
332
- end
@@ -1,3 +0,0 @@
1
- {
2
- "status": 204
3
- }
@@ -1,12 +0,0 @@
1
- {
2
- "data": {
3
- "valid": true,
4
- "gate_account_id": "gacct_0123456789abcdefghjkmnpqrs",
5
- "status": "active",
6
- "created_at": "2026-04-01T12:00:00.000Z",
7
- "expires_at": "2026-06-30T12:00:00.000Z"
8
- },
9
- "meta": {
10
- "request_id": "req_0123456789abcdef0123456789abcdef"
11
- }
12
- }
@@ -1,10 +0,0 @@
1
- {
2
- "data": {
3
- "object": "gate_dashboard_login",
4
- "gate_account_id": "gacct_0123456789abcdefghjkmnpqrs",
5
- "account_name": "my-project"
6
- },
7
- "meta": {
8
- "request_id": "req_0123456789abcdef0123456789abcdef"
9
- }
10
- }
@@ -1,12 +0,0 @@
1
- {
2
- "data": {
3
- "object": "gate_login_session",
4
- "id": "gate_1123456789abcdefghjkmnpqrs",
5
- "status": "pending",
6
- "consent_url": "https://usefoil.com/gate?session=gate_1123456789abcdefghjkmnpqrs",
7
- "expires_at": "2026-04-04T20:20:00.000Z"
8
- },
9
- "meta": {
10
- "request_id": "req_0123456789abcdef0123456789abcdef"
11
- }
12
- }
@@ -1,45 +0,0 @@
1
- {
2
- "data": {
3
- "id": "foil",
4
- "status": "active",
5
- "discoverable": true,
6
- "name": "Foil",
7
- "description": "Detect AI agents and malicious users",
8
- "website": "https://usefoil.com",
9
- "dashboard_login_url": "https://dashboard.usefoil.com/auth/gate",
10
- "env_vars": [
11
- {
12
- "name": "Publishable key",
13
- "key": "FOIL_PUBLISHABLE_KEY",
14
- "secret": false
15
- },
16
- {
17
- "name": "Secret key",
18
- "key": "FOIL_SECRET_KEY",
19
- "secret": true
20
- }
21
- ],
22
- "docs_url": "https://usefoil.com/docs",
23
- "sdks": [
24
- {
25
- "label": "Node",
26
- "install": "npm install @abxy/foil-server",
27
- "url": "https://www.npmjs.com/package/@abxy/foil-server"
28
- }
29
- ],
30
- "branding": {
31
- "logo_url": "https://usefoil.com/logo.png",
32
- "primary_color": "#117BE7",
33
- "secondary_color": "#0B5CAD",
34
- "ascii_art": "FOIL",
35
- "verified": true
36
- },
37
- "consent": {
38
- "terms_url": "https://usefoil.com/terms",
39
- "privacy_url": "https://usefoil.com/privacy"
40
- }
41
- },
42
- "meta": {
43
- "request_id": "req_0123456789abcdef0123456789abcdef"
44
- }
45
- }
@@ -1,47 +0,0 @@
1
- {
2
- "data": [
3
- {
4
- "id": "foil",
5
- "status": "active",
6
- "discoverable": true,
7
- "name": "Foil",
8
- "description": "Detect AI agents and malicious users",
9
- "website": "https://usefoil.com",
10
- "dashboard_login_url": "https://dashboard.usefoil.com/auth/gate",
11
- "env_vars": [
12
- {
13
- "name": "Publishable key",
14
- "key": "FOIL_PUBLISHABLE_KEY",
15
- "secret": false
16
- },
17
- {
18
- "name": "Secret key",
19
- "key": "FOIL_SECRET_KEY",
20
- "secret": true
21
- }
22
- ],
23
- "docs_url": "https://usefoil.com/docs",
24
- "sdks": [
25
- {
26
- "label": "Node",
27
- "install": "npm install @abxy/foil-server",
28
- "url": "https://www.npmjs.com/package/@abxy/foil-server"
29
- }
30
- ],
31
- "branding": {
32
- "logo_url": "https://usefoil.com/logo.png",
33
- "primary_color": "#117BE7",
34
- "secondary_color": "#0B5CAD",
35
- "ascii_art": "FOIL",
36
- "verified": true
37
- },
38
- "consent": {
39
- "terms_url": "https://usefoil.com/terms",
40
- "privacy_url": "https://usefoil.com/privacy"
41
- }
42
- }
43
- ],
44
- "meta": {
45
- "request_id": "req_0123456789abcdef0123456789abcdef"
46
- }
47
- }
@@ -1,49 +0,0 @@
1
- {
2
- "data": {
3
- "object": "gate_service",
4
- "id": "acme_prod",
5
- "status": "active",
6
- "discoverable": false,
7
- "name": "Acme Production",
8
- "description": "Acme production signup flow",
9
- "website": "https://acme.example.com",
10
- "dashboard_login_url": "https://dashboard.acme.example.com/auth/gate",
11
- "env_vars": [
12
- {
13
- "name": "Publishable key",
14
- "key": "ACME_PUBLISHABLE_KEY",
15
- "secret": false
16
- },
17
- {
18
- "name": "Secret key",
19
- "key": "ACME_SECRET_KEY",
20
- "secret": true
21
- }
22
- ],
23
- "docs_url": "https://docs.acme.example.com/signup",
24
- "sdks": [
25
- {
26
- "label": "Node",
27
- "install": "npm install @acme/sdk",
28
- "url": "https://www.npmjs.com/package/@acme/sdk"
29
- }
30
- ],
31
- "branding": {
32
- "logo_url": "https://acme.example.com/logo.png",
33
- "primary_color": "#117BE7",
34
- "secondary_color": "#0B5CAD",
35
- "ascii_art": "ACME",
36
- "verified": false
37
- },
38
- "consent": {
39
- "terms_url": "https://acme.example.com/terms",
40
- "privacy_url": "https://acme.example.com/privacy"
41
- },
42
- "created_at": "2026-04-03T20:00:00.000Z",
43
- "updated_at": "2026-04-03T20:00:00.000Z",
44
- "webhook_endpoint_id": "we_0123456789abcdef0123456789abcdef"
45
- },
46
- "meta": {
47
- "request_id": "req_0123456789abcdef0123456789abcdef"
48
- }
49
- }
@@ -1,49 +0,0 @@
1
- {
2
- "data": {
3
- "object": "gate_service",
4
- "id": "acme_prod",
5
- "status": "active",
6
- "discoverable": false,
7
- "name": "Acme Production",
8
- "description": "Acme production signup flow",
9
- "website": "https://acme.example.com",
10
- "dashboard_login_url": "https://dashboard.acme.example.com/auth/gate",
11
- "env_vars": [
12
- {
13
- "name": "Publishable key",
14
- "key": "ACME_PUBLISHABLE_KEY",
15
- "secret": false
16
- },
17
- {
18
- "name": "Secret key",
19
- "key": "ACME_SECRET_KEY",
20
- "secret": true
21
- }
22
- ],
23
- "docs_url": "https://docs.acme.example.com/signup",
24
- "sdks": [
25
- {
26
- "label": "Node",
27
- "install": "npm install @acme/sdk",
28
- "url": "https://www.npmjs.com/package/@acme/sdk"
29
- }
30
- ],
31
- "branding": {
32
- "logo_url": "https://acme.example.com/logo.png",
33
- "primary_color": "#117BE7",
34
- "secondary_color": "#0B5CAD",
35
- "ascii_art": "ACME",
36
- "verified": false
37
- },
38
- "consent": {
39
- "terms_url": "https://acme.example.com/terms",
40
- "privacy_url": "https://acme.example.com/privacy"
41
- },
42
- "created_at": "2026-04-03T20:00:00.000Z",
43
- "updated_at": "2026-04-03T20:05:00.000Z",
44
- "webhook_endpoint_id": "we_0123456789abcdef0123456789abcdef"
45
- },
46
- "meta": {
47
- "request_id": "req_0123456789abcdef0123456789abcdef"
48
- }
49
- }
@@ -1,49 +0,0 @@
1
- {
2
- "data": {
3
- "object": "gate_service",
4
- "id": "acme_prod",
5
- "status": "disabled",
6
- "discoverable": false,
7
- "name": "Acme Production",
8
- "description": "Acme production signup flow",
9
- "website": "https://acme.example.com",
10
- "dashboard_login_url": "https://dashboard.acme.example.com/auth/gate",
11
- "env_vars": [
12
- {
13
- "name": "Publishable key",
14
- "key": "ACME_PUBLISHABLE_KEY",
15
- "secret": false
16
- },
17
- {
18
- "name": "Secret key",
19
- "key": "ACME_SECRET_KEY",
20
- "secret": true
21
- }
22
- ],
23
- "docs_url": "https://docs.acme.example.com/signup",
24
- "sdks": [
25
- {
26
- "label": "Node",
27
- "install": "npm install @acme/sdk",
28
- "url": "https://www.npmjs.com/package/@acme/sdk"
29
- }
30
- ],
31
- "branding": {
32
- "logo_url": "https://acme.example.com/logo.png",
33
- "primary_color": "#117BE7",
34
- "secondary_color": "#0B5CAD",
35
- "ascii_art": "ACME",
36
- "verified": false
37
- },
38
- "consent": {
39
- "terms_url": "https://acme.example.com/terms",
40
- "privacy_url": "https://acme.example.com/privacy"
41
- },
42
- "created_at": "2026-04-03T20:00:00.000Z",
43
- "updated_at": "2026-04-03T20:15:00.000Z",
44
- "webhook_endpoint_id": "we_0123456789abcdef0123456789abcdef"
45
- },
46
- "meta": {
47
- "request_id": "req_0123456789abcdef0123456789abcdef"
48
- }
49
- }