Skip to content

Use HttpApi for Environment APIs & standardize authn/authz#2858

Open
juliusmarminge wants to merge 19 commits into
mainfrom
codex/environment-httpapi-client-runtime
Open

Use HttpApi for Environment APIs & standardize authn/authz#2858
juliusmarminge wants to merge 19 commits into
mainfrom
codex/environment-httpapi-client-runtime

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented May 29, 2026

Summary

  • declare the environment metadata, auth, and orchestration HTTP surface as a shared EnvironmentHttpApi contract
  • migrate server handlers, web auth calls, client-runtime remote calls, and the project CLI to generated typed clients
  • keep success/error transforms domain-typed while preserving existing wire payloads; attach bootstrap cookies and CORS outside typed handler responses

Verification

  • bun fmt
  • bun lint (passes with existing warnings in mobile/web base)
  • bun lint:mobile
  • bun typecheck
  • cd apps/server && bun run test -- src/server.test.ts src/bin.test.ts
  • cd packages/client-runtime && bun run test -- src/remote.test.ts

Note

Replace role-based auth with OAuth-style scopes and HttpApi for environment endpoints

  • Replaces the role-based auth model (owner/client) with fine-grained OAuth scopes (e.g. orchestration:read, orchestration:operate, access:manage) across sessions, pairing links, JWT claims, and all RPC endpoints.
  • Migrates environment HTTP endpoints from HttpRouter to a typed EnvironmentHttpApi contract in packages/contracts/src/environmentHttp.ts, replacing /api/auth/bootstrap, /api/auth/ws-token, and related routes with /api/auth/browser-session, /oauth/token, and /api/auth/websocket-ticket.
  • Renames SessionCredentialService to SessionStore and splits SessionCredentialError into SessionCredentialInvalidError and SessionCredentialInternalError; all RPC methods now declare EnvironmentAuthorizationError as a possible failure.
  • Introduces database migration 031 (AuthAuthorizationScopes) that drops and recreates auth_pairing_links and auth_sessions with scopes columns, removing the role column.
  • Adds ServerSecretStore as a standalone DI service for filesystem-backed secrets with secure permissions and atomic writes.
  • Renames issueSshWebSocketTokenissueSshWebSocketTicket across the desktop bridge, IPC handlers, and WebSocket URL construction (wsTokenwsTicket).
  • Risk: migration 031 drops all existing pairing links and sessions; upgrading users will be signed out and must re-authenticate.

Macroscope summarized 67cd500.


Note

High Risk
Large auth refactor (pairing, sessions, token exchange, WebSocket tickets) with client and persistence contract changes; active sessions/pairing data may be invalidated depending on migrations bundled with the full PR.

Overview
This PR consolidates server authentication into EnvironmentAuth, PairingGrantStore, and SessionStore, and drops the older ServerAuth / AuthControlPlane / BootstrapCredentialService / SessionCredentialService layer split (including empty Services/* stubs).

Authorization model: pairing grants and sessions are keyed by OAuth-style scope lists instead of owner/client roles. Bootstrap exchange for remote clients returns AuthAccessTokenResult (access_token, scope, etc.); session methods use bearer-access-token. WebSocket upgrades use a wsTicket query param and AuthWebSocketTicketResult instead of a wsToken / token result.

Clients: Desktop SSH IPC calls @t3tools/client-runtime remote helpers after resolveLoopbackSshHttpBaseUrl, removes DesktopSshRemoteApi, and renames IPC to issueSshWebSocketTicket. Mobile pairing sends mobileAuthClientMetadata() on bootstrap and persists bootstrap.access_token.

Tests move to the new modules (e.g. sshEnvironment.test.ts, EnvironmentAuth*.test.ts) with updated expectations for scopes and error tags.

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bc9d4e22-4fcf-4a1c-87f9-06795e496473

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/environment-httpapi-client-runtime

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

@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels May 29, 2026
Comment thread apps/web/src/environments/primary/auth.ts
@juliusmarminge juliusmarminge force-pushed the codex/environment-httpapi-client-runtime branch from 28f8946 to a9767d3 Compare May 29, 2026 07:16
Comment thread apps/server/src/auth/http.ts Outdated
@juliusmarminge juliusmarminge changed the title Use generated Environment HttpApi clients Use HttpApi for Environment APIs & improve authn/authz May 29, 2026
@juliusmarminge juliusmarminge changed the title Use HttpApi for Environment APIs & improve authn/authz Use HttpApi for Environment APIs & standardize authn/authz May 29, 2026
Base automatically changed from t3code/mobile-remote-connect to main May 30, 2026 00:00
juliusmarminge and others added 7 commits May 29, 2026 17:03
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge juliusmarminge force-pushed the codex/environment-httpapi-client-runtime branch from 597e56d to f9c9f4d Compare May 30, 2026 00:05
@juliusmarminge juliusmarminge marked this pull request as ready for review May 30, 2026 00:06
Comment thread apps/server/src/http.ts Outdated
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented May 30, 2026

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

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

Co-authored-by: codex <codex@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

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 and found 2 potential issues.

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

Autofix Details

Bugbot Autofix prepared a fix for 1 of the 2 issues found in the latest run.

  • ✅ Fixed: Local requireEnvironmentScope shadows exported function with different behavior
    • Renamed the local function from requireEnvironmentScope to authenticateAndRequireScope to clearly distinguish it from the exported function in auth/http.ts and reflect that it performs both authentication and scope checking.

Create PR

Or push these changes by commenting:

@cursor push c2980e6b47
Preview (c2980e6b47)
diff --git a/apps/server/src/http.ts b/apps/server/src/http.ts
--- a/apps/server/src/http.ts
+++ b/apps/server/src/http.ts
@@ -81,7 +81,7 @@
   return redirectUrl.toString();
 }
 
-const requireEnvironmentScope = (
+const authenticateAndRequireScope = (
   scope: typeof AuthOrchestrationReadScope | typeof AuthOrchestrationOperateScope,
 ) =>
   Effect.gen(function* () {
@@ -123,7 +123,7 @@
   "POST",
   OTLP_TRACES_PROXY_PATH,
   Effect.gen(function* () {
-    yield* requireEnvironmentScope(AuthOrchestrationOperateScope);
+    yield* authenticateAndRequireScope(AuthOrchestrationOperateScope);
     const request = yield* HttpServerRequest.HttpServerRequest;
     const config = yield* ServerConfig;
     const otlpTracesUrl = config.otlpTracesUrl;
@@ -177,7 +177,7 @@
   "GET",
   `${ATTACHMENTS_ROUTE_PREFIX}/*`,
   Effect.gen(function* () {
-    yield* requireEnvironmentScope(AuthOrchestrationReadScope);
+    yield* authenticateAndRequireScope(AuthOrchestrationReadScope);
     const request = yield* HttpServerRequest.HttpServerRequest;
     const url = HttpServerRequest.toURL(request);
     if (Option.isNone(url)) {
@@ -238,7 +238,7 @@
   "GET",
   "/api/project-favicon",
   Effect.gen(function* () {
-    yield* requireEnvironmentScope(AuthOrchestrationReadScope);
+    yield* authenticateAndRequireScope(AuthOrchestrationReadScope);
     const request = yield* HttpServerRequest.HttpServerRequest;
     const url = HttpServerRequest.toURL(request);
     if (Option.isNone(url)) {

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

Comment thread apps/server/src/http.ts Outdated
Comment thread apps/server/src/http.ts
juliusmarminge and others added 2 commits May 29, 2026 17:48
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
@cursor
Copy link
Copy Markdown
Contributor

cursor Bot commented May 30, 2026

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: CORS middleware silently drops headers on unhandled failures
    • Replaced the success-only yield*/setHeaders pattern with HttpEffect.appendPreResponseHandlerUnsafe which registers CORS headers as a pre-response handler that runs on all HTTP responses including error responses from unhandled failures, matching the built-in HttpMiddleware.cors() behavior.

Create PR

Or push these changes by commenting:

@cursor push 7c9551256f
Preview (7c9551256f)
diff --git a/apps/server/src/http.ts b/apps/server/src/http.ts
--- a/apps/server/src/http.ts
+++ b/apps/server/src/http.ts
@@ -15,6 +15,7 @@
   HttpBody,
   HttpClient,
   HttpClientResponse,
+  HttpEffect,
   HttpRouter,
   HttpServerResponse,
   HttpServerRequest,
@@ -59,8 +60,14 @@
           },
         });
       }
-      const response = yield* httpEffect;
-      return HttpServerResponse.setHeaders(response, browserApiCorsHeaders);
+      HttpEffect.appendPreResponseHandlerUnsafe(
+        request,
+        (
+          _req: HttpServerRequest.HttpServerRequest,
+          response: HttpServerResponse.HttpServerResponse,
+        ) => Effect.succeed(HttpServerResponse.setHeaders(response, browserApiCorsHeaders)),
+      );
+      return yield* httpEffect;
     }),
   { global: true },
 );

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

Co-authored-by: codex <codex@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

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 and found 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Pairing links with admin scope hidden from listings
    • Decoupled the subject assignment so issuePairingCredential always uses 'one-time-token' (keeping user-created pairings visible), while issueStartupPairingUrl directly assigns 'administrative-bootstrap' (keeping startup links hidden).

Create PR

Or push these changes by commenting:

@cursor push a9cff672ee
Preview (a9cff672ee)
diff --git a/apps/server/src/auth/Layers/ServerAuth.ts b/apps/server/src/auth/Layers/ServerAuth.ts
--- a/apps/server/src/auth/Layers/ServerAuth.ts
+++ b/apps/server/src/auth/Layers/ServerAuth.ts
@@ -1,5 +1,4 @@
 import {
-  AuthAccessManageScope,
   AuthAccessTokenType,
   AuthAdministrativeScopes,
   AuthStandardClientScopes,
@@ -229,9 +228,7 @@
     authControlPlane
       .createPairingLink({
         scopes: input?.scopes ?? AuthStandardClientScopes,
-        subject: input?.scopes?.includes(AuthAccessManageScope)
-          ? "administrative-bootstrap"
-          : "one-time-token",
+        subject: "one-time-token",
         ...(input?.label ? { label: input.label } : {}),
       })
       .pipe(
@@ -329,15 +326,27 @@
     );
 
   const issueStartupPairingUrl: ServerAuthShape["issueStartupPairingUrl"] = (baseUrl) =>
-    issuePairingCredential({ scopes: AuthAdministrativeScopes }).pipe(
-      Effect.map((issued) => {
-        const url = new URL(baseUrl);
-        url.pathname = "/pair";
-        url.searchParams.delete("token");
-        url.hash = new URLSearchParams([["token", issued.credential]]).toString();
-        return url.toString();
-      }),
-    );
+    authControlPlane
+      .createPairingLink({
+        scopes: AuthAdministrativeScopes,
+        subject: "administrative-bootstrap",
+      })
+      .pipe(
+        Effect.mapError(
+          (cause) =>
+            new ServerAuthInternalError({
+              message: "Failed to issue startup pairing credential.",
+              cause,
+            }),
+        ),
+        Effect.map((issued) => {
+          const url = new URL(baseUrl);
+          url.pathname = "/pair";
+          url.searchParams.delete("token");
+          url.hash = new URLSearchParams([["token", issued.credential]]).toString();
+          return url.toString();
+        }),
+      );
 
   const issueWebSocketTicket: ServerAuthShape["issueWebSocketTicket"] = (session) =>
     sessions.issueWebSocketToken(session.sessionId).pipe(

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

Comment thread apps/server/src/auth/Layers/ServerAuth.ts Outdated
Co-authored-by: codex <codex@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

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 and found 1 potential issue.

There are 5 total unresolved issues (including 4 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: CLI pairing list omits administrative-bootstrap exclusion filter
    • Added { excludeSubjects: ["administrative-bootstrap"] } to the CLI's listPairingLinks() call to match the HTTP API behavior in ServerAuth.

Create PR

Or push these changes by commenting:

@cursor push 33d826e224
Preview (33d826e224)
diff --git a/apps/server/src/cli/auth.ts b/apps/server/src/cli/auth.ts
--- a/apps/server/src/cli/auth.ts
+++ b/apps/server/src/cli/auth.ts
@@ -124,7 +124,9 @@
       flags,
       (authControlPlane) =>
         Effect.gen(function* () {
-          const pairingLinks = yield* authControlPlane.listPairingLinks();
+          const pairingLinks = yield* authControlPlane.listPairingLinks({
+            excludeSubjects: ["administrative-bootstrap"],
+          });
           yield* Console.log(formatPairingCredentialList(pairingLinks, { json: flags.json }));
         }),
       {

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

Comment thread apps/server/src/cli/auth.ts Outdated
Co-authored-by: codex <codex@users.noreply.github.com>
Comment thread apps/server/src/auth/http.ts
juliusmarminge and others added 4 commits May 29, 2026 22:16
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

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 and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Missing scopes field in AuthenticatedSession returned from auth
    • Added catchTag for ServerAuthInternalError in getSessionState to gracefully degrade to unauthenticated state on internal errors (e.g. database failures), restoring the never-fail contract and updated the type signature and caller accordingly.

Create PR

Or push these changes by commenting:

@cursor push 9614dc8fc3
Preview (9614dc8fc3)
diff --git a/apps/server/src/auth/EnvironmentAuth.ts b/apps/server/src/auth/EnvironmentAuth.ts
--- a/apps/server/src/auth/EnvironmentAuth.ts
+++ b/apps/server/src/auth/EnvironmentAuth.ts
@@ -91,7 +91,7 @@
   readonly getDescriptor: () => Effect.Effect<ServerAuthDescriptor>;
   readonly getSessionState: (
     request: HttpServerRequest.HttpServerRequest,
-  ) => Effect.Effect<AuthSessionState, ServerAuthInternalError>;
+  ) => Effect.Effect<AuthSessionState>;
   readonly createBrowserSession: (
     credential: string,
     requestMetadata: AuthClientMetadata,
@@ -296,12 +296,18 @@
             ...(session.expiresAt ? { expiresAt: DateTime.toUtc(session.expiresAt) } : {}),
           }) satisfies AuthSessionState,
       ),
-      Effect.catchTag("ServerAuthInvalidCredentialError", () =>
-        Effect.succeed({
-          authenticated: false,
-          auth: descriptor,
-        } satisfies AuthSessionState),
-      ),
+      Effect.catchTags({
+        ServerAuthInvalidCredentialError: () =>
+          Effect.succeed({
+            authenticated: false,
+            auth: descriptor,
+          } satisfies AuthSessionState),
+        ServerAuthInternalError: () =>
+          Effect.succeed({
+            authenticated: false,
+            auth: descriptor,
+          } satisfies AuthSessionState),
+      }),
     );
 
   const createBrowserSession: EnvironmentAuthShape["createBrowserSession"] = (

diff --git a/apps/server/src/auth/http.ts b/apps/server/src/auth/http.ts
--- a/apps/server/src/auth/http.ts
+++ b/apps/server/src/auth/http.ts
@@ -167,13 +167,7 @@
         Effect.fn("environment.auth.session")(function* (args) {
           yield* annotateEnvironmentRequest(args.endpoint.name);
           const request = yield* HttpServerRequest.HttpServerRequest;
-          return yield* serverAuth
-            .getSessionState(request)
-            .pipe(
-              Effect.catchTag("ServerAuthInternalError", (error) =>
-                failEnvironmentInternal("internal_error", error),
-              ),
-            );
+          return yield* serverAuth.getSessionState(request);
         }),
       )
       .handle(

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

Reviewed by Cursor Bugbot for commit 295145c. Configure here.

Comment thread apps/server/src/auth/EnvironmentAuth.ts
juliusmarminge and others added 2 commits May 31, 2026 19:50
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
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