diff --git a/docs/deploy/foundry-self-hosting.mdx b/docs/deploy/foundry-self-hosting.mdx index cbf639a..9146a46 100644 --- a/docs/deploy/foundry-self-hosting.mdx +++ b/docs/deploy/foundry-self-hosting.mdx @@ -36,9 +36,9 @@ That recipe sets `NODE_ENV=development`, which enables the dotenv loader. These values can be safely defaulted for local development: - `APP_URL=http://localhost:4173` -- `BETTER_AUTH_URL=http://localhost:4173` +- `BETTER_AUTH_URL=http://localhost:7741` - `BETTER_AUTH_SECRET=sandbox-agent-foundry-development-only-change-me` -- `GITHUB_REDIRECT_URI=http://localhost:4173/api/rivet/app/auth/github/callback` +- `GITHUB_REDIRECT_URI=http://localhost:7741/api/auth/github/callback` These should be treated as development-only values. @@ -90,7 +90,7 @@ Recommended GitHub App permissions: - Repository `Checks: Read` - Repository `Commit statuses: Read` -Set the webhook URL to `https:///api/rivet/app/webhooks/github` and generate a webhook secret. Store the secret as `GITHUB_WEBHOOK_SECRET`. +Set the webhook URL to `https:///api/webhooks/github` and generate a webhook secret. Store the secret as `GITHUB_WEBHOOK_SECRET`. Recommended webhook subscriptions: diff --git a/foundry/packages/backend/src/actors/workspace/app-shell.ts b/foundry/packages/backend/src/actors/workspace/app-shell.ts index aff0fe1..6786073 100644 --- a/foundry/packages/backend/src/actors/workspace/app-shell.ts +++ b/foundry/packages/backend/src/actors/workspace/app-shell.ts @@ -832,7 +832,7 @@ export const workspaceAppActions = { customerId, customerEmail: session.currentUserEmail, planId: input.planId, - successUrl: `${appShell.appUrl}/api/rivet/app/billing/checkout/complete?organizationId=${encodeURIComponent( + successUrl: `${appShell.apiUrl}/api/billing/checkout/complete?organizationId=${encodeURIComponent( input.organizationId, )}&foundrySession=${encodeURIComponent(input.sessionId)}&session_id={CHECKOUT_SESSION_ID}`, cancelUrl: `${appShell.appUrl}/organizations/${input.organizationId}/billing?foundrySession=${encodeURIComponent(input.sessionId)}`, diff --git a/foundry/packages/backend/src/index.ts b/foundry/packages/backend/src/index.ts index 2af1fa6..4c3dd14 100644 --- a/foundry/packages/backend/src/index.ts +++ b/foundry/packages/backend/src/index.ts @@ -73,7 +73,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise origin ?? "*", credentials: true, @@ -103,7 +103,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise origin ?? "*", credentials: true, @@ -132,12 +132,12 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { + app.get("/api/app/snapshot", async (c) => { const sessionId = await resolveSessionId(c); return c.json(await appWorkspaceAction(async (workspace) => await workspace.getAppSnapshot({ sessionId }))); }); - app.get("/api/rivet/app/auth/github/start", async (c) => { + app.get("/api/auth/github/start", async (c) => { const sessionId = await resolveSessionId(c); const result = await appWorkspaceAction(async (workspace) => await workspace.startAppGithubAuth({ sessionId })); return Response.redirect(result.url, 302); @@ -154,20 +154,20 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { + app.post("/api/app/sign-out", async (c) => { const sessionId = await resolveSessionId(c); return c.json(await appWorkspaceAction(async (workspace) => await workspace.signOutApp({ sessionId }))); }); - app.post("/api/rivet/app/onboarding/starter-repo/skip", async (c) => { + app.post("/api/app/onboarding/starter-repo/skip", async (c) => { const sessionId = await resolveSessionId(c); return c.json(await appWorkspaceAction(async (workspace) => await workspace.skipAppStarterRepo({ sessionId }))); }); - app.post("/api/rivet/app/organizations/:organizationId/starter-repo/star", async (c) => { + app.post("/api/app/organizations/:organizationId/starter-repo/star", async (c) => { const sessionId = await resolveSessionId(c); return c.json( await appWorkspaceAction( @@ -180,7 +180,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { + app.post("/api/app/organizations/:organizationId/select", async (c) => { const sessionId = await resolveSessionId(c); return c.json( await appWorkspaceAction( @@ -193,7 +193,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { + app.patch("/api/app/organizations/:organizationId/profile", async (c) => { const sessionId = await resolveSessionId(c); const body = await c.req.json(); return c.json( @@ -210,7 +210,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { + app.post("/api/app/organizations/:organizationId/import", async (c) => { const sessionId = await resolveSessionId(c); return c.json( await appWorkspaceAction( @@ -223,7 +223,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { + app.post("/api/app/organizations/:organizationId/reconnect", async (c) => { const sessionId = await resolveSessionId(c); return c.json( await appWorkspaceAction( @@ -236,7 +236,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { + app.post("/api/app/organizations/:organizationId/billing/checkout", async (c) => { const sessionId = await resolveSessionId(c); const body = await c.req.json().catch(() => ({})); const planId = body?.planId === "free" || body?.planId === "team" ? (body.planId as FoundryBillingPlanId) : "team"; @@ -249,7 +249,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { + app.get("/api/billing/checkout/complete", async (c) => { const organizationId = c.req.query("organizationId"); const sessionId = c.req.query("foundrySession"); const checkoutSessionId = c.req.query("session_id"); @@ -264,7 +264,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { + app.post("/api/app/organizations/:organizationId/billing/portal", async (c) => { const sessionId = await resolveSessionId(c); return c.json( await (await appWorkspace()).createAppBillingPortalSession({ @@ -274,7 +274,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { + app.post("/api/app/organizations/:organizationId/billing/cancel", async (c) => { const sessionId = await resolveSessionId(c); return c.json( await (await appWorkspace()).cancelAppScheduledRenewal({ @@ -284,7 +284,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { + app.post("/api/app/organizations/:organizationId/billing/resume", async (c) => { const sessionId = await resolveSessionId(c); return c.json( await (await appWorkspace()).resumeAppSubscription({ @@ -294,7 +294,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { + app.post("/api/app/workspaces/:workspaceId/seat-usage", async (c) => { const sessionId = await resolveSessionId(c); return c.json( await (await appWorkspace()).recordAppSeatUsage({ @@ -313,10 +313,9 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { + app.post("/api/webhooks/github", async (c) => { const payload = await c.req.text(); await (await appWorkspace()).handleAppGithubWebhook({ payload, diff --git a/foundry/packages/backend/src/services/app-shell-runtime.ts b/foundry/packages/backend/src/services/app-shell-runtime.ts index 896ecee..84cb326 100644 --- a/foundry/packages/backend/src/services/app-shell-runtime.ts +++ b/foundry/packages/backend/src/services/app-shell-runtime.ts @@ -47,12 +47,14 @@ export type AppShellStripeClient = Pick< export interface AppShellServices { appUrl: string; + apiUrl: string; github: AppShellGithubClient; stripe: AppShellStripeClient; } export interface CreateAppShellServicesOptions { appUrl?: string; + apiUrl?: string; github?: AppShellGithubClient; stripe?: AppShellStripeClient; } @@ -60,6 +62,7 @@ export interface CreateAppShellServicesOptions { export function createDefaultAppShellServices(options: CreateAppShellServicesOptions = {}): AppShellServices { return { appUrl: (options.appUrl ?? process.env.APP_URL ?? "http://localhost:4173").replace(/\/$/, ""), + apiUrl: (options.apiUrl ?? process.env.BETTER_AUTH_URL ?? process.env.APP_URL ?? "http://localhost:7741").replace(/\/$/, ""), github: options.github ?? new GitHubAppClient(), stripe: options.stripe ?? new StripeAppClient(), }; diff --git a/foundry/packages/client/src/backend-client.ts b/foundry/packages/client/src/backend-client.ts index f01a110..b98efbf 100644 --- a/foundry/packages/client/src/backend-client.ts +++ b/foundry/packages/client/src/backend-client.ts @@ -269,6 +269,14 @@ export function createBackendClientFromConfig(config: AppConfig): BackendClient }); } +function stripTrailingSlash(value: string): string { + return value.replace(/\/$/, ""); +} + +function deriveAppApiEndpoint(endpoint: string): string { + return stripTrailingSlash(endpoint).replace(/\/api\/rivet$/, "/api"); +} + function isLoopbackHost(hostname: string): boolean { const h = hostname.toLowerCase(); return h === "127.0.0.1" || h === "localhost" || h === "0.0.0.0" || h === "::1"; @@ -386,6 +394,8 @@ export function createBackendClient(options: BackendClientOptions): BackendClien return createMockBackendClient(options.defaultWorkspaceId); } + const rivetApiEndpoint = stripTrailingSlash(options.endpoint); + const appApiEndpoint = deriveAppApiEndpoint(options.endpoint); let clientPromise: Promise | null = null; let appSessionId = typeof window !== "undefined" ? window.localStorage.getItem("sandbox-agent-foundry:remote-app-session") : null; const workbenchSubscriptions = new Map< @@ -434,7 +444,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien headers.set("Content-Type", "application/json"); } - const res = await fetch(`${options.endpoint.replace(/\/$/, "")}${path}`, { + const res = await fetch(`${appApiEndpoint}${path}`, { ...init, headers, credentials: "include", @@ -465,22 +475,22 @@ export function createBackendClient(options: BackendClientOptions): BackendClien // Use the serverless /metadata endpoint to discover the manager endpoint. // If the server reports a loopback clientEndpoint (127.0.0.1), rewrite to the same host // as the configured endpoint so remote browsers/clients can connect. - const configured = new URL(options.endpoint); + const configured = new URL(rivetApiEndpoint); const configuredOrigin = `${configured.protocol}//${configured.host}`; const initialNamespace = undefined; - const metadata = await fetchMetadataWithRetry(options.endpoint, initialNamespace, { + const metadata = await fetchMetadataWithRetry(rivetApiEndpoint, initialNamespace, { timeoutMs: 30_000, requestTimeoutMs: 8_000, }); // Candidate endpoint: manager endpoint if provided, otherwise stick to the configured endpoint. - const candidateEndpoint = metadata.clientEndpoint ? rewriteLoopbackClientEndpoint(metadata.clientEndpoint, configuredOrigin) : options.endpoint; + const candidateEndpoint = metadata.clientEndpoint ? rewriteLoopbackClientEndpoint(metadata.clientEndpoint, configuredOrigin) : rivetApiEndpoint; // If the manager port isn't reachable from this client (common behind reverse proxies), // fall back to the configured serverless endpoint to avoid hanging requests. const shouldUseCandidate = metadata.clientEndpoint ? await probeMetadataEndpoint(candidateEndpoint, metadata.clientNamespace, 1_500) : true; - const resolvedEndpoint = shouldUseCandidate ? candidateEndpoint : options.endpoint; + const resolvedEndpoint = shouldUseCandidate ? candidateEndpoint : rivetApiEndpoint; return createClient({ endpoint: resolvedEndpoint, @@ -676,10 +686,10 @@ export function createBackendClient(options: BackendClientOptions): BackendClien async signInWithGithub(): Promise { if (typeof window !== "undefined") { - window.location.assign(`${options.endpoint.replace(/\/$/, "")}/app/auth/github/start`); + window.location.assign(`${appApiEndpoint}/auth/github/start`); return; } - await redirectTo("/app/auth/github/start"); + await redirectTo("/auth/github/start"); }, async signOutApp(): Promise { diff --git a/foundry/packages/frontend/vite.config.ts b/foundry/packages/frontend/vite.config.ts index d176346..739f497 100644 --- a/foundry/packages/frontend/vite.config.ts +++ b/foundry/packages/frontend/vite.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ server: { port: 4173, proxy: { - "/api/rivet": { + "/api": { target: backendProxyTarget, changeOrigin: true, },