Implement server auth bootstrap and pairing flow#1768
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 4 issues found in the latest run.
- ✅ Fixed: Pairing URL points to root, token lost during redirect
- Added
url.pathname = "/pair"inissueStartupPairingUrlso the generated URL navigates directly to/pair?token=..., avoiding the redirect that strips the query parameter.
- Added
- ✅ Fixed: Secret store
setswallows write errors silently- Changed
Effect.map(() => new SecretStoreError(...))toEffect.flatMap(() => Effect.fail(new SecretStoreError(...)))so write failures properly propagate as Effect errors instead of being swallowed as success values.
- Changed
- ✅ Fixed: Bootstrap credential consume has TOCTOU race condition
- Replaced the non-atomic
Ref.get+Ref.updatesequence with a singleRef.modifycall that atomically reads the grant, validates it, and updates the map in one operation.
- Replaced the non-atomic
- ✅ Fixed: Duplicate one-time tokens issued during startup
- In non-desktop mode,
resolveStartupBrowserTargetis now called once and the resulting URL is reused for both logging and browser opening, avoiding issuing two separate one-time tokens.
- In non-desktop mode,
Or push these changes by commenting:
@cursor push f99e419f7c
Preview (f99e419f7c)
diff --git a/apps/server/src/auth/Layers/BootstrapCredentialService.ts b/apps/server/src/auth/Layers/BootstrapCredentialService.ts
--- a/apps/server/src/auth/Layers/BootstrapCredentialService.ts
+++ b/apps/server/src/auth/Layers/BootstrapCredentialService.ts
@@ -49,47 +49,60 @@
return credential;
});
+ type ConsumeResult =
+ | { readonly _tag: "error"; readonly error: BootstrapCredentialError }
+ | { readonly _tag: "ok"; readonly grant: BootstrapGrant };
+
const consume: BootstrapCredentialServiceShape["consume"] = (credential) =>
Effect.gen(function* () {
- const current = yield* Ref.get(grantsRef);
- const grant = current.get(credential);
- if (!grant) {
- return yield* new BootstrapCredentialError({
- message: "Unknown bootstrap credential.",
- });
- }
+ const now = yield* DateTime.now;
+ const result = yield* Ref.modify(
+ grantsRef,
+ (current): readonly [ConsumeResult, Map<string, StoredBootstrapGrant>] => {
+ const grant = current.get(credential);
+ if (!grant) {
+ return [
+ {
+ _tag: "error",
+ error: new BootstrapCredentialError({ message: "Unknown bootstrap credential." }),
+ },
+ current,
+ ];
+ }
- if (DateTime.isGreaterThanOrEqualTo(yield* DateTime.now, grant.expiresAt)) {
- yield* Ref.update(grantsRef, (state) => {
- const next = new Map(state);
- next.delete(credential);
- return next;
- });
- return yield* new BootstrapCredentialError({
- message: "Bootstrap credential expired.",
- });
- }
-
- const remainingUses = grant.remainingUses;
- if (typeof remainingUses === "number") {
- yield* Ref.update(grantsRef, (state) => {
- const next = new Map(state);
- if (remainingUses <= 1) {
+ if (DateTime.isGreaterThanOrEqualTo(now, grant.expiresAt)) {
+ const next = new Map(current);
next.delete(credential);
- } else {
- next.set(credential, {
- ...grant,
- remainingUses: remainingUses - 1,
- });
+ return [
+ {
+ _tag: "error",
+ error: new BootstrapCredentialError({ message: "Bootstrap credential expired." }),
+ },
+ next,
+ ];
}
- return next;
- });
+
+ const next = new Map(current);
+ const remainingUses = grant.remainingUses;
+ if (typeof remainingUses === "number") {
+ if (remainingUses <= 1) {
+ next.delete(credential);
+ } else {
+ next.set(credential, { ...grant, remainingUses: remainingUses - 1 });
+ }
+ }
+
+ return [
+ { _tag: "ok", grant: { method: grant.method, expiresAt: grant.expiresAt } },
+ next,
+ ];
+ },
+ );
+
+ if (result._tag === "error") {
+ return yield* result.error;
}
-
- return {
- method: grant.method,
- expiresAt: grant.expiresAt,
- } satisfies BootstrapGrant;
+ return result.grant;
});
return {
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
@@ -124,6 +124,7 @@
bootstrapCredentials.issueOneTimeToken().pipe(
Effect.map((credential) => {
const url = new URL(baseUrl);
+ url.pathname = "/pair";
url.searchParams.set("token", credential);
return url.toString();
}),
diff --git a/apps/server/src/auth/Layers/ServerSecretStore.ts b/apps/server/src/auth/Layers/ServerSecretStore.ts
--- a/apps/server/src/auth/Layers/ServerSecretStore.ts
+++ b/apps/server/src/auth/Layers/ServerSecretStore.ts
@@ -47,8 +47,10 @@
Effect.catch((cause) =>
fileSystem.remove(tempPath).pipe(
Effect.orElseSucceed(() => undefined),
- Effect.map(
- () => new SecretStoreError({ message: `Failed to persist secret ${name}.`, cause }),
+ Effect.flatMap(() =>
+ Effect.fail(
+ new SecretStoreError({ message: `Failed to persist secret ${name}.`, cause }),
+ ),
),
),
),
diff --git a/apps/server/src/serverRuntimeStartup.ts b/apps/server/src/serverRuntimeStartup.ts
--- a/apps/server/src/serverRuntimeStartup.ts
+++ b/apps/server/src/serverRuntimeStartup.ts
@@ -388,8 +388,22 @@
yield* Effect.logInfo("Authentication required. Open T3 Code using the pairing URL.", {
pairingUrl,
});
+ if (!serverConfig.noBrowser) {
+ const { openBrowser } = yield* Open;
+ yield* runStartupPhase(
+ "browser.open",
+ openBrowser(pairingUrl).pipe(
+ Effect.catch(() =>
+ Effect.logInfo("browser auto-open unavailable", {
+ hint: `Open ${pairingUrl} in your browser.`,
+ }),
+ ),
+ ),
+ );
+ }
+ } else {
+ yield* runStartupPhase("browser.open", maybeOpenBrowser);
}
- yield* runStartupPhase("browser.open", maybeOpenBrowser);
yield* Effect.logDebug("startup phase: complete");
}),
);You can send follow-ups to the cloud agent here.
ApprovabilityVerdict: 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. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Auth bootstrap fetch uses unsupported ws:// protocol URL
- Added a wsUrlToHttpUrl() helper that converts ws:/wss: protocols to http:/https: and applied it at all three call sites where resolvePrimaryEnvironmentBootstrapUrl() is passed to fetch().
Or push these changes by commenting:
@cursor push 59ffed3254
Preview (59ffed3254)
diff --git a/apps/web/src/authBootstrap.test.ts b/apps/web/src/authBootstrap.test.ts
--- a/apps/web/src/authBootstrap.test.ts
+++ b/apps/web/src/authBootstrap.test.ts
@@ -86,10 +86,10 @@
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock.mock.calls[0]?.[0]).toEqual(
- new URL("/api/auth/session", "ws://localhost:3773/"),
+ new URL("/api/auth/session", "http://localhost:3773/"),
);
expect(fetchMock.mock.calls[1]?.[0]).toEqual(
- new URL("/api/auth/bootstrap", "ws://localhost:3773/"),
+ new URL("/api/auth/bootstrap", "http://localhost:3773/"),
);
});
diff --git a/apps/web/src/authBootstrap.ts b/apps/web/src/authBootstrap.ts
--- a/apps/web/src/authBootstrap.ts
+++ b/apps/web/src/authBootstrap.ts
@@ -2,6 +2,13 @@
import { resolvePrimaryEnvironmentBootstrapUrl } from "./environmentBootstrap";
+function wsUrlToHttpUrl(url: string): string {
+ const parsed = new URL(url);
+ if (parsed.protocol === "ws:") parsed.protocol = "http:";
+ else if (parsed.protocol === "wss:") parsed.protocol = "https:";
+ return parsed.href;
+}
+
export type ServerAuthGateState =
| { status: "authenticated" }
| {
@@ -80,7 +87,7 @@
}
async function bootstrapServerAuth(): Promise<ServerAuthGateState> {
- const baseUrl = resolvePrimaryEnvironmentBootstrapUrl();
+ const baseUrl = wsUrlToHttpUrl(resolvePrimaryEnvironmentBootstrapUrl());
const bootstrapCredential = getBootstrapCredential();
const currentSession = await fetchSessionState(baseUrl);
if (currentSession.authenticated) {
@@ -112,7 +119,10 @@
throw new Error("Enter a pairing token to continue.");
}
- await exchangeBootstrapCredential(resolvePrimaryEnvironmentBootstrapUrl(), trimmedCredential);
+ await exchangeBootstrapCredential(
+ wsUrlToHttpUrl(resolvePrimaryEnvironmentBootstrapUrl()),
+ trimmedCredential,
+ );
stripPairingTokenFromUrl();
}You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Cached auth state not invalidated after successful pairing
- Added
bootstrapPromise = nullafter the successful credential exchange insubmitServerAuthCredential, so subsequent calls toresolveInitialServerAuthGateStatere-evaluate the auth state instead of returning the stale cached requires-auth promise.
- Added
Or push these changes by commenting:
@cursor push 264a9be49a
Preview (264a9be49a)
diff --git a/apps/web/src/authBootstrap.ts b/apps/web/src/authBootstrap.ts
--- a/apps/web/src/authBootstrap.ts
+++ b/apps/web/src/authBootstrap.ts
@@ -123,6 +123,7 @@
await exchangeBootstrapCredential(resolvePrimaryEnvironmentHttpBaseUrl(), trimmedCredential);
stripPairingTokenFromUrl();
+ bootstrapPromise = null;
}
export function resolveInitialServerAuthGateState(): Promise<ServerAuthGateState> {You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
There are 5 total unresolved issues (including 2 from previous reviews).
Autofix Details
Bugbot Autofix prepared fixes for 2 of the 3 issues found in the latest run.
- ✅ Fixed: Token split allows extra segments to pass verification
- Added a check that
token.split(".")produces exactly 2 parts before destructuring, rejecting tokens with extra segments.
- Added a check that
- ✅ Fixed: Secret store getOrCreateRandom has TOCTOU race condition
- Wrapped the read-then-write sequence in
getOrCreateRandomwith aSemaphore(1)mutex to ensure atomicity.
- Wrapped the read-then-write sequence in
Or push these changes by commenting:
@cursor push f69311f5db
Preview (f69311f5db)
diff --git a/apps/server/src/auth/Layers/ServerSecretStore.ts b/apps/server/src/auth/Layers/ServerSecretStore.ts
--- a/apps/server/src/auth/Layers/ServerSecretStore.ts
+++ b/apps/server/src/auth/Layers/ServerSecretStore.ts
@@ -1,6 +1,6 @@
import * as Crypto from "node:crypto";
-import { Effect, FileSystem, Layer, Path } from "effect";
+import { Effect, FileSystem, Layer, Path, Semaphore } from "effect";
import * as PlatformError from "effect/PlatformError";
import { ServerConfig } from "../../config.ts";
@@ -60,6 +60,8 @@
);
};
+ const mutex = yield* Semaphore.make(1);
+
const getOrCreateRandom: ServerSecretStoreShape["getOrCreateRandom"] = (name, bytes) =>
get(name).pipe(
Effect.flatMap((existing) => {
@@ -70,6 +72,7 @@
const generated = Crypto.randomBytes(bytes);
return set(name, generated).pipe(Effect.as(Uint8Array.from(generated)));
}),
+ mutex.withPermits(1),
);
const remove: ServerSecretStoreShape["remove"] = (name) =>
diff --git a/apps/server/src/auth/Layers/SessionCredentialService.ts b/apps/server/src/auth/Layers/SessionCredentialService.ts
--- a/apps/server/src/auth/Layers/SessionCredentialService.ts
+++ b/apps/server/src/auth/Layers/SessionCredentialService.ts
@@ -56,7 +56,13 @@
});
const verify: SessionCredentialServiceShape["verify"] = Effect.fn("verify")(function* (token) {
- const [encodedPayload, signature] = token.split(".");
+ const parts = token.split(".");
+ if (parts.length !== 2) {
+ return yield* new SessionCredentialError({
+ message: "Malformed session token.",
+ });
+ }
+ const [encodedPayload, signature] = parts;
if (!encodedPayload || !signature) {
return yield* new SessionCredentialError({
message: "Malformed session token.",You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
🟢 Low
VITE_DEV_SERVER_URL is trimmed and validated by the parent on lines 8-17, but the child process receives the untrimmed value directly from process.env via childEnv. When the original environment variable contains whitespace, the parent accepts the URL after trimming while the child receives the raw value, causing URL parsing failures in the Electron app. Consider passing the validated devServerUrl to the child explicitly, or ensuring childEnv uses the trimmed value.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/desktop/scripts/dev-electron.mjs around line 65:
`VITE_DEV_SERVER_URL` is trimmed and validated by the parent on lines 8-17, but the child process receives the untrimmed value directly from `process.env` via `childEnv`. When the original environment variable contains whitespace, the parent accepts the URL after trimming while the child receives the raw value, causing URL parsing failures in the Electron app. Consider passing the validated `devServerUrl` to the child explicitly, or ensuring `childEnv` uses the trimmed value.
Evidence trail:
apps/desktop/scripts/dev-electron.mjs lines 8, 37, and 65-73 at REVIEWED_COMMIT:
- Line 8: `const devServerUrl = process.env.VITE_DEV_SERVER_URL?.trim();` (trimmed for validation)
- Line 37: `const childEnv = { ...process.env };` (spreads original untrimmed env)
- Lines 65-73: `spawn(..., { env: childEnv, ... })` (child receives untrimmed value)
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 7 total unresolved issues (including 5 from previous reviews).
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Session token exposed in response body alongside HttpOnly cookie
- Stripped sessionToken from the bootstrap JSON response body by destructuring it out before serialization, so the token is only transmitted via the httpOnly cookie.
- ✅ Fixed: Session cookie missing
secureflag for non-loopback environments- Added conditional
secure: descriptor.policy === "remote-reachable"to the cookie options so the flag is set when the server is configured for remote access.
- Added conditional
Or push these changes by commenting:
@cursor push 21a3606f32
Preview (21a3606f32)
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
@@ -40,12 +40,14 @@
);
const result = yield* serverAuth.exchangeBootstrapCredential(payload.credential);
- return yield* HttpServerResponse.jsonUnsafe(result, { status: 200 }).pipe(
+ const { sessionToken: _token, ...responseBody } = result;
+ return yield* HttpServerResponse.jsonUnsafe(responseBody, { status: 200 }).pipe(
HttpServerResponse.setCookie(descriptor.sessionCookieName, result.sessionToken, {
expires: DateTime.toDate(result.expiresAt),
httpOnly: true,
path: "/",
sameSite: "lax",
+ secure: descriptor.policy === "remote-reachable",
}),
);
}).pipe(Effect.catchTag("AuthError", (error) => Effect.succeed(toUnauthorizedResponse(error)))),
diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts
--- a/apps/server/src/server.test.ts
+++ b/apps/server/src/server.test.ts
@@ -492,6 +492,12 @@
return `http://127.0.0.1:${address.port}${pathname}`;
});
+function parseSessionTokenFromSetCookie(setCookie: string | null): string | null {
+ if (!setCookie) return null;
+ const match = /t3_session=([^;]+)/.exec(setCookie);
+ return match?.[1] ?? null;
+}
+
const bootstrapBrowserSession = (credential = defaultDesktopBootstrapToken) =>
Effect.gen(function* () {
const bootstrapUrl = yield* getHttpServerUrl("/api/auth/bootstrap");
@@ -509,13 +515,15 @@
const body = (yield* Effect.promise(() => response.json())) as {
readonly authenticated: boolean;
readonly sessionMethod: string;
- readonly sessionToken: string;
readonly expiresAt: string;
};
+ const cookie = response.headers.get("set-cookie");
+ const sessionToken = parseSessionTokenFromSetCookie(cookie);
return {
response,
body,
- cookie: response.headers.get("set-cookie"),
+ cookie,
+ sessionToken,
};
});
@@ -525,18 +533,18 @@
return cachedDefaultSessionToken;
}
- const { response, body } = yield* bootstrapBrowserSession(credential);
- if (!response.ok) {
+ const { response, sessionToken } = yield* bootstrapBrowserSession(credential);
+ if (!response.ok || !sessionToken) {
return yield* Effect.fail(
new Error(`Expected bootstrap session response to succeed, got ${response.status}`),
);
}
if (credential === defaultDesktopBootstrapToken) {
- cachedDefaultSessionToken = body.sessionToken;
+ cachedDefaultSessionToken = sessionToken;
}
- return body.sessionToken;
+ return sessionToken;
});
const getWsServerUrl = (
@@ -720,13 +728,15 @@
Effect.gen(function* () {
yield* buildAppUnderTest();
- const { response: bootstrapResponse, body: bootstrapBody } = yield* bootstrapBrowserSession();
+ const { response: bootstrapResponse, sessionToken: bootstrapSessionToken } =
+ yield* bootstrapBrowserSession();
assert.equal(bootstrapResponse.status, 200);
+ assert(bootstrapSessionToken, "Expected session token in Set-Cookie header");
const wsUrl = appendSessionTokenToUrl(
yield* getWsServerUrl("/ws", { authenticated: false }),
- bootstrapBody.sessionToken,
+ bootstrapSessionToken,
);
const response = yield* Effect.scoped(
withWsRpcClient(wsUrl, (client) => client[WS_METHODS.serverGetConfig]({})),
diff --git a/apps/web/src/authBootstrap.test.ts b/apps/web/src/authBootstrap.test.ts
--- a/apps/web/src/authBootstrap.test.ts
+++ b/apps/web/src/authBootstrap.test.ts
@@ -65,7 +65,6 @@
jsonResponse({
authenticated: true,
sessionMethod: "browser-session-cookie",
- sessionToken: "session-token",
expiresAt: "2026-04-05T00:00:00.000Z",
}),
);
@@ -207,7 +206,6 @@
jsonResponse({
authenticated: true,
sessionMethod: "browser-session-cookie",
- sessionToken: "session-token",
expiresAt: "2026-04-05T00:00:00.000Z",
}),
);
diff --git a/apps/web/src/authBootstrap.ts b/apps/web/src/authBootstrap.ts
--- a/apps/web/src/authBootstrap.ts
+++ b/apps/web/src/authBootstrap.ts
@@ -1,4 +1,4 @@
-import type { AuthBootstrapInput, AuthBootstrapResult, AuthSessionState } from "@t3tools/contracts";
+import type { AuthBootstrapInput, AuthSessionState } from "@t3tools/contracts";
import { resolveServerHttpUrl } from "./lib/utils";
export type ServerAuthGateState =
@@ -56,7 +56,7 @@
return (await response.json()) as AuthSessionState;
}
-async function exchangeBootstrapCredential(credential: string): Promise<AuthBootstrapResult> {
+async function exchangeBootstrapCredential(credential: string): Promise<void> {
const payload: AuthBootstrapInput = { credential };
const response = await fetch(resolveServerHttpUrl({ pathname: "/api/auth/bootstrap" }), {
body: JSON.stringify(payload),
@@ -71,8 +71,6 @@
const message = await response.text();
throw new Error(message || `Failed to bootstrap auth session (${response.status}).`);
}
-
- return (await response.json()) as AuthBootstrapResult;
}
async function bootstrapServerAuth(): Promise<ServerAuthGateState> {
diff --git a/apps/web/test/authHttpHandlers.ts b/apps/web/test/authHttpHandlers.ts
--- a/apps/web/test/authHttpHandlers.ts
+++ b/apps/web/test/authHttpHandlers.ts
@@ -2,7 +2,6 @@
import { HttpResponse, http } from "msw";
const TEST_SESSION_EXPIRES_AT = "2026-05-01T12:00:00.000Z";
-const TEST_SESSION_TOKEN = "browser-test-session-token";
export function createAuthenticatedSessionHandlers(getAuthDescriptor: () => ServerAuthDescriptor) {
return [
@@ -18,7 +17,6 @@
HttpResponse.json({
authenticated: true,
sessionMethod: "browser-session-cookie",
- sessionToken: TEST_SESSION_TOKEN,
expiresAt: TEST_SESSION_EXPIRES_AT,
}),
),You can send follow-ups to the cloud agent here.
| httpOnly: true, | ||
| path: "/", | ||
| sameSite: "lax", | ||
| }), |
There was a problem hiding this comment.
Session cookie missing secure flag for non-loopback environments
Medium Severity
The session cookie set by the bootstrap endpoint omits the secure flag entirely. While this is fine for local development over HTTP, the auth model explicitly supports remote-reachable environments where TLS is expected. Without secure, the cookie could be sent over plaintext HTTP on a remote/tunneled connection, exposing the session token to network eavesdropping. The flag could be set conditionally based on the auth policy or the request protocol.
Reviewed by Cursor Bugbot for commit dca54c7. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
There are 10 total unresolved issues (including 7 from previous reviews).
Autofix Details
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
- ✅ Fixed: Desktop bootstrap token expires before backend ready
- Increased DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES from 5 to 10 minutes to provide a safer margin for slow backend startups, window creation, and renderer bootstrap exchange.
- ✅ Fixed: Unused parameter in
buildReconnectTitleafter refactoring- Removed the dead
buildReconnectTitlefunction entirely and inlined the constant string "Disconnected from T3 Server" at the single call site.
- Removed the dead
- ✅ Fixed: Module-level shared mutable state across parallel test runs
- Replaced the bare cached token string with a generation-tagged object so that stale tokens from prior server builds are automatically invalidated when
buildAppUnderTestincrements the generation counter.
- Replaced the bare cached token string with a generation-tagged object so that stale tokens from prior server builds are automatically invalidated when
Or push these changes by commenting:
@cursor push d5344ec03b
Preview (d5344ec03b)
diff --git a/apps/server/src/auth/Layers/BootstrapCredentialService.ts b/apps/server/src/auth/Layers/BootstrapCredentialService.ts
--- a/apps/server/src/auth/Layers/BootstrapCredentialService.ts
+++ b/apps/server/src/auth/Layers/BootstrapCredentialService.ts
@@ -22,7 +22,7 @@
readonly grant: BootstrapGrant;
};
-const DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES = Duration.minutes(5);
+const DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES = Duration.minutes(10);
export const makeBootstrapCredentialService = Effect.gen(function* () {
const config = yield* ServerConfig;
diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts
--- a/apps/server/src/server.test.ts
+++ b/apps/server/src/server.test.ts
@@ -108,7 +108,8 @@
repositoryIdentity: true,
},
};
-let cachedDefaultSessionToken: string | null = null;
+let serverBuildGeneration = 0;
+let cachedDefaultSessionToken: { token: string; generation: number } | null = null;
const makeDefaultOrchestrationReadModel = () => {
const now = new Date().toISOString();
@@ -293,7 +294,7 @@
};
}) =>
Effect.gen(function* () {
- cachedDefaultSessionToken = null;
+ serverBuildGeneration += 1;
const fileSystem = yield* FileSystem.FileSystem;
const tempBaseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-router-test-" });
const baseDir = options?.config?.baseDir ?? tempBaseDir;
@@ -521,8 +522,13 @@
const getAuthenticatedSessionToken = (credential = defaultDesktopBootstrapToken) =>
Effect.gen(function* () {
- if (credential === defaultDesktopBootstrapToken && cachedDefaultSessionToken) {
- return cachedDefaultSessionToken;
+ const currentGeneration = serverBuildGeneration;
+ if (
+ credential === defaultDesktopBootstrapToken &&
+ cachedDefaultSessionToken &&
+ cachedDefaultSessionToken.generation === currentGeneration
+ ) {
+ return cachedDefaultSessionToken.token;
}
const { response, body } = yield* bootstrapBrowserSession(credential);
@@ -533,7 +539,7 @@
}
if (credential === defaultDesktopBootstrapToken) {
- cachedDefaultSessionToken = body.sessionToken;
+ cachedDefaultSessionToken = { token: body.sessionToken, generation: currentGeneration };
}
return body.sessionToken;
diff --git a/apps/web/src/components/WebSocketConnectionSurface.tsx b/apps/web/src/components/WebSocketConnectionSurface.tsx
--- a/apps/web/src/components/WebSocketConnectionSurface.tsx
+++ b/apps/web/src/components/WebSocketConnectionSurface.tsx
@@ -54,10 +54,6 @@
return "Retries exhausted trying to reconnect";
}
-function buildReconnectTitle(_status: WsConnectionStatus): string {
- return "Disconnected from T3 Server";
-}
-
function describeRecoveredToast(
previousDisconnectedAt: string | null,
connectedAt: string | null,
@@ -270,7 +266,7 @@
? `Reconnecting... ${formatReconnectAttemptLabel(status)}`
: `Reconnecting in ${formatRetryCountdown(status.nextRetryAt, nowMs)}... ${formatReconnectAttemptLabel(status)}`,
timeout: 0,
- title: buildReconnectTitle(status),
+ title: "Disconnected from T3 Server",
type: "loading" as const,
data: {
hideCopyButton: true,You can send follow-ups to the cloud agent here.
3b06cc9 to
3759e05
Compare
314c455 to
54f905c
Compare
3759e05 to
4caff6f
Compare
There was a problem hiding this comment.
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: Desktop bootstrap token consumed but still advertised via IPC
- The IPC handler now clears backendBootstrapToken after the first read, so subsequent calls return undefined instead of the stale, already-consumed token.
Or push these changes by commenting:
@cursor push eebbb23e4b
Preview (eebbb23e4b)
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -1239,10 +1239,14 @@
ipcMain.removeAllListeners(GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL);
ipcMain.on(GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL, (event) => {
+ const token = backendBootstrapToken || undefined;
+ if (token) {
+ backendBootstrapToken = "";
+ }
event.returnValue = {
label: "Local environment",
wsUrl: backendWsUrl || null,
- bootstrapToken: backendBootstrapToken || undefined,
+ bootstrapToken: token,
} as const;
});You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 4 total unresolved issues (including 2 from previous reviews).
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Network-accessible preference permanently lost on transient network failure
- Added a
degradedflag to skip persisting the settings when the mode was downgraded from network-accessible to local-only due to no available LAN address, preserving the user's original preference.
- Added a
- ✅ Fixed: Loopback hostname check inconsistent with auth policy
- Added
normalizedHostname.startsWith("127.")toisLoopbackHostnameinhttp.tsto align with the broader127.x.x.xrange already recognized byisLoopbackHostinServerAuthPolicy.ts.
- Added
Or push these changes by commenting:
@cursor push 3b7b4cb840
Preview (3b7b4cb840)
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -220,11 +220,13 @@
...(advertisedHostOverride ? { advertisedHostOverride } : {}),
});
+ let degraded = false;
if (mode === "network-accessible" && exposure.mode !== "network-accessible") {
if (options?.rejectIfUnavailable) {
throw new Error("No reachable network address is available for this desktop right now.");
}
mode = "local-only";
+ degraded = true;
}
desktopServerExposureMode = exposure.mode;
@@ -241,7 +243,7 @@
backendEndpointUrl = exposure.endpointUrl;
backendAdvertisedHost = exposure.advertisedHost;
- if (options?.persist || exposure.mode !== mode) {
+ if (!degraded && (options?.persist || exposure.mode !== mode)) {
writeDesktopSettings(DESKTOP_SETTINGS_PATH, desktopSettings);
}
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
@@ -34,7 +34,7 @@
.trim()
.toLowerCase()
.replace(/^\[(.*)\]$/, "$1");
- return LOOPBACK_HOSTNAMES.has(normalizedHostname);
+ return LOOPBACK_HOSTNAMES.has(normalizedHostname) || normalizedHostname.startsWith("127.");
}
export function resolveDevRedirectUrl(devUrl: URL, requestUrl: URL): string {You can send follow-ups to the cloud agent here.
9f966e4 to
dc69564
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Unused
authSessionRouteLayerexport is dead code- Removed the unused
authSessionRouteLayerexport which was dead code, since onlyauthSessionCorsRouteLayer(with CORS support) is actually imported and used inserver.ts.
- Removed the unused
Or push these changes by commenting:
@cursor push b263b7fc96
Preview (b263b7fc96)
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
@@ -28,17 +28,6 @@
);
});
-export const authSessionRouteLayer = HttpRouter.add(
- "GET",
- "/api/auth/session",
- Effect.gen(function* () {
- const request = yield* HttpServerRequest.HttpServerRequest;
- const serverAuth = yield* ServerAuth;
- const session = yield* serverAuth.getSessionState(request);
- return HttpServerResponse.jsonUnsafe(session, { status: 200 });
- }),
-);
-
const REMOTE_AUTH_ALLOW_METHODS = "GET, POST, OPTIONS";
const REMOTE_AUTH_ALLOW_HEADERS = "authorization, content-type";You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Fallback log message never emitted due to mutated state
- Captured the original serverExposureMode into a local variable before calling applyDesktopServerExposureMode, so the fallback check compares against the pre-mutation value.
Or push these changes by commenting:
@cursor push 552d1cfcbe
Preview (552d1cfcbe)
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -1667,18 +1667,16 @@
`bootstrap restoring persisted server exposure mode mode=${desktopSettings.serverExposureMode}`,
);
}
- const serverExposureState = await applyDesktopServerExposureMode(
- desktopSettings.serverExposureMode,
- {
- persist: desktopSettings.serverExposureMode !== DEFAULT_DESKTOP_SETTINGS.serverExposureMode,
- },
- );
+ const requestedExposureMode = desktopSettings.serverExposureMode;
+ const serverExposureState = await applyDesktopServerExposureMode(requestedExposureMode, {
+ persist: requestedExposureMode !== DEFAULT_DESKTOP_SETTINGS.serverExposureMode,
+ });
writeDesktopLogHeader(`bootstrap resolved backend endpoint baseUrl=${backendHttpUrl}`);
if (serverExposureState.endpointUrl) {
writeDesktopLogHeader(
`bootstrap enabled network access endpointUrl=${serverExposureState.endpointUrl}`,
);
- } else if (desktopSettings.serverExposureMode === "network-accessible") {
+ } else if (requestedExposureMode === "network-accessible") {
writeDesktopLogHeader(
"bootstrap fell back to local-only because no advertised network host was available",
);You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Dev redirect skips non-loopback LAN requests to dev server
- Removed the isLoopbackHostname guard from the dev redirect condition so all requests are redirected to the Vite dev server when devUrl is configured, regardless of the incoming hostname.
Or push these changes by commenting:
@cursor push b05c9dd39a
Preview (b05c9dd39a)
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
@@ -219,7 +219,7 @@
}
const config = yield* ServerConfig;
- if (config.devUrl && isLoopbackHostname(url.value.hostname)) {
+ if (config.devUrl) {
return HttpServerResponse.redirect(resolveDevRedirectUrl(config.devUrl, url.value), {
status: 302,
});You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Loopback local URLs unreachable when binding specific host
- Changed the specific-host branch in resolveDesktopServerExposure to compute localHttpUrl and localWsUrl using selectedHost instead of the hardcoded 127.0.0.1 loopback address, so the desktop app connects to the address the server actually binds to.
Or push these changes by commenting:
@cursor push 94810f1dd5
Preview (94810f1dd5)
diff --git a/apps/desktop/src/serverExposure.ts b/apps/desktop/src/serverExposure.ts
--- a/apps/desktop/src/serverExposure.ts
+++ b/apps/desktop/src/serverExposure.ts
@@ -130,8 +130,8 @@
return {
mode: input.mode,
bindHost: selectedHost,
- localHttpUrl,
- localWsUrl,
+ localHttpUrl: `http://${selectedHost}:${input.port}`,
+ localWsUrl: `ws://${selectedHost}:${input.port}`,
endpointUrl: `http://${selectedHost}:${input.port}`,
advertisedHost: selectedHost,
availableHosts,You can send follow-ups to the cloud agent here.
- Create secrets with exclusive open and retry after AlreadyExists - Treat expired bootstrap tokens as hard failures - Improve CLI and keybinding validation messages
- Persist the user’s requested server exposure mode even when a safer mode is applied temporarily - Route auth bootstrap cookies through the session credential service - Tighten secret-store read error handling for concurrent startup
There was a problem hiding this comment.
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 fixes for both issues found in the latest run.
- ✅ Fixed: Unused
appliedModeparameter creates misleading API- Removed the unused
appliedModefield from the function signature, its call site inmain.ts, and the test.
- Removed the unused
- ✅ Fixed: Session cookie name duplicated across independent modules
- Extracted
SESSION_COOKIE_NAMEtoServices/SessionCredentialService.tsand imported it in bothServerAuthPolicy.tsand the layerSessionCredentialService.ts, eliminating the duplicated constant.
- Extracted
Or push these changes by commenting:
@cursor push d38c659f7a
Preview (d38c659f7a)
diff --git a/apps/desktop/src/desktopSettings.test.ts b/apps/desktop/src/desktopSettings.test.ts
--- a/apps/desktop/src/desktopSettings.test.ts
+++ b/apps/desktop/src/desktopSettings.test.ts
@@ -50,7 +50,6 @@
},
{
requestedMode: "network-accessible",
- appliedMode: "local-only",
},
),
).toEqual({
diff --git a/apps/desktop/src/desktopSettings.ts b/apps/desktop/src/desktopSettings.ts
--- a/apps/desktop/src/desktopSettings.ts
+++ b/apps/desktop/src/desktopSettings.ts
@@ -14,7 +14,6 @@
settings: DesktopSettings,
input: {
readonly requestedMode: DesktopServerExposureMode;
- readonly appliedMode: DesktopServerExposureMode;
},
): DesktopSettings {
const persistedMode = input.requestedMode;
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -237,7 +237,6 @@
desktopServerExposureMode = exposure.mode;
desktopSettings = setDesktopServerExposurePreference(desktopSettings, {
requestedMode,
- appliedMode: exposure.mode,
});
backendBindHost = exposure.bindHost;
backendHttpUrl = exposure.localHttpUrl;
diff --git a/apps/server/src/auth/Layers/ServerAuthPolicy.ts b/apps/server/src/auth/Layers/ServerAuthPolicy.ts
--- a/apps/server/src/auth/Layers/ServerAuthPolicy.ts
+++ b/apps/server/src/auth/Layers/ServerAuthPolicy.ts
@@ -3,9 +3,8 @@
import { ServerConfig } from "../../config.ts";
import { ServerAuthPolicy, type ServerAuthPolicyShape } from "../Services/ServerAuthPolicy.ts";
+import { SESSION_COOKIE_NAME } from "../Services/SessionCredentialService.ts";
-const SESSION_COOKIE_NAME = "t3_session";
-
const isWildcardHost = (host: string | undefined): boolean =>
host === "0.0.0.0" || host === "::" || host === "[::]";
diff --git a/apps/server/src/auth/Layers/SessionCredentialService.ts b/apps/server/src/auth/Layers/SessionCredentialService.ts
--- a/apps/server/src/auth/Layers/SessionCredentialService.ts
+++ b/apps/server/src/auth/Layers/SessionCredentialService.ts
@@ -6,6 +6,7 @@
import { AuthSessionRepository } from "../../persistence/Services/AuthSessions.ts";
import { ServerSecretStore } from "../Services/ServerSecretStore.ts";
import {
+ SESSION_COOKIE_NAME,
SessionCredentialError,
SessionCredentialService,
type IssuedSession,
@@ -21,7 +22,6 @@
} from "../tokenCodec.ts";
const SIGNING_SECRET_NAME = "server-signing-key";
-const SESSION_COOKIE_NAME = "t3_session";
const DEFAULT_SESSION_TTL = Duration.days(30);
const DEFAULT_WEBSOCKET_TOKEN_TTL = Duration.minutes(5);
diff --git a/apps/server/src/auth/Services/SessionCredentialService.ts b/apps/server/src/auth/Services/SessionCredentialService.ts
--- a/apps/server/src/auth/Services/SessionCredentialService.ts
+++ b/apps/server/src/auth/Services/SessionCredentialService.ts
@@ -81,6 +81,8 @@
readonly markDisconnected: (sessionId: AuthSessionId) => Effect.Effect<void, never>;
}
+export const SESSION_COOKIE_NAME = "t3_session";
+
export class SessionCredentialService extends ServiceMap.Service<
SessionCredentialService,
SessionCredentialServiceShapeYou can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Electron browser detection is unreachable dead code
- Moved the Electron check before the Chrome check in inferBrowser so Electron's UA string (which contains 'Chrome/') is correctly identified as Electron.
Or push these changes by commenting:
@cursor push bb1611aabd
Preview (bb1611aabd)
diff --git a/apps/server/src/auth/utils.ts b/apps/server/src/auth/utils.ts
--- a/apps/server/src/auth/utils.ts
+++ b/apps/server/src/auth/utils.ts
@@ -67,10 +67,10 @@
const normalized = userAgent;
if (/Edg\//.test(normalized)) return "Edge";
if (/OPR\//.test(normalized)) return "Opera";
+ if (/Electron\//.test(normalized)) return "Electron";
if (/Firefox\//.test(normalized)) return "Firefox";
if (/Chrome\//.test(normalized) || /CriOS\//.test(normalized)) return "Chrome";
if (/Safari\//.test(normalized) && !/Chrome\//.test(normalized)) return "Safari";
- if (/Electron\//.test(normalized)) return "Electron";
return undefined;
}You can send follow-ups to the cloud agent here.
- Stop retrying subscriptions after application-level stream failures - Keep retry loops for transport disconnects only - Add tests for both failure paths
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Fragile string comparison routes credential consume control flow
- Added a structured
reasondiscriminant field ('not-found' | 'expired') to the ConsumeResult error variant and replaced the fragile message string comparison with a check onseededResult.reason !== "not-found".
- Added a structured
Or push these changes by commenting:
@cursor push c52134326d
Preview (c52134326d)
diff --git a/apps/server/src/auth/Layers/BootstrapCredentialService.ts b/apps/server/src/auth/Layers/BootstrapCredentialService.ts
--- a/apps/server/src/auth/Layers/BootstrapCredentialService.ts
+++ b/apps/server/src/auth/Layers/BootstrapCredentialService.ts
@@ -21,6 +21,7 @@
type ConsumeResult =
| {
readonly _tag: "error";
+ readonly reason: "not-found" | "expired";
readonly error: BootstrapCredentialError;
}
| {
@@ -178,6 +179,7 @@
return [
{
_tag: "error",
+ reason: "not-found",
error: invalidBootstrapCredentialError("Unknown bootstrap credential."),
},
current,
@@ -190,6 +192,7 @@
return [
{
_tag: "error",
+ reason: "expired",
error: invalidBootstrapCredentialError("Bootstrap credential expired."),
},
next,
@@ -227,7 +230,7 @@
if (seededResult._tag === "success") {
return seededResult.grant;
}
- if (seededResult.error.message !== "Unknown bootstrap credential.") {
+ if (seededResult.reason !== "not-found") {
return yield* seededResult.error;
}You can send follow-ups to the cloud agent here.
- Switch browser component test mocks to async `vi.importActual` - Keep real `@tanstack/react-query` and environment exports available to tests
…parison in credential consume flow
Replace exact message string comparison with a 'reason' discriminant field
on the ConsumeResult error variant ('not-found' | 'expired') to determine
whether to fall through from in-memory seeded grants to the database-backed
pairing link lookup. This prevents the database lookup from silently breaking
if the error message text is ever changed.
…parison in credential consume flow
Replace exact message string comparison with a 'reason' discriminant field
on the ConsumeResult error variant ('not-found' | 'expired') to determine
whether to fall through from in-memory seeded grants to the database-backed
pairing link lookup. This prevents the database lookup from silently breaking
if the error message text is ever changed.
Applied via @cursor push command
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Browser/OS inference skips lowercasing unlike device type inference
- Added
.toLowerCase()to thenormalizedassignment in bothinferBrowserandinferOs, and updated all regex patterns to use lowercase to match the existing pattern ininferDeviceType.
- Added
Or push these changes by commenting:
@cursor push 726137b8ff
Preview (726137b8ff)
diff --git a/apps/server/src/auth/utils.ts b/apps/server/src/auth/utils.ts
--- a/apps/server/src/auth/utils.ts
+++ b/apps/server/src/auth/utils.ts
@@ -64,13 +64,13 @@
if (!userAgent) {
return undefined;
}
- const normalized = userAgent;
- if (/Edg\//.test(normalized)) return "Edge";
- if (/OPR\//.test(normalized)) return "Opera";
- if (/Firefox\//.test(normalized)) return "Firefox";
- if (/Chrome\//.test(normalized) || /CriOS\//.test(normalized)) return "Chrome";
- if (/Safari\//.test(normalized) && !/Chrome\//.test(normalized)) return "Safari";
- if (/Electron\//.test(normalized)) return "Electron";
+ const normalized = userAgent.toLowerCase();
+ if (/edg\//.test(normalized)) return "Edge";
+ if (/opr\//.test(normalized)) return "Opera";
+ if (/firefox\//.test(normalized)) return "Firefox";
+ if (/chrome\//.test(normalized) || /crios\//.test(normalized)) return "Chrome";
+ if (/safari\//.test(normalized) && !/chrome\//.test(normalized)) return "Safari";
+ if (/electron\//.test(normalized)) return "Electron";
return undefined;
}
@@ -78,12 +78,12 @@
if (!userAgent) {
return undefined;
}
- const normalized = userAgent;
- if (/iPhone|iPad|iPod/.test(normalized)) return "iOS";
- if (/Android/.test(normalized)) return "Android";
- if (/Mac OS X|Macintosh/.test(normalized)) return "macOS";
- if (/Windows NT/.test(normalized)) return "Windows";
- if (/Linux/.test(normalized)) return "Linux";
+ const normalized = userAgent.toLowerCase();
+ if (/iphone|ipad|ipod/.test(normalized)) return "iOS";
+ if (/android/.test(normalized)) return "Android";
+ if (/mac os x|macintosh/.test(normalized)) return "macOS";
+ if (/windows nt/.test(normalized)) return "Windows";
+ if (/linux/.test(normalized)) return "Linux";
return undefined;
}You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 63460ab. Configure here.
…eviceType Applied via @cursor push command
- improve remote host normalization and snapshot recovery - sync browser chrome and terminal colors with active theme - add QR code and runtime store coverage
- Extract the empty chat state into a shared component - Sync theme-color and app chrome to the active surface - Fix Electron auth metadata detection for user agents
Co-authored-by: codex <codex@users.noreply.github.com>
…threadId (#2) * Raise slow RPC ack warning threshold to 15s (pingdotgg#1760) * Use active worktree path for workspace saves (pingdotgg#1762) * Stream git status updates over WebSocket (pingdotgg#1763) Co-authored-by: codex <codex@users.noreply.github.com> * fix(web): unwrap windows shell command wrappers (pingdotgg#1719) * Rename "Chat" to "Build" in interaction mode toggle (pingdotgg#1769) Co-authored-by: Julius Marminge <julius0216@outlook.com> * Assign default capabilities to Codex custom models (pingdotgg#1793) * Add project rename support in the sidebar (pingdotgg#1798) * Support multi-select pending user inputs (pingdotgg#1797) * Add Zed support to Open actions via editor command aliases (pingdotgg#1303) Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: Julius Marminge <julius0216@outlook.com> * Closes pingdotgg#1795 - Support building and developing in a devcontainer (pingdotgg#1791) * Add explicit timeouts to CI and release workflows (pingdotgg#1825) * fix(web): distinguish singular/plural in pending action submit label (pingdotgg#1826) * Refactor web stores into atomic slices ready to split ChatView (pingdotgg#1708) * Add VSCode Insiders and VSCodium icons (pingdotgg#1847) * Prepare datamodel for multi-environment (pingdotgg#1765) Co-authored-by: justsomelegs <145564979+justsomelegs@users.noreply.github.com> Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com> * Implement server auth bootstrap and pairing flow (pingdotgg#1768) Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: Julius Marminge <julius@macmini.local> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com> * Use dev proxy for loopback auth and environment requests (pingdotgg#1853) * Refresh local git status on turn completion (pingdotgg#1821) Co-authored-by: codex <codex@users.noreply.github.com> * fix(desktop): add Copy Link action for chat links (pingdotgg#1835) * fix: map runtime modes to correct permission levels (pingdotgg#1587) Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com> * Fix persisted composer image hydration typo (pingdotgg#1831) * Clarify environment and workspace picker labels (pingdotgg#1854) * Scope git toast state by thread ref (pingdotgg#1855) * fix build (pingdotgg#1859) * Stabilize keybindings toast stream setup (pingdotgg#1860) Co-authored-by: Julius Marminge <julius@macmini.local> * feat(web): add embeddable thread route for canvas tile hosts Adds /embed/thread/:environmentId/:threadId — a standalone route that renders the existing ChatView without the app sidebar chrome. This is the iframe target for t3-canvas agent shapes (see rororowyourboat/t3-canvas#3). - New file-based route embed.thread.\$environmentId.\$threadId.tsx - __root.tsx bypasses AppSidebarLayout for any /embed/* pathname so the environment connection + websocket surface + toasts still initialize but the sidebar/diff/plan chrome does not render - minimal=1 search param is parsed and wired to a data attribute on the container for future targeted CSS; chrome hiding (BranchToolbar, PlanSidebar, ThreadTerminalDrawer) stays as a follow-up pass - routeTree.gen.ts regenerated by the @tanstack/router-plugin --------- Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: legs <145564979+justsomelegs@users.noreply.github.com> Co-authored-by: sonder <168988030+heysonder@users.noreply.github.com> Co-authored-by: Adem Ben Abdallah <96244394+AdemBenAbdallah@users.noreply.github.com> Co-authored-by: Kyle Gottfried <6462596+Spitfire1900@users.noreply.github.com> Co-authored-by: Jacob <589761+jvzijp@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com> Co-authored-by: Julius Marminge <julius@macmini.local> Co-authored-by: Klemencina <56873773+Klemencina@users.noreply.github.com> Co-authored-by: Oskar Sekutowicz <me.oski646@gmail.com> Co-authored-by: Noxire <59626436+noxire-dev@users.noreply.github.com>



Summary
/pairroute for browser pairingbeforeLoad, add a first-paint loading shell, and document the auth architectureTesting
Note
High Risk
Touches core authentication/session handling for both HTTP and WebSocket paths and changes desktop bootstrap/exposure behavior; mistakes could lock users out or unintentionally expose a server on the network.
Overview
Adds a server-wide auth layer built around one-time bootstrap credentials (desktop bootstrap + pairing tokens), persisted/signed session credentials (cookie + bearer), and WebSocket upgrade auth via a new
ServerAuthservice stack.Introduces new auth HTTP APIs (
/api/auth/session,/api/auth/bootstrap,/api/auth/bootstrap/bearer,/api/auth/ws-token, plus owner-only pairing/client management routes) and supporting Effect layers/services (ServerSecretStore,BootstrapCredentialService,SessionCredentialService,AuthControlPlane) with extensive tests.Updates the desktop app’s startup/IPC to replace the legacy
getWsUrlflow withgetLocalEnvironmentBootstrap(HTTP+WS base URLs + bootstrap token), adds persisted server exposure mode (local-only vs network-accessible) with LAN endpoint discovery and relaunch-on-change behavior, and tightens dev tooling to requireVITE_DEV_SERVER_URL.Adds architecture documentation for remote environments (
.docs/remote-architecture.md) and updates formatter/CI checks for the new preload bridge surface.Reviewed by Cursor Bugbot for commit 21c748b. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Implement server auth bootstrap and pairing flow with session management and CLI
ServerAuth,BootstrapCredentialService, andSessionCredentialServicebacked by SQLite (migrations 020–022), supporting browser-session-cookie and bearer-session-token methods.POST /api/auth/bootstrap,/api/auth/bootstrap/bearer,GET /api/auth/session,POST /api/auth/ws-token) and enforces authentication on WebSocket upgrades and selected routes.t3 authCLI subcommands to create/list/revoke pairing links and issue/list/revoke bearer sessions, replacing the old--auth-tokenflag withdesktopBootstrapToken.local-onlyvsnetwork-accessibleserver exposure modes with IPC APIs (getServerExposureState,setServerExposureMode), persisted settings, and a new backend readiness gate polling/api/auth/session./pairpairing route, a/settings/connectionspage, and multi-environment support in the sidebar, chat view, and branch toolbar.getWsUrlIPC andauthToken/T3CODE_AUTH_TOKENare removed; consumers ofDesktopBridge,KnownEnvironment, andWsRpcClientAPIs must migrate to the new interfaces.Macroscope summarized 21c748b.