the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add JWT auth and sandbox start/stop support

- Add lib/generateJwt and make JWT_SECRET/COOKIE_SECRET required in env
- Include Authorization Bearer JWT when calling sandbox
create/start/stop/delete
- DB schema: add sandboxes.repo and started_at, include migrations and
snapshots
- Update lexicons/types (status description -> "RUNNING", response ref
-> actor profile)
- Frontend: add start/stop/delete API calls, profile atom, UI/hooks
updates, add lodash dep

+1835 -97
+8 -3
apps/api/lexicons/sandbox/defs.json
··· 100 100 }, 101 101 "status": { 102 102 "type": "string", 103 - "description": "The current status of the sandbox, e.g. 'STARTED', 'STOPPED', etc." 103 + "description": "The current status of the sandbox, e.g. 'RUNNING', 'STOPPED', etc." 104 104 }, 105 105 "startedAt": { 106 106 "type": "string", ··· 111 111 "description": "The sandbox timeout in seconds" 112 112 }, 113 113 "baseSandbox": { 114 - "type": "ref", 115 - "ref": "io.pocketenv.sandbox.defs#sandboxViewBasic" 114 + "type": "string", 115 + "description": "The base sandbox that this sandbox was created from, if any. This can be used to determine the template or configuration used to create the sandbox." 116 116 }, 117 117 "website": { 118 118 "type": "string", ··· 171 171 "createdAt": { 172 172 "type": "string", 173 173 "format": "datetime" 174 + }, 175 + "owner": { 176 + "type": "ref", 177 + "description": "The user who created the sandbox", 178 + "ref": "io.pocketenv.user.defs#userViewBasic" 174 179 } 175 180 } 176 181 },
+1 -1
apps/api/lexicons/sandbox/getSandbox.json
··· 21 21 "encoding": "application/json", 22 22 "schema": { 23 23 "type": "ref", 24 - "ref": "io.pocketenv.sandbox.defs#sandboxViewDetailed" 24 + "ref": "io.pocketenv.actor.defs#profileViewDetailed" 25 25 } 26 26 } 27 27 }
+10 -4
apps/api/pkl/defs/sandbox/defs.pkl
··· 101 101 } 102 102 ["status"] = new StringType { 103 103 type = "string" 104 - description = "The current status of the sandbox, e.g. 'STARTED', 'STOPPED', etc." 104 + description = "The current status of the sandbox, e.g. 'RUNNING', 'STOPPED', etc." 105 105 } 106 106 ["startedAt"] = new StringType { 107 107 type = "string" ··· 111 111 type = "integer" 112 112 description = "The sandbox timeout in seconds" 113 113 } 114 - ["baseSandbox"] = new Ref { 115 - type = "ref" 116 - ref = "io.pocketenv.sandbox.defs#sandboxViewBasic" 114 + ["baseSandbox"] = new StringType { 115 + type = "string" 116 + description = 117 + "The base sandbox that this sandbox was created from, if any. This can be used to determine the template or configuration used to create the sandbox." 117 118 } 118 119 ["website"] = new StringType { 119 120 type = "string" ··· 172 173 ["createdAt"] = new StringType { 173 174 type = "string" 174 175 format = "datetime" 176 + } 177 + ["owner"] = new Ref { 178 + type = "ref" 179 + ref = "io.pocketenv.user.defs#userViewBasic" 180 + description = "The user who created the sandbox" 175 181 } 176 182 } 177 183 }
+1 -1
apps/api/pkl/defs/sandbox/getSandbox.pkl
··· 20 20 encoding = "application/json" 21 21 schema = new Ref { 22 22 type = "ref" 23 - ref = "io.pocketenv.sandbox.defs#sandboxViewDetailed" 23 + ref = "io.pocketenv.actor.defs#profileViewDetailed" 24 24 } 25 25 } 26 26 }
+10 -4
apps/api/src/lexicon/lexicons.ts
··· 351 351 status: { 352 352 type: "string", 353 353 description: 354 - "The current status of the sandbox, e.g. 'STARTED', 'STOPPED', etc.", 354 + "The current status of the sandbox, e.g. 'RUNNING', 'STOPPED', etc.", 355 355 }, 356 356 startedAt: { 357 357 type: "string", ··· 362 362 description: "The sandbox timeout in seconds", 363 363 }, 364 364 baseSandbox: { 365 - type: "ref", 366 - ref: "lex:io.pocketenv.sandbox.defs#sandboxViewBasic", 365 + type: "string", 366 + description: 367 + "The base sandbox that this sandbox was created from, if any. This can be used to determine the template or configuration used to create the sandbox.", 367 368 }, 368 369 website: { 369 370 type: "string", ··· 424 425 createdAt: { 425 426 type: "string", 426 427 format: "datetime", 428 + }, 429 + owner: { 430 + type: "ref", 431 + description: "The user who created the sandbox", 432 + ref: "lex:io.pocketenv.user.defs#userViewBasic", 427 433 }, 428 434 }, 429 435 }, ··· 525 531 encoding: "application/json", 526 532 schema: { 527 533 type: "ref", 528 - ref: "lex:io.pocketenv.sandbox.defs#sandboxViewDetailed", 534 + ref: "lex:io.pocketenv.actor.defs#profileViewDetailed", 529 535 }, 530 536 }, 531 537 },
+5 -2
apps/api/src/lexicon/types/io/pocketenv/sandbox/defs.ts
··· 5 5 import { lexicons } from "../../../../lexicons"; 6 6 import { isObj, hasProp } from "../../../../util"; 7 7 import { CID } from "multiformats/cid"; 8 + import type * as IoPocketenvUserDefs from "../user/defs"; 8 9 9 10 export interface SandboxViewBasic { 10 11 /** Name of the sandbox */ ··· 52 53 /** The provider of the sandbox, e.g. 'daytona', 'vercel', 'cloudflare', etc. */ 53 54 provider?: string; 54 55 description?: string; 55 - /** The current status of the sandbox, e.g. 'STARTED', 'STOPPED', etc. */ 56 + /** The current status of the sandbox, e.g. 'RUNNING', 'STOPPED', etc. */ 56 57 status?: string; 57 58 startedAt?: string; 58 59 /** The sandbox timeout in seconds */ 59 60 timeout?: number; 60 - baseSandbox?: SandboxViewBasic; 61 + /** The base sandbox that this sandbox was created from, if any. This can be used to determine the template or configuration used to create the sandbox. */ 62 + baseSandbox?: string; 61 63 /** Any URI related to the sandbox */ 62 64 website?: string; 63 65 /** URI to an image logo for the sandbox */ ··· 77 79 /** Number of times the sandbox has been installed by users. */ 78 80 installs?: number; 79 81 createdAt?: string; 82 + owner?: IoPocketenvUserDefs.UserViewBasic; 80 83 [k: string]: unknown; 81 84 } 82 85
+2 -2
apps/api/src/lexicon/types/io/pocketenv/sandbox/getSandbox.ts
··· 7 7 import { isObj, hasProp } from "../../../../util"; 8 8 import { CID } from "multiformats/cid"; 9 9 import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 - import type * as IoPocketenvSandboxDefs from "./defs"; 10 + import type * as IoPocketenvActorDefs from "../actor/defs"; 11 11 12 12 export interface QueryParams { 13 13 /** The sandbox ID or URI to retrieve */ ··· 15 15 } 16 16 17 17 export type InputSchema = undefined; 18 - export type OutputSchema = IoPocketenvSandboxDefs.SandboxViewDetailed; 18 + export type OutputSchema = IoPocketenvActorDefs.ProfileViewDetailed; 19 19 export type HandlerInput = undefined; 20 20 21 21 export interface HandlerSuccess {
+3 -3
apps/api/src/lib/env.ts
··· 14 14 PUBLIC_URL: str({ default: "http://localhost:8000" }), 15 15 DB_PATH: str({ devDefault: ":memory:" }), 16 16 KV_DB_PATH: str({ devDefault: ":memory:" }), 17 - COOKIE_SECRET: str({ devDefault: "00000000000000000000000000000000" }), 18 - FRONTEND_URL: str({ devDefault: "http://localhost:5173" }), 19 - JWT_SECRET: str({ devDefault: "00000000000000000000000000000000" }), 17 + COOKIE_SECRET: str({}), 18 + FRONTEND_URL: str({ devDefault: "http://localhost:5174" }), 19 + JWT_SECRET: str({}), 20 20 POSTGRES_URL: str({}), 21 21 REDIS_URL: str({ default: "redis://localhost:6379" }), 22 22 POCKETENV_DID: str({}),
+11
apps/api/src/lib/generateJwt.ts
··· 1 + import jwt from "@tsndr/cloudflare-worker-jwt"; 2 + import { env } from "./env"; 3 + 4 + export default function generateJwt(did: string) { 5 + return jwt.sign( 6 + { 7 + did, 8 + }, 9 + env.JWT_SECRET, 10 + ); 11 + }
+1 -6
apps/api/src/schema/sandbox-secrets.ts
··· 14 14 .notNull() 15 15 .references(() => secrets.id), 16 16 }, 17 - (table) => ({ 18 - uniqueSandboxSecret: uniqueIndex("unique_sandbox_secret").on( 19 - table.sandboxId, 20 - table.secretId, 21 - ), 22 - }), 17 + (t) => [uniqueIndex("unique_sandbox_secret").on(t.sandboxId, t.secretId)], 23 18 ); 24 19 25 20 export type SelectSandboxSecrets = InferSelectModel<typeof sandboxSecrets>;
+3 -6
apps/api/src/schema/sandbox-variables.ts
··· 14 14 .notNull() 15 15 .references(() => variables.id), 16 16 }, 17 - (table) => ({ 18 - uniqueSandboxSecret: uniqueIndex("unique_sandbox_variables").on( 19 - table.sandboxId, 20 - table.variableId, 21 - ), 22 - }), 17 + (t) => [ 18 + uniqueIndex("unique_sandbox_variables").on(t.sandboxId, t.variableId), 19 + ], 23 20 ); 24 21 25 22 export type SelectSandboxVariables = InferSelectModel<typeof sandboxVariables>;
+2
apps/api/src/schema/sandboxes.ts
··· 14 14 name: text("name").unique().notNull(), 15 15 displayName: text("display_name"), 16 16 uri: text("uri").unique(), 17 + repo: text("repo"), 17 18 provider: text("provider").default("cloudflare").notNull(), 18 19 description: text("description"), 19 20 logo: text("logo"), ··· 29 30 sleepAfter: text("sleep_after"), 30 31 sandbox_id: text("sandbox_id"), 31 32 installs: integer("installs").default(0).notNull(), 33 + startedAt: timestamp("started_at"), 32 34 createdAt: timestamp("created_at").defaultNow().notNull(), 33 35 updatedAt: timestamp("updated_at").defaultNow().notNull(), 34 36 });
+14 -4
apps/api/src/xrpc/io/pocketenv/sandbox/createSandbox.ts
··· 1 1 import type { HandlerAuth } from "@atproto/xrpc-server"; 2 - import { consola } from "consola"; 3 2 import type { Context } from "context"; 4 3 import type { Server } from "lexicon"; 5 4 import type { HandlerInput } from "lexicon/types/io/pocketenv/sandbox/createSandbox"; 5 + import generateJwt from "lib/generateJwt"; 6 6 7 7 export default function (server: Server, ctx: Context) { 8 8 const createSandbox = async (input: HandlerInput, auth: HandlerAuth) => { 9 - const res = await ctx.sandbox.post("/v1/sandboxes", { 10 - provider: "daytona", 11 - }); 9 + const res = await ctx.sandbox.post( 10 + "/v1/sandboxes", 11 + { 12 + provider: "daytona", 13 + }, 14 + { 15 + ...(auth?.credentials && { 16 + headers: { 17 + Authorization: `Bearer ${generateJwt(auth.credentials.did)}`, 18 + }, 19 + }), 20 + }, 21 + ); 12 22 return { 13 23 id: res.data.id, 14 24 name: input.body.name || "Unnamed Sandbox",
+8 -1
apps/api/src/xrpc/io/pocketenv/sandbox/deleteSandbox.ts
··· 2 2 import type { Context } from "context"; 3 3 import type { Server } from "lexicon"; 4 4 import type { QueryParams } from "lexicon/types/io/pocketenv/sandbox/deleteSandbox"; 5 + import generateJwt from "lib/generateJwt"; 5 6 6 7 export default function (server: Server, ctx: Context) { 7 8 const deleteSandbox = async (params: QueryParams, auth: HandlerAuth) => { 8 - await ctx.sandbox.delete(`/v1/sandboxes/${params.id}`); 9 + await ctx.sandbox.delete(`/v1/sandboxes/${params.id}`, { 10 + ...(auth?.credentials && { 11 + headers: { 12 + Authorization: `Bearer ${generateJwt(auth.credentials.did)}`, 13 + }, 14 + }), 15 + }); 9 16 return {}; 10 17 }; 11 18 server.io.pocketenv.sandbox.deleteSandbox({
+31 -12
apps/api/src/xrpc/io/pocketenv/sandbox/getSandbox.ts
··· 10 10 import schema from "schema"; 11 11 import { eq, or } from "drizzle-orm"; 12 12 import { consola } from "consola"; 13 + import type { SelectUser } from "schema/users"; 13 14 14 15 export default function (server: Server, ctx: Context) { 15 16 const getSandbox = (params: QueryParams, auth: HandlerAuth) => ··· 42 43 }: { 43 44 params: QueryParams; 44 45 ctx: Context; 45 - }): Effect.Effect<SelectSandbox | undefined, Error> => { 46 + }): Effect.Effect< 47 + { sandboxes: SelectSandbox; users: SelectUser | null } | undefined, 48 + Error 49 + > => { 46 50 return Effect.tryPromise({ 47 51 try: async () => 48 52 ctx.db 49 53 .select() 50 54 .from(schema.sandboxes) 55 + .leftJoin(schema.users, eq(schema.sandboxes.userId, schema.users.id)) 51 56 .where( 52 57 or( 53 58 eq(schema.sandboxes.id, params.id), ··· 64 69 }; 65 70 66 71 const presentation = ( 67 - sandbox: SelectSandbox | undefined, 72 + data: { sandboxes: SelectSandbox; users: SelectUser | null } | undefined, 68 73 ): Effect.Effect<OutputSchema, never> => { 69 74 return Effect.sync(() => ({ 70 - sandbox: sandbox && { 71 - id: sandbox.id, 72 - name: sandbox.name, 73 - displayName: sandbox.displayName, 74 - description: sandbox.description!, 75 - logo: sandbox.logo!, 76 - readme: sandbox.readme!, 77 - installs: sandbox.installs, 78 - uri: sandbox.uri, 79 - createdAt: sandbox.createdAt.toISOString(), 75 + sandbox: data?.sandboxes && { 76 + id: data.sandboxes.id, 77 + name: data.sandboxes.name, 78 + displayName: data.sandboxes.displayName, 79 + description: data.sandboxes.description, 80 + baseSandbox: data.sandboxes.base, 81 + status: data.sandboxes.status, 82 + repo: data.sandboxes.repo, 83 + logo: data.sandboxes.logo, 84 + readme: data.sandboxes.readme, 85 + installs: data.sandboxes.installs, 86 + uri: data.sandboxes.uri, 87 + vcpus: data.sandboxes.vcpus, 88 + memory: data.sandboxes.memory, 89 + disk: data.sandboxes.disk, 90 + createdAt: data.sandboxes.createdAt.toISOString(), 91 + startedAt: data.sandboxes.startedAt?.toISOString(), 92 + ...(data.users && { 93 + owner: { 94 + ...data.users, 95 + createdAt: data.users.createdAt.toISOString(), 96 + updatedAt: data.users.updatedAt.toISOString(), 97 + }, 98 + }), 80 99 }, 81 100 })); 82 101 };
+8 -1
apps/api/src/xrpc/io/pocketenv/sandbox/startSandbox.ts
··· 2 2 import type { Context } from "context"; 3 3 import type { Server } from "lexicon"; 4 4 import type { QueryParams } from "lexicon/types/io/pocketenv/sandbox/startSandbox"; 5 + import generateJwt from "lib/generateJwt"; 5 6 6 7 export default function (server: Server, ctx: Context) { 7 8 const startSandbox = async (params: QueryParams, auth: HandlerAuth) => { 8 - await ctx.sandbox.post(`/v1/sandboxes/${params.id}/start`); 9 + await ctx.sandbox.post(`/v1/sandboxes/${params.id}/start`, undefined, { 10 + ...(auth?.credentials && { 11 + headers: { 12 + Authorization: `Bearer ${generateJwt(auth.credentials.did)}`, 13 + }, 14 + }), 15 + }); 9 16 return {}; 10 17 }; 11 18 server.io.pocketenv.sandbox.startSandbox({
+8 -1
apps/api/src/xrpc/io/pocketenv/sandbox/stopSandbox.ts
··· 2 2 import type { Context } from "context"; 3 3 import type { Server } from "lexicon"; 4 4 import type { QueryParams } from "lexicon/types/io/pocketenv/sandbox/stopSandbox"; 5 + import generateJwt from "lib/generateJwt"; 5 6 6 7 export default function (server: Server, ctx: Context) { 7 8 const stopSandbox = async (params: QueryParams, auth: HandlerAuth) => { 8 - await ctx.sandbox.post(`/v1/sandboxes/${params.id}/stop`); 9 + await ctx.sandbox.post(`/v1/sandboxes/${params.id}/stop`, undefined, { 10 + ...(auth?.credentials && { 11 + headers: { 12 + Authorization: `Bearer ${generateJwt(auth.credentials.did)}`, 13 + }, 14 + }), 15 + }); 9 16 return {}; 10 17 }; 11 18 server.io.pocketenv.sandbox.stopSandbox({
+2
apps/cf-sandbox/drizzle/0010_motionless_shriek.sql
··· 1 + ALTER TABLE "sandboxes" ALTER COLUMN "id" SET DEFAULT sandbox_id();--> statement-breakpoint 2 + ALTER TABLE "sandboxes" ADD COLUMN "repo" text;
+1
apps/cf-sandbox/drizzle/0011_nappy_magdalene.sql
··· 1 + ALTER TABLE "sandboxes" ADD COLUMN "started_at" timestamp;
+738
apps/cf-sandbox/drizzle/meta/0010_snapshot.json
··· 1 + { 2 + "id": "331b62c8-60f5-4b34-89e8-363f7c38f9ca", 3 + "prevId": "d1c82d20-d1e2-4d46-8662-108d46349bb7", 4 + "version": "7", 5 + "dialect": "postgresql", 6 + "tables": { 7 + "public.authorized_keys": { 8 + "name": "authorized_keys", 9 + "schema": "", 10 + "columns": { 11 + "id": { 12 + "name": "id", 13 + "type": "text", 14 + "primaryKey": true, 15 + "notNull": true, 16 + "default": "xata_id()" 17 + }, 18 + "sandbox_id": { 19 + "name": "sandbox_id", 20 + "type": "text", 21 + "primaryKey": false, 22 + "notNull": false 23 + }, 24 + "public_key": { 25 + "name": "public_key", 26 + "type": "text", 27 + "primaryKey": false, 28 + "notNull": true 29 + }, 30 + "created_at": { 31 + "name": "created_at", 32 + "type": "timestamp", 33 + "primaryKey": false, 34 + "notNull": true, 35 + "default": "now()" 36 + } 37 + }, 38 + "indexes": {}, 39 + "foreignKeys": { 40 + "authorized_keys_sandbox_id_sandboxes_id_fk": { 41 + "name": "authorized_keys_sandbox_id_sandboxes_id_fk", 42 + "tableFrom": "authorized_keys", 43 + "tableTo": "sandboxes", 44 + "columnsFrom": [ 45 + "sandbox_id" 46 + ], 47 + "columnsTo": [ 48 + "id" 49 + ], 50 + "onDelete": "no action", 51 + "onUpdate": "no action" 52 + } 53 + }, 54 + "compositePrimaryKeys": {}, 55 + "uniqueConstraints": {}, 56 + "policies": {}, 57 + "checkConstraints": {}, 58 + "isRLSEnabled": false 59 + }, 60 + "public.sandbox_secrets": { 61 + "name": "sandbox_secrets", 62 + "schema": "", 63 + "columns": { 64 + "id": { 65 + "name": "id", 66 + "type": "text", 67 + "primaryKey": true, 68 + "notNull": true, 69 + "default": "xata_id()" 70 + }, 71 + "sandbox_id": { 72 + "name": "sandbox_id", 73 + "type": "text", 74 + "primaryKey": false, 75 + "notNull": true 76 + }, 77 + "secret_id": { 78 + "name": "secret_id", 79 + "type": "text", 80 + "primaryKey": false, 81 + "notNull": true 82 + } 83 + }, 84 + "indexes": { 85 + "unique_sandbox_secret": { 86 + "name": "unique_sandbox_secret", 87 + "columns": [ 88 + { 89 + "expression": "sandbox_id", 90 + "isExpression": false, 91 + "asc": true, 92 + "nulls": "last" 93 + }, 94 + { 95 + "expression": "secret_id", 96 + "isExpression": false, 97 + "asc": true, 98 + "nulls": "last" 99 + } 100 + ], 101 + "isUnique": true, 102 + "concurrently": false, 103 + "method": "btree", 104 + "with": {} 105 + } 106 + }, 107 + "foreignKeys": { 108 + "sandbox_secrets_sandbox_id_sandboxes_id_fk": { 109 + "name": "sandbox_secrets_sandbox_id_sandboxes_id_fk", 110 + "tableFrom": "sandbox_secrets", 111 + "tableTo": "sandboxes", 112 + "columnsFrom": [ 113 + "sandbox_id" 114 + ], 115 + "columnsTo": [ 116 + "id" 117 + ], 118 + "onDelete": "no action", 119 + "onUpdate": "no action" 120 + }, 121 + "sandbox_secrets_secret_id_secrets_id_fk": { 122 + "name": "sandbox_secrets_secret_id_secrets_id_fk", 123 + "tableFrom": "sandbox_secrets", 124 + "tableTo": "secrets", 125 + "columnsFrom": [ 126 + "secret_id" 127 + ], 128 + "columnsTo": [ 129 + "id" 130 + ], 131 + "onDelete": "no action", 132 + "onUpdate": "no action" 133 + } 134 + }, 135 + "compositePrimaryKeys": {}, 136 + "uniqueConstraints": {}, 137 + "policies": {}, 138 + "checkConstraints": {}, 139 + "isRLSEnabled": false 140 + }, 141 + "public.sandbox_variables": { 142 + "name": "sandbox_variables", 143 + "schema": "", 144 + "columns": { 145 + "id": { 146 + "name": "id", 147 + "type": "text", 148 + "primaryKey": true, 149 + "notNull": true, 150 + "default": "xata_id()" 151 + }, 152 + "sandbox_id": { 153 + "name": "sandbox_id", 154 + "type": "text", 155 + "primaryKey": false, 156 + "notNull": true 157 + }, 158 + "variable_id": { 159 + "name": "variable_id", 160 + "type": "text", 161 + "primaryKey": false, 162 + "notNull": true 163 + } 164 + }, 165 + "indexes": { 166 + "unique_sandbox_variables": { 167 + "name": "unique_sandbox_variables", 168 + "columns": [ 169 + { 170 + "expression": "sandbox_id", 171 + "isExpression": false, 172 + "asc": true, 173 + "nulls": "last" 174 + }, 175 + { 176 + "expression": "variable_id", 177 + "isExpression": false, 178 + "asc": true, 179 + "nulls": "last" 180 + } 181 + ], 182 + "isUnique": true, 183 + "concurrently": false, 184 + "method": "btree", 185 + "with": {} 186 + } 187 + }, 188 + "foreignKeys": { 189 + "sandbox_variables_sandbox_id_sandboxes_id_fk": { 190 + "name": "sandbox_variables_sandbox_id_sandboxes_id_fk", 191 + "tableFrom": "sandbox_variables", 192 + "tableTo": "sandboxes", 193 + "columnsFrom": [ 194 + "sandbox_id" 195 + ], 196 + "columnsTo": [ 197 + "id" 198 + ], 199 + "onDelete": "no action", 200 + "onUpdate": "no action" 201 + }, 202 + "sandbox_variables_variable_id_variables_id_fk": { 203 + "name": "sandbox_variables_variable_id_variables_id_fk", 204 + "tableFrom": "sandbox_variables", 205 + "tableTo": "variables", 206 + "columnsFrom": [ 207 + "variable_id" 208 + ], 209 + "columnsTo": [ 210 + "id" 211 + ], 212 + "onDelete": "no action", 213 + "onUpdate": "no action" 214 + } 215 + }, 216 + "compositePrimaryKeys": {}, 217 + "uniqueConstraints": {}, 218 + "policies": {}, 219 + "checkConstraints": {}, 220 + "isRLSEnabled": false 221 + }, 222 + "public.sandbox_volumes": { 223 + "name": "sandbox_volumes", 224 + "schema": "", 225 + "columns": { 226 + "id": { 227 + "name": "id", 228 + "type": "text", 229 + "primaryKey": true, 230 + "notNull": true, 231 + "default": "xata_id()" 232 + }, 233 + "sandbox_id": { 234 + "name": "sandbox_id", 235 + "type": "text", 236 + "primaryKey": false, 237 + "notNull": true 238 + }, 239 + "volume_id": { 240 + "name": "volume_id", 241 + "type": "text", 242 + "primaryKey": false, 243 + "notNull": true 244 + } 245 + }, 246 + "indexes": {}, 247 + "foreignKeys": { 248 + "sandbox_volumes_sandbox_id_sandboxes_id_fk": { 249 + "name": "sandbox_volumes_sandbox_id_sandboxes_id_fk", 250 + "tableFrom": "sandbox_volumes", 251 + "tableTo": "sandboxes", 252 + "columnsFrom": [ 253 + "sandbox_id" 254 + ], 255 + "columnsTo": [ 256 + "id" 257 + ], 258 + "onDelete": "no action", 259 + "onUpdate": "no action" 260 + }, 261 + "sandbox_volumes_volume_id_volumes_id_fk": { 262 + "name": "sandbox_volumes_volume_id_volumes_id_fk", 263 + "tableFrom": "sandbox_volumes", 264 + "tableTo": "volumes", 265 + "columnsFrom": [ 266 + "volume_id" 267 + ], 268 + "columnsTo": [ 269 + "id" 270 + ], 271 + "onDelete": "no action", 272 + "onUpdate": "no action" 273 + } 274 + }, 275 + "compositePrimaryKeys": {}, 276 + "uniqueConstraints": {}, 277 + "policies": {}, 278 + "checkConstraints": {}, 279 + "isRLSEnabled": false 280 + }, 281 + "public.sandboxes": { 282 + "name": "sandboxes", 283 + "schema": "", 284 + "columns": { 285 + "id": { 286 + "name": "id", 287 + "type": "text", 288 + "primaryKey": true, 289 + "notNull": true, 290 + "default": "sandbox_id()" 291 + }, 292 + "base": { 293 + "name": "base", 294 + "type": "text", 295 + "primaryKey": false, 296 + "notNull": false 297 + }, 298 + "name": { 299 + "name": "name", 300 + "type": "text", 301 + "primaryKey": false, 302 + "notNull": true 303 + }, 304 + "display_name": { 305 + "name": "display_name", 306 + "type": "text", 307 + "primaryKey": false, 308 + "notNull": false 309 + }, 310 + "uri": { 311 + "name": "uri", 312 + "type": "text", 313 + "primaryKey": false, 314 + "notNull": false 315 + }, 316 + "repo": { 317 + "name": "repo", 318 + "type": "text", 319 + "primaryKey": false, 320 + "notNull": false 321 + }, 322 + "provider": { 323 + "name": "provider", 324 + "type": "text", 325 + "primaryKey": false, 326 + "notNull": true, 327 + "default": "'cloudflare'" 328 + }, 329 + "description": { 330 + "name": "description", 331 + "type": "text", 332 + "primaryKey": false, 333 + "notNull": false 334 + }, 335 + "logo": { 336 + "name": "logo", 337 + "type": "text", 338 + "primaryKey": false, 339 + "notNull": false 340 + }, 341 + "readme": { 342 + "name": "readme", 343 + "type": "text", 344 + "primaryKey": false, 345 + "notNull": false 346 + }, 347 + "public_key": { 348 + "name": "public_key", 349 + "type": "text", 350 + "primaryKey": false, 351 + "notNull": true 352 + }, 353 + "user_id": { 354 + "name": "user_id", 355 + "type": "text", 356 + "primaryKey": false, 357 + "notNull": false 358 + }, 359 + "instance_type": { 360 + "name": "instance_type", 361 + "type": "text", 362 + "primaryKey": false, 363 + "notNull": false 364 + }, 365 + "vcpus": { 366 + "name": "vcpus", 367 + "type": "integer", 368 + "primaryKey": false, 369 + "notNull": false 370 + }, 371 + "memory": { 372 + "name": "memory", 373 + "type": "integer", 374 + "primaryKey": false, 375 + "notNull": false 376 + }, 377 + "disk": { 378 + "name": "disk", 379 + "type": "integer", 380 + "primaryKey": false, 381 + "notNull": false 382 + }, 383 + "status": { 384 + "name": "status", 385 + "type": "text", 386 + "primaryKey": false, 387 + "notNull": true 388 + }, 389 + "keep_alive": { 390 + "name": "keep_alive", 391 + "type": "boolean", 392 + "primaryKey": false, 393 + "notNull": true, 394 + "default": false 395 + }, 396 + "sleep_after": { 397 + "name": "sleep_after", 398 + "type": "text", 399 + "primaryKey": false, 400 + "notNull": false 401 + }, 402 + "sandbox_id": { 403 + "name": "sandbox_id", 404 + "type": "text", 405 + "primaryKey": false, 406 + "notNull": false 407 + }, 408 + "installs": { 409 + "name": "installs", 410 + "type": "integer", 411 + "primaryKey": false, 412 + "notNull": true, 413 + "default": 0 414 + }, 415 + "created_at": { 416 + "name": "created_at", 417 + "type": "timestamp", 418 + "primaryKey": false, 419 + "notNull": true, 420 + "default": "now()" 421 + }, 422 + "updated_at": { 423 + "name": "updated_at", 424 + "type": "timestamp", 425 + "primaryKey": false, 426 + "notNull": true, 427 + "default": "now()" 428 + } 429 + }, 430 + "indexes": {}, 431 + "foreignKeys": { 432 + "sandboxes_user_id_users_id_fk": { 433 + "name": "sandboxes_user_id_users_id_fk", 434 + "tableFrom": "sandboxes", 435 + "tableTo": "users", 436 + "columnsFrom": [ 437 + "user_id" 438 + ], 439 + "columnsTo": [ 440 + "id" 441 + ], 442 + "onDelete": "no action", 443 + "onUpdate": "no action" 444 + } 445 + }, 446 + "compositePrimaryKeys": {}, 447 + "uniqueConstraints": { 448 + "sandboxes_name_unique": { 449 + "name": "sandboxes_name_unique", 450 + "nullsNotDistinct": false, 451 + "columns": [ 452 + "name" 453 + ] 454 + }, 455 + "sandboxes_uri_unique": { 456 + "name": "sandboxes_uri_unique", 457 + "nullsNotDistinct": false, 458 + "columns": [ 459 + "uri" 460 + ] 461 + } 462 + }, 463 + "policies": {}, 464 + "checkConstraints": {}, 465 + "isRLSEnabled": false 466 + }, 467 + "public.secrets": { 468 + "name": "secrets", 469 + "schema": "", 470 + "columns": { 471 + "id": { 472 + "name": "id", 473 + "type": "text", 474 + "primaryKey": true, 475 + "notNull": true, 476 + "default": "secret_id()" 477 + }, 478 + "name": { 479 + "name": "name", 480 + "type": "text", 481 + "primaryKey": false, 482 + "notNull": true 483 + }, 484 + "value": { 485 + "name": "value", 486 + "type": "text", 487 + "primaryKey": false, 488 + "notNull": true 489 + }, 490 + "created_at": { 491 + "name": "created_at", 492 + "type": "timestamp", 493 + "primaryKey": false, 494 + "notNull": true, 495 + "default": "now()" 496 + } 497 + }, 498 + "indexes": {}, 499 + "foreignKeys": {}, 500 + "compositePrimaryKeys": {}, 501 + "uniqueConstraints": {}, 502 + "policies": {}, 503 + "checkConstraints": {}, 504 + "isRLSEnabled": false 505 + }, 506 + "public.snapshots": { 507 + "name": "snapshots", 508 + "schema": "", 509 + "columns": { 510 + "id": { 511 + "name": "id", 512 + "type": "text", 513 + "primaryKey": true, 514 + "notNull": true, 515 + "default": "snapshot_id()" 516 + }, 517 + "slug": { 518 + "name": "slug", 519 + "type": "text", 520 + "primaryKey": false, 521 + "notNull": true 522 + }, 523 + "created_at": { 524 + "name": "created_at", 525 + "type": "timestamp", 526 + "primaryKey": false, 527 + "notNull": true, 528 + "default": "now()" 529 + } 530 + }, 531 + "indexes": {}, 532 + "foreignKeys": {}, 533 + "compositePrimaryKeys": {}, 534 + "uniqueConstraints": { 535 + "snapshots_slug_unique": { 536 + "name": "snapshots_slug_unique", 537 + "nullsNotDistinct": false, 538 + "columns": [ 539 + "slug" 540 + ] 541 + } 542 + }, 543 + "policies": {}, 544 + "checkConstraints": {}, 545 + "isRLSEnabled": false 546 + }, 547 + "public.users": { 548 + "name": "users", 549 + "schema": "", 550 + "columns": { 551 + "id": { 552 + "name": "id", 553 + "type": "text", 554 + "primaryKey": true, 555 + "notNull": true, 556 + "default": "xata_id()" 557 + }, 558 + "did": { 559 + "name": "did", 560 + "type": "text", 561 + "primaryKey": false, 562 + "notNull": true 563 + }, 564 + "display_name": { 565 + "name": "display_name", 566 + "type": "text", 567 + "primaryKey": false, 568 + "notNull": false 569 + }, 570 + "handle": { 571 + "name": "handle", 572 + "type": "text", 573 + "primaryKey": false, 574 + "notNull": true 575 + }, 576 + "avatar": { 577 + "name": "avatar", 578 + "type": "text", 579 + "primaryKey": false, 580 + "notNull": false 581 + }, 582 + "created_at": { 583 + "name": "created_at", 584 + "type": "timestamp", 585 + "primaryKey": false, 586 + "notNull": true, 587 + "default": "now()" 588 + }, 589 + "updated_at": { 590 + "name": "updated_at", 591 + "type": "timestamp", 592 + "primaryKey": false, 593 + "notNull": true, 594 + "default": "now()" 595 + } 596 + }, 597 + "indexes": {}, 598 + "foreignKeys": {}, 599 + "compositePrimaryKeys": {}, 600 + "uniqueConstraints": { 601 + "users_did_unique": { 602 + "name": "users_did_unique", 603 + "nullsNotDistinct": false, 604 + "columns": [ 605 + "did" 606 + ] 607 + }, 608 + "users_handle_unique": { 609 + "name": "users_handle_unique", 610 + "nullsNotDistinct": false, 611 + "columns": [ 612 + "handle" 613 + ] 614 + } 615 + }, 616 + "policies": {}, 617 + "checkConstraints": {}, 618 + "isRLSEnabled": false 619 + }, 620 + "public.variables": { 621 + "name": "variables", 622 + "schema": "", 623 + "columns": { 624 + "id": { 625 + "name": "id", 626 + "type": "text", 627 + "primaryKey": true, 628 + "notNull": true, 629 + "default": "variable_id()" 630 + }, 631 + "name": { 632 + "name": "name", 633 + "type": "text", 634 + "primaryKey": false, 635 + "notNull": true 636 + }, 637 + "value": { 638 + "name": "value", 639 + "type": "text", 640 + "primaryKey": false, 641 + "notNull": true 642 + }, 643 + "created_at": { 644 + "name": "created_at", 645 + "type": "timestamp", 646 + "primaryKey": false, 647 + "notNull": true, 648 + "default": "now()" 649 + }, 650 + "updated_at": { 651 + "name": "updated_at", 652 + "type": "timestamp", 653 + "primaryKey": false, 654 + "notNull": true, 655 + "default": "now()" 656 + } 657 + }, 658 + "indexes": {}, 659 + "foreignKeys": {}, 660 + "compositePrimaryKeys": {}, 661 + "uniqueConstraints": {}, 662 + "policies": {}, 663 + "checkConstraints": {}, 664 + "isRLSEnabled": false 665 + }, 666 + "public.volumes": { 667 + "name": "volumes", 668 + "schema": "", 669 + "columns": { 670 + "id": { 671 + "name": "id", 672 + "type": "text", 673 + "primaryKey": true, 674 + "notNull": true, 675 + "default": "volume_id()" 676 + }, 677 + "slug": { 678 + "name": "slug", 679 + "type": "text", 680 + "primaryKey": false, 681 + "notNull": true 682 + }, 683 + "size": { 684 + "name": "size", 685 + "type": "integer", 686 + "primaryKey": false, 687 + "notNull": true 688 + }, 689 + "size_unit": { 690 + "name": "size_unit", 691 + "type": "text", 692 + "primaryKey": false, 693 + "notNull": true 694 + }, 695 + "created_at": { 696 + "name": "created_at", 697 + "type": "timestamp", 698 + "primaryKey": false, 699 + "notNull": true, 700 + "default": "now()" 701 + }, 702 + "updated_at": { 703 + "name": "updated_at", 704 + "type": "timestamp", 705 + "primaryKey": false, 706 + "notNull": true, 707 + "default": "now()" 708 + } 709 + }, 710 + "indexes": {}, 711 + "foreignKeys": {}, 712 + "compositePrimaryKeys": {}, 713 + "uniqueConstraints": { 714 + "volumes_slug_unique": { 715 + "name": "volumes_slug_unique", 716 + "nullsNotDistinct": false, 717 + "columns": [ 718 + "slug" 719 + ] 720 + } 721 + }, 722 + "policies": {}, 723 + "checkConstraints": {}, 724 + "isRLSEnabled": false 725 + } 726 + }, 727 + "enums": {}, 728 + "schemas": {}, 729 + "sequences": {}, 730 + "roles": {}, 731 + "policies": {}, 732 + "views": {}, 733 + "_meta": { 734 + "columns": {}, 735 + "schemas": {}, 736 + "tables": {} 737 + } 738 + }
+744
apps/cf-sandbox/drizzle/meta/0011_snapshot.json
··· 1 + { 2 + "id": "91e8a6f6-d86c-49d0-b476-e8dd8abb9d1e", 3 + "prevId": "331b62c8-60f5-4b34-89e8-363f7c38f9ca", 4 + "version": "7", 5 + "dialect": "postgresql", 6 + "tables": { 7 + "public.authorized_keys": { 8 + "name": "authorized_keys", 9 + "schema": "", 10 + "columns": { 11 + "id": { 12 + "name": "id", 13 + "type": "text", 14 + "primaryKey": true, 15 + "notNull": true, 16 + "default": "xata_id()" 17 + }, 18 + "sandbox_id": { 19 + "name": "sandbox_id", 20 + "type": "text", 21 + "primaryKey": false, 22 + "notNull": false 23 + }, 24 + "public_key": { 25 + "name": "public_key", 26 + "type": "text", 27 + "primaryKey": false, 28 + "notNull": true 29 + }, 30 + "created_at": { 31 + "name": "created_at", 32 + "type": "timestamp", 33 + "primaryKey": false, 34 + "notNull": true, 35 + "default": "now()" 36 + } 37 + }, 38 + "indexes": {}, 39 + "foreignKeys": { 40 + "authorized_keys_sandbox_id_sandboxes_id_fk": { 41 + "name": "authorized_keys_sandbox_id_sandboxes_id_fk", 42 + "tableFrom": "authorized_keys", 43 + "tableTo": "sandboxes", 44 + "columnsFrom": [ 45 + "sandbox_id" 46 + ], 47 + "columnsTo": [ 48 + "id" 49 + ], 50 + "onDelete": "no action", 51 + "onUpdate": "no action" 52 + } 53 + }, 54 + "compositePrimaryKeys": {}, 55 + "uniqueConstraints": {}, 56 + "policies": {}, 57 + "checkConstraints": {}, 58 + "isRLSEnabled": false 59 + }, 60 + "public.sandbox_secrets": { 61 + "name": "sandbox_secrets", 62 + "schema": "", 63 + "columns": { 64 + "id": { 65 + "name": "id", 66 + "type": "text", 67 + "primaryKey": true, 68 + "notNull": true, 69 + "default": "xata_id()" 70 + }, 71 + "sandbox_id": { 72 + "name": "sandbox_id", 73 + "type": "text", 74 + "primaryKey": false, 75 + "notNull": true 76 + }, 77 + "secret_id": { 78 + "name": "secret_id", 79 + "type": "text", 80 + "primaryKey": false, 81 + "notNull": true 82 + } 83 + }, 84 + "indexes": { 85 + "unique_sandbox_secret": { 86 + "name": "unique_sandbox_secret", 87 + "columns": [ 88 + { 89 + "expression": "sandbox_id", 90 + "isExpression": false, 91 + "asc": true, 92 + "nulls": "last" 93 + }, 94 + { 95 + "expression": "secret_id", 96 + "isExpression": false, 97 + "asc": true, 98 + "nulls": "last" 99 + } 100 + ], 101 + "isUnique": true, 102 + "concurrently": false, 103 + "method": "btree", 104 + "with": {} 105 + } 106 + }, 107 + "foreignKeys": { 108 + "sandbox_secrets_sandbox_id_sandboxes_id_fk": { 109 + "name": "sandbox_secrets_sandbox_id_sandboxes_id_fk", 110 + "tableFrom": "sandbox_secrets", 111 + "tableTo": "sandboxes", 112 + "columnsFrom": [ 113 + "sandbox_id" 114 + ], 115 + "columnsTo": [ 116 + "id" 117 + ], 118 + "onDelete": "no action", 119 + "onUpdate": "no action" 120 + }, 121 + "sandbox_secrets_secret_id_secrets_id_fk": { 122 + "name": "sandbox_secrets_secret_id_secrets_id_fk", 123 + "tableFrom": "sandbox_secrets", 124 + "tableTo": "secrets", 125 + "columnsFrom": [ 126 + "secret_id" 127 + ], 128 + "columnsTo": [ 129 + "id" 130 + ], 131 + "onDelete": "no action", 132 + "onUpdate": "no action" 133 + } 134 + }, 135 + "compositePrimaryKeys": {}, 136 + "uniqueConstraints": {}, 137 + "policies": {}, 138 + "checkConstraints": {}, 139 + "isRLSEnabled": false 140 + }, 141 + "public.sandbox_variables": { 142 + "name": "sandbox_variables", 143 + "schema": "", 144 + "columns": { 145 + "id": { 146 + "name": "id", 147 + "type": "text", 148 + "primaryKey": true, 149 + "notNull": true, 150 + "default": "xata_id()" 151 + }, 152 + "sandbox_id": { 153 + "name": "sandbox_id", 154 + "type": "text", 155 + "primaryKey": false, 156 + "notNull": true 157 + }, 158 + "variable_id": { 159 + "name": "variable_id", 160 + "type": "text", 161 + "primaryKey": false, 162 + "notNull": true 163 + } 164 + }, 165 + "indexes": { 166 + "unique_sandbox_variables": { 167 + "name": "unique_sandbox_variables", 168 + "columns": [ 169 + { 170 + "expression": "sandbox_id", 171 + "isExpression": false, 172 + "asc": true, 173 + "nulls": "last" 174 + }, 175 + { 176 + "expression": "variable_id", 177 + "isExpression": false, 178 + "asc": true, 179 + "nulls": "last" 180 + } 181 + ], 182 + "isUnique": true, 183 + "concurrently": false, 184 + "method": "btree", 185 + "with": {} 186 + } 187 + }, 188 + "foreignKeys": { 189 + "sandbox_variables_sandbox_id_sandboxes_id_fk": { 190 + "name": "sandbox_variables_sandbox_id_sandboxes_id_fk", 191 + "tableFrom": "sandbox_variables", 192 + "tableTo": "sandboxes", 193 + "columnsFrom": [ 194 + "sandbox_id" 195 + ], 196 + "columnsTo": [ 197 + "id" 198 + ], 199 + "onDelete": "no action", 200 + "onUpdate": "no action" 201 + }, 202 + "sandbox_variables_variable_id_variables_id_fk": { 203 + "name": "sandbox_variables_variable_id_variables_id_fk", 204 + "tableFrom": "sandbox_variables", 205 + "tableTo": "variables", 206 + "columnsFrom": [ 207 + "variable_id" 208 + ], 209 + "columnsTo": [ 210 + "id" 211 + ], 212 + "onDelete": "no action", 213 + "onUpdate": "no action" 214 + } 215 + }, 216 + "compositePrimaryKeys": {}, 217 + "uniqueConstraints": {}, 218 + "policies": {}, 219 + "checkConstraints": {}, 220 + "isRLSEnabled": false 221 + }, 222 + "public.sandbox_volumes": { 223 + "name": "sandbox_volumes", 224 + "schema": "", 225 + "columns": { 226 + "id": { 227 + "name": "id", 228 + "type": "text", 229 + "primaryKey": true, 230 + "notNull": true, 231 + "default": "xata_id()" 232 + }, 233 + "sandbox_id": { 234 + "name": "sandbox_id", 235 + "type": "text", 236 + "primaryKey": false, 237 + "notNull": true 238 + }, 239 + "volume_id": { 240 + "name": "volume_id", 241 + "type": "text", 242 + "primaryKey": false, 243 + "notNull": true 244 + } 245 + }, 246 + "indexes": {}, 247 + "foreignKeys": { 248 + "sandbox_volumes_sandbox_id_sandboxes_id_fk": { 249 + "name": "sandbox_volumes_sandbox_id_sandboxes_id_fk", 250 + "tableFrom": "sandbox_volumes", 251 + "tableTo": "sandboxes", 252 + "columnsFrom": [ 253 + "sandbox_id" 254 + ], 255 + "columnsTo": [ 256 + "id" 257 + ], 258 + "onDelete": "no action", 259 + "onUpdate": "no action" 260 + }, 261 + "sandbox_volumes_volume_id_volumes_id_fk": { 262 + "name": "sandbox_volumes_volume_id_volumes_id_fk", 263 + "tableFrom": "sandbox_volumes", 264 + "tableTo": "volumes", 265 + "columnsFrom": [ 266 + "volume_id" 267 + ], 268 + "columnsTo": [ 269 + "id" 270 + ], 271 + "onDelete": "no action", 272 + "onUpdate": "no action" 273 + } 274 + }, 275 + "compositePrimaryKeys": {}, 276 + "uniqueConstraints": {}, 277 + "policies": {}, 278 + "checkConstraints": {}, 279 + "isRLSEnabled": false 280 + }, 281 + "public.sandboxes": { 282 + "name": "sandboxes", 283 + "schema": "", 284 + "columns": { 285 + "id": { 286 + "name": "id", 287 + "type": "text", 288 + "primaryKey": true, 289 + "notNull": true, 290 + "default": "sandbox_id()" 291 + }, 292 + "base": { 293 + "name": "base", 294 + "type": "text", 295 + "primaryKey": false, 296 + "notNull": false 297 + }, 298 + "name": { 299 + "name": "name", 300 + "type": "text", 301 + "primaryKey": false, 302 + "notNull": true 303 + }, 304 + "display_name": { 305 + "name": "display_name", 306 + "type": "text", 307 + "primaryKey": false, 308 + "notNull": false 309 + }, 310 + "uri": { 311 + "name": "uri", 312 + "type": "text", 313 + "primaryKey": false, 314 + "notNull": false 315 + }, 316 + "repo": { 317 + "name": "repo", 318 + "type": "text", 319 + "primaryKey": false, 320 + "notNull": false 321 + }, 322 + "provider": { 323 + "name": "provider", 324 + "type": "text", 325 + "primaryKey": false, 326 + "notNull": true, 327 + "default": "'cloudflare'" 328 + }, 329 + "description": { 330 + "name": "description", 331 + "type": "text", 332 + "primaryKey": false, 333 + "notNull": false 334 + }, 335 + "logo": { 336 + "name": "logo", 337 + "type": "text", 338 + "primaryKey": false, 339 + "notNull": false 340 + }, 341 + "readme": { 342 + "name": "readme", 343 + "type": "text", 344 + "primaryKey": false, 345 + "notNull": false 346 + }, 347 + "public_key": { 348 + "name": "public_key", 349 + "type": "text", 350 + "primaryKey": false, 351 + "notNull": true 352 + }, 353 + "user_id": { 354 + "name": "user_id", 355 + "type": "text", 356 + "primaryKey": false, 357 + "notNull": false 358 + }, 359 + "instance_type": { 360 + "name": "instance_type", 361 + "type": "text", 362 + "primaryKey": false, 363 + "notNull": false 364 + }, 365 + "vcpus": { 366 + "name": "vcpus", 367 + "type": "integer", 368 + "primaryKey": false, 369 + "notNull": false 370 + }, 371 + "memory": { 372 + "name": "memory", 373 + "type": "integer", 374 + "primaryKey": false, 375 + "notNull": false 376 + }, 377 + "disk": { 378 + "name": "disk", 379 + "type": "integer", 380 + "primaryKey": false, 381 + "notNull": false 382 + }, 383 + "status": { 384 + "name": "status", 385 + "type": "text", 386 + "primaryKey": false, 387 + "notNull": true 388 + }, 389 + "keep_alive": { 390 + "name": "keep_alive", 391 + "type": "boolean", 392 + "primaryKey": false, 393 + "notNull": true, 394 + "default": false 395 + }, 396 + "sleep_after": { 397 + "name": "sleep_after", 398 + "type": "text", 399 + "primaryKey": false, 400 + "notNull": false 401 + }, 402 + "sandbox_id": { 403 + "name": "sandbox_id", 404 + "type": "text", 405 + "primaryKey": false, 406 + "notNull": false 407 + }, 408 + "installs": { 409 + "name": "installs", 410 + "type": "integer", 411 + "primaryKey": false, 412 + "notNull": true, 413 + "default": 0 414 + }, 415 + "started_at": { 416 + "name": "started_at", 417 + "type": "timestamp", 418 + "primaryKey": false, 419 + "notNull": false 420 + }, 421 + "created_at": { 422 + "name": "created_at", 423 + "type": "timestamp", 424 + "primaryKey": false, 425 + "notNull": true, 426 + "default": "now()" 427 + }, 428 + "updated_at": { 429 + "name": "updated_at", 430 + "type": "timestamp", 431 + "primaryKey": false, 432 + "notNull": true, 433 + "default": "now()" 434 + } 435 + }, 436 + "indexes": {}, 437 + "foreignKeys": { 438 + "sandboxes_user_id_users_id_fk": { 439 + "name": "sandboxes_user_id_users_id_fk", 440 + "tableFrom": "sandboxes", 441 + "tableTo": "users", 442 + "columnsFrom": [ 443 + "user_id" 444 + ], 445 + "columnsTo": [ 446 + "id" 447 + ], 448 + "onDelete": "no action", 449 + "onUpdate": "no action" 450 + } 451 + }, 452 + "compositePrimaryKeys": {}, 453 + "uniqueConstraints": { 454 + "sandboxes_name_unique": { 455 + "name": "sandboxes_name_unique", 456 + "nullsNotDistinct": false, 457 + "columns": [ 458 + "name" 459 + ] 460 + }, 461 + "sandboxes_uri_unique": { 462 + "name": "sandboxes_uri_unique", 463 + "nullsNotDistinct": false, 464 + "columns": [ 465 + "uri" 466 + ] 467 + } 468 + }, 469 + "policies": {}, 470 + "checkConstraints": {}, 471 + "isRLSEnabled": false 472 + }, 473 + "public.secrets": { 474 + "name": "secrets", 475 + "schema": "", 476 + "columns": { 477 + "id": { 478 + "name": "id", 479 + "type": "text", 480 + "primaryKey": true, 481 + "notNull": true, 482 + "default": "secret_id()" 483 + }, 484 + "name": { 485 + "name": "name", 486 + "type": "text", 487 + "primaryKey": false, 488 + "notNull": true 489 + }, 490 + "value": { 491 + "name": "value", 492 + "type": "text", 493 + "primaryKey": false, 494 + "notNull": true 495 + }, 496 + "created_at": { 497 + "name": "created_at", 498 + "type": "timestamp", 499 + "primaryKey": false, 500 + "notNull": true, 501 + "default": "now()" 502 + } 503 + }, 504 + "indexes": {}, 505 + "foreignKeys": {}, 506 + "compositePrimaryKeys": {}, 507 + "uniqueConstraints": {}, 508 + "policies": {}, 509 + "checkConstraints": {}, 510 + "isRLSEnabled": false 511 + }, 512 + "public.snapshots": { 513 + "name": "snapshots", 514 + "schema": "", 515 + "columns": { 516 + "id": { 517 + "name": "id", 518 + "type": "text", 519 + "primaryKey": true, 520 + "notNull": true, 521 + "default": "snapshot_id()" 522 + }, 523 + "slug": { 524 + "name": "slug", 525 + "type": "text", 526 + "primaryKey": false, 527 + "notNull": true 528 + }, 529 + "created_at": { 530 + "name": "created_at", 531 + "type": "timestamp", 532 + "primaryKey": false, 533 + "notNull": true, 534 + "default": "now()" 535 + } 536 + }, 537 + "indexes": {}, 538 + "foreignKeys": {}, 539 + "compositePrimaryKeys": {}, 540 + "uniqueConstraints": { 541 + "snapshots_slug_unique": { 542 + "name": "snapshots_slug_unique", 543 + "nullsNotDistinct": false, 544 + "columns": [ 545 + "slug" 546 + ] 547 + } 548 + }, 549 + "policies": {}, 550 + "checkConstraints": {}, 551 + "isRLSEnabled": false 552 + }, 553 + "public.users": { 554 + "name": "users", 555 + "schema": "", 556 + "columns": { 557 + "id": { 558 + "name": "id", 559 + "type": "text", 560 + "primaryKey": true, 561 + "notNull": true, 562 + "default": "xata_id()" 563 + }, 564 + "did": { 565 + "name": "did", 566 + "type": "text", 567 + "primaryKey": false, 568 + "notNull": true 569 + }, 570 + "display_name": { 571 + "name": "display_name", 572 + "type": "text", 573 + "primaryKey": false, 574 + "notNull": false 575 + }, 576 + "handle": { 577 + "name": "handle", 578 + "type": "text", 579 + "primaryKey": false, 580 + "notNull": true 581 + }, 582 + "avatar": { 583 + "name": "avatar", 584 + "type": "text", 585 + "primaryKey": false, 586 + "notNull": false 587 + }, 588 + "created_at": { 589 + "name": "created_at", 590 + "type": "timestamp", 591 + "primaryKey": false, 592 + "notNull": true, 593 + "default": "now()" 594 + }, 595 + "updated_at": { 596 + "name": "updated_at", 597 + "type": "timestamp", 598 + "primaryKey": false, 599 + "notNull": true, 600 + "default": "now()" 601 + } 602 + }, 603 + "indexes": {}, 604 + "foreignKeys": {}, 605 + "compositePrimaryKeys": {}, 606 + "uniqueConstraints": { 607 + "users_did_unique": { 608 + "name": "users_did_unique", 609 + "nullsNotDistinct": false, 610 + "columns": [ 611 + "did" 612 + ] 613 + }, 614 + "users_handle_unique": { 615 + "name": "users_handle_unique", 616 + "nullsNotDistinct": false, 617 + "columns": [ 618 + "handle" 619 + ] 620 + } 621 + }, 622 + "policies": {}, 623 + "checkConstraints": {}, 624 + "isRLSEnabled": false 625 + }, 626 + "public.variables": { 627 + "name": "variables", 628 + "schema": "", 629 + "columns": { 630 + "id": { 631 + "name": "id", 632 + "type": "text", 633 + "primaryKey": true, 634 + "notNull": true, 635 + "default": "variable_id()" 636 + }, 637 + "name": { 638 + "name": "name", 639 + "type": "text", 640 + "primaryKey": false, 641 + "notNull": true 642 + }, 643 + "value": { 644 + "name": "value", 645 + "type": "text", 646 + "primaryKey": false, 647 + "notNull": true 648 + }, 649 + "created_at": { 650 + "name": "created_at", 651 + "type": "timestamp", 652 + "primaryKey": false, 653 + "notNull": true, 654 + "default": "now()" 655 + }, 656 + "updated_at": { 657 + "name": "updated_at", 658 + "type": "timestamp", 659 + "primaryKey": false, 660 + "notNull": true, 661 + "default": "now()" 662 + } 663 + }, 664 + "indexes": {}, 665 + "foreignKeys": {}, 666 + "compositePrimaryKeys": {}, 667 + "uniqueConstraints": {}, 668 + "policies": {}, 669 + "checkConstraints": {}, 670 + "isRLSEnabled": false 671 + }, 672 + "public.volumes": { 673 + "name": "volumes", 674 + "schema": "", 675 + "columns": { 676 + "id": { 677 + "name": "id", 678 + "type": "text", 679 + "primaryKey": true, 680 + "notNull": true, 681 + "default": "volume_id()" 682 + }, 683 + "slug": { 684 + "name": "slug", 685 + "type": "text", 686 + "primaryKey": false, 687 + "notNull": true 688 + }, 689 + "size": { 690 + "name": "size", 691 + "type": "integer", 692 + "primaryKey": false, 693 + "notNull": true 694 + }, 695 + "size_unit": { 696 + "name": "size_unit", 697 + "type": "text", 698 + "primaryKey": false, 699 + "notNull": true 700 + }, 701 + "created_at": { 702 + "name": "created_at", 703 + "type": "timestamp", 704 + "primaryKey": false, 705 + "notNull": true, 706 + "default": "now()" 707 + }, 708 + "updated_at": { 709 + "name": "updated_at", 710 + "type": "timestamp", 711 + "primaryKey": false, 712 + "notNull": true, 713 + "default": "now()" 714 + } 715 + }, 716 + "indexes": {}, 717 + "foreignKeys": {}, 718 + "compositePrimaryKeys": {}, 719 + "uniqueConstraints": { 720 + "volumes_slug_unique": { 721 + "name": "volumes_slug_unique", 722 + "nullsNotDistinct": false, 723 + "columns": [ 724 + "slug" 725 + ] 726 + } 727 + }, 728 + "policies": {}, 729 + "checkConstraints": {}, 730 + "isRLSEnabled": false 731 + } 732 + }, 733 + "enums": {}, 734 + "schemas": {}, 735 + "sequences": {}, 736 + "roles": {}, 737 + "policies": {}, 738 + "views": {}, 739 + "_meta": { 740 + "columns": {}, 741 + "schemas": {}, 742 + "tables": {} 743 + } 744 + }
+14
apps/cf-sandbox/drizzle/meta/_journal.json
··· 71 71 "when": 1771395054419, 72 72 "tag": "0009_sticky_madame_masque", 73 73 "breakpoints": true 74 + }, 75 + { 76 + "idx": 10, 77 + "version": "7", 78 + "when": 1771519396530, 79 + "tag": "0010_motionless_shriek", 80 + "breakpoints": true 81 + }, 82 + { 83 + "idx": 11, 84 + "version": "7", 85 + "when": 1771520023773, 86 + "tag": "0011_nappy_magdalene", 87 + "breakpoints": true 74 88 } 75 89 ] 76 90 }
+1 -6
apps/cf-sandbox/src/schema/sandbox-secrets.ts
··· 16 16 .notNull() 17 17 .references(() => secrets.id), 18 18 }, 19 - (table) => ({ 20 - uniqueSandboxSecret: uniqueIndex("unique_sandbox_secret").on( 21 - table.sandboxId, 22 - table.secretId, 23 - ), 24 - }), 19 + (t) => [uniqueIndex("unique_sandbox_secret").on(t.sandboxId, t.secretId)], 25 20 ); 26 21 27 22 export type SelectSandboxSecrets = InferSelectModel<typeof sandboxSecrets>;
+3 -6
apps/cf-sandbox/src/schema/sandbox-variables.ts
··· 16 16 .notNull() 17 17 .references(() => variables.id), 18 18 }, 19 - (table) => ({ 20 - uniqueSandboxSecret: uniqueIndex("unique_sandbox_variables").on( 21 - table.sandboxId, 22 - table.variableId, 23 - ), 24 - }), 19 + (t) => [ 20 + uniqueIndex("unique_sandbox_variables").on(t.sandboxId, t.variableId), 21 + ], 25 22 ); 26 23 27 24 export type SelectSandboxVariables = InferSelectModel<typeof sandboxVariables>;
+2
apps/cf-sandbox/src/schema/sandboxes.ts
··· 16 16 name: text("name").unique().notNull(), 17 17 displayName: text("display_name"), 18 18 uri: text("uri").unique(), 19 + repo: text("repo"), 19 20 provider: text("provider").default("cloudflare").notNull(), 20 21 description: text("description"), 21 22 logo: text("logo"), ··· 31 32 sleepAfter: text("sleep_after"), 32 33 sandbox_id: text("sandbox_id"), 33 34 installs: integer("installs").default(0).notNull(), 35 + startedAt: timestamp("started_at"), 34 36 createdAt: timestamp("created_at").defaultNow().notNull(), 35 37 updatedAt: timestamp("updated_at").defaultNow().notNull(), 36 38 });
+2
apps/sandbox/.env.example
··· 12 12 VERCEL_TEAM_ID= 13 13 DENO_API_TOKEN= 14 14 DENO_DEPLOY_TOKEN= 15 + JWT_SECRET= 16 + COOKIE_SECRET=
+1
apps/sandbox/src/context.ts
··· 2 2 3 3 export type Context = { 4 4 db: ReturnType<typeof getConnection>; 5 + did?: string; 5 6 };
+28 -3
apps/sandbox/src/index.ts
··· 7 7 sandboxSecrets, 8 8 sandboxVariables, 9 9 secrets, 10 + users, 10 11 variables, 11 12 } from "./schema/mod.ts"; 12 13 import { ··· 28 29 import { NodePgQueryResultHKT } from "drizzle-orm/node-postgres"; 29 30 import chalk from "chalk"; 30 31 import process from "node:process"; 32 + import jwt from "@tsndr/cloudflare-worker-jwt"; 31 33 32 34 const app = new Hono<{ Variables: Context }>(); 33 35 34 36 app.use("*", async (c, next) => { 35 37 c.set("db", getConnection()); 38 + const token = c.req.header("Authorization")?.split(" ")[1]?.trim(); 39 + if (token) { 40 + try { 41 + const decoded = await jwt.verify(token, process.env.JWT_SECRET!); 42 + c.set("did", decoded?.payload.sub); 43 + } catch (err) { 44 + consola.error("JWT verification failed:", err); 45 + } 46 + } 36 47 await next(); 37 48 }); 38 49 ··· 87 98 } while (true); 88 99 89 100 const record = await c.var.db.transaction(async (tx) => { 101 + const did = c.get("did"); 102 + const user = await tx 103 + .select() 104 + .from(users) 105 + .where(eq(users.did, did || "")) 106 + .execute() 107 + .then((res) => res[0]); 90 108 let [record] = await tx 91 109 .insert(sandboxes) 92 110 .values({ ··· 94 112 name, 95 113 provider: params.provider, 96 114 publicKey: process.env.PUBLIC_KEY!, 97 - userId: "rec_d6626djac0kg4q000020", 115 + userId: user?.id, 98 116 instanceType: "standard-1", 99 117 keepAlive: params.keepAlive, 100 118 sleepAfter: params.sleepAfter, ··· 121 139 122 140 [record] = await tx 123 141 .update(sandboxes) 124 - .set({ status: "RUNNING", sandbox_id: sandboxId }) 142 + .set({ 143 + status: "RUNNING", 144 + sandbox_id: sandboxId, 145 + startedAt: new Date(), 146 + vcpus: params.vcpus, 147 + memory: params.memory, 148 + disk: params.disk, 149 + }) 125 150 .where(eq(sandboxes.id, record.id)) 126 151 .returning() 127 152 .execute(); ··· 178 203 await sandbox.start(); 179 204 await c.var.db 180 205 .update(sandboxes) 181 - .set({ status: "RUNNING" }) 206 + .set({ status: "RUNNING", startedAt: new Date() }) 182 207 .where(eq(sandboxes.id, c.req.param("sandboxId"))) 183 208 .execute(); 184 209 return c.json({});
+6 -1
apps/sandbox/src/providers/daytona/mod.ts
··· 1 1 import BaseProvider, { BaseSandbox, SandboxOptions } from "../mod.ts"; 2 2 import { Daytona, Sandbox } from "@daytonaio/sdk"; 3 3 import process from "node:process"; 4 + import consola from "consola"; 4 5 5 6 export class DaytonaSandbox implements BaseSandbox { 6 7 constructor(private sandbox: Sandbox) {} ··· 10 11 } 11 12 12 13 async stop(): Promise<void> { 13 - await this.sandbox.stop(); 14 + try { 15 + await this.sandbox.stop(); 16 + } catch (error) { 17 + consola.error("Error stopping Daytona sandbox:", error); 18 + } 14 19 } 15 20 16 21 async delete(): Promise<void> {
+6 -1
apps/sandbox/src/providers/deno/mod.ts
··· 1 1 import BaseProvider, { BaseSandbox, SandboxOptions } from "../mod.ts"; 2 2 import { Sandbox } from "@deno/sandbox"; 3 3 import process from "node:process"; 4 + import consola from "consola"; 4 5 5 6 export class DenoSandbox implements BaseSandbox { 6 7 constructor(private sandbox: Sandbox) {} ··· 10 11 } 11 12 12 13 async stop(): Promise<void> { 13 - await this.sandbox.kill(); 14 + try { 15 + await this.sandbox.kill(); 16 + } catch (error) { 17 + consola.error("Error killing sandbox:", error); 18 + } 14 19 } 15 20 16 21 async delete(): Promise<void> {
+6 -1
apps/sandbox/src/providers/vercel/mod.ts
··· 1 1 import BaseProvider, { BaseSandbox, SandboxOptions } from "../mod.ts"; 2 2 import { Sandbox } from "@vercel/sandbox"; 3 3 import process from "node:process"; 4 + import consola from "consola"; 4 5 5 6 export class VercelSandbox implements BaseSandbox { 6 7 constructor(private sandbox: Sandbox) {} ··· 10 11 } 11 12 12 13 async stop(): Promise<void> { 13 - await this.sandbox.stop(); 14 + try { 15 + await this.sandbox.stop(); 16 + } catch (error) { 17 + consola.error("Error stopping Vercel sandbox:", error); 18 + } 14 19 } 15 20 16 21 async delete(): Promise<void> {
+1 -6
apps/sandbox/src/schema/sandbox-secrets.ts
··· 16 16 .notNull() 17 17 .references(() => secrets.id), 18 18 }, 19 - (table) => ({ 20 - uniqueSandboxSecret: uniqueIndex("unique_sandbox_secret").on( 21 - table.sandboxId, 22 - table.secretId, 23 - ), 24 - }), 19 + (t) => [uniqueIndex("unique_sandbox_secret").on(t.sandboxId, t.secretId)], 25 20 ); 26 21 27 22 export type SelectSandboxSecrets = InferSelectModel<typeof sandboxSecrets>;
+3 -6
apps/sandbox/src/schema/sandbox-variables.ts
··· 16 16 .notNull() 17 17 .references(() => variables.id), 18 18 }, 19 - (table) => ({ 20 - uniqueSandboxSecret: uniqueIndex("unique_sandbox_variables").on( 21 - table.sandboxId, 22 - table.variableId, 23 - ), 24 - }), 19 + (t) => [ 20 + uniqueIndex("unique_sandbox_variables").on(t.sandboxId, t.variableId), 21 + ], 25 22 ); 26 23 27 24 export type SelectSandboxVariables = InferSelectModel<typeof sandboxVariables>;
+2
apps/sandbox/src/schema/sandboxes.ts
··· 15 15 base: text("base"), 16 16 name: text("name").unique().notNull(), 17 17 uri: text("uri").unique(), 18 + repo: text("repo"), 18 19 provider: text("provider").default("cloudflare").notNull(), 19 20 description: text("description"), 20 21 logo: text("logo"), ··· 30 31 sleepAfter: text("sleep_after"), 31 32 sandbox_id: text("sandbox_id"), 32 33 installs: integer("installs").default(0).notNull(), 34 + startedAt: timestamp("started_at"), 33 35 createdAt: timestamp("created_at").defaultNow().notNull(), 34 36 updatedAt: timestamp("updated_at").defaultNow().notNull(), 35 37 });
+3
apps/sandbox/src/types/sandbox.ts
··· 33 33 provider: z.enum(["daytona", "vercel", "deno"]).optional().default("deno"), 34 34 base: z.enum(["openclaw"]).optional().default("openclaw"), 35 35 keepAlive: z.boolean().optional().default(false), 36 + vcpus: z.number().optional().default(1), 37 + memory: z.number().optional().default(4), 38 + disk: z.number().optional().default(3), 36 39 sleepAfter: z 37 40 .string() 38 41 .regex(
+6
apps/web/bun.lock
··· 22 22 "effect": "^3.19.16", 23 23 "flyonui": "^2.4.1", 24 24 "jotai": "^2.17.1", 25 + "lodash": "^4.17.23", 25 26 "ramda": "^0.32.0", 26 27 "react": "^19.2.0", 27 28 "react-content-loader": "^7.1.2", ··· 32 33 "devDependencies": { 33 34 "@eslint/js": "^9.39.1", 34 35 "@tailwindcss/vite": "^4.1.18", 36 + "@types/lodash": "^4.17.23", 35 37 "@types/node": "^24.10.1", 36 38 "@types/react": "^19.2.7", 37 39 "@types/react-dom": "^19.2.3", ··· 346 348 347 349 "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], 348 350 351 + "@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="], 352 + 349 353 "@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="], 350 354 351 355 "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], ··· 649 653 "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], 650 654 651 655 "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], 656 + 657 + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], 652 658 653 659 "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], 654 660
+2
apps/web/package.json
··· 27 27 "effect": "^3.19.16", 28 28 "flyonui": "^2.4.1", 29 29 "jotai": "^2.17.1", 30 + "lodash": "^4.17.23", 30 31 "ramda": "^0.32.0", 31 32 "react": "^19.2.0", 32 33 "react-content-loader": "^7.1.2", ··· 37 38 "devDependencies": { 38 39 "@eslint/js": "^9.39.1", 39 40 "@tailwindcss/vite": "^4.1.18", 41 + "@types/lodash": "^4.17.23", 40 42 "@types/node": "^24.10.1", 41 43 "@types/react": "^19.2.7", 42 44 "@types/react-dom": "^19.2.3",
+21
apps/web/src/api/sandbox.ts
··· 36 36 client.get<{ sandboxes: Sandbox[]; total: number }>( 37 37 `/xrpc/io.pocketenv.sandbox.getSandboxes?offset=${offset ?? 0}&limit=${limit ?? 30}`, 38 38 ); 39 + 40 + export const stopSandbox = (id: string) => 41 + client.post(`/xrpc/io.pocketenv.sandbox.stopSandbox?id=${id}`, undefined, { 42 + headers: { 43 + Authorization: `Bearer ${localStorage.getItem("token")}`, 44 + }, 45 + }); 46 + 47 + export const deleteSandbox = (id: string) => 48 + client.post(`/xrpc/io.pocketenv.sandbox.deleteSandbox?id=${id}`, undefined, { 49 + headers: { 50 + Authorization: `Bearer ${localStorage.getItem("token")}`, 51 + }, 52 + }); 53 + 54 + export const startSandbox = (id: string) => 55 + client.post(`/xrpc/io.pocketenv.sandbox.startSandbox?id=${id}`, undefined, { 56 + headers: { 57 + Authorization: `Bearer ${localStorage.getItem("token")}`, 58 + }, 59 + });
+4
apps/web/src/atoms/profile.ts
··· 1 + import { atom } from "jotai"; 2 + import type { Profile } from "../types/profile"; 3 + 4 + export const profileAtom = atom<Profile | undefined>(undefined);
+9
apps/web/src/components/navbar/Navbar.tsx
··· 6 6 import Logo from "../../assets/logo.png"; 7 7 import SignIn from "../signin"; 8 8 import { useCurrentProfileQuery } from "../../hooks/useProfile"; 9 + import { useAtom } from "jotai"; 10 + import { profileAtom } from "../../atoms/profile"; 9 11 10 12 export type NavbarProps = { 11 13 title: string; ··· 14 16 }; 15 17 16 18 function Navbar({ title, project, withLogo }: NavbarProps) { 19 + const [, setProfile] = useAtom(profileAtom); 17 20 const [open, setOpen] = useState(false); 18 21 const [modalOpen, setModalOpen] = useState(false); 19 22 const [signInModalOpen, setSignInModalOpen] = useState(false); ··· 31 34 setOpen(false); 32 35 navigate({ to: "/" }); 33 36 }; 37 + 38 + useEffect(() => { 39 + if (profile) { 40 + setProfile(profile); 41 + } 42 + }, [profile, setProfile]); 34 43 35 44 useEffect(() => { 36 45 const handleClickOutside = (event: MouseEvent) => {
+1 -1
apps/web/src/components/signin/index.tsx
··· 1 - import Signin from "./Signin"; 1 + import Signin from "./SignIn"; 2 2 3 3 export default Signin;
+1
apps/web/src/consts.ts
··· 1 1 export const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8789"; 2 2 export const WS_URL = import.meta.env.VITE_WS_URL || "ws://localhost:8790"; 3 + export const POCKETENV_DID = "did:plc:aturpi2ls3yvsmhc6wybomun";
+21
apps/web/src/hooks/useSandbox.ts
··· 2 2 import { 3 3 claimSandbox, 4 4 createSandbox, 5 + deleteSandbox, 5 6 getSandbox, 6 7 getSandboxes, 8 + startSandbox, 9 + stopSandbox, 7 10 } from "../api/sandbox"; 8 11 9 12 export const useSandboxesQuery = (offset?: number, limit?: number) => ··· 37 40 id, 38 41 }), 39 42 }); 43 + 44 + export const useStopSandboxMutation = () => 45 + useMutation({ 46 + mutationKey: ["stopSandbox"], 47 + mutationFn: async (id: string) => stopSandbox(id), 48 + }); 49 + 50 + export const useDeleteSandboxMutation = () => 51 + useMutation({ 52 + mutationKey: ["deleteSandbox"], 53 + mutationFn: async (id: string) => deleteSandbox(id), 54 + }); 55 + 56 + export const useStartSandboxMutation = () => 57 + useMutation({ 58 + mutationKey: ["startSandbox"], 59 + mutationFn: async (id: string) => startSandbox(id), 60 + });
+74 -14
apps/web/src/pages/sandbox/Sandbox.tsx
··· 1 1 import { useState } from "react"; 2 2 import Navbar from "../../components/navbar"; 3 - import SignIn from "../../components/signin/Signin"; 3 + import SignIn from "../../components/signin"; 4 4 import { useLocation, useNavigate } from "@tanstack/react-router"; 5 - import { useSandboxQuery } from "../../hooks/useSandbox"; 6 - import { consola } from "consola"; 5 + import { 6 + useSandboxQuery, 7 + useStartSandboxMutation, 8 + useStopSandboxMutation, 9 + } from "../../hooks/useSandbox"; 10 + import _ from "lodash"; 11 + import dayjs from "dayjs"; 12 + import relativeTime from "dayjs/plugin/relativeTime"; 13 + import { useAtomValue } from "jotai"; 14 + import { profileAtom } from "../../atoms/profile"; 15 + import { useQueryClient } from "@tanstack/react-query"; 16 + 17 + dayjs.extend(relativeTime); 7 18 8 19 function New() { 20 + const profile = useAtomValue(profileAtom); 21 + const queryClient = useQueryClient(); 22 + const { mutate: stopSandbox } = useStopSandboxMutation(); 23 + const { mutate: startSandbox } = useStartSandboxMutation(); 9 24 const isAuthenticated = !!localStorage.getItem("token"); 10 25 const [signInModalOpen, setSignInModalOpen] = useState(false); 11 26 const navigate = useNavigate(); ··· 31 46 32 47 const { data, isLoading } = useSandboxQuery(getSandboxIdFromPath()); 33 48 34 - consola.info("Sandbox data:", data, isLoading); 35 - 36 49 const onClaim = () => { 37 50 if (isAuthenticated) { 51 + // create a new sandbox for the user and then navigate to it 38 52 navigate({ 39 53 to: "/did:plc:pyzvvyrh6eudle55nhqe62tv/sandbox/3mezx5ymmjs26", 40 54 }); ··· 92 106 <tbody> 93 107 <tr> 94 108 <td> 95 - <span className="badge badge-soft badge-success rounded-full bg-green-400/10"> 96 - Running 109 + <span 110 + className={`badge badge-soft ${data?.sandbox?.status === "RUNNING" ? "badge-success" : ""} rounded-full ${data?.sandbox.status === "RUNNING" ? "bg-green-400/10" : "bg-white/15 rounded"}`} 111 + > 112 + {_.upperFirst(_.camelCase(data.sandbox.status))} 97 113 </span> 98 114 </td> 99 - <td>1 minute ago</td> 115 + <td> 116 + {data?.sandbox?.startedAt 117 + ? dayjs(data.sandbox.startedAt).fromNow() 118 + : "-"} 119 + </td> 100 120 <td>5m</td> 101 121 <td> 102 122 <span className="badge badge-soft badge-primary bg-blue-400/10 rounded-full"> 103 - 1 CPU 123 + {data?.sandbox?.vcpus} CPU 104 124 </span> 105 125 <span className="badge badge-soft badge-primary bg-blue-400/10 rounded-full ml-2"> 106 - 4 GiB RAM 126 + {data?.sandbox?.memory} GiB RAM 107 127 </span> 108 128 </td> 109 129 </tr> ··· 111 131 </table> 112 132 </div> 113 133 </div> 114 - <button className="btn btn-outline btn-lg hover:text-white"> 115 - <span className="icon-[tabler--player-stop-filled] size-5 shrink-0"></span> 116 - Stop Sandbox 117 - </button> 134 + {data?.sandbox?.status === "RUNNING" && 135 + ((profile && data?.sandbox?.uri?.includes(profile.did)) || 136 + !data?.sandbox?.owner) && ( 137 + <button 138 + onClick={() => 139 + stopSandbox(data!.sandbox!.id, { 140 + onSuccess: async () => { 141 + await queryClient.invalidateQueries({ 142 + queryKey: ["sandbox", data?.sandbox?.id], 143 + }); 144 + await queryClient.invalidateQueries({ 145 + queryKey: ["sandbox", data?.sandbox?.uri], 146 + }); 147 + }, 148 + }) 149 + } 150 + className="btn btn-outline btn-lg hover:text-white" 151 + > 152 + <span className="icon-[tabler--player-stop-filled] size-5 shrink-0"></span> 153 + Stop Sandbox 154 + </button> 155 + )} 156 + {data?.sandbox?.status !== "RUNNING" && 157 + ((profile && data?.sandbox?.uri?.includes(profile.did)) || 158 + !data?.sandbox?.owner) && ( 159 + <button 160 + onClick={() => 161 + startSandbox(data!.sandbox!.id, { 162 + onSuccess: async () => { 163 + await queryClient.invalidateQueries({ 164 + queryKey: ["sandbox", data?.sandbox?.id], 165 + }); 166 + await queryClient.invalidateQueries({ 167 + queryKey: ["sandbox", data?.sandbox?.uri], 168 + }); 169 + }, 170 + }) 171 + } 172 + className="btn btn-outline btn-lg hover:text-white" 173 + > 174 + <span className="icon-[tabler--player-play-filled] size-5 shrink-0"></span> 175 + Start Sandbox 176 + </button> 177 + )} 118 178 </div> 119 179 </div> 120 180 </>
-1
apps/web/src/pages/signin/SignIn.tsx
··· 1 - import { useNavigate } from "@tanstack/react-router"; 2 1 import { useState } from "react"; 3 2 import { API_URL } from "../../consts"; 4 3
+7
apps/web/src/types/sandbox.ts
··· 1 + import type { Profile } from "./profile"; 2 + 1 3 export type Sandbox = { 2 4 id: string; 3 5 name: string; ··· 6 8 description?: string; 7 9 logo?: string; 8 10 readme?: string; 11 + vcpus?: number; 12 + memory?: number; 9 13 installs: number; 14 + status: "RUNNING" | "STOPPED"; 15 + startedAt?: string; 10 16 createdAt: string; 17 + owner: Profile | null; 11 18 };