Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions web/app/api/cli/config/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ export function GET(request: Request): Response {
subrouter: {
url: subrouterURL,
exchangeUrl: new URL(
"/api/subrouter/exchange",
"/api/subrouter/tenant-exchange",
request.url,
).toString(),
},
},
{
headers: {
"cache-control": "public, max-age=300",
"cache-control": "no-store",
},
},
);
Expand Down
59 changes: 59 additions & 0 deletions web/app/api/subrouter/tenant-exchange/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { resolveSubrouterRequestContext } from "../../../../services/subrouter/requestContext";
import { subrouterErrorResponse } from "../../../../services/subrouter/routeHelpers";
import { env } from "../../../env";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export async function POST(request: Request): Promise<Response> {
const resolved = await resolveSubrouterRequestContext(request, {
permission: "use-or-manage",
allowCookie: false,
});
if (!resolved.ok) return resolved.response;

try {
const controlToken = env.SUBROUTER_STACK_TENANT_DELETE_TOKEN?.trim();
const hostedUrl = env.SUBROUTER_HOSTED_URL?.trim().replace(
/\/+$/,
"",
);
if (!controlToken || !hostedUrl) {
return Response.json(
{ error: "service_unavailable" },
{ status: 503 },
);
}
const capabilities = [
...(resolved.value.team.manageAccounts ? ["manage_accounts"] : []),
...(resolved.value.team.use ? ["use"] : []),
];
const upstream = await fetch(`${hostedUrl}/_subrouter/auth/stack`, {
method: "POST",
headers: {
authorization: `Bearer ${resolved.value.accessToken}`,
"content-type": "application/json",
"x-subrouter-stack-control-token": controlToken,
},
body: JSON.stringify({
capabilities,
teamId: resolved.value.team.teamId,
teamName: resolved.value.team.teamName,
}),
cache: "no-store",
});
Comment on lines +31 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file list =="
git ls-files | rg 'web/app/api/subrouter/tenant-exchange/route\.ts|permission|authorization|deadline|abort|fetch' | head -200

echo "== target file =="
cat -n web/app/api/subrouter/tenant-exchange/route.ts | sed -n '1,120p'

echo "== search relevant terms =="
rg -n "resolve|resolved\.value|accessToken|authorization|deadline|AbortSignal|fetch\(" web/app/api/web app web | rg "tenant-exchange|subrouter|auth" -n | head -250

Repository: manaflow-ai/cmux

Length of output: 29951


🌐 Web query:

next.js NextRequest context deadline authorization header

💡 Result:

In Next.js, NextRequest extends the standard Web Request API, meaning it adheres to standard request handling patterns rather than having a built-in context or deadline object [1][2]. Handling Authorization Headers To access an Authorization header in Next.js middleware, use the standard Headers API via request.headers.get('authorization') [3]. Because Next.js middleware operates in the Edge runtime, you must use this standard Web API rather than legacy Node.js request properties [4][3]. To pass authorization or other context downstream (e.g., to API routes or Server Components), you must explicitly set them in the outgoing request headers using NextResponse.next [5][6]: // middleware.ts example import { NextResponse, NextRequest } from 'next/server'; export function middleware(request: NextRequest) { const requestHeaders = new Headers(request.headers); // Set context/headers for downstream routes requestHeaders.set('x-user-id', 'extracted-user-id'); return NextResponse.next({ request: { headers: requestHeaders, }, }); } Context and Deadlines Next.js does not provide a native "context" object or request "deadline" (timeout) mechanism within the NextRequest API [1][2]. 1. Context: There is no built-in mechanism to pass state between middleware and the application other than by modifying request headers [7]. Developers often use headers to pass verified user identities or other context metadata downstream [7][4]. 2. Deadlines: Next.js does not expose a request-level deadline property. If you need to enforce timeouts (e.g., for database queries or API calls performed within middleware), you must implement them manually using standard JavaScript approaches like AbortController [7]. Best Practices Avoid setting large headers, as this can lead to 431 Request Header Fields Too Large errors [5]. Additionally, verify authentication inside sensitive functions (like Server Functions) rather than relying solely on middleware-based authorization, as middleware can be bypassed if routes are not correctly matched [8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tracked tests around hosted subrouter =="
git ls-files 'web/tests/*subrouter*.test.*' | sort
for f in $(git ls-files 'web/tests/*subrouter*.test.*'); do
  echo "--- $f wc"
  wc -l "$f"
done

echo "== target tests references route =="
for f in $(git ls-files 'web/tests/*subrouter*.test.*'); do
  echo "--- $f route.ts refs"
  rg -n "tenant-exchange|exchangeTeam|_subrouter/auth/stack|signal:|timeout|stubFetch|AbortSignal" "$f" || true
done

echo "== requestContext =="
cat -n web/services/subrouter/requestContext.ts | sed -n '1,220p'

echo "== subrouter routeHelpers =="
cat -n web/services/subrouter/routeHelpers.ts | sed -n '1,180p'

echo "== existing bounded fetch patterns =="
cat -n web/app/lib/agent-page-canonical-fetch.ts | sed -n '1,120p'
cat -n web/scripts/subrouter/migrate-legacy-tenants.ts | sed -n '370,400p'

Repository: manaflow-ai/cmux

Length of output: 17024


Bound the hosted tenant exchange request with a cancellation-aware signal.

resolveSubrouterRequestContext() returns a value, so the stack authorization deadline has ended before this route makes the external fetch. The call also has no signal, so a stalled hosted service can hold route capacity. Use an AbortSignal with a reasonable timeout for this hosted request, and add test coverage for the timeout response path.

🤖 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 `@web/app/api/subrouter/tenant-exchange/route.ts` around lines 31 - 44, Update
the hosted tenant exchange fetch in the route handler using
resolveSubrouterRequestContext to include an AbortSignal with a reasonable
timeout, ensuring stalled requests are cancelled independently of the completed
context resolution. Add coverage verifying the timeout/cancellation response
path and preserve existing successful exchange behavior.

const body = await upstream.text();
Comment on lines +31 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate target and related files =="
fd -a 'route\.ts$' . | rg 'web/app/api/subrouter/exchange-v2/route\.ts|subrouter|exchange' || true

echo "== target excerpt =="
if [ -f web/app/api/subrouter/exchange-v2/route.ts ]; then
  wc -l web/app/api/subrouter/exchange-v2/route.ts
  cat -n web/app/api/subrouter/exchange-v2/route.ts
fi

echo "== reference resolveSubrouterRequestContext usages =="
rg -n "resolveSubrouterRequestContext|subrouter.*auth/stack|_subrouter/auth/stack|setTimeout|AbortSignal|with.*abort|cancellation|deadline" web/app web/lib packages apps . --glob '!node_modules' --glob '!dist' --glob '!build' | head -200

Repository: manaflow-ai/cmux

Length of output: 22236


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('web/app/api/subrouter/exchange-v2/route.ts')
text = p.read_text() if p.exists() else ''
checks = {
    'contains_fetch_call': 'fetch(`${hostedUrl}/_subrouter/auth/stack`' in text,
    'contains_upstream_text': 'const body = await upstream.text()' in text,
    'contains_no_abort_signal': '"signal"' not in text or text.find('"signal"') > text.find('fetch(') and 'AbortSignal' not in text,
    'contains_abort_controller': 'AbortController' in text,
    'imports_resolveSubrouterRequestContext': 'resolveSubrouterRequestContext' in text,
}
for k, v in checks.items():
    print(f'{k}={v}')
PY

Repository: manaflow-ai/cmux

Length of output: 316


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate subrouter requestContext files =="
fd -a 'requestContext\.(ts|tsx|js|jsx)$' web | sort

echo "== inspect requestContext =="
for f in $(fd -a 'requestContext\.(ts|tsx|js|jsx)$' web | sort); do
  echo "--- $f ---"
  wc -l "$f"
  cat -n "$f"
done

echo "== inspect tenant-exchange route =="
f="web/app/api/subrouter/tenant-exchange/route.ts"
if [ -f "$f" ]; then
  wc -l "$f"
  cat -n "$f"
fi

echo "== inspect subrouter route tests under web =="
fd -a -e ts -e tsx 'subrouter.*test|test.*subrouter' web | sort
for f in $(fd -a -e ts -e tsx 'subrouter.*test|test.*subrouter' web | sort); do
  echo "--- $f ---"
  wc -l "$f"
  sed -n '1,240p' "$f"
done

Repository: manaflow-ai/cmux

Length of output: 43186


Bound the hosted Stack exchange request with a cancellation-aware deadline.

resolveSubrouterRequestContext finishes with an unbounded signal after this call exits, then web/app/api/subrouter/tenant-exchange/route.ts calls fetch(...) and reads upstream.text() without a signal. A stalled _subrouter/auth/stack request can hold the tenant-exchange request until the platform timeout. Pass the request signal or another cancellation-aware boundary into the outbound call, and add coverage for an aborted hosted request.

🤖 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 `@web/app/api/subrouter/exchange-v2/route.ts` around lines 31 - 45, The hosted
Stack exchange request in the route handler must use a cancellation-aware
deadline instead of allowing fetch and upstream.text() to run indefinitely.
Update the flow around resolveSubrouterRequestContext and the outbound
_subrouter/auth/stack fetch to pass the request signal or an equivalent bounded
signal through the fetch and response-reading path, and add coverage verifying
an aborted hosted request is cancelled.

if (!upstream.ok) {
return new Response(body, {
status: upstream.status,
headers: { "content-type": "text/plain; charset=utf-8" },
});
Comment on lines +43 to +50

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate file =="
wc -l web/app/api/subrouter/tenant-exchange/route.ts
cat -n web/app/api/subrouter/tenant-exchange/route.ts

echo "== route helpers and cache header helpers =="
rg -n "Cache-Control|cache-control|jsonResponse|serviceUnavailableResponse|unauthorized" web/services/subrouter -S
cat -n web/services/subrouter/routeHelpers.ts | sed -n '1,240p'

echo "== subrouter auth helpers for cache headers =="
rg -n "Cache-Control|cache-control|unauthorized|forbidden|serviceUnavailable" web/services/subrouter web/app/api/subrouter -S

Repository: manaflow-ai/cmux

Length of output: 13353


Sensitive Data Exposure (CWE-525): Use of Web Browser Cache Containing Sensitive Information

Reachability: External

Reachability path
● Entry
  web/app/api/cli/config/route.ts:6
  runtime
│
▼
● Sink
  web/app/api/subrouter/tenant-exchange/route.ts

Set Cache-Control: no-store on every response that includes unsubstituted upstream body content.

cache: "no-store" only affects the outbound fetch. This branch returns upstream.text() without a cache restriction, and the catch path also returns subrouter response content without cache-control: no-store. Add that header to these error responses and any equivalent subrouter error exits that include upstream body content.

🤖 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 `@web/app/api/subrouter/tenant-exchange/route.ts` around lines 43 - 50, Update
the error-response paths in the tenant exchange handler, including the
!upstream.ok branch and catch path, to set Cache-Control: no-store whenever
returning unsubstituted upstream or subrouter response content. Preserve the
existing status, body, and content-type behavior, and apply the same header to
any equivalent error exits in the route.

}
const tenant: unknown = JSON.parse(body);
return Response.json(tenant, {
headers: { "cache-control": "no-store" },
});
} catch (error) {
return subrouterErrorResponse(error);
}
}
Comment on lines +8 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add behavior-level regression coverage before the implementation commit.

This PR fixes a CLI login regression. Add a failing test that verifies the CLI receives the v2 URL and that the v2 exchange request includes the scoped capabilities and control header. Put that test in a test-only commit before the implementation commit. As per coding guidelines, “Regression fixes should use two commits: the first adds only the failing test, and the second adds the fix.”

🤖 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 `@web/app/api/subrouter/exchange-v2/route.ts` around lines 7 - 59, Add
behavior-level regression coverage for the CLI login flow before the
implementation change, using the relevant v2 exchange route and CLI login
symbols. The test must verify that the CLI receives the v2 URL and that the
exchange request sends the scoped capabilities plus the control-token header;
place only this failing test in the first commit, with the implementation fix
committed separately afterward.

Source: Coding guidelines

2 changes: 1 addition & 1 deletion web/services/subrouter/hostedClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function createHostedSubrouterClient(options: {
const fetchImpl = options.fetch ?? fetch;
const tenantDeleteToken = (
options.tenantDeleteToken ??
process.env.SUBROUTER_STACK_TENANT_DELETE_TOKEN ??
env.SUBROUTER_STACK_TENANT_DELETE_TOKEN ??
""
).trim();
const assertTenantControlConfigured = (): void => {
Expand Down
5 changes: 3 additions & 2 deletions web/tests/cli-config-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe("CLI config route", () => {
await withCliConfigEnvironment(testEnvironment, async () => {
const response = GET(new Request("https://cmux.com/api/cli/config"));
expect(response.status).toBe(200);
expect(response.headers.get("cache-control")).toBe("no-store");
expect(await response.json()).toEqual({
version: 2,
auth: {
Expand All @@ -66,7 +67,7 @@ describe("CLI config route", () => {
},
subrouter: {
url: testEnvironment.SUBROUTER_HOSTED_URL,
exchangeUrl: "https://cmux.com/api/subrouter/exchange",
exchangeUrl: "https://cmux.com/api/subrouter/tenant-exchange",
},
});
});
Expand All @@ -84,7 +85,7 @@ describe("CLI config route", () => {
"http://127.0.0.1:4152/handler/cli-auth-confirm",
);
expect(body.subrouter.exchangeUrl).toBe(
"http://127.0.0.1:4152/api/subrouter/exchange",
"http://127.0.0.1:4152/api/subrouter/tenant-exchange",
);
});
});
Expand Down