Skip to content

fix(connect): explain DPoP connection failures - #8351

Open
extoci wants to merge 6 commits into
pingdotgg:mainfrom
extoci:fix/connect-dpop-clock-diagnostics
Open

fix(connect): explain DPoP connection failures#8351
extoci wants to merge 6 commits into
pingdotgg:mainfrom
extoci:fix/connect-dpop-clock-diagnostics

Conversation

@extoci

@extoci extoci commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Problem

DPoP proof failures often appear as a generic connection error. A device clock mismatch is the most common cause, but a client can also be talking to an older T3 Connect server or relay.

What changed

  • Add typed, safe DPoP failure categories across the shared verifier, environment server, relay, and contracts.
  • Show a plain-language date/time hint when the category is absent (old server/relay) or confirms a time-window failure.
  • Show a neutral retry and trace-ID hint for known non-clock failures.
  • Preserve the hint in web, desktop-wrapped web, and mobile connection/status surfaces.
  • Add focused tests and user/operations documentation.
  • Record stable DPoP failure codes in server and relay spans for diagnosis.

Compatibility

The new response field is optional. Updated clients understand responses from older servers and relays, and older clients can ignore the additive field from newer servers. Because clock skew is the most common DPoP failure, clients use the date/time hint when dpopFailureReason is absent. This is the safest first diagnostic for older or otherwise unknown responses; newer responses use a neutral retry hint for known non-clock failures.

Validation

  • git diff --check passes.
  • Bun syntax bundling passes for all changed TypeScript/TSX entries (626 modules).
  • Focused PR tests pass: 11 files, 220 tests passed.
  • Package-scoped typechecks pass for server, relay, web, mobile, client-runtime, contracts, and shared. tsgo emitted suggestions only, with no errors.
  • Lint passes for all changed TypeScript files.

Focused verification is complete here. Please run the repository CI suite before merge.

Note

Add dpopFailureReason to auth errors and surface DPoP failure hints in clients

  • Introduces DpopFailureReason in baseSchemas.ts and adds it as an optional field to EnvironmentAuthInvalidError and RelayAuthInvalidError; server and relay now populate it with categorized codes derived from DPoP verifier failures.
  • verifyDpopProof in dpop.ts returns structured DpopVerificationFailureCode values instead of generic errors; relay verifyAndConsume raises the new DpopProofRejected instead of HttpApiError.Unauthorized.
  • Client runtime adds dpopFailureHint and relayProtectedErrorMessage in errorPresentation.ts to translate failure reasons into user-facing hints (clock hint for time_window, retry hint for others).
  • Web and mobile UI components switch to whitespace-pre-line/break-words rendering when error text contains newlines, so multi-line hints display without truncation.
  • Risk: verifyDpopProof now checks signature before time window, so an out-of-window but invalidly signed proof reports invalid_signature instead of time_window; relay callers of verifyAndConsume must handle DpopProofRejected instead of Unauthorized.

Macroscope summarized d479fc6.


Note

Medium Risk
Touches DPoP verification order and auth error payloads across environment server, relay, and all Connect clients; changes are additive and tested but still sit on the authentication path.

Overview
DPoP auth failures now carry an optional safe dpopFailureReason on environment and relay auth_invalid responses, backed by a shared DpopFailureReason schema and structured failure codes from verifyDpopProof (signature is validated before the time window so bad signatures no longer masquerade as clock skew).

Servers and relay map low-level verifier codes into those categories, attach them to API errors, and record environment.dpop.failure_code / relay.dpop.failure_code on spans. Relay DPoP verification returns DpopProofRejected with explicit codes instead of a generic unauthorized.

Clients centralize messaging in errorPresentation (dpopFailureHint, relayProtectedErrorMessage): missing or time_window reasons get a date/time hint; other known reasons get a neutral retry/trace hint. Mobile and web connection/status UI stop truncating messages that contain newlines so multi-line hints stay readable.

Tests and docs cover backward compatibility (older peers without the field still get the clock hint) and user-facing troubleshooting.

Reviewed by Cursor Bugbot for commit d479fc6. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes

    • Improved connection and authentication errors with clearer DPoP-specific guidance.
    • Added date and time troubleshooting hints when proof failures may be caused by clock differences.
    • Added retry guidance for other authentication failures.
    • Preserved detailed failure context across cloud, relay, WebSocket, and token connections.
  • User Interface

    • Multi-line error messages and status hints now wrap correctly instead of being truncated across mobile and web screens.
  • Documentation

    • Added troubleshooting and observability guidance for DPoP failures and clock-related connection issues.

@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e6218ea7-6875-4e9b-b9b6-c430d8167296

📥 Commits

Reviewing files that changed from the base of the PR and between a6797b3 and d479fc6.

📒 Files selected for processing (46)
  • apps/mobile/src/features/cloud/linkEnvironment.test.ts
  • apps/mobile/src/features/cloud/linkEnvironment.ts
  • apps/mobile/src/features/connection/CloudEnvironmentRows.tsx
  • apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx
  • apps/mobile/src/features/projects/AddProjectScreen.tsx
  • apps/server/src/auth/EnvironmentAuth.ts
  • apps/server/src/auth/dpop.test.ts
  • apps/server/src/auth/dpop.ts
  • apps/server/src/auth/http.ts
  • apps/server/src/http.ts
  • apps/server/src/server.test.ts
  • apps/server/src/ws.ts
  • apps/web/src/cloud/linkEnvironment.ts
  • apps/web/src/components/CommandPaletteResults.tsx
  • apps/web/src/components/chat/ComposerBannerStack.tsx
  • apps/web/src/components/clerk/MobileClientsUserProfilePage.tsx
  • apps/web/src/components/clerk/T3ConnectUserProfilePage.tsx
  • apps/web/src/components/cloud/CloudEnvironmentConnectList.tsx
  • apps/web/src/components/cloud/ConnectOnboardingDialog.tsx
  • apps/web/src/components/settings/ConnectionsSettings.tsx
  • apps/web/src/components/settings/ProviderSettingsPanel.tsx
  • apps/web/src/components/settings/settingsLayout.tsx
  • apps/web/src/components/ui/toast.tsx
  • docs/internals/environment-auth.md
  • docs/operations/observability.md
  • docs/operations/relay-observability.md
  • docs/user/remote-access.md
  • infra/relay/src/auth/DpopProofs.ts
  • infra/relay/src/auth/DpopProofs.verifyAndConsume.test.ts
  • infra/relay/src/http/Api.test.ts
  • infra/relay/src/http/Api.ts
  • packages/client-runtime/src/authorization/layer.test.ts
  • packages/client-runtime/src/authorization/service.ts
  • packages/client-runtime/src/connection/errors.test.ts
  • packages/client-runtime/src/connection/errors.ts
  • packages/client-runtime/src/relay/errorPresentation.test.ts
  • packages/client-runtime/src/relay/errorPresentation.ts
  • packages/client-runtime/src/relay/index.ts
  • packages/client-runtime/src/relay/managedRelay.test.ts
  • packages/client-runtime/src/relay/managedRelayState.test.ts
  • packages/client-runtime/src/relay/managedRelayState.ts
  • packages/contracts/src/baseSchemas.ts
  • packages/contracts/src/environmentHttp.ts
  • packages/contracts/src/relay.ts
  • packages/shared/src/dpop.test.ts
  • packages/shared/src/dpop.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 27, 2026

@macroscopeapp macroscopeapp 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.

UI consistency review of the changed web surfaces. The new multi-line DPoP hint is handled correctly where it is rendered (whitespace-pre-line break-words instead of truncate, and ConnectionStatusDot already sets max-w-80 whitespace-pre-wrap for the tooltip). Two consistency points on the wrap-vs-truncate treatment are noted inline.

Posted via Macroscope — UI Consistency

Comment on lines 234 to +236
className={cn(
"mt-1 truncate text-xs",
"mt-1 text-xs",
statusHasHint ? "whitespace-pre-line break-words" : "truncate",

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.

The text.includes("\n") ? "whitespace-pre-line break-words" : "truncate" decision is now duplicated in three web call sites (here, settings/ProviderSettingsPanel.tsx ~line 267, settings/ConnectionsSettings.tsx ~line 1470), while CommandPaletteResults.tsx and T3ConnectUserProfilePage.tsx apply the wrap classes unconditionally. The hint format (\n\n separator) is owned by client-runtime, so a change there has to be re-found in every row.

Consider a small named helper next to the status presentation code — e.g. statusTextWrapClassName(statusText) in components/cloud/cloudEnvironmentConnectionPresentation.ts returning "whitespace-pre-line break-words" or "truncate" — and calling it from each row, keeping contextual color/spacing classes at the call site.

Posted via Macroscope — UI Consistency

Comment on lines +1467 to +1476
<span
className={cn(
"min-w-0",
connectionStatusText(environment.connection).includes("\n")
? "whitespace-pre-line break-words"
: "truncate",
)}
>
{connectionStatusText(environment.connection)}
</span>

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.

connectionStatusText(environment.connection) is evaluated twice in this row, and statusTooltip (line 1376) already holds the same string. Reusing it keeps the wrap decision and the rendered text from drifting apart, and matches the sibling rows that hoist statusText/statusHasHint before the JSX.

Suggested change
<span
className={cn(
"min-w-0",
connectionStatusText(environment.connection).includes("\n")
? "whitespace-pre-line break-words"
: "truncate",
)}
>
{connectionStatusText(environment.connection)}
</span>
<span
className={cn(
"min-w-0",
statusTooltip.includes("\n") ? "whitespace-pre-line break-words" : "truncate",
)}
>
{statusTooltip}
</span>

Posted via Macroscope — UI Consistency

@macroscopeapp

macroscopeapp Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

UI Consistency: 2 findings

This PR makes several status/error strings multi-line ("...\n\nHint: ...") and updates most web renderers to whitespace-pre-line break-words. Two web renderers that read the same newly multi-line strings were not updated, so the hint collapses into the preceding sentence there (HTML whitespace collapsing) instead of rendering on its own line.

1. apps/web/src/components/clerk/MobileClientsUserProfilePage.tsx:133

devicesState.error comes from useManagedRelayDevices() -> readManagedRelaySnapshotState(), which this PR changed to return relayProtectedErrorMessage(...) — including "Relay rejected the DPoP proof.\n\nHint: Check the date and time on both devices, then try again.". The sibling page T3ConnectUserProfilePage.tsx:219 was updated for exactly this string, but this paragraph still uses plain classes, so the same relay error renders without the hint break. Consider:

-              <p className="mt-0.5 text-xs text-muted-foreground">{devicesState.error}</p>
+              <p className="mt-0.5 whitespace-pre-line break-words text-xs text-muted-foreground">
+                {devicesState.error}
+              </p>

2. apps/web/src/components/settings/ProviderSettingsPanel.tsx:182 (rendered at line 187)

EnvironmentUnavailableRow passes connectionStatusText(environment.connection) into SettingsRow's description, whose <p> in settingsLayout.tsx has no whitespace handling. The device picker rows further down in this same file were gated on statusText.includes("\n"), so the two status surfaces in one panel now disagree. Smallest fix: wrap the description in a span with whitespace-pre-line break-words (or pass the same conditional class) so the hint keeps its own line.

Neither change alters layout for single-line status text.

Posted via Macroscope — UI Consistency

@macroscopeapp macroscopeapp 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.

Reviewed the web UI changes that make status/error text render the new multi-line DPoP hint. The gated whitespace-pre-line break-words vs truncate treatment looks correct for the rows that were updated (no fixed-height or virtualized rows are involved, and ConnectionStatusDot's tooltip already sets whitespace-pre-wrap).

Two sibling render sites that display the same now-multi-line messages were not updated, so the hint collapses onto one line there. Details inline.

Posted via Macroscope — UI Consistency

Comment on lines +219 to +221
<p className="mt-1 whitespace-pre-line break-words text-xs text-muted-foreground">
{environmentsState.error}
</p>

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.

The sibling mobile-clients page renders the same relay snapshot error string and was not updated, so the new hint collapses to one run-on line there. useManagedRelayDevices and useManagedRelayEnvironments both go through readManagedRelaySnapshotState, which now returns relayProtectedErrorMessage(...) (which can contain \n\n), but MobileClientsUserProfilePage.tsx:133 still renders it with default whitespace handling.

Suggest applying the same treatment there so the two T3 Connect profile pages present the same message identically:

-              <p className="mt-0.5 text-xs text-muted-foreground">{devicesState.error}</p>
+              <p className="mt-0.5 whitespace-pre-line break-words text-xs text-muted-foreground">
+                {devicesState.error}
+              </p>

Posted via Macroscope — UI Consistency

Comment on lines +1467 to +1476
<span
className={cn(
"min-w-0",
connectionStatusText(environment.connection).includes("\n")
? "whitespace-pre-line break-words"
: "truncate",
)}
>
{connectionStatusText(environment.connection)}
</span>

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.

The T3 Connect row further down this file (status={operationError ?? primaryCloudLinkState.error}, around line 1687) can now receive a message built by relayProtectedErrorMessage (link/challenge relay calls failing with invalid_dpop), but SettingsRow wraps status in a plain <div className="pt-0.5 text-xs text-muted-foreground"> with no whitespace handling, so the \n\n hint collapses into the sentence instead of reading as a hint line.

Smallest fix is to pass the status as pre-line text at that call site:

-          status={operationError ?? primaryCloudLinkState.error}
+          status={
+            (operationError ?? primaryCloudLinkState.error) ? (
+              <span className="whitespace-pre-line break-words">
+                {operationError ?? primaryCloudLinkState.error}
+              </span>
+            ) : null
+          }

Given this newline-sniffing pattern is now repeated at four web call sites (plus the mobile rows), a small shared helper (e.g. a hintAwareTextClassName(text) in ~/lib/utils) would keep the treatment from drifting between surfaces.

Posted via Macroscope — UI Consistency

@macroscopeapp macroscopeapp 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.

One remaining web renderer of the new multi-line message still collapses the hint. Details inline.

Posted via Macroscope — UI Consistency

import { request, runStream } from "@t3tools/client-runtime/rpc";
import { makeEnvironmentHttpApiClient } from "@t3tools/client-runtime/rpc";
import { ManagedRelay } from "@t3tools/client-runtime/relay";
import { ManagedRelay, relayProtectedErrorMessage } from "@t3tools/client-runtime/relay";

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.

decodedRelayClientError (line 135) now builds "<action>: Relay rejected the DPoP proof.\n\nHint: …" from relayProtectedErrorMessage, and that string reaches the error toast in apps/web/src/cloud/useCloudLinkController.ts:61 (description: message). Toast.Description is rendered with min-w-0 select-text wrap-break-word … and no whitespace-pre-line, so the hint collapses into the sentence there — unlike the row renderers updated in this PR and unlike mobile.

Smallest fix that matches the pattern used for EnvironmentUnavailableRow is to pass the description as a pre-line node at the call site:

-      description: message,
+      description: message.includes("\n") ? (
+        <span className="whitespace-pre-line">{message}</span>
+      ) : (
+        message
+      ),

(or add whitespace-pre-line to descriptionClassName in components/ui/toast.tsx if every toast should preserve author-supplied line breaks).

Posted via Macroscope — UI Consistency

@macroscopeapp

macroscopeapp Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

UI Consistency: 2 findings

The renderers flagged in earlier runs (toast description, SettingsRow status, MobileClientsUserProfilePage) are now handled. Two remaining web consumers of the newly multi-line strings still render them with default whitespace handling, so the \n\n hint collapses into the preceding sentence instead of reading as its own line. Both files are outside this PR's diff, so the notes are here rather than inline.

1. apps/web/src/components/ChatView.tsx:2193 (composer "environment unavailable" banner)

unavailableConnection.error is ConnectionBlockedError.detail verbatim (presentConnectionState passes lastFailure.message through unchanged), which mapRemoteDpopEnvironmentError / mapManagedRelayError now build as "...\n\nHint: Check the date and time on both devices, then try again.". It reaches ComposerBannerStackAlertDescription, whose class list is flex flex-col gap-2.5 text-muted-foreground — no whitespace-pre-line, so HTML collapses the blank line.

Smallest fix is at the call site (mirrors the renderedDescription pattern added to ProviderSettingsPanel.tsx in this PR), keeping AlertDescription itself unchanged:

           title: `${activeEnvironmentUnavailableState.label}: ${connectionStatusTitle(unavailableConnection)}`,
-          description:
-            unavailableConnection.error ??
-            "Reconnect this environment before sending messages or running actions.",
+          description: unavailableConnection.error ? (
+            <span className="whitespace-pre-line break-words">{unavailableConnection.error}</span>
+          ) : (
+            "Reconnect this environment before sending messages or running actions."
+          ),

2. apps/web/src/components/cloud/ConnectOnboardingDialog.tsx:377 (PublishStep)

controller.operationError is set in useCloudLinkController.ts:57 from CloudEnvironmentLinkError.message, which decodedRelayClientError (apps/web/src/cloud/linkEnvironment.ts:135-137) now composes from the shared relayProtectedErrorMessage, i.e. "<action>: Relay rejected the DPoP proof.\n\nHint: ...". The same string already renders correctly in the toast and in SettingsRow; this onboarding paragraph is the last plain renderer of it.

-      {operationError ? <p className="text-xs text-destructive">{operationError}</p> : null}
+      {operationError ? (
+        <p className="whitespace-pre-line break-words text-xs text-destructive">{operationError}</p>
+      ) : null}

Posted via Macroscope — UI Consistency

@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Head commit changed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@extoci
extoci force-pushed the fix/connect-dpop-clock-diagnostics branch from 7fcedaf to d479fc6 Compare August 27, 2026 13:08
@extoci
extoci marked this pull request as ready for review August 27, 2026 13:25
@macroscopeapp

macroscopeapp Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR changes authentication-path DPoP verification and error contracts across shared code, relay, server, client runtime, web, and mobile, making it a broad cross-cutting runtime change rather than a contained presentation fix. Sensitive authentication directories and verifier behavior are involved, while the remaining UI comments are comparatively minor or already addressed in the current diff.

You can add or adjust custom eligibility rules. Learn more.

@extoci

extoci commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please run one final review against the current head commit.

Verification was performed by GPT-5.6-Luna on behalf of extoci via T3 Code.

@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

@extoci I will review the current head commit of #8351.

Action performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant