summaryrefslogtreecommitdiffhomepage
path: root/cloud/core/src
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-08-28 16:44:55 -0400
committerFrank <[email protected]>2025-08-28 16:44:55 -0400
commitc6ef92634d0ae026a59e023e69847b481975462b (patch)
treec88bfbbabf1f46ca922e3212dd929b34d6229a1a /cloud/core/src
parentf97fdceb01c69ca563e755c6d50312ef7352f663 (diff)
downloadopencode-c6ef92634d0ae026a59e023e69847b481975462b.tar.gz
opencode-c6ef92634d0ae026a59e023e69847b481975462b.zip
wip cloud
Diffstat (limited to 'cloud/core/src')
-rw-r--r--cloud/core/src/billing.ts93
-rw-r--r--cloud/core/src/key.ts79
-rw-r--r--cloud/core/src/user.ts18
3 files changed, 189 insertions, 1 deletions
diff --git a/cloud/core/src/billing.ts b/cloud/core/src/billing.ts
index 1a7bb2946..94ba23b83 100644
--- a/cloud/core/src/billing.ts
+++ b/cloud/core/src/billing.ts
@@ -1,12 +1,13 @@
import { Resource } from "sst"
import { Stripe } from "stripe"
import { Database, eq, sql } from "./drizzle"
-import { BillingTable, UsageTable } from "./schema/billing.sql"
+import { BillingTable, PaymentTable, UsageTable } from "./schema/billing.sql"
import { Actor } from "./actor"
import { fn } from "./util/fn"
import { z } from "zod"
import { Identifier } from "./identifier"
import { centsToMicroCents } from "./util/price"
+import { User } from "./user"
export namespace Billing {
export const stripe = () =>
@@ -29,6 +30,28 @@ export namespace Billing {
)
}
+ export const payments = async () => {
+ return await Database.use((tx) =>
+ tx
+ .select()
+ .from(PaymentTable)
+ .where(eq(PaymentTable.workspaceID, Actor.workspace()))
+ .orderBy(sql`${PaymentTable.timeCreated} DESC`)
+ .limit(100),
+ )
+ }
+
+ export const usages = async () => {
+ return await Database.use((tx) =>
+ tx
+ .select()
+ .from(UsageTable)
+ .where(eq(UsageTable.workspaceID, Actor.workspace()))
+ .orderBy(sql`${UsageTable.timeCreated} DESC`)
+ .limit(100),
+ )
+ }
+
export const consume = fn(
z.object({
requestID: z.string().optional(),
@@ -68,4 +91,72 @@ export namespace Billing {
})
},
)
+
+ export const generateCheckoutUrl = fn(
+ z.object({
+ successUrl: z.string(),
+ cancelUrl: z.string(),
+ }),
+ async (input) => {
+ const account = Actor.assert("user")
+ const { successUrl, cancelUrl } = input
+
+ const user = await User.fromID(account.properties.userID)
+ 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: user.email,
+ customer_creation: "always",
+ }),
+ metadata: {
+ workspaceID: Actor.workspace(),
+ },
+ currency: "usd",
+ payment_method_types: ["card"],
+ success_url: successUrl,
+ cancel_url: cancelUrl,
+ })
+
+ return session.url
+ },
+ )
+
+ export const generatePortalUrl = fn(
+ z.object({
+ returnUrl: z.string(),
+ }),
+ async (input) => {
+ const { returnUrl } = input
+
+ 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: returnUrl,
+ })
+
+ return session.url
+ },
+ )
}
diff --git a/cloud/core/src/key.ts b/cloud/core/src/key.ts
new file mode 100644
index 000000000..cf4f6e410
--- /dev/null
+++ b/cloud/core/src/key.ts
@@ -0,0 +1,79 @@
+import { z } from "zod"
+import { fn } from "./util/fn"
+import { Actor } from "./actor"
+import { and, Database, eq, sql } from "./drizzle"
+import { Identifier } from "./identifier"
+import { KeyTable } from "./schema/key.sql"
+
+export namespace Key {
+ export const list = async () => {
+ 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 keys
+ }
+
+ export const create = fn(z.object({ name: z.string().min(1).max(255) }), async (input) => {
+ const user = Actor.assert("user")
+ const { name } = input
+
+ // 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 {
+ key: secretKey,
+ id: keyRecord[0].id,
+ name: keyRecord[0].name,
+ created: keyRecord[0].timeCreated,
+ }
+ })
+
+ export const remove = fn(z.object({ id: z.string() }), async (input) => {
+ const user = Actor.assert("user")
+ const { id } = input
+
+ const result = await Database.use((tx) =>
+ tx
+ .delete(KeyTable)
+ .where(and(eq(KeyTable.id, id), eq(KeyTable.workspaceID, user.properties.workspaceID)))
+ .returning({ id: KeyTable.id }),
+ )
+
+ if (result.length === 0) {
+ throw new Error("Key not found")
+ }
+
+ return { id: result[0].id }
+ })
+}
diff --git a/cloud/core/src/user.ts b/cloud/core/src/user.ts
new file mode 100644
index 000000000..7914926ff
--- /dev/null
+++ b/cloud/core/src/user.ts
@@ -0,0 +1,18 @@
+import { z } from "zod"
+import { eq } from "drizzle-orm"
+import { fn } from "./util/fn"
+import { Database } from "./drizzle"
+import { UserTable } from "./schema/user.sql"
+
+export namespace User {
+ export const fromID = fn(z.string(), async (id) =>
+ Database.transaction(async (tx) => {
+ return tx
+ .select()
+ .from(UserTable)
+ .where(eq(UserTable.id, id))
+ .execute()
+ .then((rows) => rows[0])
+ }),
+ )
+}