diff --git a/web/app/api/cli/config/route.ts b/web/app/api/cli/config/route.ts index e194d61f9c7b..10f1e09edc12 100644 --- a/web/app/api/cli/config/route.ts +++ b/web/app/api/cli/config/route.ts @@ -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", }, }, ); diff --git a/web/app/api/subrouter/tenant-exchange/route.ts b/web/app/api/subrouter/tenant-exchange/route.ts new file mode 100644 index 000000000000..ce56c8a3f73f --- /dev/null +++ b/web/app/api/subrouter/tenant-exchange/route.ts @@ -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 { + 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", + }); + const body = await upstream.text(); + if (!upstream.ok) { + return new Response(body, { + status: upstream.status, + headers: { "content-type": "text/plain; charset=utf-8" }, + }); + } + const tenant: unknown = JSON.parse(body); + return Response.json(tenant, { + headers: { "cache-control": "no-store" }, + }); + } catch (error) { + return subrouterErrorResponse(error); + } +} diff --git a/web/services/subrouter/hostedClient.ts b/web/services/subrouter/hostedClient.ts index 6be962ffb61b..ff10149ea7c4 100644 --- a/web/services/subrouter/hostedClient.ts +++ b/web/services/subrouter/hostedClient.ts @@ -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 => { diff --git a/web/tests/cli-config-route.test.ts b/web/tests/cli-config-route.test.ts index 7f473ab9e9fb..5e81740e3b99 100644 --- a/web/tests/cli-config-route.test.ts +++ b/web/tests/cli-config-route.test.ts @@ -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: { @@ -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", }, }); }); @@ -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", ); }); });