Skip to content

Add Connect client approval mode#3582

Open
juliusmarminge wants to merge 7 commits into
mainfrom
t3code/f4ae4e0e-f971-4d48-b4f2-9cfc31236c17
Open

Add Connect client approval mode#3582
juliusmarminge wants to merge 7 commits into
mainfrom
t3code/f4ae4e0e-f971-4d48-b4f2-9cfc31236c17

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Jun 27, 2026

Copy link
Copy Markdown
Member

Summary

  • add account-wide vs per-client approval modes for T3 Connect
  • persist Connect client approval state and expose approve/reject/revoke APIs, WebSocket auth access events, web settings UI, and CLI commands
  • propagate client metadata/proof thumbprints through relay, mobile, and client runtime; return signed pending-approval responses instead of credentials until approved

Verification

  • vp check
  • vp run typecheck
  • vp run lint:mobile
  • vp test packages/client-runtime/src/state/auth.test.ts packages/client-runtime/src/connection/resolver.test.ts infra/relay/src/environments/EnvironmentConnector.test.ts apps/mobile/src/features/cloud/linkEnvironment.test.ts apps/web/src/cloud/linkEnvironment.test.ts

Notes

  • Full vp test was run; unrelated provider tests failed in the full concurrent run but passed when rerun in isolation.

Note

High Risk
Changes authentication and credential minting for cloud connect paths; AuthAccessSnapshot and relay connect responses gain required/new fields that can break older clients if not upgraded together.

Overview
Adds T3 Connect client approval: environments can use account-wide access or require per-client approval before relay clients receive credentials.

Server & persistence: New ConnectClientStore, SQLite auth_connect_clients table, and HTTP APIs to read/set security mode and approve/reject/revoke clients by clientProofKeyThumbprint. Mint-credential in client-approval mode returns a signed pending_approval JWT response instead of a pairing credential until the client is approved. Auth access WebSocket snapshots and live events now include connectSecurityMode and connect clients.

Relay & clients: Connect requests carry optional client presentation metadata; connect/mint responses are a union of authorized credentials vs pending approval. Mobile, web, and client-runtime block or surface clear errors when approval is pending or rejected.

Product surfaces: Web Connections settings (toggle + client rows), t3 connect security CLI subcommands, and cloud link state exposing connectSecurityMode. Renames connect payload field to clientProofKeyThumbprint (from deprecated clientKeyThumbprint).

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

Note

Add Connect client approval mode with approval/rejection/revocation workflows

  • Introduces a client-approval security mode for T3 Connect, allowing server administrators to approve, reject, or revoke individual connecting clients before credentials are issued.
  • Adds a ConnectClientStore service and AuthConnectClients persistence layer (migration 033) to track client state (pending/approved/rejected) keyed by proof key thumbprint.
  • The cloud mint-credential endpoint now returns a signed pending_approval response (instead of a credential) when mode is client-approval and the client is not yet approved; clients verify and surface this response as a ConnectionBlockedError.
  • Exposes five new HTTP endpoints under /api/auth/connect-* for managing mode and clients, and extends the WebSocket auth-access stream with connectSecurityModeUpdated, connectClientUpserted, and connectClientRemoved events.
  • Adds a t3 connect security CLI subcommand to read/write security mode and list/approve/reject/revoke clients.
  • The Connections settings UI gains a mode toggle switch and a ConnectClientListRow component with per-client approve/reject/revoke actions.
  • Risk: AuthAccessSnapshot now requires connectSecurityMode and connectClients; existing decoders will reject snapshots missing these fields.

Macroscope summarized 00ead3f.

Summary by CodeRabbit

  • New Features

    • Added a new Connect approval mode for environments, including the ability to view, approve, reject, and revoke client access.
    • Connect status now shows the current approval mode and registered clients.
    • Relay connection flows can now include client details and show a pending-approval state when access needs review.
  • Bug Fixes

    • Improved handling of Connect connection results so approved and pending requests are reported correctly.
    • Updated cloud and web syncing to keep Connect security settings and client lists in sync.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a "client-approval" security mode to T3 Connect. When enabled, relay credential minting returns a pending_approval response instead of a credential until an admin approves the connecting client. Introduces a new auth_connect_clients DB table, ConnectClientStore/EnvironmentAuth server services, HTTP management endpoints, WebSocket streaming of approval events, relay connector pending-approval branching, client-runtime blocking, and web/CLI management UI.

Changes

Connect Client Approval Mode

Layer / File(s) Summary
Auth, relay, and HTTP API contracts
packages/contracts/src/auth.ts, packages/contracts/src/relay.ts, packages/contracts/src/environmentHttp.ts
Adds AuthConnectSecurityMode, AuthConnectClientStatus, AuthConnectClient, connect stream events, and extends AuthAccessSnapshot. Splits relay connect/mint responses into authorized vs pending-approval union schemas. Adds five new EnvironmentAuthHttpApi routes and result types.
DB migration and AuthConnectClientRepository
apps/server/src/persistence/Migrations/033_AuthConnectClientApprovals.ts, apps/server/src/persistence/AuthConnectClients.ts, apps/server/src/persistence/Errors.ts, apps/server/src/persistence/Migrations.ts
Creates auth_connect_clients table with two indexes via migration 033. Implements AuthConnectClientRepository with upsert, updateStatus, revoke, markSeen, and listActive SQL operations. Adds AuthConnectClientRepositoryError alias and clientProofKeyThumbprint correlation variant.
ConnectClientStore and EnvironmentAuth extension
apps/server/src/auth/ConnectClientStore.ts, apps/server/src/auth/EnvironmentAuth.ts
New ConnectClientStore Effect service manages security mode (secrets-backed) and client lifecycle with PubSub change stream. EnvironmentAuth adds seven new Connect methods, corresponding error types, and merges ConnectClientStore.layer.
Server HTTP routes, mint-credential gating, and WebSocket streaming
apps/server/src/auth/http.ts, apps/server/src/cloud/http.ts, apps/server/src/ws.ts
Auth HTTP layer gains five connect management routes. cloudMintCredentialHandler calls authorizeConnectClientRequest and returns a signed pending_approval JWT response when unapproved. readCloudLinkState includes connectSecurityMode. WebSocket auth-access stream merges streamConnectClientChanges.
Relay connector: client metadata and pending-approval
infra/relay/src/environments/EnvironmentConnector.ts, infra/relay/src/http/Api.ts, infra/relay/src/environments/EnvironmentConnector.test.ts
EnvironmentConnector.connect accepts optional client metadata, conditionally includes it in the mint payload, verifies pending-approval proofs, and returns a pending_approval response when no credential is present. Relay HTTP API forwards client from payload.
Client runtime: metadata forwarding and pending-approval blocking
packages/client-runtime/src/relay/managedRelay.ts, packages/client-runtime/src/connection/resolver.ts, packages/client-runtime/src/state/auth.ts, packages/client-runtime/src/connection/resolver.test.ts, packages/client-runtime/src/state/auth.test.ts
managedRelay.connectEnvironment gains client field and renames clientKeyThumbprintclientProofKeyThumbprint. Resolver reads ClientPresentation and passes metadata; detects status in result and fails with ConnectionBlockedError. Auth snapshot initializes connectSecurityMode/connectClients and handles three new stream event types.
Mobile linkEnvironment
apps/mobile/src/features/cloud/linkEnvironment.ts, apps/mobile/src/features/cloud/linkEnvironment.test.ts
Passes authClientMetadata() as client, guards against missing credential with approval-status-specific error, and uses authorizedConnect for all subsequent DPoP/token/URL steps.
Web primary auth client and ConnectionsSettings UI
apps/web/src/environments/primary/auth.ts, apps/web/src/environments/primary/index.ts, apps/web/src/components/settings/ConnectionsSettings.tsx, apps/web/src/cloud/linkEnvironment.test.ts, apps/web/test/environmentHttpTest.ts
Adds ServerConnectClientRecord, toServerConnectClientRecord, and four async functions for connect security mode and client approve/reject/revoke. ConnectionsSettings gains ConnectClientListRow, security mode toggle, and handler wiring with pending-state tracking and toast reporting.
CLI connect security commands
apps/server/src/cli/connect.ts
Extends connect status output with connectSecurityMode. Adds connect security subcommand family: clients, approve, reject, revoke, and mode subcommands with human/JSON output.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client Runtime
  participant Relay as Relay HTTP API
  participant Connector as EnvironmentConnector
  participant Server as Cloud Mint Handler
  participant Store as ConnectClientStore

  Client->>Relay: connectEnvironment(client, clientProofKeyThumbprint)
  Relay->>Connector: connect(client, clientProofKeyThumbprint)
  Connector->>Server: POST mintCredential(clientProofKeyThumbprint, client)
  Server->>Store: authorizeConnectClientRequest(cloudUserId, thumbprint)
  alt mode=client-approval AND status≠approved
    Store-->>Server: {mode: "client-approval", status: "pending"}
    Server-->>Connector: pending_approval proof JWT
    Connector-->>Relay: {status: "pending_approval", approvalStatus, requestedAt}
    Relay-->>Client: pending_approval response
    Client-->>Client: ConnectionBlockedError(reason: "permission")
  else approved or account mode
    Store-->>Server: {mode: "account" or status: "approved"}
    Server-->>Connector: authorized credential + expiresAt
    Connector-->>Relay: {credential, expiresAt}
    Relay-->>Client: authorized connect response
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 Hop, hop, a gate appears in the warren!
No credential for you unless admin-sworn,
A pending_approval stamp on your door,
clientProofKeyThumbprint forevermore.
Approve the bunny, reject the fox,
T3 Connect now has approval locks! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding Connect client approval mode.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description clearly summarizes the feature, verification, and risks, though it doesn’t follow the template headings verbatim.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/f4ae4e0e-f971-4d48-b4f2-9cfc31236c17

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:XXL 1,000+ changed lines (additions + deletions). labels Jun 27, 2026
@juliusmarminge

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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.

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Revoke race still mints credentials
    • Changed the fallback when markSeen returns Option.none() from returning status 'approved' to 'rejected', so a client revoked/rejected between upsert and markSeen will not receive credentials.
  • ✅ Fixed: Stale last seen after re-register
    • Added last_seen_at to the upsert ON CONFLICT UPDATE SET clause with a CASE that resets it to NULL when revoked_at is not null (revoked client re-registering), matching the pattern used for approved_at and rejected_at.

Create PR

Or push these changes by commenting:

@cursor push 87ff841023
Preview (87ff841023)
diff --git a/apps/server/src/auth/ConnectClientStore.ts b/apps/server/src/auth/ConnectClientStore.ts
--- a/apps/server/src/auth/ConnectClientStore.ts
+++ b/apps/server/src/auth/ConnectClientStore.ts
@@ -333,7 +333,7 @@
       return { mode, status: "approved" as const, client: seenClient };
     }
 
-    return { mode, status: "approved" as const, client: visibleClient };
+    return { mode, status: "rejected" as const, client: visibleClient };
   });
 
   const updateDecision = <E extends ConnectClientApprovalError | ConnectClientRejectionError>(

diff --git a/apps/server/src/persistence/AuthConnectClients.ts b/apps/server/src/persistence/AuthConnectClients.ts
--- a/apps/server/src/persistence/AuthConnectClients.ts
+++ b/apps/server/src/persistence/AuthConnectClients.ts
@@ -256,6 +256,10 @@
             WHEN auth_connect_clients.revoked_at IS NULL THEN auth_connect_clients.rejected_at
             ELSE NULL
           END,
+          last_seen_at = CASE
+            WHEN auth_connect_clients.revoked_at IS NULL THEN auth_connect_clients.last_seen_at
+            ELSE NULL
+          END,
           revoked_at = NULL
         RETURNING ${sql.unsafe(rowSelection)}
       `,

You can send follow-ups to the cloud agent here.

Comment thread apps/server/src/auth/ConnectClientStore.ts Outdated
Comment thread apps/server/src/persistence/AuthConnectClients.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (2)
apps/server/src/persistence/Migrations/033_AuthConnectClientApprovals.ts (1)

12-16: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add DB constraints for enum-like columns.

The repository decoders only accept known status and client_device_type literals, but the table allows arbitrary text. Add CHECK constraints now while this migration is new.

Proposed migration hardening
-      status TEXT NOT NULL,
+      status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'rejected')),
       client_label TEXT,
       client_ip_address TEXT,
       client_user_agent TEXT,
-      client_device_type TEXT NOT NULL DEFAULT 'unknown',
+      client_device_type TEXT NOT NULL DEFAULT 'unknown'
+        CHECK (client_device_type IN ('desktop', 'mobile', 'tablet', 'bot', 'unknown')),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/persistence/Migrations/033_AuthConnectClientApprovals.ts`
around lines 12 - 16, Add database CHECK constraints for the enum-like columns
in the 033_AuthConnectClientApprovals migration. Update the table definition so
`status` only accepts the same known literals handled by the repository
decoders, and `client_device_type` only accepts the supported device-type
literals (including the existing default). Make the constraints part of the
migration near the `CREATE TABLE` definition so `AuthConnectClientApprovals`
cannot store arbitrary text in these fields.
apps/mobile/src/features/cloud/linkEnvironment.test.ts (1)

838-838: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the new approval-blocked branch in this suite.

The production flow now short-circuits on credential-less relay responses, but this test update only verifies the renamed thumbprint field. Adding pending/rejected approval cases here would lock down the new user-visible errors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/src/features/cloud/linkEnvironment.test.ts` at line 838, The
linkEnvironment test suite only covers the renamed clientProofKeyThumbprint
field and misses the new approval-blocked path in the cloud relay flow. Update
the relevant tests around linkEnvironment to add cases for pending and rejected
approval responses, and assert the user-visible errors thrown when the
credential-less relay short-circuits. Use the existing linkEnvironment and
relay-response fixtures in this suite so the new branch stays locked down.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/server/src/auth/ConnectClientStore.ts`:
- Around line 225-227: The decodeSecurityMode helper currently defaults unknown
persisted values to "account", which can silently downgrade security mode;
update decodeSecurityMode to accept only the two valid literals and throw a
ConnectSecurityModeLoadError for anything else. Then in getSecurityMode, catch
that decode failure and surface ConnectSecurityModeLoadError so corrupted or
unexpected stored values fail closed instead of falling back.

In `@apps/server/src/persistence/AuthConnectClients.ts`:
- Around line 271-274: The status update logic in AuthConnectClients should
clear the opposite decision timestamp when a client changes state, so the audit
fields stay consistent. Update the update statement around the approved_at and
rejected_at CASE expressions so that setting status to approved also resets
rejected_at, and setting status to rejected also resets approved_at, using the
existing status/decidedAt flow in the same persistence method.
- Around line 236-260: The upsert in AuthConnectClients should also reset stale
activity metadata when a revoked client is re-requested. In the ON CONFLICT ...
DO UPDATE branch for client_proof_key_thumbprint, update the logic in the same
statement that handles revoked_at/status/approved_at/rejected_at so last_seen_at
is cleared (set to NULL) when auth_connect_clients.revoked_at is not NULL, while
preserving it only for non-revoked rows.

In `@apps/web/src/components/settings/ConnectionsSettings.tsx`:
- Around line 3073-3090: The T3 Connect approval UI is still hidden behind the
local-network reachability gate, so relay-only/local-only backends never get
access to pending approvals. Update the rendering path in ConnectionsSettings,
especially the renderAuthorizedClients usage and the
isLocalBackendRemotelyReachable branch, so client approval rows are shown
whenever client-approval mode is enabled rather than only when the backend is
locally reachable. Keep the existing handlers like handleConnectClientAction and
the approve/reject/revoke props wired up, but remove the reachability dependency
that suppresses the approval list.
- Around line 2319-2322: The desktopConnectSecurityMode fallback in
ConnectionsSettings should not default to "account" before a snapshot is loaded.
Make the value nullable (or otherwise unknown) until authAccessChanges.data
provides a snapshot, and keep the approval toggle disabled while the mode is
unresolved so an initial load error does not misrepresent an existing
client-approval policy. Update the useMemo logic and any toggle/disabled checks
that consume desktopConnectSecurityMode to handle the null/unknown state
explicitly.
- Around line 2550-2575: The connect-client mutation handler currently only
tracks the exact action key, so other actions can still run while one request is
in flight. Update the action flow in ConnectionsSettings.tsx around the async
approve/reject/revoke handler to use a shared “any pending connect-client
mutation” guard, and prevent starting a new action when one is already running.
Keep the existing approveServerConnectClient, rejectServerConnectClient, and
revokeServerConnectClient logic, but ensure the pending state is set and cleared
in a way that blocks all connect-client buttons until the active mutation
settles.

In `@infra/relay/src/environments/EnvironmentConnector.ts`:
- Around line 258-264: The pending-approval verification path in
EnvironmentConnector is not authenticating requestedAt, so a stale or tampered
timestamp can still pass and be forwarded. Update the pending-approval proof
check in EnvironmentConnector to include requestedAt in the signed/verified
fields alongside approvalStatus and clientProofKeyThumbprint, and adjust the
pending-proof helper/contract in EnvironmentConnector.test.ts so it always
builds and compares requestedAt for pending-approval responses.

In `@packages/contracts/src/relay.ts`:
- Around line 807-839: The pending-approval proof payload is missing the
requestedAt field, so the timestamp on
RelayEnvironmentMintPendingApprovalResponse is not covered by the signed proof.
Update RelayEnvironmentMintPendingApprovalResponseProofPayload in relay.ts to
include requestedAt alongside the existing pending-approval claims, and then
adjust the signer and verifier paths that handle
RelayEnvironmentMintPendingApprovalResponse /
RelayEnvironmentMintPendingApprovalResponseProofPayload to require
proof.requestedAt to match response.requestedAt.

---

Nitpick comments:
In `@apps/mobile/src/features/cloud/linkEnvironment.test.ts`:
- Line 838: The linkEnvironment test suite only covers the renamed
clientProofKeyThumbprint field and misses the new approval-blocked path in the
cloud relay flow. Update the relevant tests around linkEnvironment to add cases
for pending and rejected approval responses, and assert the user-visible errors
thrown when the credential-less relay short-circuits. Use the existing
linkEnvironment and relay-response fixtures in this suite so the new branch
stays locked down.

In `@apps/server/src/persistence/Migrations/033_AuthConnectClientApprovals.ts`:
- Around line 12-16: Add database CHECK constraints for the enum-like columns in
the 033_AuthConnectClientApprovals migration. Update the table definition so
`status` only accepts the same known literals handled by the repository
decoders, and `client_device_type` only accepts the supported device-type
literals (including the existing default). Make the constraints part of the
migration near the `CREATE TABLE` definition so `AuthConnectClientApprovals`
cannot store arbitrary text in these fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ab5f94d1-7fa2-4c92-8c74-0ce01629ba60

📥 Commits

Reviewing files that changed from the base of the PR and between 6245c54 and 4f4335b.

📒 Files selected for processing (28)
  • apps/mobile/src/features/cloud/linkEnvironment.test.ts
  • apps/mobile/src/features/cloud/linkEnvironment.ts
  • apps/server/src/auth/ConnectClientStore.ts
  • apps/server/src/auth/EnvironmentAuth.ts
  • apps/server/src/auth/http.ts
  • apps/server/src/cli/connect.ts
  • apps/server/src/cloud/http.ts
  • apps/server/src/persistence/AuthConnectClients.ts
  • apps/server/src/persistence/Errors.ts
  • apps/server/src/persistence/Migrations.ts
  • apps/server/src/persistence/Migrations/033_AuthConnectClientApprovals.ts
  • apps/server/src/ws.ts
  • apps/web/src/cloud/linkEnvironment.test.ts
  • apps/web/src/components/settings/ConnectionsSettings.tsx
  • apps/web/src/environments/primary/auth.ts
  • apps/web/src/environments/primary/index.ts
  • apps/web/test/environmentHttpTest.ts
  • infra/relay/src/environments/EnvironmentConnector.test.ts
  • infra/relay/src/environments/EnvironmentConnector.ts
  • infra/relay/src/http/Api.ts
  • packages/client-runtime/src/connection/resolver.test.ts
  • packages/client-runtime/src/connection/resolver.ts
  • packages/client-runtime/src/relay/managedRelay.ts
  • packages/client-runtime/src/state/auth.test.ts
  • packages/client-runtime/src/state/auth.ts
  • packages/contracts/src/auth.ts
  • packages/contracts/src/environmentHttp.ts
  • packages/contracts/src/relay.ts

Comment on lines +225 to +227
function decodeSecurityMode(bytes: Uint8Array): AuthConnectSecurityMode {
const value = textDecoder.decode(bytes).trim();
return value === "client-approval" ? "client-approval" : "account";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed on invalid persisted security mode.

Line 227 silently maps any unknown stored value to "account", which disables client approval if the secret is corrupted or unexpectedly encoded. Validate both allowed literals and surface ConnectSecurityModeLoadError for anything else instead of downgrading.

Suggested direction
 function decodeSecurityMode(bytes: Uint8Array): AuthConnectSecurityMode {
   const value = textDecoder.decode(bytes).trim();
-  return value === "client-approval" ? "client-approval" : "account";
+  if (value === "account" || value === "client-approval") {
+    return value;
+  }
+  throw new Error(`Invalid Connect security mode: ${value}`);
 }

Then convert that decode failure into ConnectSecurityModeLoadError in getSecurityMode.

Also applies to: 257-264

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/auth/ConnectClientStore.ts` around lines 225 - 227, The
decodeSecurityMode helper currently defaults unknown persisted values to
"account", which can silently downgrade security mode; update decodeSecurityMode
to accept only the two valid literals and throw a ConnectSecurityModeLoadError
for anything else. Then in getSecurityMode, catch that decode failure and
surface ConnectSecurityModeLoadError so corrupted or unexpected stored values
fail closed instead of falling back.

Comment thread apps/server/src/persistence/AuthConnectClients.ts
Comment on lines +271 to +274
status = ${status},
updated_at = ${decidedAt},
approved_at = CASE WHEN ${status} = 'approved' THEN ${decidedAt} ELSE approved_at END,
rejected_at = CASE WHEN ${status} = 'rejected' THEN ${decidedAt} ELSE rejected_at END,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear the opposite decision timestamp on status changes.

Approving a previously rejected client leaves rejected_at set, and rejecting a previously approved client leaves approved_at set. That creates contradictory audit state.

Proposed fix
-          approved_at = CASE WHEN ${status} = 'approved' THEN ${decidedAt} ELSE approved_at END,
-          rejected_at = CASE WHEN ${status} = 'rejected' THEN ${decidedAt} ELSE rejected_at END,
+          approved_at = CASE WHEN ${status} = 'approved' THEN ${decidedAt} ELSE NULL END,
+          rejected_at = CASE WHEN ${status} = 'rejected' THEN ${decidedAt} ELSE NULL END,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
status = ${status},
updated_at = ${decidedAt},
approved_at = CASE WHEN ${status} = 'approved' THEN ${decidedAt} ELSE approved_at END,
rejected_at = CASE WHEN ${status} = 'rejected' THEN ${decidedAt} ELSE rejected_at END,
status = ${status},
updated_at = ${decidedAt},
approved_at = CASE WHEN ${status} = 'approved' THEN ${decidedAt} ELSE NULL END,
rejected_at = CASE WHEN ${status} = 'rejected' THEN ${decidedAt} ELSE NULL END,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/persistence/AuthConnectClients.ts` around lines 271 - 274,
The status update logic in AuthConnectClients should clear the opposite decision
timestamp when a client changes state, so the audit fields stay consistent.
Update the update statement around the approved_at and rejected_at CASE
expressions so that setting status to approved also resets rejected_at, and
setting status to rejected also resets approved_at, using the existing
status/decidedAt flow in the same persistence method.

Comment on lines +2319 to +2322
const desktopConnectSecurityMode: AuthConnectSecurityMode = useMemo(() => {
const event = authAccessChanges.data;
return event?.type === "snapshot" ? event.payload.connectSecurityMode : "account";
}, [authAccessChanges.data]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Don't render an unknown approval mode as account.

Line 2321 falls back to "account" before a snapshot exists. Because the switch is only disabled while loading/mutating, a first-load error shows the backend as account-wide and lets the user overwrite an existing client-approval policy without ever seeing it. Keep this value nullable until the snapshot arrives and leave the toggle disabled in that state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/settings/ConnectionsSettings.tsx` around lines 2319 -
2322, The desktopConnectSecurityMode fallback in ConnectionsSettings should not
default to "account" before a snapshot is loaded. Make the value nullable (or
otherwise unknown) until authAccessChanges.data provides a snapshot, and keep
the approval toggle disabled while the mode is unresolved so an initial load
error does not misrepresent an existing client-approval policy. Update the
useMemo logic and any toggle/disabled checks that consume
desktopConnectSecurityMode to handle the null/unknown state explicitly.

Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
Comment thread infra/relay/src/environments/EnvironmentConnector.ts
Comment thread packages/contracts/src/relay.ts
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
Comment thread apps/server/src/persistence/AuthConnectClients.ts
Comment thread apps/server/src/auth/ConnectClientStore.ts Outdated
Comment thread apps/server/src/persistence/AuthConnectClients.ts
Comment thread apps/server/src/auth/ConnectClientStore.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a significant new authentication feature (Connect client approval mode) with database schema changes, new API endpoints, and security-sensitive authorization logic. Changes to auth/security packages and new features affecting how clients are authorized require human review.

You can customize Macroscope's approvability policy. Learn more.

Comment thread apps/server/src/auth/ConnectClientStore.ts Outdated

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

Effect service conventions: one finding in apps/server/src/auth/ConnectClientStore.ts — a pure validation failure fabricates an Error purely to satisfy a required cause.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/auth/ConnectClientStore.ts

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

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

There are 4 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Approval toggle stuck on load failure
    • Reverted decodeSecurityMode to fall back to 'account' for invalid persisted values instead of failing with ConnectSecurityModeLoadError, so the subscription and UI toggle remain functional even with corrupt secret data.
  • ✅ Resolved by another fix: Mint replay lost on mode load
    • The decodeSecurityMode fix for Bug 1 eliminates the ConnectSecurityModeLoadError failure path that caused authorizeConnectClientRequest to fail after replay guards were already consumed.
  • ✅ Fixed: Concurrent connect client actions
    • Added a synchronous useRef guard (pendingConnectClientActionRef) that is checked and set before the asynchronous React state update, preventing concurrent clicks from both passing the guard before re-render.

Create PR

Or push these changes by commenting:

@cursor push 0362fde9e2
Preview (0362fde9e2)
diff --git a/apps/server/src/auth/ConnectClientStore.ts b/apps/server/src/auth/ConnectClientStore.ts
--- a/apps/server/src/auth/ConnectClientStore.ts
+++ b/apps/server/src/auth/ConnectClientStore.ts
@@ -222,18 +222,9 @@
   };
 }
 
-function decodeSecurityMode(
-  bytes: Uint8Array,
-): Effect.Effect<AuthConnectSecurityMode, ConnectSecurityModeLoadError> {
+function decodeSecurityMode(bytes: Uint8Array): AuthConnectSecurityMode {
   const value = textDecoder.decode(bytes).trim();
-  if (value === "account" || value === "client-approval") {
-    return Effect.succeed(value);
-  }
-  return Effect.fail(
-    new ConnectSecurityModeLoadError({
-      cause: new Error(`Invalid Connect security mode: ${value}`),
-    }),
-  );
+  return value === "account" || value === "client-approval" ? value : "account";
 }
 
 function encodeSecurityMode(mode: AuthConnectSecurityMode): Uint8Array {
@@ -265,10 +256,10 @@
 
   const getSecurityMode: ConnectClientStore["Service"]["getSecurityMode"] = () =>
     secrets.get(CONNECT_SECURITY_MODE_SECRET).pipe(
+      Effect.map((mode) =>
+        Option.isSome(mode) ? decodeSecurityMode(mode.value) : ("account" as const),
+      ),
       Effect.mapError((cause) => new ConnectSecurityModeLoadError({ cause })),
-      Effect.flatMap((mode) =>
-        Option.isSome(mode) ? decodeSecurityMode(mode.value) : Effect.succeed("account" as const),
-      ),
       Effect.withSpan("ConnectClientStore.getSecurityMode"),
     );
 

diff --git a/apps/web/src/components/settings/ConnectionsSettings.tsx b/apps/web/src/components/settings/ConnectionsSettings.tsx
--- a/apps/web/src/components/settings/ConnectionsSettings.tsx
+++ b/apps/web/src/components/settings/ConnectionsSettings.tsx
@@ -8,7 +8,7 @@
   TriangleAlertIcon,
 } from "lucide-react";
 import { useAuth } from "@clerk/react";
-import { type ReactNode, memo, useCallback, useEffect, useMemo, useState } from "react";
+import { type ReactNode, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
 import {
   AuthAccessReadScope,
   AuthAccessWriteScope,
@@ -2224,6 +2224,7 @@
   const [pendingConnectClientActionKey, setPendingConnectClientActionKey] = useState<string | null>(
     null,
   );
+  const pendingConnectClientActionRef = useRef(false);
   const [addBackendDialogOpen, setAddBackendDialogOpen] = useState(false);
   const [savedBackendMode, setSavedBackendMode] = useState<"remote" | "ssh">("remote");
   const [savedBackendHost, setSavedBackendHost] = useState("");
@@ -2549,7 +2550,8 @@
 
   const handleConnectClientAction = useCallback(
     async (action: "approve" | "reject" | "revoke", clientProofKeyThumbprint: string) => {
-      if (pendingConnectClientActionKey !== null) return;
+      if (pendingConnectClientActionRef.current) return;
+      pendingConnectClientActionRef.current = true;
       const actionKey = `${action}:${clientProofKeyThumbprint}`;
       setPendingConnectClientActionKey(actionKey);
       setDesktopAccessManagementMutationError(null);
@@ -2573,10 +2575,11 @@
           }),
         );
       } finally {
+        pendingConnectClientActionRef.current = false;
         setPendingConnectClientActionKey(null);
       }
     },
-    [pendingConnectClientActionKey],
+    [],
   );
 
   const handleAddSavedBackend = useCallback(async () => {

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 136150d. Configure here.

Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx Outdated
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx Outdated
Comment thread apps/server/src/auth/ConnectClientStore.ts

@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 Effect service convention issue found. See inline comment.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/auth/ConnectClientStore.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ 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