From f24251f89e277cab2669730dc1e028573e0fe082 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 13 Jan 2026 13:36:37 -0500 Subject: sync --- packages/console/app/src/config.ts | 6 + .../app/src/routes/api/black/setup-intent.ts | 30 + packages/console/app/src/routes/auth/authorize.ts | 11 +- packages/console/app/src/routes/auth/callback.ts | 2 + packages/console/app/src/routes/black.css | 663 +++++++++++++++++++++ packages/console/app/src/routes/black.tsx | 166 ++++++ packages/console/app/src/routes/black/common.tsx | 42 ++ packages/console/app/src/routes/black/index.css | 409 ------------- packages/console/app/src/routes/black/index.tsx | 318 ++-------- .../console/app/src/routes/black/subscribe.tsx | 244 ++++++++ 10 files changed, 1223 insertions(+), 668 deletions(-) create mode 100644 packages/console/app/src/routes/api/black/setup-intent.ts create mode 100644 packages/console/app/src/routes/black.css create mode 100644 packages/console/app/src/routes/black.tsx create mode 100644 packages/console/app/src/routes/black/common.tsx delete mode 100644 packages/console/app/src/routes/black/index.css create mode 100644 packages/console/app/src/routes/black/subscribe.tsx (limited to 'packages/console/app/src') diff --git a/packages/console/app/src/config.ts b/packages/console/app/src/config.ts index 4ebb2c71a..4396e5117 100644 --- a/packages/console/app/src/config.ts +++ b/packages/console/app/src/config.ts @@ -26,4 +26,10 @@ export const config = { commits: "6,500", monthlyUsers: "650,000", }, + + // Stripe + stripe: { + publishableKey: + "pk_live_51OhXSKEclFNgdHcR9dDfYGwQeKuPfKo0IjA5kWBQIXKMFhE8QFd9bYLdPZC6klRKEgEkxJYSKuZg9U3FKHdLnF4300F9qLqMgP", + }, } as const diff --git a/packages/console/app/src/routes/api/black/setup-intent.ts b/packages/console/app/src/routes/api/black/setup-intent.ts new file mode 100644 index 000000000..eb5571616 --- /dev/null +++ b/packages/console/app/src/routes/api/black/setup-intent.ts @@ -0,0 +1,30 @@ +import type { APIEvent } from "@solidjs/start/server" +import { Billing } from "@opencode-ai/console-core/billing.js" + +export async function POST(event: APIEvent) { + try { + const body = (await event.request.json()) as { plan: string } + const plan = body.plan + + if (!plan || !["20", "100", "200"].includes(plan)) { + return Response.json({ error: "Invalid plan" }, { status: 400 }) + } + + const amount = parseInt(plan) * 100 + + const intent = await Billing.stripe().setupIntents.create({ + payment_method_types: ["card"], + metadata: { + plan, + amount: amount.toString(), + }, + }) + + return Response.json({ + clientSecret: intent.client_secret, + }) + } catch (error) { + console.error("Error creating setup intent:", error) + return Response.json({ error: "Internal server error" }, { status: 500 }) + } +} diff --git a/packages/console/app/src/routes/auth/authorize.ts b/packages/console/app/src/routes/auth/authorize.ts index 166466ef8..6be94b146 100644 --- a/packages/console/app/src/routes/auth/authorize.ts +++ b/packages/console/app/src/routes/auth/authorize.ts @@ -2,6 +2,13 @@ import type { APIEvent } from "@solidjs/start/server" import { AuthClient } from "~/context/auth" export async function GET(input: APIEvent) { - const result = await AuthClient.authorize(new URL("./callback", input.request.url).toString(), "code") - return Response.redirect(result.url, 302) + const url = new URL(input.request.url) + // TODO + // input.request.url http://localhost:3001/auth/authorize?continue=/black/subscribe + const result = await AuthClient.authorize( + new URL("/callback/subscribe?foo=bar", input.request.url).toString(), + "code", + ) + // result.url https://auth.frank.dev.opencode.ai/authorize?client_id=app&redirect_uri=http%3A%2F%2Flocalhost%3A3001%2Fauth%2Fcallback&response_type=code&state=0d3fc834-bcbc-42dc-83ab-c25c2c43c7e3 + return Response.redirect(result.url + "&continue=" + url.searchParams.get("continue"), 302) } diff --git a/packages/console/app/src/routes/auth/callback.ts b/packages/console/app/src/routes/auth/callback.ts index 9b7296791..b03bbdbe5 100644 --- a/packages/console/app/src/routes/auth/callback.ts +++ b/packages/console/app/src/routes/auth/callback.ts @@ -5,6 +5,8 @@ import { useAuthSession } from "~/context/auth" export async function GET(input: APIEvent) { const url = new URL(input.request.url) + console.log("=C=", input.request.url) + throw new Error("Not implemented") try { const code = url.searchParams.get("code") if (!code) throw new Error("No code found") diff --git a/packages/console/app/src/routes/black.css b/packages/console/app/src/routes/black.css new file mode 100644 index 000000000..dfb188ed0 --- /dev/null +++ b/packages/console/app/src/routes/black.css @@ -0,0 +1,663 @@ +[data-page="black"] { + background: #000; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: stretch; + font-family: var(--font-mono); + color: #fff; + + [data-component="header-gradient"] { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 288px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(0, 0, 0, 0) 100%); + } + + [data-component="header"] { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 40px; + flex-shrink: 0; + + /* [data-component="header-logo"] { */ + /* } */ + } + + [data-component="content"] { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + flex-grow: 1; + + [data-slot="hero"] { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 8px; + margin-top: 40px; + padding: 0 20px; + + @media (min-width: 768px) { + margin-top: 60px; + } + + h1 { + color: rgba(255, 255, 255, 0.92); + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 160%; + margin: 0; + + @media (min-width: 768px) { + font-size: 24px; + } + } + + p { + color: rgba(255, 255, 255, 0.59); + font-size: 15px; + font-style: normal; + font-weight: 400; + line-height: 160%; + margin: 0; + + @media (min-width: 768px) { + font-size: 18px; + } + } + } + + [data-slot="hero-black"] { + margin-top: 40px; + padding: 0 20px; + + @media (min-width: 768px) { + margin-top: 60px; + } + + svg { + width: 100%; + max-width: 540px; + height: auto; + filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.1)); + } + } + + [data-slot="cta"] { + display: flex; + flex-direction: column; + gap: 16px; + align-items: center; + text-align: center; + margin-top: -40px; + width: 100%; + + @media (min-width: 768px) { + margin-top: -20px; + } + + [data-slot="heading"] { + color: rgba(255, 255, 255, 0.92); + text-align: center; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 160%; /* 28.8px */ + + span { + display: inline-block; + } + } + [data-slot="subheading"] { + color: rgba(255, 255, 255, 0.59); + font-size: 15px; + font-style: normal; + font-weight: 400; + line-height: 160%; + + @media (min-width: 768px) { + font-size: 18px; + line-height: 160%; + } + } + [data-slot="button"] { + display: inline-flex; + height: 40px; + padding: 0 12px; + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 4px; + background: rgba(255, 255, 255, 0.92); + text-decoration: none; + color: #000; + font-family: "JetBrains Mono Nerd Font"; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: normal; + + &:hover { + background: #e0e0e0; + } + + &:active { + transform: scale(0.98); + } + } + [data-slot="back-soon"] { + color: rgba(255, 255, 255, 0.59); + text-align: center; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 160%; /* 20.8px */ + } + [data-slot="follow-us"] { + display: inline-flex; + height: 40px; + padding: 0 12px; + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.17); + color: rgba(255, 255, 255, 0.59); + font-family: "JetBrains Mono Nerd Font"; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + text-decoration: none; + } + + [data-slot="pricing"] { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + max-width: 540px; + padding: 0 20px; + + @media (min-width: 768px) { + padding: 0; + } + } + + [data-slot="pricing-card"] { + display: flex; + flex-direction: column; + gap: 12px; + padding: 20px; + border: 1px solid rgba(255, 255, 255, 0.17); + border-radius: 4px; + text-decoration: none; + transition: border-color 0.15s ease; + background: transparent; + cursor: pointer; + text-align: left; + + &:hover { + border-color: rgba(255, 255, 255, 0.35); + } + + [data-slot="icon"] { + color: rgba(255, 255, 255, 0.59); + } + + [data-slot="price"] { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px; + } + + [data-slot="amount"] { + color: rgba(255, 255, 255, 0.92); + font-size: 24px; + font-weight: 500; + } + + [data-slot="period"] { + color: rgba(255, 255, 255, 0.59); + font-size: 14px; + } + + [data-slot="multiplier"] { + color: rgba(255, 255, 255, 0.39); + font-size: 14px; + + &::before { + content: "·"; + margin-right: 8px; + } + } + } + + [data-slot="selected-plan"] { + display: flex; + flex-direction: column; + gap: 32px; + width: fit-content; + max-width: calc(100% - 40px); + margin: 0 auto; + } + + [data-slot="selected-card"] { + display: flex; + flex-direction: column; + gap: 16px; + padding: 20px; + border: 1px solid rgba(255, 255, 255, 0.17); + border-radius: 4px; + width: fit-content; + + [data-slot="icon"] { + color: rgba(255, 255, 255, 0.59); + } + + [data-slot="price"] { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px; + } + + [data-slot="amount"] { + color: rgba(255, 255, 255, 0.92); + font-size: 24px; + font-weight: 500; + } + + [data-slot="period"] { + color: rgba(255, 255, 255, 0.59); + font-size: 14px; + } + + [data-slot="multiplier"] { + color: rgba(255, 255, 255, 0.39); + font-size: 14px; + + &::before { + content: "·"; + margin-right: 8px; + } + } + + [data-slot="terms"] { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 12px; + text-align: left; + + li { + color: rgba(255, 255, 255, 0.59); + font-size: 13px; + line-height: 1.5; + padding-left: 16px; + position: relative; + white-space: nowrap; + + &::before { + content: "▪"; + position: absolute; + left: 0; + color: rgba(255, 255, 255, 0.39); + } + } + } + + [data-slot="actions"] { + display: flex; + gap: 16px; + margin-top: 8px; + + button, + a { + flex: 1; + display: inline-flex; + height: 48px; + padding: 0 16px; + justify-content: center; + align-items: center; + border-radius: 4px; + font-family: var(--font-mono); + font-size: 16px; + font-weight: 400; + text-decoration: none; + cursor: pointer; + } + + [data-slot="cancel"] { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.17); + color: rgba(255, 255, 255, 0.92); + + &:hover { + border-color: rgba(255, 255, 255, 0.35); + } + } + + [data-slot="continue"] { + background: rgba(255, 255, 255, 0.17); + border: 1px solid rgba(255, 255, 255, 0.17); + color: rgba(255, 255, 255, 0.59); + + &:hover { + background: rgba(255, 255, 255, 0.25); + } + } + } + } + + [data-slot="fine-print"] { + color: rgba(255, 255, 255, 0.39); + text-align: center; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 160%; /* 20.8px */ + font-style: italic; + + a { + color: rgba(255, 255, 255, 0.39); + text-decoration: underline; + } + } + } + + /* Subscribe page styles */ + [data-slot="subscribe-form"] { + display: flex; + flex-direction: column; + gap: 32px; + align-items: center; + margin-top: -18px; + width: 100%; + max-width: 540px; + padding: 0 20px; + + @media (min-width: 768px) { + margin-top: 40px; + padding: 0; + } + + [data-slot="form-card"] { + width: 100%; + border: 1px solid rgba(255, 255, 255, 0.17); + border-radius: 4px; + padding: 24px; + display: flex; + flex-direction: column; + gap: 20px; + } + + [data-slot="plan-header"] { + display: flex; + flex-direction: column; + gap: 8px; + } + + [data-slot="title"] { + color: rgba(255, 255, 255, 0.92); + font-size: 16px; + font-weight: 400; + margin-bottom: 8px; + } + + [data-slot="icon"] { + color: rgba(255, 255, 255, 0.59); + } + + [data-slot="price"] { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px; + } + + [data-slot="amount"] { + color: rgba(255, 255, 255, 0.92); + font-size: 24px; + font-weight: 500; + } + + [data-slot="period"] { + color: rgba(255, 255, 255, 0.59); + font-size: 14px; + } + + [data-slot="multiplier"] { + color: rgba(255, 255, 255, 0.39); + font-size: 14px; + + &::before { + content: "·"; + margin: 0 8px; + } + } + + [data-slot="divider"] { + height: 1px; + background: rgba(255, 255, 255, 0.17); + } + + [data-slot="section-title"] { + color: rgba(255, 255, 255, 0.92); + font-size: 16px; + font-weight: 400; + } + + [data-slot="checkout-form"] { + display: flex; + flex-direction: column; + gap: 20px; + } + + [data-slot="error"] { + color: #ff6b6b; + font-size: 14px; + } + + [data-slot="submit-button"] { + width: 100%; + height: 48px; + background: rgba(255, 255, 255, 0.92); + border: none; + border-radius: 4px; + color: #000; + font-family: var(--font-mono); + font-size: 16px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s ease; + + &:hover:not(:disabled) { + background: #e0e0e0; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + + [data-slot="charge-notice"] { + color: #d4a500; + font-size: 14px; + text-align: center; + } + + [data-slot="loading"] { + display: flex; + justify-content: center; + padding: 40px 0; + + p { + color: rgba(255, 255, 255, 0.59); + font-size: 14px; + } + } + + [data-slot="fine-print"] { + color: rgba(255, 255, 255, 0.39); + text-align: center; + font-size: 13px; + font-style: italic; + + a { + color: rgba(255, 255, 255, 0.39); + text-decoration: underline; + } + } + + [data-slot="workspace-picker"] { + [data-slot="workspace-list"] { + width: 100%; + padding: 0; + margin: 0; + list-style: none; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + align-self: stretch; + outline: none; + overflow-y: auto; + max-height: 240px; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + + [data-slot="workspace-item"] { + width: 100%; + display: flex; + padding: 8px 12px; + align-items: center; + gap: 8px; + align-self: stretch; + cursor: pointer; + + [data-slot="selected-icon"] { + visibility: hidden; + color: rgba(255, 255, 255, 0.39); + font-family: "IBM Plex Mono", monospace; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 160%; + } + + span:last-child { + color: rgba(255, 255, 255, 0.92); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 160%; + } + + &:hover, + &[data-active="true"] { + background: #161616; + + [data-slot="selected-icon"] { + visibility: visible; + } + } + } + } + } + } + } + + [data-component="footer"] { + display: flex; + flex-direction: column; + width: 100%; + justify-content: center; + align-items: center; + gap: 24px; + flex-shrink: 0; + + @media (min-width: 768px) { + height: 120px; + } + + [data-slot="footer-content"] { + display: flex; + gap: 24px; + align-items: center; + justify-content: center; + + @media (min-width: 768px) { + gap: 40px; + } + + span, + a { + color: rgba(255, 255, 255, 0.39); + font-family: "JetBrains Mono Nerd Font"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + text-decoration: none; + } + + [data-slot="github-stars"] { + color: rgba(255, 255, 255, 0.25); + font-family: "JetBrains Mono Nerd Font"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + } + + [data-slot="anomaly"] { + display: none; + + @media (min-width: 768px) { + display: block; + } + } + } + [data-slot="anomaly-alt"] { + color: rgba(255, 255, 255, 0.39); + font-family: "JetBrains Mono Nerd Font"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + text-decoration: none; + margin-bottom: 24px; + + a { + color: rgba(255, 255, 255, 0.39); + font-family: "JetBrains Mono Nerd Font"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + text-decoration: none; + } + + @media (min-width: 768px) { + display: none; + } + } + } +} diff --git a/packages/console/app/src/routes/black.tsx b/packages/console/app/src/routes/black.tsx new file mode 100644 index 000000000..5a5b139dd --- /dev/null +++ b/packages/console/app/src/routes/black.tsx @@ -0,0 +1,166 @@ +import { A, createAsync, RouteSectionProps } from "@solidjs/router" +import { createMemo } from "solid-js" +import { github } from "~/lib/github" +import { config } from "~/config" +import "./black.css" + +export default function BlackLayout(props: RouteSectionProps) { + const githubData = createAsync(() => github()) + const starCount = createMemo(() => + githubData()?.stars + ? new Intl.NumberFormat("en-US", { + notation: "compact", + compactDisplay: "short", + }).format(githubData()!.stars!) + : config.github.starsFormatted.compact, + ) + + return ( +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+

Access all the world's best coding models

+

Including Claude, GPT, Gemini and more

+
+
+ + + + + + + + + + + + + + + + + + +
+ {props.children} +
+ +
+ ) +} diff --git a/packages/console/app/src/routes/black/common.tsx b/packages/console/app/src/routes/black/common.tsx new file mode 100644 index 000000000..c1184bd20 --- /dev/null +++ b/packages/console/app/src/routes/black/common.tsx @@ -0,0 +1,42 @@ +import { Match, Switch } from "solid-js" + +export const plans = [ + { id: "20", amount: 20, multiplier: null }, + { id: "100", amount: 100, multiplier: "6x more usage than Black 20" }, + { id: "200", amount: 200, multiplier: "21x more usage than Black 20" }, +] as const + +export type Plan = (typeof plans)[number] + +export function PlanIcon(props: { plan: string }) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/packages/console/app/src/routes/black/index.css b/packages/console/app/src/routes/black/index.css deleted file mode 100644 index 418598792..000000000 --- a/packages/console/app/src/routes/black/index.css +++ /dev/null @@ -1,409 +0,0 @@ -[data-page="black"] { - background: #000; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: stretch; - font-family: var(--font-mono); - color: #fff; - - [data-component="header-gradient"] { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 288px; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(0, 0, 0, 0) 100%); - } - - [data-component="header"] { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding-top: 40px; - flex-shrink: 0; - - /* [data-component="header-logo"] { */ - /* } */ - } - - [data-component="content"] { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; - flex-grow: 1; - - [data-slot="hero-black"] { - margin-top: 110px; - - @media (min-width: 768px) { - margin-top: 150px; - } - } - - [data-slot="cta"] { - display: flex; - flex-direction: column; - gap: 32px; - align-items: center; - text-align: center; - margin-top: -18px; - - @media (min-width: 768px) { - margin-top: 40px; - } - - [data-slot="heading"] { - color: rgba(255, 255, 255, 0.92); - text-align: center; - font-size: 18px; - font-style: normal; - font-weight: 400; - line-height: 160%; /* 28.8px */ - - span { - display: inline-block; - } - } - [data-slot="subheading"] { - color: rgba(255, 255, 255, 0.59); - font-size: 15px; - font-style: normal; - font-weight: 400; - line-height: 160%; - - @media (min-width: 768px) { - font-size: 18px; - line-height: 160%; - } - } - [data-slot="button"] { - display: inline-flex; - height: 40px; - padding: 0 12px; - justify-content: center; - align-items: center; - gap: 8px; - border-radius: 4px; - background: rgba(255, 255, 255, 0.92); - text-decoration: none; - color: #000; - font-family: "JetBrains Mono Nerd Font"; - font-size: 16px; - font-style: normal; - font-weight: 500; - line-height: normal; - - &:hover { - background: #e0e0e0; - } - - &:active { - transform: scale(0.98); - } - } - [data-slot="back-soon"] { - color: rgba(255, 255, 255, 0.59); - text-align: center; - font-size: 13px; - font-style: normal; - font-weight: 400; - line-height: 160%; /* 20.8px */ - } - [data-slot="follow-us"] { - display: inline-flex; - height: 40px; - padding: 0 12px; - justify-content: center; - align-items: center; - gap: 8px; - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.17); - color: rgba(255, 255, 255, 0.59); - font-family: "JetBrains Mono Nerd Font"; - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: normal; - text-decoration: none; - } - - [data-slot="pricing"] { - display: flex; - flex-direction: column; - gap: 16px; - width: 100%; - max-width: 540px; - padding: 0 20px; - - @media (min-width: 768px) { - padding: 0; - } - } - - [data-slot="pricing-card"] { - display: flex; - flex-direction: column; - gap: 12px; - padding: 20px; - border: 1px solid rgba(255, 255, 255, 0.17); - border-radius: 4px; - text-decoration: none; - transition: border-color 0.15s ease; - background: transparent; - cursor: pointer; - text-align: left; - - &:hover { - border-color: rgba(255, 255, 255, 0.35); - } - - [data-slot="icon"] { - color: rgba(255, 255, 255, 0.59); - } - - [data-slot="price"] { - display: flex; - flex-wrap: wrap; - align-items: baseline; - gap: 8px; - } - - [data-slot="amount"] { - color: rgba(255, 255, 255, 0.92); - font-size: 24px; - font-weight: 500; - } - - [data-slot="period"] { - color: rgba(255, 255, 255, 0.59); - font-size: 14px; - } - - [data-slot="multiplier"] { - color: rgba(255, 255, 255, 0.39); - font-size: 14px; - - &::before { - content: "·"; - margin-right: 8px; - } - } - } - - [data-slot="selected-plan"] { - display: flex; - flex-direction: column; - gap: 32px; - width: fit-content; - max-width: calc(100% - 40px); - margin: 0 auto; - } - - [data-slot="selected-card"] { - display: flex; - flex-direction: column; - gap: 16px; - padding: 20px; - border: 1px solid rgba(255, 255, 255, 0.17); - border-radius: 4px; - width: fit-content; - - [data-slot="icon"] { - color: rgba(255, 255, 255, 0.59); - } - - [data-slot="price"] { - display: flex; - flex-wrap: wrap; - align-items: baseline; - gap: 8px; - } - - [data-slot="amount"] { - color: rgba(255, 255, 255, 0.92); - font-size: 24px; - font-weight: 500; - } - - [data-slot="period"] { - color: rgba(255, 255, 255, 0.59); - font-size: 14px; - } - - [data-slot="multiplier"] { - color: rgba(255, 255, 255, 0.39); - font-size: 14px; - - &::before { - content: "·"; - margin-right: 8px; - } - } - - [data-slot="terms"] { - list-style: none; - padding: 0; - margin: 0; - display: flex; - flex-direction: column; - gap: 12px; - text-align: left; - - li { - color: rgba(255, 255, 255, 0.59); - font-size: 13px; - line-height: 1.5; - padding-left: 16px; - position: relative; - white-space: nowrap; - - &::before { - content: "▪"; - position: absolute; - left: 0; - color: rgba(255, 255, 255, 0.39); - } - } - } - - [data-slot="actions"] { - display: flex; - gap: 16px; - margin-top: 8px; - - button, - a { - flex: 1; - display: inline-flex; - height: 48px; - padding: 0 16px; - justify-content: center; - align-items: center; - border-radius: 4px; - font-family: var(--font-mono); - font-size: 16px; - font-weight: 400; - text-decoration: none; - cursor: pointer; - } - - [data-slot="cancel"] { - background: transparent; - border: 1px solid rgba(255, 255, 255, 0.17); - color: rgba(255, 255, 255, 0.92); - - &:hover { - border-color: rgba(255, 255, 255, 0.35); - } - } - - [data-slot="continue"] { - background: rgba(255, 255, 255, 0.17); - border: 1px solid rgba(255, 255, 255, 0.17); - color: rgba(255, 255, 255, 0.59); - - &:hover { - background: rgba(255, 255, 255, 0.25); - } - } - } - } - - [data-slot="fine-print"] { - color: rgba(255, 255, 255, 0.39); - text-align: center; - font-size: 13px; - font-style: normal; - font-weight: 400; - line-height: 160%; /* 20.8px */ - font-style: italic; - - a { - color: rgba(255, 255, 255, 0.39); - text-decoration: underline; - } - } - } - } - - [data-component="footer"] { - display: flex; - flex-direction: column; - width: 100%; - justify-content: center; - align-items: center; - gap: 24px; - flex-shrink: 0; - - @media (min-width: 768px) { - height: 120px; - } - - [data-slot="footer-content"] { - display: flex; - gap: 24px; - align-items: center; - justify-content: center; - - @media (min-width: 768px) { - gap: 40px; - } - - span, - a { - color: rgba(255, 255, 255, 0.39); - font-family: "JetBrains Mono Nerd Font"; - font-size: 16px; - font-style: normal; - font-weight: 400; - line-height: normal; - text-decoration: none; - } - - [data-slot="github-stars"] { - color: rgba(255, 255, 255, 0.25); - font-family: "JetBrains Mono Nerd Font"; - font-size: 16px; - font-style: normal; - font-weight: 400; - line-height: normal; - } - - [data-slot="anomaly"] { - display: none; - - @media (min-width: 768px) { - display: block; - } - } - } - [data-slot="anomaly-alt"] { - color: rgba(255, 255, 255, 0.39); - font-family: "JetBrains Mono Nerd Font"; - font-size: 16px; - font-style: normal; - font-weight: 400; - line-height: normal; - text-decoration: none; - margin-bottom: 24px; - - a { - color: rgba(255, 255, 255, 0.39); - font-family: "JetBrains Mono Nerd Font"; - font-size: 16px; - font-style: normal; - font-weight: 400; - line-height: normal; - text-decoration: none; - } - - @media (min-width: 768px) { - display: none; - } - } - } -} diff --git a/packages/console/app/src/routes/black/index.tsx b/packages/console/app/src/routes/black/index.tsx index f5a375adf..2b452c812 100644 --- a/packages/console/app/src/routes/black/index.tsx +++ b/packages/console/app/src/routes/black/index.tsx @@ -1,276 +1,80 @@ -import { A, createAsync, useSearchParams } from "@solidjs/router" -import "./index.css" +import { A, useSearchParams } from "@solidjs/router" import { Title } from "@solidjs/meta" -import { github } from "~/lib/github" import { createMemo, createSignal, For, Match, Show, Switch } from "solid-js" -import { config } from "~/config" - -const plans = [ - { id: "20", amount: 20, multiplier: null }, - { id: "100", amount: 100, multiplier: "6x more usage than Black 20" }, - { id: "200", amount: 200, multiplier: "21x more usage than Black 20" }, -] as const - -function PlanIcon(props: { plan: string }) { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) -} +import { PlanIcon, plans } from "./common" export default function Black() { const [params] = useSearchParams() - const [selected, setSelected] = createSignal(params.plan as string | null) + const [selected, setSelected] = createSignal((params.plan as string) || null) const selectedPlan = createMemo(() => plans.find((p) => p.id === selected())) - const githubData = createAsync(() => github()) - const starCount = createMemo(() => - githubData()?.stars - ? new Intl.NumberFormat("en-US", { - notation: "compact", - compactDisplay: "short", - }).format(githubData()!.stars!) - : config.github.starsFormatted.compact, - ) - return ( -
+ <> opencode -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- - - - - - - - - - - - - - - - - - -
-
-
-

- Access all the world's best coding models -

-

Including Claude, GPT, Gemini, and more

-
- - -
- - {(plan) => ( - - )} - -
-

- Prices shown don't include applicable tax · Terms of Service -

-
- - {(plan) => ( -
-
+
+ + +
+ + {(plan) => ( + - - Continue - -
+ + )} + +
+

+ Prices shown don't include applicable tax · Terms of Service +

+ + + {(plan) => ( +
+
+
+
-

- Prices shown don't include applicable tax · Terms of Service +

+ ${plan().amount}{" "} + per person billed monthly + + {plan().multiplier} +

+
    +
  • Your subscription will not start immediately
  • +
  • You will be added to the waitlist and activated soon
  • +
  • Your card will be only charged when your subscription is activated
  • +
  • Usage limits apply, heavily automated use may reach limits sooner
  • +
  • Subscriptions for individuals, contact Enterprise for teams
  • +
  • Limits may be adjusted and plans may be discontinued in the future
  • +
  • Cancel your subscription at anytime
  • +
+
+ + + Continue + +
- )} - - -
-
- -
+

+ Prices shown don't include applicable tax · Terms of Service +

+
+ )} + + + + ) } diff --git a/packages/console/app/src/routes/black/subscribe.tsx b/packages/console/app/src/routes/black/subscribe.tsx new file mode 100644 index 000000000..00ce19ef6 --- /dev/null +++ b/packages/console/app/src/routes/black/subscribe.tsx @@ -0,0 +1,244 @@ +import { A, createAsync, query, redirect, useSearchParams } from "@solidjs/router" +import { Title } from "@solidjs/meta" +import { createEffect, createSignal, For, onMount, Show } from "solid-js" +import { loadStripe } from "@stripe/stripe-js" +import { Elements, PaymentElement, useStripe, useElements } from "solid-stripe" +import { config } from "~/config" +import { PlanIcon, plans } from "./common" +import { getActor } from "~/context/auth" +import { withActor } from "~/context/auth.withActor" +import { Actor } from "@opencode-ai/console-core/actor.js" +import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js" +import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" +import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" +import { createList } from "solid-list" +import { Modal } from "~/component/modal" + +const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record + +const getWorkspaces = query(async () => { + "use server" + const actor = await getActor() + if (actor.type === "public") throw redirect("/auth/authorize?continue=/black/subscribe") + return withActor(async () => { + return Database.use((tx) => + tx + .select({ + id: WorkspaceTable.id, + name: WorkspaceTable.name, + slug: WorkspaceTable.slug, + }) + .from(UserTable) + .innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id)) + .where( + and( + eq(UserTable.accountID, Actor.account()), + isNull(WorkspaceTable.timeDeleted), + isNull(UserTable.timeDeleted), + ), + ), + ) + }) +}, "black.subscribe.workspaces") + +function CheckoutForm(props: { plan: string; amount: number }) { + const stripe = useStripe() + const elements = useElements() + const [error, setError] = createSignal(null) + const [loading, setLoading] = createSignal(false) + + const handleSubmit = async (e: Event) => { + e.preventDefault() + if (!stripe() || !elements()) return + + setLoading(true) + setError(null) + + const result = await elements()!.submit() + if (result.error) { + setError(result.error.message ?? "An error occurred") + setLoading(false) + return + } + + const { error: confirmError } = await stripe()!.confirmSetup({ + elements: elements()!, + confirmParams: { + return_url: `${window.location.origin}/black/success?plan=${props.plan}`, + }, + }) + + if (confirmError) { + setError(confirmError.message ?? "An error occurred") + } + setLoading(false) + } + + return ( +
+ + +

{error()}

+
+ +

You will only be charged when your subscription is activated

+ + ) +} + +export default function BlackSubscribe() { + const workspaces = createAsync(() => getWorkspaces()) + const [selectedWorkspace, setSelectedWorkspace] = createSignal(null) + + const [params] = useSearchParams() + const plan = (params.plan as string) || "200" + const planData = plansMap[plan] || plansMap["200"] + + const [clientSecret, setClientSecret] = createSignal(null) + const [stripePromise] = createSignal(loadStripe(config.stripe.publishableKey)) + + // Auto-select if only one workspace + createEffect(() => { + const ws = workspaces() + if (ws?.length === 1 && !selectedWorkspace()) { + setSelectedWorkspace(ws[0].id) + } + }) + + // Keyboard navigation for workspace picker + const { active, setActive, onKeyDown } = createList({ + items: () => workspaces()?.map((w) => w.id) ?? [], + initialActive: null, + }) + + const handleSelectWorkspace = (id: string) => { + setSelectedWorkspace(id) + } + + onMount(async () => { + const response = await fetch("/api/black/setup-intent", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ plan }), + }) + const data = await response.json() + if (data.clientSecret) { + setClientSecret(data.clientSecret) + } + }) + + let listRef: HTMLUListElement | undefined + + // Show workspace picker if multiple workspaces and none selected + const showWorkspacePicker = () => { + const ws = workspaces() + return ws && ws.length > 1 && !selectedWorkspace() + } + + return ( + <> + Subscribe to OpenCode Black +
+
+
+

Subscribe to OpenCode Black

+
+ +
+

+ ${planData.amount} per month + + {planData.multiplier} + +

+
+
+

Add payment method

+ +

Loading payment form...

+
+ } + > + + + + +
+ + {/* Workspace picker modal */} + {}} title="Select a workspace for this plan"> +
+
    { + if (e.key === "Enter" && active()) { + handleSelectWorkspace(active()!) + } else { + onKeyDown(e) + } + }} + > + + {(workspace) => ( +
  • setActive(workspace.id)} + onClick={() => handleSelectWorkspace(workspace.id)} + > + [*] + {workspace.name || workspace.slug} +
  • + )} +
    +
+
+
+

+ Prices shown don't include applicable tax · Terms of Service +

+
+ + ) +} -- cgit v1.2.3 From 8ae10f1c9462a583b4de1d0c6170e260f95d3020 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 13 Jan 2026 13:37:48 -0500 Subject: sync --- packages/console/app/src/routes/auth/callback.ts | 2 -- 1 file changed, 2 deletions(-) (limited to 'packages/console/app/src') diff --git a/packages/console/app/src/routes/auth/callback.ts b/packages/console/app/src/routes/auth/callback.ts index b03bbdbe5..9b7296791 100644 --- a/packages/console/app/src/routes/auth/callback.ts +++ b/packages/console/app/src/routes/auth/callback.ts @@ -5,8 +5,6 @@ import { useAuthSession } from "~/context/auth" export async function GET(input: APIEvent) { const url = new URL(input.request.url) - console.log("=C=", input.request.url) - throw new Error("Not implemented") try { const code = url.searchParams.get("code") if (!code) throw new Error("No code found") -- cgit v1.2.3 From eaf18d991576771ccbc3975a6c7a9358b0da8de9 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 13 Jan 2026 17:51:20 -0500 Subject: wip: black --- infra/console.ts | 2 + packages/console/app/package.json | 2 +- packages/console/app/src/config.ts | 6 - .../app/src/routes/api/black/setup-intent.ts | 30 - .../console/app/src/routes/auth/[...callback].ts | 41 + packages/console/app/src/routes/auth/authorize.ts | 12 +- packages/console/app/src/routes/auth/callback.ts | 40 - packages/console/app/src/routes/black.css | 79 ++ packages/console/app/src/routes/black/common.tsx | 6 +- packages/console/app/src/routes/black/index.tsx | 2 +- .../console/app/src/routes/black/subscribe.tsx | 244 ---- .../app/src/routes/black/subscribe/[plan].tsx | 437 +++++++ .../core/migrations/0051_jazzy_green_goblin.sql | 1 + .../core/migrations/0052_aromatic_agent_zero.sql | 1 + .../core/migrations/meta/0051_snapshot.json | 1302 +++++++++++++++++++ .../core/migrations/meta/0052_snapshot.json | 1309 ++++++++++++++++++++ .../console/core/migrations/meta/_journal.json | 16 +- packages/console/core/src/schema/billing.sql.ts | 4 +- packages/console/core/sst-env.d.ts | 4 + packages/console/function/sst-env.d.ts | 4 + packages/console/resource/sst-env.d.ts | 4 + packages/enterprise/sst-env.d.ts | 4 + packages/function/sst-env.d.ts | 4 + sst-env.d.ts | 4 + 24 files changed, 3223 insertions(+), 335 deletions(-) delete mode 100644 packages/console/app/src/routes/api/black/setup-intent.ts create mode 100644 packages/console/app/src/routes/auth/[...callback].ts delete mode 100644 packages/console/app/src/routes/auth/callback.ts delete mode 100644 packages/console/app/src/routes/black/subscribe.tsx create mode 100644 packages/console/app/src/routes/black/subscribe/[plan].tsx create mode 100644 packages/console/core/migrations/0051_jazzy_green_goblin.sql create mode 100644 packages/console/core/migrations/0052_aromatic_agent_zero.sql create mode 100644 packages/console/core/migrations/meta/0051_snapshot.json create mode 100644 packages/console/core/migrations/meta/0052_snapshot.json (limited to 'packages/console/app/src') diff --git a/infra/console.ts b/infra/console.ts index 1368ef202..17e4deab6 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -122,6 +122,7 @@ const ZEN_MODELS = [ ] const ZEN_BLACK = new sst.Secret("ZEN_BLACK") const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY") +const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY") const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", { properties: { value: auth.url.apply((url) => url!) }, }) @@ -177,6 +178,7 @@ new sst.cloudflare.x.SolidStart("Console", { //VITE_DOCS_URL: web.url.apply((url) => url!), //VITE_API_URL: gateway.url.apply((url) => url!), VITE_AUTH_URL: auth.url.apply((url) => url!), + VITE_STRIPE_PUBLISHABLE_KEY: STRIPE_PUBLISHABLE_KEY.value, }, transform: { server: { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 23171daac..b70fedead 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -6,7 +6,7 @@ "scripts": { "typecheck": "tsgo --noEmit", "dev": "vite dev --host 0.0.0.0", - "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", + "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RtuLNE7fOCwHSD4mewwzFejyytjdGoSDK7CAvhbffwaZnPbNb2rwJICw6LTOXCmWO320fSNXvb5NzI08RZVkAxd00syfqrW7t bun sst shell --stage=dev bun dev", "build": "./script/generate-sitemap.ts && vite build && ../../opencode/script/schema.ts ./.output/public/config.json", "start": "vite start" }, diff --git a/packages/console/app/src/config.ts b/packages/console/app/src/config.ts index 4396e5117..4ebb2c71a 100644 --- a/packages/console/app/src/config.ts +++ b/packages/console/app/src/config.ts @@ -26,10 +26,4 @@ export const config = { commits: "6,500", monthlyUsers: "650,000", }, - - // Stripe - stripe: { - publishableKey: - "pk_live_51OhXSKEclFNgdHcR9dDfYGwQeKuPfKo0IjA5kWBQIXKMFhE8QFd9bYLdPZC6klRKEgEkxJYSKuZg9U3FKHdLnF4300F9qLqMgP", - }, } as const diff --git a/packages/console/app/src/routes/api/black/setup-intent.ts b/packages/console/app/src/routes/api/black/setup-intent.ts deleted file mode 100644 index eb5571616..000000000 --- a/packages/console/app/src/routes/api/black/setup-intent.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { Billing } from "@opencode-ai/console-core/billing.js" - -export async function POST(event: APIEvent) { - try { - const body = (await event.request.json()) as { plan: string } - const plan = body.plan - - if (!plan || !["20", "100", "200"].includes(plan)) { - return Response.json({ error: "Invalid plan" }, { status: 400 }) - } - - const amount = parseInt(plan) * 100 - - const intent = await Billing.stripe().setupIntents.create({ - payment_method_types: ["card"], - metadata: { - plan, - amount: amount.toString(), - }, - }) - - return Response.json({ - clientSecret: intent.client_secret, - }) - } catch (error) { - console.error("Error creating setup intent:", error) - return Response.json({ error: "Internal server error" }, { status: 500 }) - } -} diff --git a/packages/console/app/src/routes/auth/[...callback].ts b/packages/console/app/src/routes/auth/[...callback].ts new file mode 100644 index 000000000..36a9c5194 --- /dev/null +++ b/packages/console/app/src/routes/auth/[...callback].ts @@ -0,0 +1,41 @@ +import { redirect } from "@solidjs/router" +import type { APIEvent } from "@solidjs/start/server" +import { AuthClient } from "~/context/auth" +import { useAuthSession } from "~/context/auth" + +export async function GET(input: APIEvent) { + const url = new URL(input.request.url) + + try { + const code = url.searchParams.get("code") + if (!code) throw new Error("No code found") + const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`) + if (result.err) throw new Error(result.err.message) + const decoded = AuthClient.decode(result.tokens.access, {} as any) + if (decoded.err) throw new Error(decoded.err.message) + const session = await useAuthSession() + const id = decoded.subject.properties.accountID + await session.update((value) => { + return { + ...value, + account: { + ...value.account, + [id]: { + id, + email: decoded.subject.properties.email, + }, + }, + current: id, + } + }) + return redirect(url.pathname === "/auth/callback" ? "/auth" : url.pathname.replace("/auth/callback", "")) + } catch (e: any) { + return new Response( + JSON.stringify({ + error: e.message, + cause: Object.fromEntries(url.searchParams.entries()), + }), + { status: 500 }, + ) + } +} diff --git a/packages/console/app/src/routes/auth/authorize.ts b/packages/console/app/src/routes/auth/authorize.ts index 6be94b146..0f0651ae3 100644 --- a/packages/console/app/src/routes/auth/authorize.ts +++ b/packages/console/app/src/routes/auth/authorize.ts @@ -3,12 +3,8 @@ import { AuthClient } from "~/context/auth" export async function GET(input: APIEvent) { const url = new URL(input.request.url) - // TODO - // input.request.url http://localhost:3001/auth/authorize?continue=/black/subscribe - const result = await AuthClient.authorize( - new URL("/callback/subscribe?foo=bar", input.request.url).toString(), - "code", - ) - // result.url https://auth.frank.dev.opencode.ai/authorize?client_id=app&redirect_uri=http%3A%2F%2Flocalhost%3A3001%2Fauth%2Fcallback&response_type=code&state=0d3fc834-bcbc-42dc-83ab-c25c2c43c7e3 - return Response.redirect(result.url + "&continue=" + url.searchParams.get("continue"), 302) + const cont = url.searchParams.get("continue") ?? "" + const callbackUrl = new URL(`./callback${cont}`, input.request.url) + const result = await AuthClient.authorize(callbackUrl.toString(), "code") + return Response.redirect(result.url, 302) } diff --git a/packages/console/app/src/routes/auth/callback.ts b/packages/console/app/src/routes/auth/callback.ts deleted file mode 100644 index 9b7296791..000000000 --- a/packages/console/app/src/routes/auth/callback.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { redirect } from "@solidjs/router" -import type { APIEvent } from "@solidjs/start/server" -import { AuthClient } from "~/context/auth" -import { useAuthSession } from "~/context/auth" - -export async function GET(input: APIEvent) { - const url = new URL(input.request.url) - try { - const code = url.searchParams.get("code") - if (!code) throw new Error("No code found") - const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`) - if (result.err) throw new Error(result.err.message) - const decoded = AuthClient.decode(result.tokens.access, {} as any) - if (decoded.err) throw new Error(decoded.err.message) - const session = await useAuthSession() - const id = decoded.subject.properties.accountID - await session.update((value) => { - return { - ...value, - account: { - ...value.account, - [id]: { - id, - email: decoded.subject.properties.email, - }, - }, - current: id, - } - }) - return redirect("/auth") - } catch (e: any) { - return new Response( - JSON.stringify({ - error: e.message, - cause: Object.fromEntries(url.searchParams.entries()), - }), - { status: 500 }, - ) - } -} diff --git a/packages/console/app/src/routes/black.css b/packages/console/app/src/routes/black.css index dfb188ed0..72bf7a657 100644 --- a/packages/console/app/src/routes/black.css +++ b/packages/console/app/src/routes/black.css @@ -460,6 +460,39 @@ font-weight: 400; } + [data-slot="tax-id-section"] { + display: flex; + flex-direction: column; + gap: 8px; + + [data-slot="label"] { + color: rgba(255, 255, 255, 0.59); + font-size: 14px; + } + + [data-slot="input"] { + width: 100%; + height: 44px; + padding: 0 12px; + background: #1a1a1a; + border: 1px solid rgba(255, 255, 255, 0.17); + border-radius: 4px; + color: #ffffff; + font-family: var(--font-mono); + font-size: 14px; + outline: none; + transition: border-color 0.15s ease; + + &::placeholder { + color: rgba(255, 255, 255, 0.39); + } + + &:focus { + border-color: rgba(255, 255, 255, 0.35); + } + } + } + [data-slot="checkout-form"] { display: flex; flex-direction: column; @@ -500,6 +533,52 @@ text-align: center; } + [data-slot="success"] { + display: flex; + flex-direction: column; + gap: 24px; + + [data-slot="title"] { + color: rgba(255, 255, 255, 0.92); + font-size: 18px; + font-weight: 400; + margin: 0; + } + + [data-slot="details"] { + display: flex; + flex-direction: column; + gap: 16px; + + > div { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 16px; + } + + dt { + color: rgba(255, 255, 255, 0.59); + font-size: 14px; + font-weight: 400; + } + + dd { + color: rgba(255, 255, 255, 0.92); + font-size: 14px; + font-weight: 400; + margin: 0; + text-align: right; + } + } + + [data-slot="charge-notice"] { + color: #d4a500; + font-size: 14px; + text-align: left; + } + } + [data-slot="loading"] { display: flex; justify-content: center; diff --git a/packages/console/app/src/routes/black/common.tsx b/packages/console/app/src/routes/black/common.tsx index c1184bd20..dd643bbc5 100644 --- a/packages/console/app/src/routes/black/common.tsx +++ b/packages/console/app/src/routes/black/common.tsx @@ -1,9 +1,9 @@ import { Match, Switch } from "solid-js" export const plans = [ - { id: "20", amount: 20, multiplier: null }, - { id: "100", amount: 100, multiplier: "6x more usage than Black 20" }, - { id: "200", amount: 200, multiplier: "21x more usage than Black 20" }, + { id: "20", multiplier: null }, + { id: "100", multiplier: "6x more usage than Black 20" }, + { id: "200", multiplier: "21x more usage than Black 20" }, ] as const export type Plan = (typeof plans)[number] diff --git a/packages/console/app/src/routes/black/index.tsx b/packages/console/app/src/routes/black/index.tsx index 2b452c812..57d27a793 100644 --- a/packages/console/app/src/routes/black/index.tsx +++ b/packages/console/app/src/routes/black/index.tsx @@ -62,7 +62,7 @@ export default function Black() { - + Continue
diff --git a/packages/console/app/src/routes/black/subscribe.tsx b/packages/console/app/src/routes/black/subscribe.tsx deleted file mode 100644 index 00ce19ef6..000000000 --- a/packages/console/app/src/routes/black/subscribe.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import { A, createAsync, query, redirect, useSearchParams } from "@solidjs/router" -import { Title } from "@solidjs/meta" -import { createEffect, createSignal, For, onMount, Show } from "solid-js" -import { loadStripe } from "@stripe/stripe-js" -import { Elements, PaymentElement, useStripe, useElements } from "solid-stripe" -import { config } from "~/config" -import { PlanIcon, plans } from "./common" -import { getActor } from "~/context/auth" -import { withActor } from "~/context/auth.withActor" -import { Actor } from "@opencode-ai/console-core/actor.js" -import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js" -import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" -import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" -import { createList } from "solid-list" -import { Modal } from "~/component/modal" - -const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record - -const getWorkspaces = query(async () => { - "use server" - const actor = await getActor() - if (actor.type === "public") throw redirect("/auth/authorize?continue=/black/subscribe") - return withActor(async () => { - return Database.use((tx) => - tx - .select({ - id: WorkspaceTable.id, - name: WorkspaceTable.name, - slug: WorkspaceTable.slug, - }) - .from(UserTable) - .innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id)) - .where( - and( - eq(UserTable.accountID, Actor.account()), - isNull(WorkspaceTable.timeDeleted), - isNull(UserTable.timeDeleted), - ), - ), - ) - }) -}, "black.subscribe.workspaces") - -function CheckoutForm(props: { plan: string; amount: number }) { - const stripe = useStripe() - const elements = useElements() - const [error, setError] = createSignal(null) - const [loading, setLoading] = createSignal(false) - - const handleSubmit = async (e: Event) => { - e.preventDefault() - if (!stripe() || !elements()) return - - setLoading(true) - setError(null) - - const result = await elements()!.submit() - if (result.error) { - setError(result.error.message ?? "An error occurred") - setLoading(false) - return - } - - const { error: confirmError } = await stripe()!.confirmSetup({ - elements: elements()!, - confirmParams: { - return_url: `${window.location.origin}/black/success?plan=${props.plan}`, - }, - }) - - if (confirmError) { - setError(confirmError.message ?? "An error occurred") - } - setLoading(false) - } - - return ( -
- - -

{error()}

-
- -

You will only be charged when your subscription is activated

- - ) -} - -export default function BlackSubscribe() { - const workspaces = createAsync(() => getWorkspaces()) - const [selectedWorkspace, setSelectedWorkspace] = createSignal(null) - - const [params] = useSearchParams() - const plan = (params.plan as string) || "200" - const planData = plansMap[plan] || plansMap["200"] - - const [clientSecret, setClientSecret] = createSignal(null) - const [stripePromise] = createSignal(loadStripe(config.stripe.publishableKey)) - - // Auto-select if only one workspace - createEffect(() => { - const ws = workspaces() - if (ws?.length === 1 && !selectedWorkspace()) { - setSelectedWorkspace(ws[0].id) - } - }) - - // Keyboard navigation for workspace picker - const { active, setActive, onKeyDown } = createList({ - items: () => workspaces()?.map((w) => w.id) ?? [], - initialActive: null, - }) - - const handleSelectWorkspace = (id: string) => { - setSelectedWorkspace(id) - } - - onMount(async () => { - const response = await fetch("/api/black/setup-intent", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ plan }), - }) - const data = await response.json() - if (data.clientSecret) { - setClientSecret(data.clientSecret) - } - }) - - let listRef: HTMLUListElement | undefined - - // Show workspace picker if multiple workspaces and none selected - const showWorkspacePicker = () => { - const ws = workspaces() - return ws && ws.length > 1 && !selectedWorkspace() - } - - return ( - <> - Subscribe to OpenCode Black -
-
-
-

Subscribe to OpenCode Black

-
- -
-

- ${planData.amount} per month - - {planData.multiplier} - -

-
-
-

Add payment method

- -

Loading payment form...

-
- } - > - - - - -
- - {/* Workspace picker modal */} - {}} title="Select a workspace for this plan"> -
-
    { - if (e.key === "Enter" && active()) { - handleSelectWorkspace(active()!) - } else { - onKeyDown(e) - } - }} - > - - {(workspace) => ( -
  • setActive(workspace.id)} - onClick={() => handleSelectWorkspace(workspace.id)} - > - [*] - {workspace.name || workspace.slug} -
  • - )} -
    -
-
-
-

- Prices shown don't include applicable tax · Terms of Service -

-
- - ) -} diff --git a/packages/console/app/src/routes/black/subscribe/[plan].tsx b/packages/console/app/src/routes/black/subscribe/[plan].tsx new file mode 100644 index 000000000..e0992a1e2 --- /dev/null +++ b/packages/console/app/src/routes/black/subscribe/[plan].tsx @@ -0,0 +1,437 @@ +import { A, action, createAsync, query, redirect, useParams } from "@solidjs/router" +import { Title } from "@solidjs/meta" +import { createEffect, createSignal, For, Show } from "solid-js" +import { type Stripe, type PaymentMethod, loadStripe } from "@stripe/stripe-js" +import { Elements, PaymentElement, useStripe, useElements, AddressElement } from "solid-stripe" +import { PlanIcon, plans } from "../common" +import { getActor, useAuthSession } from "~/context/auth" +import { withActor } from "~/context/auth.withActor" +import { Actor } from "@opencode-ai/console-core/actor.js" +import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js" +import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" +import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" +import { createList } from "solid-list" +import { Modal } from "~/component/modal" +import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js" +import { Billing } from "@opencode-ai/console-core/billing.js" + +const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record +const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!) + +const getWorkspaces = query(async () => { + "use server" + const actor = await getActor() + if (actor.type === "public") throw redirect("/auth/authorize?continue=/black/subscribe") + return withActor(async () => { + return Database.use((tx) => + tx + .select({ + id: WorkspaceTable.id, + name: WorkspaceTable.name, + slug: WorkspaceTable.slug, + billing: { + customerID: BillingTable.customerID, + paymentMethodID: BillingTable.paymentMethodID, + paymentMethodType: BillingTable.paymentMethodType, + paymentMethodLast4: BillingTable.paymentMethodLast4, + }, + }) + .from(UserTable) + .innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id)) + .innerJoin(BillingTable, eq(WorkspaceTable.id, BillingTable.workspaceID)) + .where( + and( + eq(UserTable.accountID, Actor.account()), + isNull(WorkspaceTable.timeDeleted), + isNull(UserTable.timeDeleted), + ), + ), + ) + }) +}, "black.subscribe.workspaces") + +const createSetupIntent = action(async (input: { plan: string; workspaceID: string }) => { + "use server" + const { plan, workspaceID } = input + + if (!plan || !["20", "100", "200"].includes(plan)) { + return { error: "Invalid plan" } + } + + if (!workspaceID) { + return { error: "Workspace ID is required" } + } + + const actor = await getActor() + if (actor.type === "public") { + return { error: "Unauthorized" } + } + + const session = await useAuthSession() + const account = session.data.account?.[session.data.current ?? ""] + const email = account?.email + + const stripe = Billing.stripe() + + let customerID = await Database.use((tx) => + tx + .select({ customerID: BillingTable.customerID }) + .from(BillingTable) + .where(eq(BillingTable.workspaceID, workspaceID)) + .then((rows) => rows[0].customerID), + ) + if (!customerID) { + const customer = await stripe.customers.create({ + email, + metadata: { + workspaceID, + }, + }) + customerID = customer.id + } + + const intent = await stripe.setupIntents.create({ + customer: customerID, + payment_method_types: ["card"], + metadata: { + workspaceID, + }, + }) + + return { clientSecret: intent.client_secret } +}) + +const bookSubscription = action( + async (input: { + workspaceID: string + paymentMethodID: string + paymentMethodType: string + paymentMethodLast4?: string + }) => { + "use server" + const actor = await getActor() + if (actor.type === "public") { + return { error: "Unauthorized" } + } + + await Database.use((tx) => + tx + .update(BillingTable) + .set({ + paymentMethodID: input.paymentMethodID, + paymentMethodType: input.paymentMethodType, + paymentMethodLast4: input.paymentMethodLast4, + timeSubscriptionBooked: new Date(), + }) + .where(eq(BillingTable.workspaceID, input.workspaceID)), + ) + + return { success: true } + }, +) + +interface SuccessData { + plan: string + paymentMethodType: string + paymentMethodLast4?: string +} + +function PaymentSuccess(props: SuccessData) { + return ( +
+

You're on the OpenCode Black waitlist

+
+
+
Subscription plan
+
OpenCode Black {props.plan}
+
+
+
Amount
+
${props.plan} per month
+
+
+
Payment method
+
+ {props.paymentMethodType}}> + + {props.paymentMethodType} - {props.paymentMethodLast4} + + +
+
+
+
Date joined
+
{new Date().toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
+
+
+

Your card will be charged when your subscription is activated

+
+ ) +} + +function PaymentForm(props: { plan: string; workspaceID: string; onSuccess: (data: SuccessData) => void }) { + const stripe = useStripe() + const elements = useElements() + const [error, setError] = createSignal(null) + const [loading, setLoading] = createSignal(false) + + const handleSubmit = async (e: Event) => { + e.preventDefault() + if (!stripe() || !elements()) return + + setLoading(true) + setError(null) + + const result = await elements()!.submit() + if (result.error) { + setError(result.error.message ?? "An error occurred") + setLoading(false) + return + } + + const { error: confirmError, setupIntent } = await stripe()!.confirmSetup({ + elements: elements()!, + confirmParams: { + expand: ["setup_intent.payment_method"], + payment_method_data: { + allow_redisplay: "always", + }, + }, + redirect: "if_required", + }) + + if (confirmError) { + setError(confirmError.message ?? "An error occurred") + setLoading(false) + return + } + + if (setupIntent?.status === "succeeded") { + const pm = setupIntent.payment_method as PaymentMethod + + await bookSubscription({ + workspaceID: props.workspaceID, + paymentMethodID: pm.id, + paymentMethodType: pm.type, + paymentMethodLast4: pm.card?.last4, + }) + + props.onSuccess({ + plan: props.plan, + paymentMethodType: pm.type, + paymentMethodLast4: pm.card?.last4, + }) + } + + setLoading(false) + } + + return ( +
+ + + +

{error()}

+
+ +

You will only be charged when your subscription is activated

+ + ) +} + +export default function BlackSubscribe() { + const workspaces = createAsync(() => getWorkspaces()) + const [selectedWorkspace, setSelectedWorkspace] = createSignal(null) + const [success, setSuccess] = createSignal(null) + + const params = useParams() + const plan = params.plan || "200" + const planData = plansMap[plan] || plansMap["200"] + + const [clientSecret, setClientSecret] = createSignal(null) + const [setupError, setSetupError] = createSignal(null) + const [stripe, setStripe] = createSignal(null) + + // Resolve stripe promise once + createEffect(() => { + stripePromise.then((s) => { + if (s) setStripe(s) + }) + }) + + // Auto-select if only one workspace + createEffect(() => { + const ws = workspaces() + if (ws?.length === 1 && !selectedWorkspace()) { + setSelectedWorkspace(ws[0].id) + } + }) + + // Fetch setup intent when workspace is selected (unless workspace already has payment method) + createEffect(() => { + const id = selectedWorkspace() + if (!id) return + + const ws = workspaces()?.find((w) => w.id === id) + if (ws?.billing.paymentMethodID) { + setSuccess({ + plan, + paymentMethodType: ws.billing.paymentMethodType!, + paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined, + }) + return + } + + setClientSecret(null) + setSetupError(null) + + createSetupIntent({ plan, workspaceID: id }) + .then((data) => { + if (data.clientSecret) { + setClientSecret(data.clientSecret) + } else if (data.error) { + setSetupError(data.error) + } + }) + .catch(() => setSetupError("Failed to initialize payment")) + }) + + // Keyboard navigation for workspace picker + const { active, setActive, onKeyDown } = createList({ + items: () => workspaces()?.map((w) => w.id) ?? [], + initialActive: null, + }) + + const handleSelectWorkspace = (id: string) => { + setSelectedWorkspace(id) + } + + let listRef: HTMLUListElement | undefined + + // Show workspace picker if multiple workspaces and none selected + const showWorkspacePicker = () => { + const ws = workspaces() + return ws && ws.length > 1 && !selectedWorkspace() + } + + return ( + <> + Subscribe to OpenCode Black +
+
+ +
+

Subscribe to OpenCode Black

+
+ +
+

+ ${planData.id} per month + + {planData.multiplier} + +

+
+
+

Payment method

+ + +

{setupError()}

+
+ + +

{selectedWorkspace() ? "Loading payment form..." : "Select a workspace to continue"}

+
+ } + > + + + +
+ + } + > + {(data) => } + +
+ + {/* Workspace picker modal */} + {}} title="Select a workspace for this plan"> +
+
    { + if (e.key === "Enter" && active()) { + handleSelectWorkspace(active()!) + } else { + onKeyDown(e) + } + }} + > + + {(workspace) => ( +
  • setActive(workspace.id)} + onClick={() => handleSelectWorkspace(workspace.id)} + > + [*] + {workspace.name || workspace.slug} +
  • + )} +
    +
+
+
+

+ Prices shown don't include applicable tax · Terms of Service +

+
+ + ) +} diff --git a/packages/console/core/migrations/0051_jazzy_green_goblin.sql b/packages/console/core/migrations/0051_jazzy_green_goblin.sql new file mode 100644 index 000000000..cadb4a709 --- /dev/null +++ b/packages/console/core/migrations/0051_jazzy_green_goblin.sql @@ -0,0 +1 @@ +ALTER TABLE `billing` ADD `time_subscription_booked` timestamp(3); \ No newline at end of file diff --git a/packages/console/core/migrations/0052_aromatic_agent_zero.sql b/packages/console/core/migrations/0052_aromatic_agent_zero.sql new file mode 100644 index 000000000..c53ba5e2b --- /dev/null +++ b/packages/console/core/migrations/0052_aromatic_agent_zero.sql @@ -0,0 +1 @@ +ALTER TABLE `billing` ADD `subscription_plan` enum('20','100','200'); \ No newline at end of file diff --git a/packages/console/core/migrations/meta/0051_snapshot.json b/packages/console/core/migrations/meta/0051_snapshot.json new file mode 100644 index 000000000..38002a288 --- /dev/null +++ b/packages/console/core/migrations/meta/0051_snapshot.json @@ -0,0 +1,1302 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "14cbf4c8-55f1-4488-956f-56fb5ccb3a5a", + "prevId": "a0d18802-c390-47d4-98f1-d1f83869cf0c", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "account_id_pk": { + "name": "account_id_pk", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "auth": { + "name": "auth", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "enum('email','github','google')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "provider": { + "name": "provider", + "columns": [ + "provider", + "subject" + ], + "isUnique": true + }, + "account_id": { + "name": "account_id", + "columns": [ + "account_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "auth_id_pk": { + "name": "auth_id_pk", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "benchmark": { + "name": "benchmark", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent": { + "name": "agent", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result": { + "name": "result", + "type": "mediumtext", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "time_created": { + "name": "time_created", + "columns": [ + "time_created" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "benchmark_id_pk": { + "name": "benchmark_id_pk", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "billing": { + "name": "billing", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_id": { + "name": "payment_method_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_type": { + "name": "payment_method_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_last4": { + "name": "payment_method_last4", + "type": "varchar(4)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "balance": { + "name": "balance", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monthly_limit": { + "name": "monthly_limit", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_usage": { + "name": "monthly_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_monthly_usage_updated": { + "name": "time_monthly_usage_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload": { + "name": "reload", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload_trigger": { + "name": "reload_trigger", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload_amount": { + "name": "reload_amount", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload_error": { + "name": "reload_error", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_reload_error": { + "name": "time_reload_error", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_reload_locked_till": { + "name": "time_reload_locked_till", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "varchar(28)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_coupon_id": { + "name": "subscription_coupon_id", + "type": "varchar(28)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_subscription_booked": { + "name": "time_subscription_booked", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_customer_id": { + "name": "global_customer_id", + "columns": [ + "customer_id" + ], + "isUnique": true + }, + "global_subscription_id": { + "name": "global_subscription_id", + "columns": [ + "subscription_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "billing_workspace_id_id_pk": { + "name": "billing_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "payment": { + "name": "payment", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoice_id": { + "name": "invoice_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_id": { + "name": "payment_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_refunded": { + "name": "time_refunded", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enrichment": { + "name": "enrichment", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "payment_workspace_id_id_pk": { + "name": "payment_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "subscription": { + "name": "subscription", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rolling_usage": { + "name": "rolling_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fixed_usage": { + "name": "fixed_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_rolling_updated": { + "name": "time_rolling_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_fixed_updated": { + "name": "time_fixed_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "workspace_user_id": { + "name": "workspace_user_id", + "columns": [ + "workspace_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "subscription_workspace_id_id_pk": { + "name": "subscription_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "usage": { + "name": "usage", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_5m_tokens": { + "name": "cache_write_5m_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_1h_tokens": { + "name": "cache_write_1h_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_id": { + "name": "key_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enrichment": { + "name": "enrichment", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "usage_time_created": { + "name": "usage_time_created", + "columns": [ + "workspace_id", + "time_created" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_workspace_id_id_pk": { + "name": "usage_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ip_rate_limit": { + "name": "ip_rate_limit", + "columns": { + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval": { + "name": "interval", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "ip_rate_limit_ip_interval_pk": { + "name": "ip_rate_limit_ip_interval_pk", + "columns": [ + "ip", + "interval" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ip": { + "name": "ip", + "columns": { + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage": { + "name": "usage", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "ip_ip_pk": { + "name": "ip_ip_pk", + "columns": [ + "ip" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "key": { + "name": "key", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_used": { + "name": "time_used", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_key": { + "name": "global_key", + "columns": [ + "key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "key_workspace_id_id_pk": { + "name": "key_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "model": { + "name": "model", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "model_workspace_model": { + "name": "model_workspace_model", + "columns": [ + "workspace_id", + "model" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_workspace_id_id_pk": { + "name": "model_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "provider": { + "name": "provider", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentials": { + "name": "credentials", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "workspace_provider": { + "name": "workspace_provider", + "columns": [ + "workspace_id", + "provider" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "provider_workspace_id_id_pk": { + "name": "provider_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_seen": { + "name": "time_seen", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('admin','member')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monthly_limit": { + "name": "monthly_limit", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_usage": { + "name": "monthly_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_monthly_usage_updated": { + "name": "time_monthly_usage_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_account_id": { + "name": "user_account_id", + "columns": [ + "workspace_id", + "account_id" + ], + "isUnique": true + }, + "user_email": { + "name": "user_email", + "columns": [ + "workspace_id", + "email" + ], + "isUnique": true + }, + "global_account_id": { + "name": "global_account_id", + "columns": [ + "account_id" + ], + "isUnique": false + }, + "global_email": { + "name": "global_email", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_workspace_id_id_pk": { + "name": "user_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "workspace": { + "name": "workspace", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "slug": { + "name": "slug", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "workspace_id": { + "name": "workspace_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/console/core/migrations/meta/0052_snapshot.json b/packages/console/core/migrations/meta/0052_snapshot.json new file mode 100644 index 000000000..b5e1b4b8c --- /dev/null +++ b/packages/console/core/migrations/meta/0052_snapshot.json @@ -0,0 +1,1309 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "00774acd-a1e5-49c0-b296-cacc9506a566", + "prevId": "14cbf4c8-55f1-4488-956f-56fb5ccb3a5a", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "account_id_pk": { + "name": "account_id_pk", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "auth": { + "name": "auth", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "enum('email','github','google')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "provider": { + "name": "provider", + "columns": [ + "provider", + "subject" + ], + "isUnique": true + }, + "account_id": { + "name": "account_id", + "columns": [ + "account_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "auth_id_pk": { + "name": "auth_id_pk", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "benchmark": { + "name": "benchmark", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent": { + "name": "agent", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result": { + "name": "result", + "type": "mediumtext", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "time_created": { + "name": "time_created", + "columns": [ + "time_created" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "benchmark_id_pk": { + "name": "benchmark_id_pk", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "billing": { + "name": "billing", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_id": { + "name": "payment_method_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_type": { + "name": "payment_method_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_last4": { + "name": "payment_method_last4", + "type": "varchar(4)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "balance": { + "name": "balance", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monthly_limit": { + "name": "monthly_limit", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_usage": { + "name": "monthly_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_monthly_usage_updated": { + "name": "time_monthly_usage_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload": { + "name": "reload", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload_trigger": { + "name": "reload_trigger", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload_amount": { + "name": "reload_amount", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload_error": { + "name": "reload_error", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_reload_error": { + "name": "time_reload_error", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_reload_locked_till": { + "name": "time_reload_locked_till", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "varchar(28)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_coupon_id": { + "name": "subscription_coupon_id", + "type": "varchar(28)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_plan": { + "name": "subscription_plan", + "type": "enum('20','100','200')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_subscription_booked": { + "name": "time_subscription_booked", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_customer_id": { + "name": "global_customer_id", + "columns": [ + "customer_id" + ], + "isUnique": true + }, + "global_subscription_id": { + "name": "global_subscription_id", + "columns": [ + "subscription_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "billing_workspace_id_id_pk": { + "name": "billing_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "payment": { + "name": "payment", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoice_id": { + "name": "invoice_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_id": { + "name": "payment_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_refunded": { + "name": "time_refunded", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enrichment": { + "name": "enrichment", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "payment_workspace_id_id_pk": { + "name": "payment_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "subscription": { + "name": "subscription", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rolling_usage": { + "name": "rolling_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fixed_usage": { + "name": "fixed_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_rolling_updated": { + "name": "time_rolling_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_fixed_updated": { + "name": "time_fixed_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "workspace_user_id": { + "name": "workspace_user_id", + "columns": [ + "workspace_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "subscription_workspace_id_id_pk": { + "name": "subscription_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "usage": { + "name": "usage", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_5m_tokens": { + "name": "cache_write_5m_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_1h_tokens": { + "name": "cache_write_1h_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_id": { + "name": "key_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enrichment": { + "name": "enrichment", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "usage_time_created": { + "name": "usage_time_created", + "columns": [ + "workspace_id", + "time_created" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_workspace_id_id_pk": { + "name": "usage_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ip_rate_limit": { + "name": "ip_rate_limit", + "columns": { + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval": { + "name": "interval", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "ip_rate_limit_ip_interval_pk": { + "name": "ip_rate_limit_ip_interval_pk", + "columns": [ + "ip", + "interval" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ip": { + "name": "ip", + "columns": { + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage": { + "name": "usage", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "ip_ip_pk": { + "name": "ip_ip_pk", + "columns": [ + "ip" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "key": { + "name": "key", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_used": { + "name": "time_used", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_key": { + "name": "global_key", + "columns": [ + "key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "key_workspace_id_id_pk": { + "name": "key_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "model": { + "name": "model", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "model_workspace_model": { + "name": "model_workspace_model", + "columns": [ + "workspace_id", + "model" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_workspace_id_id_pk": { + "name": "model_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "provider": { + "name": "provider", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentials": { + "name": "credentials", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "workspace_provider": { + "name": "workspace_provider", + "columns": [ + "workspace_id", + "provider" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "provider_workspace_id_id_pk": { + "name": "provider_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_seen": { + "name": "time_seen", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('admin','member')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monthly_limit": { + "name": "monthly_limit", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_usage": { + "name": "monthly_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_monthly_usage_updated": { + "name": "time_monthly_usage_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_account_id": { + "name": "user_account_id", + "columns": [ + "workspace_id", + "account_id" + ], + "isUnique": true + }, + "user_email": { + "name": "user_email", + "columns": [ + "workspace_id", + "email" + ], + "isUnique": true + }, + "global_account_id": { + "name": "global_account_id", + "columns": [ + "account_id" + ], + "isUnique": false + }, + "global_email": { + "name": "global_email", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_workspace_id_id_pk": { + "name": "user_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "workspace": { + "name": "workspace", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "slug": { + "name": "slug", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "workspace_id": { + "name": "workspace_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/console/core/migrations/meta/_journal.json b/packages/console/core/migrations/meta/_journal.json index 9982daef5..019817f91 100644 --- a/packages/console/core/migrations/meta/_journal.json +++ b/packages/console/core/migrations/meta/_journal.json @@ -358,6 +358,20 @@ "when": 1767931290031, "tag": "0050_bumpy_mephistopheles", "breakpoints": true + }, + { + "idx": 51, + "version": "5", + "when": 1768341152722, + "tag": "0051_jazzy_green_goblin", + "breakpoints": true + }, + { + "idx": 52, + "version": "5", + "when": 1768343920467, + "tag": "0052_aromatic_agent_zero", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/console/core/src/schema/billing.sql.ts b/packages/console/core/src/schema/billing.sql.ts index 6c2cfcf96..f1300f849 100644 --- a/packages/console/core/src/schema/billing.sql.ts +++ b/packages/console/core/src/schema/billing.sql.ts @@ -1,4 +1,4 @@ -import { bigint, boolean, index, int, json, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core" +import { bigint, boolean, index, int, json, mysqlEnum, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core" import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types" import { workspaceIndexes } from "./workspace.sql" @@ -23,6 +23,8 @@ export const BillingTable = mysqlTable( timeReloadLockedTill: utc("time_reload_locked_till"), subscriptionID: varchar("subscription_id", { length: 28 }), subscriptionCouponID: varchar("subscription_coupon_id", { length: 28 }), + subscriptionPlan: mysqlEnum("subscription_plan", ["20", "100", "200"] as const), + timeSubscriptionBooked: utc("time_subscription_booked"), }, (table) => [ ...workspaceIndexes(table), diff --git a/packages/console/core/sst-env.d.ts b/packages/console/core/sst-env.d.ts index 96fada3e3..b8e50a261 100644 --- a/packages/console/core/sst-env.d.ts +++ b/packages/console/core/sst-env.d.ts @@ -78,6 +78,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "STRIPE_PUBLISHABLE_KEY": { + "type": "sst.sst.Secret" + "value": string + } "STRIPE_SECRET_KEY": { "type": "sst.sst.Secret" "value": string diff --git a/packages/console/function/sst-env.d.ts b/packages/console/function/sst-env.d.ts index 96fada3e3..b8e50a261 100644 --- a/packages/console/function/sst-env.d.ts +++ b/packages/console/function/sst-env.d.ts @@ -78,6 +78,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "STRIPE_PUBLISHABLE_KEY": { + "type": "sst.sst.Secret" + "value": string + } "STRIPE_SECRET_KEY": { "type": "sst.sst.Secret" "value": string diff --git a/packages/console/resource/sst-env.d.ts b/packages/console/resource/sst-env.d.ts index 96fada3e3..b8e50a261 100644 --- a/packages/console/resource/sst-env.d.ts +++ b/packages/console/resource/sst-env.d.ts @@ -78,6 +78,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "STRIPE_PUBLISHABLE_KEY": { + "type": "sst.sst.Secret" + "value": string + } "STRIPE_SECRET_KEY": { "type": "sst.sst.Secret" "value": string diff --git a/packages/enterprise/sst-env.d.ts b/packages/enterprise/sst-env.d.ts index 96fada3e3..b8e50a261 100644 --- a/packages/enterprise/sst-env.d.ts +++ b/packages/enterprise/sst-env.d.ts @@ -78,6 +78,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "STRIPE_PUBLISHABLE_KEY": { + "type": "sst.sst.Secret" + "value": string + } "STRIPE_SECRET_KEY": { "type": "sst.sst.Secret" "value": string diff --git a/packages/function/sst-env.d.ts b/packages/function/sst-env.d.ts index 96fada3e3..b8e50a261 100644 --- a/packages/function/sst-env.d.ts +++ b/packages/function/sst-env.d.ts @@ -78,6 +78,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "STRIPE_PUBLISHABLE_KEY": { + "type": "sst.sst.Secret" + "value": string + } "STRIPE_SECRET_KEY": { "type": "sst.sst.Secret" "value": string diff --git a/sst-env.d.ts b/sst-env.d.ts index 035a5fc21..3160fc165 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -104,6 +104,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "STRIPE_PUBLISHABLE_KEY": { + "type": "sst.sst.Secret" + "value": string + } "STRIPE_SECRET_KEY": { "type": "sst.sst.Secret" "value": string -- cgit v1.2.3 From 45fa4eda15f3a066618357e0832b290405a395ca Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 13 Jan 2026 19:15:14 -0500 Subject: wip: black --- packages/console/app/src/routes/black/common.tsx | 1 + packages/console/app/src/routes/black/index.tsx | 4 +- .../app/src/routes/black/subscribe/[plan].tsx | 211 ++++++++++----------- 3 files changed, 107 insertions(+), 109 deletions(-) (limited to 'packages/console/app/src') diff --git a/packages/console/app/src/routes/black/common.tsx b/packages/console/app/src/routes/black/common.tsx index dd643bbc5..950531da1 100644 --- a/packages/console/app/src/routes/black/common.tsx +++ b/packages/console/app/src/routes/black/common.tsx @@ -6,6 +6,7 @@ export const plans = [ { id: "200", multiplier: "21x more usage than Black 20" }, ] as const +export type PlanID = (typeof plans)[number]["id"] export type Plan = (typeof plans)[number] export function PlanIcon(props: { plan: string }) { diff --git a/packages/console/app/src/routes/black/index.tsx b/packages/console/app/src/routes/black/index.tsx index 57d27a793..5d924a64b 100644 --- a/packages/console/app/src/routes/black/index.tsx +++ b/packages/console/app/src/routes/black/index.tsx @@ -22,7 +22,7 @@ export default function Black() {

- ${plan.amount} per month + ${plan.id} per month {plan.multiplier} @@ -43,7 +43,7 @@ export default function Black() {

- ${plan().amount}{" "} + ${plan().id}{" "} per person billed monthly {plan().multiplier} diff --git a/packages/console/app/src/routes/black/subscribe/[plan].tsx b/packages/console/app/src/routes/black/subscribe/[plan].tsx index e0992a1e2..b78d9138d 100644 --- a/packages/console/app/src/routes/black/subscribe/[plan].tsx +++ b/packages/console/app/src/routes/black/subscribe/[plan].tsx @@ -1,9 +1,9 @@ -import { A, action, createAsync, query, redirect, useParams } from "@solidjs/router" +import { A, action, createAsync, json, query, redirect, useParams } from "@solidjs/router" import { Title } from "@solidjs/meta" -import { createEffect, createSignal, For, Show } from "solid-js" +import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js" import { type Stripe, type PaymentMethod, loadStripe } from "@stripe/stripe-js" import { Elements, PaymentElement, useStripe, useElements, AddressElement } from "solid-stripe" -import { PlanIcon, plans } from "../common" +import { PlanID, plans } from "../common" import { getActor, useAuthSession } from "~/context/auth" import { withActor } from "~/context/auth.withActor" import { Actor } from "@opencode-ai/console-core/actor.js" @@ -15,7 +15,7 @@ import { Modal } from "~/component/modal" import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js" import { Billing } from "@opencode-ai/console-core/billing.js" -const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record +const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!) const getWorkspaces = query(async () => { @@ -34,6 +34,7 @@ const getWorkspaces = query(async () => { paymentMethodID: BillingTable.paymentMethodID, paymentMethodType: BillingTable.paymentMethodType, paymentMethodLast4: BillingTable.paymentMethodLast4, + subscriptionID: BillingTable.subscriptionID, }, }) .from(UserTable) @@ -50,85 +51,80 @@ const getWorkspaces = query(async () => { }) }, "black.subscribe.workspaces") -const createSetupIntent = action(async (input: { plan: string; workspaceID: string }) => { +const createSetupIntent = async (input: { plan: string; workspaceID: string }) => { "use server" const { plan, workspaceID } = input - if (!plan || !["20", "100", "200"].includes(plan)) { - return { error: "Invalid plan" } - } - - if (!workspaceID) { - return { error: "Workspace ID is required" } - } + if (!plan || !["20", "100", "200"].includes(plan)) return { error: "Invalid plan" } + if (!workspaceID) return { error: "Workspace ID is required" } - const actor = await getActor() - if (actor.type === "public") { - return { error: "Unauthorized" } - } + return withActor(async () => { + const session = await useAuthSession() + const account = session.data.account?.[session.data.current ?? ""] + const email = account?.email - const session = await useAuthSession() - const account = session.data.account?.[session.data.current ?? ""] - const email = account?.email + const customer = await Database.use((tx) => + tx + .select({ + customerID: BillingTable.customerID, + subscriptionID: BillingTable.subscriptionID, + }) + .from(BillingTable) + .where(eq(BillingTable.workspaceID, workspaceID)) + .then((rows) => rows[0]), + ) + if (customer?.subscriptionID) { + return { error: "This workspace already has a subscription" } + } - const stripe = Billing.stripe() + let customerID = customer?.customerID + if (!customerID) { + const customer = await Billing.stripe().customers.create({ + email, + metadata: { + workspaceID, + }, + }) + customerID = customer.id + } - let customerID = await Database.use((tx) => - tx - .select({ customerID: BillingTable.customerID }) - .from(BillingTable) - .where(eq(BillingTable.workspaceID, workspaceID)) - .then((rows) => rows[0].customerID), - ) - if (!customerID) { - const customer = await stripe.customers.create({ - email, + const intent = await Billing.stripe().setupIntents.create({ + customer: customerID, + payment_method_types: ["card"], metadata: { workspaceID, }, }) - customerID = customer.id - } - const intent = await stripe.setupIntents.create({ - customer: customerID, - payment_method_types: ["card"], - metadata: { - workspaceID, - }, - }) - - return { clientSecret: intent.client_secret } -}) - -const bookSubscription = action( - async (input: { - workspaceID: string - paymentMethodID: string - paymentMethodType: string - paymentMethodLast4?: string - }) => { - "use server" - const actor = await getActor() - if (actor.type === "public") { - return { error: "Unauthorized" } - } - - await Database.use((tx) => - tx - .update(BillingTable) - .set({ - paymentMethodID: input.paymentMethodID, - paymentMethodType: input.paymentMethodType, - paymentMethodLast4: input.paymentMethodLast4, - timeSubscriptionBooked: new Date(), - }) - .where(eq(BillingTable.workspaceID, input.workspaceID)), - ) + return { clientSecret: intent.client_secret ?? undefined } + }, workspaceID) +} - return { success: true } - }, -) +const bookSubscription = async (input: { + workspaceID: string + plan: PlanID + paymentMethodID: string + paymentMethodType: string + paymentMethodLast4?: string +}) => { + "use server" + return withActor( + () => + Database.use((tx) => + tx + .update(BillingTable) + .set({ + paymentMethodID: input.paymentMethodID, + paymentMethodType: input.paymentMethodType, + paymentMethodLast4: input.paymentMethodLast4, + subscriptionPlan: input.plan, + timeSubscriptionBooked: new Date(), + }) + .where(eq(BillingTable.workspaceID, input.workspaceID)), + ), + input.workspaceID, + ) +} interface SuccessData { plan: string @@ -136,7 +132,16 @@ interface SuccessData { paymentMethodLast4?: string } -function PaymentSuccess(props: SuccessData) { +function Failure(props: { message: string }) { + return ( +

+

Uh oh, something went wrong

+

{props.message}

+
+ ) +} + +function Success(props: SuccessData) { return (

You're on the OpenCode Black waitlist

@@ -169,10 +174,10 @@ function PaymentSuccess(props: SuccessData) { ) } -function PaymentForm(props: { plan: string; workspaceID: string; onSuccess: (data: SuccessData) => void }) { +function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data: SuccessData) => void }) { const stripe = useStripe() const elements = useElements() - const [error, setError] = createSignal(null) + const [error, setError] = createSignal(undefined) const [loading, setLoading] = createSignal(false) const handleSubmit = async (e: Event) => { @@ -180,7 +185,7 @@ function PaymentForm(props: { plan: string; workspaceID: string; onSuccess: (dat if (!stripe() || !elements()) return setLoading(true) - setError(null) + setError(undefined) const result = await elements()!.submit() if (result.error) { @@ -211,6 +216,7 @@ function PaymentForm(props: { plan: string; workspaceID: string; onSuccess: (dat await bookSubscription({ workspaceID: props.workspaceID, + plan: props.plan, paymentMethodID: pm.id, paymentMethodType: pm.type, paymentMethodLast4: pm.card?.last4, @@ -243,16 +249,14 @@ function PaymentForm(props: { plan: string; workspaceID: string; onSuccess: (dat export default function BlackSubscribe() { const workspaces = createAsync(() => getWorkspaces()) - const [selectedWorkspace, setSelectedWorkspace] = createSignal(null) - const [success, setSuccess] = createSignal(null) - + const [selectedWorkspace, setSelectedWorkspace] = createSignal(undefined) + const [success, setSuccess] = createSignal(undefined) + const [failure, setFailure] = createSignal(undefined) + const [clientSecret, setClientSecret] = createSignal(undefined) + const [stripe, setStripe] = createSignal(undefined) const params = useParams() - const plan = params.plan || "200" - const planData = plansMap[plan] || plansMap["200"] - - const [clientSecret, setClientSecret] = createSignal(null) - const [setupError, setSetupError] = createSignal(null) - const [stripe, setStripe] = createSignal(null) + const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"] + const plan = planData.id // Resolve stripe promise once createEffect(() => { @@ -275,27 +279,28 @@ export default function BlackSubscribe() { if (!id) return const ws = workspaces()?.find((w) => w.id === id) - if (ws?.billing.paymentMethodID) { + if (ws?.billing?.subscriptionID) { + setFailure("This workspace already has a subscription") + return + } + if (ws?.billing?.paymentMethodID) { setSuccess({ - plan, + plan: planData.id, paymentMethodType: ws.billing.paymentMethodType!, paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined, }) return } - setClientSecret(null) - setSetupError(null) - createSetupIntent({ plan, workspaceID: id }) .then((data) => { - if (data.clientSecret) { + if (data.error) { + setFailure(data.error) + } else if ("clientSecret" in data) { setClientSecret(data.clientSecret) - } else if (data.error) { - setSetupError(data.error) } }) - .catch(() => setSetupError("Failed to initialize payment")) + .catch(() => setFailure("Failed to initialize payment")) }) // Keyboard navigation for workspace picker @@ -321,15 +326,13 @@ export default function BlackSubscribe() { Subscribe to OpenCode Black
- + {(data) => } + {(data) => } + <>

Subscribe to OpenCode Black

-
- -

${planData.id} per month @@ -340,10 +343,6 @@ export default function BlackSubscribe() {

Payment method

- -

{setupError()}

-
- - + - } - > - {(data) => } - + +
{/* Workspace picker modal */} -- cgit v1.2.3 From 82319bbd836762ac485f911409267d004b1599b9 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 13 Jan 2026 19:46:14 -0500 Subject: wip: black --- .../app/src/routes/black/subscribe/[plan].tsx | 44 +++++++++++++++------- 1 file changed, 30 insertions(+), 14 deletions(-) (limited to 'packages/console/app/src') diff --git a/packages/console/app/src/routes/black/subscribe/[plan].tsx b/packages/console/app/src/routes/black/subscribe/[plan].tsx index b78d9138d..5e13799dd 100644 --- a/packages/console/app/src/routes/black/subscribe/[plan].tsx +++ b/packages/console/app/src/routes/black/subscribe/[plan].tsx @@ -1,4 +1,4 @@ -import { A, action, createAsync, json, query, redirect, useParams } from "@solidjs/router" +import { A, createAsync, query, redirect, useParams } from "@solidjs/router" import { Title } from "@solidjs/meta" import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js" import { type Stripe, type PaymentMethod, loadStripe } from "@stripe/stripe-js" @@ -35,6 +35,7 @@ const getWorkspaces = query(async () => { paymentMethodType: BillingTable.paymentMethodType, paymentMethodLast4: BillingTable.paymentMethodLast4, subscriptionID: BillingTable.subscriptionID, + timeSubscriptionBooked: BillingTable.timeSubscriptionBooked, }, }) .from(UserTable) @@ -86,6 +87,14 @@ const createSetupIntent = async (input: { plan: string; workspaceID: string }) = }, }) customerID = customer.id + await Database.use((tx) => + tx + .update(BillingTable) + .set({ + customerID, + }) + .where(eq(BillingTable.workspaceID, workspaceID)), + ) } const intent = await Billing.stripe().setupIntents.create({ @@ -135,8 +144,7 @@ interface SuccessData { function Failure(props: { message: string }) { return (
-

Uh oh, something went wrong

-

{props.message}

+

Uh oh! {props.message}

) } @@ -197,7 +205,7 @@ function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data const { error: confirmError, setupIntent } = await stripe()!.confirmSetup({ elements: elements()!, confirmParams: { - expand: ["setup_intent.payment_method"], + expand: ["payment_method"], payment_method_data: { allow_redisplay: "always", }, @@ -211,6 +219,8 @@ function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data return } + // TODO + console.log(setupIntent) if (setupIntent?.status === "succeeded") { const pm = setupIntent.payment_method as PaymentMethod @@ -274,7 +284,7 @@ export default function BlackSubscribe() { }) // Fetch setup intent when workspace is selected (unless workspace already has payment method) - createEffect(() => { + createEffect(async () => { const id = selectedWorkspace() if (!id) return @@ -284,6 +294,15 @@ export default function BlackSubscribe() { return } if (ws?.billing?.paymentMethodID) { + if (!ws?.billing?.timeSubscriptionBooked) { + await bookSubscription({ + workspaceID: id, + plan: planData.id, + paymentMethodID: ws.billing.paymentMethodID!, + paymentMethodType: ws.billing.paymentMethodType!, + paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined, + }) + } setSuccess({ plan: planData.id, paymentMethodType: ws.billing.paymentMethodType!, @@ -292,15 +311,12 @@ export default function BlackSubscribe() { return } - createSetupIntent({ plan, workspaceID: id }) - .then((data) => { - if (data.error) { - setFailure(data.error) - } else if ("clientSecret" in data) { - setClientSecret(data.clientSecret) - } - }) - .catch(() => setFailure("Failed to initialize payment")) + const result = await createSetupIntent({ plan, workspaceID: id }) + if (result.error) { + setFailure(result.error) + } else if ("clientSecret" in result) { + setClientSecret(result.clientSecret) + } }) // Keyboard navigation for workspace picker -- cgit v1.2.3 From bc557e828d150b02e54776e47f36d2b684da2f9f Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 13 Jan 2026 23:51:16 +0800 Subject: console: reduce desktop download cache ttl to 5 minutes --- packages/console/app/src/routes/download/[platform].ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/console/app/src') diff --git a/packages/console/app/src/routes/download/[platform].ts b/packages/console/app/src/routes/download/[platform].ts index d8b72aba1..2c30a8036 100644 --- a/packages/console/app/src/routes/download/[platform].ts +++ b/packages/console/app/src/routes/download/[platform].ts @@ -24,7 +24,7 @@ export async function GET({ params: { platform } }: APIEvent) { const resp = await fetch(`https://github.com/anomalyco/opencode/releases/latest/download/${assetName}`, { cf: { // in case gh releases has rate limits - cacheTtl: 60 * 60 * 24, + cacheTtl: 60 * 5, cacheEverything: true, }, } as any) -- cgit v1.2.3