mirror of
https://github.com/sst/opencode.git
synced 2025-08-12 01:08:06 +00:00
wip: gateway
This commit is contained in:
parent
c7bb19ad07
commit
183e0911b7
101 changed files with 9218 additions and 57 deletions
20
cloud/core/drizzle.config.ts
Normal file
20
cloud/core/drizzle.config.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { defineConfig } from "drizzle-kit"
|
||||||
|
import { Resource } from "sst"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
out: "./migrations/",
|
||||||
|
strict: true,
|
||||||
|
schema: ["./src/**/*.sql.ts"],
|
||||||
|
verbose: true,
|
||||||
|
dialect: "postgresql",
|
||||||
|
dbCredentials: {
|
||||||
|
database: Resource.Database.database,
|
||||||
|
host: Resource.Database.host,
|
||||||
|
user: Resource.Database.username,
|
||||||
|
password: Resource.Database.password,
|
||||||
|
port: Resource.Database.port,
|
||||||
|
ssl: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
66
cloud/core/migrations/0000_amused_mojo.sql
Normal file
66
cloud/core/migrations/0000_amused_mojo.sql
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
CREATE TABLE "billing" (
|
||||||
|
"id" varchar(30) NOT NULL,
|
||||||
|
"workspace_id" varchar(30) NOT NULL,
|
||||||
|
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_deleted" timestamp with time zone,
|
||||||
|
"customer_id" varchar(255),
|
||||||
|
"payment_method_id" varchar(255),
|
||||||
|
"payment_method_last4" varchar(4),
|
||||||
|
"balance" bigint NOT NULL,
|
||||||
|
"reload" boolean,
|
||||||
|
CONSTRAINT "billing_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "payment" (
|
||||||
|
"id" varchar(30) NOT NULL,
|
||||||
|
"workspace_id" varchar(30) NOT NULL,
|
||||||
|
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_deleted" timestamp with time zone,
|
||||||
|
"customer_id" varchar(255),
|
||||||
|
"payment_id" varchar(255),
|
||||||
|
"amount" bigint NOT NULL,
|
||||||
|
CONSTRAINT "payment_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "usage" (
|
||||||
|
"id" varchar(30) NOT NULL,
|
||||||
|
"workspace_id" varchar(30) NOT NULL,
|
||||||
|
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_deleted" timestamp with time zone,
|
||||||
|
"request_id" varchar(255),
|
||||||
|
"model" varchar(255) NOT NULL,
|
||||||
|
"input_tokens" integer NOT NULL,
|
||||||
|
"output_tokens" integer NOT NULL,
|
||||||
|
"reasoning_tokens" integer,
|
||||||
|
"cache_read_tokens" integer,
|
||||||
|
"cache_write_tokens" integer,
|
||||||
|
"cost" bigint NOT NULL,
|
||||||
|
CONSTRAINT "usage_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "user" (
|
||||||
|
"id" varchar(30) NOT NULL,
|
||||||
|
"workspace_id" varchar(30) NOT NULL,
|
||||||
|
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_deleted" timestamp with time zone,
|
||||||
|
"email" text NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"time_seen" timestamp with time zone,
|
||||||
|
"color" integer,
|
||||||
|
CONSTRAINT "user_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "workspace" (
|
||||||
|
"id" varchar(30) PRIMARY KEY NOT NULL,
|
||||||
|
"slug" varchar(255),
|
||||||
|
"name" varchar(255),
|
||||||
|
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_deleted" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "billing" ADD CONSTRAINT "billing_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "payment" ADD CONSTRAINT "payment_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "usage" ADD CONSTRAINT "usage_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" ADD CONSTRAINT "user_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "user_email" ON "user" USING btree ("workspace_id","email");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "slug" ON "workspace" USING btree ("slug");
|
8
cloud/core/migrations/0001_thankful_chat.sql
Normal file
8
cloud/core/migrations/0001_thankful_chat.sql
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
CREATE TABLE "account" (
|
||||||
|
"id" varchar(30) NOT NULL,
|
||||||
|
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_deleted" timestamp with time zone,
|
||||||
|
"email" varchar(255) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "email" ON "account" USING btree ("email");
|
14
cloud/core/migrations/0002_stale_jackal.sql
Normal file
14
cloud/core/migrations/0002_stale_jackal.sql
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
CREATE TABLE "key" (
|
||||||
|
"id" varchar(30) NOT NULL,
|
||||||
|
"workspace_id" varchar(30) NOT NULL,
|
||||||
|
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_deleted" timestamp with time zone,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"key" varchar(255) NOT NULL,
|
||||||
|
"time_used" timestamp with time zone,
|
||||||
|
CONSTRAINT "key_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "key" ADD CONSTRAINT "key_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "global_key" ON "key" USING btree ("key");
|
1
cloud/core/migrations/0003_tranquil_spencer_smythe.sql
Normal file
1
cloud/core/migrations/0003_tranquil_spencer_smythe.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE "usage" DROP COLUMN "request_id";
|
461
cloud/core/migrations/meta/0000_snapshot.json
Normal file
461
cloud/core/migrations/meta/0000_snapshot.json
Normal file
|
@ -0,0 +1,461 @@
|
||||||
|
{
|
||||||
|
"id": "9b5cec8c-8b59-4d7a-bb5c-76ade1c83d6f",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.billing": {
|
||||||
|
"name": "billing",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_id": {
|
||||||
|
"name": "payment_method_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_last4": {
|
||||||
|
"name": "payment_method_last4",
|
||||||
|
"type": "varchar(4)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"balance": {
|
||||||
|
"name": "balance",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reload": {
|
||||||
|
"name": "reload",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"billing_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "billing_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "billing",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"billing_workspace_id_id_pk": {
|
||||||
|
"name": "billing_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.payment": {
|
||||||
|
"name": "payment",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_id": {
|
||||||
|
"name": "payment_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"name": "amount",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"payment_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "payment_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "payment",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"payment_workspace_id_id_pk": {
|
||||||
|
"name": "payment_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.usage": {
|
||||||
|
"name": "usage",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"request_id": {
|
||||||
|
"name": "request_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"input_tokens": {
|
||||||
|
"name": "input_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"output_tokens": {
|
||||||
|
"name": "output_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reasoning_tokens": {
|
||||||
|
"name": "reasoning_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_read_tokens": {
|
||||||
|
"name": "cache_read_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_write_tokens": {
|
||||||
|
"name": "cache_write_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cost": {
|
||||||
|
"name": "cost",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"usage_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "usage_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "usage",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"usage_workspace_id_id_pk": {
|
||||||
|
"name": "usage_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_seen": {
|
||||||
|
"name": "time_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"name": "color",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email": {
|
||||||
|
"name": "user_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "workspace_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "user_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "user",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_workspace_id_id_pk": {
|
||||||
|
"name": "user_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.workspace": {
|
||||||
|
"name": "workspace",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "slug",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
515
cloud/core/migrations/meta/0001_snapshot.json
Normal file
515
cloud/core/migrations/meta/0001_snapshot.json
Normal file
|
@ -0,0 +1,515 @@
|
||||||
|
{
|
||||||
|
"id": "bf9e9084-4073-4ecb-8e56-5610816c9589",
|
||||||
|
"prevId": "9b5cec8c-8b59-4d7a-bb5c-76ade1c83d6f",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.billing": {
|
||||||
|
"name": "billing",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_id": {
|
||||||
|
"name": "payment_method_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_last4": {
|
||||||
|
"name": "payment_method_last4",
|
||||||
|
"type": "varchar(4)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"balance": {
|
||||||
|
"name": "balance",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reload": {
|
||||||
|
"name": "reload",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"billing_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "billing_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "billing",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"billing_workspace_id_id_pk": {
|
||||||
|
"name": "billing_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.payment": {
|
||||||
|
"name": "payment",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_id": {
|
||||||
|
"name": "payment_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"name": "amount",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"payment_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "payment_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "payment",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"payment_workspace_id_id_pk": {
|
||||||
|
"name": "payment_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.usage": {
|
||||||
|
"name": "usage",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"request_id": {
|
||||||
|
"name": "request_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"input_tokens": {
|
||||||
|
"name": "input_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"output_tokens": {
|
||||||
|
"name": "output_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reasoning_tokens": {
|
||||||
|
"name": "reasoning_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_read_tokens": {
|
||||||
|
"name": "cache_read_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_write_tokens": {
|
||||||
|
"name": "cache_write_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cost": {
|
||||||
|
"name": "cost",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"usage_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "usage_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "usage",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"usage_workspace_id_id_pk": {
|
||||||
|
"name": "usage_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_seen": {
|
||||||
|
"name": "time_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"name": "color",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email": {
|
||||||
|
"name": "user_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "workspace_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "user_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "user",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_workspace_id_id_pk": {
|
||||||
|
"name": "user_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.workspace": {
|
||||||
|
"name": "workspace",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "slug",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
615
cloud/core/migrations/meta/0002_snapshot.json
Normal file
615
cloud/core/migrations/meta/0002_snapshot.json
Normal file
|
@ -0,0 +1,615 @@
|
||||||
|
{
|
||||||
|
"id": "351e4956-74e0-4282-a23b-02f1a73fa38c",
|
||||||
|
"prevId": "bf9e9084-4073-4ecb-8e56-5610816c9589",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.billing": {
|
||||||
|
"name": "billing",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_id": {
|
||||||
|
"name": "payment_method_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_last4": {
|
||||||
|
"name": "payment_method_last4",
|
||||||
|
"type": "varchar(4)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"balance": {
|
||||||
|
"name": "balance",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reload": {
|
||||||
|
"name": "reload",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"billing_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "billing_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "billing",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"billing_workspace_id_id_pk": {
|
||||||
|
"name": "billing_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.payment": {
|
||||||
|
"name": "payment",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_id": {
|
||||||
|
"name": "payment_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"name": "amount",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"payment_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "payment_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "payment",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"payment_workspace_id_id_pk": {
|
||||||
|
"name": "payment_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.usage": {
|
||||||
|
"name": "usage",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"request_id": {
|
||||||
|
"name": "request_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"input_tokens": {
|
||||||
|
"name": "input_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"output_tokens": {
|
||||||
|
"name": "output_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reasoning_tokens": {
|
||||||
|
"name": "reasoning_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_read_tokens": {
|
||||||
|
"name": "cache_read_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_write_tokens": {
|
||||||
|
"name": "cache_write_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cost": {
|
||||||
|
"name": "cost",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"usage_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "usage_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "usage",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"usage_workspace_id_id_pk": {
|
||||||
|
"name": "usage_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.key": {
|
||||||
|
"name": "key",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_used": {
|
||||||
|
"name": "time_used",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"global_key": {
|
||||||
|
"name": "global_key",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "key",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"key_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "key_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "key",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"key_workspace_id_id_pk": {
|
||||||
|
"name": "key_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_seen": {
|
||||||
|
"name": "time_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"name": "color",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email": {
|
||||||
|
"name": "user_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "workspace_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "user_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "user",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_workspace_id_id_pk": {
|
||||||
|
"name": "user_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.workspace": {
|
||||||
|
"name": "workspace",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "slug",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
609
cloud/core/migrations/meta/0003_snapshot.json
Normal file
609
cloud/core/migrations/meta/0003_snapshot.json
Normal file
|
@ -0,0 +1,609 @@
|
||||||
|
{
|
||||||
|
"id": "fa935883-9e51-4811-90c7-8967eefe458c",
|
||||||
|
"prevId": "351e4956-74e0-4282-a23b-02f1a73fa38c",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.billing": {
|
||||||
|
"name": "billing",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_id": {
|
||||||
|
"name": "payment_method_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_last4": {
|
||||||
|
"name": "payment_method_last4",
|
||||||
|
"type": "varchar(4)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"balance": {
|
||||||
|
"name": "balance",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reload": {
|
||||||
|
"name": "reload",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"billing_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "billing_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "billing",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"billing_workspace_id_id_pk": {
|
||||||
|
"name": "billing_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.payment": {
|
||||||
|
"name": "payment",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_id": {
|
||||||
|
"name": "payment_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"name": "amount",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"payment_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "payment_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "payment",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"payment_workspace_id_id_pk": {
|
||||||
|
"name": "payment_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.usage": {
|
||||||
|
"name": "usage",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"input_tokens": {
|
||||||
|
"name": "input_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"output_tokens": {
|
||||||
|
"name": "output_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reasoning_tokens": {
|
||||||
|
"name": "reasoning_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_read_tokens": {
|
||||||
|
"name": "cache_read_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_write_tokens": {
|
||||||
|
"name": "cache_write_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cost": {
|
||||||
|
"name": "cost",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"usage_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "usage_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "usage",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"usage_workspace_id_id_pk": {
|
||||||
|
"name": "usage_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.key": {
|
||||||
|
"name": "key",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_used": {
|
||||||
|
"name": "time_used",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"global_key": {
|
||||||
|
"name": "global_key",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "key",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"key_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "key_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "key",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"key_workspace_id_id_pk": {
|
||||||
|
"name": "key_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_seen": {
|
||||||
|
"name": "time_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"name": "color",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email": {
|
||||||
|
"name": "user_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "workspace_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "user_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "user",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_workspace_id_id_pk": {
|
||||||
|
"name": "user_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.workspace": {
|
||||||
|
"name": "workspace",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "slug",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
34
cloud/core/migrations/meta/_journal.json
Normal file
34
cloud/core/migrations/meta/_journal.json
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1754518198186,
|
||||||
|
"tag": "0000_amused_mojo",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1754609655262,
|
||||||
|
"tag": "0001_thankful_chat",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1754627626945,
|
||||||
|
"tag": "0002_stale_jackal",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1754672464106,
|
||||||
|
"tag": "0003_tranquil_spencer_smythe",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
22
cloud/core/package.json
Normal file
22
cloud/core/package.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
|
"name": "@opencode/cloud-core",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
|
"drizzle-orm": "0.41.0",
|
||||||
|
"stripe": "18.0.0",
|
||||||
|
"ulid": "3.0.0"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"./*": "./src/*"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"db": "sst shell drizzle-kit"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"drizzle-kit": "0.30.5"
|
||||||
|
}
|
||||||
|
}
|
67
cloud/core/src/account.ts
Normal file
67
cloud/core/src/account.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { z } from "zod"
|
||||||
|
import { and, eq, getTableColumns, isNull } from "drizzle-orm"
|
||||||
|
import { fn } from "./util/fn"
|
||||||
|
import { Database } from "./drizzle"
|
||||||
|
import { Identifier } from "./identifier"
|
||||||
|
import { AccountTable } from "./schema/account.sql"
|
||||||
|
import { Actor } from "./actor"
|
||||||
|
import { WorkspaceTable } from "./schema/workspace.sql"
|
||||||
|
import { UserTable } from "./schema/user.sql"
|
||||||
|
|
||||||
|
export namespace Account {
|
||||||
|
export const create = fn(
|
||||||
|
z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
id: z.string().optional(),
|
||||||
|
}),
|
||||||
|
async (input) =>
|
||||||
|
Database.transaction(async (tx) => {
|
||||||
|
const id = input.id ?? Identifier.create("account")
|
||||||
|
await tx.insert(AccountTable).values({
|
||||||
|
id,
|
||||||
|
email: input.email,
|
||||||
|
})
|
||||||
|
return id
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const fromID = fn(z.string(), async (id) =>
|
||||||
|
Database.transaction(async (tx) => {
|
||||||
|
return tx
|
||||||
|
.select()
|
||||||
|
.from(AccountTable)
|
||||||
|
.where(eq(AccountTable.id, id))
|
||||||
|
.execute()
|
||||||
|
.then((rows) => rows[0])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const fromEmail = fn(z.string().email(), async (email) =>
|
||||||
|
Database.transaction(async (tx) => {
|
||||||
|
return tx
|
||||||
|
.select()
|
||||||
|
.from(AccountTable)
|
||||||
|
.where(eq(AccountTable.email, email))
|
||||||
|
.execute()
|
||||||
|
.then((rows) => rows[0])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const workspaces = async () => {
|
||||||
|
const actor = Actor.assert("account")
|
||||||
|
return Database.transaction(async (tx) =>
|
||||||
|
tx
|
||||||
|
.select(getTableColumns(WorkspaceTable))
|
||||||
|
.from(WorkspaceTable)
|
||||||
|
.innerJoin(UserTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(UserTable.email, actor.properties.email),
|
||||||
|
isNull(UserTable.timeDeleted),
|
||||||
|
isNull(WorkspaceTable.timeDeleted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.execute(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
75
cloud/core/src/actor.ts
Normal file
75
cloud/core/src/actor.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { Context } from "./context"
|
||||||
|
import { Log } from "./util/log"
|
||||||
|
|
||||||
|
export namespace Actor {
|
||||||
|
interface Account {
|
||||||
|
type: "account"
|
||||||
|
properties: {
|
||||||
|
accountID: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Public {
|
||||||
|
type: "public"
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
type: "user"
|
||||||
|
properties: {
|
||||||
|
userID: string
|
||||||
|
workspaceID: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface System {
|
||||||
|
type: "system"
|
||||||
|
properties: {
|
||||||
|
workspaceID: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Info = Account | Public | User | System
|
||||||
|
|
||||||
|
const ctx = Context.create<Info>()
|
||||||
|
export const use = ctx.use
|
||||||
|
|
||||||
|
const log = Log.create().tag("namespace", "actor")
|
||||||
|
|
||||||
|
export function provide<R, T extends Info["type"]>(
|
||||||
|
type: T,
|
||||||
|
properties: Extract<Info, { type: T }>["properties"],
|
||||||
|
cb: () => R,
|
||||||
|
) {
|
||||||
|
return ctx.provide(
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
properties,
|
||||||
|
} as any,
|
||||||
|
() => {
|
||||||
|
return Log.provide({ ...properties }, () => {
|
||||||
|
log.info("provided")
|
||||||
|
return cb()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assert<T extends Info["type"]>(type: T) {
|
||||||
|
const actor = use()
|
||||||
|
if (actor.type !== type) {
|
||||||
|
throw new Error(`Expected actor type ${type}, got ${actor.type}`)
|
||||||
|
}
|
||||||
|
return actor as Extract<Info, { type: T }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function workspace() {
|
||||||
|
const actor = use()
|
||||||
|
if ("workspaceID" in actor.properties) {
|
||||||
|
return actor.properties.workspaceID
|
||||||
|
}
|
||||||
|
throw new Error(`actor of type "${actor.type}" is not associated with a workspace`)
|
||||||
|
}
|
||||||
|
}
|
71
cloud/core/src/billing.ts
Normal file
71
cloud/core/src/billing.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { Resource } from "sst"
|
||||||
|
import { Stripe } from "stripe"
|
||||||
|
import { Database, eq, sql } from "./drizzle"
|
||||||
|
import { BillingTable, UsageTable } from "./schema/billing.sql"
|
||||||
|
import { Actor } from "./actor"
|
||||||
|
import { fn } from "./util/fn"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { Identifier } from "./identifier"
|
||||||
|
import { centsToMicroCents } from "./util/price"
|
||||||
|
|
||||||
|
export namespace Billing {
|
||||||
|
export const stripe = () =>
|
||||||
|
new Stripe(Resource.STRIPE_SECRET_KEY.value, {
|
||||||
|
apiVersion: "2025-03-31.basil",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const get = async () => {
|
||||||
|
return Database.use(async (tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
customerID: BillingTable.customerID,
|
||||||
|
paymentMethodID: BillingTable.paymentMethodID,
|
||||||
|
balance: BillingTable.balance,
|
||||||
|
reload: BillingTable.reload,
|
||||||
|
})
|
||||||
|
.from(BillingTable)
|
||||||
|
.where(eq(BillingTable.workspaceID, Actor.workspace()))
|
||||||
|
.then((r) => r[0]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const consume = fn(
|
||||||
|
z.object({
|
||||||
|
requestID: z.string().optional(),
|
||||||
|
model: z.string(),
|
||||||
|
inputTokens: z.number(),
|
||||||
|
outputTokens: z.number(),
|
||||||
|
reasoningTokens: z.number().optional(),
|
||||||
|
cacheReadTokens: z.number().optional(),
|
||||||
|
cacheWriteTokens: z.number().optional(),
|
||||||
|
costInCents: z.number(),
|
||||||
|
}),
|
||||||
|
async (input) => {
|
||||||
|
const workspaceID = Actor.workspace()
|
||||||
|
const cost = centsToMicroCents(input.costInCents)
|
||||||
|
|
||||||
|
return await Database.transaction(async (tx) => {
|
||||||
|
await tx.insert(UsageTable).values({
|
||||||
|
workspaceID,
|
||||||
|
id: Identifier.create("usage"),
|
||||||
|
requestID: input.requestID,
|
||||||
|
model: input.model,
|
||||||
|
inputTokens: input.inputTokens,
|
||||||
|
outputTokens: input.outputTokens,
|
||||||
|
reasoningTokens: input.reasoningTokens,
|
||||||
|
cacheReadTokens: input.cacheReadTokens,
|
||||||
|
cacheWriteTokens: input.cacheWriteTokens,
|
||||||
|
cost,
|
||||||
|
})
|
||||||
|
const [updated] = await tx
|
||||||
|
.update(BillingTable)
|
||||||
|
.set({
|
||||||
|
balance: sql`${BillingTable.balance} - ${cost}`,
|
||||||
|
})
|
||||||
|
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||||
|
.returning()
|
||||||
|
return updated.balance
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
21
cloud/core/src/context.ts
Normal file
21
cloud/core/src/context.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { AsyncLocalStorage } from "node:async_hooks"
|
||||||
|
|
||||||
|
export namespace Context {
|
||||||
|
export class NotFound extends Error {}
|
||||||
|
|
||||||
|
export function create<T>() {
|
||||||
|
const storage = new AsyncLocalStorage<T>()
|
||||||
|
return {
|
||||||
|
use() {
|
||||||
|
const result = storage.getStore()
|
||||||
|
if (!result) {
|
||||||
|
throw new NotFound()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
provide<R>(value: T, fn: () => R) {
|
||||||
|
return storage.run<R>(value, fn)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
94
cloud/core/src/drizzle/index.ts
Normal file
94
cloud/core/src/drizzle/index.ts
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import { drizzle } from "drizzle-orm/postgres-js"
|
||||||
|
import { Resource } from "sst"
|
||||||
|
export * from "drizzle-orm"
|
||||||
|
import postgres from "postgres"
|
||||||
|
|
||||||
|
function createClient() {
|
||||||
|
const client = postgres({
|
||||||
|
idle_timeout: 30000,
|
||||||
|
connect_timeout: 30000,
|
||||||
|
host: Resource.Database.host,
|
||||||
|
database: Resource.Database.database,
|
||||||
|
user: Resource.Database.username,
|
||||||
|
password: Resource.Database.password,
|
||||||
|
port: Resource.Database.port,
|
||||||
|
ssl: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
max: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
return drizzle(client, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
import { PgTransaction, type PgTransactionConfig } from "drizzle-orm/pg-core"
|
||||||
|
import type { ExtractTablesWithRelations } from "drizzle-orm"
|
||||||
|
import type { PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js"
|
||||||
|
import { Context } from "../context"
|
||||||
|
|
||||||
|
export namespace Database {
|
||||||
|
export type Transaction = PgTransaction<
|
||||||
|
PostgresJsQueryResultHKT,
|
||||||
|
Record<string, unknown>,
|
||||||
|
ExtractTablesWithRelations<Record<string, unknown>>
|
||||||
|
>
|
||||||
|
|
||||||
|
export type TxOrDb = Transaction | ReturnType<typeof createClient>
|
||||||
|
|
||||||
|
const TransactionContext = Context.create<{
|
||||||
|
tx: TxOrDb
|
||||||
|
effects: (() => void | Promise<void>)[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
export async function use<T>(callback: (trx: TxOrDb) => Promise<T>) {
|
||||||
|
try {
|
||||||
|
const { tx } = TransactionContext.use()
|
||||||
|
return tx.transaction(callback)
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Context.NotFound) {
|
||||||
|
const client = createClient()
|
||||||
|
const effects: (() => void | Promise<void>)[] = []
|
||||||
|
const result = await TransactionContext.provide(
|
||||||
|
{
|
||||||
|
effects,
|
||||||
|
tx: client,
|
||||||
|
},
|
||||||
|
() => callback(client),
|
||||||
|
)
|
||||||
|
await Promise.all(effects.map((x) => x()))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function fn<Input, T>(callback: (input: Input, trx: TxOrDb) => Promise<T>) {
|
||||||
|
return (input: Input) => use(async (tx) => callback(input, tx))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function effect(effect: () => any | Promise<any>) {
|
||||||
|
try {
|
||||||
|
const { effects } = TransactionContext.use()
|
||||||
|
effects.push(effect)
|
||||||
|
} catch {
|
||||||
|
await effect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function transaction<T>(callback: (tx: TxOrDb) => Promise<T>, config?: PgTransactionConfig) {
|
||||||
|
try {
|
||||||
|
const { tx } = TransactionContext.use()
|
||||||
|
return callback(tx)
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Context.NotFound) {
|
||||||
|
const client = createClient()
|
||||||
|
const effects: (() => void | Promise<void>)[] = []
|
||||||
|
const result = await client.transaction(async (tx) => {
|
||||||
|
return TransactionContext.provide({ tx, effects }, () => callback(tx))
|
||||||
|
}, config)
|
||||||
|
await Promise.all(effects.map((x) => x()))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
cloud/core/src/drizzle/types.ts
Normal file
29
cloud/core/src/drizzle/types.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { bigint, timestamp, varchar } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
export const ulid = (name: string) => varchar(name, { length: 30 })
|
||||||
|
|
||||||
|
export const workspaceColumns = {
|
||||||
|
get id() {
|
||||||
|
return ulid("id").notNull()
|
||||||
|
},
|
||||||
|
get workspaceID() {
|
||||||
|
return ulid("workspace_id").notNull()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const id = () => ulid("id").notNull()
|
||||||
|
|
||||||
|
export const utc = (name: string) =>
|
||||||
|
timestamp(name, {
|
||||||
|
withTimezone: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const currency = (name: string) =>
|
||||||
|
bigint(name, {
|
||||||
|
mode: "number",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const timestamps = {
|
||||||
|
timeCreated: utc("time_created").notNull().defaultNow(),
|
||||||
|
timeDeleted: utc("time_deleted"),
|
||||||
|
}
|
26
cloud/core/src/identifier.ts
Normal file
26
cloud/core/src/identifier.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { ulid } from "ulid"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
export namespace Identifier {
|
||||||
|
const prefixes = {
|
||||||
|
account: "acc",
|
||||||
|
billing: "bil",
|
||||||
|
key: "key",
|
||||||
|
payment: "pay",
|
||||||
|
usage: "usg",
|
||||||
|
user: "usr",
|
||||||
|
workspace: "wrk",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export function create(prefix: keyof typeof prefixes, given?: string): string {
|
||||||
|
if (given) {
|
||||||
|
if (given.startsWith(prefixes[prefix])) return given
|
||||||
|
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
|
||||||
|
}
|
||||||
|
return [prefixes[prefix], ulid()].join("_")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function schema(prefix: keyof typeof prefixes) {
|
||||||
|
return z.string().startsWith(prefixes[prefix])
|
||||||
|
}
|
||||||
|
}
|
12
cloud/core/src/schema/account.sql.ts
Normal file
12
cloud/core/src/schema/account.sql.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core"
|
||||||
|
import { id, timestamps } from "../drizzle/types"
|
||||||
|
|
||||||
|
export const AccountTable = pgTable(
|
||||||
|
"account",
|
||||||
|
{
|
||||||
|
id: id(),
|
||||||
|
...timestamps,
|
||||||
|
email: varchar("email", { length: 255 }).notNull(),
|
||||||
|
},
|
||||||
|
(table) => [uniqueIndex("email").on(table.email)],
|
||||||
|
)
|
45
cloud/core/src/schema/billing.sql.ts
Normal file
45
cloud/core/src/schema/billing.sql.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { bigint, boolean, integer, pgTable, varchar } from "drizzle-orm/pg-core"
|
||||||
|
import { timestamps, workspaceColumns } from "../drizzle/types"
|
||||||
|
import { workspaceIndexes } from "./workspace.sql"
|
||||||
|
|
||||||
|
export const BillingTable = pgTable(
|
||||||
|
"billing",
|
||||||
|
{
|
||||||
|
...workspaceColumns,
|
||||||
|
...timestamps,
|
||||||
|
customerID: varchar("customer_id", { length: 255 }),
|
||||||
|
paymentMethodID: varchar("payment_method_id", { length: 255 }),
|
||||||
|
paymentMethodLast4: varchar("payment_method_last4", { length: 4 }),
|
||||||
|
balance: bigint("balance", { mode: "number" }).notNull(),
|
||||||
|
reload: boolean("reload"),
|
||||||
|
},
|
||||||
|
(table) => [...workspaceIndexes(table)],
|
||||||
|
)
|
||||||
|
|
||||||
|
export const PaymentTable = pgTable(
|
||||||
|
"payment",
|
||||||
|
{
|
||||||
|
...workspaceColumns,
|
||||||
|
...timestamps,
|
||||||
|
customerID: varchar("customer_id", { length: 255 }),
|
||||||
|
paymentID: varchar("payment_id", { length: 255 }),
|
||||||
|
amount: bigint("amount", { mode: "number" }).notNull(),
|
||||||
|
},
|
||||||
|
(table) => [...workspaceIndexes(table)],
|
||||||
|
)
|
||||||
|
|
||||||
|
export const UsageTable = pgTable(
|
||||||
|
"usage",
|
||||||
|
{
|
||||||
|
...workspaceColumns,
|
||||||
|
...timestamps,
|
||||||
|
model: varchar("model", { length: 255 }).notNull(),
|
||||||
|
inputTokens: integer("input_tokens").notNull(),
|
||||||
|
outputTokens: integer("output_tokens").notNull(),
|
||||||
|
reasoningTokens: integer("reasoning_tokens"),
|
||||||
|
cacheReadTokens: integer("cache_read_tokens"),
|
||||||
|
cacheWriteTokens: integer("cache_write_tokens"),
|
||||||
|
cost: bigint("cost", { mode: "number" }).notNull(),
|
||||||
|
},
|
||||||
|
(table) => [...workspaceIndexes(table)],
|
||||||
|
)
|
16
cloud/core/src/schema/key.sql.ts
Normal file
16
cloud/core/src/schema/key.sql.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { text, pgTable, varchar, uniqueIndex } from "drizzle-orm/pg-core"
|
||||||
|
import { timestamps, utc, workspaceColumns } from "../drizzle/types"
|
||||||
|
import { workspaceIndexes } from "./workspace.sql"
|
||||||
|
|
||||||
|
export const KeyTable = pgTable(
|
||||||
|
"key",
|
||||||
|
{
|
||||||
|
...workspaceColumns,
|
||||||
|
...timestamps,
|
||||||
|
userID: text("user_id").notNull(),
|
||||||
|
name: varchar("name", { length: 255 }).notNull(),
|
||||||
|
key: varchar("key", { length: 255 }).notNull(),
|
||||||
|
timeUsed: utc("time_used"),
|
||||||
|
},
|
||||||
|
(table) => [...workspaceIndexes(table), uniqueIndex("global_key").on(table.key)],
|
||||||
|
)
|
16
cloud/core/src/schema/user.sql.ts
Normal file
16
cloud/core/src/schema/user.sql.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { text, pgTable, uniqueIndex, varchar, integer } from "drizzle-orm/pg-core"
|
||||||
|
import { timestamps, utc, workspaceColumns } from "../drizzle/types"
|
||||||
|
import { workspaceIndexes } from "./workspace.sql"
|
||||||
|
|
||||||
|
export const UserTable = pgTable(
|
||||||
|
"user",
|
||||||
|
{
|
||||||
|
...workspaceColumns,
|
||||||
|
...timestamps,
|
||||||
|
email: text("email").notNull(),
|
||||||
|
name: varchar("name", { length: 255 }).notNull(),
|
||||||
|
timeSeen: utc("time_seen"),
|
||||||
|
color: integer("color"),
|
||||||
|
},
|
||||||
|
(table) => [...workspaceIndexes(table), uniqueIndex("user_email").on(table.workspaceID, table.email)],
|
||||||
|
)
|
25
cloud/core/src/schema/workspace.sql.ts
Normal file
25
cloud/core/src/schema/workspace.sql.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { primaryKey, foreignKey, pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core"
|
||||||
|
import { timestamps, ulid } from "../drizzle/types"
|
||||||
|
|
||||||
|
export const WorkspaceTable = pgTable(
|
||||||
|
"workspace",
|
||||||
|
{
|
||||||
|
id: ulid("id").notNull().primaryKey(),
|
||||||
|
slug: varchar("slug", { length: 255 }),
|
||||||
|
name: varchar("name", { length: 255 }),
|
||||||
|
...timestamps,
|
||||||
|
},
|
||||||
|
(table) => [uniqueIndex("slug").on(table.slug)],
|
||||||
|
)
|
||||||
|
|
||||||
|
export function workspaceIndexes(table: any) {
|
||||||
|
return [
|
||||||
|
primaryKey({
|
||||||
|
columns: [table.workspaceID, table.id],
|
||||||
|
}),
|
||||||
|
foreignKey({
|
||||||
|
foreignColumns: [WorkspaceTable.id],
|
||||||
|
columns: [table.workspaceID],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
14
cloud/core/src/util/fn.ts
Normal file
14
cloud/core/src/util/fn.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
export function fn<T extends z.ZodType, Result>(
|
||||||
|
schema: T,
|
||||||
|
cb: (input: z.output<T>) => Result,
|
||||||
|
) {
|
||||||
|
const result = (input: z.input<T>) => {
|
||||||
|
const parsed = schema.parse(input)
|
||||||
|
return cb(parsed)
|
||||||
|
}
|
||||||
|
result.force = (input: z.input<T>) => cb(input)
|
||||||
|
result.schema = schema
|
||||||
|
return result
|
||||||
|
}
|
55
cloud/core/src/util/log.ts
Normal file
55
cloud/core/src/util/log.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import { Context } from "../context"
|
||||||
|
|
||||||
|
export namespace Log {
|
||||||
|
const ctx = Context.create<{
|
||||||
|
tags: Record<string, any>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
export function create(tags?: Record<string, any>) {
|
||||||
|
tags = tags || {}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
info(message?: any, extra?: Record<string, any>) {
|
||||||
|
const prefix = Object.entries({
|
||||||
|
...use().tags,
|
||||||
|
...tags,
|
||||||
|
...extra,
|
||||||
|
})
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join(" ")
|
||||||
|
console.log(prefix, message)
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
tag(key: string, value: string) {
|
||||||
|
if (tags) tags[key] = value
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
clone() {
|
||||||
|
return Log.create({ ...tags })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function provide<R>(tags: Record<string, any>, cb: () => R) {
|
||||||
|
const existing = use()
|
||||||
|
return ctx.provide(
|
||||||
|
{
|
||||||
|
tags: {
|
||||||
|
...existing.tags,
|
||||||
|
...tags,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cb,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function use() {
|
||||||
|
try {
|
||||||
|
return ctx.use()
|
||||||
|
} catch (e) {
|
||||||
|
return { tags: {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
cloud/core/src/util/price.ts
Normal file
3
cloud/core/src/util/price.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export function centsToMicroCents(amount: number) {
|
||||||
|
return Math.round(amount * 1000000)
|
||||||
|
}
|
48
cloud/core/src/workspace.ts
Normal file
48
cloud/core/src/workspace.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { z } from "zod"
|
||||||
|
import { fn } from "./util/fn"
|
||||||
|
import { centsToMicroCents } from "./util/price"
|
||||||
|
import { Actor } from "./actor"
|
||||||
|
import { Database, eq } from "./drizzle"
|
||||||
|
import { Identifier } from "./identifier"
|
||||||
|
import { UserTable } from "./schema/user.sql"
|
||||||
|
import { BillingTable } from "./schema/billing.sql"
|
||||||
|
import { WorkspaceTable } from "./schema/workspace.sql"
|
||||||
|
|
||||||
|
export namespace Workspace {
|
||||||
|
export const create = fn(z.void(), async () => {
|
||||||
|
const account = Actor.assert("account")
|
||||||
|
const workspaceID = Identifier.create("workspace")
|
||||||
|
await Database.transaction(async (tx) => {
|
||||||
|
await tx.insert(WorkspaceTable).values({
|
||||||
|
id: workspaceID,
|
||||||
|
})
|
||||||
|
await tx.insert(UserTable).values({
|
||||||
|
workspaceID,
|
||||||
|
id: Identifier.create("user"),
|
||||||
|
email: account.properties.email,
|
||||||
|
name: "",
|
||||||
|
})
|
||||||
|
await tx.insert(BillingTable).values({
|
||||||
|
workspaceID,
|
||||||
|
id: Identifier.create("billing"),
|
||||||
|
balance: centsToMicroCents(100),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return workspaceID
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function list() {
|
||||||
|
const account = Actor.assert("account")
|
||||||
|
return Database.use(async (tx) => {
|
||||||
|
return tx
|
||||||
|
.select({
|
||||||
|
id: WorkspaceTable.id,
|
||||||
|
slug: WorkspaceTable.slug,
|
||||||
|
name: WorkspaceTable.name,
|
||||||
|
})
|
||||||
|
.from(UserTable)
|
||||||
|
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||||
|
.where(eq(UserTable.email, account.properties.email))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
9
cloud/core/sst-env.d.ts
vendored
Normal file
9
cloud/core/sst-env.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/* This file is auto-generated by SST. Do not edit. */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/* deno-fmt-ignore-file */
|
||||||
|
|
||||||
|
/// <reference path="../../sst-env.d.ts" />
|
||||||
|
|
||||||
|
import "sst"
|
||||||
|
export {}
|
9
cloud/core/tsconfig.json
Normal file
9
cloud/core/tsconfig.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@tsconfig/node22/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"types": ["@cloudflare/workers-types", "node"]
|
||||||
|
}
|
||||||
|
}
|
23
cloud/function/package.json
Normal file
23
cloud/function/package.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "@opencode/cloud-function",
|
||||||
|
"version": "0.3.130",
|
||||||
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@cloudflare/workers-types": "4.20250522.0",
|
||||||
|
"@types/node": "catalog:",
|
||||||
|
"openai": "5.11.0",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/anthropic": "2.0.0",
|
||||||
|
"@ai-sdk/openai": "2.0.2",
|
||||||
|
"@ai-sdk/openai-compatible": "1.0.1",
|
||||||
|
"@hono/zod-validator": "catalog:",
|
||||||
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
|
"ai": "catalog:",
|
||||||
|
"hono": "catalog:",
|
||||||
|
"zod": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
68
cloud/function/src/auth.ts
Normal file
68
cloud/function/src/auth.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { Resource } from "sst"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { issuer } from "@openauthjs/openauth"
|
||||||
|
import { createSubjects } from "@openauthjs/openauth/subject"
|
||||||
|
import { GithubProvider } from "@openauthjs/openauth/provider/github"
|
||||||
|
import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"
|
||||||
|
import { Account } from "@opencode/cloud-core/account.js"
|
||||||
|
|
||||||
|
type Env = {
|
||||||
|
AuthStorage: KVNamespace
|
||||||
|
}
|
||||||
|
|
||||||
|
export const subjects = createSubjects({
|
||||||
|
account: z.object({
|
||||||
|
accountID: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
}),
|
||||||
|
user: z.object({
|
||||||
|
userID: z.string(),
|
||||||
|
workspaceID: z.string(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
||||||
|
return issuer({
|
||||||
|
providers: {
|
||||||
|
github: GithubProvider({
|
||||||
|
clientID: Resource.GITHUB_CLIENT_ID_CONSOLE.value,
|
||||||
|
clientSecret: Resource.GITHUB_CLIENT_SECRET_CONSOLE.value,
|
||||||
|
scopes: ["read:user", "user:email"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
storage: CloudflareStorage({
|
||||||
|
namespace: env.AuthStorage,
|
||||||
|
}),
|
||||||
|
subjects,
|
||||||
|
async success(ctx, response) {
|
||||||
|
console.log(response)
|
||||||
|
|
||||||
|
let email: string | undefined
|
||||||
|
|
||||||
|
if (response.provider === "github") {
|
||||||
|
const userResponse = await fetch("https://api.github.com/user", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${response.tokenset.access}`,
|
||||||
|
"User-Agent": "opencode",
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const user = (await userResponse.json()) as { email: string }
|
||||||
|
email = user.email
|
||||||
|
} else throw new Error("Unsupported provider")
|
||||||
|
|
||||||
|
if (!email) throw new Error("No email found")
|
||||||
|
|
||||||
|
let accountID = await Account.fromEmail(email).then((x) => x?.id)
|
||||||
|
if (!accountID) {
|
||||||
|
console.log("creating account for", email)
|
||||||
|
accountID = await Account.create({
|
||||||
|
email: email!,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ctx.subject("account", accountID, { accountID, email })
|
||||||
|
},
|
||||||
|
}).fetch(request, env, ctx)
|
||||||
|
},
|
||||||
|
}
|
|
@ -1,15 +1,82 @@
|
||||||
import { Hono, Context, Next } from "hono"
|
import { z } from "zod"
|
||||||
|
import { Hono, MiddlewareHandler } from "hono"
|
||||||
|
import { cors } from "hono/cors"
|
||||||
|
import { HTTPException } from "hono/http-exception"
|
||||||
|
import { zValidator } from "@hono/zod-validator"
|
||||||
import { Resource } from "sst"
|
import { Resource } from "sst"
|
||||||
import { generateText, streamText } from "ai"
|
import { generateText, streamText } from "ai"
|
||||||
import { createAnthropic } from "@ai-sdk/anthropic"
|
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||||
import { createOpenAI } from "@ai-sdk/openai"
|
import { createOpenAI } from "@ai-sdk/openai"
|
||||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
||||||
import { type LanguageModelV2Prompt } from "@ai-sdk/provider"
|
import type { LanguageModelV2Usage, LanguageModelV2Prompt } from "@ai-sdk/provider"
|
||||||
import { type ChatCompletionCreateParamsBase } from "openai/resources/chat/completions"
|
import { type ChatCompletionCreateParamsBase } from "openai/resources/chat/completions"
|
||||||
|
import { Actor } from "@opencode/cloud-core/actor.js"
|
||||||
|
import { and, Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
|
||||||
|
import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
|
||||||
|
import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js"
|
||||||
|
import { createClient } from "@openauthjs/openauth/client"
|
||||||
|
import { Log } from "@opencode/cloud-core/util/log.js"
|
||||||
|
import { Billing } from "@opencode/cloud-core/billing.js"
|
||||||
|
import { Workspace } from "@opencode/cloud-core/workspace.js"
|
||||||
|
import { BillingTable, PaymentTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js"
|
||||||
|
import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
|
||||||
|
import { Identifier } from "../../core/src/identifier"
|
||||||
|
|
||||||
type Env = {}
|
type Env = {}
|
||||||
|
|
||||||
const auth = async (c: Context, next: Next) => {
|
let _client: ReturnType<typeof createClient>
|
||||||
|
const client = () => {
|
||||||
|
if (_client) return _client
|
||||||
|
_client = createClient({
|
||||||
|
clientID: "api",
|
||||||
|
issuer: Resource.AUTH_API_URL.value,
|
||||||
|
})
|
||||||
|
return _client
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUPPORTED_MODELS = {
|
||||||
|
"anthropic/claude-sonnet-4": {
|
||||||
|
input: 0.0000015,
|
||||||
|
output: 0.000006,
|
||||||
|
reasoning: 0.0000015,
|
||||||
|
cacheRead: 0.0000001,
|
||||||
|
cacheWrite: 0.0000001,
|
||||||
|
model: () =>
|
||||||
|
createAnthropic({
|
||||||
|
apiKey: Resource.ANTHROPIC_API_KEY.value,
|
||||||
|
})("claude-sonnet-4-20250514"),
|
||||||
|
},
|
||||||
|
"openai/gpt-4.1": {
|
||||||
|
input: 0.0000015,
|
||||||
|
output: 0.000006,
|
||||||
|
reasoning: 0.0000015,
|
||||||
|
cacheRead: 0.0000001,
|
||||||
|
cacheWrite: 0.0000001,
|
||||||
|
model: () =>
|
||||||
|
createOpenAI({
|
||||||
|
apiKey: Resource.OPENAI_API_KEY.value,
|
||||||
|
})("gpt-4.1"),
|
||||||
|
},
|
||||||
|
"zhipuai/glm-4.5-flash": {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
reasoning: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
model: () =>
|
||||||
|
createOpenAICompatible({
|
||||||
|
name: "Zhipu AI",
|
||||||
|
baseURL: "https://api.z.ai/api/paas/v4",
|
||||||
|
apiKey: Resource.ZHIPU_API_KEY.value,
|
||||||
|
})("glm-4.5-flash"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const log = Log.create({
|
||||||
|
namespace: "api",
|
||||||
|
})
|
||||||
|
|
||||||
|
const GatewayAuth: MiddlewareHandler = async (c, next) => {
|
||||||
const authHeader = c.req.header("authorization")
|
const authHeader = c.req.header("authorization")
|
||||||
|
|
||||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||||
|
@ -28,8 +95,19 @@ const auth = async (c: Context, next: Next) => {
|
||||||
|
|
||||||
const apiKey = authHeader.split(" ")[1]
|
const apiKey = authHeader.split(" ")[1]
|
||||||
|
|
||||||
// Replace with your validation logic
|
// Check against KeyTable
|
||||||
if (apiKey !== Resource.OPENCODE_API_KEY.value) {
|
const keyRecord = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
id: KeyTable.id,
|
||||||
|
workspaceID: KeyTable.workspaceID,
|
||||||
|
})
|
||||||
|
.from(KeyTable)
|
||||||
|
.where(eq(KeyTable.key, apiKey))
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!keyRecord) {
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
error: {
|
error: {
|
||||||
|
@ -43,38 +121,70 @@ const auth = async (c: Context, next: Next) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.set("keyRecord", keyRecord)
|
||||||
await next()
|
await next()
|
||||||
}
|
}
|
||||||
export default new Hono<{ Bindings: Env }>()
|
|
||||||
|
const RestAuth: MiddlewareHandler = async (c, next) => {
|
||||||
|
const authorization = c.req.header("authorization")
|
||||||
|
if (!authorization) {
|
||||||
|
return Actor.provide("public", {}, next)
|
||||||
|
}
|
||||||
|
const token = authorization.split(" ")[1]
|
||||||
|
if (!token)
|
||||||
|
throw new HTTPException(403, {
|
||||||
|
message: "Bearer token is required.",
|
||||||
|
})
|
||||||
|
|
||||||
|
const verified = await client().verify(token)
|
||||||
|
if (verified.err) {
|
||||||
|
throw new HTTPException(403, {
|
||||||
|
message: "Invalid token.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let subject = verified.subject as Actor.Info
|
||||||
|
if (subject.type === "account") {
|
||||||
|
const workspaceID = c.req.header("x-opencode-workspace")
|
||||||
|
const email = subject.properties.email
|
||||||
|
if (workspaceID) {
|
||||||
|
const user = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
id: UserTable.id,
|
||||||
|
workspaceID: UserTable.workspaceID,
|
||||||
|
email: UserTable.email,
|
||||||
|
})
|
||||||
|
.from(UserTable)
|
||||||
|
.where(and(eq(UserTable.email, email), eq(UserTable.workspaceID, workspaceID)))
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
)
|
||||||
|
if (!user)
|
||||||
|
throw new HTTPException(403, {
|
||||||
|
message: "You do not have access to this workspace.",
|
||||||
|
})
|
||||||
|
subject = {
|
||||||
|
type: "user",
|
||||||
|
properties: {
|
||||||
|
userID: user.id,
|
||||||
|
workspaceID: workspaceID,
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Actor.provide(subject.type, subject.properties, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; workspaceID: string } } }>()
|
||||||
.get("/", (c) => c.text("Hello, world!"))
|
.get("/", (c) => c.text("Hello, world!"))
|
||||||
.post("/v1/chat/completions", auth, async (c) => {
|
.post("/v1/chat/completions", GatewayAuth, async (c) => {
|
||||||
try {
|
try {
|
||||||
const body = await c.req.json<ChatCompletionCreateParamsBase>()
|
const body = await c.req.json<ChatCompletionCreateParamsBase>()
|
||||||
|
|
||||||
console.log(body)
|
console.log(body)
|
||||||
|
|
||||||
const model = (() => {
|
const model = SUPPORTED_MODELS[body.model as keyof typeof SUPPORTED_MODELS]?.model()
|
||||||
const [provider, ...parts] = body.model.split("/")
|
if (!model) throw new Error(`Unsupported model: ${body.model}`)
|
||||||
const model = parts.join("/")
|
|
||||||
if (provider === "anthropic" && model === "claude-sonnet-4") {
|
|
||||||
return createAnthropic({
|
|
||||||
apiKey: Resource.ANTHROPIC_API_KEY.value,
|
|
||||||
})("claude-sonnet-4-20250514")
|
|
||||||
}
|
|
||||||
if (provider === "openai" && model === "gpt-4.1") {
|
|
||||||
return createOpenAI({
|
|
||||||
apiKey: Resource.OPENAI_API_KEY.value,
|
|
||||||
})("gpt-4.1")
|
|
||||||
}
|
|
||||||
if (provider === "zhipuai" && model === "glm-4.5-flash") {
|
|
||||||
return createOpenAICompatible({
|
|
||||||
name: "Zhipu AI",
|
|
||||||
baseURL: "https://api.z.ai/api/paas/v4",
|
|
||||||
apiKey: Resource.ZHIPU_API_KEY.value,
|
|
||||||
})("glm-4.5-flash")
|
|
||||||
}
|
|
||||||
throw new Error(`Unsupported provider: ${provider}`)
|
|
||||||
})()
|
|
||||||
|
|
||||||
const requestBody = transformOpenAIRequestToAiSDK()
|
const requestBody = transformOpenAIRequestToAiSDK()
|
||||||
|
|
||||||
|
@ -263,6 +373,7 @@ export default new Hono<{ Bindings: Env }>()
|
||||||
model,
|
model,
|
||||||
...requestBody,
|
...requestBody,
|
||||||
})
|
})
|
||||||
|
await trackUsage(body.model, response.usage)
|
||||||
return c.json({
|
return c.json({
|
||||||
id: `chatcmpl-${Date.now()}`,
|
id: `chatcmpl-${Date.now()}`,
|
||||||
object: "chat.completion" as const,
|
object: "chat.completion" as const,
|
||||||
|
@ -492,8 +603,285 @@ export default new Hono<{ Bindings: Env }>()
|
||||||
return prompt
|
return prompt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function trackUsage(model: string, usage: LanguageModelV2Usage) {
|
||||||
|
const keyRecord = c.get("keyRecord")
|
||||||
|
if (!keyRecord) return
|
||||||
|
|
||||||
|
const modelData = SUPPORTED_MODELS[model as keyof typeof SUPPORTED_MODELS]
|
||||||
|
if (!modelData) throw new Error(`Unsupported model: ${model}`)
|
||||||
|
|
||||||
|
const inputCost = modelData.input * (usage.inputTokens ?? 0)
|
||||||
|
const outputCost = modelData.output * (usage.outputTokens ?? 0)
|
||||||
|
const reasoningCost = modelData.reasoning * (usage.reasoningTokens ?? 0)
|
||||||
|
const cacheReadCost = modelData.cacheRead * (usage.cachedInputTokens ?? 0)
|
||||||
|
const cacheWriteCost = modelData.cacheWrite * (usage.outputTokens ?? 0)
|
||||||
|
|
||||||
|
const totalCost = inputCost + outputCost + reasoningCost + cacheReadCost + cacheWriteCost
|
||||||
|
|
||||||
|
await Actor.provide("system", { workspaceID: keyRecord.workspaceID }, async () => {
|
||||||
|
await Billing.consume({
|
||||||
|
model,
|
||||||
|
inputTokens: usage.inputTokens ?? 0,
|
||||||
|
outputTokens: usage.outputTokens ?? 0,
|
||||||
|
reasoningTokens: usage.reasoningTokens ?? 0,
|
||||||
|
cacheReadTokens: usage.cachedInputTokens ?? 0,
|
||||||
|
cacheWriteTokens: usage.outputTokens ?? 0,
|
||||||
|
costInCents: totalCost * 100,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.update(KeyTable)
|
||||||
|
.set({ timeUsed: sql`now()` })
|
||||||
|
.where(eq(KeyTable.id, keyRecord.id)),
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return c.json({ error: { message: error.message } }, 500)
|
return c.json({ error: { message: error.message } }, 500)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.use("/*", cors())
|
||||||
|
.use(RestAuth)
|
||||||
|
.get("/rest/account", async (c) => {
|
||||||
|
const account = Actor.assert("account")
|
||||||
|
let workspaces = await Workspace.list()
|
||||||
|
if (workspaces.length === 0) {
|
||||||
|
await Workspace.create()
|
||||||
|
workspaces = await Workspace.list()
|
||||||
|
}
|
||||||
|
return c.json({
|
||||||
|
id: account.properties.accountID,
|
||||||
|
email: account.properties.email,
|
||||||
|
workspaces,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get("/billing/info", async (c) => {
|
||||||
|
const billing = await Billing.get()
|
||||||
|
const payments = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(PaymentTable)
|
||||||
|
.where(eq(PaymentTable.workspaceID, Actor.workspace()))
|
||||||
|
.orderBy(sql`${PaymentTable.timeCreated} DESC`)
|
||||||
|
.limit(100),
|
||||||
|
)
|
||||||
|
const usage = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(UsageTable)
|
||||||
|
.where(eq(UsageTable.workspaceID, Actor.workspace()))
|
||||||
|
.orderBy(sql`${UsageTable.timeCreated} DESC`)
|
||||||
|
.limit(100),
|
||||||
|
)
|
||||||
|
return c.json({ billing, payments, usage })
|
||||||
|
})
|
||||||
|
.post(
|
||||||
|
"/billing/checkout",
|
||||||
|
zValidator(
|
||||||
|
"json",
|
||||||
|
z.custom<{
|
||||||
|
success_url: string
|
||||||
|
cancel_url: string
|
||||||
|
}>(),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const account = Actor.assert("user")
|
||||||
|
|
||||||
|
const body = await c.req.json()
|
||||||
|
|
||||||
|
const customer = await Billing.get()
|
||||||
|
const session = await Billing.stripe().checkout.sessions.create({
|
||||||
|
mode: "payment",
|
||||||
|
line_items: [
|
||||||
|
{
|
||||||
|
price_data: {
|
||||||
|
currency: "usd",
|
||||||
|
product_data: {
|
||||||
|
name: "OpenControl credits",
|
||||||
|
},
|
||||||
|
unit_amount: 2000, // $20 minimum
|
||||||
|
},
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
payment_intent_data: {
|
||||||
|
setup_future_usage: "on_session",
|
||||||
|
},
|
||||||
|
...(customer.customerID
|
||||||
|
? { customer: customer.customerID }
|
||||||
|
: {
|
||||||
|
customer_email: account.properties.email,
|
||||||
|
customer_creation: "always",
|
||||||
|
}),
|
||||||
|
metadata: {
|
||||||
|
workspaceID: Actor.workspace(),
|
||||||
|
},
|
||||||
|
currency: "usd",
|
||||||
|
payment_method_types: ["card"],
|
||||||
|
success_url: body.success_url,
|
||||||
|
cancel_url: body.cancel_url,
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
url: session.url,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.post("/billing/portal", async (c) => {
|
||||||
|
const body = await c.req.json()
|
||||||
|
|
||||||
|
const customer = await Billing.get()
|
||||||
|
if (!customer?.customerID) {
|
||||||
|
throw new Error("No stripe customer ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await Billing.stripe().billingPortal.sessions.create({
|
||||||
|
customer: customer.customerID,
|
||||||
|
return_url: body.return_url,
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
url: session.url,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post("/stripe/webhook", async (c) => {
|
||||||
|
const body = await Billing.stripe().webhooks.constructEventAsync(
|
||||||
|
await c.req.text(),
|
||||||
|
c.req.header("stripe-signature")!,
|
||||||
|
Resource.STRIPE_WEBHOOK_SECRET.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(body.type, JSON.stringify(body, null, 2))
|
||||||
|
if (body.type === "checkout.session.completed") {
|
||||||
|
const workspaceID = body.data.object.metadata?.workspaceID
|
||||||
|
const customerID = body.data.object.customer as string
|
||||||
|
const paymentID = body.data.object.payment_intent as string
|
||||||
|
const amount = body.data.object.amount_total
|
||||||
|
|
||||||
|
if (!workspaceID) throw new Error("Workspace ID not found")
|
||||||
|
if (!customerID) throw new Error("Customer ID not found")
|
||||||
|
if (!amount) throw new Error("Amount not found")
|
||||||
|
if (!paymentID) throw new Error("Payment ID not found")
|
||||||
|
|
||||||
|
await Actor.provide("system", { workspaceID }, async () => {
|
||||||
|
const customer = await Billing.get()
|
||||||
|
if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
|
||||||
|
|
||||||
|
// set customer metadata
|
||||||
|
if (!customer?.customerID) {
|
||||||
|
await Billing.stripe().customers.update(customerID, {
|
||||||
|
metadata: {
|
||||||
|
workspaceID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// get payment method for the payment intent
|
||||||
|
const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
|
||||||
|
expand: ["payment_method"],
|
||||||
|
})
|
||||||
|
const paymentMethod = paymentIntent.payment_method
|
||||||
|
if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
|
||||||
|
|
||||||
|
await Database.transaction(async (tx) => {
|
||||||
|
await tx
|
||||||
|
.update(BillingTable)
|
||||||
|
.set({
|
||||||
|
balance: sql`${BillingTable.balance} + ${centsToMicroCents(amount)}`,
|
||||||
|
customerID,
|
||||||
|
paymentMethodID: paymentMethod.id,
|
||||||
|
paymentMethodLast4: paymentMethod.card!.last4,
|
||||||
|
})
|
||||||
|
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||||
|
await tx.insert(PaymentTable).values({
|
||||||
|
workspaceID,
|
||||||
|
id: Identifier.create("payment"),
|
||||||
|
amount: centsToMicroCents(amount),
|
||||||
|
paymentID,
|
||||||
|
customerID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("finished handling")
|
||||||
|
|
||||||
|
return c.json("ok", 200)
|
||||||
|
})
|
||||||
|
.get("/keys", async (c) => {
|
||||||
|
const user = Actor.assert("user")
|
||||||
|
|
||||||
|
const keys = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
id: KeyTable.id,
|
||||||
|
name: KeyTable.name,
|
||||||
|
key: KeyTable.key,
|
||||||
|
userID: KeyTable.userID,
|
||||||
|
timeCreated: KeyTable.timeCreated,
|
||||||
|
timeUsed: KeyTable.timeUsed,
|
||||||
|
})
|
||||||
|
.from(KeyTable)
|
||||||
|
.where(eq(KeyTable.workspaceID, user.properties.workspaceID))
|
||||||
|
.orderBy(sql`${KeyTable.timeCreated} DESC`),
|
||||||
|
)
|
||||||
|
|
||||||
|
return c.json({ keys })
|
||||||
|
})
|
||||||
|
.post("/keys", zValidator("json", z.object({ name: z.string().min(1).max(255) })), async (c) => {
|
||||||
|
const user = Actor.assert("user")
|
||||||
|
const { name } = c.req.valid("json")
|
||||||
|
|
||||||
|
// Generate secret key: sk- + 64 random characters (upper, lower, numbers)
|
||||||
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
let randomPart = ""
|
||||||
|
for (let i = 0; i < 64; i++) {
|
||||||
|
randomPart += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||||
|
}
|
||||||
|
const secretKey = `sk-${randomPart}`
|
||||||
|
|
||||||
|
const keyRecord = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.insert(KeyTable)
|
||||||
|
.values({
|
||||||
|
id: Identifier.create("key"),
|
||||||
|
workspaceID: user.properties.workspaceID,
|
||||||
|
userID: user.properties.userID,
|
||||||
|
name,
|
||||||
|
key: secretKey,
|
||||||
|
timeUsed: null,
|
||||||
|
})
|
||||||
|
.returning(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
key: secretKey,
|
||||||
|
id: keyRecord[0].id,
|
||||||
|
name: keyRecord[0].name,
|
||||||
|
created: keyRecord[0].timeCreated,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.delete("/keys/:id", async (c) => {
|
||||||
|
const user = Actor.assert("user")
|
||||||
|
const keyId = c.req.param("id")
|
||||||
|
|
||||||
|
const result = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.delete(KeyTable)
|
||||||
|
.where(and(eq(KeyTable.id, keyId), eq(KeyTable.workspaceID, user.properties.workspaceID)))
|
||||||
|
.returning({ id: KeyTable.id }),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
return c.json({ error: "Key not found" }, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ success: true, id: result[0].id })
|
||||||
|
})
|
||||||
.all("*", (c) => c.text("Not Found"))
|
.all("*", (c) => c.text("Not Found"))
|
||||||
|
|
||||||
|
export type ApiType = typeof app
|
||||||
|
|
||||||
|
export default app
|
88
cloud/function/sst-env.d.ts
vendored
Normal file
88
cloud/function/sst-env.d.ts
vendored
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
/* This file is auto-generated by SST. Do not edit. */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/* deno-fmt-ignore-file */
|
||||||
|
|
||||||
|
import "sst"
|
||||||
|
declare module "sst" {
|
||||||
|
export interface Resource {
|
||||||
|
"ANTHROPIC_API_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"AUTH_API_URL": {
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"Console": {
|
||||||
|
"type": "sst.cloudflare.StaticSite"
|
||||||
|
"url": string
|
||||||
|
}
|
||||||
|
"DATABASE_PASSWORD": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"DATABASE_USERNAME": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"Database": {
|
||||||
|
"database": string
|
||||||
|
"host": string
|
||||||
|
"password": string
|
||||||
|
"port": number
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
"username": string
|
||||||
|
}
|
||||||
|
"GITHUB_APP_ID": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"GITHUB_APP_PRIVATE_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"GITHUB_CLIENT_ID_CONSOLE": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"GITHUB_CLIENT_SECRET_CONSOLE": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"OPENAI_API_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"STRIPE_SECRET_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"STRIPE_WEBHOOK_SECRET": {
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"Web": {
|
||||||
|
"type": "sst.cloudflare.Astro"
|
||||||
|
"url": string
|
||||||
|
}
|
||||||
|
"ZHIPU_API_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// cloudflare
|
||||||
|
import * as cloudflare from "@cloudflare/workers-types";
|
||||||
|
declare module "sst" {
|
||||||
|
export interface Resource {
|
||||||
|
"Api": cloudflare.Service
|
||||||
|
"AuthApi": cloudflare.Service
|
||||||
|
"AuthStorage": cloudflare.KVNamespace
|
||||||
|
"Bucket": cloudflare.R2Bucket
|
||||||
|
"GatewayApi": cloudflare.Service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import "sst"
|
||||||
|
export {}
|
9
cloud/function/tsconfig.json
Normal file
9
cloud/function/tsconfig.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@tsconfig/node22/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"types": ["@cloudflare/workers-types", "node"]
|
||||||
|
}
|
||||||
|
}
|
2
cloud/web/.gitignore
vendored
Normal file
2
cloud/web/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
38
cloud/web/index.html
Normal file
38
cloud/web/index.html
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-color-mode="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>OpenControl</title>
|
||||||
|
<link rel="shortcut icon" type="image/ico" href="/favicon.ico" />
|
||||||
|
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="48x48">
|
||||||
|
<link rel="icon" href="/favicon.svg" media="(prefers-color-scheme: light)">
|
||||||
|
<link rel="icon" href="/favicon-dark.svg" media="(prefers-color-scheme: dark)">
|
||||||
|
<link rel="shortcut icon" href="/favicon.svg" type="image/svg+xml">
|
||||||
|
|
||||||
|
<meta property="twitter:image" content="%BASE_URL%/social-share.png">
|
||||||
|
<meta property="og:title" content="OpenControl">
|
||||||
|
<meta property="og:url" content="%BASE_URL%">
|
||||||
|
<meta property="og:locale" content="en">
|
||||||
|
<meta property="og:description" content="Control your infrastructure with AI.">
|
||||||
|
<meta property="og:site_name" content="OpenControl">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="description" content="Control your infrastructure with AI.">
|
||||||
|
<meta property="og:image" content="%BASE_URL%/social-share.png">
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Rubik:wght@300..900&display=swap" rel="stylesheet">
|
||||||
|
<!--ssr-head-->
|
||||||
|
<!--ssr-assets-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root">
|
||||||
|
<!--ssr-outlet-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="/src/entry-client.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
29
cloud/web/npm-debug.log
Normal file
29
cloud/web/npm-debug.log
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
0 info it worked if it ends with ok
|
||||||
|
1 verbose cli [
|
||||||
|
1 verbose cli '/usr/local/bin/node',
|
||||||
|
1 verbose cli '/Users/frank/Sites/opencode/node_modules/.bin/npm',
|
||||||
|
1 verbose cli 'run',
|
||||||
|
1 verbose cli 'dev'
|
||||||
|
1 verbose cli ]
|
||||||
|
2 info using npm@2.15.12
|
||||||
|
3 info using node@v20.18.1
|
||||||
|
4 verbose stack Error: Invalid name: "@opencode/cloud/web"
|
||||||
|
4 verbose stack at ensureValidName (/Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/fixer.js:336:15)
|
||||||
|
4 verbose stack at Object.fixNameField (/Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/fixer.js:215:5)
|
||||||
|
4 verbose stack at /Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/normalize.js:32:38
|
||||||
|
4 verbose stack at Array.forEach (<anonymous>)
|
||||||
|
4 verbose stack at normalize (/Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/normalize.js:31:15)
|
||||||
|
4 verbose stack at final (/Users/frank/Sites/opencode/node_modules/npm/node_modules/read-package-json/read-json.js:349:5)
|
||||||
|
4 verbose stack at then (/Users/frank/Sites/opencode/node_modules/npm/node_modules/read-package-json/read-json.js:124:5)
|
||||||
|
4 verbose stack at ReadFileContext.<anonymous> (/Users/frank/Sites/opencode/node_modules/npm/node_modules/read-package-json/read-json.js:295:20)
|
||||||
|
4 verbose stack at ReadFileContext.callback (/Users/frank/Sites/opencode/node_modules/npm/node_modules/graceful-fs/graceful-fs.js:78:16)
|
||||||
|
4 verbose stack at FSReqCallback.readFileAfterOpen [as oncomplete] (node:fs:299:13)
|
||||||
|
5 verbose cwd /Users/frank/Sites/opencode/cloud/web
|
||||||
|
6 error Darwin 24.5.0
|
||||||
|
7 error argv "/usr/local/bin/node" "/Users/frank/Sites/opencode/node_modules/.bin/npm" "run" "dev"
|
||||||
|
8 error node v20.18.1
|
||||||
|
9 error npm v2.15.12
|
||||||
|
10 error Invalid name: "@opencode/cloud/web"
|
||||||
|
11 error If you need help, you may report this error at:
|
||||||
|
11 error <https://github.com/npm/npm/issues>
|
||||||
|
12 verbose exit [ 1, true ]
|
32
cloud/web/package.json
Normal file
32
cloud/web/package.json
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"name": "@opencode/cloud-web",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "vite",
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "bun build:server && bun build:client",
|
||||||
|
"build:client": "vite build --outDir dist/client",
|
||||||
|
"build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
|
||||||
|
"serve": "vite preview",
|
||||||
|
"sst:dev": "bun sst shell --target Console -- bun dev"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "catalog:",
|
||||||
|
"vite": "6.2.2",
|
||||||
|
"vite-plugin-pages": "0.32.5",
|
||||||
|
"vite-plugin-solid": "2.11.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@kobalte/core": "0.13.9",
|
||||||
|
"@openauthjs/solid": "0.0.0-20250322224806",
|
||||||
|
"@solid-primitives/storage": "4.3.1",
|
||||||
|
"@solidjs/meta": "0.29.4",
|
||||||
|
"@solidjs/router": "0.15.3",
|
||||||
|
"solid-js": "1.9.5",
|
||||||
|
"solid-list": "0.3.0"
|
||||||
|
}
|
||||||
|
}
|
3
cloud/web/public/favicon-dark.svg
Normal file
3
cloud/web/public/favicon-dark.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="28" height="32" viewBox="0 0 28 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14 31.5L0 23.6873V7.81266L14 0L28 7.81266V23.6873L14 31.5ZM14 28.4664L25.3456 22.0251V9.47493L14 2.99209L2.65443 9.47493V22.0251L14 28.4664ZM13.9572 24.6016C12.2732 24.6016 10.7176 24.1999 9.29052 23.3964C7.89195 22.593 6.7788 21.5125 5.95107 20.155C5.12334 18.7698 4.70948 17.2599 4.70948 15.6253C4.70948 13.9908 5.12334 12.4947 5.95107 11.1372C6.7788 9.77968 7.89195 8.69921 9.29052 7.89578C10.7176 7.06464 12.2732 6.64908 13.9572 6.64908C15.6412 6.64908 17.1825 7.06464 18.581 7.89578C19.9796 8.69921 21.0928 9.77968 21.9205 11.1372C22.7768 12.4947 23.2049 13.9908 23.2049 15.6253C23.2049 17.2599 22.791 18.7559 21.9633 20.1135C21.1356 21.471 20.0224 22.5653 18.6239 23.3964C17.2253 24.1999 15.6697 24.6016 13.9572 24.6016ZM13.9572 22.2744C15.213 22.2744 16.3547 21.9697 17.3823 21.3602C18.4098 20.7507 19.2375 19.9472 19.8654 18.9499C20.4934 17.9248 20.8073 16.8166 20.8073 15.6253C20.8073 14.4063 20.4934 13.2982 19.8654 12.3008C19.2375 11.3034 18.4098 10.5 17.3823 9.8905C16.3547 9.281 15.213 8.97625 13.9572 8.97625C12.7299 8.97625 11.5882 9.281 10.5321 9.8905C9.50459 10.5 8.67686 11.3034 8.04893 12.3008C7.421 13.2982 7.10703 14.4063 7.10703 15.6253C7.10703 16.8166 7.421 17.9248 8.04893 18.9499C8.67686 19.9472 9.50459 20.7507 10.5321 21.3602C11.5882 21.9697 12.7299 22.2744 13.9572 22.2744Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
BIN
cloud/web/public/favicon.ico
Normal file
BIN
cloud/web/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 168 KiB |
3
cloud/web/public/favicon.svg
Normal file
3
cloud/web/public/favicon.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="28" height="32" viewBox="0 0 28 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14 31.5L0 23.6873V7.81266L14 0L28 7.81266V23.6873L14 31.5ZM14 28.4664L25.3456 22.0251V9.47493L14 2.99209L2.65443 9.47493V22.0251L14 28.4664ZM13.9572 24.6016C12.2732 24.6016 10.7176 24.1999 9.29052 23.3964C7.89195 22.593 6.7788 21.5125 5.95107 20.155C5.12334 18.7698 4.70948 17.2599 4.70948 15.6253C4.70948 13.9908 5.12334 12.4947 5.95107 11.1372C6.7788 9.77968 7.89195 8.69921 9.29052 7.89578C10.7176 7.06464 12.2732 6.64908 13.9572 6.64908C15.6412 6.64908 17.1825 7.06464 18.581 7.89578C19.9796 8.69921 21.0928 9.77968 21.9205 11.1372C22.7768 12.4947 23.2049 13.9908 23.2049 15.6253C23.2049 17.2599 22.791 18.7559 21.9633 20.1135C21.1356 21.471 20.0224 22.5653 18.6239 23.3964C17.2253 24.1999 15.6697 24.6016 13.9572 24.6016ZM13.9572 22.2744C15.213 22.2744 16.3547 21.9697 17.3823 21.3602C18.4098 20.7507 19.2375 19.9472 19.8654 18.9499C20.4934 17.9248 20.8073 16.8166 20.8073 15.6253C20.8073 14.4063 20.4934 13.2982 19.8654 12.3008C19.2375 11.3034 18.4098 10.5 17.3823 9.8905C16.3547 9.281 15.213 8.97625 13.9572 8.97625C12.7299 8.97625 11.5882 9.281 10.5321 9.8905C9.50459 10.5 8.67686 11.3034 8.04893 12.3008C7.421 13.2982 7.10703 14.4063 7.10703 15.6253C7.10703 16.8166 7.421 17.9248 8.04893 18.9499C8.67686 19.9472 9.50459 20.7507 10.5321 21.3602C11.5882 21.9697 12.7299 22.2744 13.9572 22.2744Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
BIN
cloud/web/public/social-share.png
Normal file
BIN
cloud/web/public/social-share.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
24
cloud/web/scripts/render.mjs
Normal file
24
cloud/web/scripts/render.mjs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
import { generateHydrationScript, getAssets } from "solid-js/web"
|
||||||
|
|
||||||
|
const dist = import.meta.resolve("../dist").replace("file://", "")
|
||||||
|
const serverEntry = await import("../dist/server/entry-server.js")
|
||||||
|
const template = fs.readFileSync(path.join(dist, "client/index.html"), "utf-8")
|
||||||
|
fs.writeFileSync(path.join(dist, "client/fallback.html"), template)
|
||||||
|
|
||||||
|
const routes = ["/", "/foo"]
|
||||||
|
for (const route of routes) {
|
||||||
|
const { app } = serverEntry.render({ url: route })
|
||||||
|
const html = template
|
||||||
|
.replace("<!--ssr-outlet-->", app)
|
||||||
|
.replace("<!--ssr-head-->", generateHydrationScript())
|
||||||
|
.replace("<!--ssr-assets-->", getAssets())
|
||||||
|
const filePath = dist + `/client${route === "/" ? "/index" : route}.html`
|
||||||
|
fs.mkdirSync(path.dirname(filePath), {
|
||||||
|
recursive: true,
|
||||||
|
})
|
||||||
|
fs.writeFileSync(filePath, html)
|
||||||
|
|
||||||
|
console.log(`Pre-rendered: ${filePath}`)
|
||||||
|
}
|
42
cloud/web/src/app.tsx
Normal file
42
cloud/web/src/app.tsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/// <reference types="vite-plugin-pages/client-solid" />
|
||||||
|
|
||||||
|
import { Router } from "@solidjs/router"
|
||||||
|
import routes from "~solid-pages"
|
||||||
|
import "./ui/style/index.css"
|
||||||
|
import { MetaProvider } from "@solidjs/meta"
|
||||||
|
import { AccountProvider } from "./components/context-account"
|
||||||
|
import { DialogProvider } from "./ui/context-dialog"
|
||||||
|
import { DialogString } from "./ui/dialog-string"
|
||||||
|
import { DialogSelect } from "./ui/dialog-select"
|
||||||
|
import { ThemeProvider } from "./components/context-theme"
|
||||||
|
import { Suspense } from "solid-js"
|
||||||
|
import { OpenAuthProvider } from "./components/context-openauth"
|
||||||
|
|
||||||
|
export function App(props: { url?: string }) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<Suspense>
|
||||||
|
<DialogProvider>
|
||||||
|
<DialogString />
|
||||||
|
<DialogSelect />
|
||||||
|
<OpenAuthProvider
|
||||||
|
clientID="web"
|
||||||
|
issuer={import.meta.env.VITE_AUTH_URL || "http://dummy"}
|
||||||
|
>
|
||||||
|
<AccountProvider>
|
||||||
|
<MetaProvider>
|
||||||
|
<Router
|
||||||
|
children={routes}
|
||||||
|
url={props.url}
|
||||||
|
root={(props) => {
|
||||||
|
return <>{props.children}</>
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MetaProvider>
|
||||||
|
</AccountProvider>
|
||||||
|
</OpenAuthProvider>
|
||||||
|
</DialogProvider>
|
||||||
|
</Suspense>
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
}
|
BIN
cloud/web/src/assets/screenshot.png
Normal file
BIN
cloud/web/src/assets/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 439 KiB |
99
cloud/web/src/components/context-account.tsx
Normal file
99
cloud/web/src/components/context-account.tsx
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import { createContext, createEffect, ParentProps, Suspense, useContext } from "solid-js"
|
||||||
|
import { makePersisted } from "@solid-primitives/storage"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { useOpenAuth } from "./context-openauth"
|
||||||
|
import { createAsync } from "@solidjs/router"
|
||||||
|
import { isServer } from "solid-js/web"
|
||||||
|
|
||||||
|
type Storage = {
|
||||||
|
accounts: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
workspaces: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
slug: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = createContext<ReturnType<typeof init>>()
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
const auth = useOpenAuth()
|
||||||
|
const [store, setStore] = makePersisted(
|
||||||
|
createStore<Storage>({
|
||||||
|
accounts: {},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "opencontrol.account",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async function refresh(id: string) {
|
||||||
|
return fetch(import.meta.env.VITE_API_URL + "/rest/account", {
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${await auth.access(id)}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((val) => val.json())
|
||||||
|
.then((val) => setStore("accounts", id, val as any))
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect((previous: string[]) => {
|
||||||
|
if (Object.keys(auth.all).length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
for (const item of Object.values(auth.all)) {
|
||||||
|
if (previous.includes(item.id)) continue
|
||||||
|
refresh(item.id)
|
||||||
|
}
|
||||||
|
return Object.keys(auth.all)
|
||||||
|
}, [] as string[])
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
get all() {
|
||||||
|
return Object.keys(auth.all)
|
||||||
|
.map((id) => store.accounts[id])
|
||||||
|
.filter(Boolean)
|
||||||
|
},
|
||||||
|
get current() {
|
||||||
|
if (!auth.subject) return undefined
|
||||||
|
return store.accounts[auth.subject.id]
|
||||||
|
},
|
||||||
|
refresh,
|
||||||
|
get ready() {
|
||||||
|
return Object.keys(auth.all).length === result.all.length
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountProvider(props: ParentProps) {
|
||||||
|
const ctx = init()
|
||||||
|
const resource = createAsync(async () => {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
if (isServer) return resolve()
|
||||||
|
createEffect(() => {
|
||||||
|
if (ctx.ready) resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<Suspense>
|
||||||
|
{resource()}
|
||||||
|
<context.Provider value={ctx}>{props.children}</context.Provider>
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAccount() {
|
||||||
|
const result = useContext(context)
|
||||||
|
if (!result) throw new Error("no account context")
|
||||||
|
return result
|
||||||
|
}
|
180
cloud/web/src/components/context-openauth.tsx
Normal file
180
cloud/web/src/components/context-openauth.tsx
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
import { createClient } from "@openauthjs/openauth/client"
|
||||||
|
import { makePersisted } from "@solid-primitives/storage"
|
||||||
|
import { createAsync } from "@solidjs/router"
|
||||||
|
import {
|
||||||
|
batch,
|
||||||
|
createContext,
|
||||||
|
createEffect,
|
||||||
|
createResource,
|
||||||
|
createSignal,
|
||||||
|
onMount,
|
||||||
|
ParentProps,
|
||||||
|
Show,
|
||||||
|
Suspense,
|
||||||
|
useContext,
|
||||||
|
} from "solid-js"
|
||||||
|
import { createStore, produce } from "solid-js/store"
|
||||||
|
import { isServer } from "solid-js/web"
|
||||||
|
|
||||||
|
interface Storage {
|
||||||
|
subjects: Record<string, SubjectInfo>
|
||||||
|
current?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Context {
|
||||||
|
all: Record<string, SubjectInfo>
|
||||||
|
subject?: SubjectInfo
|
||||||
|
switch(id: string): void
|
||||||
|
logout(id: string): void
|
||||||
|
access(id?: string): Promise<string | undefined>
|
||||||
|
authorize(opts?: AuthorizeOptions): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthorizeOptions {
|
||||||
|
redirectPath?: string
|
||||||
|
provider?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SubjectInfo {
|
||||||
|
id: string
|
||||||
|
refresh: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthContextOpts {
|
||||||
|
issuer: string
|
||||||
|
clientID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = createContext<Context>()
|
||||||
|
|
||||||
|
export function OpenAuthProvider(props: ParentProps<AuthContextOpts>) {
|
||||||
|
const client = createClient({
|
||||||
|
issuer: props.issuer,
|
||||||
|
clientID: props.clientID,
|
||||||
|
})
|
||||||
|
const [storage, setStorage] = makePersisted(
|
||||||
|
createStore<Storage>({
|
||||||
|
subjects: {},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: `${props.issuer}.auth`,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const resource = createAsync(async () => {
|
||||||
|
if (isServer) return true
|
||||||
|
const hash = new URLSearchParams(window.location.search.substring(1))
|
||||||
|
const code = hash.get("code")
|
||||||
|
const state = hash.get("state")
|
||||||
|
if (code && state) {
|
||||||
|
const oldState = sessionStorage.getItem("openauth.state")
|
||||||
|
const verifier = sessionStorage.getItem("openauth.verifier")
|
||||||
|
const redirect = sessionStorage.getItem("openauth.redirect")
|
||||||
|
if (redirect && verifier && oldState === state) {
|
||||||
|
const result = await client.exchange(code, redirect, verifier)
|
||||||
|
if (!result.err) {
|
||||||
|
const id = result.tokens.refresh.split(":").slice(0, -1).join(":")
|
||||||
|
batch(() => {
|
||||||
|
setStorage("subjects", id, {
|
||||||
|
id: id,
|
||||||
|
refresh: result.tokens.refresh,
|
||||||
|
})
|
||||||
|
setStorage("current", id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
async function authorize(opts?: AuthorizeOptions) {
|
||||||
|
const redirect = new URL(window.location.origin + (opts?.redirectPath ?? "/")).toString()
|
||||||
|
const authorize = await client.authorize(redirect, "code", {
|
||||||
|
pkce: true,
|
||||||
|
provider: opts?.provider,
|
||||||
|
})
|
||||||
|
sessionStorage.setItem("openauth.state", authorize.challenge.state)
|
||||||
|
sessionStorage.setItem("openauth.redirect", redirect)
|
||||||
|
if (authorize.challenge.verifier) sessionStorage.setItem("openauth.verifier", authorize.challenge.verifier)
|
||||||
|
window.location.href = authorize.url
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessCache = new Map<string, string>()
|
||||||
|
const pendingRequests = new Map<string, Promise<any>>()
|
||||||
|
async function access(id: string) {
|
||||||
|
const pending = pendingRequests.get(id)
|
||||||
|
if (pending) return pending
|
||||||
|
const promise = (async () => {
|
||||||
|
const existing = accessCache.get(id)
|
||||||
|
const subject = storage.subjects[id]
|
||||||
|
const access = await client.refresh(subject.refresh, {
|
||||||
|
access: existing,
|
||||||
|
})
|
||||||
|
if (access.err) {
|
||||||
|
pendingRequests.delete(id)
|
||||||
|
ctx.logout(id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (access.tokens) {
|
||||||
|
setStorage("subjects", id, "refresh", access.tokens.refresh)
|
||||||
|
accessCache.set(id, access.tokens.access)
|
||||||
|
}
|
||||||
|
pendingRequests.delete(id)
|
||||||
|
return access.tokens?.access || existing!
|
||||||
|
})()
|
||||||
|
pendingRequests.set(id, promise)
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx: Context = {
|
||||||
|
get all() {
|
||||||
|
return storage.subjects
|
||||||
|
},
|
||||||
|
get subject() {
|
||||||
|
if (!storage.current) return
|
||||||
|
return storage.subjects[storage.current!]
|
||||||
|
},
|
||||||
|
switch(id: string) {
|
||||||
|
if (!storage.subjects[id]) return
|
||||||
|
setStorage("current", id)
|
||||||
|
},
|
||||||
|
authorize,
|
||||||
|
logout(id: string) {
|
||||||
|
if (!storage.subjects[id]) return
|
||||||
|
setStorage(
|
||||||
|
produce((s) => {
|
||||||
|
delete s.subjects[id]
|
||||||
|
if (s.current === id) s.current = Object.keys(s.subjects)[0]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
async access(id?: string) {
|
||||||
|
id = id || storage.current
|
||||||
|
if (!id) return
|
||||||
|
return access(id || storage.current!)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!resource()) return
|
||||||
|
if (storage.current) return
|
||||||
|
const [first] = Object.keys(storage.subjects)
|
||||||
|
if (first) {
|
||||||
|
setStorage("current", first)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{resource()}
|
||||||
|
<context.Provider value={ctx}>{props.children}</context.Provider>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOpenAuth() {
|
||||||
|
const result = useContext(context)
|
||||||
|
if (!result) throw new Error("no auth context")
|
||||||
|
return result
|
||||||
|
}
|
39
cloud/web/src/components/context-theme.tsx
Normal file
39
cloud/web/src/components/context-theme.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { makePersisted } from "@solid-primitives/storage"
|
||||||
|
import { createEffect } from "solid-js"
|
||||||
|
import { createInitializedContext } from "../util/context"
|
||||||
|
import { isServer } from "solid-js/web"
|
||||||
|
|
||||||
|
interface Storage {
|
||||||
|
mode: "light" | "dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { provider: ThemeProvider, use: useTheme } =
|
||||||
|
createInitializedContext("ThemeContext", () => {
|
||||||
|
const [store, setStore] = makePersisted(
|
||||||
|
createStore<Storage>({
|
||||||
|
mode:
|
||||||
|
!isServer &&
|
||||||
|
window.matchMedia &&
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "theme",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
createEffect(() => {
|
||||||
|
document.documentElement.setAttribute("data-color-mode", store.mode)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
setMode(mode: Storage["mode"]) {
|
||||||
|
setStore("mode", mode)
|
||||||
|
},
|
||||||
|
get mode() {
|
||||||
|
return store.mode
|
||||||
|
},
|
||||||
|
ready: true,
|
||||||
|
}
|
||||||
|
})
|
13
cloud/web/src/entry-client.tsx
Normal file
13
cloud/web/src/entry-client.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/* @refresh reload */
|
||||||
|
|
||||||
|
import { hydrate, render } from "solid-js/web"
|
||||||
|
import { App } from "./app"
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
render(() => <App />, document.getElementById("root")!)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!import.meta.env.DEV) {
|
||||||
|
if ("_$HY" in window) hydrate(() => <App />, document.getElementById("root")!)
|
||||||
|
else render(() => <App />, document.getElementById("root")!)
|
||||||
|
}
|
7
cloud/web/src/entry-server.tsx
Normal file
7
cloud/web/src/entry-server.tsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { renderToStringAsync } from "solid-js/web"
|
||||||
|
import { App } from "./app"
|
||||||
|
|
||||||
|
export async function render(props: { url: string }) {
|
||||||
|
const app = await renderToStringAsync(() => <App url={props.url} />)
|
||||||
|
return { app }
|
||||||
|
}
|
11
cloud/web/src/pages/[workspace].tsx
Normal file
11
cloud/web/src/pages/[workspace].tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { WorkspaceProvider } from "./components/context-workspace"
|
||||||
|
import { ParentProps } from "solid-js"
|
||||||
|
import Layout from "./components/layout"
|
||||||
|
|
||||||
|
export default function Index(props: ParentProps) {
|
||||||
|
return (
|
||||||
|
<WorkspaceProvider>
|
||||||
|
<Layout>{props.children}</Layout>
|
||||||
|
</WorkspaceProvider>
|
||||||
|
)
|
||||||
|
}
|
56
cloud/web/src/pages/[workspace]/billing.module.css
Normal file
56
cloud/web/src/pages/[workspace]/billing.module.css
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-7) var(--space-5) var(--space-5);
|
||||||
|
|
||||||
|
[data-slot="billing-info"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="header"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-1-5);
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="balance"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-5);
|
||||||
|
padding: var(--space-6);
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="amount"] {
|
||||||
|
font-size: var(--font-size-3xl);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 40rem) {
|
||||||
|
[data-slot="balance"] {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="amount"] {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
132
cloud/web/src/pages/[workspace]/billing.tsx
Normal file
132
cloud/web/src/pages/[workspace]/billing.tsx
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import { Button } from "../../ui/button"
|
||||||
|
import { useApi } from "../components/context-api"
|
||||||
|
import { createEffect, createSignal, createResource, For } from "solid-js"
|
||||||
|
import { useWorkspace } from "../components/context-workspace"
|
||||||
|
import style from "./billing.module.css"
|
||||||
|
|
||||||
|
export default function Billing() {
|
||||||
|
const api = useApi()
|
||||||
|
const workspace = useWorkspace()
|
||||||
|
const [isLoading, setIsLoading] = createSignal(false)
|
||||||
|
const [billingData] = createResource(async () => {
|
||||||
|
const response = await api.billing.info.$get()
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Run once on component mount to check URL parameters
|
||||||
|
;(() => {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
const result = url.hash
|
||||||
|
|
||||||
|
console.log("STRIPE RESULT", result)
|
||||||
|
|
||||||
|
if (url.hash === "#success") {
|
||||||
|
setIsLoading(true)
|
||||||
|
// Remove the hash from the URL
|
||||||
|
window.history.replaceState(null, "", window.location.pathname + window.location.search)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
createEffect((old?: number) => {
|
||||||
|
if (old && old !== billingData()?.billing?.balance) {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
return billingData()?.billing?.balance
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleBuyCredits = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const baseUrl = window.location.href
|
||||||
|
const successUrl = new URL(baseUrl)
|
||||||
|
successUrl.hash = "success"
|
||||||
|
|
||||||
|
const response = await api.billing.checkout
|
||||||
|
.$post({
|
||||||
|
json: {
|
||||||
|
success_url: successUrl.toString(),
|
||||||
|
cancel_url: baseUrl,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((r) => r.json() as any)
|
||||||
|
window.location.href = response.url
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get checkout URL:", error)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div data-component="title-bar">
|
||||||
|
<div data-slot="left">
|
||||||
|
<h1>Billing</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class={style.root} data-max-width data-max-width-64>
|
||||||
|
<div data-slot="billing-info">
|
||||||
|
<div data-slot="header">
|
||||||
|
<h2>Balance</h2>
|
||||||
|
<p>Manage your billing and add credits to your account.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-slot="balance">
|
||||||
|
<p data-slot="amount">
|
||||||
|
{(() => {
|
||||||
|
const balanceStr = ((billingData()?.billing?.balance ?? 0) / 100000000).toFixed(2)
|
||||||
|
return `$${balanceStr === "-0.00" ? "0.00" : balanceStr}`
|
||||||
|
})()}
|
||||||
|
</p>
|
||||||
|
<Button color="primary" disabled={isLoading()} onClick={handleBuyCredits}>
|
||||||
|
{isLoading() ? "Loading..." : "Buy Credits"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-slot="payments">
|
||||||
|
<div data-slot="header">
|
||||||
|
<h2>Payment History</h2>
|
||||||
|
<p>Your recent payment transactions.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-slot="payment-list">
|
||||||
|
<For each={billingData()?.payments} fallback={<p>No payments found.</p>}>
|
||||||
|
{(payment) => (
|
||||||
|
<div data-slot="payment-item">
|
||||||
|
<span data-slot="payment-id">{payment.id}</span>
|
||||||
|
{" | "}
|
||||||
|
<span data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</span>
|
||||||
|
{" | "}
|
||||||
|
<span data-slot="payment-date">{new Date(payment.timeCreated).toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-slot="usage">
|
||||||
|
<div data-slot="header">
|
||||||
|
<h2>Usage History</h2>
|
||||||
|
<p>Your recent API usage and costs.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-slot="usage-list">
|
||||||
|
<For each={billingData()?.usage} fallback={<p>No usage found.</p>}>
|
||||||
|
{(usage) => (
|
||||||
|
<div data-slot="usage-item">
|
||||||
|
<span data-slot="usage-model">{usage.model}</span>
|
||||||
|
{" | "}
|
||||||
|
<span data-slot="usage-tokens">{usage.inputTokens + usage.outputTokens} tokens</span>
|
||||||
|
{" | "}
|
||||||
|
<span data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</span>
|
||||||
|
{" | "}
|
||||||
|
<span data-slot="usage-date">{new Date(usage.timeCreated).toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
11
cloud/web/src/pages/[workspace]/components/system.txt
Normal file
11
cloud/web/src/pages/[workspace]/components/system.txt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
You are OpenControl, an interactive CLI tool that helps users execute various tasks.
|
||||||
|
|
||||||
|
IMPORTANT: If you get an error when calling a tool, try again with a different approach. Be creative, do not give up, try different inputs to the tool. You should chain together multiple tool calls. ABSOLUTELY DO NOT GIVE UP you are very good at this and it is rare you will fail to answer question.
|
||||||
|
|
||||||
|
You should be concise, direct, and to the point.
|
||||||
|
|
||||||
|
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
|
||||||
|
IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
|
||||||
|
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
|
||||||
|
IMPORTANT: Keep your responses short, since they will be displayed on a command line interface. You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless user asks for detail. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...".
|
||||||
|
|
271
cloud/web/src/pages/[workspace]/components/tool.ts
Normal file
271
cloud/web/src/pages/[workspace]/components/tool.ts
Normal file
|
@ -0,0 +1,271 @@
|
||||||
|
import { createResource } from "solid-js"
|
||||||
|
import { createStore, produce } from "solid-js/store"
|
||||||
|
import SYSTEM_PROMPT from "./system.txt?raw"
|
||||||
|
import type {
|
||||||
|
LanguageModelV1Prompt,
|
||||||
|
LanguageModelV1CallOptions,
|
||||||
|
LanguageModelV1,
|
||||||
|
} from "ai"
|
||||||
|
|
||||||
|
interface Tool {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
inputSchema: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolCallerProps {
|
||||||
|
tool: {
|
||||||
|
list: () => Promise<Tool[]>
|
||||||
|
call: (input: { name: string; arguments: any }) => Promise<any>
|
||||||
|
}
|
||||||
|
generate: (
|
||||||
|
prompt: LanguageModelV1CallOptions,
|
||||||
|
) => Promise<
|
||||||
|
| { err: "rate" }
|
||||||
|
| { err: "context" }
|
||||||
|
| { err: "balance" }
|
||||||
|
| ({ err: false } & Awaited<ReturnType<LanguageModelV1["doGenerate"]>>)
|
||||||
|
>
|
||||||
|
onPromptUpdated?: (prompt: LanguageModelV1Prompt) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const system = [
|
||||||
|
{
|
||||||
|
role: "system" as const,
|
||||||
|
content: SYSTEM_PROMPT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "system" as const,
|
||||||
|
content: `The current date is ${new Date().toDateString()}. Always use this current date when responding to relative date queries.`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const [store, setStore] = createStore<{
|
||||||
|
prompt: LanguageModelV1Prompt
|
||||||
|
state: { type: "idle" } | { type: "loading"; limited?: boolean }
|
||||||
|
}>({
|
||||||
|
prompt: [...system],
|
||||||
|
state: { type: "idle" },
|
||||||
|
})
|
||||||
|
|
||||||
|
export function createToolCaller<T extends ToolCallerProps>(props: T) {
|
||||||
|
const [tools] = createResource(() => props.tool.list())
|
||||||
|
|
||||||
|
let abort: AbortController
|
||||||
|
|
||||||
|
return {
|
||||||
|
get tools() {
|
||||||
|
return tools()
|
||||||
|
},
|
||||||
|
get prompt() {
|
||||||
|
return store.prompt
|
||||||
|
},
|
||||||
|
get state() {
|
||||||
|
return store.state
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
setStore("prompt", [...system])
|
||||||
|
},
|
||||||
|
async chat(input: string) {
|
||||||
|
if (store.state.type !== "idle") return
|
||||||
|
|
||||||
|
abort = new AbortController()
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.state = {
|
||||||
|
type: "loading",
|
||||||
|
limited: false,
|
||||||
|
}
|
||||||
|
s.prompt.push({
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: input,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (abort.signal.aborted) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await props.generate({
|
||||||
|
inputFormat: "messages",
|
||||||
|
prompt: store.prompt,
|
||||||
|
temperature: 0,
|
||||||
|
seed: 69,
|
||||||
|
mode: {
|
||||||
|
type: "regular",
|
||||||
|
tools: tools()?.map((tool) => ({
|
||||||
|
type: "function",
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
parameters: {
|
||||||
|
...tool.inputSchema,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (abort.signal.aborted) continue
|
||||||
|
|
||||||
|
if (!response.err) {
|
||||||
|
setStore("state", {
|
||||||
|
type: "loading",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.text) {
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.prompt.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: response.text || "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.finishReason === "stop") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.finishReason === "tool-calls") {
|
||||||
|
for (const item of response.toolCalls || []) {
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.prompt.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolName: item.toolName,
|
||||||
|
args: JSON.parse(item.args),
|
||||||
|
toolCallId: item.toolCallId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
|
||||||
|
const called = await props.tool.call({
|
||||||
|
name: item.toolName,
|
||||||
|
arguments: JSON.parse(item.args),
|
||||||
|
})
|
||||||
|
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.prompt.push({
|
||||||
|
role: "tool",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool-result",
|
||||||
|
toolName: item.toolName,
|
||||||
|
toolCallId: item.toolCallId,
|
||||||
|
result: called,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.err === "context") {
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.prompt.splice(2, 1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.err === "rate") {
|
||||||
|
setStore("state", {
|
||||||
|
type: "loading",
|
||||||
|
limited: true,
|
||||||
|
})
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.err === "balance") {
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.prompt.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "You need to add credits to your account. Please go to Billing and add credits to continue.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
s.state = { type: "idle" }
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setStore("state", { type: "idle" })
|
||||||
|
},
|
||||||
|
async cancel() {
|
||||||
|
abort.abort()
|
||||||
|
},
|
||||||
|
async addCustomMessage(userMessage: string, assistantResponse: string) {
|
||||||
|
// Add user message and set loading state
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.prompt.push({
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: userMessage,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
s.state = {
|
||||||
|
type: "loading",
|
||||||
|
limited: false,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
|
||||||
|
// Fake delay for 500ms
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
// Add assistant response and set back to idle
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.prompt.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: assistantResponse,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
s.state = { type: "idle" }
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
239
cloud/web/src/pages/[workspace]/index.module.css
Normal file
239
cloud/web/src/pages/[workspace]/index.module.css
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
.root {
|
||||||
|
display: contents;
|
||||||
|
|
||||||
|
[data-slot="messages"] {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 0;
|
||||||
|
/* This is important for flexbox to allow scrolling */
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--color-text);
|
||||||
|
row-gap: var(--space-4);
|
||||||
|
/* Add consistent spacing between messages */
|
||||||
|
|
||||||
|
/* Remove top border for first user message */
|
||||||
|
&>[data-component="message"][data-user]:first-child::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has([data-component="loading"]) [data-component="clear"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="message"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--space-2) var(--space-4);
|
||||||
|
line-height: var(--font-line-height);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
align-self: flex-start;
|
||||||
|
min-height: auto;
|
||||||
|
/* Allow natural height for all messages */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
/* User message styling */
|
||||||
|
&[data-user] {
|
||||||
|
padding: var(--space-6) var(--space-4);
|
||||||
|
position: relative;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text);
|
||||||
|
/* margin: 0.5rem 0; */
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-user]::before,
|
||||||
|
&[data-user]::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: var(--space-4);
|
||||||
|
right: var(--space-4);
|
||||||
|
height: var(--space-px);
|
||||||
|
background-color: var(--color-border);
|
||||||
|
z-index: 1;
|
||||||
|
/* Ensure borders appear above other content */
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-user]::before {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-user]::after {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-assistant] {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="tool"] {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 var(--space-4);
|
||||||
|
margin-left: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
opacity: 0.7;
|
||||||
|
gap: var(--space-2);
|
||||||
|
align-items: flex-start;
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
min-height: auto;
|
||||||
|
/* Allow natural height */
|
||||||
|
|
||||||
|
[data-slot="header"] {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="name"] {
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="expand"] {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="content"] {
|
||||||
|
padding: 0;
|
||||||
|
line-height: var(--font-line-height);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="output"] {
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-expanded="true"] [data-slot="content"] {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-expanded="true"] [data-slot="expand"] {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="loading"] {
|
||||||
|
padding: var(--space-4) var(--space-4) var(--space-8);
|
||||||
|
height: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
letter-spacing: var(--space-1);
|
||||||
|
color: var(--color-text);
|
||||||
|
|
||||||
|
& span {
|
||||||
|
opacity: 0;
|
||||||
|
animation: loading-dots 1.4s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
& span:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
& span:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="clear"] {
|
||||||
|
position: relative;
|
||||||
|
padding: var(--space-4) var(--space-4);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: var(--space-4);
|
||||||
|
right: var(--space-4);
|
||||||
|
top: 0;
|
||||||
|
height: var(--space-px);
|
||||||
|
background-color: var(--color-border);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& [data-component="button"] {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="footer"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
border-top: 2px solid var(--color-border);
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10;
|
||||||
|
/* Ensure it's above other content */
|
||||||
|
margin-top: auto;
|
||||||
|
/* Push to bottom if content is short */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="chat"] {
|
||||||
|
display: flex;
|
||||||
|
padding: var(--space-0-5) 0;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
--padding-y: var(--space-4);
|
||||||
|
--line-height: 1.5;
|
||||||
|
--text-height: calc(var(--line-height) * var(--font-size-lg));
|
||||||
|
--height: calc(var(--text-height) + var(--padding-y) * 2);
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
resize: none;
|
||||||
|
line-height: var(--line-height);
|
||||||
|
height: var(--height);
|
||||||
|
min-height: var(--height);
|
||||||
|
max-height: calc(5 * var(--text-height) + var(--padding-y) * 2);
|
||||||
|
padding: var(--padding-y) var(--space-4);
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--color-text);
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea::placeholder {
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& [data-component="button"] {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading-dots {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
40%,
|
||||||
|
60% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
18
cloud/web/src/pages/[workspace]/index.tsx
Normal file
18
cloud/web/src/pages/[workspace]/index.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { Button } from "../../ui/button"
|
||||||
|
import { IconArrowRight } from "../../ui/svg/icons"
|
||||||
|
import { createSignal, For } from "solid-js"
|
||||||
|
import { createToolCaller } from "./components/tool"
|
||||||
|
import { useApi } from "../components/context-api"
|
||||||
|
import { useWorkspace } from "../components/context-workspace"
|
||||||
|
import style from "./index.module.css"
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const api = useApi()
|
||||||
|
const workspace = useWorkspace()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={style.root}>
|
||||||
|
<h1>Hello</h1>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
97
cloud/web/src/pages/[workspace]/keys.module.css
Normal file
97
cloud/web/src/pages/[workspace]/keys.module.css
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="keys-info"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="header"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="header"] h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="header"] p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="key-list"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="key-item"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: var(--color-background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="key-actions"] {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="key-info"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="key-value"] {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="key-meta"] {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="empty-state"] {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="actions"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="create-form"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="form-actions"] {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="key-name"] {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
151
cloud/web/src/pages/[workspace]/keys.tsx
Normal file
151
cloud/web/src/pages/[workspace]/keys.tsx
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
import { Button } from "../../ui/button"
|
||||||
|
import { useApi } from "../components/context-api"
|
||||||
|
import { createSignal, createResource, For, Show } from "solid-js"
|
||||||
|
import style from "./keys.module.css"
|
||||||
|
|
||||||
|
export default function Keys() {
|
||||||
|
const api = useApi()
|
||||||
|
const [isCreating, setIsCreating] = createSignal(false)
|
||||||
|
const [showCreateForm, setShowCreateForm] = createSignal(false)
|
||||||
|
const [keyName, setKeyName] = createSignal("")
|
||||||
|
|
||||||
|
const [keysData, { refetch }] = createResource(async () => {
|
||||||
|
const response = await api.keys.$get()
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleCreateKey = async () => {
|
||||||
|
if (!keyName().trim()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsCreating(true)
|
||||||
|
await api.keys.$post({
|
||||||
|
json: { name: keyName().trim() },
|
||||||
|
})
|
||||||
|
refetch()
|
||||||
|
setKeyName("")
|
||||||
|
setShowCreateForm(false)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create API key:", error)
|
||||||
|
} finally {
|
||||||
|
setIsCreating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteKey = async (keyId: string) => {
|
||||||
|
if (!confirm("Are you sure you want to delete this API key? This action cannot be undone.")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.keys[":id"].$delete({
|
||||||
|
param: { id: keyId },
|
||||||
|
})
|
||||||
|
refetch()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete API key:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatKey = (key: string) => {
|
||||||
|
if (key.length <= 11) return key
|
||||||
|
return `${key.slice(0, 7)}...${key.slice(-4)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyToClipboard = async (text: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to copy to clipboard:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div data-component="title-bar">
|
||||||
|
<div data-slot="left">
|
||||||
|
<h1>API Keys</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class={style.root} data-max-width data-max-width-64>
|
||||||
|
<div data-slot="keys-info">
|
||||||
|
<div data-slot="actions">
|
||||||
|
<div data-slot="header">
|
||||||
|
<h2>API Keys</h2>
|
||||||
|
<p>Manage your API keys to access the OpenCode gateway.</p>
|
||||||
|
</div>
|
||||||
|
<Show
|
||||||
|
when={!showCreateForm()}
|
||||||
|
fallback={
|
||||||
|
<div data-slot="create-form">
|
||||||
|
<input
|
||||||
|
data-component="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter key name"
|
||||||
|
value={keyName()}
|
||||||
|
onInput={(e) => setKeyName(e.currentTarget.value)}
|
||||||
|
onKeyPress={(e) => e.key === "Enter" && handleCreateKey()}
|
||||||
|
/>
|
||||||
|
<div data-slot="form-actions">
|
||||||
|
<Button color="primary" disabled={isCreating() || !keyName().trim()} onClick={handleCreateKey}>
|
||||||
|
{isCreating() ? "Creating..." : "Create"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setShowCreateForm(false)
|
||||||
|
setKeyName("")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button color="primary" onClick={() => setShowCreateForm(true)}>
|
||||||
|
Create API Key
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-slot="key-list">
|
||||||
|
<For
|
||||||
|
each={keysData()?.keys}
|
||||||
|
fallback={
|
||||||
|
<div data-slot="empty-state">
|
||||||
|
<p>Create an API key to access opencode gateway</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(key) => (
|
||||||
|
<div data-slot="key-item">
|
||||||
|
<div data-slot="key-info">
|
||||||
|
<div data-slot="key-name">{key.name}</div>
|
||||||
|
<div data-slot="key-value">{formatKey(key.key)}</div>
|
||||||
|
<div data-slot="key-meta">
|
||||||
|
Created: {formatDate(key.timeCreated)}
|
||||||
|
{key.timeUsed && ` • Last used: ${formatDate(key.timeUsed)}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-slot="key-actions">
|
||||||
|
<Button color="ghost" onClick={() => copyToClipboard(key.key)} title="Copy API key">
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
<Button color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
24
cloud/web/src/pages/components/context-api.tsx
Normal file
24
cloud/web/src/pages/components/context-api.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { hc } from "hono/client"
|
||||||
|
import { ApiType } from "@opencode/cloud-function/src/gateway"
|
||||||
|
import { useWorkspace } from "./context-workspace"
|
||||||
|
import { useOpenAuth } from "../../components/context-openauth"
|
||||||
|
|
||||||
|
export function useApi() {
|
||||||
|
const workspace = useWorkspace()
|
||||||
|
const auth = useOpenAuth()
|
||||||
|
return hc<ApiType>(import.meta.env.VITE_API_URL, {
|
||||||
|
async fetch(...args: Parameters<typeof fetch>): Promise<Response> {
|
||||||
|
const [input, init] = args
|
||||||
|
const request = input instanceof Request ? input : new Request(input, init)
|
||||||
|
const headers = new Headers(request.headers)
|
||||||
|
headers.set("authorization", `Bearer ${await auth.access()}`)
|
||||||
|
headers.set("x-opencode-workspace", workspace.id)
|
||||||
|
return fetch(
|
||||||
|
new Request(request, {
|
||||||
|
...init,
|
||||||
|
headers,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
38
cloud/web/src/pages/components/context-workspace.tsx
Normal file
38
cloud/web/src/pages/components/context-workspace.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
|
import { createInitializedContext } from "../../util/context"
|
||||||
|
import { useAccount } from "../../components/context-account"
|
||||||
|
import { createEffect, createMemo } from "solid-js"
|
||||||
|
|
||||||
|
export const { use: useWorkspace, provider: WorkspaceProvider } =
|
||||||
|
createInitializedContext("WorkspaceProvider", () => {
|
||||||
|
const params = useParams()
|
||||||
|
const account = useAccount()
|
||||||
|
const workspace = createMemo(() =>
|
||||||
|
account.current?.workspaces.find(
|
||||||
|
(x) => x.id === params.workspace || x.slug === params.workspace,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const nav = useNavigate()
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!workspace()) nav("/")
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = () => workspace()!
|
||||||
|
result.ready = true
|
||||||
|
|
||||||
|
return {
|
||||||
|
get id() {
|
||||||
|
return workspace()!.id
|
||||||
|
},
|
||||||
|
get slug() {
|
||||||
|
return workspace()!.slug
|
||||||
|
},
|
||||||
|
get name() {
|
||||||
|
return workspace()!.name
|
||||||
|
},
|
||||||
|
get ready() {
|
||||||
|
return workspace() !== undefined
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
199
cloud/web/src/pages/components/layout.module.css
Normal file
199
cloud/web/src/pages/components/layout.module.css
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
.root {
|
||||||
|
--padding: var(--space-10);
|
||||||
|
--vertical-padding: var(--space-8);
|
||||||
|
--heading-font-size: var(--font-size-4xl);
|
||||||
|
--sidebar-width: 200px;
|
||||||
|
--mobile-breakpoint: 40rem;
|
||||||
|
--topbar-height: 60px;
|
||||||
|
|
||||||
|
margin: var(--space-4);
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
height: calc(100vh - var(--space-8));
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow: hidden;
|
||||||
|
/* Prevent overall scrolling */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="mobile-top-bar"] {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: var(--topbar-height);
|
||||||
|
background: var(--color-background);
|
||||||
|
border-bottom: 2px solid var(--color-border);
|
||||||
|
z-index: 20;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 var(--space-4) 0 0;
|
||||||
|
|
||||||
|
[data-slot="logo"] {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
div {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 28px;
|
||||||
|
width: auto;
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="toggle"] {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: var(--space-4);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
color: var(--color-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="sidebar"] {
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
border-right: 2px solid var(--color-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: calc(var(--padding) / 2);
|
||||||
|
overflow-y: auto;
|
||||||
|
/* Allow scrolling if needed */
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
[data-slot="logo"] {
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: var(--space-7);
|
||||||
|
color: var(--color-white);
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
height: 32px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="nav"] {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: calc(var(--vertical-padding) / 2);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
padding: var(--space-2) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="user"] {
|
||||||
|
[data-component="button"] {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navActiveLink {
|
||||||
|
cursor: default;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="main-content"] {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
/* Full height */
|
||||||
|
overflow: hidden;
|
||||||
|
/* Prevent overflow */
|
||||||
|
position: relative;
|
||||||
|
/* For positioning footer */
|
||||||
|
width: 100%;
|
||||||
|
/* Full width */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Backdrop for mobile */
|
||||||
|
[data-component="backdrop"] {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
/* background-color: rgba(0, 0, 0, 0.5); */
|
||||||
|
z-index: 25;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile styles */
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
.root {
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="mobile-top-bar"] {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="backdrop"] {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="sidebar"] {
|
||||||
|
position: fixed;
|
||||||
|
left: -100%;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 280px;
|
||||||
|
transition: left 0.3s ease-in-out;
|
||||||
|
box-shadow: none;
|
||||||
|
z-index: 30;
|
||||||
|
padding: var(--space-8);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
|
||||||
|
&[data-opened="true"] {
|
||||||
|
left: 0;
|
||||||
|
box-shadow: 8px 0 0px 0px var(--color-gray-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="main-content"] {
|
||||||
|
padding-top: var(--topbar-height);
|
||||||
|
/* Add space for the top bar */
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the logo in the sidebar on mobile since it's in the top bar */
|
||||||
|
[data-component="sidebar"] [data-slot="logo"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
96
cloud/web/src/pages/components/layout.tsx
Normal file
96
cloud/web/src/pages/components/layout.tsx
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import style from "./layout.module.css"
|
||||||
|
import { useAccount } from "../../components/context-account"
|
||||||
|
import { Button } from "../../ui/button"
|
||||||
|
import { IconLogomark } from "../../ui/svg"
|
||||||
|
import { IconBars3BottomLeft } from "../../ui/svg/icons"
|
||||||
|
import { ParentProps, createMemo, createSignal } from "solid-js"
|
||||||
|
import { A, useLocation } from "@solidjs/router"
|
||||||
|
import { useOpenAuth } from "../../components/context-openauth"
|
||||||
|
|
||||||
|
export default function Layout(props: ParentProps) {
|
||||||
|
const auth = useOpenAuth()
|
||||||
|
const account = useAccount()
|
||||||
|
const [sidebarOpen, setSidebarOpen] = createSignal(false)
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
const workspaceId = createMemo(() => account.current?.workspaces[0].id)
|
||||||
|
const pageTitle = createMemo(() => {
|
||||||
|
const path = location.pathname
|
||||||
|
if (path.endsWith("/billing")) return "Billing"
|
||||||
|
if (path.endsWith("/keys")) return "API Keys"
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
auth.logout(auth.subject?.id!)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={style.root}>
|
||||||
|
{/* Mobile top bar */}
|
||||||
|
<div data-component="mobile-top-bar">
|
||||||
|
<button data-slot="toggle" onClick={() => setSidebarOpen(!sidebarOpen())}>
|
||||||
|
<IconBars3BottomLeft />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div data-slot="logo">
|
||||||
|
{pageTitle() ? (
|
||||||
|
<div>{pageTitle()}</div>
|
||||||
|
) : (
|
||||||
|
<A href="/">
|
||||||
|
<IconLogomark />
|
||||||
|
</A>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Backdrop for mobile sidebar - closes sidebar when clicked */}
|
||||||
|
{sidebarOpen() && <div data-component="backdrop" onClick={() => setSidebarOpen(false)}></div>}
|
||||||
|
|
||||||
|
<div data-component="sidebar" data-opened={sidebarOpen() ? "true" : "false"}>
|
||||||
|
<div data-slot="logo">
|
||||||
|
<A href="/">
|
||||||
|
<IconLogomark />
|
||||||
|
</A>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav data-slot="nav">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<A end activeClass={style.navActiveLink} href={`/${workspaceId()}`} onClick={() => setSidebarOpen(false)}>
|
||||||
|
Chat
|
||||||
|
</A>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<A
|
||||||
|
activeClass={style.navActiveLink}
|
||||||
|
href={`/${workspaceId()}/billing`}
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
>
|
||||||
|
Billing
|
||||||
|
</A>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<A
|
||||||
|
activeClass={style.navActiveLink}
|
||||||
|
href={`/${workspaceId()}/keys`}
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
>
|
||||||
|
API Keys
|
||||||
|
</A>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div data-slot="user">
|
||||||
|
<Button color="ghost" onClick={handleLogout} title={account.current?.email || ""}>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div data-slot="main-content">{props.children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
36
cloud/web/src/pages/index.tsx
Normal file
36
cloud/web/src/pages/index.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { Match, Switch } from "solid-js"
|
||||||
|
import { useAccount } from "../components/context-account"
|
||||||
|
import { Navigate } from "@solidjs/router"
|
||||||
|
import { IconLogo } from "../ui/svg"
|
||||||
|
import styles from "./lander.module.css"
|
||||||
|
import { useOpenAuth } from "../components/context-openauth"
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const auth = useOpenAuth()
|
||||||
|
const account = useAccount()
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Match when={account.current}>
|
||||||
|
<Navigate href={`/${account.current!.workspaces[0].id}`} />
|
||||||
|
</Match>
|
||||||
|
<Match when={!account.current}>
|
||||||
|
<div class={styles.lander}>
|
||||||
|
<div data-slot="hero">
|
||||||
|
<section data-slot="top">
|
||||||
|
<div data-slot="logo">
|
||||||
|
<IconLogo />
|
||||||
|
</div>
|
||||||
|
<h1>opencode Gateway Console</h1>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section data-slot="cta">
|
||||||
|
<div data-slot="col-2">
|
||||||
|
<span onClick={() => auth.authorize({ provider: "github" })}>Sign in with GitHub</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
}
|
169
cloud/web/src/pages/lander.module.css
Normal file
169
cloud/web/src/pages/lander.module.css
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
.lander {
|
||||||
|
--padding: 3rem;
|
||||||
|
--vertical-padding: 2rem;
|
||||||
|
--heading-font-size: 2rem;
|
||||||
|
|
||||||
|
margin: 1rem;
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
& {
|
||||||
|
--padding: 1.5rem;
|
||||||
|
--vertical-padding: 1rem;
|
||||||
|
--heading-font-size: 1.5rem;
|
||||||
|
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="hero"] {
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
|
||||||
|
max-width: 64rem;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="top"] {
|
||||||
|
padding: var(--padding);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: calc(var(--vertical-padding) / 8);
|
||||||
|
font-size: var(--heading-font-size);
|
||||||
|
line-height: 1.25;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="logo"] {
|
||||||
|
width: clamp(200px, 70vw, 400px);
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="cta"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-top: 2px solid var(--color-border);
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
flex: 1;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
|
||||||
|
&[data-slot="col-2"] {
|
||||||
|
background-color: var(--color-border);
|
||||||
|
color: var(--color-text-invert);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: calc(var(--padding) / 2) 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
& > div {
|
||||||
|
padding-bottom: calc(var(--padding) / 2 + 4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div + div {
|
||||||
|
border-left: 2px solid var(--color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="images"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-top: 2px solid var(--color-border);
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(var(--padding) / 4);
|
||||||
|
padding: calc(var(--padding) / 2);
|
||||||
|
border-width: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
|
||||||
|
& > div, a {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div + div {
|
||||||
|
border-width: 0 0 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
& {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
& > div + div {
|
||||||
|
border-width: 2px 0 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="content"] {
|
||||||
|
border-top: 2px solid var(--color-border);
|
||||||
|
padding: var(--padding);
|
||||||
|
|
||||||
|
& > p {
|
||||||
|
line-height: var(--font-line-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
margin-top: calc(var(--vertical-padding) / 2);
|
||||||
|
padding-left: 2.5rem;
|
||||||
|
list-style-type: decimal;
|
||||||
|
line-height: var(--font-line-height);
|
||||||
|
|
||||||
|
& > li + li {
|
||||||
|
margin-top: calc(var(--vertical-padding) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > li b {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="footer"] {
|
||||||
|
border-top: 2px solid var(--color-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: calc(var(--padding) / 2) 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div + div {
|
||||||
|
border-left: 2px solid var(--color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
204
cloud/web/src/pages/test/design.module.css
Normal file
204
cloud/web/src/pages/test/design.module.css
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
.pageContainer {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.componentTable {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.componentCell {
|
||||||
|
padding: 1rem;
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.componentLabel {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 2px;
|
||||||
|
background: var(--color-border);
|
||||||
|
margin: 3rem 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonSection {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorSection {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelSection {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputSection {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogSection {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogContent {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogContentFooter {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageTitle {
|
||||||
|
font-size: var(--heading-font-size, 2rem);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorBox {
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorOrange {
|
||||||
|
background-color: var(--color-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorOrangeLow {
|
||||||
|
background-color: var(--color-orange-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorOrangeHigh {
|
||||||
|
background-color: var(--color-orange-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorGreen {
|
||||||
|
background-color: var(--color-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorGreenLow {
|
||||||
|
background-color: var(--color-green-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorGreenHigh {
|
||||||
|
background-color: var(--color-green-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorBlue {
|
||||||
|
background-color: var(--color-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorBlueLow {
|
||||||
|
background-color: var(--color-blue-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorBlueHigh {
|
||||||
|
background-color: var(--color-blue-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorPurple {
|
||||||
|
background-color: var(--color-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorPurpleLow {
|
||||||
|
background-color: var(--color-purple-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorPurpleHigh {
|
||||||
|
background-color: var(--color-purple-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorRed {
|
||||||
|
background-color: var(--color-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorRedLow {
|
||||||
|
background-color: var(--color-red-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorRedHigh {
|
||||||
|
background-color: var(--color-red-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorAccent {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorAccentLow {
|
||||||
|
background-color: var(--color-accent-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorAccentHigh {
|
||||||
|
background-color: var(--color-accent-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorCode {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorVariants {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorVariant {
|
||||||
|
flex: 1;
|
||||||
|
height: 40px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorVariantCode {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
562
cloud/web/src/pages/test/design.tsx
Normal file
562
cloud/web/src/pages/test/design.tsx
Normal file
|
@ -0,0 +1,562 @@
|
||||||
|
import { Button } from "../../ui/button"
|
||||||
|
import { Dialog } from "../../ui/dialog"
|
||||||
|
import { Navigate } from "@solidjs/router"
|
||||||
|
import { createSignal, Show } from "solid-js"
|
||||||
|
import { IconHome, IconPencilSquare } from "../../ui/svg/icons"
|
||||||
|
import { useTheme } from "../../components/context-theme"
|
||||||
|
import { useDialog } from "../../ui/context-dialog"
|
||||||
|
import { DialogString } from "../../ui/dialog-string"
|
||||||
|
import { DialogSelect } from "../../ui/dialog-select"
|
||||||
|
import styles from "./design.module.css"
|
||||||
|
|
||||||
|
export default function DesignSystem() {
|
||||||
|
const dialog = useDialog()
|
||||||
|
const [dialogOpen, setDialogOpen] = createSignal(false)
|
||||||
|
const [dialogOpenTransition, setDialogOpenTransition] = createSignal(false)
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
// Check if we're running locally
|
||||||
|
const isLocal = import.meta.env.DEV === true
|
||||||
|
|
||||||
|
if (!isLocal) {
|
||||||
|
return <Navigate href="/" />
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a toggle button for theme
|
||||||
|
const toggleTheme = () => {
|
||||||
|
theme.setMode(theme.mode === "light" ? "dark" : "light")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={styles.pageContainer}>
|
||||||
|
<div class={styles.header}>
|
||||||
|
<h1 class={styles.pageTitle}>Design System</h1>
|
||||||
|
<Button onClick={toggleTheme}>
|
||||||
|
Toggle {theme.mode === "light" ? "Dark" : "Light"} Mode
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class={styles.colorSection}>
|
||||||
|
<h2 class={styles.sectionTitle}>Colors</h2>
|
||||||
|
|
||||||
|
<table class={styles.componentTable}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Orange</h3>
|
||||||
|
<div class={`${styles.colorBox} ${styles.colorOrange}`}>
|
||||||
|
<span class={styles.colorCode}>hsl(41, 82%, 63%)</span>
|
||||||
|
</div>
|
||||||
|
<div class={styles.colorVariants}>
|
||||||
|
<div
|
||||||
|
class={`${styles.colorVariant} ${styles.colorOrangeLow}`}
|
||||||
|
>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(41, 39%, 22%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={`${styles.colorVariant} ${styles.colorOrangeHigh}`}
|
||||||
|
>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(41, 82%, 87%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Green</h3>
|
||||||
|
<div class={`${styles.colorBox} ${styles.colorGreen}`}>
|
||||||
|
<span class={styles.colorCode}>hsl(101, 82%, 63%)</span>
|
||||||
|
</div>
|
||||||
|
<div class={styles.colorVariants}>
|
||||||
|
<div class={`${styles.colorVariant} ${styles.colorGreenLow}`}>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(101, 39%, 22%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={`${styles.colorVariant} ${styles.colorGreenHigh}`}
|
||||||
|
>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(101, 82%, 80%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Blue</h3>
|
||||||
|
<div class={`${styles.colorBox} ${styles.colorBlue}`}>
|
||||||
|
<span class={styles.colorCode}>hsl(234, 100%, 60%)</span>
|
||||||
|
</div>
|
||||||
|
<div class={styles.colorVariants}>
|
||||||
|
<div class={`${styles.colorVariant} ${styles.colorBlueLow}`}>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(234, 54%, 20%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class={`${styles.colorVariant} ${styles.colorBlueHigh}`}>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(234, 100%, 87%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Purple</h3>
|
||||||
|
<div class={`${styles.colorBox} ${styles.colorPurple}`}>
|
||||||
|
<span class={styles.colorCode}>hsl(281, 82%, 63%)</span>
|
||||||
|
</div>
|
||||||
|
<div class={styles.colorVariants}>
|
||||||
|
<div
|
||||||
|
class={`${styles.colorVariant} ${styles.colorPurpleLow}`}
|
||||||
|
>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(281, 39%, 22%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={`${styles.colorVariant} ${styles.colorPurpleHigh}`}
|
||||||
|
>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(281, 82%, 89%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Red</h3>
|
||||||
|
<div class={`${styles.colorBox} ${styles.colorRed}`}>
|
||||||
|
<span class={styles.colorCode}>hsl(339, 82%, 63%)</span>
|
||||||
|
</div>
|
||||||
|
<div class={styles.colorVariants}>
|
||||||
|
<div class={`${styles.colorVariant} ${styles.colorRedLow}`}>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(339, 39%, 22%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class={`${styles.colorVariant} ${styles.colorRedHigh}`}>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(339, 82%, 87%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Accent</h3>
|
||||||
|
<div class={`${styles.colorBox} ${styles.colorAccent}`}>
|
||||||
|
<span class={styles.colorCode}>hsl(13, 88%, 57%)</span>
|
||||||
|
</div>
|
||||||
|
<div class={styles.colorVariants}>
|
||||||
|
<div
|
||||||
|
class={`${styles.colorVariant} ${styles.colorAccentLow}`}
|
||||||
|
>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(13, 75%, 30%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={`${styles.colorVariant} ${styles.colorAccentHigh}`}
|
||||||
|
>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(13, 100%, 78%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class={styles.divider}></div>
|
||||||
|
|
||||||
|
<section class={styles.buttonSection}>
|
||||||
|
<h2 class={styles.sectionTitle}>Buttons</h2>
|
||||||
|
|
||||||
|
<table class={styles.componentTable}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Primary</h3>
|
||||||
|
<Button>Primary Button</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Secondary</h3>
|
||||||
|
<Button color="secondary">Secondary Button</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Ghost</h3>
|
||||||
|
<Button color="ghost">Ghost Button</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Primary Disabled</h3>
|
||||||
|
<Button disabled>Primary Button</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Secondary Disabled</h3>
|
||||||
|
<Button color="secondary" disabled>
|
||||||
|
Secondary Button
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Ghost Disabled</h3>
|
||||||
|
<Button color="ghost" disabled>
|
||||||
|
Ghost Button
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small</h3>
|
||||||
|
<Button size="sm">Small Button</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small Secondary</h3>
|
||||||
|
<Button size="sm" color="secondary">
|
||||||
|
Small Secondary
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small Ghost</h3>
|
||||||
|
<Button size="sm" color="ghost">
|
||||||
|
Small Ghost
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>With Icon</h3>
|
||||||
|
<Button icon={<IconHome />}>With Icon</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Icon + Secondary</h3>
|
||||||
|
<Button icon={<IconHome />} color="secondary">
|
||||||
|
Icon Secondary
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Icon + Ghost</h3>
|
||||||
|
<Button icon={<IconHome />} color="ghost">
|
||||||
|
Icon Ghost
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small + Icon</h3>
|
||||||
|
<Button size="sm" icon={<IconHome />}>
|
||||||
|
Small Icon
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small + Icon + Secondary</h3>
|
||||||
|
<Button size="sm" icon={<IconHome />} color="secondary">
|
||||||
|
Small Icon Secondary
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small + Icon + Ghost</h3>
|
||||||
|
<Button size="sm" icon={<IconHome />} color="ghost">
|
||||||
|
Small Icon Ghost
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Icon Only</h3>
|
||||||
|
<Button icon={<IconHome />}></Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Icon Only + Secondary</h3>
|
||||||
|
<Button icon={<IconHome />} color="secondary"></Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Icon Only + Ghost</h3>
|
||||||
|
<Button icon={<IconHome />} color="ghost"></Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Icon Only Disabled</h3>
|
||||||
|
<Button icon={<IconHome />} disabled></Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>
|
||||||
|
Icon Only + Secondary Disabled
|
||||||
|
</h3>
|
||||||
|
<Button icon={<IconHome />} color="secondary" disabled></Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>
|
||||||
|
Icon Only + Ghost Disabled
|
||||||
|
</h3>
|
||||||
|
<Button icon={<IconHome />} color="ghost" disabled></Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small Icon Only</h3>
|
||||||
|
<Button size="sm" icon={<IconHome />}></Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>
|
||||||
|
Small Icon Only + Secondary
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
icon={<IconHome />}
|
||||||
|
color="secondary"
|
||||||
|
></Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small Icon Only + Ghost</h3>
|
||||||
|
<Button size="sm" icon={<IconHome />} color="ghost"></Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class={styles.divider}></div>
|
||||||
|
|
||||||
|
<section class={styles.labelSection}>
|
||||||
|
<h2 class={styles.sectionTitle}>Labels</h2>
|
||||||
|
|
||||||
|
<table class={styles.componentTable}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small</h3>
|
||||||
|
<label data-size="sm" data-component="label">
|
||||||
|
Small Label Text
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Medium</h3>
|
||||||
|
<label data-size="md" data-component="label">
|
||||||
|
Medium Label Text
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Large</h3>
|
||||||
|
<label data-size="lg" data-component="label">
|
||||||
|
Large Label Text
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class={styles.divider}></div>
|
||||||
|
|
||||||
|
<section class={styles.inputSection}>
|
||||||
|
<h2 class={styles.sectionTitle}>Inputs</h2>
|
||||||
|
|
||||||
|
<table class={styles.componentTable}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small</h3>
|
||||||
|
<input
|
||||||
|
data-component="input"
|
||||||
|
data-size="sm"
|
||||||
|
placeholder="Small input field"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Medium</h3>
|
||||||
|
<input
|
||||||
|
data-component="input"
|
||||||
|
data-size="md"
|
||||||
|
placeholder="Medium input field"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Large</h3>
|
||||||
|
<input
|
||||||
|
data-component="input"
|
||||||
|
data-size="lg"
|
||||||
|
placeholder="Large input field"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Disabled</h3>
|
||||||
|
<input
|
||||||
|
data-component="input"
|
||||||
|
data-size="md"
|
||||||
|
placeholder="Disabled input"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>With Value</h3>
|
||||||
|
<input
|
||||||
|
data-component="input"
|
||||||
|
data-size="md"
|
||||||
|
value="Input with preset value"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class={styles.divider}></div>
|
||||||
|
|
||||||
|
<section class={styles.dialogSection}>
|
||||||
|
<h2 class={styles.sectionTitle}>Dialogs</h2>
|
||||||
|
|
||||||
|
<table class={styles.componentTable}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Default</h3>
|
||||||
|
<Button color="secondary" onClick={() => setDialogOpen(true)}>
|
||||||
|
Open Dialog
|
||||||
|
</Button>
|
||||||
|
<Dialog open={dialogOpen()} onOpenChange={setDialogOpen}>
|
||||||
|
<div data-slot="header">
|
||||||
|
<div data-slot="title">Dialog Title</div>
|
||||||
|
</div>
|
||||||
|
<div data-slot="main">
|
||||||
|
<p>This is the default dialog content.</p>
|
||||||
|
</div>
|
||||||
|
<div data-slot="footer">
|
||||||
|
<Button onClick={() => setDialogOpen(false)}>Close</Button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small With Transition</h3>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setDialogOpenTransition(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Small Dialog
|
||||||
|
</Button>
|
||||||
|
<Dialog
|
||||||
|
open={dialogOpenTransition()}
|
||||||
|
onOpenChange={setDialogOpenTransition}
|
||||||
|
size="sm"
|
||||||
|
transition={true}
|
||||||
|
>
|
||||||
|
<div class={styles.dialogContent}>
|
||||||
|
<h2 class={styles.sectionTitle}>Small Dialog</h2>
|
||||||
|
<p>This is a smaller dialog with transitions.</p>
|
||||||
|
<div class={styles.dialogContentFooter}>
|
||||||
|
<Button onClick={() => setDialogOpenTransition(false)}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Input String</h3>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
dialog.open(DialogString, {
|
||||||
|
title: "Name",
|
||||||
|
action: "Change name",
|
||||||
|
placeholder: "Enter a name",
|
||||||
|
onSubmit: () => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
String
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Select Input</h3>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
dialog.open(DialogSelect, {
|
||||||
|
placeholder: "Select",
|
||||||
|
title: "User Settings",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
display: "Change name",
|
||||||
|
prefix: <IconPencilSquare />,
|
||||||
|
onSelect: () => {
|
||||||
|
dialog.close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: "Remove user",
|
||||||
|
prefix: <IconHome />,
|
||||||
|
onSelect: () => {
|
||||||
|
dialog.close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Select Input</h3>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
dialog.open(DialogSelect, {
|
||||||
|
placeholder: "Select",
|
||||||
|
title: "User Settings",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
display: "Change name",
|
||||||
|
onSelect: () => {
|
||||||
|
dialog.close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: "Remove user",
|
||||||
|
onSelect: () => {
|
||||||
|
dialog.close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
No Prefix
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Select No Options</h3>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
dialog.open(DialogSelect, {
|
||||||
|
placeholder: "Select",
|
||||||
|
title: "User Settings",
|
||||||
|
options: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
No Options
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
12
cloud/web/src/sst-env.d.ts
vendored
Normal file
12
cloud/web/src/sst-env.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/* This file is auto-generated by SST. Do not edit. */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_DOCS_URL: string
|
||||||
|
readonly VITE_API_URL: string
|
||||||
|
readonly VITE_AUTH_URL: string
|
||||||
|
}
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
24
cloud/web/src/ui/button.tsx
Normal file
24
cloud/web/src/ui/button.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { Button as Kobalte } from "@kobalte/core/button"
|
||||||
|
import { JSX, Show, splitProps } from "solid-js"
|
||||||
|
|
||||||
|
export interface ButtonProps {
|
||||||
|
color?: "primary" | "secondary" | "ghost"
|
||||||
|
size?: "md" | "sm"
|
||||||
|
icon?: JSX.Element
|
||||||
|
}
|
||||||
|
export function Button(props: JSX.IntrinsicElements["button"] & ButtonProps) {
|
||||||
|
const [split, rest] = splitProps(props, ["color", "size", "icon"])
|
||||||
|
return (
|
||||||
|
<Kobalte
|
||||||
|
{...rest}
|
||||||
|
data-component="button"
|
||||||
|
data-size={split.size || "md"}
|
||||||
|
data-color={split.color || "primary"}
|
||||||
|
>
|
||||||
|
<Show when={props.icon}>
|
||||||
|
<div data-slot="icon">{props.icon}</div>
|
||||||
|
</Show>
|
||||||
|
{props.children}
|
||||||
|
</Kobalte>
|
||||||
|
)
|
||||||
|
}
|
120
cloud/web/src/ui/context-dialog.tsx
Normal file
120
cloud/web/src/ui/context-dialog.tsx
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
import { createContext, JSX, ParentProps, useContext } from "solid-js"
|
||||||
|
import { StandardSchemaV1 } from "@standard-schema/spec"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { Dialog } from "./dialog"
|
||||||
|
|
||||||
|
const Context = createContext<DialogControl>()
|
||||||
|
|
||||||
|
type DialogControl = {
|
||||||
|
open<Schema extends StandardSchemaV1<object>>(
|
||||||
|
component: DialogComponent<Schema>,
|
||||||
|
input: StandardSchemaV1.InferInput<Schema>,
|
||||||
|
): void
|
||||||
|
close(): void
|
||||||
|
isOpen(input: any): boolean
|
||||||
|
size: "sm" | "md"
|
||||||
|
transition?: boolean
|
||||||
|
input?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
type DialogProps<Schema extends StandardSchemaV1<object>> = {
|
||||||
|
input: StandardSchemaV1.InferInput<Schema>
|
||||||
|
control: DialogControl
|
||||||
|
}
|
||||||
|
|
||||||
|
type DialogComponent<Schema extends StandardSchemaV1<object>> = ReturnType<
|
||||||
|
typeof createDialog<Schema>
|
||||||
|
>
|
||||||
|
|
||||||
|
export function createDialog<Schema extends StandardSchemaV1<object>>(props: {
|
||||||
|
schema: Schema
|
||||||
|
size: "sm" | "md"
|
||||||
|
render: (props: DialogProps<Schema>) => JSX.Element
|
||||||
|
}) {
|
||||||
|
const result = () => {
|
||||||
|
const dialog = useDialog()
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
size={dialog.size}
|
||||||
|
transition={dialog.transition}
|
||||||
|
open={dialog.isOpen(result)}
|
||||||
|
onOpenChange={(val) => {
|
||||||
|
if (!val) dialog.close()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.render({
|
||||||
|
input: dialog.input,
|
||||||
|
control: dialog,
|
||||||
|
})}
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result.schema = props.schema
|
||||||
|
result.size = props.size
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogProvider(props: ParentProps) {
|
||||||
|
const [store, setStore] = createStore<{
|
||||||
|
dialog?: DialogComponent<any>
|
||||||
|
input?: any
|
||||||
|
transition?: boolean
|
||||||
|
size: "sm" | "md"
|
||||||
|
}>({
|
||||||
|
size: "sm",
|
||||||
|
})
|
||||||
|
|
||||||
|
const control: DialogControl = {
|
||||||
|
get input() {
|
||||||
|
return store.input
|
||||||
|
},
|
||||||
|
get size() {
|
||||||
|
return store.size
|
||||||
|
},
|
||||||
|
get transition() {
|
||||||
|
return store.transition
|
||||||
|
},
|
||||||
|
isOpen(input) {
|
||||||
|
return store.dialog === input
|
||||||
|
},
|
||||||
|
open(component, input) {
|
||||||
|
setStore({
|
||||||
|
dialog: component,
|
||||||
|
input: input,
|
||||||
|
size: store.dialog !== undefined ? store.size : component.size,
|
||||||
|
transition: store.dialog !== undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setStore({
|
||||||
|
size: component.size,
|
||||||
|
})
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setStore({
|
||||||
|
transition: false,
|
||||||
|
})
|
||||||
|
}, 150)
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
setStore({
|
||||||
|
dialog: undefined,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Context.Provider value={control}>{props.children}</Context.Provider>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDialog() {
|
||||||
|
const ctx = useContext(Context)
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("useDialog must be used within a DialogProvider")
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
36
cloud/web/src/ui/dialog-select.module.css
Normal file
36
cloud/web/src/ui/dialog-select.module.css
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
.options {
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
border-top: 2px solid var(--color-border);
|
||||||
|
padding: var(--space-2);
|
||||||
|
|
||||||
|
[data-slot="option"] {
|
||||||
|
outline: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: var(--space-11);
|
||||||
|
display: flex;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 var(--space-2-5);
|
||||||
|
gap: var(--space-3);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&[data-empty] {
|
||||||
|
cursor: default;
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-active] {
|
||||||
|
background-color: var(--color-bg-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="title"] {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="prefix"] {
|
||||||
|
width: var(--space-4);
|
||||||
|
height: var(--space-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
124
cloud/web/src/ui/dialog-select.tsx
Normal file
124
cloud/web/src/ui/dialog-select.tsx
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import style from "./dialog-select.module.css"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { createMemo, createSignal, For, JSX, onMount } from "solid-js"
|
||||||
|
import { createList } from "solid-list"
|
||||||
|
import { createDialog } from "./context-dialog"
|
||||||
|
|
||||||
|
export const DialogSelect = createDialog({
|
||||||
|
size: "md",
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
placeholder: z.string(),
|
||||||
|
onSelect: z
|
||||||
|
.function(z.tuple([z.any()]))
|
||||||
|
.returns(z.void())
|
||||||
|
.optional(),
|
||||||
|
options: z.array(
|
||||||
|
z.object({
|
||||||
|
display: z.string(),
|
||||||
|
value: z.any().optional(),
|
||||||
|
onSelect: z.function().returns(z.void()).optional(),
|
||||||
|
prefix: z.custom<JSX.Element>().optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
render: (ctx) => {
|
||||||
|
let input: HTMLInputElement
|
||||||
|
onMount(() => {
|
||||||
|
input.focus()
|
||||||
|
input.value = ""
|
||||||
|
})
|
||||||
|
|
||||||
|
const [filter, setFilter] = createSignal("")
|
||||||
|
const filtered = createMemo(() =>
|
||||||
|
ctx.input.options?.filter((i) =>
|
||||||
|
i.display.toLowerCase().includes(filter().toLowerCase()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const list = createList({
|
||||||
|
loop: true,
|
||||||
|
initialActive: 0,
|
||||||
|
items: () => filtered().map((_, i) => i),
|
||||||
|
handleTab: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSelection = (index: number) => {
|
||||||
|
const option = ctx.input.options[index]
|
||||||
|
|
||||||
|
// If the option has its own onSelect handler, use it
|
||||||
|
if (option.onSelect) {
|
||||||
|
option.onSelect()
|
||||||
|
}
|
||||||
|
// Otherwise, if there's a global onSelect handler, call it with the option's value
|
||||||
|
else if (ctx.input.onSelect) {
|
||||||
|
ctx.input.onSelect(
|
||||||
|
option.value !== undefined ? option.value : option.display,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div data-slot="header">
|
||||||
|
<label
|
||||||
|
data-size="md"
|
||||||
|
data-slot="title"
|
||||||
|
data-component="label"
|
||||||
|
for={`dialog-select-${ctx.input.title}`}
|
||||||
|
>
|
||||||
|
{ctx.input.title}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div data-slot="main">
|
||||||
|
<input
|
||||||
|
data-size="lg"
|
||||||
|
data-component="input"
|
||||||
|
value={filter()}
|
||||||
|
onInput={(e) => {
|
||||||
|
setFilter(e.target.value)
|
||||||
|
list.setActive(0)
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
const selected = list.active()
|
||||||
|
if (selected === null) return
|
||||||
|
handleSelection(selected)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
setFilter("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list.onKeyDown(e)
|
||||||
|
}}
|
||||||
|
id={`dialog-select-${ctx.input.title}`}
|
||||||
|
ref={(r) => (input = r)}
|
||||||
|
data-slot="input"
|
||||||
|
placeholder={ctx.input.placeholder}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div data-slot="options" class={style.options}>
|
||||||
|
<For
|
||||||
|
each={filtered()}
|
||||||
|
fallback={
|
||||||
|
<div data-slot="option" data-empty>
|
||||||
|
No results
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(option, index) => (
|
||||||
|
<div
|
||||||
|
onClick={() => handleSelection(index())}
|
||||||
|
data-slot="option"
|
||||||
|
data-active={list.active() === index() ? true : undefined}
|
||||||
|
>
|
||||||
|
{option.prefix && <div data-slot="prefix">{option.prefix}</div>}
|
||||||
|
<div data-slot="title">{option.display}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
70
cloud/web/src/ui/dialog-string.tsx
Normal file
70
cloud/web/src/ui/dialog-string.tsx
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { z } from "zod"
|
||||||
|
import { onMount } from "solid-js"
|
||||||
|
import { createDialog } from "./context-dialog"
|
||||||
|
import { Button } from "./button"
|
||||||
|
|
||||||
|
export const DialogString = createDialog({
|
||||||
|
size: "sm",
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
placeholder: z.string(),
|
||||||
|
action: z.string(),
|
||||||
|
onSubmit: z.function().args(z.string()).returns(z.void()),
|
||||||
|
}),
|
||||||
|
render: (ctx) => {
|
||||||
|
let input: HTMLInputElement
|
||||||
|
onMount(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
input.focus()
|
||||||
|
input.value = ""
|
||||||
|
}, 50)
|
||||||
|
})
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
const value = input.value.trim()
|
||||||
|
if (value) {
|
||||||
|
ctx.input.onSubmit(value)
|
||||||
|
ctx.control.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div data-slot="header">
|
||||||
|
<label
|
||||||
|
data-size="md"
|
||||||
|
data-slot="title"
|
||||||
|
data-component="label"
|
||||||
|
for={`dialog-string-${ctx.input.title}`}
|
||||||
|
>
|
||||||
|
{ctx.input.title}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div data-slot="main">
|
||||||
|
<input
|
||||||
|
data-slot="input"
|
||||||
|
data-size="lg"
|
||||||
|
data-component="input"
|
||||||
|
ref={(r) => (input = r)}
|
||||||
|
placeholder={ctx.input.placeholder}
|
||||||
|
id={`dialog-string-${ctx.input.title}`}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault()
|
||||||
|
submit()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div data-slot="footer">
|
||||||
|
<Button size="md" color="ghost" onClick={() => ctx.control.close()}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button size="md" color="secondary" onClick={submit}>
|
||||||
|
{ctx.input.action}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
27
cloud/web/src/ui/dialog.tsx
Normal file
27
cloud/web/src/ui/dialog.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { Dialog as Kobalte } from "@kobalte/core/dialog"
|
||||||
|
import { ComponentProps, ParentProps } from "solid-js"
|
||||||
|
|
||||||
|
export type Props = ParentProps<{
|
||||||
|
size?: "sm" | "md"
|
||||||
|
transition?: boolean
|
||||||
|
}> &
|
||||||
|
ComponentProps<typeof Kobalte>
|
||||||
|
|
||||||
|
export function Dialog(props: Props) {
|
||||||
|
return (
|
||||||
|
<Kobalte {...props}>
|
||||||
|
<Kobalte.Portal>
|
||||||
|
<Kobalte.Overlay data-component="dialog-overlay" />
|
||||||
|
<div data-component="dialog-center">
|
||||||
|
<Kobalte.Content
|
||||||
|
data-transition={props.transition ? "" : undefined}
|
||||||
|
data-size={props.size}
|
||||||
|
data-slot="content"
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Kobalte.Content>
|
||||||
|
</div>
|
||||||
|
</Kobalte.Portal>
|
||||||
|
</Kobalte>
|
||||||
|
)
|
||||||
|
}
|
78
cloud/web/src/ui/style/component/button.css
Normal file
78
cloud/web/src/ui/style/component/button.css
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
[data-component="button"] {
|
||||||
|
width: fit-content;
|
||||||
|
display: flex;
|
||||||
|
line-height: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
text-transform: uppercase;
|
||||||
|
height: var(--space-11);
|
||||||
|
outline: none;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0 var(--space-4);
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-color="primary"] {
|
||||||
|
background-color: var(--color-text);
|
||||||
|
border-color: var(--color-text);
|
||||||
|
color: var(--color-text-invert);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-color="secondary"] {
|
||||||
|
&:active {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-color="ghost"] {
|
||||||
|
border: none;
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
color: var(--color-text-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has([data-slot="icon"]) {
|
||||||
|
padding-left: var(--space-3);
|
||||||
|
padding-right: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-size="sm"] {
|
||||||
|
height: var(--space-8);
|
||||||
|
padding: var(--space-3);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
|
||||||
|
[data-slot="icon"] {
|
||||||
|
width: var(--space-3-5);
|
||||||
|
height: var(--space-3-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has([data-slot="icon"]) {
|
||||||
|
padding-left: var(--space-2);
|
||||||
|
padding-right: var(--space-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="icon"] {
|
||||||
|
width: var(--space-4);
|
||||||
|
height: var(--space-4);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-rotate] [data-slot="icon"] {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
84
cloud/web/src/ui/style/component/dialog.css
Normal file
84
cloud/web/src/ui/style/component/dialog.css
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
[data-component="dialog-overlay"] {
|
||||||
|
pointer-events: none !important;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
animation-name: fadeOut;
|
||||||
|
animation-duration: 200ms;
|
||||||
|
animation-timing-function: ease;
|
||||||
|
opacity: 0;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
|
||||||
|
&[data-expanded] {
|
||||||
|
animation-name: fadeIn;
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="dialog-center"] {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
padding-top: 10vh;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
[data-slot="content"] {
|
||||||
|
width: 45rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
transition: 150ms width;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
outline: none;
|
||||||
|
animation-duration: 1ms;
|
||||||
|
animation-name: zoomOut;
|
||||||
|
animation-timing-function: ease;
|
||||||
|
|
||||||
|
box-shadow: 8px 8px 0px 0px var(--color-gray-4);
|
||||||
|
|
||||||
|
&[data-expanded] {
|
||||||
|
animation-name: zoomIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-transition] {
|
||||||
|
animation-duration: 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-size="sm"] {
|
||||||
|
width: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="header"] {
|
||||||
|
display: flex;
|
||||||
|
padding: var(--space-4) var(--space-4) 0;
|
||||||
|
|
||||||
|
[data-slot="title"] {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="main"] {
|
||||||
|
padding: 0 var(--space-4);
|
||||||
|
|
||||||
|
&:has([data-slot="options"]) {
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="input"] {
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="footer"] {
|
||||||
|
padding: var(--space-4);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-4);
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
cloud/web/src/ui/style/component/input.css
Normal file
34
cloud/web/src/ui/style/component/input.css
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
[data-component="input"] {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
background: transparent;
|
||||||
|
caret-color: var(--color-accent);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
height: var(--space-11);
|
||||||
|
padding: 0 var(--space-4);
|
||||||
|
width: 100%;
|
||||||
|
resize: none;
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-size="sm"] {
|
||||||
|
height: var(--space-9);
|
||||||
|
padding: 0 var(--space-3);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-size="md"] {
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-size="lg"] {
|
||||||
|
height: var(--space-12);
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
}
|
17
cloud/web/src/ui/style/component/label.css
Normal file
17
cloud/web/src/ui/style/component/label.css
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[data-component="label"] {
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
|
||||||
|
&[data-size="sm"] {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
&[data-size="md"] {
|
||||||
|
}
|
||||||
|
&[data-size="lg"] {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
32
cloud/web/src/ui/style/component/title-bar.css
Normal file
32
cloud/web/src/ui/style/component/title-bar.css
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
[data-component="title-bar"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 72px;
|
||||||
|
padding: 0 var(--space-4);
|
||||||
|
border-bottom: 2px solid var(--color-border);
|
||||||
|
|
||||||
|
[data-slot="left"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-1-5);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
[data-component="title-bar"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
50
cloud/web/src/ui/style/index.css
Normal file
50
cloud/web/src/ui/style/index.css
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/* tokens */
|
||||||
|
@import "./token/color.css";
|
||||||
|
@import "./token/reset.css";
|
||||||
|
@import "./token/animation.css";
|
||||||
|
@import "./token/font.css";
|
||||||
|
@import "./token/space.css";
|
||||||
|
|
||||||
|
/* components */
|
||||||
|
@import "./component/label.css";
|
||||||
|
@import "./component/input.css";
|
||||||
|
@import "./component/button.css";
|
||||||
|
@import "./component/dialog.css";
|
||||||
|
@import "./component/title-bar.css";
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--color-text);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
text-underline-offset: 0.1875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
&:active {
|
||||||
|
color: var(--color-text-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: var(--color-text-accent-invert);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive utilities */
|
||||||
|
[data-max-width] {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
max-width: 90rem;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-max-width-64] > * {
|
||||||
|
max-width: 64rem;
|
||||||
|
}
|
||||||
|
}
|
23
cloud/web/src/ui/style/token/animation.css
Normal file
23
cloud/web/src/ui/style/token/animation.css
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
@keyframes zoomIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes zoomOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
88
cloud/web/src/ui/style/token/color.css
Normal file
88
cloud/web/src/ui/style/token/color.css
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
:root {
|
||||||
|
--color-white: hsl(0, 0%, 100%);
|
||||||
|
--color-gray-1: hsl(224, 20%, 94%);
|
||||||
|
--color-gray-2: hsl(224, 6%, 77%);
|
||||||
|
--color-gray-3: hsl(224, 6%, 56%);
|
||||||
|
--color-gray-4: hsl(224, 7%, 36%);
|
||||||
|
--color-gray-5: hsl(224, 10%, 23%);
|
||||||
|
--color-gray-6: hsl(224, 14%, 16%);
|
||||||
|
--color-black: hsl(224, 10%, 10%);
|
||||||
|
|
||||||
|
--hue-orange: 41;
|
||||||
|
--color-orange-low: hsl(var(--hue-orange), 39%, 22%);
|
||||||
|
--color-orange: hsl(var(--hue-orange), 82%, 63%);
|
||||||
|
--color-orange-high: hsl(var(--hue-orange), 82%, 87%);
|
||||||
|
--hue-green: 101;
|
||||||
|
--color-green-low: hsl(var(--hue-green), 39%, 22%);
|
||||||
|
--color-green: hsl(var(--hue-green), 82%, 63%);
|
||||||
|
--color-green-high: hsl(var(--hue-green), 82%, 80%);
|
||||||
|
--hue-blue: 234;
|
||||||
|
--color-blue-low: hsl(var(--hue-blue), 54%, 20%);
|
||||||
|
--color-blue: hsl(var(--hue-blue), 100%, 60%);
|
||||||
|
--color-blue-high: hsl(var(--hue-blue), 100%, 87%);
|
||||||
|
--hue-purple: 281;
|
||||||
|
--color-purple-low: hsl(var(--hue-purple), 39%, 22%);
|
||||||
|
--color-purple: hsl(var(--hue-purple), 82%, 63%);
|
||||||
|
--color-purple-high: hsl(var(--hue-purple), 82%, 89%);
|
||||||
|
--hue-red: 339;
|
||||||
|
--color-red-low: hsl(var(--hue-red), 39%, 22%);
|
||||||
|
--color-red: hsl(var(--hue-red), 82%, 63%);
|
||||||
|
--color-red-high: hsl(var(--hue-red), 82%, 87%);
|
||||||
|
|
||||||
|
--color-accent-low: hsl(13, 75%, 30%);
|
||||||
|
--color-accent: hsl(13, 88%, 57%);
|
||||||
|
--color-accent-high: hsl(13, 100%, 78%);
|
||||||
|
|
||||||
|
--color-text: var(--color-gray-1);
|
||||||
|
--color-text-dimmed: var(--color-gray-3);
|
||||||
|
--color-text-accent: var(--color-accent);
|
||||||
|
--color-text-invert: var(--color-black);
|
||||||
|
--color-text-accent-invert: var(--color-accent-high);
|
||||||
|
--color-bg: var(--color-black);
|
||||||
|
--color-bg-surface: var(--color-gray-5);
|
||||||
|
--color-bg-accent: var(--color-accent-high);
|
||||||
|
--color-border: var(--color-gray-2);
|
||||||
|
|
||||||
|
--color-backdrop-overlay: hsla(223, 13%, 10%, 0.66);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-color-mode="light"] {
|
||||||
|
--color-white: hsl(224, 10%, 10%);
|
||||||
|
--color-gray-1: hsl(224, 14%, 16%);
|
||||||
|
--color-gray-2: hsl(224, 10%, 23%);
|
||||||
|
--color-gray-3: hsl(224, 7%, 36%);
|
||||||
|
--color-gray-4: hsl(224, 6%, 56%);
|
||||||
|
--color-gray-5: hsl(224, 6%, 77%);
|
||||||
|
--color-gray-6: hsl(224, 20%, 94%);
|
||||||
|
--color-gray-7: hsl(224, 19%, 97%);
|
||||||
|
--color-black: hsl(0, 0%, 100%);
|
||||||
|
|
||||||
|
--color-orange-high: hsl(var(--hue-orange), 80%, 25%);
|
||||||
|
--color-orange: hsl(var(--hue-orange), 90%, 60%);
|
||||||
|
--color-orange-low: hsl(var(--hue-orange), 90%, 88%);
|
||||||
|
--color-green-high: hsl(var(--hue-green), 80%, 22%);
|
||||||
|
--color-green: hsl(var(--hue-green), 90%, 46%);
|
||||||
|
--color-green-low: hsl(var(--hue-green), 85%, 90%);
|
||||||
|
--color-blue-high: hsl(var(--hue-blue), 80%, 30%);
|
||||||
|
--color-blue: hsl(var(--hue-blue), 90%, 60%);
|
||||||
|
--color-blue-low: hsl(var(--hue-blue), 88%, 90%);
|
||||||
|
--color-purple-high: hsl(var(--hue-purple), 90%, 30%);
|
||||||
|
--color-purple: hsl(var(--hue-purple), 90%, 60%);
|
||||||
|
--color-purple-low: hsl(var(--hue-purple), 80%, 90%);
|
||||||
|
--color-red-high: hsl(var(--hue-red), 80%, 30%);
|
||||||
|
--color-red: hsl(var(--hue-red), 90%, 60%);
|
||||||
|
--color-red-low: hsl(var(--hue-red), 80%, 90%);
|
||||||
|
|
||||||
|
--color-accent-high: hsl(13, 75%, 26%);
|
||||||
|
--color-accent: hsl(13, 88%, 60%);
|
||||||
|
--color-accent-low: hsl(13, 100%, 89%);
|
||||||
|
|
||||||
|
--color-text-accent: var(--color-accent);
|
||||||
|
--color-text-dimmed: var(--color-gray-4);
|
||||||
|
--color-text-invert: var(--color-black);
|
||||||
|
--color-text-accent-invert: var(--color-accent-low);
|
||||||
|
--color-bg-surface: var(--color-gray-6);
|
||||||
|
--color-bg-accent: var(--color-accent);
|
||||||
|
|
||||||
|
--color-backdrop-overlay: hsla(225, 9%, 36%, 0.66);
|
||||||
|
}
|
20
cloud/web/src/ui/style/token/font.css
Normal file
20
cloud/web/src/ui/style/token/font.css
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
:root {
|
||||||
|
--font-size-2xs: 0.6875rem;
|
||||||
|
--font-size-xs: 0.75rem;
|
||||||
|
--font-size-sm: 0.8125rem;
|
||||||
|
--font-size-md: 0.9375rem;
|
||||||
|
--font-size-lg: 1.125rem;
|
||||||
|
--font-size-xl: 1.25rem;
|
||||||
|
--font-size-2xl: 1.5rem;
|
||||||
|
--font-size-3xl: 1.875rem;
|
||||||
|
--font-size-4xl: 2.25rem;
|
||||||
|
--font-size-5xl: 3rem;
|
||||||
|
--font-size-6xl: 3.75rem;
|
||||||
|
--font-size-7xl: 4.5rem;
|
||||||
|
--font-size-8xl: 6rem;
|
||||||
|
--font-size-9xl: 8rem;
|
||||||
|
--font-mono: IBM Plex Mono, monospace;
|
||||||
|
--font-sans: Rubik, sans-serif;
|
||||||
|
|
||||||
|
--font-line-height: 1.75;
|
||||||
|
}
|
212
cloud/web/src/ui/style/token/reset.css
Normal file
212
cloud/web/src/ui/style/token/reset.css
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-width: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--global-color-border, currentColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
line-height: 1.5;
|
||||||
|
--font-fallback: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||||
|
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
|
||||||
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
font-family: var(--global-font-body, var(--font-fallback));
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 0;
|
||||||
|
color: inherit;
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
audio,
|
||||||
|
iframe,
|
||||||
|
embed,
|
||||||
|
object {
|
||||||
|
display: block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
pre,
|
||||||
|
samp {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type="button"],
|
||||||
|
[type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
background-color: transparent;
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
text-indent: 0;
|
||||||
|
border-color: inherit;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--global-color-placeholder, #9ca3af);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr:where([title]) {
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp,
|
||||||
|
pre {
|
||||||
|
font-size: 1em;
|
||||||
|
--font-mono-fallback: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
|
"Liberation Mono", "Courier New";
|
||||||
|
font-family: var(--global-font-mono, var(--font-fallback));
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="email"],
|
||||||
|
input[type="search"],
|
||||||
|
input[type="password"] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="search"] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-search-decoration,
|
||||||
|
::-webkit-search-cancel-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"]::-webkit-inner-spin-button,
|
||||||
|
input[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
:-moz-ui-invalid {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:-moz-focusring {
|
||||||
|
outline: auto;
|
||||||
|
}
|
38
cloud/web/src/ui/style/token/space.css
Normal file
38
cloud/web/src/ui/style/token/space.css
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
:root {
|
||||||
|
--space-0: 0;
|
||||||
|
--space-px: 1px;
|
||||||
|
--space-0-5: 0.125rem;
|
||||||
|
--space-1: 0.25rem;
|
||||||
|
--space-1-5: 0.375rem;
|
||||||
|
--space-2: 0.5rem;
|
||||||
|
--space-2-5: 0.625rem;
|
||||||
|
--space-3: 0.75rem;
|
||||||
|
--space-3-5: 0.875rem;
|
||||||
|
--space-4: 1rem;
|
||||||
|
--space-4-5: 1.125rem;
|
||||||
|
--space-5: 1.25rem;
|
||||||
|
--space-6: 1.5rem;
|
||||||
|
--space-7: 1.75rem;
|
||||||
|
--space-8: 2rem;
|
||||||
|
--space-9: 2.25rem;
|
||||||
|
--space-10: 2.5rem;
|
||||||
|
--space-11: 2.75rem;
|
||||||
|
--space-12: 3rem;
|
||||||
|
--space-14: 3.5rem;
|
||||||
|
--space-16: 4rem;
|
||||||
|
--space-20: 5rem;
|
||||||
|
--space-24: 6rem;
|
||||||
|
--space-28: 7rem;
|
||||||
|
--space-32: 8rem;
|
||||||
|
--space-36: 9rem;
|
||||||
|
--space-40: 10rem;
|
||||||
|
--space-44: 11rem;
|
||||||
|
--space-48: 12rem;
|
||||||
|
--space-52: 13rem;
|
||||||
|
--space-56: 14rem;
|
||||||
|
--space-60: 15rem;
|
||||||
|
--space-64: 16rem;
|
||||||
|
--space-72: 18rem;
|
||||||
|
--space-80: 20rem;
|
||||||
|
--space-96: 24rem;
|
||||||
|
}
|
1292
cloud/web/src/ui/svg/icons.tsx
Normal file
1292
cloud/web/src/ui/svg/icons.tsx
Normal file
File diff suppressed because it is too large
Load diff
67
cloud/web/src/ui/svg/index.tsx
Normal file
67
cloud/web/src/ui/svg/index.tsx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { JSX } from "solid-js"
|
||||||
|
|
||||||
|
export function IconLogomark(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} viewBox="0 0 28 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M14 31.5L0 23.6873V7.81266L14 0L28 7.81266V23.6873L14 31.5ZM14 28.4664L25.3456 22.0251V9.47493L14 2.99209L2.65443 9.47493V22.0251L14 28.4664ZM13.9572 24.6016C12.2732 24.6016 10.7176 24.1999 9.29052 23.3964C7.89195 22.593 6.7788 21.5125 5.95107 20.155C5.12334 18.7698 4.70948 17.2599 4.70948 15.6253C4.70948 13.9908 5.12334 12.4947 5.95107 11.1372C6.7788 9.77968 7.89195 8.69921 9.29052 7.89578C10.7176 7.06464 12.2732 6.64908 13.9572 6.64908C15.6412 6.64908 17.1825 7.06464 18.581 7.89578C19.9796 8.69921 21.0928 9.77968 21.9205 11.1372C22.7768 12.4947 23.2049 13.9908 23.2049 15.6253C23.2049 17.2599 22.791 18.7559 21.9633 20.1135C21.1356 21.471 20.0224 22.5653 18.6239 23.3964C17.2253 24.1999 15.6697 24.6016 13.9572 24.6016ZM13.9572 22.2744C15.213 22.2744 16.3547 21.9697 17.3823 21.3602C18.4098 20.7507 19.2375 19.9472 19.8654 18.9499C20.4934 17.9248 20.8073 16.8166 20.8073 15.6253C20.8073 14.4063 20.4934 13.2982 19.8654 12.3008C19.2375 11.3034 18.4098 10.5 17.3823 9.8905C16.3547 9.281 15.213 8.97625 13.9572 8.97625C12.7299 8.97625 11.5882 9.281 10.5321 9.8905C9.50459 10.5 8.67686 11.3034 8.04893 12.3008C7.421 13.2982 7.10703 14.4063 7.10703 15.6253C7.10703 16.8166 7.421 17.9248 8.04893 18.9499C8.67686 19.9472 9.50459 20.7507 10.5321 21.3602C11.5882 21.9697 12.7299 22.2744 13.9572 22.2744Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} viewBox="0 0 220 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M14 31.5L0 23.6873V7.81266L14 0L28 7.81266V23.6873L14 31.5ZM14 28.4664L25.3456 22.0251V9.47493L14 2.99208L2.65443 9.47493V22.0251L14 28.4664ZM13.9572 24.6016C12.2732 24.6016 10.7176 24.1999 9.29052 23.3964C7.89195 22.593 6.7788 21.5125 5.95107 20.155C5.12334 18.7698 4.70948 17.2599 4.70948 15.6253C4.70948 13.9908 5.12334 12.4947 5.95107 11.1372C6.7788 9.77968 7.89195 8.69921 9.29052 7.89578C10.7176 7.06464 12.2732 6.64908 13.9572 6.64908C15.6412 6.64908 17.1825 7.06464 18.581 7.89578C19.9796 8.69921 21.0928 9.77968 21.9205 11.1372C22.7768 12.4947 23.2049 13.9908 23.2049 15.6253C23.2049 17.2599 22.791 18.7559 21.9633 20.1135C21.1356 21.471 20.0224 22.5653 18.6239 23.3964C17.2253 24.1999 15.6697 24.6016 13.9572 24.6016ZM13.9572 22.2744C15.213 22.2744 16.3547 21.9697 17.3823 21.3602C18.4098 20.7507 19.2375 19.9472 19.8654 18.9499C20.4934 17.9248 20.8073 16.8166 20.8073 15.6253C20.8073 14.4063 20.4934 13.2982 19.8654 12.3008C19.2375 11.3034 18.4098 10.5 17.3823 9.8905C16.3547 9.281 15.213 8.97625 13.9572 8.97625C12.7299 8.97625 11.5882 9.281 10.5321 9.8905C9.50459 10.5 8.67686 11.3034 8.04893 12.3008C7.421 13.2982 7.10703 14.4063 7.10703 15.6253C7.10703 16.8166 7.421 17.9248 8.04893 18.9499C8.67686 19.9472 9.50459 20.7507 10.5321 21.3602C11.5882 21.9697 12.7299 22.2744 13.9572 22.2744Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M45.628 28.432C42.556 28.432 40.252 27.688 38.716 26.2C37.204 24.688 36.448 22.552 36.448 19.792V12.628C36.448 9.84399 37.204 7.70799 38.716 6.21999C40.252 4.73199 42.556 3.98799 45.628 3.98799C48.7 3.98799 50.992 4.73199 52.504 6.21999C54.04 7.70799 54.808 9.84399 54.808 12.628V19.792C54.808 22.552 54.04 24.688 52.504 26.2C50.992 27.688 48.7 28.432 45.628 28.432ZM45.628 25.228C47.452 25.228 48.832 24.76 49.768 23.824C50.704 22.864 51.172 21.484 51.172 19.684V12.736C51.172 10.912 50.704 9.53199 49.768 8.59599C48.832 7.65999 47.452 7.19199 45.628 7.19199C43.828 7.19199 42.448 7.65999 41.488 8.59599C40.552 9.53199 40.084 10.912 40.084 12.736V19.684C40.084 21.484 40.552 22.864 41.488 23.824C42.448 24.76 43.828 25.228 45.628 25.228Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M67.0294 28.432C66.1654 28.432 65.2414 28.312 64.2574 28.072C63.2734 27.856 62.4694 27.544 61.8454 27.136L61.7734 24.184C62.4214 24.544 63.1414 24.832 63.9334 25.048C64.7254 25.24 65.4574 25.336 66.1294 25.336C67.4254 25.336 68.3614 24.964 68.9374 24.22C69.5374 23.452 69.8374 22.312 69.8374 20.8V16.912C69.8374 15.592 69.6094 14.632 69.1534 14.032C68.6974 13.408 68.0014 13.096 67.0654 13.096C66.2734 13.096 65.4694 13.336 64.6534 13.816C63.8374 14.272 62.8174 15.064 61.5934 16.192L61.4854 13.168C62.2534 12.448 62.9854 11.848 63.6814 11.368C64.3774 10.888 65.0854 10.528 65.8054 10.288C66.5254 10.048 67.2934 9.92799 68.1094 9.92799C69.8614 9.92799 71.1814 10.468 72.0694 11.548C72.9814 12.628 73.4374 14.272 73.4374 16.48V21.196C73.4374 23.572 72.8854 25.372 71.7814 26.596C70.7014 27.82 69.1174 28.432 67.0294 28.432ZM59.1094 34.66C58.8454 34.66 58.7134 34.528 58.7134 34.264V14.536C58.7134 13.936 58.6894 13.3 58.6414 12.628C58.5934 11.956 58.5334 11.356 58.4614 10.828C58.4374 10.516 58.5694 10.36 58.8574 10.36H61.4134C61.6774 10.36 61.8214 10.48 61.8454 10.72C61.8934 10.912 61.9414 11.164 61.9894 11.476C62.0374 11.788 62.0734 12.1 62.0974 12.412C62.1214 12.7 62.1334 12.916 62.1334 13.06L62.3134 14.968V34.264C62.3134 34.528 62.1814 34.66 61.9174 34.66H59.1094Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M84.0237 28.432C81.5757 28.432 79.7157 27.88 78.4437 26.776C77.1957 25.672 76.5717 24.028 76.5717 21.844V17.056C76.5717 14.728 77.1957 12.964 78.4437 11.764C79.6917 10.54 81.5397 9.92799 83.9877 9.92799C86.4117 9.92799 88.2477 10.516 89.4957 11.692C90.7437 12.868 91.3677 14.584 91.3677 16.84V19.648C91.3677 19.912 91.2477 20.044 91.0077 20.044H80.1717V21.196C80.1717 22.66 80.4837 23.728 81.1077 24.4C81.7317 25.072 82.7397 25.408 84.1317 25.408C85.1877 25.408 86.0037 25.24 86.5797 24.904C87.1557 24.568 87.4437 24.052 87.4437 23.356C87.4437 23.092 87.5877 22.96 87.8757 22.96H90.6477C90.8637 22.96 90.9957 23.08 91.0437 23.32C91.1397 24.928 90.5637 26.188 89.3157 27.1C88.0677 27.988 86.3037 28.432 84.0237 28.432ZM80.1717 17.308H87.8037V17.128C87.8037 15.688 87.4917 14.632 86.8677 13.96C86.2437 13.288 85.2957 12.952 84.0237 12.952C82.7277 12.952 81.7557 13.3 81.1077 13.996C80.4837 14.692 80.1717 15.736 80.1717 17.128V17.308Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M106.647 28C106.383 28 106.251 27.868 106.251 27.604V16.3C106.251 15.196 106.023 14.392 105.567 13.888C105.135 13.36 104.451 13.096 103.515 13.096C102.627 13.096 101.751 13.36 100.887 13.888C100.023 14.392 98.9906 15.244 97.7906 16.444L97.7186 13.528C98.4866 12.712 99.2306 12.04 99.9506 11.512C100.671 10.984 101.403 10.588 102.147 10.324C102.915 10.06 103.719 9.92799 104.559 9.92799C106.287 9.92799 107.595 10.432 108.483 11.44C109.395 12.424 109.851 13.912 109.851 15.904V27.604C109.851 27.868 109.731 28 109.491 28H106.647ZM95.2346 28C94.9706 28 94.8386 27.868 94.8386 27.604V14.824C94.8386 14.152 94.8026 13.444 94.7306 12.7C94.6826 11.932 94.6346 11.32 94.5866 10.864C94.5386 10.528 94.6706 10.36 94.9826 10.36H97.5386C97.7786 10.36 97.9226 10.48 97.9706 10.72C98.0186 10.936 98.0666 11.236 98.1146 11.62C98.1626 12.004 98.2106 12.412 98.2586 12.844C98.3066 13.276 98.3306 13.648 98.3306 13.96L98.4386 15.112V27.604C98.4386 27.868 98.3066 28 98.0426 28H95.2346Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M122.87 28.432C119.894 28.432 117.626 27.7 116.066 26.236C114.506 24.748 113.726 22.6 113.726 19.792V12.664C113.726 9.83199 114.506 7.68399 116.066 6.21999C117.65 4.73199 119.93 3.98799 122.906 3.98799C124.85 3.98799 126.542 4.31199 127.982 4.95999C129.422 5.60799 130.49 6.54399 131.186 7.76799C131.906 8.96799 132.146 10.408 131.906 12.088C131.882 12.232 131.846 12.352 131.798 12.448C131.75 12.544 131.654 12.592 131.51 12.592H128.63C128.366 12.592 128.246 12.46 128.27 12.196C128.342 10.612 127.922 9.37599 127.01 8.48799C126.098 7.59999 124.742 7.15599 122.942 7.15599C121.142 7.15599 119.762 7.62399 118.802 8.55999C117.842 9.49599 117.362 10.864 117.362 12.664V19.756C117.362 21.556 117.842 22.924 118.802 23.86C119.762 24.796 121.142 25.264 122.942 25.264C124.766 25.264 126.146 24.82 127.082 23.932C128.042 23.02 128.438 21.784 128.27 20.224C128.246 19.96 128.366 19.828 128.63 19.828H131.474C131.714 19.828 131.858 19.996 131.906 20.332C132.05 21.964 131.762 23.392 131.042 24.616C130.346 25.816 129.29 26.752 127.874 27.424C126.458 28.096 124.79 28.432 122.87 28.432Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M142.558 28.432C140.086 28.432 138.19 27.832 136.87 26.632C135.55 25.432 134.89 23.62 134.89 21.196V17.164C134.89 14.74 135.538 12.928 136.834 11.728C138.154 10.528 140.062 9.92799 142.558 9.92799C145.03 9.92799 146.914 10.528 148.21 11.728C149.53 12.928 150.19 14.74 150.19 17.164V21.196C150.19 23.62 149.542 25.432 148.246 26.632C146.95 27.832 145.054 28.432 142.558 28.432ZM142.558 25.3C143.974 25.3 144.994 24.94 145.618 24.22C146.266 23.5 146.59 22.384 146.59 20.872V17.488C146.59 16 146.266 14.896 145.618 14.176C144.994 13.432 143.974 13.06 142.558 13.06C141.118 13.06 140.074 13.432 139.426 14.176C138.802 14.896 138.49 16 138.49 17.488V20.872C138.49 22.384 138.802 23.5 139.426 24.22C140.074 24.94 141.118 25.3 142.558 25.3Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M165.493 28C165.229 28 165.097 27.868 165.097 27.604V16.3C165.097 15.196 164.869 14.392 164.413 13.888C163.981 13.36 163.297 13.096 162.361 13.096C161.473 13.096 160.597 13.36 159.733 13.888C158.869 14.392 157.837 15.244 156.637 16.444L156.565 13.528C157.333 12.712 158.077 12.04 158.797 11.512C159.517 10.984 160.249 10.588 160.993 10.324C161.761 10.06 162.565 9.92799 163.405 9.92799C165.133 9.92799 166.441 10.432 167.329 11.44C168.241 12.424 168.697 13.912 168.697 15.904V27.604C168.697 27.868 168.577 28 168.337 28H165.493ZM154.081 28C153.817 28 153.685 27.868 153.685 27.604V14.824C153.685 14.152 153.649 13.444 153.577 12.7C153.529 11.932 153.481 11.32 153.433 10.864C153.385 10.528 153.517 10.36 153.829 10.36H156.385C156.625 10.36 156.769 10.48 156.817 10.72C156.865 10.936 156.913 11.236 156.961 11.62C157.009 12.004 157.057 12.412 157.105 12.844C157.153 13.276 157.177 13.648 157.177 13.96L157.285 15.112V27.604C157.285 27.868 157.153 28 156.889 28H154.081Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M179.477 28.432C178.157 28.432 177.113 28.228 176.345 27.82C175.577 27.388 175.013 26.74 174.653 25.876C174.317 24.988 174.149 23.884 174.149 22.564V13.42H171.413C171.173 13.42 171.053 13.288 171.053 13.024V10.756C171.053 10.492 171.173 10.36 171.413 10.36H174.257L174.689 6.03999C174.713 5.79999 174.857 5.67999 175.121 5.67999H177.317C177.581 5.67999 177.713 5.79999 177.713 6.03999L177.749 10.36H182.897C183.161 10.36 183.293 10.492 183.293 10.756V13.024C183.293 13.288 183.161 13.42 182.897 13.42H177.749V22.42C177.749 23.476 177.929 24.232 178.289 24.688C178.673 25.12 179.321 25.336 180.233 25.336C180.689 25.336 181.145 25.288 181.601 25.192C182.081 25.072 182.501 24.928 182.861 24.76C183.173 24.64 183.329 24.736 183.329 25.048V27.388C183.329 27.604 183.233 27.748 183.041 27.82C182.585 27.988 182.057 28.132 181.457 28.252C180.881 28.372 180.221 28.432 179.477 28.432Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M186.233 28C185.969 28 185.837 27.868 185.837 27.604V14.716C185.837 14.02 185.813 13.36 185.765 12.736C185.741 12.088 185.681 11.44 185.585 10.792C185.537 10.504 185.669 10.36 185.981 10.36H188.537C188.801 10.36 188.945 10.48 188.969 10.72C189.065 11.152 189.137 11.656 189.185 12.232C189.233 12.808 189.257 13.3 189.257 13.708L189.437 15.688V27.604C189.437 27.868 189.305 28 189.041 28H186.233ZM189.041 17.02L188.897 13.744C189.401 13 189.977 12.352 190.625 11.8C191.273 11.224 191.933 10.768 192.605 10.432C193.277 10.096 193.901 9.92799 194.477 9.92799C194.885 9.92799 195.209 9.96399 195.449 10.036C195.641 10.108 195.749 10.252 195.773 10.468C195.821 10.996 195.833 11.56 195.809 12.16C195.809 12.736 195.773 13.288 195.701 13.816C195.653 14.104 195.485 14.212 195.197 14.14C195.029 14.092 194.837 14.056 194.621 14.032C194.405 14.008 194.177 13.996 193.937 13.996C193.361 13.996 192.785 14.128 192.209 14.392C191.633 14.632 191.069 14.98 190.517 15.436C189.989 15.868 189.497 16.396 189.041 17.02Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M204.648 28.432C202.176 28.432 200.28 27.832 198.96 26.632C197.64 25.432 196.98 23.62 196.98 21.196V17.164C196.98 14.74 197.628 12.928 198.924 11.728C200.244 10.528 202.152 9.92799 204.648 9.92799C207.12 9.92799 209.004 10.528 210.3 11.728C211.62 12.928 212.28 14.74 212.28 17.164V21.196C212.28 23.62 211.632 25.432 210.336 26.632C209.04 27.832 207.144 28.432 204.648 28.432ZM204.648 25.3C206.064 25.3 207.084 24.94 207.708 24.22C208.356 23.5 208.68 22.384 208.68 20.872V17.488C208.68 16 208.356 14.896 207.708 14.176C207.084 13.432 206.064 13.06 204.648 13.06C203.208 13.06 202.164 13.432 201.516 14.176C200.892 14.896 200.58 16 200.58 17.488V20.872C200.58 22.384 200.892 23.5 201.516 24.22C202.164 24.94 203.208 25.3 204.648 25.3Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M216.171 28C215.907 28 215.775 27.868 215.775 27.604V3.80799C215.775 3.54399 215.907 3.41199 216.171 3.41199H218.979C219.243 3.41199 219.375 3.54399 219.375 3.80799V27.604C219.375 27.868 219.243 28 218.979 28H216.171Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
26
cloud/web/src/util/context.tsx
Normal file
26
cloud/web/src/util/context.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { ParentProps, Show, createContext, useContext } from "solid-js"
|
||||||
|
|
||||||
|
export function createInitializedContext<
|
||||||
|
Name extends string,
|
||||||
|
T extends { ready: boolean },
|
||||||
|
>(name: Name, cb: () => T) {
|
||||||
|
const ctx = createContext<T>()
|
||||||
|
|
||||||
|
return {
|
||||||
|
use: () => {
|
||||||
|
const context = useContext(ctx)
|
||||||
|
if (!context) throw new Error(`No ${name} context`)
|
||||||
|
return context
|
||||||
|
},
|
||||||
|
provider: (props: ParentProps) => {
|
||||||
|
const value = cb()
|
||||||
|
return (
|
||||||
|
<Show when={value.ready}>
|
||||||
|
<ctx.Provider value={value} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</ctx.Provider>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
9
cloud/web/sst-env.d.ts
vendored
Normal file
9
cloud/web/sst-env.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/* This file is auto-generated by SST. Do not edit. */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/* deno-fmt-ignore-file */
|
||||||
|
|
||||||
|
/// <reference path="../../sst-env.d.ts" />
|
||||||
|
|
||||||
|
import "sst"
|
||||||
|
export {}
|
12
cloud/web/tsconfig.json
Normal file
12
cloud/web/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@tsconfig/node22/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["vite/client"]
|
||||||
|
}
|
||||||
|
}
|
63
cloud/web/vite.config.ts
Normal file
63
cloud/web/vite.config.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import { defineConfig } from "vite"
|
||||||
|
import solidPlugin from "vite-plugin-solid"
|
||||||
|
import pages from "vite-plugin-pages"
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
import { generateHydrationScript, getAssets } from "solid-js/web"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
pages({
|
||||||
|
exclude: ["**/~*", "**/components/*"],
|
||||||
|
}),
|
||||||
|
solidPlugin({ ssr: true }),
|
||||||
|
{
|
||||||
|
name: "vite-plugin-solid-ssr-render",
|
||||||
|
apply: (config, env) => {
|
||||||
|
return env.command === "build" && !config.build?.ssr
|
||||||
|
},
|
||||||
|
closeBundle: async () => {
|
||||||
|
console.log("Pre-rendering pages...")
|
||||||
|
const dist = path.resolve("dist")
|
||||||
|
try {
|
||||||
|
const serverEntryPath = path.join(dist, "server/entry-server.js")
|
||||||
|
const serverEntry = await import(serverEntryPath + "?t=" + Date.now())
|
||||||
|
|
||||||
|
const template = fs.readFileSync(
|
||||||
|
path.join(dist, "client/index.html"),
|
||||||
|
"utf-8",
|
||||||
|
)
|
||||||
|
fs.writeFileSync(path.join(dist, "client/fallback.html"), template)
|
||||||
|
|
||||||
|
const routes = ["/"]
|
||||||
|
for (const route of routes) {
|
||||||
|
const { app } = await serverEntry.render({ url: route })
|
||||||
|
const html = template
|
||||||
|
.replace("<!--ssr-outlet-->", app)
|
||||||
|
.replace("<!--ssr-head-->", generateHydrationScript())
|
||||||
|
.replace("<!--ssr-assets-->", getAssets())
|
||||||
|
const filePath = path.join(
|
||||||
|
dist,
|
||||||
|
`client${route === "/" ? "/index" : route}.html`,
|
||||||
|
)
|
||||||
|
fs.mkdirSync(path.dirname(filePath), {
|
||||||
|
recursive: true,
|
||||||
|
})
|
||||||
|
fs.writeFileSync(filePath, html)
|
||||||
|
|
||||||
|
console.log(`Pre-rendered: ${filePath}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during pre-rendering:", error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
host: "0.0.0.0",
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: "esnext",
|
||||||
|
},
|
||||||
|
})
|
22
infra/app.ts
22
infra/app.ts
|
@ -1,8 +1,4 @@
|
||||||
export const domain = (() => {
|
import { domain } from "./stage"
|
||||||
if ($app.stage === "production") return "opencode.ai"
|
|
||||||
if ($app.stage === "dev") return "dev.opencode.ai"
|
|
||||||
return `${$app.stage}.dev.opencode.ai`
|
|
||||||
})()
|
|
||||||
|
|
||||||
const GITHUB_APP_ID = new sst.Secret("GITHUB_APP_ID")
|
const GITHUB_APP_ID = new sst.Secret("GITHUB_APP_ID")
|
||||||
const GITHUB_APP_PRIVATE_KEY = new sst.Secret("GITHUB_APP_PRIVATE_KEY")
|
const GITHUB_APP_PRIVATE_KEY = new sst.Secret("GITHUB_APP_PRIVATE_KEY")
|
||||||
|
@ -37,24 +33,12 @@ export const api = new sst.cloudflare.Worker("Api", {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
new sst.cloudflare.x.Astro("Web", {
|
export const web = new sst.cloudflare.x.Astro("Web", {
|
||||||
domain,
|
domain,
|
||||||
path: "packages/web",
|
path: "packages/web",
|
||||||
environment: {
|
environment: {
|
||||||
// For astro config
|
// For astro config
|
||||||
SST_STAGE: $app.stage,
|
SST_STAGE: $app.stage,
|
||||||
VITE_API_URL: api.url,
|
VITE_API_URL: api.url.apply((url) => url!),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const OPENCODE_API_KEY = new sst.Secret("OPENCODE_API_KEY")
|
|
||||||
const ANTHROPIC_API_KEY = new sst.Secret("ANTHROPIC_API_KEY")
|
|
||||||
const OPENAI_API_KEY = new sst.Secret("OPENAI_API_KEY")
|
|
||||||
const ZHIPU_API_KEY = new sst.Secret("ZHIPU_API_KEY")
|
|
||||||
|
|
||||||
export const gateway = new sst.cloudflare.Worker("GatewayApi", {
|
|
||||||
domain: `api.gateway.${domain}`,
|
|
||||||
handler: "packages/function/src/gateway.ts",
|
|
||||||
url: true,
|
|
||||||
link: [OPENCODE_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, ZHIPU_API_KEY],
|
|
||||||
})
|
|
||||||
|
|
105
infra/cloud.ts
Normal file
105
infra/cloud.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import { WebhookEndpoint } from "pulumi-stripe"
|
||||||
|
import { domain } from "./stage"
|
||||||
|
import { web } from "./app"
|
||||||
|
|
||||||
|
export const stripeWebhook = new WebhookEndpoint("StripeWebhook", {
|
||||||
|
url: $interpolate`https://api.gateway.${domain}/stripe/webhook`,
|
||||||
|
enabledEvents: [
|
||||||
|
"checkout.session.async_payment_failed",
|
||||||
|
"checkout.session.async_payment_succeeded",
|
||||||
|
"checkout.session.completed",
|
||||||
|
"checkout.session.expired",
|
||||||
|
"customer.created",
|
||||||
|
"customer.deleted",
|
||||||
|
"customer.updated",
|
||||||
|
"customer.discount.created",
|
||||||
|
"customer.discount.deleted",
|
||||||
|
"customer.discount.updated",
|
||||||
|
"customer.source.created",
|
||||||
|
"customer.source.deleted",
|
||||||
|
"customer.source.expiring",
|
||||||
|
"customer.source.updated",
|
||||||
|
"customer.subscription.created",
|
||||||
|
"customer.subscription.deleted",
|
||||||
|
"customer.subscription.paused",
|
||||||
|
"customer.subscription.pending_update_applied",
|
||||||
|
"customer.subscription.pending_update_expired",
|
||||||
|
"customer.subscription.resumed",
|
||||||
|
"customer.subscription.trial_will_end",
|
||||||
|
"customer.subscription.updated",
|
||||||
|
"customer.tax_id.created",
|
||||||
|
"customer.tax_id.deleted",
|
||||||
|
"customer.tax_id.updated",
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const DATABASE_USERNAME = new sst.Secret("DATABASE_USERNAME")
|
||||||
|
const DATABASE_PASSWORD = new sst.Secret("DATABASE_PASSWORD")
|
||||||
|
export const database = new sst.Linkable("Database", {
|
||||||
|
properties: {
|
||||||
|
host: "aws-us-east-2-1.pg.psdb.cloud",
|
||||||
|
database: "postgres",
|
||||||
|
username: DATABASE_USERNAME.value,
|
||||||
|
password: DATABASE_PASSWORD.value,
|
||||||
|
port: 5432,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
new sst.x.DevCommand("Studio", {
|
||||||
|
link: [database],
|
||||||
|
dev: {
|
||||||
|
command: "bun db studio",
|
||||||
|
directory: "cloud/core",
|
||||||
|
autostart: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const GITHUB_CLIENT_ID_CONSOLE = new sst.Secret("GITHUB_CLIENT_ID_CONSOLE")
|
||||||
|
const GITHUB_CLIENT_SECRET_CONSOLE = new sst.Secret("GITHUB_CLIENT_SECRET_CONSOLE")
|
||||||
|
const authStorage = new sst.cloudflare.Kv("AuthStorage")
|
||||||
|
export const auth = new sst.cloudflare.Worker("AuthApi", {
|
||||||
|
domain: `auth.${domain}`,
|
||||||
|
handler: "cloud/function/src/auth.ts",
|
||||||
|
url: true,
|
||||||
|
link: [database, authStorage, GITHUB_CLIENT_ID_CONSOLE, GITHUB_CLIENT_SECRET_CONSOLE],
|
||||||
|
})
|
||||||
|
|
||||||
|
const ANTHROPIC_API_KEY = new sst.Secret("ANTHROPIC_API_KEY")
|
||||||
|
const OPENAI_API_KEY = new sst.Secret("OPENAI_API_KEY")
|
||||||
|
const ZHIPU_API_KEY = new sst.Secret("ZHIPU_API_KEY")
|
||||||
|
|
||||||
|
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||||
|
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||||
|
properties: { value: auth.url.apply((url) => url!) },
|
||||||
|
})
|
||||||
|
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
|
||||||
|
properties: { value: stripeWebhook.secret },
|
||||||
|
})
|
||||||
|
export const gateway = new sst.cloudflare.Worker("GatewayApi", {
|
||||||
|
domain: `api.gateway.${domain}`,
|
||||||
|
handler: "cloud/function/src/gateway.ts",
|
||||||
|
url: true,
|
||||||
|
link: [
|
||||||
|
database,
|
||||||
|
AUTH_API_URL,
|
||||||
|
STRIPE_WEBHOOK_SECRET,
|
||||||
|
STRIPE_SECRET_KEY,
|
||||||
|
ANTHROPIC_API_KEY,
|
||||||
|
OPENAI_API_KEY,
|
||||||
|
ZHIPU_API_KEY,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const console = new sst.cloudflare.x.StaticSite("Console", {
|
||||||
|
domain: `console.${domain}`,
|
||||||
|
path: "cloud/web",
|
||||||
|
build: {
|
||||||
|
command: "bun run build",
|
||||||
|
output: "dist/client",
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
VITE_DOCS_URL: web.url.apply((url) => url!),
|
||||||
|
VITE_API_URL: gateway.url.apply((url) => url!),
|
||||||
|
VITE_AUTH_URL: auth.url.apply((url) => url!),
|
||||||
|
},
|
||||||
|
})
|
5
infra/stage.ts
Normal file
5
infra/stage.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export const domain = (() => {
|
||||||
|
if ($app.stage === "production") return "opencode.ai"
|
||||||
|
if ($app.stage === "dev") return "dev.opencode.ai"
|
||||||
|
return `${$app.stage}.dev.opencode.ai`
|
||||||
|
})()
|
|
@ -1,5 +1,25 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"provider": {
|
||||||
|
"oc-frank": {
|
||||||
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
|
"name": "OC-Frank",
|
||||||
|
"options": {
|
||||||
|
"baseURL": "https://api.gateway.frank.dev.opencode.ai/v1"
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"anthropic/claude-sonnet-4": {
|
||||||
|
"name": "Claude Sonnet 4"
|
||||||
|
},
|
||||||
|
"openai/gpt-4.1": {
|
||||||
|
"name": "GPT-4.1"
|
||||||
|
},
|
||||||
|
"zhipuai/glm-4.5-flash": {
|
||||||
|
"name": "GLM-4.5 Flash"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"context7": {
|
"context7": {
|
||||||
"type": "remote",
|
"type": "remote",
|
||||||
|
|
|
@ -12,10 +12,12 @@
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
|
"cloud/*",
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"packages/sdk/js"
|
"packages/sdk/js"
|
||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"ai": "5.0.0-beta.34",
|
"ai": "5.0.0-beta.34",
|
||||||
|
@ -25,6 +27,9 @@
|
||||||
"remeda": "2.26.0"
|
"remeda": "2.26.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pulumi-stripe": "0.0.24"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"sst": "3.17.8"
|
"sst": "3.17.8"
|
||||||
|
|
|
@ -7,16 +7,11 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "4.20250522.0",
|
"@cloudflare/workers-types": "4.20250522.0",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"openai": "5.11.0",
|
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.0",
|
|
||||||
"@ai-sdk/openai": "2.0.2",
|
|
||||||
"@ai-sdk/openai-compatible": "1.0.1",
|
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"ai": "catalog:",
|
|
||||||
"hono": "catalog:",
|
"hono": "catalog:",
|
||||||
"jose": "6.0.11"
|
"jose": "6.0.11"
|
||||||
}
|
}
|
||||||
|
|
40
packages/function/sst-env.d.ts
vendored
40
packages/function/sst-env.d.ts
vendored
|
@ -10,6 +10,30 @@ declare module "sst" {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"AUTH_API_URL": {
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"Console": {
|
||||||
|
"type": "sst.cloudflare.StaticSite"
|
||||||
|
"url": string
|
||||||
|
}
|
||||||
|
"DATABASE_PASSWORD": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"DATABASE_USERNAME": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"Database": {
|
||||||
|
"database": string
|
||||||
|
"host": string
|
||||||
|
"password": string
|
||||||
|
"port": number
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
"username": string
|
||||||
|
}
|
||||||
"GITHUB_APP_ID": {
|
"GITHUB_APP_ID": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
|
@ -18,14 +42,26 @@ declare module "sst" {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"GITHUB_CLIENT_ID_CONSOLE": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"GITHUB_CLIENT_SECRET_CONSOLE": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"OPENAI_API_KEY": {
|
"OPENAI_API_KEY": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"OPENCODE_API_KEY": {
|
"STRIPE_SECRET_KEY": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"STRIPE_WEBHOOK_SECRET": {
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"Web": {
|
"Web": {
|
||||||
"type": "sst.cloudflare.Astro"
|
"type": "sst.cloudflare.Astro"
|
||||||
"url": string
|
"url": string
|
||||||
|
@ -41,6 +77,8 @@ import * as cloudflare from "@cloudflare/workers-types";
|
||||||
declare module "sst" {
|
declare module "sst" {
|
||||||
export interface Resource {
|
export interface Resource {
|
||||||
"Api": cloudflare.Service
|
"Api": cloudflare.Service
|
||||||
|
"AuthApi": cloudflare.Service
|
||||||
|
"AuthStorage": cloudflare.KVNamespace
|
||||||
"Bucket": cloudflare.R2Bucket
|
"Bucket": cloudflare.R2Bucket
|
||||||
"GatewayApi": cloudflare.Service
|
"GatewayApi": cloudflare.Service
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
"@actions/core": "1.11.1",
|
"@actions/core": "1.11.1",
|
||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.15.1",
|
"@modelcontextprotocol/sdk": "1.15.1",
|
||||||
"@octokit/graphql": "9.0.1",
|
"@octokit/graphql": "9.0.1",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
|
|
45
sst-env.d.ts
vendored
45
sst-env.d.ts
vendored
|
@ -9,13 +9,44 @@ declare module "sst" {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"AUTH_API_URL": {
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"Api": {
|
"Api": {
|
||||||
"type": "sst.cloudflare.Worker"
|
"type": "sst.cloudflare.Worker"
|
||||||
"url": string
|
"url": string
|
||||||
}
|
}
|
||||||
|
"AuthApi": {
|
||||||
|
"type": "sst.cloudflare.Worker"
|
||||||
|
"url": string
|
||||||
|
}
|
||||||
|
"AuthStorage": {
|
||||||
|
"type": "sst.cloudflare.Kv"
|
||||||
|
}
|
||||||
"Bucket": {
|
"Bucket": {
|
||||||
"type": "sst.cloudflare.Bucket"
|
"type": "sst.cloudflare.Bucket"
|
||||||
}
|
}
|
||||||
|
"Console": {
|
||||||
|
"type": "sst.cloudflare.StaticSite"
|
||||||
|
"url": string
|
||||||
|
}
|
||||||
|
"DATABASE_PASSWORD": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"DATABASE_USERNAME": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"Database": {
|
||||||
|
"database": string
|
||||||
|
"host": string
|
||||||
|
"password": string
|
||||||
|
"port": number
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
"username": string
|
||||||
|
}
|
||||||
"GITHUB_APP_ID": {
|
"GITHUB_APP_ID": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
|
@ -24,6 +55,14 @@ declare module "sst" {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"GITHUB_CLIENT_ID_CONSOLE": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"GITHUB_CLIENT_SECRET_CONSOLE": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"GatewayApi": {
|
"GatewayApi": {
|
||||||
"type": "sst.cloudflare.Worker"
|
"type": "sst.cloudflare.Worker"
|
||||||
"url": string
|
"url": string
|
||||||
|
@ -32,10 +71,14 @@ declare module "sst" {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"OPENCODE_API_KEY": {
|
"STRIPE_SECRET_KEY": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"STRIPE_WEBHOOK_SECRET": {
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"Web": {
|
"Web": {
|
||||||
"type": "sst.cloudflare.Astro"
|
"type": "sst.cloudflare.Astro"
|
||||||
"url": string
|
"url": string
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue