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 sandbox provider preference and UI

Add SandboxProviderPref lexicon type and validation utilities.
Update getPreferences and putPreferences to query provider auth
tables (daytona/deno/sprites/vercel) and return/save provider info.
Add provider settings page, route, sidebar entry (commented), and
select dark-mode styles. Fix minor schema formatting and a typo in the
sprite token field name.

+488 -43
+24 -1
apps/api/lexicons/sandbox/defs.json
··· 299 299 } 300 300 } 301 301 }, 302 + "sandboxProviderPref": { 303 + "type": "object", 304 + "required": [ 305 + "name" 306 + ], 307 + "properties": { 308 + "name": { 309 + "type": "string", 310 + "description": "The name of the sandbox provider", 311 + "minLength": 1 312 + }, 313 + "apiKey": { 314 + "type": "string", 315 + "description": "The encrypted API key used to authenticate with the sandbox provider.", 316 + "minLength": 1 317 + }, 318 + "redactedApiKey": { 319 + "type": "string", 320 + "description": "The redacted API key for the sandbox provider, returned in API responses." 321 + } 322 + } 323 + }, 302 324 "preferences": { 303 325 "type": "array", 304 326 "items": { ··· 308 330 "#secretPref", 309 331 "#variablePref", 310 332 "#filePref", 311 - "#volumePref" 333 + "#volumePref", 334 + "#sandboxProviderPref" 312 335 ] 313 336 } 314 337 },
+21
apps/api/pkl/defs/sandbox/defs.pkl
··· 302 302 } 303 303 description = "A volume to add to the sandbox" 304 304 } 305 + ["sandboxProviderPref"] = new ObjectType { 306 + type = "object" 307 + required = List("name") 308 + properties { 309 + ["name"] = new StringType { 310 + type = "string" 311 + description = "The name of the sandbox provider" 312 + minLength = 1 313 + } 314 + ["apiKey"] = new StringType { 315 + type = "string" 316 + description = "The encrypted API key used to authenticate with the sandbox provider." 317 + minLength = 1 318 + } 319 + ["redactedApiKey"] = new StringType { 320 + type = "string" 321 + description = "The redacted API key for the sandbox provider, returned in API responses." 322 + } 323 + } 324 + } 305 325 ["preferences"] = new Array { 306 326 type = "array" 307 327 items = new Union { ··· 313 333 "#variablePref", 314 334 "#filePref", 315 335 "#volumePref", 336 + "#sandboxProviderPref", 316 337 ) 317 338 } 318 339 }
+23
apps/api/src/lexicon/lexicons.ts
··· 913 913 }, 914 914 }, 915 915 }, 916 + sandboxProviderPref: { 917 + type: "object", 918 + required: ["name"], 919 + properties: { 920 + name: { 921 + type: "string", 922 + description: "The name of the sandbox provider", 923 + minLength: 1, 924 + }, 925 + apiKey: { 926 + type: "string", 927 + description: 928 + "The encrypted API key used to authenticate with the sandbox provider.", 929 + minLength: 1, 930 + }, 931 + redactedApiKey: { 932 + type: "string", 933 + description: 934 + "The redacted API key for the sandbox provider, returned in API responses.", 935 + }, 936 + }, 937 + }, 916 938 preferences: { 917 939 type: "array", 918 940 items: { ··· 923 945 "lex:io.pocketenv.sandbox.defs#variablePref", 924 946 "lex:io.pocketenv.sandbox.defs#filePref", 925 947 "lex:io.pocketenv.sandbox.defs#volumePref", 948 + "lex:io.pocketenv.sandbox.defs#sandboxProviderPref", 926 949 ], 927 950 }, 928 951 },
+23
apps/api/src/lexicon/types/io/pocketenv/sandbox/defs.ts
··· 216 216 return lexicons.validate("io.pocketenv.sandbox.defs#volumePref", v); 217 217 } 218 218 219 + export interface SandboxProviderPref { 220 + /** The name of the sandbox provider */ 221 + name: string; 222 + /** The encrypted API key used to authenticate with the sandbox provider. */ 223 + apiKey?: string; 224 + /** The redacted API key for the sandbox provider, returned in API responses. */ 225 + redactedApiKey?: string; 226 + [k: string]: unknown; 227 + } 228 + 229 + export function isSandboxProviderPref(v: unknown): v is SandboxProviderPref { 230 + return ( 231 + isObj(v) && 232 + hasProp(v, "$type") && 233 + v.$type === "io.pocketenv.sandbox.defs#sandboxProviderPref" 234 + ); 235 + } 236 + 237 + export function validateSandboxProviderPref(v: unknown): ValidationResult { 238 + return lexicons.validate("io.pocketenv.sandbox.defs#sandboxProviderPref", v); 239 + } 240 + 219 241 export type Preferences = ( 220 242 | SandboxDetailsPref 221 243 | SecretPref 222 244 | VariablePref 223 245 | FilePref 224 246 | VolumePref 247 + | SandboxProviderPref 225 248 | { $type: string; [k: string]: unknown } 226 249 )[]; 227 250
+1 -3
apps/api/src/schema/daytona-auth.ts
··· 6 6 const daytonaAuth = pgTable( 7 7 "daytona_auth", 8 8 { 9 - id: text("id") 10 - .primaryKey() 11 - .default(sql`xata_id()`), 9 + id: text("id").primaryKey().default(sql`xata_id()`), 12 10 sandboxId: text("sandbox_id") 13 11 .notNull() 14 12 .references(() => sandboxes.id, { onDelete: "cascade" }),
+1 -3
apps/api/src/schema/deno-auth.ts
··· 6 6 const denoAuth = pgTable( 7 7 "deno_auth", 8 8 { 9 - id: text("id") 10 - .primaryKey() 11 - .default(sql`xata_id()`), 9 + id: text("id").primaryKey().default(sql`xata_id()`), 12 10 sandboxId: text("sandbox_id") 13 11 .notNull() 14 12 .references(() => sandboxes.id, { onDelete: "cascade" }),
+1 -3
apps/api/src/schema/integrations.ts
··· 5 5 const integrations = pgTable( 6 6 "integrations", 7 7 { 8 - id: text("id") 9 - .primaryKey() 10 - .default(sql`xata_id()`), 8 + id: text("id").primaryKey().default(sql`xata_id()`), 11 9 sandboxId: text("sandbox_id") 12 10 .notNull() 13 11 .references(() => sandboxes.id, { onDelete: "cascade" }),
+1 -3
apps/api/src/schema/sandbox-files.ts
··· 6 6 const sandboxFiles = pgTable( 7 7 "sandbox_files", 8 8 { 9 - id: text("id") 10 - .primaryKey() 11 - .default(sql`file_id()`), 9 + id: text("id").primaryKey().default(sql`file_id()`), 12 10 sandboxId: text("sandbox_id") 13 11 .notNull() 14 12 .references(() => sandboxes.id, { onDelete: "cascade" }),
+1 -3
apps/api/src/schema/sandbox-ports.ts
··· 12 12 const sandboxPorts = pgTable( 13 13 "sandbox_ports", 14 14 { 15 - id: text("id") 16 - .primaryKey() 17 - .default(sql`xata_id()`), 15 + id: text("id").primaryKey().default(sql`xata_id()`), 18 16 sandboxId: text("sandbox_id") 19 17 .notNull() 20 18 .references(() => sandboxes.id, { onDelete: "cascade" }),
+1 -3
apps/api/src/schema/sandbox-secrets.ts
··· 6 6 const sandboxSecrets = pgTable( 7 7 "sandbox_secrets", 8 8 { 9 - id: text("id") 10 - .primaryKey() 11 - .default(sql`xata_id()`), 9 + id: text("id").primaryKey().default(sql`xata_id()`), 12 10 sandboxId: text("sandbox_id") 13 11 .notNull() 14 12 .references(() => sandboxes.id, { onDelete: "cascade" }),
+1 -3
apps/api/src/schema/sandbox-variables.ts
··· 6 6 const sandboxVariables = pgTable( 7 7 "sandbox_variables", 8 8 { 9 - id: text("id") 10 - .primaryKey() 11 - .default(sql`xata_id()`), 9 + id: text("id").primaryKey().default(sql`xata_id()`), 12 10 sandboxId: text("sandbox_id") 13 11 .notNull() 14 12 .references(() => sandboxes.id, { onDelete: "cascade" }),
+1 -3
apps/api/src/schema/sandbox-volumes.ts
··· 6 6 const sandboxVolumes = pgTable( 7 7 "sandbox_volumes", 8 8 { 9 - id: text("id") 10 - .primaryKey() 11 - .default(sql`volume_id()`), 9 + id: text("id").primaryKey().default(sql`volume_id()`), 12 10 sandboxId: text("sandbox_id") 13 11 .notNull() 14 12 .references(() => sandboxes.id, { onDelete: "cascade" }),
+1 -3
apps/api/src/schema/services.ts
··· 5 5 const services = pgTable( 6 6 "services", 7 7 { 8 - id: text("id") 9 - .primaryKey() 10 - .default(sql`xata_id()`), 8 + id: text("id").primaryKey().default(sql`xata_id()`), 11 9 sandboxId: text("sandbox_id") 12 10 .notNull() 13 11 .references(() => sandboxes.id, { onDelete: "cascade" }),
+1 -1
apps/api/src/schema/sprite-auth.ts
··· 16 16 .notNull() 17 17 .references(() => users.id), 18 18 spriteToken: text("sprite_token").notNull(), 19 - redatedSpriteToken: text("redated_sprite_token").notNull(), 19 + redactedSpriteToken: text("redacted_sprite_token").notNull(), 20 20 createdAt: timestamp("created_at").defaultNow().notNull(), 21 21 }, 22 22 (t) => [uniqueIndex("unique_sprite_auth").on(t.sandboxId, t.userId)],
+1 -3
apps/api/src/schema/ssh-keys.ts
··· 5 5 const sshKeys = pgTable( 6 6 "ssh_keys", 7 7 { 8 - id: text("id") 9 - .primaryKey() 10 - .default(sql`xata_id()`), 8 + id: text("id").primaryKey().default(sql`xata_id()`), 11 9 sandboxId: text("sandbox_id") 12 10 .notNull() 13 11 .references(() => sandboxes.id, { onDelete: "cascade" }),
+1 -3
apps/api/src/schema/tailscale-auth-keys.ts
··· 3 3 import sandboxes from "./sandboxes"; 4 4 5 5 const tailscaleAuthKey = pgTable("tailscale_auth_keys", { 6 - id: text("id") 7 - .primaryKey() 8 - .default(sql`xata_id()`), 6 + id: text("id").primaryKey().default(sql`xata_id()`), 9 7 sandboxId: text("sandbox_id") 10 8 .notNull() 11 9 .references(() => sandboxes.id, { onDelete: "cascade" }),
+1 -3
apps/api/src/schema/vercel-auth.ts
··· 6 6 const vercelAuth = pgTable( 7 7 "vercel_auth", 8 8 { 9 - id: text("id") 10 - .primaryKey() 11 - .default(sql`xata_id()`), 9 + id: text("id").primaryKey().default(sql`xata_id()`), 12 10 sandboxId: text("sandbox_id") 13 11 .notNull() 14 12 .references(() => sandboxes.id, { onDelete: "cascade" }),
+76 -3
apps/api/src/xrpc/io/pocketenv/sandbox/getPreferences.ts
··· 1 1 import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 2 2 import type { Context } from "context"; 3 + import { and, eq, or } from "drizzle-orm"; 3 4 import type { Server } from "lexicon"; 4 - import type { QueryParams } from "lexicon/types/io/pocketenv/sandbox/getPreferences"; 5 + import type { SandboxProviderPref } from "lexicon/types/io/pocketenv/sandbox/defs"; 6 + import type { 7 + QueryParams, 8 + OutputSchema, 9 + } from "lexicon/types/io/pocketenv/sandbox/getPreferences"; 10 + import daytonaAuth from "schema/daytona-auth"; 11 + import denoAuth from "schema/deno-auth"; 12 + import sandboxes from "schema/sandboxes"; 13 + import spriteAuth from "schema/sprite-auth"; 14 + import users from "schema/users"; 15 + import vercelAuth from "schema/vercel-auth"; 5 16 6 17 export default function (server: Server, ctx: Context) { 7 18 const getPreferences = async (params: QueryParams, auth: HandlerAuth) => { 8 - return {}; 19 + if (!auth.credentials) { 20 + throw new XRPCError(401, "Unauthorized"); 21 + } 22 + 23 + const [user] = await ctx.db 24 + .select() 25 + .from(users) 26 + .where(eq(users.did, auth.credentials.did)) 27 + .execute(); 28 + 29 + if (!user) { 30 + throw new XRPCError(404, "User not found"); 31 + } 32 + 33 + const sandboxFilter = or( 34 + eq(sandboxes.sandboxId, params.id), 35 + eq(sandboxes.uri, params.id), 36 + eq(sandboxes.name, params.id), 37 + ); 38 + 39 + const [daytona, deno, sprite, vercel] = await Promise.all([ 40 + ctx.db 41 + .select() 42 + .from(daytonaAuth) 43 + .leftJoin(sandboxes, eq(daytonaAuth.sandboxId, sandboxes.id)) 44 + .where(and(eq(daytonaAuth.userId, user.id), sandboxFilter)) 45 + .execute() 46 + .then(([row]) => row?.daytona_auth), 47 + ctx.db 48 + .select() 49 + .from(denoAuth) 50 + .leftJoin(sandboxes, eq(denoAuth.sandboxId, sandboxes.id)) 51 + .where(and(eq(denoAuth.userId, user.id), sandboxFilter)) 52 + .execute() 53 + .then(([row]) => row?.deno_auth), 54 + ctx.db 55 + .select() 56 + .from(spriteAuth) 57 + .leftJoin(sandboxes, eq(spriteAuth.sandboxId, sandboxes.id)) 58 + .where(and(eq(spriteAuth.userId, user.id), sandboxFilter)) 59 + .execute() 60 + .then(([row]) => row?.sprite_auth), 61 + ctx.db 62 + .select() 63 + .from(vercelAuth) 64 + .leftJoin(sandboxes, eq(vercelAuth.sandboxId, sandboxes.id)) 65 + .where(and(eq(vercelAuth.userId, user.id), sandboxFilter)) 66 + .execute() 67 + .then(([row]) => row?.vercel_auth), 68 + ]); 69 + 70 + if (!daytona && !deno && !sprite && !vercel) { 71 + return []; 72 + } 73 + 74 + const provider = ( 75 + (daytona && { name: "daytona" as const, redactedApiKey: daytona.redactedApiKey }) || 76 + (deno && { name: "deno" as const, redactedApiKey: deno.redactedDenoToken }) || 77 + (sprite && { name: "sprites" as const, redactedApiKey: sprite.redactedSpriteToken }) || 78 + (vercel && { name: "vercel" as const, redactedApiKey: vercel.redactedVercelToken }) 79 + )!; 80 + 81 + return [provider satisfies SandboxProviderPref]; 9 82 }; 10 83 server.io.pocketenv.sandbox.getPreferences({ 11 84 auth: ctx.authVerifier, ··· 13 86 const result = await getPreferences(params, auth); 14 87 return { 15 88 encoding: "application/json", 16 - body: result as any, // TODO: Implement getPreferences 89 + body: result satisfies OutputSchema, 17 90 }; 18 91 }, 19 92 });
+115 -2
apps/api/src/xrpc/io/pocketenv/sandbox/putPreferences.ts
··· 1 1 import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 2 2 import { updateSandbox } from "atproto/sandbox"; 3 3 import type { Context } from "context"; 4 - import { and, eq } from "drizzle-orm"; 4 + import { and, eq, type ExtractTablesWithRelations } from "drizzle-orm"; 5 5 import type { Server } from "lexicon"; 6 - import { isSandboxDetailsPref } from "lexicon/types/io/pocketenv/sandbox/defs"; 6 + import { 7 + isSandboxDetailsPref, 8 + isSandboxProviderPref, 9 + type SandboxProviderPref, 10 + } from "lexicon/types/io/pocketenv/sandbox/defs"; 7 11 import type { HandlerInput } from "lexicon/types/io/pocketenv/sandbox/putPreferences"; 8 12 import { createAgent } from "lib/agent"; 9 13 import sandboxes from "schema/sandboxes"; 10 14 import users from "schema/users"; 11 15 import { consola } from "consola"; 16 + import daytonaAuth from "schema/daytona-auth"; 17 + import denoAuth from "schema/deno-auth"; 18 + import vercelAuth from "schema/vercel-auth"; 19 + import spriteAuth from "schema/sprite-auth"; 20 + import type { PgTransaction } from "drizzle-orm/pg-core"; 21 + import type { NodePgQueryResultHKT } from "drizzle-orm/node-postgres"; 12 22 13 23 export default function (server: Server, ctx: Context) { 14 24 const putPreferences = async (input: HandlerInput, auth: HandlerAuth) => { ··· 69 79 }), 70 80 ); 71 81 } 82 + if (isSandboxProviderPref(pref)) { 83 + await ctx.db.transaction(async (tx) => { 84 + await tx 85 + .update(sandboxes) 86 + .set({ provider: pref.name }) 87 + .where( 88 + and( 89 + eq(sandboxes.id, input.body.sandboxId), 90 + eq(sandboxes.userId, user.id), 91 + ), 92 + ) 93 + .execute(); 94 + await saveSandboxProvider(tx, user, input, pref); 95 + }); 96 + } 72 97 } 73 98 return {}; 74 99 }; ··· 79 104 }, 80 105 }); 81 106 } 107 + 108 + const saveSandboxProvider = async ( 109 + tx: PgTransaction< 110 + NodePgQueryResultHKT, 111 + Record<string, never>, 112 + ExtractTablesWithRelations<Record<string, never>> 113 + >, 114 + user: { id: string; did: string }, 115 + input: HandlerInput, 116 + pref: SandboxProviderPref, 117 + ) => { 118 + switch (pref.name) { 119 + case "daytona": 120 + await tx 121 + .insert(daytonaAuth) 122 + .values({ 123 + userId: user.id, 124 + sandboxId: input.body.sandboxId, 125 + apiKey: pref.apiKey!, 126 + redactedApiKey: pref.redactedApiKey!, 127 + }) 128 + .onConflictDoUpdate({ 129 + target: [daytonaAuth.sandboxId, daytonaAuth.userId], 130 + set: { 131 + apiKey: pref.apiKey!, 132 + redactedApiKey: pref.redactedApiKey!, 133 + }, 134 + }) 135 + .execute(); 136 + break; 137 + case "deno": 138 + await tx 139 + .insert(denoAuth) 140 + .values({ 141 + userId: user.id, 142 + sandboxId: input.body.sandboxId, 143 + deployToken: pref.apiKey!, 144 + redactedDenoToken: pref.redactedApiKey!, 145 + }) 146 + .onConflictDoUpdate({ 147 + target: [denoAuth.sandboxId, denoAuth.userId], 148 + set: { 149 + deployToken: pref.apiKey!, 150 + redactedDenoToken: pref.redactedApiKey!, 151 + }, 152 + }) 153 + .execute(); 154 + break; 155 + case "vercel": 156 + await tx 157 + .insert(vercelAuth) 158 + .values({ 159 + userId: user.id, 160 + sandboxId: input.body.sandboxId, 161 + vercelToken: pref.apiKey!, 162 + redactedVercelToken: pref.redactedApiKey!, 163 + }) 164 + .onConflictDoUpdate({ 165 + target: [vercelAuth.sandboxId, vercelAuth.userId], 166 + set: { 167 + vercelToken: pref.apiKey!, 168 + redactedVercelToken: pref.redactedApiKey!, 169 + }, 170 + }) 171 + .execute(); 172 + break; 173 + case "sprites": 174 + await tx 175 + .insert(spriteAuth) 176 + .values({ 177 + userId: user.id, 178 + sandboxId: input.body.sandboxId, 179 + spriteToken: pref.apiKey!, 180 + redactedSpriteToken: pref.redactedApiKey!, 181 + }) 182 + .onConflictDoUpdate({ 183 + target: [spriteAuth.sandboxId, spriteAuth.userId], 184 + set: { 185 + spriteToken: pref.apiKey!, 186 + redactedSpriteToken: pref.redactedApiKey!, 187 + }, 188 + }) 189 + .execute(); 190 + break; 191 + default: 192 + break; 193 + } 194 + };
+14
apps/web/src/index.css
··· 174 174 background-color: #06051d !important; 175 175 } 176 176 177 + :root.dark .select { 178 + background-color: #06051d !important; 179 + color: #e5e5e5; 180 + border: 1px solid rgba(229, 229, 229, 0.2) !important; 181 + box-shadow: none !important; 182 + outline: none !important; 183 + } 184 + 185 + :root.dark .select:focus { 186 + border: 2px solid var(--color-primary) !important; 187 + box-shadow: none !important; 188 + outline: none !important; 189 + } 190 + 177 191 /* Modal backgrounds - applies to both light and dark mode */ 178 192 .modal-content { 179 193 background-color: #06051d !important;
+1
apps/web/src/layouts/Main.tsx
··· 35 35 /^\/did:plc:[a-z0-9]+\/sandbox\/[a-z0-9]+\/repository$/.test(path) || 36 36 /^\/did:plc:[a-z0-9]+\/sandbox\/[a-z0-9]+\/ports$/.test(path) || 37 37 /^\/did:plc:[a-z0-9]+\/sandbox\/[a-z0-9]+\/services/.test(path) || 38 + /^\/did:plc:[a-z0-9]+\/sandbox\/[a-z0-9]+\/provider/.test(path) || 38 39 /^\/did:plc:[a-z0-9]+\/sandbox\/[a-z0-9]+\/tailscale$/.test(path) 39 40 ) 40 41 return "Settings";
+125
apps/web/src/pages/settings/provider/Provider.tsx
··· 1 + import { useRouterState } from "@tanstack/react-router"; 2 + import { zodResolver } from "@hookform/resolvers/zod"; 3 + import { useForm, useWatch } from "react-hook-form"; 4 + import { z } from "zod"; 5 + import { useSandboxQuery } from "../../../hooks/useSandbox"; 6 + import Main from "../../../layouts/Main"; 7 + import Sidebar from "../sidebar/Sidebar"; 8 + 9 + const LABELS = { 10 + daytona: "Daytona API Key", 11 + vercel: "Vercel Access Token", 12 + deno: "Deno Deploy Token", 13 + sprites: "Sprites API Key", 14 + } as const; 15 + 16 + type Provider = keyof typeof LABELS | "cloudflare"; 17 + 18 + const schema = z 19 + .object({ 20 + provider: z.enum(["cloudflare", "daytona", "vercel", "deno", "sprites"]), 21 + apiKey: z.string().optional(), 22 + }) 23 + .superRefine((data, ctx) => { 24 + if (data.provider !== "cloudflare" && !data.apiKey?.trim()) { 25 + ctx.addIssue({ 26 + code: z.ZodIssueCode.custom, 27 + message: `${LABELS[data.provider as keyof typeof LABELS]} is required`, 28 + path: ["apiKey"], 29 + }); 30 + } 31 + }); 32 + 33 + type FormValues = z.infer<typeof schema>; 34 + 35 + function Services() { 36 + const routerState = useRouterState(); 37 + const pathname = routerState.location.pathname; 38 + const { data } = useSandboxQuery( 39 + `at:/${pathname.replace("/provider", "").replace("sandbox", "io.pocketenv.sandbox")}`, 40 + ); 41 + 42 + const { 43 + register, 44 + handleSubmit, 45 + control, 46 + formState: { errors }, 47 + } = useForm<FormValues>({ 48 + resolver: zodResolver(schema), 49 + defaultValues: { provider: "daytona", apiKey: "" }, 50 + }); 51 + 52 + const provider = useWatch({ control, name: "provider" }) as Provider; 53 + 54 + const onSubmit = (values: FormValues) => { 55 + console.log(values); 56 + }; 57 + 58 + return ( 59 + <Main 60 + sidebar={<Sidebar />} 61 + root={data?.sandbox?.name} 62 + rootLink={pathname.replace("/provider", "")} 63 + > 64 + <> 65 + <div className="w-[95%] m-auto"> 66 + <div className="flex flex-row items-center"> 67 + <h1 className="mb-1 text-xl flex-1">Sandbox Provider</h1> 68 + </div> 69 + <p className="opacity-60 mb-5"> 70 + A Sandbox provider is responsible for running your Sandbox and 71 + providing the necessary resources. 72 + </p> 73 + <form onSubmit={handleSubmit(onSubmit)}> 74 + <div className="w-full overflow-x-auto"> 75 + <div className="flex flex-row"> 76 + <div className="w-96 mr-6"> 77 + <label className="label-text"> 78 + Pick your Sandbox Provider 79 + </label> 80 + <select 81 + {...register("provider")} 82 + className="select select-lg font-medium text-[15px]" 83 + > 84 + <option value="cloudflare"> 85 + Cloudflare Sandbox (Recommended) 86 + </option> 87 + <option value="daytona">Daytona</option> 88 + <option value="vercel">Vercel Sandbox</option> 89 + <option value="deno">Deno Sandbox</option> 90 + <option value="sprites">Sprites</option> 91 + </select> 92 + </div> 93 + {provider !== "cloudflare" && ( 94 + <div className="w-96"> 95 + <label className="label-text">{LABELS[provider]}</label> 96 + <input 97 + {...register("apiKey")} 98 + type="text" 99 + className="input input-lg font-medium text-[15px]" 100 + /> 101 + {errors.apiKey && ( 102 + <p className="text-error text-sm mt-1"> 103 + {errors.apiKey.message} 104 + </p> 105 + )} 106 + </div> 107 + )} 108 + </div> 109 + <div> 110 + <button 111 + type="submit" 112 + className="btn btn-primary font-semibold mt-5" 113 + > 114 + Save 115 + </button> 116 + </div> 117 + </div> 118 + </form> 119 + </div> 120 + </> 121 + </Main> 122 + ); 123 + } 124 + 125 + export default Services;
+3
apps/web/src/pages/settings/provider/index.tsx
··· 1 + import Provider from "./Provider"; 2 + 3 + export default Provider;
+16
apps/web/src/pages/settings/sidebar/Sidebar.tsx
··· 70 70 {!isCollapsed && "General"} 71 71 </Link> 72 72 </li> 73 + {/*<li> 74 + <Link 75 + to={`/${did}/sandbox/${rkey}/provider`} 76 + className={`${ 77 + isActive("/provider") 78 + ? "active bg-white/7 text-[#00e8c6]! font-semibold rounded-full" 79 + : "rounded-full hover:text-white" 80 + } ${isCollapsed ? "justify-center px-2" : ""}`} 81 + title={isCollapsed ? "Sandbox Provider" : undefined} 82 + > 83 + <span 84 + className={`icon-[solar--play-outline] size-5 ${isCollapsed ? "" : "mr-2.5"}`} 85 + ></span> 86 + {!isCollapsed && "Sandbox Provider"} 87 + </Link> 88 + </li>*/} 73 89 <li> 74 90 <Link 75 91 to={`/${did}/sandbox/${rkey}/repository`}
+21
apps/web/src/routeTree.gen.ts
··· 28 28 import { Route as DidSandboxRkeyServicesRouteImport } from './routes/$did.sandbox.$rkey/services' 29 29 import { Route as DidSandboxRkeySecretsRouteImport } from './routes/$did.sandbox.$rkey/secrets' 30 30 import { Route as DidSandboxRkeyRepositoryRouteImport } from './routes/$did.sandbox.$rkey/repository' 31 + import { Route as DidSandboxRkeyProviderRouteImport } from './routes/$did.sandbox.$rkey/provider' 31 32 import { Route as DidSandboxRkeyPortsRouteImport } from './routes/$did.sandbox.$rkey/ports' 32 33 import { Route as DidSandboxRkeyIntegrationsRouteImport } from './routes/$did.sandbox.$rkey/integrations' 33 34 import { Route as DidSandboxRkeyFilesRouteImport } from './routes/$did.sandbox.$rkey/files' ··· 128 129 path: '/repository', 129 130 getParentRoute: () => DidSandboxRkeyRoute, 130 131 } as any) 132 + const DidSandboxRkeyProviderRoute = DidSandboxRkeyProviderRouteImport.update({ 133 + id: '/provider', 134 + path: '/provider', 135 + getParentRoute: () => DidSandboxRkeyRoute, 136 + } as any) 131 137 const DidSandboxRkeyPortsRoute = DidSandboxRkeyPortsRouteImport.update({ 132 138 id: '/ports', 133 139 path: '/ports', ··· 159 165 '/$did/sandbox/$rkey/files': typeof DidSandboxRkeyFilesRoute 160 166 '/$did/sandbox/$rkey/integrations': typeof DidSandboxRkeyIntegrationsRoute 161 167 '/$did/sandbox/$rkey/ports': typeof DidSandboxRkeyPortsRoute 168 + '/$did/sandbox/$rkey/provider': typeof DidSandboxRkeyProviderRoute 162 169 '/$did/sandbox/$rkey/repository': typeof DidSandboxRkeyRepositoryRoute 163 170 '/$did/sandbox/$rkey/secrets': typeof DidSandboxRkeySecretsRoute 164 171 '/$did/sandbox/$rkey/services': typeof DidSandboxRkeyServicesRoute ··· 182 189 '/$did/sandbox/$rkey/files': typeof DidSandboxRkeyFilesRoute 183 190 '/$did/sandbox/$rkey/integrations': typeof DidSandboxRkeyIntegrationsRoute 184 191 '/$did/sandbox/$rkey/ports': typeof DidSandboxRkeyPortsRoute 192 + '/$did/sandbox/$rkey/provider': typeof DidSandboxRkeyProviderRoute 185 193 '/$did/sandbox/$rkey/repository': typeof DidSandboxRkeyRepositoryRoute 186 194 '/$did/sandbox/$rkey/secrets': typeof DidSandboxRkeySecretsRoute 187 195 '/$did/sandbox/$rkey/services': typeof DidSandboxRkeyServicesRoute ··· 207 215 '/$did/sandbox/$rkey/files': typeof DidSandboxRkeyFilesRoute 208 216 '/$did/sandbox/$rkey/integrations': typeof DidSandboxRkeyIntegrationsRoute 209 217 '/$did/sandbox/$rkey/ports': typeof DidSandboxRkeyPortsRoute 218 + '/$did/sandbox/$rkey/provider': typeof DidSandboxRkeyProviderRoute 210 219 '/$did/sandbox/$rkey/repository': typeof DidSandboxRkeyRepositoryRoute 211 220 '/$did/sandbox/$rkey/secrets': typeof DidSandboxRkeySecretsRoute 212 221 '/$did/sandbox/$rkey/services': typeof DidSandboxRkeyServicesRoute ··· 233 242 | '/$did/sandbox/$rkey/files' 234 243 | '/$did/sandbox/$rkey/integrations' 235 244 | '/$did/sandbox/$rkey/ports' 245 + | '/$did/sandbox/$rkey/provider' 236 246 | '/$did/sandbox/$rkey/repository' 237 247 | '/$did/sandbox/$rkey/secrets' 238 248 | '/$did/sandbox/$rkey/services' ··· 256 266 | '/$did/sandbox/$rkey/files' 257 267 | '/$did/sandbox/$rkey/integrations' 258 268 | '/$did/sandbox/$rkey/ports' 269 + | '/$did/sandbox/$rkey/provider' 259 270 | '/$did/sandbox/$rkey/repository' 260 271 | '/$did/sandbox/$rkey/secrets' 261 272 | '/$did/sandbox/$rkey/services' ··· 280 291 | '/$did/sandbox/$rkey/files' 281 292 | '/$did/sandbox/$rkey/integrations' 282 293 | '/$did/sandbox/$rkey/ports' 294 + | '/$did/sandbox/$rkey/provider' 283 295 | '/$did/sandbox/$rkey/repository' 284 296 | '/$did/sandbox/$rkey/secrets' 285 297 | '/$did/sandbox/$rkey/services' ··· 439 451 preLoaderRoute: typeof DidSandboxRkeyRepositoryRouteImport 440 452 parentRoute: typeof DidSandboxRkeyRoute 441 453 } 454 + '/$did/sandbox/$rkey/provider': { 455 + id: '/$did/sandbox/$rkey/provider' 456 + path: '/provider' 457 + fullPath: '/$did/sandbox/$rkey/provider' 458 + preLoaderRoute: typeof DidSandboxRkeyProviderRouteImport 459 + parentRoute: typeof DidSandboxRkeyRoute 460 + } 442 461 '/$did/sandbox/$rkey/ports': { 443 462 id: '/$did/sandbox/$rkey/ports' 444 463 path: '/ports' ··· 467 486 DidSandboxRkeyFilesRoute: typeof DidSandboxRkeyFilesRoute 468 487 DidSandboxRkeyIntegrationsRoute: typeof DidSandboxRkeyIntegrationsRoute 469 488 DidSandboxRkeyPortsRoute: typeof DidSandboxRkeyPortsRoute 489 + DidSandboxRkeyProviderRoute: typeof DidSandboxRkeyProviderRoute 470 490 DidSandboxRkeyRepositoryRoute: typeof DidSandboxRkeyRepositoryRoute 471 491 DidSandboxRkeySecretsRoute: typeof DidSandboxRkeySecretsRoute 472 492 DidSandboxRkeyServicesRoute: typeof DidSandboxRkeyServicesRoute ··· 482 502 DidSandboxRkeyFilesRoute: DidSandboxRkeyFilesRoute, 483 503 DidSandboxRkeyIntegrationsRoute: DidSandboxRkeyIntegrationsRoute, 484 504 DidSandboxRkeyPortsRoute: DidSandboxRkeyPortsRoute, 505 + DidSandboxRkeyProviderRoute: DidSandboxRkeyProviderRoute, 485 506 DidSandboxRkeyRepositoryRoute: DidSandboxRkeyRepositoryRoute, 486 507 DidSandboxRkeySecretsRoute: DidSandboxRkeySecretsRoute, 487 508 DidSandboxRkeyServicesRoute: DidSandboxRkeyServicesRoute,
apps/web/src/routes/$did.sandbox.$rkey/ports.ts apps/web/src/routes/$did.sandbox.$rkey/ports.tsx
+13
apps/web/src/routes/$did.sandbox.$rkey/provider.tsx
··· 1 + import { createFileRoute, redirect } from "@tanstack/react-router"; 2 + import ProviderPage from "../../pages/settings/provider"; 3 + 4 + export const Route = createFileRoute("/$did/sandbox/$rkey/provider")({ 5 + beforeLoad: () => { 6 + const isAuthenticated = !!localStorage.getItem("token"); 7 + 8 + if (!isAuthenticated) { 9 + throw redirect({ to: "/" }); 10 + } 11 + }, 12 + component: ProviderPage, 13 + });