diff options
Diffstat (limited to 'cloud/function')
| -rw-r--r-- | cloud/function/src/gateway.ts | 317 |
1 files changed, 2 insertions, 315 deletions
diff --git a/cloud/function/src/gateway.ts b/cloud/function/src/gateway.ts index c8b39990f..aed02b4d4 100644 --- a/cloud/function/src/gateway.ts +++ b/cloud/function/src/gateway.ts @@ -1,39 +1,18 @@ -import { z } from "zod" import { Hono, MiddlewareHandler } from "hono" -import { cors } from "hono/cors" -import { HTTPException } from "hono/http-exception" -import { zValidator } from "@hono/zod-validator" import { Resource } from "@opencode/cloud-core/util/resource.js" -import { type ProviderMetadata, type LanguageModelUsage, generateText, streamText } from "ai" +import { type ProviderMetadata, type LanguageModelUsage } from "ai" import { createAnthropic } from "@ai-sdk/anthropic" import { createOpenAI } from "@ai-sdk/openai" import { createOpenAICompatible } from "@ai-sdk/openai-compatible" import type { LanguageModelV2Prompt } from "@ai-sdk/provider" import { type ChatCompletionCreateParamsBase } from "openai/resources/chat/completions" import { Actor } from "@opencode/cloud-core/actor.js" -import { and, Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js" -import { UserTable } from "@opencode/cloud-core/schema/user.sql.js" +import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js" import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js" -import { createClient } from "@openauthjs/openauth/client" -import { Log } from "@opencode/cloud-core/util/log.js" import { Billing } from "@opencode/cloud-core/billing.js" -import { Workspace } from "@opencode/cloud-core/workspace.js" -import { BillingTable, PaymentTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js" -import { centsToMicroCents } from "@opencode/cloud-core/util/price.js" -import { Identifier } from "../../core/src/identifier" type Env = {} -let _client: ReturnType<typeof createClient> -const client = () => { - if (_client) return _client - _client = createClient({ - clientID: "api", - issuer: Resource.AUTH_API_URL.value, - }) - return _client -} - const SUPPORTED_MODELS = { "anthropic/claude-sonnet-4": { input: 0.0000015, @@ -72,10 +51,6 @@ const SUPPORTED_MODELS = { }, } -const log = Log.create({ - namespace: "api", -}) - const GatewayAuth: MiddlewareHandler = async (c, next) => { const authHeader = c.req.header("authorization") @@ -125,56 +100,6 @@ const GatewayAuth: MiddlewareHandler = async (c, next) => { await next() } -const RestAuth: MiddlewareHandler = async (c, next) => { - const authorization = c.req.header("authorization") - if (!authorization) { - return Actor.provide("public", {}, next) - } - const token = authorization.split(" ")[1] - if (!token) - throw new HTTPException(403, { - message: "Bearer token is required.", - }) - - const verified = await client().verify(token) - if (verified.err) { - throw new HTTPException(403, { - message: "Invalid token.", - }) - } - let subject = verified.subject as Actor.Info - if (subject.type === "account") { - const workspaceID = c.req.header("x-opencode-workspace") - const email = subject.properties.email - if (workspaceID) { - const user = await Database.use((tx) => - tx - .select({ - id: UserTable.id, - workspaceID: UserTable.workspaceID, - email: UserTable.email, - }) - .from(UserTable) - .where(and(eq(UserTable.email, email), eq(UserTable.workspaceID, workspaceID))) - .then((rows) => rows[0]), - ) - if (!user) - throw new HTTPException(403, { - message: "You do not have access to this workspace.", - }) - subject = { - type: "user", - properties: { - userID: user.id, - workspaceID: workspaceID, - email: user.email, - }, - } - } - } - await Actor.provide(subject.type, subject.properties, next) -} - const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; workspaceID: string } } }>() .get("/", (c) => c.text("Hello, world!")) .post("/v1/chat/completions", GatewayAuth, async (c) => { @@ -664,244 +589,6 @@ const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; wor } }) }) - .use("/*", cors()) - .use(RestAuth) - .get("/rest/account", async (c) => { - const account = Actor.assert("account") - let workspaces = await Workspace.list() - if (workspaces.length === 0) { - await Workspace.create() - workspaces = await Workspace.list() - } - return c.json({ - id: account.properties.accountID, - email: account.properties.email, - workspaces, - }) - }) - .get("/billing/info", async (c) => { - const billing = await Billing.get() - const payments = await Database.use((tx) => - tx - .select() - .from(PaymentTable) - .where(eq(PaymentTable.workspaceID, Actor.workspace())) - .orderBy(sql`${PaymentTable.timeCreated} DESC`) - .limit(100), - ) - const usage = await Database.use((tx) => - tx - .select() - .from(UsageTable) - .where(eq(UsageTable.workspaceID, Actor.workspace())) - .orderBy(sql`${UsageTable.timeCreated} DESC`) - .limit(100), - ) - return c.json({ billing, payments, usage }) - }) - .post( - "/billing/checkout", - zValidator( - "json", - z.custom<{ - success_url: string - cancel_url: string - }>(), - ), - async (c) => { - const account = Actor.assert("user") - - const body = await c.req.json() - - const customer = await Billing.get() - const session = await Billing.stripe().checkout.sessions.create({ - mode: "payment", - line_items: [ - { - price_data: { - currency: "usd", - product_data: { - name: "opencode credits", - }, - unit_amount: 2000, // $20 minimum - }, - quantity: 1, - }, - ], - payment_intent_data: { - setup_future_usage: "on_session", - }, - ...(customer.customerID - ? { customer: customer.customerID } - : { - customer_email: account.properties.email, - customer_creation: "always", - }), - metadata: { - workspaceID: Actor.workspace(), - }, - currency: "usd", - payment_method_types: ["card"], - success_url: body.success_url, - cancel_url: body.cancel_url, - }) - - return c.json({ - url: session.url, - }) - }, - ) - .post("/billing/portal", async (c) => { - const body = await c.req.json() - - const customer = await Billing.get() - if (!customer?.customerID) { - throw new Error("No stripe customer ID") - } - - const session = await Billing.stripe().billingPortal.sessions.create({ - customer: customer.customerID, - return_url: body.return_url, - }) - - return c.json({ - url: session.url, - }) - }) - .post("/stripe/webhook", async (c) => { - const body = await Billing.stripe().webhooks.constructEventAsync( - await c.req.text(), - c.req.header("stripe-signature")!, - Resource.STRIPE_WEBHOOK_SECRET.value, - ) - - console.log(body.type, JSON.stringify(body, null, 2)) - if (body.type === "checkout.session.completed") { - const workspaceID = body.data.object.metadata?.workspaceID - const customerID = body.data.object.customer as string - const paymentID = body.data.object.payment_intent as string - const amount = body.data.object.amount_total - - if (!workspaceID) throw new Error("Workspace ID not found") - if (!customerID) throw new Error("Customer ID not found") - if (!amount) throw new Error("Amount not found") - if (!paymentID) throw new Error("Payment ID not found") - - await Actor.provide("system", { workspaceID }, async () => { - const customer = await Billing.get() - if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch") - - // set customer metadata - if (!customer?.customerID) { - await Billing.stripe().customers.update(customerID, { - metadata: { - workspaceID, - }, - }) - } - - // get payment method for the payment intent - const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, { - expand: ["payment_method"], - }) - const paymentMethod = paymentIntent.payment_method - if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded") - - await Database.transaction(async (tx) => { - await tx - .update(BillingTable) - .set({ - balance: sql`${BillingTable.balance} + ${centsToMicroCents(amount)}`, - customerID, - paymentMethodID: paymentMethod.id, - paymentMethodLast4: paymentMethod.card!.last4, - }) - .where(eq(BillingTable.workspaceID, workspaceID)) - await tx.insert(PaymentTable).values({ - workspaceID, - id: Identifier.create("payment"), - amount: centsToMicroCents(amount), - paymentID, - customerID, - }) - }) - }) - } - - console.log("finished handling") - - return c.json("ok", 200) - }) - .get("/keys", async (c) => { - const user = Actor.assert("user") - - const keys = await Database.use((tx) => - tx - .select({ - id: KeyTable.id, - name: KeyTable.name, - key: KeyTable.key, - userID: KeyTable.userID, - timeCreated: KeyTable.timeCreated, - timeUsed: KeyTable.timeUsed, - }) - .from(KeyTable) - .where(eq(KeyTable.workspaceID, user.properties.workspaceID)) - .orderBy(sql`${KeyTable.timeCreated} DESC`), - ) - - return c.json({ keys }) - }) - .post("/keys", zValidator("json", z.object({ name: z.string().min(1).max(255) })), async (c) => { - const user = Actor.assert("user") - const { name } = c.req.valid("json") - - // Generate secret key: sk- + 64 random characters (upper, lower, numbers) - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - let randomPart = "" - for (let i = 0; i < 64; i++) { - randomPart += chars.charAt(Math.floor(Math.random() * chars.length)) - } - const secretKey = `sk-${randomPart}` - - const keyRecord = await Database.use((tx) => - tx - .insert(KeyTable) - .values({ - id: Identifier.create("key"), - workspaceID: user.properties.workspaceID, - userID: user.properties.userID, - name, - key: secretKey, - timeUsed: null, - }) - .returning(), - ) - - return c.json({ - key: secretKey, - id: keyRecord[0].id, - name: keyRecord[0].name, - created: keyRecord[0].timeCreated, - }) - }) - .delete("/keys/:id", async (c) => { - const user = Actor.assert("user") - const keyId = c.req.param("id") - - const result = await Database.use((tx) => - tx - .delete(KeyTable) - .where(and(eq(KeyTable.id, keyId), eq(KeyTable.workspaceID, user.properties.workspaceID))) - .returning({ id: KeyTable.id }), - ) - - if (result.length === 0) { - return c.json({ error: "Key not found" }, 404) - } - - return c.json({ success: true, id: result[0].id }) - }) .all("*", (c) => c.text("Not Found")) export type ApiType = typeof app |
