foil-server 0.2.3__py3-none-any.whl
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.
- foil_server/__init__.py +130 -0
- foil_server/client.py +1032 -0
- foil_server/errors.py +26 -0
- foil_server/gate_delivery.py +444 -0
- foil_server/sealed_token.py +121 -0
- foil_server/types.py +488 -0
- foil_server-0.2.3.dist-info/METADATA +196 -0
- foil_server-0.2.3.dist-info/RECORD +10 -0
- foil_server-0.2.3.dist-info/WHEEL +4 -0
- foil_server-0.2.3.dist-info/licenses/LICENSE +21 -0
foil_server/client.py
ADDED
|
@@ -0,0 +1,1032 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Iterator
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .errors import FoilApiError, FoilConfigurationError
|
|
9
|
+
from .types import (
|
|
10
|
+
ApiKey,
|
|
11
|
+
AgentTokenVerification,
|
|
12
|
+
Attribution,
|
|
13
|
+
Decision,
|
|
14
|
+
DecisionManipulation,
|
|
15
|
+
Event,
|
|
16
|
+
EventSubject,
|
|
17
|
+
GateDashboardLogin,
|
|
18
|
+
GateDeliveryBundle,
|
|
19
|
+
GateDeliveryEnvelope,
|
|
20
|
+
GateLoginSession,
|
|
21
|
+
GateManagedService,
|
|
22
|
+
GateRegistryEntry,
|
|
23
|
+
GateServiceBranding,
|
|
24
|
+
GateServiceConsent,
|
|
25
|
+
GateServiceEnvVar,
|
|
26
|
+
GateServiceSdkInstall,
|
|
27
|
+
GateSessionCreate,
|
|
28
|
+
GateSessionDeliveryAcknowledgement,
|
|
29
|
+
GateSessionPollData,
|
|
30
|
+
IssuedApiKey,
|
|
31
|
+
ListResult,
|
|
32
|
+
RequestContext,
|
|
33
|
+
ScoreBreakdown,
|
|
34
|
+
SessionDetail,
|
|
35
|
+
SessionDecision,
|
|
36
|
+
SessionDetailRequest,
|
|
37
|
+
SessionSummary,
|
|
38
|
+
Organization,
|
|
39
|
+
VerifiedFoilSignal,
|
|
40
|
+
VerificationResult,
|
|
41
|
+
VerifiedFoilToken,
|
|
42
|
+
VisitorFingerprintActivity,
|
|
43
|
+
VisitorFingerprintAnchors,
|
|
44
|
+
VisitorFingerprintComponents,
|
|
45
|
+
VisitorFingerprintDetail,
|
|
46
|
+
VisitorFingerprintLatestRequest,
|
|
47
|
+
VisitorFingerprintLifecycle,
|
|
48
|
+
VisitorFingerprintLink,
|
|
49
|
+
VisitorFingerprintSessionSummary,
|
|
50
|
+
VisitorFingerprintStorage,
|
|
51
|
+
VisitorFingerprintSummary,
|
|
52
|
+
WebhookDelivery,
|
|
53
|
+
WebhookEndpoint,
|
|
54
|
+
WebhookTest,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
DEFAULT_BASE_URL = "https://api.usefoil.com"
|
|
58
|
+
DEFAULT_TIMEOUT = 30.0
|
|
59
|
+
SDK_CLIENT_HEADER = "foil-server-python/0.1.0"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _compact_query(query: dict[str, Any]) -> dict[str, Any]:
|
|
63
|
+
return {key: value for key, value in query.items() if value is not None}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _parse_decision(data: dict[str, Any]) -> Decision:
|
|
67
|
+
manipulation_raw = data.get("manipulation")
|
|
68
|
+
manipulation = None
|
|
69
|
+
if isinstance(manipulation_raw, dict):
|
|
70
|
+
manipulation = DecisionManipulation(
|
|
71
|
+
score=int(manipulation_raw["score"]) if isinstance(manipulation_raw.get("score"), int) else None,
|
|
72
|
+
verdict=manipulation_raw.get("verdict")
|
|
73
|
+
if isinstance(manipulation_raw.get("verdict"), str) or manipulation_raw.get("verdict") is None
|
|
74
|
+
else None,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return Decision(
|
|
78
|
+
event_id=str(data["event_id"]),
|
|
79
|
+
verdict=str(data["verdict"]),
|
|
80
|
+
risk_score=int(data["risk_score"]),
|
|
81
|
+
phase=data.get("phase") if isinstance(data.get("phase"), str) or data.get("phase") is None else None,
|
|
82
|
+
is_provisional=data.get("is_provisional")
|
|
83
|
+
if isinstance(data.get("is_provisional"), bool) or data.get("is_provisional") is None
|
|
84
|
+
else None,
|
|
85
|
+
manipulation=manipulation,
|
|
86
|
+
evaluation_duration_ms=int(data["evaluation_duration_ms"])
|
|
87
|
+
if isinstance(data.get("evaluation_duration_ms"), int)
|
|
88
|
+
else None,
|
|
89
|
+
evaluated_at=str(data["evaluated_at"]),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _parse_request_context(data: dict[str, Any]) -> RequestContext:
|
|
94
|
+
return RequestContext(
|
|
95
|
+
user_agent=str(data["user_agent"]),
|
|
96
|
+
url=str(data["url"]),
|
|
97
|
+
screen_size=data.get("screen_size")
|
|
98
|
+
if isinstance(data.get("screen_size"), str) or data.get("screen_size") is None
|
|
99
|
+
else None,
|
|
100
|
+
is_touch_capable=data.get("is_touch_capable")
|
|
101
|
+
if isinstance(data.get("is_touch_capable"), bool) or data.get("is_touch_capable") is None
|
|
102
|
+
else None,
|
|
103
|
+
ip_address=str(data["ip_address"]),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _parse_session_detail_request(data: dict[str, Any]) -> SessionDetailRequest:
|
|
108
|
+
return SessionDetailRequest(
|
|
109
|
+
url=str(data["url"]),
|
|
110
|
+
referrer=data.get("referrer")
|
|
111
|
+
if isinstance(data.get("referrer"), str) or data.get("referrer") is None
|
|
112
|
+
else None,
|
|
113
|
+
user_agent=str(data["user_agent"]),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _parse_session_decision(data: dict[str, Any]) -> SessionDecision:
|
|
118
|
+
return SessionDecision(
|
|
119
|
+
event_id=str(data["event_id"]),
|
|
120
|
+
automation_status=str(data["automation_status"]),
|
|
121
|
+
risk_score=int(data["risk_score"]),
|
|
122
|
+
evaluation_phase=data.get("evaluation_phase")
|
|
123
|
+
if isinstance(data.get("evaluation_phase"), str) or data.get("evaluation_phase") is None
|
|
124
|
+
else None,
|
|
125
|
+
decision_status=str(data["decision_status"]),
|
|
126
|
+
evaluated_at=str(data["evaluated_at"]),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _parse_visitor_fingerprint_link(data: dict[str, Any] | None) -> VisitorFingerprintLink | None:
|
|
131
|
+
if data is None:
|
|
132
|
+
return None
|
|
133
|
+
return VisitorFingerprintLink(
|
|
134
|
+
object=str(data["object"]),
|
|
135
|
+
id=str(data["id"]),
|
|
136
|
+
confidence=int(data["confidence"]) if isinstance(data.get("confidence"), int) else None,
|
|
137
|
+
identified_at=data.get("identified_at")
|
|
138
|
+
if isinstance(data.get("identified_at"), str) or data.get("identified_at") is None
|
|
139
|
+
else None,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _parse_session_summary(data: dict[str, Any]) -> SessionSummary:
|
|
144
|
+
return SessionSummary(
|
|
145
|
+
object=str(data["object"]),
|
|
146
|
+
id=str(data["id"]),
|
|
147
|
+
created_at=data.get("created_at"),
|
|
148
|
+
latest_decision=_parse_decision(dict(data["latest_decision"])),
|
|
149
|
+
visitor_fingerprint=_parse_visitor_fingerprint_link(data.get("visitor_fingerprint")),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _parse_session_detail(data: dict[str, Any]) -> SessionDetail:
|
|
154
|
+
return SessionDetail(
|
|
155
|
+
object=str(data["object"]),
|
|
156
|
+
id=str(data["id"]),
|
|
157
|
+
created_at=data.get("created_at"),
|
|
158
|
+
decision=_parse_session_decision(dict(data["decision"])),
|
|
159
|
+
highlights=[dict(item) for item in data.get("highlights", []) if isinstance(item, dict)],
|
|
160
|
+
attribution=dict(data.get("attribution")) if isinstance(data.get("attribution"), dict) else None,
|
|
161
|
+
web_bot_auth=dict(data.get("web_bot_auth")) if isinstance(data.get("web_bot_auth"), dict) else None,
|
|
162
|
+
network=dict(data.get("network", {})) if isinstance(data.get("network"), dict) else {},
|
|
163
|
+
runtime_integrity=dict(data.get("runtime_integrity", {}))
|
|
164
|
+
if isinstance(data.get("runtime_integrity"), dict)
|
|
165
|
+
else {},
|
|
166
|
+
native_runtime_integrity=dict(data.get("native_runtime_integrity"))
|
|
167
|
+
if isinstance(data.get("native_runtime_integrity"), dict)
|
|
168
|
+
else None,
|
|
169
|
+
native_app=dict(data.get("native_app")) if isinstance(data.get("native_app"), dict) else None,
|
|
170
|
+
native_carrier=dict(data.get("native_carrier")) if isinstance(data.get("native_carrier"), dict) else None,
|
|
171
|
+
native_motion_print=dict(data.get("native_motion_print"))
|
|
172
|
+
if isinstance(data.get("native_motion_print"), dict)
|
|
173
|
+
else None,
|
|
174
|
+
device_identity=dict(data.get("device_identity")) if isinstance(data.get("device_identity"), dict) else None,
|
|
175
|
+
install_id=data.get("install_id") if isinstance(data.get("install_id"), str) else None,
|
|
176
|
+
visitor_fingerprint=dict(data.get("visitor_fingerprint"))
|
|
177
|
+
if isinstance(data.get("visitor_fingerprint"), dict)
|
|
178
|
+
else None,
|
|
179
|
+
connection_fingerprint=dict(data.get("connection_fingerprint", {}))
|
|
180
|
+
if isinstance(data.get("connection_fingerprint"), dict)
|
|
181
|
+
else {},
|
|
182
|
+
previous_decisions=[_parse_session_decision(dict(item)) for item in data.get("previous_decisions", [])],
|
|
183
|
+
request=_parse_session_detail_request(dict(data["request"])),
|
|
184
|
+
browser=dict(data.get("browser", {})) if isinstance(data.get("browser"), dict) else {},
|
|
185
|
+
device=dict(data.get("device", {})) if isinstance(data.get("device"), dict) else {},
|
|
186
|
+
analysis_coverage={
|
|
187
|
+
str(key): bool(value)
|
|
188
|
+
for key, value in dict(data.get("analysis_coverage", {})).items()
|
|
189
|
+
if isinstance(key, str)
|
|
190
|
+
}
|
|
191
|
+
if isinstance(data.get("analysis_coverage"), dict)
|
|
192
|
+
else {},
|
|
193
|
+
signals_fired=[dict(item) for item in data.get("signals_fired", []) if isinstance(item, dict)],
|
|
194
|
+
client_telemetry=dict(data.get("client_telemetry", {}))
|
|
195
|
+
if isinstance(data.get("client_telemetry"), dict)
|
|
196
|
+
else {},
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _parse_visitor_fingerprint_summary(data: dict[str, Any]) -> VisitorFingerprintSummary:
|
|
201
|
+
lifecycle_raw = dict(data["lifecycle"])
|
|
202
|
+
latest_request_raw = dict(data["latest_request"])
|
|
203
|
+
storage_raw = dict(data["storage"])
|
|
204
|
+
anchors_raw = dict(data["anchors"])
|
|
205
|
+
|
|
206
|
+
return VisitorFingerprintSummary(
|
|
207
|
+
object=str(data["object"]),
|
|
208
|
+
id=str(data["id"]),
|
|
209
|
+
lifecycle=VisitorFingerprintLifecycle(
|
|
210
|
+
first_seen_at=str(lifecycle_raw["first_seen_at"]),
|
|
211
|
+
last_seen_at=str(lifecycle_raw["last_seen_at"]),
|
|
212
|
+
seen_count=int(lifecycle_raw["seen_count"]),
|
|
213
|
+
expires_at=str(lifecycle_raw["expires_at"]),
|
|
214
|
+
),
|
|
215
|
+
latest_request=VisitorFingerprintLatestRequest(
|
|
216
|
+
user_agent=str(latest_request_raw["user_agent"]),
|
|
217
|
+
ip_address=str(latest_request_raw["ip_address"]),
|
|
218
|
+
),
|
|
219
|
+
storage=VisitorFingerprintStorage(
|
|
220
|
+
cookies=bool(storage_raw["cookies"]),
|
|
221
|
+
local_storage=bool(storage_raw["local_storage"]),
|
|
222
|
+
indexed_db=bool(storage_raw["indexed_db"]),
|
|
223
|
+
service_worker=bool(storage_raw["service_worker"]),
|
|
224
|
+
window_name=bool(storage_raw["window_name"]),
|
|
225
|
+
),
|
|
226
|
+
anchors=VisitorFingerprintAnchors(
|
|
227
|
+
webgl_hash=anchors_raw.get("webgl_hash")
|
|
228
|
+
if isinstance(anchors_raw.get("webgl_hash"), str) or anchors_raw.get("webgl_hash") is None
|
|
229
|
+
else None,
|
|
230
|
+
parameters_hash=anchors_raw.get("parameters_hash")
|
|
231
|
+
if isinstance(anchors_raw.get("parameters_hash"), str) or anchors_raw.get("parameters_hash") is None
|
|
232
|
+
else None,
|
|
233
|
+
audio_hash=anchors_raw.get("audio_hash")
|
|
234
|
+
if isinstance(anchors_raw.get("audio_hash"), str) or anchors_raw.get("audio_hash") is None
|
|
235
|
+
else None,
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _parse_score_breakdown(data: dict[str, Any]) -> ScoreBreakdown:
|
|
241
|
+
categories_raw = data.get("categories", {})
|
|
242
|
+
categories = (
|
|
243
|
+
{str(key): int(value) for key, value in dict(categories_raw).items()}
|
|
244
|
+
if isinstance(categories_raw, dict)
|
|
245
|
+
else {}
|
|
246
|
+
)
|
|
247
|
+
return ScoreBreakdown(categories=categories)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _parse_visitor_fingerprint_detail(data: dict[str, Any]) -> VisitorFingerprintDetail:
|
|
251
|
+
summary = _parse_visitor_fingerprint_summary(data)
|
|
252
|
+
components_raw = dict(data.get("components", {}))
|
|
253
|
+
activity_raw = dict(data.get("activity", {}))
|
|
254
|
+
|
|
255
|
+
sessions = [
|
|
256
|
+
VisitorFingerprintSessionSummary(
|
|
257
|
+
session_id=str(item["session_id"]),
|
|
258
|
+
decision=_parse_decision(dict(item["decision"])),
|
|
259
|
+
request=_parse_request_context(dict(item["request"])),
|
|
260
|
+
score_breakdown=_parse_score_breakdown(dict(item["score_breakdown"])),
|
|
261
|
+
)
|
|
262
|
+
for item in activity_raw.get("sessions", [])
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
return VisitorFingerprintDetail(
|
|
266
|
+
**summary.__dict__,
|
|
267
|
+
components=VisitorFingerprintComponents(
|
|
268
|
+
vector=[int(value) for value in components_raw.get("vector", [])],
|
|
269
|
+
),
|
|
270
|
+
activity=VisitorFingerprintActivity(sessions=sessions),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _parse_organization(data: dict[str, Any]) -> Organization:
|
|
275
|
+
return Organization(
|
|
276
|
+
object=str(data["object"]),
|
|
277
|
+
id=str(data["id"]),
|
|
278
|
+
name=str(data["name"]),
|
|
279
|
+
slug=str(data["slug"]),
|
|
280
|
+
status=str(data["status"]),
|
|
281
|
+
created_at=str(data["created_at"]),
|
|
282
|
+
updated_at=data.get("updated_at"),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _parse_api_key(data: dict[str, Any]) -> ApiKey:
|
|
287
|
+
return ApiKey(
|
|
288
|
+
object=str(data["object"]),
|
|
289
|
+
id=str(data["id"]),
|
|
290
|
+
type=str(data["type"]),
|
|
291
|
+
name=str(data["name"]),
|
|
292
|
+
environment=str(data["environment"]),
|
|
293
|
+
allowed_origins=[str(value) for value in data.get("allowed_origins", [])]
|
|
294
|
+
if data.get("allowed_origins") is not None
|
|
295
|
+
else None,
|
|
296
|
+
scopes=[str(value) for value in data.get("scopes", [])] if data.get("scopes") is not None else None,
|
|
297
|
+
rate_limit=int(data["rate_limit"]) if isinstance(data.get("rate_limit"), int) else None,
|
|
298
|
+
status=str(data["status"]),
|
|
299
|
+
key_preview=str(data["key_preview"]),
|
|
300
|
+
display_key=data.get("display_key"),
|
|
301
|
+
last_used_at=data.get("last_used_at"),
|
|
302
|
+
created_at=str(data["created_at"]),
|
|
303
|
+
rotated_at=data.get("rotated_at"),
|
|
304
|
+
revoked_at=data.get("revoked_at"),
|
|
305
|
+
grace_expires_at=data.get("grace_expires_at"),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _parse_issued_api_key(data: dict[str, Any]) -> IssuedApiKey:
|
|
310
|
+
api_key = _parse_api_key(data)
|
|
311
|
+
return IssuedApiKey(**api_key.__dict__, revealed_key=str(data["revealed_key"]))
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _parse_gate_service_env_var(data: dict[str, Any]) -> GateServiceEnvVar:
|
|
315
|
+
return GateServiceEnvVar(
|
|
316
|
+
name=str(data["name"]),
|
|
317
|
+
key=str(data["key"]),
|
|
318
|
+
secret=bool(data["secret"]),
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _parse_gate_service_sdk_install(data: dict[str, Any]) -> GateServiceSdkInstall:
|
|
323
|
+
return GateServiceSdkInstall(
|
|
324
|
+
label=str(data["label"]),
|
|
325
|
+
install=str(data["install"]),
|
|
326
|
+
url=str(data["url"]),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _parse_gate_service_branding(data: dict[str, Any] | None) -> GateServiceBranding:
|
|
331
|
+
payload = data or {}
|
|
332
|
+
return GateServiceBranding(
|
|
333
|
+
verified=bool(payload.get("verified", False)),
|
|
334
|
+
logo_url=payload.get("logo_url") if isinstance(payload.get("logo_url"), str) else None,
|
|
335
|
+
primary_color=payload.get("primary_color") if isinstance(payload.get("primary_color"), str) else None,
|
|
336
|
+
secondary_color=payload.get("secondary_color") if isinstance(payload.get("secondary_color"), str) else None,
|
|
337
|
+
ascii_art=payload.get("ascii_art") if isinstance(payload.get("ascii_art"), str) else None,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _parse_gate_service_consent(data: dict[str, Any] | None) -> GateServiceConsent:
|
|
342
|
+
payload = data or {}
|
|
343
|
+
return GateServiceConsent(
|
|
344
|
+
terms_url=payload.get("terms_url") if isinstance(payload.get("terms_url"), str) else None,
|
|
345
|
+
privacy_url=payload.get("privacy_url") if isinstance(payload.get("privacy_url"), str) else None,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _parse_gate_registry_entry(data: dict[str, Any]) -> GateRegistryEntry:
|
|
350
|
+
return GateRegistryEntry(
|
|
351
|
+
id=str(data["id"]),
|
|
352
|
+
status=str(data["status"]),
|
|
353
|
+
discoverable=bool(data["discoverable"]),
|
|
354
|
+
name=str(data["name"]),
|
|
355
|
+
description=str(data["description"]),
|
|
356
|
+
website=str(data["website"]),
|
|
357
|
+
env_vars=[_parse_gate_service_env_var(dict(item)) for item in data.get("env_vars", [])],
|
|
358
|
+
docs_url=str(data["docs_url"]),
|
|
359
|
+
sdks=[_parse_gate_service_sdk_install(dict(item)) for item in data.get("sdks", [])],
|
|
360
|
+
branding=_parse_gate_service_branding(dict(data["branding"])) if isinstance(data.get("branding"), dict) else _parse_gate_service_branding(None),
|
|
361
|
+
consent=_parse_gate_service_consent(dict(data["consent"])) if isinstance(data.get("consent"), dict) else _parse_gate_service_consent(None),
|
|
362
|
+
dashboard_login_url=data.get("dashboard_login_url")
|
|
363
|
+
if isinstance(data.get("dashboard_login_url"), str) or data.get("dashboard_login_url") is None
|
|
364
|
+
else None,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _parse_gate_managed_service(data: dict[str, Any]) -> GateManagedService:
|
|
369
|
+
entry = _parse_gate_registry_entry(data)
|
|
370
|
+
return GateManagedService(
|
|
371
|
+
**entry.__dict__,
|
|
372
|
+
object=str(data["object"]),
|
|
373
|
+
webhook_endpoint_id=data.get("webhook_endpoint_id")
|
|
374
|
+
if isinstance(data.get("webhook_endpoint_id"), str) or data.get("webhook_endpoint_id") is None
|
|
375
|
+
else None,
|
|
376
|
+
created_at=str(data["created_at"]),
|
|
377
|
+
updated_at=str(data["updated_at"]),
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _parse_webhook_endpoint(data: dict[str, Any]) -> WebhookEndpoint:
|
|
382
|
+
return WebhookEndpoint(
|
|
383
|
+
object=str(data["object"]),
|
|
384
|
+
id=str(data["id"]),
|
|
385
|
+
name=str(data["name"]),
|
|
386
|
+
url=str(data["url"]),
|
|
387
|
+
status=str(data["status"]),
|
|
388
|
+
event_types=[str(item) for item in data.get("event_types", [])],
|
|
389
|
+
signing_secret=data.get("signing_secret") if isinstance(data.get("signing_secret"), str) else None,
|
|
390
|
+
created_at=str(data["created_at"]),
|
|
391
|
+
updated_at=str(data["updated_at"]),
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _parse_webhook_delivery(data: dict[str, Any]) -> WebhookDelivery:
|
|
396
|
+
return WebhookDelivery(
|
|
397
|
+
object=str(data["object"]),
|
|
398
|
+
id=str(data["id"]),
|
|
399
|
+
event_id=str(data["event_id"]),
|
|
400
|
+
endpoint_id=str(data["endpoint_id"]),
|
|
401
|
+
event_type=str(data["event_type"]),
|
|
402
|
+
status=str(data["status"]),
|
|
403
|
+
attempts=int(data["attempts"]),
|
|
404
|
+
response_status=int(data["response_status"]) if isinstance(data.get("response_status"), int) else None,
|
|
405
|
+
response_body=data.get("response_body")
|
|
406
|
+
if isinstance(data.get("response_body"), str) or data.get("response_body") is None
|
|
407
|
+
else None,
|
|
408
|
+
error=data.get("error") if isinstance(data.get("error"), str) or data.get("error") is None else None,
|
|
409
|
+
created_at=str(data["created_at"]),
|
|
410
|
+
updated_at=str(data["updated_at"]),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _parse_event_subject(data: dict[str, Any]) -> EventSubject:
|
|
415
|
+
return EventSubject(
|
|
416
|
+
type=str(data["type"]),
|
|
417
|
+
id=str(data["id"]),
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _parse_event(data: dict[str, Any]) -> Event:
|
|
422
|
+
return Event(
|
|
423
|
+
object=str(data["object"]),
|
|
424
|
+
id=str(data["id"]),
|
|
425
|
+
type=str(data["type"]),
|
|
426
|
+
subject=_parse_event_subject(dict(data["subject"])),
|
|
427
|
+
data=dict(data.get("data", {})) if isinstance(data.get("data"), dict) else {},
|
|
428
|
+
webhook_deliveries=[
|
|
429
|
+
_parse_webhook_delivery(dict(item))
|
|
430
|
+
for item in data.get("webhook_deliveries", [])
|
|
431
|
+
if isinstance(item, dict)
|
|
432
|
+
],
|
|
433
|
+
created_at=str(data["created_at"]),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _parse_webhook_test(data: dict[str, Any]) -> WebhookTest:
|
|
438
|
+
latest_delivery = data.get("latest_delivery")
|
|
439
|
+
return WebhookTest(
|
|
440
|
+
object=str(data["object"]),
|
|
441
|
+
event_id=str(data["event_id"]),
|
|
442
|
+
delivery_ids=[str(item) for item in data.get("delivery_ids", [])],
|
|
443
|
+
latest_delivery=_parse_webhook_delivery(dict(latest_delivery)) if isinstance(latest_delivery, dict) else None,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _parse_gate_delivery_envelope(data: dict[str, Any]) -> GateDeliveryEnvelope:
|
|
448
|
+
return GateDeliveryEnvelope(
|
|
449
|
+
version=int(data["version"]),
|
|
450
|
+
algorithm=str(data["algorithm"]),
|
|
451
|
+
key_id=str(data["key_id"]),
|
|
452
|
+
ephemeral_public_key=str(data["ephemeral_public_key"]),
|
|
453
|
+
salt=str(data["salt"]),
|
|
454
|
+
iv=str(data["iv"]),
|
|
455
|
+
ciphertext=str(data["ciphertext"]),
|
|
456
|
+
tag=str(data["tag"]),
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _parse_gate_delivery_bundle(data: dict[str, Any] | None) -> GateDeliveryBundle | None:
|
|
461
|
+
if data is None:
|
|
462
|
+
return None
|
|
463
|
+
return GateDeliveryBundle(
|
|
464
|
+
integrator=_parse_gate_delivery_envelope(dict(data["integrator"])),
|
|
465
|
+
gate=_parse_gate_delivery_envelope(dict(data["gate"])),
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _parse_gate_session_create(data: dict[str, Any]) -> GateSessionCreate:
|
|
470
|
+
return GateSessionCreate(
|
|
471
|
+
object=str(data["object"]),
|
|
472
|
+
id=str(data["id"]),
|
|
473
|
+
status=str(data["status"]),
|
|
474
|
+
poll_token=str(data["poll_token"]),
|
|
475
|
+
consent_url=str(data["consent_url"]),
|
|
476
|
+
expires_at=str(data["expires_at"]),
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _parse_gate_session_poll(data: dict[str, Any]) -> GateSessionPollData:
|
|
481
|
+
return GateSessionPollData(
|
|
482
|
+
object=str(data["object"]),
|
|
483
|
+
id=str(data["id"]),
|
|
484
|
+
status=str(data["status"]),
|
|
485
|
+
expires_at=data.get("expires_at") if isinstance(data.get("expires_at"), str) or data.get("expires_at") is None else None,
|
|
486
|
+
gate_account_id=data.get("gate_account_id") if isinstance(data.get("gate_account_id"), str) or data.get("gate_account_id") is None else None,
|
|
487
|
+
account_name=data.get("account_name") if isinstance(data.get("account_name"), str) or data.get("account_name") is None else None,
|
|
488
|
+
delivery_bundle=_parse_gate_delivery_bundle(dict(data["delivery_bundle"])) if isinstance(data.get("delivery_bundle"), dict) else None,
|
|
489
|
+
docs_url=data.get("docs_url") if isinstance(data.get("docs_url"), str) or data.get("docs_url") is None else None,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _parse_gate_session_delivery_acknowledgement(data: dict[str, Any]) -> GateSessionDeliveryAcknowledgement:
|
|
494
|
+
return GateSessionDeliveryAcknowledgement(
|
|
495
|
+
object=str(data["object"]),
|
|
496
|
+
gate_session_id=str(data["gate_session_id"]),
|
|
497
|
+
status=str(data["status"]),
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _parse_gate_login_session(data: dict[str, Any]) -> GateLoginSession:
|
|
502
|
+
return GateLoginSession(
|
|
503
|
+
object=str(data["object"]),
|
|
504
|
+
id=str(data["id"]),
|
|
505
|
+
status=str(data["status"]),
|
|
506
|
+
consent_url=str(data["consent_url"]),
|
|
507
|
+
expires_at=str(data["expires_at"]),
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _parse_gate_dashboard_login(data: dict[str, Any]) -> GateDashboardLogin:
|
|
512
|
+
return GateDashboardLogin(
|
|
513
|
+
object=str(data["object"]),
|
|
514
|
+
gate_account_id=str(data["gate_account_id"]),
|
|
515
|
+
account_name=str(data["account_name"]),
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _parse_agent_token_verification(data: dict[str, Any]) -> AgentTokenVerification:
|
|
520
|
+
return AgentTokenVerification(
|
|
521
|
+
valid=bool(data["valid"]),
|
|
522
|
+
gate_account_id=data.get("gate_account_id") if isinstance(data.get("gate_account_id"), str) or data.get("gate_account_id") is None else None,
|
|
523
|
+
status=data.get("status") if isinstance(data.get("status"), str) or data.get("status") is None else None,
|
|
524
|
+
created_at=data.get("created_at") if isinstance(data.get("created_at"), str) or data.get("created_at") is None else None,
|
|
525
|
+
expires_at=data.get("expires_at") if isinstance(data.get("expires_at"), str) or data.get("expires_at") is None else None,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _normalize_list(items: list[Any], pagination: dict[str, Any]) -> ListResult[Any]:
|
|
530
|
+
return ListResult(
|
|
531
|
+
items=items,
|
|
532
|
+
limit=int(pagination["limit"]),
|
|
533
|
+
has_more=bool(pagination["has_more"]),
|
|
534
|
+
next_cursor=pagination.get("next_cursor"),
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class _BaseAPI:
|
|
539
|
+
def __init__(self, client: "Foil") -> None:
|
|
540
|
+
self._client = client
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class SessionsAPI(_BaseAPI):
|
|
544
|
+
def list(
|
|
545
|
+
self,
|
|
546
|
+
*,
|
|
547
|
+
limit: int | None = None,
|
|
548
|
+
cursor: str | None = None,
|
|
549
|
+
verdict: str | None = None,
|
|
550
|
+
search: str | None = None,
|
|
551
|
+
) -> ListResult[SessionSummary]:
|
|
552
|
+
response = self._client._request_json(
|
|
553
|
+
"GET",
|
|
554
|
+
"/v1/sessions",
|
|
555
|
+
query=_compact_query(
|
|
556
|
+
{
|
|
557
|
+
"limit": limit,
|
|
558
|
+
"cursor": cursor,
|
|
559
|
+
"verdict": verdict,
|
|
560
|
+
"search": search,
|
|
561
|
+
}
|
|
562
|
+
),
|
|
563
|
+
)
|
|
564
|
+
return _normalize_list(
|
|
565
|
+
[_parse_session_summary(dict(item)) for item in response["data"]],
|
|
566
|
+
dict(response["pagination"]),
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
def get(self, session_id: str) -> SessionDetail:
|
|
570
|
+
response = self._client._request_json("GET", f"/v1/sessions/{session_id}")
|
|
571
|
+
return _parse_session_detail(dict(response["data"]))
|
|
572
|
+
|
|
573
|
+
def iter(
|
|
574
|
+
self,
|
|
575
|
+
*,
|
|
576
|
+
limit: int | None = None,
|
|
577
|
+
verdict: str | None = None,
|
|
578
|
+
search: str | None = None,
|
|
579
|
+
) -> Iterator[SessionSummary]:
|
|
580
|
+
cursor: str | None = None
|
|
581
|
+
while True:
|
|
582
|
+
page = self.list(limit=limit, cursor=cursor, verdict=verdict, search=search)
|
|
583
|
+
for item in page.items:
|
|
584
|
+
yield item
|
|
585
|
+
if not page.has_more or not page.next_cursor:
|
|
586
|
+
break
|
|
587
|
+
cursor = page.next_cursor
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
class FingerprintsAPI(_BaseAPI):
|
|
591
|
+
def list(
|
|
592
|
+
self,
|
|
593
|
+
*,
|
|
594
|
+
limit: int | None = None,
|
|
595
|
+
cursor: str | None = None,
|
|
596
|
+
search: str | None = None,
|
|
597
|
+
sort: str | None = None,
|
|
598
|
+
) -> ListResult[VisitorFingerprintSummary]:
|
|
599
|
+
response = self._client._request_json(
|
|
600
|
+
"GET",
|
|
601
|
+
"/v1/fingerprints",
|
|
602
|
+
query=_compact_query(
|
|
603
|
+
{
|
|
604
|
+
"limit": limit,
|
|
605
|
+
"cursor": cursor,
|
|
606
|
+
"search": search,
|
|
607
|
+
"sort": sort,
|
|
608
|
+
}
|
|
609
|
+
),
|
|
610
|
+
)
|
|
611
|
+
return _normalize_list(
|
|
612
|
+
[_parse_visitor_fingerprint_summary(dict(item)) for item in response["data"]],
|
|
613
|
+
dict(response["pagination"]),
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
def get(self, visitor_id: str) -> VisitorFingerprintDetail:
|
|
617
|
+
response = self._client._request_json("GET", f"/v1/fingerprints/{visitor_id}")
|
|
618
|
+
return _parse_visitor_fingerprint_detail(dict(response["data"]))
|
|
619
|
+
|
|
620
|
+
def iter(
|
|
621
|
+
self,
|
|
622
|
+
*,
|
|
623
|
+
limit: int | None = None,
|
|
624
|
+
search: str | None = None,
|
|
625
|
+
sort: str | None = None,
|
|
626
|
+
) -> Iterator[VisitorFingerprintSummary]:
|
|
627
|
+
cursor: str | None = None
|
|
628
|
+
while True:
|
|
629
|
+
page = self.list(limit=limit, cursor=cursor, search=search, sort=sort)
|
|
630
|
+
for item in page.items:
|
|
631
|
+
yield item
|
|
632
|
+
if not page.has_more or not page.next_cursor:
|
|
633
|
+
break
|
|
634
|
+
cursor = page.next_cursor
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
class ApiKeysAPI(_BaseAPI):
|
|
638
|
+
def create(
|
|
639
|
+
self,
|
|
640
|
+
organization_id: str,
|
|
641
|
+
*,
|
|
642
|
+
name: str,
|
|
643
|
+
type: str | None = None,
|
|
644
|
+
environment: str | None = None,
|
|
645
|
+
allowed_origins: list[str] | None = None,
|
|
646
|
+
scopes: list[str] | None = None,
|
|
647
|
+
) -> IssuedApiKey:
|
|
648
|
+
response = self._client._request_json(
|
|
649
|
+
"POST",
|
|
650
|
+
f"/v1/organizations/{organization_id}/api-keys",
|
|
651
|
+
body=_compact_query(
|
|
652
|
+
{
|
|
653
|
+
"name": name,
|
|
654
|
+
"type": type,
|
|
655
|
+
"environment": environment,
|
|
656
|
+
"allowed_origins": allowed_origins,
|
|
657
|
+
"scopes": scopes,
|
|
658
|
+
}
|
|
659
|
+
),
|
|
660
|
+
)
|
|
661
|
+
return _parse_issued_api_key(dict(response["data"]))
|
|
662
|
+
|
|
663
|
+
def list(
|
|
664
|
+
self,
|
|
665
|
+
organization_id: str,
|
|
666
|
+
*,
|
|
667
|
+
limit: int | None = None,
|
|
668
|
+
cursor: str | None = None,
|
|
669
|
+
) -> ListResult[ApiKey]:
|
|
670
|
+
response = self._client._request_json(
|
|
671
|
+
"GET",
|
|
672
|
+
f"/v1/organizations/{organization_id}/api-keys",
|
|
673
|
+
query=_compact_query({"limit": limit, "cursor": cursor}),
|
|
674
|
+
)
|
|
675
|
+
return _normalize_list(
|
|
676
|
+
[_parse_api_key(dict(item)) for item in response["data"]],
|
|
677
|
+
dict(response["pagination"]),
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
def update(
|
|
681
|
+
self,
|
|
682
|
+
organization_id: str,
|
|
683
|
+
key_id: str,
|
|
684
|
+
*,
|
|
685
|
+
name: str | None = None,
|
|
686
|
+
allowed_origins: list[str] | None = None,
|
|
687
|
+
scopes: list[str] | None = None,
|
|
688
|
+
) -> ApiKey:
|
|
689
|
+
response = self._client._request_json(
|
|
690
|
+
"PATCH",
|
|
691
|
+
f"/v1/organizations/{organization_id}/api-keys/{key_id}",
|
|
692
|
+
body=_compact_query({"name": name, "allowed_origins": allowed_origins, "scopes": scopes}),
|
|
693
|
+
)
|
|
694
|
+
return _parse_api_key(dict(response["data"]))
|
|
695
|
+
|
|
696
|
+
def revoke(self, organization_id: str, key_id: str) -> ApiKey:
|
|
697
|
+
response = self._client._request_json(
|
|
698
|
+
"DELETE",
|
|
699
|
+
f"/v1/organizations/{organization_id}/api-keys/{key_id}",
|
|
700
|
+
)
|
|
701
|
+
return _parse_api_key(dict(response["data"]))
|
|
702
|
+
|
|
703
|
+
def rotate(self, organization_id: str, key_id: str) -> IssuedApiKey:
|
|
704
|
+
response = self._client._request_json(
|
|
705
|
+
"POST",
|
|
706
|
+
f"/v1/organizations/{organization_id}/api-keys/{key_id}/rotations",
|
|
707
|
+
)
|
|
708
|
+
return _parse_issued_api_key(dict(response["data"]))
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
class OrganizationsAPI(_BaseAPI):
|
|
712
|
+
def __init__(self, client: "Foil") -> None:
|
|
713
|
+
super().__init__(client)
|
|
714
|
+
self.api_keys = ApiKeysAPI(client)
|
|
715
|
+
|
|
716
|
+
def create(self, *, name: str, slug: str) -> Organization:
|
|
717
|
+
response = self._client._request_json(
|
|
718
|
+
"POST",
|
|
719
|
+
"/v1/organizations",
|
|
720
|
+
body={"name": name, "slug": slug},
|
|
721
|
+
)
|
|
722
|
+
return _parse_organization(dict(response["data"]))
|
|
723
|
+
|
|
724
|
+
def get(self, organization_id: str) -> Organization:
|
|
725
|
+
response = self._client._request_json("GET", f"/v1/organizations/{organization_id}")
|
|
726
|
+
return _parse_organization(dict(response["data"]))
|
|
727
|
+
|
|
728
|
+
def update(
|
|
729
|
+
self,
|
|
730
|
+
organization_id: str,
|
|
731
|
+
*,
|
|
732
|
+
name: str | None = None,
|
|
733
|
+
status: str | None = None,
|
|
734
|
+
) -> Organization:
|
|
735
|
+
response = self._client._request_json(
|
|
736
|
+
"PATCH",
|
|
737
|
+
f"/v1/organizations/{organization_id}",
|
|
738
|
+
body=_compact_query({"name": name, "status": status}),
|
|
739
|
+
)
|
|
740
|
+
return _parse_organization(dict(response["data"]))
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
class GateRegistryAPI(_BaseAPI):
|
|
744
|
+
def list(self) -> list[GateRegistryEntry]:
|
|
745
|
+
response = self._client._request_json("GET", "/v1/gate/registry", auth_mode="none")
|
|
746
|
+
return [_parse_gate_registry_entry(dict(item)) for item in response["data"]]
|
|
747
|
+
|
|
748
|
+
def get(self, service_id: str) -> GateRegistryEntry:
|
|
749
|
+
response = self._client._request_json("GET", f"/v1/gate/registry/{service_id}", auth_mode="none")
|
|
750
|
+
return _parse_gate_registry_entry(dict(response["data"]))
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
class GateServicesAPI(_BaseAPI):
|
|
754
|
+
def list(self) -> list[GateManagedService]:
|
|
755
|
+
response = self._client._request_json("GET", "/v1/gate/services")
|
|
756
|
+
return [_parse_gate_managed_service(dict(item)) for item in response["data"]]
|
|
757
|
+
|
|
758
|
+
def get(self, service_id: str) -> GateManagedService:
|
|
759
|
+
response = self._client._request_json("GET", f"/v1/gate/services/{service_id}")
|
|
760
|
+
return _parse_gate_managed_service(dict(response["data"]))
|
|
761
|
+
|
|
762
|
+
def create(self, **body: Any) -> GateManagedService:
|
|
763
|
+
response = self._client._request_json("POST", "/v1/gate/services", body=body)
|
|
764
|
+
return _parse_gate_managed_service(dict(response["data"]))
|
|
765
|
+
|
|
766
|
+
def update(self, service_id: str, **body: Any) -> GateManagedService:
|
|
767
|
+
response = self._client._request_json("PATCH", f"/v1/gate/services/{service_id}", body=body)
|
|
768
|
+
return _parse_gate_managed_service(dict(response["data"]))
|
|
769
|
+
|
|
770
|
+
def disable(self, service_id: str) -> GateManagedService:
|
|
771
|
+
response = self._client._request_json("DELETE", f"/v1/gate/services/{service_id}")
|
|
772
|
+
return _parse_gate_managed_service(dict(response["data"]))
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
class GateSessionsAPI(_BaseAPI):
|
|
776
|
+
def create(
|
|
777
|
+
self,
|
|
778
|
+
*,
|
|
779
|
+
service_id: str,
|
|
780
|
+
account_name: str,
|
|
781
|
+
delivery: dict[str, Any],
|
|
782
|
+
metadata: dict[str, Any] | None = None,
|
|
783
|
+
) -> GateSessionCreate:
|
|
784
|
+
body = {
|
|
785
|
+
"service_id": service_id,
|
|
786
|
+
"account_name": account_name,
|
|
787
|
+
"delivery": delivery,
|
|
788
|
+
}
|
|
789
|
+
if metadata is not None:
|
|
790
|
+
body["metadata"] = metadata
|
|
791
|
+
response = self._client._request_json("POST", "/v1/gate/sessions", body=body, auth_mode="none")
|
|
792
|
+
return _parse_gate_session_create(dict(response["data"]))
|
|
793
|
+
|
|
794
|
+
def poll(self, gate_session_id: str, *, poll_token: str) -> GateSessionPollData:
|
|
795
|
+
response = self._client._request_json(
|
|
796
|
+
"GET",
|
|
797
|
+
f"/v1/gate/sessions/{gate_session_id}",
|
|
798
|
+
auth_mode="bearer",
|
|
799
|
+
bearer_token=poll_token,
|
|
800
|
+
)
|
|
801
|
+
return _parse_gate_session_poll(dict(response["data"]))
|
|
802
|
+
|
|
803
|
+
def acknowledge(self, gate_session_id: str, *, poll_token: str, ack_token: str) -> GateSessionDeliveryAcknowledgement:
|
|
804
|
+
response = self._client._request_json(
|
|
805
|
+
"POST",
|
|
806
|
+
f"/v1/gate/sessions/{gate_session_id}/ack",
|
|
807
|
+
body={"ack_token": ack_token},
|
|
808
|
+
auth_mode="bearer",
|
|
809
|
+
bearer_token=poll_token,
|
|
810
|
+
)
|
|
811
|
+
return _parse_gate_session_delivery_acknowledgement(dict(response["data"]))
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
class GateLoginSessionsAPI(_BaseAPI):
|
|
815
|
+
def create(self, *, service_id: str, agent_token: str) -> GateLoginSession:
|
|
816
|
+
response = self._client._request_json(
|
|
817
|
+
"POST",
|
|
818
|
+
"/v1/gate/login-sessions",
|
|
819
|
+
body={"service_id": service_id},
|
|
820
|
+
auth_mode="bearer",
|
|
821
|
+
bearer_token=agent_token,
|
|
822
|
+
)
|
|
823
|
+
return _parse_gate_login_session(dict(response["data"]))
|
|
824
|
+
|
|
825
|
+
def consume(self, *, code: str) -> GateDashboardLogin:
|
|
826
|
+
response = self._client._request_json(
|
|
827
|
+
"POST",
|
|
828
|
+
"/v1/gate/login-sessions/consume",
|
|
829
|
+
body={"code": code},
|
|
830
|
+
)
|
|
831
|
+
return _parse_gate_dashboard_login(dict(response["data"]))
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
class GateAgentTokensAPI(_BaseAPI):
|
|
835
|
+
def verify(self, *, agent_token: str) -> AgentTokenVerification:
|
|
836
|
+
response = self._client._request_json(
|
|
837
|
+
"POST",
|
|
838
|
+
"/v1/gate/agent-tokens/verify",
|
|
839
|
+
body={"agent_token": agent_token},
|
|
840
|
+
)
|
|
841
|
+
return _parse_agent_token_verification(dict(response["data"]))
|
|
842
|
+
|
|
843
|
+
def revoke(self, *, agent_token: str) -> None:
|
|
844
|
+
self._client._request_json(
|
|
845
|
+
"POST",
|
|
846
|
+
"/v1/gate/agent-tokens/revoke",
|
|
847
|
+
body={"agent_token": agent_token},
|
|
848
|
+
expect_content=False,
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
class GateAPI(_BaseAPI):
|
|
853
|
+
def __init__(self, client: "Foil") -> None:
|
|
854
|
+
super().__init__(client)
|
|
855
|
+
self.registry = GateRegistryAPI(client)
|
|
856
|
+
self.services = GateServicesAPI(client)
|
|
857
|
+
self.sessions = GateSessionsAPI(client)
|
|
858
|
+
self.login_sessions = GateLoginSessionsAPI(client)
|
|
859
|
+
self.agent_tokens = GateAgentTokensAPI(client)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
class WebhooksAPI(_BaseAPI):
|
|
863
|
+
def list_endpoints(self, organization_id: str) -> ListResult[WebhookEndpoint]:
|
|
864
|
+
response = self._client._request_json("GET", f"/v1/organizations/{organization_id}/webhooks/endpoints")
|
|
865
|
+
return _normalize_list(
|
|
866
|
+
[_parse_webhook_endpoint(dict(item)) for item in response["data"]],
|
|
867
|
+
dict(response["pagination"]),
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
def create_endpoint(
|
|
871
|
+
self,
|
|
872
|
+
organization_id: str,
|
|
873
|
+
*,
|
|
874
|
+
name: str,
|
|
875
|
+
url: str,
|
|
876
|
+
event_types: list[str],
|
|
877
|
+
) -> WebhookEndpoint:
|
|
878
|
+
response = self._client._request_json(
|
|
879
|
+
"POST",
|
|
880
|
+
f"/v1/organizations/{organization_id}/webhooks/endpoints",
|
|
881
|
+
body={"name": name, "url": url, "event_types": event_types},
|
|
882
|
+
)
|
|
883
|
+
return _parse_webhook_endpoint(dict(response["data"]))
|
|
884
|
+
|
|
885
|
+
def update_endpoint(self, organization_id: str, endpoint_id: str, **body: Any) -> WebhookEndpoint:
|
|
886
|
+
response = self._client._request_json(
|
|
887
|
+
"PATCH",
|
|
888
|
+
f"/v1/organizations/{organization_id}/webhooks/endpoints/{endpoint_id}",
|
|
889
|
+
body=body,
|
|
890
|
+
)
|
|
891
|
+
return _parse_webhook_endpoint(dict(response["data"]))
|
|
892
|
+
|
|
893
|
+
def disable_endpoint(self, organization_id: str, endpoint_id: str) -> WebhookEndpoint:
|
|
894
|
+
response = self._client._request_json(
|
|
895
|
+
"DELETE",
|
|
896
|
+
f"/v1/organizations/{organization_id}/webhooks/endpoints/{endpoint_id}",
|
|
897
|
+
)
|
|
898
|
+
return _parse_webhook_endpoint(dict(response["data"]))
|
|
899
|
+
|
|
900
|
+
def rotate_secret(self, organization_id: str, endpoint_id: str) -> WebhookEndpoint:
|
|
901
|
+
response = self._client._request_json(
|
|
902
|
+
"POST",
|
|
903
|
+
f"/v1/organizations/{organization_id}/webhooks/endpoints/{endpoint_id}/rotations",
|
|
904
|
+
)
|
|
905
|
+
return _parse_webhook_endpoint(dict(response["data"]))
|
|
906
|
+
|
|
907
|
+
def send_test(self, organization_id: str, endpoint_id: str) -> WebhookTest:
|
|
908
|
+
response = self._client._request_json(
|
|
909
|
+
"POST",
|
|
910
|
+
f"/v1/organizations/{organization_id}/webhooks/endpoints/{endpoint_id}/test",
|
|
911
|
+
)
|
|
912
|
+
return _parse_webhook_test(dict(response["data"]))
|
|
913
|
+
|
|
914
|
+
def list_events(
|
|
915
|
+
self,
|
|
916
|
+
organization_id: str,
|
|
917
|
+
*,
|
|
918
|
+
endpoint_id: str | None = None,
|
|
919
|
+
type: str | None = None,
|
|
920
|
+
limit: int | None = None,
|
|
921
|
+
) -> ListResult[Event]:
|
|
922
|
+
response = self._client._request_json(
|
|
923
|
+
"GET",
|
|
924
|
+
f"/v1/organizations/{organization_id}/events",
|
|
925
|
+
query=_compact_query({"endpoint_id": endpoint_id, "type": type, "limit": limit}),
|
|
926
|
+
)
|
|
927
|
+
return _normalize_list(
|
|
928
|
+
[_parse_event(dict(item)) for item in response["data"]],
|
|
929
|
+
dict(response["pagination"]),
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
def retrieve_event(self, organization_id: str, event_id: str) -> Event:
|
|
933
|
+
response = self._client._request_json("GET", f"/v1/organizations/{organization_id}/events/{event_id}")
|
|
934
|
+
return _parse_event(dict(response["data"]))
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
class Foil:
|
|
938
|
+
def __init__(
|
|
939
|
+
self,
|
|
940
|
+
*,
|
|
941
|
+
secret_key: str | None = None,
|
|
942
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
943
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
944
|
+
user_agent: str | None = None,
|
|
945
|
+
transport: httpx.BaseTransport | None = None,
|
|
946
|
+
) -> None:
|
|
947
|
+
resolved_secret = secret_key or os.getenv("FOIL_SECRET_KEY")
|
|
948
|
+
headers = {
|
|
949
|
+
"Accept": "application/json",
|
|
950
|
+
"X-Foil-Client": SDK_CLIENT_HEADER,
|
|
951
|
+
}
|
|
952
|
+
if user_agent:
|
|
953
|
+
headers["User-Agent"] = user_agent
|
|
954
|
+
|
|
955
|
+
self._secret_key = resolved_secret
|
|
956
|
+
self._client = httpx.Client(
|
|
957
|
+
base_url=base_url or DEFAULT_BASE_URL,
|
|
958
|
+
timeout=timeout,
|
|
959
|
+
transport=transport,
|
|
960
|
+
headers=headers,
|
|
961
|
+
)
|
|
962
|
+
self.sessions = SessionsAPI(self)
|
|
963
|
+
self.fingerprints = FingerprintsAPI(self)
|
|
964
|
+
self.organizations = OrganizationsAPI(self)
|
|
965
|
+
self.gate = GateAPI(self)
|
|
966
|
+
self.webhooks = WebhooksAPI(self)
|
|
967
|
+
|
|
968
|
+
def close(self) -> None:
|
|
969
|
+
self._client.close()
|
|
970
|
+
|
|
971
|
+
def __enter__(self) -> "Foil":
|
|
972
|
+
return self
|
|
973
|
+
|
|
974
|
+
def __exit__(self, *_args: object) -> None:
|
|
975
|
+
self.close()
|
|
976
|
+
|
|
977
|
+
def _request_json(
|
|
978
|
+
self,
|
|
979
|
+
method: str,
|
|
980
|
+
path: str,
|
|
981
|
+
*,
|
|
982
|
+
query: dict[str, Any] | None = None,
|
|
983
|
+
body: dict[str, Any] | None = None,
|
|
984
|
+
expect_content: bool = True,
|
|
985
|
+
auth_mode: str = "secret",
|
|
986
|
+
bearer_token: str | None = None,
|
|
987
|
+
) -> dict[str, Any]:
|
|
988
|
+
headers: dict[str, str] = {}
|
|
989
|
+
if auth_mode == "bearer":
|
|
990
|
+
if not bearer_token:
|
|
991
|
+
raise FoilConfigurationError("Missing bearer token for this Foil request.")
|
|
992
|
+
headers["Authorization"] = f"Bearer {bearer_token}"
|
|
993
|
+
elif auth_mode == "secret":
|
|
994
|
+
if not self._secret_key:
|
|
995
|
+
raise FoilConfigurationError(
|
|
996
|
+
"Missing Foil secret key. Pass secret_key explicitly or set FOIL_SECRET_KEY."
|
|
997
|
+
)
|
|
998
|
+
headers["Authorization"] = f"Bearer {self._secret_key}"
|
|
999
|
+
response = self._client.request(method, path, params=query, json=body, headers=headers)
|
|
1000
|
+
request_id = response.headers.get("x-request-id")
|
|
1001
|
+
|
|
1002
|
+
if response.status_code >= 400:
|
|
1003
|
+
payload: dict[str, Any]
|
|
1004
|
+
try:
|
|
1005
|
+
payload = response.json()
|
|
1006
|
+
except ValueError:
|
|
1007
|
+
payload = {}
|
|
1008
|
+
|
|
1009
|
+
if isinstance(payload.get("error"), dict):
|
|
1010
|
+
error = payload["error"]
|
|
1011
|
+
details = error.get("details")
|
|
1012
|
+
raise FoilApiError(
|
|
1013
|
+
status=response.status_code,
|
|
1014
|
+
code=str(error.get("code", "request.failed")),
|
|
1015
|
+
message=str(error.get("message", response.text or response.reason_phrase)),
|
|
1016
|
+
request_id=request_id or error.get("request_id"),
|
|
1017
|
+
field_errors=list(details.get("fields", [])) if isinstance(details, dict) else [],
|
|
1018
|
+
docs_url=error.get("docs_url"),
|
|
1019
|
+
body=payload,
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
raise FoilApiError(
|
|
1023
|
+
status=response.status_code,
|
|
1024
|
+
code="request.failed",
|
|
1025
|
+
message=response.text or response.reason_phrase,
|
|
1026
|
+
request_id=request_id,
|
|
1027
|
+
body=payload,
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
if not expect_content or response.status_code == 204:
|
|
1031
|
+
return {}
|
|
1032
|
+
return response.json()
|