Skip to content

ENG-12481 feat(sdk): read third-party credentials an app is connected to - #7192

Open
adhami3310 wants to merge 2 commits into
mainfrom
khaleel/eng-12481-sdk-connections
Open

adhami3310 wants to merge 2 commits into
mainfrom
khaleel/eng-12481-sdk-connections

Conversation

@adhami3310

@adhami3310 adhami3310 commented Sep 18, 2026

Copy link
Copy Markdown
Member

The SDK half of ENG-12481. ENG-12434 shipped brokered connections β€” the seam, the store, the ledger and the routes β€” and nothing called them, because no client could. This is a client.

# Inside a deployed app, which is started with its own access token.
with ReflexCloud() as client:
    token = client.apps.connections.credential(app_id, "openai").access_token
    token = client.apps.connections.credential(app_id, "openai", end_user=user_id).access_token

API

Method Route Notes
apps.connections.providers() GET /connections/providers What can be connected.
list(app_id) GET /apps/{id}/connections What the app itself has connected.
status(app_id, provider, *, end_user) GET .../status
credential(app_id, provider, *, end_user) GET .../credential The docstring says to read it per call: the API refreshes it on the way out, so a stored copy outlives what the provider accepts.
connect_link(app_id, provider, *, end_user, return_to) POST .../authorize or .../session One method over two routes that differ only in whose connection they start.
disconnect(app_id, provider, *, end_user) DELETE .../{provider} or POST .../disconnect Likewise.

end_user picks whose connection a call acts on. It travels as X-End-User, and the header is absent rather than empty when it is not passed, because the API reads a missing header as the app's own connection β€” a helper that always sent it would have an app read a visitor's row. There are tests for both directions.

Two things this adds to the client itself

  • extra_headers on the internal request path, which is what X-End-User needed; nothing else uses it.
  • APIStatusError.code, read from X-Reflex-Error-Code. These routes are the first whose refusals a caller must branch on β€” not_connected, connection_gone, unsupported_credential, prior_revoke_owed, broker_unavailable, connections_unconfigured β€” and matching on prose was the only way before. flexgen emits that header on every ServiceError refusal, so this pays off beyond connections. I stopped at the field rather than minting named exception classes; that can follow if callers want it.

Handling the credential

Credential.access_token is the first live secret in an SDK model, and a frozen dataclass prints every field. It is declared field(repr=False) with a test that repr() does not contain the token, so printing the model, or an exception rendering a local, discloses nothing. The API sends Cache-Control: no-store and the client stores nothing.

Not in this PR

  • An app still cannot learn its own id. Every route takes {app_id}, and platform_env_for_app ships only the token and the backend URL. The token resolves to an app server-side, but no route answers "which app am I", so code inside a deployed app cannot fill the path parameter. That is a flexgen change; the SDK takes app_id from the caller meanwhile.
  • The sandbox-side helper, and the ticket's question of whether the builder preview gets connections at all. The preview's token is the user's, whose app_id is NULL, so the routes refuse it; an unpublished thread has no app row either.

Testing

  • uv run pytest tests/units/reflex_build_sdk: 728 passed, covering each method through both clients, both X-End-User directions, all three credential kinds, the refusal code, the masked repr, and that a failed session is not retried into a second link.
  • The schema snapshot is refreshed, so the four new models are contract-checked against the API's own declarations rather than taken from the source.
  • uv run pytest tests/units: 9579 passed, 78.8% coverage. One unrelated flake, test_state_manager_lock_expire, which passes on its own.
  • pyright, ruff and pre-commit clean.
  • Not exercised against a live broker: nothing has been, and the ticket keeps that as a manual gate before CONNECTIONS_ENABLED goes on.

Review in cubic

@adhami3310
adhami3310 requested a review from a team as a code owner September 18, 2026 00:57
@linear-code

linear-code Bot commented Sep 18, 2026

Copy link
Copy Markdown

ENG-12481

@greptile-apps

greptile-apps Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with the new connection APIs matching the documented service contract and no actionable failures identified.

Summary

This PR adds synchronous and asynchronous SDK support for third-party app connections.

  • Exposes provider discovery, connection listing and status, live credential retrieval, authorization links, and disconnection.
  • Adds optional per-request headers so connection calls can identify an end user without changing app-owned behavior.
  • Adds structured refusal codes to API errors and masks live credentials from dataclass representations.
  • Updates the OpenAPI snapshot, SDK documentation, release notes, and unit coverage for the new API.

Reviews (1) Β· Last reviewed commit: "Name the connections news fragment after..."

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid β€” if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests/units/reflex_build_sdk/test_errors.py">

<violation number="1" location="tests/units/reflex_build_sdk/test_errors.py:70">
P2: Because `detail` and `x-reflex-error-code` have the same value, this test does not verify that `APIStatusError.code` comes from the header. Use a different detail value so a regression to detail-based extraction fails.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

def test_status_error_code_from_the_header():
response = _response(
409,
json={"detail": "not_connected"},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Because detail and x-reflex-error-code have the same value, this test does not verify that APIStatusError.code comes from the header. Use a different detail value so a regression to detail-based extraction fails.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At tests/units/reflex_build_sdk/test_errors.py, line 70:

<comment>Because `detail` and `x-reflex-error-code` have the same value, this test does not verify that `APIStatusError.code` comes from the header. Use a different detail value so a regression to detail-based extraction fails.</comment>

<file context>
@@ -64,6 +64,19 @@ def test_status_error_type(status_code: int, error_type: type[APIStatusError]):
+def test_status_error_code_from_the_header():
+    response = _response(
+        409,
+        json={"detail": "not_connected"},
+        headers={"x-reflex-error-code": "not_connected"},
+    )
</file context>
Suggested change
json={"detail": "not_connected"},
json={"detail": "Connection unavailable"},

@codspeed

codspeed Bot commented Sep 18, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

βœ… 53 untouched benchmarks
⏩ 9 skipped benchmarks1


Comparing khaleel/eng-12481-sdk-connections (f696e2d) with main (0d8acb3)

Open in CodSpeed

Footnotes

  1. 9 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 16 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid β€” if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/reflex-build-sdk/src/reflex_build_sdk/_base.py">

<violation number="1" location="packages/reflex-build-sdk/src/reflex_build_sdk/_base.py:223">
P3: When an internal caller supplies `X-Request-ID`, `Accept`, or `User-Agent` through `extra_headers`, this merge replaces the SDK-owned header even though `_build_request` promises a fresh request ID. That makes `APIError.request_id` caller-controlled and can conflate requests when the same value is reused. Merge extra headers before the SDK-owned headers or reject reserved names.</violation>
</file>

<file name="tests/units/reflex_build_sdk/_async/resources/test_connections.py">

<violation number="1" location="tests/units/reflex_build_sdk/_async/resources/test_connections.py:141">
P2: The credential tests never exercise `end_user`, so they cannot catch a regression that stops forwarding `X-End-User` for user credentials. Call `credential` with `end_user=END_USER` and assert the header, while retaining the existing app-scoped coverage elsewhere.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

client: AsyncReflexCloud, mock_api: MockAPI, body: dict, credential: Credential
):
mock_api.add("GET", f"{PROVIDER_PATH}/credential", reply(200, json=body))
assert await client.apps.connections.credential(APP_ID, "openai") == credential

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The credential tests never exercise end_user, so they cannot catch a regression that stops forwarding X-End-User for user credentials. Call credential with end_user=END_USER and assert the header, while retaining the existing app-scoped coverage elsewhere.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At tests/units/reflex_build_sdk/_async/resources/test_connections.py, line 141:

<comment>The credential tests never exercise `end_user`, so they cannot catch a regression that stops forwarding `X-End-User` for user credentials. Call `credential` with `end_user=END_USER` and assert the header, while retaining the existing app-scoped coverage elsewhere.</comment>

<file context>
@@ -0,0 +1,229 @@
+    client: AsyncReflexCloud, mock_api: MockAPI, body: dict, credential: Credential
+):
+    mock_api.add("GET", f"{PROVIDER_PATH}/credential", reply(200, json=body))
+    assert await client.apps.connections.credential(APP_ID, "openai") == credential
+
+
</file context>
Suggested change
assert await client.apps.connections.credential(APP_ID, "openai") == credential
assert await client.apps.connections.credential(
APP_ID, "openai", end_user=END_USER
) == credential
assert mock_api.requests[0].headers["X-End-User"] == END_USER

"Accept": "application/json",
"User-Agent": user_agent(),
"X-Request-ID": uuid.uuid4().hex,
**(extra_headers or {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When an internal caller supplies X-Request-ID, Accept, or User-Agent through extra_headers, this merge replaces the SDK-owned header even though _build_request promises a fresh request ID. That makes APIError.request_id caller-controlled and can conflate requests when the same value is reused. Merge extra headers before the SDK-owned headers or reject reserved names.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At packages/reflex-build-sdk/src/reflex_build_sdk/_base.py, line 223:

<comment>When an internal caller supplies `X-Request-ID`, `Accept`, or `User-Agent` through `extra_headers`, this merge replaces the SDK-owned header even though `_build_request` promises a fresh request ID. That makes `APIError.request_id` caller-controlled and can conflate requests when the same value is reused. Merge extra headers before the SDK-owned headers or reject reserved names.</comment>

<file context>
@@ -218,6 +220,7 @@ def _build_request(
             "Accept": "application/json",
             "User-Agent": user_agent(),
             "X-Request-ID": uuid.uuid4().hex,
+            **(extra_headers or {}),
         }
         if authenticated:
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants