summaryrefslogtreecommitdiffhomepage
path: root/cloud/function/src/gateway.ts
diff options
context:
space:
mode:
Diffstat (limited to 'cloud/function/src/gateway.ts')
-rw-r--r--cloud/function/src/gateway.ts317
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