foil-server 0.2.3__tar.gz → 0.2.4__tar.gz

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 (63) hide show
  1. {foil_server-0.2.3 → foil_server-0.2.4}/PKG-INFO +3 -1
  2. {foil_server-0.2.3 → foil_server-0.2.4}/README.md +2 -0
  3. {foil_server-0.2.3 → foil_server-0.2.4}/foil_server/client.py +22 -0
  4. {foil_server-0.2.3 → foil_server-0.2.4}/foil_server/types.py +2 -0
  5. {foil_server-0.2.3 → foil_server-0.2.4}/pyproject.toml +1 -1
  6. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/sessions/detail.json +1 -0
  7. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/sessions/list.json +1 -0
  8. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/manifest.json +6 -0
  9. {foil_server-0.2.3 → foil_server-0.2.4}/spec/openapi.json +141 -4
  10. {foil_server-0.2.3 → foil_server-0.2.4}/tests/test_client.py +23 -0
  11. {foil_server-0.2.3 → foil_server-0.2.4}/tests/test_contract.py +11 -1
  12. {foil_server-0.2.3 → foil_server-0.2.4}/.github/workflows/ci.yml +0 -0
  13. {foil_server-0.2.3 → foil_server-0.2.4}/.github/workflows/release.yml +0 -0
  14. {foil_server-0.2.3 → foil_server-0.2.4}/.gitignore +0 -0
  15. {foil_server-0.2.3 → foil_server-0.2.4}/LICENSE +0 -0
  16. {foil_server-0.2.3 → foil_server-0.2.4}/RELEASING.md +0 -0
  17. {foil_server-0.2.3 → foil_server-0.2.4}/foil_server/__init__.py +0 -0
  18. {foil_server-0.2.3 → foil_server-0.2.4}/foil_server/errors.py +0 -0
  19. {foil_server-0.2.3 → foil_server-0.2.4}/foil_server/gate_delivery.py +0 -0
  20. {foil_server-0.2.3 → foil_server-0.2.4}/foil_server/sealed_token.py +0 -0
  21. {foil_server-0.2.3 → foil_server-0.2.4}/scripts/check-spec-sync.sh +0 -0
  22. {foil_server-0.2.3 → foil_server-0.2.4}/spec/LICENSE +0 -0
  23. {foil_server-0.2.3 → foil_server-0.2.4}/spec/README.md +0 -0
  24. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/fingerprints/detail.json +0 -0
  25. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/fingerprints/list.json +0 -0
  26. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/gate/agent-token-revoke.json +0 -0
  27. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/gate/agent-token-verify.json +0 -0
  28. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/gate/login-session-consume.json +0 -0
  29. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/gate/login-session-create.json +0 -0
  30. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/gate/registry-detail.json +0 -0
  31. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/gate/registry-list.json +0 -0
  32. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/gate/service-create.json +0 -0
  33. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/gate/service-detail.json +0 -0
  34. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/gate/service-disable.json +0 -0
  35. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/gate/service-update.json +0 -0
  36. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/gate/services-list.json +0 -0
  37. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/gate/session-ack.json +0 -0
  38. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/gate/session-create.json +0 -0
  39. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/gate/session-poll.json +0 -0
  40. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/organizations/api-key-create.json +0 -0
  41. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/organizations/api-key-list.json +0 -0
  42. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/organizations/api-key-revoke.json +0 -0
  43. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/organizations/api-key-rotate.json +0 -0
  44. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/organizations/api-key-update.json +0 -0
  45. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/organizations/organization-create.json +0 -0
  46. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/organizations/organization-update.json +0 -0
  47. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/api/organizations/organization.json +0 -0
  48. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/errors/invalid-api-key.json +0 -0
  49. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/errors/missing-api-key.json +0 -0
  50. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/errors/not-found.json +0 -0
  51. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/errors/validation-error.json +0 -0
  52. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/gate-delivery/approved-webhook-payload.valid.json +0 -0
  53. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/gate-delivery/delivery-request.json +0 -0
  54. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/gate-delivery/env-policy.json +0 -0
  55. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/gate-delivery/vector.v1.json +0 -0
  56. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/gate-delivery/webhook-signature.json +0 -0
  57. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/sealed-token/invalid.json +0 -0
  58. {foil_server-0.2.3 → foil_server-0.2.4}/spec/fixtures/sealed-token/vector.v1.json +0 -0
  59. {foil_server-0.2.3 → foil_server-0.2.4}/spec/sealed-token.md +0 -0
  60. {foil_server-0.2.3 → foil_server-0.2.4}/tests/test_gate_delivery.py +0 -0
  61. {foil_server-0.2.3 → foil_server-0.2.4}/tests/test_helpers.py +0 -0
  62. {foil_server-0.2.3 → foil_server-0.2.4}/tests/test_live.py +0 -0
  63. {foil_server-0.2.3 → foil_server-0.2.4}/tests/test_sealed_token.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: foil-server
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Official Foil Python server SDK
5
5
  Project-URL: Homepage, https://github.com/abxy-labs/foil-server-python
6
6
  Project-URL: Repository, https://github.com/abxy-labs/foil-server-python
@@ -57,6 +57,8 @@ client = Foil(secret_key="sk_live_...")
57
57
 
58
58
  page = client.sessions.list(verdict="bot", limit=25)
59
59
  session = client.sessions.get("sid_123")
60
+ client.sessions.attach_client_user("sid_123", "user_123")
61
+ client.sessions.clear_client_user("sid_123")
60
62
  ```
61
63
 
62
64
  ### Sealed token verification
@@ -42,6 +42,8 @@ client = Foil(secret_key="sk_live_...")
42
42
 
43
43
  page = client.sessions.list(verdict="bot", limit=25)
44
44
  session = client.sessions.get("sid_123")
45
+ client.sessions.attach_client_user("sid_123", "user_123")
46
+ client.sessions.clear_client_user("sid_123")
45
47
  ```
46
48
 
47
49
  ### Sealed token verification
@@ -145,6 +145,9 @@ def _parse_session_summary(data: dict[str, Any]) -> SessionSummary:
145
145
  object=str(data["object"]),
146
146
  id=str(data["id"]),
147
147
  created_at=data.get("created_at"),
148
+ client_user_id=data.get("client_user_id")
149
+ if isinstance(data.get("client_user_id"), str) or data.get("client_user_id") is None
150
+ else None,
148
151
  latest_decision=_parse_decision(dict(data["latest_decision"])),
149
152
  visitor_fingerprint=_parse_visitor_fingerprint_link(data.get("visitor_fingerprint")),
150
153
  )
@@ -155,6 +158,9 @@ def _parse_session_detail(data: dict[str, Any]) -> SessionDetail:
155
158
  object=str(data["object"]),
156
159
  id=str(data["id"]),
157
160
  created_at=data.get("created_at"),
161
+ client_user_id=data.get("client_user_id")
162
+ if isinstance(data.get("client_user_id"), str) or data.get("client_user_id") is None
163
+ else None,
158
164
  decision=_parse_session_decision(dict(data["decision"])),
159
165
  highlights=[dict(item) for item in data.get("highlights", []) if isinstance(item, dict)],
160
166
  attribution=dict(data.get("attribution")) if isinstance(data.get("attribution"), dict) else None,
@@ -570,6 +576,22 @@ class SessionsAPI(_BaseAPI):
570
576
  response = self._client._request_json("GET", f"/v1/sessions/{session_id}")
571
577
  return _parse_session_detail(dict(response["data"]))
572
578
 
579
+ def attach_client_user(self, session_id: str, client_user_id: str) -> SessionDetail:
580
+ response = self._client._request_json(
581
+ "PATCH",
582
+ f"/v1/sessions/{session_id}",
583
+ body={"client_user_id": client_user_id},
584
+ )
585
+ return _parse_session_detail(dict(response["data"]))
586
+
587
+ def clear_client_user(self, session_id: str) -> SessionDetail:
588
+ response = self._client._request_json(
589
+ "PATCH",
590
+ f"/v1/sessions/{session_id}",
591
+ body={"client_user_id": None},
592
+ )
593
+ return _parse_session_detail(dict(response["data"]))
594
+
573
595
  def iter(
574
596
  self,
575
597
  *,
@@ -72,6 +72,7 @@ class SessionSummary:
72
72
  object: str
73
73
  id: str
74
74
  created_at: str | None
75
+ client_user_id: str | None
75
76
  latest_decision: Decision
76
77
  visitor_fingerprint: VisitorFingerprintLink | None
77
78
 
@@ -81,6 +82,7 @@ class SessionDetail:
81
82
  object: str
82
83
  id: str
83
84
  created_at: str | None
85
+ client_user_id: str | None
84
86
  decision: SessionDecision
85
87
  highlights: list[dict[str, Any]]
86
88
  attribution: dict[str, Any] | None
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "foil-server"
7
- version = "0.2.3"
7
+ version = "0.2.4"
8
8
  description = "Official Foil Python server SDK"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -3,6 +3,7 @@
3
3
  "object": "session",
4
4
  "id": "sid_0123456789abcdefghjkmnpqrs",
5
5
  "created_at": "2026-03-24T20:00:00.000Z",
6
+ "client_user_id": "user_123",
6
7
  "decision": {
7
8
  "event_id": "evt_23456789abcdefghjkmnpqrstv",
8
9
  "automation_status": "automated",
@@ -4,6 +4,7 @@
4
4
  "object": "session",
5
5
  "id": "sid_0123456789abcdefghjkmnpqrs",
6
6
  "created_at": "2026-03-24T20:00:00.000Z",
7
+ "client_user_id": "user_123",
7
8
  "latest_decision": {
8
9
  "event_id": "evt_23456789abcdefghjkmnpqrstv",
9
10
  "verdict": "human",
@@ -36,6 +36,12 @@
36
36
  "method": "get",
37
37
  "status": "200"
38
38
  },
39
+ {
40
+ "file": "api/sessions/detail.json",
41
+ "path": "/v1/sessions/{sessionId}",
42
+ "method": "patch",
43
+ "status": "200"
44
+ },
39
45
  {
40
46
  "file": "api/fingerprints/list.json",
41
47
  "path": "/v1/fingerprints",
@@ -14,11 +14,11 @@
14
14
  "tags": [
15
15
  {
16
16
  "name": "Sessions",
17
- "description": "Durable session readback endpoints."
17
+ "description": "Session readback endpoints."
18
18
  },
19
19
  {
20
20
  "name": "Visitor fingerprints",
21
- "description": "Durable visitor fingerprint readback endpoints."
21
+ "description": "Visitor fingerprint readback endpoints."
22
22
  },
23
23
  {
24
24
  "name": "Organizations",
@@ -103,6 +103,7 @@
103
103
  "object": "session",
104
104
  "id": "sid_87wfetm98myh9awj0dpt28a3kc",
105
105
  "created_at": "2026-03-24T20:00:00.000Z",
106
+ "client_user_id": "user_123",
106
107
  "latest_decision": {
107
108
  "event_id": "evt_srwqc0yqhz4bxvtrdtf69q7vkr",
108
109
  "verdict": "human",
@@ -279,6 +280,7 @@
279
280
  "object": "session",
280
281
  "id": "sid_87wfetm98myh9awj0dpt28a3kc",
281
282
  "created_at": "2026-03-24T20:00:00.000Z",
283
+ "client_user_id": "user_123",
282
284
  "decision": {
283
285
  "event_id": "evt_srwqc0yqhz4bxvtrdtf69q7vkr",
284
286
  "automation_status": "automated",
@@ -908,6 +910,87 @@
908
910
  }
909
911
  },
910
912
  "description": "Requires the `sessions:read` secret-key scope."
913
+ },
914
+ "patch": {
915
+ "tags": [
916
+ "Sessions"
917
+ ],
918
+ "operationId": "updateSession",
919
+ "summary": "Update a session",
920
+ "parameters": [
921
+ {
922
+ "name": "sessionId",
923
+ "in": "path",
924
+ "required": true,
925
+ "schema": {
926
+ "$ref": "#/components/schemas/SessionId",
927
+ "example": "sid_87wfetm98myh9awj0dpt28a3kc"
928
+ }
929
+ }
930
+ ],
931
+ "requestBody": {
932
+ "required": true,
933
+ "content": {
934
+ "application/json": {
935
+ "schema": {
936
+ "$ref": "#/components/schemas/UpdateSessionRequest"
937
+ }
938
+ }
939
+ }
940
+ },
941
+ "responses": {
942
+ "200": {
943
+ "description": "Updated session detail response.",
944
+ "content": {
945
+ "application/json": {
946
+ "schema": {
947
+ "$ref": "#/components/schemas/SessionDetailResponse"
948
+ }
949
+ }
950
+ }
951
+ },
952
+ "401": {
953
+ "description": "Missing or invalid API key.",
954
+ "content": {
955
+ "application/json": {
956
+ "schema": {
957
+ "$ref": "#/components/schemas/ApiErrorEnvelope"
958
+ }
959
+ }
960
+ }
961
+ },
962
+ "403": {
963
+ "description": "Secret key missing the required `sessions:update` scope.",
964
+ "content": {
965
+ "application/json": {
966
+ "schema": {
967
+ "$ref": "#/components/schemas/ApiErrorEnvelope"
968
+ }
969
+ }
970
+ }
971
+ },
972
+ "404": {
973
+ "description": "Session not found.",
974
+ "content": {
975
+ "application/json": {
976
+ "schema": {
977
+ "$ref": "#/components/schemas/ApiErrorEnvelope"
978
+ }
979
+ }
980
+ }
981
+ },
982
+ "422": {
983
+ "description": "Invalid update payload.",
984
+ "content": {
985
+ "application/json": {
986
+ "schema": {
987
+ "$ref": "#/components/schemas/ApiErrorEnvelope"
988
+ }
989
+ }
990
+ }
991
+ }
992
+ },
993
+ "description": "Set or clear the customer-supplied `client_user_id` for a session. Requires the `sessions:update` secret-key scope."
911
994
  }
912
995
  },
913
996
  "/v1/fingerprints": {
@@ -2139,6 +2222,7 @@
2139
2222
  "environment": "live",
2140
2223
  "scopes": [
2141
2224
  "sessions:read",
2225
+ "sessions:update",
2142
2226
  "fingerprints:read"
2143
2227
  ]
2144
2228
  }
@@ -7292,6 +7376,7 @@
7292
7376
  "*",
7293
7377
  "sessions:list",
7294
7378
  "sessions:read",
7379
+ "sessions:update",
7295
7380
  "fingerprints:list",
7296
7381
  "fingerprints:read",
7297
7382
  "organizations:create",
@@ -7674,6 +7759,7 @@
7674
7759
  "*",
7675
7760
  "sessions:list",
7676
7761
  "sessions:read",
7762
+ "sessions:update",
7677
7763
  "fingerprints:list",
7678
7764
  "fingerprints:read",
7679
7765
  "organizations:create",
@@ -7703,6 +7789,7 @@
7703
7789
  "environment": "live",
7704
7790
  "scopes": [
7705
7791
  "sessions:read",
7792
+ "sessions:update",
7706
7793
  "fingerprints:read"
7707
7794
  ]
7708
7795
  },
@@ -10343,6 +10430,7 @@
10343
10430
  "*",
10344
10431
  "sessions:list",
10345
10432
  "sessions:read",
10433
+ "sessions:update",
10346
10434
  "fingerprints:list",
10347
10435
  "fingerprints:read",
10348
10436
  "organizations:create",
@@ -11725,7 +11813,8 @@
11725
11813
  "device",
11726
11814
  "analysis_coverage",
11727
11815
  "signals_fired",
11728
- "client_telemetry"
11816
+ "client_telemetry",
11817
+ "client_user_id"
11729
11818
  ],
11730
11819
  "properties": {
11731
11820
  "object": {
@@ -11744,6 +11833,15 @@
11744
11833
  "format": "date-time",
11745
11834
  "example": "2026-03-24T20:00:00.000Z"
11746
11835
  },
11836
+ "client_user_id": {
11837
+ "type": [
11838
+ "string",
11839
+ "null"
11840
+ ],
11841
+ "maxLength": 256,
11842
+ "description": "Customer-supplied identifier for the end user associated with this Foil session. Set with PATCH /v1/sessions/{sessionId}.",
11843
+ "example": "user_123"
11844
+ },
11747
11845
  "decision": {
11748
11846
  "$ref": "#/components/schemas/SessionDetailDecision",
11749
11847
  "example": {
@@ -12647,6 +12745,7 @@
12647
12745
  "object": "session",
12648
12746
  "id": "sid_87wfetm98myh9awj0dpt28a3kc",
12649
12747
  "created_at": "2026-03-24T20:00:00.000Z",
12748
+ "client_user_id": "user_123",
12650
12749
  "decision": {
12651
12750
  "event_id": "evt_srwqc0yqhz4bxvtrdtf69q7vkr",
12652
12751
  "automation_status": "automated",
@@ -13379,6 +13478,7 @@
13379
13478
  "object": "session",
13380
13479
  "id": "sid_87wfetm98myh9awj0dpt28a3kc",
13381
13480
  "created_at": "2026-03-24T20:00:00.000Z",
13481
+ "client_user_id": "user_123",
13382
13482
  "decision": {
13383
13483
  "event_id": "evt_srwqc0yqhz4bxvtrdtf69q7vkr",
13384
13484
  "automation_status": "automated",
@@ -13903,6 +14003,7 @@
13903
14003
  "object": "session",
13904
14004
  "id": "sid_87wfetm98myh9awj0dpt28a3kc",
13905
14005
  "created_at": "2026-03-24T20:00:00.000Z",
14006
+ "client_user_id": "user_123",
13906
14007
  "decision": {
13907
14008
  "event_id": "evt_srwqc0yqhz4bxvtrdtf69q7vkr",
13908
14009
  "automation_status": "automated",
@@ -15263,6 +15364,7 @@
15263
15364
  "object": "session",
15264
15365
  "id": "sid_87wfetm98myh9awj0dpt28a3kc",
15265
15366
  "created_at": "2026-03-24T20:00:00.000Z",
15367
+ "client_user_id": "user_123",
15266
15368
  "latest_decision": {
15267
15369
  "event_id": "evt_srwqc0yqhz4bxvtrdtf69q7vkr",
15268
15370
  "verdict": "human",
@@ -15289,6 +15391,7 @@
15289
15391
  "object": "session",
15290
15392
  "id": "sid_87wfetm98myh9awj0dpt28a3kc",
15291
15393
  "created_at": "2026-03-24T20:00:00.000Z",
15394
+ "client_user_id": "user_123",
15292
15395
  "latest_decision": {
15293
15396
  "event_id": "evt_srwqc0yqhz4bxvtrdtf69q7vkr",
15294
15397
  "verdict": "human",
@@ -15332,6 +15435,7 @@
15332
15435
  "object": "session",
15333
15436
  "id": "sid_87wfetm98myh9awj0dpt28a3kc",
15334
15437
  "created_at": "2026-03-24T20:00:00.000Z",
15438
+ "client_user_id": "user_123",
15335
15439
  "latest_decision": {
15336
15440
  "event_id": "evt_srwqc0yqhz4bxvtrdtf69q7vkr",
15337
15441
  "verdict": "human",
@@ -18169,7 +18273,8 @@
18169
18273
  "id",
18170
18274
  "created_at",
18171
18275
  "latest_decision",
18172
- "visitor_fingerprint"
18276
+ "visitor_fingerprint",
18277
+ "client_user_id"
18173
18278
  ],
18174
18279
  "properties": {
18175
18280
  "object": {
@@ -18188,6 +18293,15 @@
18188
18293
  "format": "date-time",
18189
18294
  "example": "2026-03-24T20:00:00.000Z"
18190
18295
  },
18296
+ "client_user_id": {
18297
+ "type": [
18298
+ "string",
18299
+ "null"
18300
+ ],
18301
+ "maxLength": 256,
18302
+ "description": "Customer-supplied identifier for the end user associated with this Foil session. Set with PATCH /v1/sessions/{sessionId}.",
18303
+ "example": "user_123"
18304
+ },
18191
18305
  "latest_decision": {
18192
18306
  "$ref": "#/components/schemas/Decision",
18193
18307
  "example": {
@@ -18232,6 +18346,7 @@
18232
18346
  "object": "session",
18233
18347
  "id": "sid_87wfetm98myh9awj0dpt28a3kc",
18234
18348
  "created_at": "2026-03-24T20:00:00.000Z",
18349
+ "client_user_id": "user_123",
18235
18350
  "latest_decision": {
18236
18351
  "event_id": "evt_srwqc0yqhz4bxvtrdtf69q7vkr",
18237
18352
  "verdict": "human",
@@ -18314,6 +18429,7 @@
18314
18429
  "*",
18315
18430
  "sessions:list",
18316
18431
  "sessions:read",
18432
+ "sessions:update",
18317
18433
  "fingerprints:list",
18318
18434
  "fingerprints:read",
18319
18435
  "organizations:create",
@@ -18495,6 +18611,27 @@
18495
18611
  "status": "active"
18496
18612
  }
18497
18613
  },
18614
+ "UpdateSessionRequest": {
18615
+ "type": "object",
18616
+ "additionalProperties": false,
18617
+ "required": [
18618
+ "client_user_id"
18619
+ ],
18620
+ "properties": {
18621
+ "client_user_id": {
18622
+ "type": [
18623
+ "string",
18624
+ "null"
18625
+ ],
18626
+ "maxLength": 256,
18627
+ "description": "Customer-supplied identifier for the end user associated with this Foil session. Set with PATCH /v1/sessions/{sessionId}.",
18628
+ "example": "user_123"
18629
+ }
18630
+ },
18631
+ "example": {
18632
+ "client_user_id": "user_123"
18633
+ }
18634
+ },
18498
18635
  "UpdateWebhookEndpointRequest": {
18499
18636
  "type": "object",
18500
18637
  "additionalProperties": false,
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  import os
4
5
  import unittest
5
6
 
@@ -142,6 +143,7 @@ class ClientTests(unittest.TestCase):
142
143
  try:
143
144
  session = client.sessions.get("sid_0123456789abcdefghjkmnpqrs")
144
145
  self.assertEqual(session.id, "sid_0123456789abcdefghjkmnpqrs")
146
+ self.assertEqual(session.client_user_id, "user_123")
145
147
  self.assertIsNone(session.native_runtime_integrity)
146
148
  self.assertIsNone(session.native_app)
147
149
  self.assertIsNone(session.native_carrier)
@@ -155,6 +157,27 @@ class ClientTests(unittest.TestCase):
155
157
  finally:
156
158
  client.close()
157
159
 
160
+ def test_attach_and_clear_client_user_id(self) -> None:
161
+ session_fixture = load_fixture("api/sessions/detail.json")
162
+ seen_bodies: list[dict[str, object]] = []
163
+
164
+ def handler(request: httpx.Request) -> httpx.Response:
165
+ self.assertEqual(request.method, "PATCH")
166
+ self.assertEqual(request.url.path, "/v1/sessions/sid_0123456789abcdefghjkmnpqrs")
167
+ self.assertEqual(request.headers["Authorization"], "Bearer sk_live_test")
168
+ seen_bodies.append(json.loads(request.content.decode("utf-8")))
169
+ return json_response(session_fixture)
170
+
171
+ client = Foil(secret_key="sk_live_test", transport=httpx.MockTransport(handler))
172
+ try:
173
+ attached = client.sessions.attach_client_user("sid_0123456789abcdefghjkmnpqrs", "user_123")
174
+ cleared = client.sessions.clear_client_user("sid_0123456789abcdefghjkmnpqrs")
175
+ self.assertEqual(attached.id, "sid_0123456789abcdefghjkmnpqrs")
176
+ self.assertEqual(cleared.id, "sid_0123456789abcdefghjkmnpqrs")
177
+ self.assertEqual(seen_bodies, [{"client_user_id": "user_123"}, {"client_user_id": None}])
178
+ finally:
179
+ client.close()
180
+
158
181
  def test_organizations_and_api_keys(self) -> None:
159
182
  organization_fixture = load_fixture("api/organizations/organization.json")
160
183
  organization_create_fixture = load_fixture("api/organizations/organization-create.json")
@@ -104,6 +104,14 @@ class ContractTests(unittest.TestCase):
104
104
  self.assertEqual(schemas["ApiKeyId"]["pattern"], "^key_[0123456789abcdefghjkmnpqrstvwxyz]{26}$")
105
105
 
106
106
  self.assertEqual(strip_examples(schemas["SessionSummary"]["properties"]["id"]), {"$ref": "#/components/schemas/SessionId"})
107
+ self.assertEqual(
108
+ strip_examples(schemas["SessionSummary"]["properties"]["client_user_id"]),
109
+ {
110
+ "type": ["string", "null"],
111
+ "maxLength": 256,
112
+ "description": "Customer-supplied identifier for the end user associated with this Foil session. Set with PATCH /v1/sessions/{sessionId}.",
113
+ },
114
+ )
107
115
  self.assertEqual(strip_examples(schemas["Organization"]["properties"]["status"]), {"$ref": "#/components/schemas/OrganizationStatus"})
108
116
  self.assertEqual(strip_examples(schemas["ApiKey"]["properties"]["status"]), {"$ref": "#/components/schemas/ApiKeyStatus"})
109
117
  self.assertEqual(
@@ -113,7 +121,7 @@ class ContractTests(unittest.TestCase):
113
121
  self.assertEqual(schemas["OrganizationStatus"]["enum"], ["active", "suspended", "deleted"])
114
122
  self.assertEqual(schemas["ApiKeyStatus"]["enum"], ["active", "rotating", "revoked"])
115
123
  self.assertTrue(
116
- {"decision", "highlights", "attribution", "web_bot_auth", "network", "runtime_integrity", "visitor_fingerprint", "connection_fingerprint", "previous_decisions", "request", "browser", "device", "analysis_coverage", "signals_fired", "client_telemetry"}.issubset(
124
+ {"client_user_id", "decision", "highlights", "attribution", "web_bot_auth", "network", "runtime_integrity", "visitor_fingerprint", "connection_fingerprint", "previous_decisions", "request", "browser", "device", "analysis_coverage", "signals_fired", "client_telemetry"}.issubset(
117
125
  set(schemas["SessionDetail"]["required"])
118
126
  )
119
127
  )
@@ -157,6 +165,8 @@ class ContractTests(unittest.TestCase):
157
165
 
158
166
  self.assertEqual(paths["/v1/sessions"]["get"]["operationId"], "listSessions")
159
167
  self.assertEqual(paths["/v1/sessions"]["get"]["tags"], ["Sessions"])
168
+ self.assertEqual(paths["/v1/sessions/{sessionId}"]["patch"]["operationId"], "updateSession")
169
+ self.assertEqual(paths["/v1/sessions/{sessionId}"]["patch"]["tags"], ["Sessions"])
160
170
  self.assertEqual(paths["/v1/fingerprints/{visitorId}"]["get"]["operationId"], "getVisitorFingerprint")
161
171
  self.assertEqual(paths["/v1/fingerprints/{visitorId}"]["get"]["tags"], ["Visitor fingerprints"])
162
172
  self.assertEqual(paths["/v1/organizations/{organizationId}"]["patch"]["operationId"], "updateOrganization")
File without changes
File without changes
File without changes
File without changes
File without changes