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
56 changes: 50 additions & 6 deletions web/app/api/coderouter/accounts/[accountId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { removeAccount } from "../../../../../services/coderouter/accounts";
import { resolveCodeRouterRequestContext } from "../../../../../services/coderouter/requestContext";
import { captureCoderouterEvent } from "../../../../../services/coderouter/analytics";
import {
addCoderouterBreadcrumb,
reportCoderouterFailure,
} from "../../../../../services/coderouter/observability";

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

const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const UUID =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

export function createDeleteAccountHandler(dependencies: {
readonly resolve: typeof resolveCodeRouterRequestContext;
Expand All @@ -23,13 +29,51 @@ export function createDeleteAccountHandler(dependencies: {
if (!UUID.test(accountId)) {
return Response.json({ error: "invalid_request" }, { status: 400 });
}
const result = await dependencies.remove({
teamId: resolved.value.team.teamId,
accountId,
});
let result;
try {
result = await dependencies.remove({
teamId: resolved.value.team.teamId,
accountId,
});
} catch (error) {
reportCoderouterFailure("rds", error, { operation: "remove_account" });
return Response.json(
{
error: "account_remove_unavailable",
message:
"coderouter could not remove this account. Nothing was partially removed; retry shortly.",
retryable: true,
},
{
status: 503,
headers: { "cache-control": "no-store", "retry-after": "5" },
},
);
}
if (!result.removed) {
return Response.json({ error: "not_found" }, { status: 404 });
return Response.json(
{
error: "not_found",
message:
"That coderouter account no longer exists. Refresh with `cr` and retry if needed.",
retryable: false,
},
{ status: 404 },
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
}
captureCoderouterEvent({
event: "coderouter_account_removed",
userId: resolved.value.user.id,
teamId: resolved.value.team.teamId,
properties: {
last_account: result.lastAccount,
legacy_cleanup_pending: result.legacyCleanupPending,
},
});
addCoderouterBreadcrumb("account", "Provider account removed", {
last_account: result.lastAccount,
legacy_cleanup_pending: result.legacyCleanupPending,
});
return Response.json(result, {
headers: { "cache-control": "no-store" },
});
Expand Down
68 changes: 62 additions & 6 deletions web/app/api/coderouter/accounts/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { addAccount, parseCredential } from "../../../../services/coderouter/accounts";
import {
addAccount,
parseCredential,
} from "../../../../services/coderouter/accounts";
import {
resolveCoderouterUsageTeam,
resolveCodeRouterRequestContext,
} from "../../../../services/coderouter/requestContext";
import { accountsWithUsage } from "../../../../services/coderouter/usage";
import { captureCoderouterEvent } from "../../../../services/coderouter/analytics";
import {
addCoderouterBreadcrumb,
reportCoderouterFailure,
} from "../../../../services/coderouter/observability";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";
Expand Down Expand Up @@ -36,6 +44,22 @@ export async function GET(request: Request): Promise<Response> {
timing("serialize", serializeMs),
timing("total", performance.now() - startedAt),
].join(", ");
captureCoderouterEvent({
event: "coderouter_account_status_viewed",
userId: resolved.stackUserId,
teamId: resolved.teamId,
properties: {
account_count: result.accounts.length,
account_error_count: result.accounts.filter(
(account) => "usageError" in account && Boolean(account.usageError),
).length,
duration_ms: Math.round(performance.now() - startedAt),
},
});
addCoderouterBreadcrumb("status", "Account status loaded", {
account_count: result.accounts.length,
duration_ms: Math.round(performance.now() - startedAt),
});
return new Response(body, {
headers: {
"cache-control": "no-store",
Expand Down Expand Up @@ -69,11 +93,43 @@ export async function POST(request: Request): Promise<Response> {
if (!credential) {
return Response.json({ error: "invalid_request" }, { status: 400 });
}
const result = await addAccount(resolved.value.team.teamId, credential);
return Response.json(result, {
status: result.alreadyExists ? 200 : 201,
headers: { "cache-control": "no-store" },
});
try {
const result = await addAccount(resolved.value.team.teamId, credential);
captureCoderouterEvent({
event: "coderouter_account_added",
userId: resolved.value.user.id,
teamId: resolved.value.team.teamId,
properties: {
provider: credential.provider,
already_exists: result.alreadyExists,
},
});
addCoderouterBreadcrumb("account", "Provider account stored", {
provider: credential.provider,
already_exists: result.alreadyExists,
});
return Response.json(result, {
status: result.alreadyExists ? 200 : 201,
headers: { "cache-control": "no-store" },
});
} catch (error) {
reportCoderouterFailure("rds", error, { operation: "add_account" });
return Response.json(
{
error: "account_store_unavailable",
message:
"coderouter could not store this account. Your local provider sign-in was not removed; retry `cr add` shortly.",
retryable: true,
},
{
status: 503,
headers: {
"cache-control": "no-store",
"retry-after": "5",
},
},
);
}
}

function timing(name: string, duration: number): string {
Expand Down
78 changes: 69 additions & 9 deletions web/app/api/coderouter/session/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import {
} from "../../../../services/coderouter/repository";
import { resolveCodeRouterRequestContext } from "../../../../services/coderouter/requestContext";
import { captureCoderouterError } from "../../../../services/errors";
import { captureCoderouterEvent } from "../../../../services/coderouter/analytics";
import {
addCoderouterBreadcrumb,
reportCoderouterFailure,
} from "../../../../services/coderouter/observability";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";
Expand Down Expand Up @@ -35,9 +40,20 @@ export function makeCoderouterSessionGetHandler(
return async function GET(request: Request): Promise<Response> {
const authorization = request.headers.get("authorization")?.trim() ?? "";
const token = /^Bearer[ \t]+(.+)$/i.exec(authorization)?.[1]?.trim();
if (!token || !await authenticate(token)) {
if (!token || !(await authenticate(token))) {
addCoderouterBreadcrumb(
"auth",
"Route session validation rejected",
{},
"warning",
);
return Response.json(
{ error: "unauthorized" },
{
error: "unauthorized",
message:
"Your coderouter session expired or was revoked. Run `cr login` and retry.",
retryable: false,
},
{ status: 401, headers: { "cache-control": "no-store" } },
);
}
Expand All @@ -57,9 +73,14 @@ export function makeCoderouterSessionPostHandler(
const userId = resolved.value.user.id;
if (dependencies.hostedProRequired()) {
try {
if (!await dependencies.hasActivePro(userId)) {
if (!(await dependencies.hasActivePro(userId))) {
return Response.json(
{ error: "pro_required" },
{
error: "pro_required",
message:
"Hosted coderouter requires cmux Pro. Upgrade or connect a self-hosted server.",
retryable: false,
},
{
status: 402,
headers: { "cache-control": "no-store" },
Expand All @@ -72,24 +93,57 @@ export function makeCoderouterSessionPostHandler(
route: "/api/coderouter/session",
});
return Response.json(
{ error: "entitlement_unavailable" },
{
error: "entitlement_unavailable",
message:
"coderouter could not verify your Pro entitlement. Nothing was charged or changed; retry shortly.",
retryable: true,
},
{
status: 503,
headers: { "cache-control": "no-store" },
},
);
}
}
const issued = await dependencies.issueToken(
resolved.value.team.teamId,
let issued;
try {
issued = await dependencies.issueToken(
resolved.value.team.teamId,
userId,
);
} catch (error) {
reportCoderouterFailure("rds", error, {
operation: "issue_route_session",
});
return Response.json(
{
error: "session_unavailable",
message:
"coderouter could not create a route session. Retry shortly; no account credentials were changed.",
retryable: true,
},
{
status: 503,
headers: { "cache-control": "no-store", "retry-after": "5" },
},
);
}
captureCoderouterEvent({
event: "coderouter_route_session_issued",
userId,
);
teamId: resolved.value.team.teamId,
properties: { hosted_pro_required: dependencies.hostedProRequired() },
});
addCoderouterBreadcrumb("session", "Route session issued");
return Response.json(
{
teamId: resolved.value.team.teamId,
token: issued.token,
expiresAt: issued.expiresAt.toISOString(),
openaiBaseUrl: new URL("/v1", request.url).toString().replace(/\/$/, ""),
openaiBaseUrl: new URL("/v1", request.url)
.toString()
.replace(/\/$/, ""),
},
{ headers: { "cache-control": "no-store" } },
);
Expand All @@ -104,6 +158,12 @@ export async function DELETE(request: Request): Promise<Response> {
return Response.json({ error: "invalid_request" }, { status: 400 });
}
await revokeRouteToken(resolved.value.team.teamId, routeToken);
captureCoderouterEvent({
event: "coderouter_route_session_revoked",
userId: resolved.value.user.id,
teamId: resolved.value.team.teamId,
});
addCoderouterBreadcrumb("session", "Route session revoked");
return new Response(null, {
status: 204,
headers: { "cache-control": "no-store" },
Expand Down
Loading