chore: recover bogota workspace state

This commit is contained in:
Nathan Flurry 2026-03-09 19:57:56 -07:00
parent 5d65013aa5
commit e08d1b4dca
436 changed files with 172093 additions and 455 deletions

View file

@ -1,11 +1,15 @@
import { Hono } from "hono";
import { cors } from "hono/cors";
import { initActorRuntimeContext } from "./actors/context.js";
import { registry } from "./actors/index.js";
import { registry, resolveManagerPort } from "./actors/index.js";
import { workspaceKey } from "./actors/keys.js";
import { loadConfig } from "./config/backend.js";
import { createBackends, createNotificationService } from "./notifications/index.js";
import { createDefaultDriver } from "./driver.js";
import { createProviderRegistry } from "./providers/index.js";
import { createClient } from "rivetkit/client";
import { FactoryAppStore } from "./services/app-state.js";
import type { FactoryBillingPlanId, FactoryOrganization } from "@sandbox-agent/factory-shared";
export interface BackendStartOptions {
host?: string;
@ -45,6 +49,34 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
initActorRuntimeContext(config, providers, notifications, driver);
const inner = registry.serve();
const actorClient = createClient({
endpoint: `http://127.0.0.1:${resolveManagerPort()}`,
disableMetadataLookup: true,
}) as any;
const syncOrganizationRepos = async (organization: FactoryOrganization): Promise<void> => {
const workspace = await actorClient.workspace.getOrCreate(workspaceKey(organization.workspaceId), {
createWithInput: organization.workspaceId,
});
const existing = await workspace.listRepos({ workspaceId: organization.workspaceId });
const existingRemotes = new Set(existing.map((repo: { remoteUrl: string }) => repo.remoteUrl));
for (const repo of organization.repoCatalog) {
const remoteUrl = `mockgithub://${repo}`;
if (existingRemotes.has(remoteUrl)) {
continue;
}
await workspace.addRepo({
workspaceId: organization.workspaceId,
remoteUrl,
});
}
};
const appStore = new FactoryAppStore({
onOrganizationReposReady: syncOrganizationRepos,
});
const managerOrigin = `http://127.0.0.1:${resolveManagerPort()}`;
// Wrap in a Hono app mounted at /api/rivet to serve on the backend port.
// Uses Bun.serve — cannot use @hono/node-server because it conflicts with
@ -54,23 +86,115 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
app.use(
"/api/rivet/*",
cors({
origin: "*",
allowHeaders: ["Content-Type", "Authorization", "x-rivet-token"],
origin: (origin) => origin ?? "*",
credentials: true,
allowHeaders: ["Content-Type", "Authorization", "x-rivet-token", "x-factory-session"],
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
exposeHeaders: ["Content-Type"],
exposeHeaders: ["Content-Type", "x-factory-session"],
})
);
app.use(
"/api/rivet",
cors({
origin: "*",
allowHeaders: ["Content-Type", "Authorization", "x-rivet-token"],
origin: (origin) => origin ?? "*",
credentials: true,
allowHeaders: ["Content-Type", "Authorization", "x-rivet-token", "x-factory-session"],
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
exposeHeaders: ["Content-Type"],
exposeHeaders: ["Content-Type", "x-factory-session"],
})
);
const resolveSessionId = (c: any): string => {
const requested = c.req.header("x-factory-session");
const sessionId = appStore.ensureSession(requested);
c.header("x-factory-session", sessionId);
return sessionId;
};
app.get("/api/rivet/app/snapshot", (c) => {
const sessionId = resolveSessionId(c);
return c.json(appStore.getSnapshot(sessionId));
});
app.post("/api/rivet/app/sign-in", async (c) => {
const sessionId = resolveSessionId(c);
const body = await c.req.json().catch(() => ({}));
const userId = typeof body?.userId === "string" ? body.userId : undefined;
return c.json(appStore.signInWithGithub(sessionId, userId));
});
app.post("/api/rivet/app/sign-out", (c) => {
const sessionId = resolveSessionId(c);
return c.json(appStore.signOut(sessionId));
});
app.post("/api/rivet/app/organizations/:organizationId/select", async (c) => {
const sessionId = resolveSessionId(c);
return c.json(await appStore.selectOrganization(sessionId, c.req.param("organizationId")));
});
app.patch("/api/rivet/app/organizations/:organizationId/profile", async (c) => {
const body = await c.req.json();
return c.json(
appStore.updateOrganizationProfile({
organizationId: c.req.param("organizationId"),
displayName: typeof body?.displayName === "string" ? body.displayName : "",
slug: typeof body?.slug === "string" ? body.slug : "",
primaryDomain: typeof body?.primaryDomain === "string" ? body.primaryDomain : "",
}),
);
});
app.post("/api/rivet/app/organizations/:organizationId/import", async (c) => {
return c.json(await appStore.triggerRepoImport(c.req.param("organizationId")));
});
app.post("/api/rivet/app/organizations/:organizationId/reconnect", (c) => {
return c.json(appStore.reconnectGithub(c.req.param("organizationId")));
});
app.post("/api/rivet/app/organizations/:organizationId/billing/checkout", async (c) => {
const body = await c.req.json().catch(() => ({}));
const planId =
body?.planId === "free" || body?.planId === "team" || body?.planId === "enterprise"
? (body.planId as FactoryBillingPlanId)
: "team";
return c.json(appStore.completeHostedCheckout(c.req.param("organizationId"), planId));
});
app.post("/api/rivet/app/organizations/:organizationId/billing/cancel", (c) => {
return c.json(appStore.cancelScheduledRenewal(c.req.param("organizationId")));
});
app.post("/api/rivet/app/organizations/:organizationId/billing/resume", (c) => {
return c.json(appStore.resumeSubscription(c.req.param("organizationId")));
});
app.post("/api/rivet/app/workspaces/:workspaceId/seat-usage", (c) => {
const sessionId = resolveSessionId(c);
const workspaceId = c.req.param("workspaceId");
const userEmail = appStore.findUserEmailForWorkspace(workspaceId, sessionId);
if (userEmail) {
appStore.recordSeatUsage(workspaceId, userEmail);
}
return c.json(appStore.getSnapshot(sessionId));
});
const proxyManagerRequest = async (c: any) => {
const source = new URL(c.req.url);
const target = new URL(source.pathname.replace(/^\/api\/rivet/, "") + source.search, managerOrigin);
return await fetch(new Request(target.toString(), c.req.raw));
};
const forward = async (c: any) => {
try {
const pathname = new URL(c.req.url).pathname;
if (
pathname === "/api/rivet/actors" ||
pathname.startsWith("/api/rivet/actors/") ||
pathname.startsWith("/api/rivet/gateway/")
) {
return await proxyManagerRequest(c);
}
// RivetKit serverless handler is configured with basePath `/api/rivet` by default.
return await inner.fetch(c.req.raw);
} catch (err) {