Skip to content

Commit 8e53385

Browse files
Domantasclaude
andcommitted
fix(transport): send norbix-project-id and norbix-account-id instead of X-CM-*
The SDK sent the project and account only as X-CM-ProjectId and X-CM-AccountId. The gateway reads neither name — only norbix-project-id and norbix-account-id (AuthStatics) — so with an account bearer token every Api project call failed with CM-ERRORS-PROPERTY-001 "Property ProjectId is required". Same fix as the Go SDK (56f34b6): the legacy names are no longer sent. Sync and async transports both change; a new test checks the header names for a project-scoped Api call and an account Hub call, and that the X-CM-* names are absent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2d9b664 commit 8e53385

17 files changed

Lines changed: 128 additions & 51 deletions

‎docs/hub/account.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Access with `norbix.hub.account`.
44

55
Every method here needs only a key or a bearer token (scope `project`): the
66
gateway takes the account from the signed-in session, or from the project id
7-
in the path, and never reads the `X-CM-AccountId` header. Four routes are
7+
in the path, and never reads the `norbix-account-id` header. Four routes are
88
public on the gateway and need no token and no `account_id` at all (scope
99
`unauthenticated`, no `Authorization` header is sent): sign-up
1010
(`create_account`), accepting an invitation

‎src/norbix_python/transport.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ def send(
109109
if token:
110110
headers["Authorization"] = f"Bearer {token}"
111111

112-
headers["X-CM-ProjectId"] = self._cfg.project_id
112+
headers["norbix-project-id"] = self._cfg.project_id
113113
if self._cfg.account_id:
114-
headers["X-CM-AccountId"] = self._cfg.account_id
114+
headers["norbix-account-id"] = self._cfg.account_id
115115
# Environment selector: per-call override wins over the client default.
116116
# "PROD" is the backend default, so the header is omitted for it.
117117
resolved_env = env if env is not None else self._cfg.env
@@ -275,9 +275,9 @@ async def send(
275275
if token:
276276
headers["Authorization"] = f"Bearer {token}"
277277

278-
headers["X-CM-ProjectId"] = self._cfg.project_id
278+
headers["norbix-project-id"] = self._cfg.project_id
279279
if self._cfg.account_id:
280-
headers["X-CM-AccountId"] = self._cfg.account_id
280+
headers["norbix-account-id"] = self._cfg.account_id
281281
# Environment selector: per-call override wins over the client default.
282282
# "PROD" is the backend default, so the header is omitted for it.
283283
resolved_env = env if env is not None else self._cfg.env

‎tests/api/test_ai.py‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def test_api_ai_get_end_user_chat_availability_request_shape() -> None:
2929
assert transport.last_request['method'] == 'GET'
3030
assert transport.last_request['url'].endswith('/v3/ai/chat/availability')
3131
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
32-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
32+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
3333

3434

3535
def test_api_ai_list_end_user_chat_sessions_request_shape() -> None:
@@ -39,7 +39,7 @@ def test_api_ai_list_end_user_chat_sessions_request_shape() -> None:
3939
assert transport.last_request['method'] == 'GET'
4040
assert transport.last_request['url'].endswith('/v3/ai/chat/sessions')
4141
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
42-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
42+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
4343

4444

4545
def test_api_ai_create_end_user_chat_session_request_shape() -> None:
@@ -49,7 +49,7 @@ def test_api_ai_create_end_user_chat_session_request_shape() -> None:
4949
assert transport.last_request['method'] == 'POST'
5050
assert transport.last_request['url'].endswith('/v3/ai/chat/sessions')
5151
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
52-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
52+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
5353

5454

5555
def test_api_ai_get_end_user_chat_session_request_shape() -> None:
@@ -59,7 +59,7 @@ def test_api_ai_get_end_user_chat_session_request_shape() -> None:
5959
assert transport.last_request['method'] == 'GET'
6060
assert transport.last_request['url'].endswith('/v3/ai/chat/sessions/stub-SessionId')
6161
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
62-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
62+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
6363

6464

6565
def test_api_ai_rename_end_user_chat_session_request_shape() -> None:
@@ -69,7 +69,7 @@ def test_api_ai_rename_end_user_chat_session_request_shape() -> None:
6969
assert transport.last_request['method'] == 'PATCH'
7070
assert transport.last_request['url'].endswith('/v3/ai/chat/sessions/stub-SessionId')
7171
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
72-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
72+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
7373

7474

7575
def test_api_ai_delete_end_user_chat_session_request_shape() -> None:
@@ -79,7 +79,7 @@ def test_api_ai_delete_end_user_chat_session_request_shape() -> None:
7979
assert transport.last_request['method'] == 'DELETE'
8080
assert transport.last_request['url'].endswith('/v3/ai/chat/sessions/stub-SessionId')
8181
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
82-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
82+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
8383

8484

8585
def test_api_ai_pin_end_user_chat_session_request_shape() -> None:
@@ -89,7 +89,7 @@ def test_api_ai_pin_end_user_chat_session_request_shape() -> None:
8989
assert transport.last_request['method'] == 'PUT'
9090
assert transport.last_request['url'].endswith('/v3/ai/chat/sessions/stub-SessionId/pin')
9191
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
92-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
92+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
9393

9494

9595
def test_api_ai_archive_end_user_chat_session_request_shape() -> None:
@@ -99,7 +99,7 @@ def test_api_ai_archive_end_user_chat_session_request_shape() -> None:
9999
assert transport.last_request['method'] == 'PUT'
100100
assert transport.last_request['url'].endswith('/v3/ai/chat/sessions/stub-SessionId/archive')
101101
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
102-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
102+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
103103

104104

105105
def test_api_ai_get_end_user_chat_entries_request_shape() -> None:
@@ -109,7 +109,7 @@ def test_api_ai_get_end_user_chat_entries_request_shape() -> None:
109109
assert transport.last_request['method'] == 'GET'
110110
assert transport.last_request['url'].endswith('/v3/ai/chat/sessions/stub-SessionId/entries')
111111
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
112-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
112+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
113113

114114

115115
def test_api_ai_set_end_user_chat_entry_feedback_request_shape() -> None:
@@ -119,7 +119,7 @@ def test_api_ai_set_end_user_chat_entry_feedback_request_shape() -> None:
119119
assert transport.last_request['method'] == 'PUT'
120120
assert transport.last_request['url'].endswith('/v3/ai/chat/sessions/stub-SessionId/entries/stub-EntryId/feedback')
121121
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
122-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
122+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
123123

124124

125125
def test_api_ai_list_end_user_chat_attachments_request_shape() -> None:
@@ -129,7 +129,7 @@ def test_api_ai_list_end_user_chat_attachments_request_shape() -> None:
129129
assert transport.last_request['method'] == 'GET'
130130
assert transport.last_request['url'].endswith('/v3/ai/chat/sessions/stub-SessionId/attachments')
131131
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
132-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
132+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
133133

134134

135135
def test_api_ai_upload_end_user_chat_attachment_request_shape() -> None:
@@ -139,7 +139,7 @@ def test_api_ai_upload_end_user_chat_attachment_request_shape() -> None:
139139
assert transport.last_request['method'] == 'POST'
140140
assert transport.last_request['url'].endswith('/v3/ai/chat/sessions/stub-SessionId/attachments')
141141
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
142-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
142+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
143143

144144

145145
def test_api_ai_delete_end_user_chat_attachment_request_shape() -> None:
@@ -149,7 +149,7 @@ def test_api_ai_delete_end_user_chat_attachment_request_shape() -> None:
149149
assert transport.last_request['method'] == 'DELETE'
150150
assert transport.last_request['url'].endswith('/v3/ai/chat/attachments/stub-AttachmentId')
151151
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
152-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
152+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
153153

154154

155155
def test_api_ai_list_end_user_chat_memory_request_shape() -> None:
@@ -159,7 +159,7 @@ def test_api_ai_list_end_user_chat_memory_request_shape() -> None:
159159
assert transport.last_request['method'] == 'GET'
160160
assert transport.last_request['url'].endswith('/v3/ai/chat/memory')
161161
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
162-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
162+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
163163

164164

165165
def test_api_ai_forget_end_user_chat_memory_request_shape() -> None:
@@ -169,7 +169,7 @@ def test_api_ai_forget_end_user_chat_memory_request_shape() -> None:
169169
assert transport.last_request['method'] == 'DELETE'
170170
assert transport.last_request['url'].endswith('/v3/ai/chat/memory/stub-NoteId')
171171
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
172-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
172+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
173173

174174

175175
def test_api_ai_start_end_user_chat_turn_request_shape() -> None:
@@ -179,5 +179,5 @@ def test_api_ai_start_end_user_chat_turn_request_shape() -> None:
179179
assert transport.last_request['method'] == 'POST'
180180
assert transport.last_request['url'].endswith('/v3/ai/chat/turn')
181181
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
182-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
182+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
183183

‎tests/hub/test_account.py‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ def test_hub_account_get_account_collaborators_needs_only_a_token() -> None:
292292
client.hub.account.get_account_collaborators()
293293
assert transport.last_request is not None
294294
assert transport.last_request['method'] == 'GET'
295-
assert 'x-cm-accountid' not in transport.last_request['headers']
295+
assert 'norbix-account-id' not in transport.last_request['headers']
296296

297297
def test_hub_account_send_invite_to_team_member_request_shape() -> None:
298298
client, transport = make_client(account_id='acc-1')
@@ -327,7 +327,7 @@ def test_hub_account_get_project_ai_settings_request_shape() -> None:
327327
assert transport.last_request['method'] == 'GET'
328328
assert transport.last_request['url'].endswith('/v3/account/projects/stub-projectId/ai/settings')
329329
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
330-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
330+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
331331

332332

333333
def test_hub_account_update_project_ai_settings_request_shape() -> None:
@@ -337,7 +337,7 @@ def test_hub_account_update_project_ai_settings_request_shape() -> None:
337337
assert transport.last_request['method'] == 'PUT'
338338
assert transport.last_request['url'].endswith('/v3/account/projects/stub-projectId/ai/settings')
339339
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
340-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
340+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
341341

342342

343343
def test_hub_account_create_project_ai_assistant_request_shape() -> None:
@@ -347,7 +347,7 @@ def test_hub_account_create_project_ai_assistant_request_shape() -> None:
347347
assert transport.last_request['method'] == 'POST'
348348
assert transport.last_request['url'].endswith('/v3/account/projects/stub-projectId/ai/assistants')
349349
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
350-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
350+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
351351

352352

353353
def test_hub_account_update_project_ai_assistant_request_shape() -> None:
@@ -357,7 +357,7 @@ def test_hub_account_update_project_ai_assistant_request_shape() -> None:
357357
assert transport.last_request['method'] == 'PUT'
358358
assert transport.last_request['url'].endswith('/v3/account/projects/stub-projectId/ai/assistants/stub-assistantId')
359359
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
360-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
360+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
361361

362362

363363
def test_hub_account_delete_project_ai_assistant_request_shape() -> None:
@@ -367,7 +367,7 @@ def test_hub_account_delete_project_ai_assistant_request_shape() -> None:
367367
assert transport.last_request['method'] == 'DELETE'
368368
assert transport.last_request['url'].endswith('/v3/account/projects/stub-projectId/ai/assistants/stub-assistantId')
369369
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
370-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
370+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
371371

372372

373373
def test_hub_account_get_project_ai_usage_request_shape() -> None:
@@ -377,7 +377,7 @@ def test_hub_account_get_project_ai_usage_request_shape() -> None:
377377
assert transport.last_request['method'] == 'GET'
378378
assert transport.last_request['url'].endswith('/v3/account/projects/stub-projectId/ai/usage')
379379
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
380-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
380+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
381381

382382

383383
def test_hub_account_set_admin_portal_enabled_request_shape() -> None:
@@ -387,7 +387,7 @@ def test_hub_account_set_admin_portal_enabled_request_shape() -> None:
387387
assert transport.last_request['method'] == 'PUT'
388388
assert transport.last_request['url'].endswith('/v3/account/projects/stub-projectId/admin-portal/enabled')
389389
assert transport.last_request['headers']['authorization'] == 'Bearer test-token'
390-
assert transport.last_request['headers']['x-cm-projectid'] == 'test-project'
390+
assert transport.last_request['headers']['norbix-project-id'] == 'test-project'
391391

392392

393393
def test_hub_account_check_project_languages_request_shape() -> None:
@@ -402,7 +402,7 @@ def test_hub_account_check_project_languages_request_shape() -> None:
402402
assert transport.last_request['method'] == 'POST'
403403
assert urlparse(transport.last_request['url']).path == '/v3/account/projects/proj-1/settings/languages/check'
404404
assert json.loads(transport.last_request['body']) == {"defaultLanguage": "en", "languages": ["en", "de"]}
405-
assert transport.last_request['headers']['x-cm-accountid'] == 'acc-1'
405+
assert transport.last_request['headers']['norbix-account-id'] == 'acc-1'
406406

407407
def test_async_hub_account_check_project_languages_hits_the_route() -> None:
408408
import asyncio

‎tests/hub/test_account_me.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def test_get_my_account_user_profile_reads_the_profile_and_phone() -> None:
4848
profile = _sync_client(seen, PROFILE).hub.account.get_my_account_user_profile()
4949

5050
assert [(r.method, urlparse(str(r.url)).path) for r in seen] == [("GET", "/v3/account/me")]
51-
assert "x-cm-accountid" not in seen[0].headers
51+
assert "norbix-account-id" not in seen[0].headers
5252
assert profile["item"]["email"] == "ada@example.test"
5353
assert profile["item"]["generalInfo"]["phone"] == "+37060000000"
5454

@@ -72,7 +72,7 @@ def test_get_account_collaborators_sends_flat_paging_and_the_project_filter() ->
7272
url = urlparse(str(seen[0].url))
7373
assert (seen[0].method, url.path) == ("GET", "/v3/account/collaborators")
7474
assert parse_qs(url.query) == {"projectId": ["proj_2"], "pageSize": ["50"], "startingAfter": ["member_20"]}
75-
assert "x-cm-accountid" not in seen[0].headers
75+
assert "norbix-account-id" not in seen[0].headers
7676

7777

7878
def test_async_account_me_and_team_list_need_only_a_token() -> None:

‎tests/hub/test_account_project.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def _check(request: httpx.Request, verb: str, path: str, body: dict[str, Any] |
5454
assert request.method == verb
5555
assert request.url.path == "/v3" + path
5656
assert request.headers["authorization"] == "Bearer test-token"
57-
assert request.headers["x-cm-projectid"] == "test-project"
57+
assert request.headers["norbix-project-id"] == "test-project"
5858
if body is None:
5959
assert request.content == b""
6060
else:

‎tests/hub/test_account_token_only.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
The gateway takes the account from the signed-in session on each route below
44
(``UserSession.UserAuth.AccountId``), or from the project id in the path, or
5-
needs no account at all — and it never reads the ``X-CM-AccountId`` header.
5+
needs no account at all — and it never reads the ``norbix-account-id`` header.
66
So each method must work on a client WITHOUT ``account_id``, like the
77
TypeScript, .NET, Go, Kotlin and Swift SDKs. One test per method, for the
88
sync and the async client: verb, resolved path, Bearer header and no account
@@ -83,7 +83,7 @@ def handler(request: httpx.Request) -> httpx.Response:
8383
def _assert_token_only(seen: list[httpx.Request], verb: str, path: str) -> None:
8484
assert [(r.method, urlparse(str(r.url)).path) for r in seen] == [(verb, path)]
8585
assert seen[0].headers["authorization"] == "Bearer test-token"
86-
assert "x-cm-accountid" not in seen[0].headers
86+
assert "norbix-account-id" not in seen[0].headers
8787

8888

8989
@pytest.mark.parametrize(("module", "method", "verb", "path", "ids"), CASES, ids=IDS)
@@ -139,7 +139,7 @@ async def run() -> None:
139139
def _assert_public(seen: list[httpx.Request], verb: str, path: str, fields: dict[str, str]) -> None:
140140
assert [(r.method, urlparse(str(r.url)).path) for r in seen] == [(verb, path)]
141141
assert "authorization" not in seen[0].headers
142-
assert "x-cm-accountid" not in seen[0].headers
142+
assert "norbix-account-id" not in seen[0].headers
143143
if verb == "GET":
144144
assert dict(parse_qsl(urlparse(str(seen[0].url)).query)) == fields
145145
else:

0 commit comments

Comments
 (0)