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 port expose/unexpose support

+2866 -4
+26
apps/api/lexicons/port/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "io.pocketenv.port.defs", 4 + "defs": { 5 + "portView": { 6 + "type": "object", 7 + "description": "A view of a port exposed by a sandbox.", 8 + "properties": { 9 + "port": { 10 + "type": "integer", 11 + "description": "The port number.", 12 + "maximum": 65535, 13 + "minimum": 1 14 + }, 15 + "description": { 16 + "type": "string", 17 + "description": "A description of the port." 18 + }, 19 + "previewUrl": { 20 + "type": "string", 21 + "description": "A URL for previewing the service running on the port" 22 + } 23 + } 24 + } 25 + } 26 + }
+43
apps/api/lexicons/sandbox/exposePort.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "io.pocketenv.sandbox.exposePort", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Expose a port for a sandbox.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "id" 12 + ], 13 + "properties": { 14 + "id": { 15 + "type": "string", 16 + "description": "The sandbox ID." 17 + } 18 + } 19 + }, 20 + "input": { 21 + "encoding": "application/json", 22 + "schema": { 23 + "type": "object", 24 + "required": [ 25 + "port" 26 + ], 27 + "properties": { 28 + "port": { 29 + "type": "integer", 30 + "description": "The port number to expose.", 31 + "maximum": 65535, 32 + "minimum": 1 33 + }, 34 + "description": { 35 + "type": "string", 36 + "description": "A description of the port." 37 + } 38 + } 39 + } 40 + } 41 + } 42 + } 43 + }
+37
apps/api/lexicons/sandbox/getExposedPorts.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "io.pocketenv.sandbox.getExposedPorts", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get the list of exposed ports for a sandbox.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "id" 12 + ], 13 + "properties": { 14 + "id": { 15 + "type": "string", 16 + "description": "The sandbox ID." 17 + } 18 + } 19 + }, 20 + "output": { 21 + "encoding": "application/json", 22 + "schema": { 23 + "type": "object", 24 + "properties": { 25 + "ports": { 26 + "type": "array", 27 + "items": { 28 + "type": "ref", 29 + "ref": "io.pocketenv.port.defs#portView" 30 + } 31 + } 32 + } 33 + } 34 + } 35 + } 36 + } 37 + }
+39
apps/api/lexicons/sandbox/unexposePort.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "io.pocketenv.sandbox.unexposePort", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Unexpose a port for a sandbox.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "id" 12 + ], 13 + "properties": { 14 + "id": { 15 + "type": "string", 16 + "description": "The sandbox ID." 17 + } 18 + } 19 + }, 20 + "input": { 21 + "encoding": "application/json", 22 + "schema": { 23 + "type": "object", 24 + "required": [ 25 + "port" 26 + ], 27 + "properties": { 28 + "port": { 29 + "type": "integer", 30 + "description": "The port number to unexpose.", 31 + "maximum": 65535, 32 + "minimum": 1 33 + } 34 + } 35 + } 36 + } 37 + } 38 + } 39 + }
+26
apps/api/pkl/defs/port/defs.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "io.pocketenv.port.defs" 5 + defs = new Mapping<String, ObjectType> { 6 + ["portView"] { 7 + type = "object" 8 + description = "A view of a port exposed by a sandbox." 9 + properties { 10 + ["port"] = new IntegerType { 11 + type = "integer" 12 + description = "The port number." 13 + minimum = 1 14 + maximum = 65535 15 + } 16 + ["description"] = new StringType { 17 + type = "string" 18 + description = "A description of the port." 19 + } 20 + ["previewUrl"] = new StringType { 21 + type = "string" 22 + description = "A URL for previewing the service running on the port" 23 + } 24 + } 25 + } 26 + }
+39
apps/api/pkl/defs/sandbox/exposePort.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "io.pocketenv.sandbox.exposePort" 5 + defs = new Mapping<String, Procedure> { 6 + ["main"] { 7 + type = "procedure" 8 + description = "Expose a port for a sandbox." 9 + parameters { 10 + type = "params" 11 + required = List("id") 12 + properties { 13 + ["id"] = new StringType { 14 + type = "string" 15 + description = "The sandbox ID." 16 + } 17 + } 18 + } 19 + input { 20 + encoding = "application/json" 21 + schema { 22 + type = "object" 23 + required = List("port") 24 + properties { 25 + ["port"] = new IntegerType { 26 + type = "integer" 27 + description = "The port number to expose." 28 + minimum = 1 29 + maximum = 65535 30 + } 31 + ["description"] = new StringType { 32 + type = "string" 33 + description = "A description of the port." 34 + } 35 + } 36 + } 37 + } 38 + } 39 + }
+34
apps/api/pkl/defs/sandbox/getExposedPorts.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "io.pocketenv.sandbox.getExposedPorts" 5 + defs = new Mapping<String, Query> { 6 + ["main"] { 7 + type = "query" 8 + description = "Get the list of exposed ports for a sandbox." 9 + parameters { 10 + type = "params" 11 + required = List("id") 12 + properties { 13 + ["id"] = new StringType { 14 + type = "string" 15 + description = "The sandbox ID." 16 + } 17 + } 18 + } 19 + output { 20 + encoding = "application/json" 21 + schema = new ObjectType { 22 + type = "object" 23 + properties { 24 + ["ports"] = new Array { 25 + type = "array" 26 + items = new Ref { 27 + ref = "io.pocketenv.port.defs#portView" 28 + } 29 + } 30 + } 31 + } 32 + } 33 + } 34 + }
+35
apps/api/pkl/defs/sandbox/unexposePort.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "io.pocketenv.sandbox.unexposePort" 5 + defs = new Mapping<String, Procedure> { 6 + ["main"] { 7 + type = "procedure" 8 + description = "Unexpose a port for a sandbox." 9 + parameters { 10 + type = "params" 11 + required = List("id") 12 + properties { 13 + ["id"] = new StringType { 14 + type = "string" 15 + description = "The sandbox ID." 16 + } 17 + } 18 + } 19 + input { 20 + encoding = "application/json" 21 + schema { 22 + type = "object" 23 + required = List("port") 24 + properties { 25 + ["port"] = new IntegerType { 26 + type = "integer" 27 + description = "The port number to unexpose." 28 + minimum = 1 29 + maximum = 65535 30 + } 31 + } 32 + } 33 + } 34 + } 35 + }
+36
apps/api/src/lexicon/index.ts
··· 21 21 import type * as IoPocketenvSandboxCreateIntegration from "./types/io/pocketenv/sandbox/createIntegration"; 22 22 import type * as IoPocketenvSandboxCreateSandbox from "./types/io/pocketenv/sandbox/createSandbox"; 23 23 import type * as IoPocketenvSandboxDeleteSandbox from "./types/io/pocketenv/sandbox/deleteSandbox"; 24 + import type * as IoPocketenvSandboxExposePort from "./types/io/pocketenv/sandbox/exposePort"; 25 + import type * as IoPocketenvSandboxGetExposedPorts from "./types/io/pocketenv/sandbox/getExposedPorts"; 24 26 import type * as IoPocketenvSandboxGetIntegrations from "./types/io/pocketenv/sandbox/getIntegrations"; 25 27 import type * as IoPocketenvSandboxGetPreferences from "./types/io/pocketenv/sandbox/getPreferences"; 26 28 import type * as IoPocketenvSandboxGetSandbox from "./types/io/pocketenv/sandbox/getSandbox"; ··· 34 36 import type * as IoPocketenvSandboxPutTailscaleToken from "./types/io/pocketenv/sandbox/putTailscaleToken"; 35 37 import type * as IoPocketenvSandboxStartSandbox from "./types/io/pocketenv/sandbox/startSandbox"; 36 38 import type * as IoPocketenvSandboxStopSandbox from "./types/io/pocketenv/sandbox/stopSandbox"; 39 + import type * as IoPocketenvSandboxUnexposePort from "./types/io/pocketenv/sandbox/unexposePort"; 37 40 import type * as IoPocketenvSandboxUpdateSandboxSettings from "./types/io/pocketenv/sandbox/updateSandboxSettings"; 38 41 import type * as IoPocketenvSecretAddSecret from "./types/io/pocketenv/secret/addSecret"; 39 42 import type * as IoPocketenvSecretDeleteSecret from "./types/io/pocketenv/secret/deleteSecret"; ··· 254 257 return this._server.xrpc.method(nsid, cfg); 255 258 } 256 259 260 + exposePort<AV extends AuthVerifier>( 261 + cfg: ConfigOf< 262 + AV, 263 + IoPocketenvSandboxExposePort.Handler<ExtractAuth<AV>>, 264 + IoPocketenvSandboxExposePort.HandlerReqCtx<ExtractAuth<AV>> 265 + >, 266 + ) { 267 + const nsid = "io.pocketenv.sandbox.exposePort"; // @ts-ignore 268 + return this._server.xrpc.method(nsid, cfg); 269 + } 270 + 271 + getExposedPorts<AV extends AuthVerifier>( 272 + cfg: ConfigOf< 273 + AV, 274 + IoPocketenvSandboxGetExposedPorts.Handler<ExtractAuth<AV>>, 275 + IoPocketenvSandboxGetExposedPorts.HandlerReqCtx<ExtractAuth<AV>> 276 + >, 277 + ) { 278 + const nsid = "io.pocketenv.sandbox.getExposedPorts"; // @ts-ignore 279 + return this._server.xrpc.method(nsid, cfg); 280 + } 281 + 257 282 getIntegrations<AV extends AuthVerifier>( 258 283 cfg: ConfigOf< 259 284 AV, ··· 394 419 >, 395 420 ) { 396 421 const nsid = "io.pocketenv.sandbox.stopSandbox"; // @ts-ignore 422 + return this._server.xrpc.method(nsid, cfg); 423 + } 424 + 425 + unexposePort<AV extends AuthVerifier>( 426 + cfg: ConfigOf< 427 + AV, 428 + IoPocketenvSandboxUnexposePort.Handler<ExtractAuth<AV>>, 429 + IoPocketenvSandboxUnexposePort.HandlerReqCtx<ExtractAuth<AV>> 430 + >, 431 + ) { 432 + const nsid = "io.pocketenv.sandbox.unexposePort"; // @ts-ignore 397 433 return this._server.xrpc.method(nsid, cfg); 398 434 } 399 435
+139
apps/api/src/lexicon/lexicons.ts
··· 413 413 }, 414 414 }, 415 415 }, 416 + IoPocketenvPortDefs: { 417 + lexicon: 1, 418 + id: "io.pocketenv.port.defs", 419 + defs: { 420 + portView: { 421 + type: "object", 422 + description: "A view of a port exposed by a sandbox.", 423 + properties: { 424 + port: { 425 + type: "integer", 426 + description: "The port number.", 427 + maximum: 65535, 428 + minimum: 1, 429 + }, 430 + description: { 431 + type: "string", 432 + description: "A description of the port.", 433 + }, 434 + previewUrl: { 435 + type: "string", 436 + description: "A URL for previewing the service running on the port", 437 + }, 438 + }, 439 + }, 440 + }, 441 + }, 416 442 IoPocketenvSandboxClaimSandbox: { 417 443 lexicon: 1, 418 444 id: "io.pocketenv.sandbox.claimSandbox", ··· 1017 1043 }, 1018 1044 }, 1019 1045 }, 1046 + IoPocketenvSandboxExposePort: { 1047 + lexicon: 1, 1048 + id: "io.pocketenv.sandbox.exposePort", 1049 + defs: { 1050 + main: { 1051 + type: "procedure", 1052 + description: "Expose a port for a sandbox.", 1053 + parameters: { 1054 + type: "params", 1055 + required: ["id"], 1056 + properties: { 1057 + id: { 1058 + type: "string", 1059 + description: "The sandbox ID.", 1060 + }, 1061 + }, 1062 + }, 1063 + input: { 1064 + encoding: "application/json", 1065 + schema: { 1066 + type: "object", 1067 + required: ["port"], 1068 + properties: { 1069 + port: { 1070 + type: "integer", 1071 + description: "The port number to expose.", 1072 + maximum: 65535, 1073 + minimum: 1, 1074 + }, 1075 + description: { 1076 + type: "string", 1077 + description: "A description of the port.", 1078 + }, 1079 + }, 1080 + }, 1081 + }, 1082 + }, 1083 + }, 1084 + }, 1085 + IoPocketenvSandboxGetExposedPorts: { 1086 + lexicon: 1, 1087 + id: "io.pocketenv.sandbox.getExposedPorts", 1088 + defs: { 1089 + main: { 1090 + type: "query", 1091 + description: "Get the list of exposed ports for a sandbox.", 1092 + parameters: { 1093 + type: "params", 1094 + required: ["id"], 1095 + properties: { 1096 + id: { 1097 + type: "string", 1098 + description: "The sandbox ID.", 1099 + }, 1100 + }, 1101 + }, 1102 + output: { 1103 + encoding: "application/json", 1104 + schema: { 1105 + type: "object", 1106 + properties: { 1107 + ports: { 1108 + type: "array", 1109 + items: { 1110 + type: "ref", 1111 + ref: "lex:io.pocketenv.port.defs#portView", 1112 + }, 1113 + }, 1114 + }, 1115 + }, 1116 + }, 1117 + }, 1118 + }, 1119 + }, 1020 1120 IoPocketenvSandboxGetIntegrations: { 1021 1121 lexicon: 1, 1022 1122 id: "io.pocketenv.sandbox.getIntegrations", ··· 1535 1635 schema: { 1536 1636 type: "ref", 1537 1637 ref: "lex:io.pocketenv.sandbox.defs#sandboxViewBasic", 1638 + }, 1639 + }, 1640 + }, 1641 + }, 1642 + }, 1643 + IoPocketenvSandboxUnexposePort: { 1644 + lexicon: 1, 1645 + id: "io.pocketenv.sandbox.unexposePort", 1646 + defs: { 1647 + main: { 1648 + type: "procedure", 1649 + description: "Unexpose a port for a sandbox.", 1650 + parameters: { 1651 + type: "params", 1652 + required: ["id"], 1653 + properties: { 1654 + id: { 1655 + type: "string", 1656 + description: "The sandbox ID.", 1657 + }, 1658 + }, 1659 + }, 1660 + input: { 1661 + encoding: "application/json", 1662 + schema: { 1663 + type: "object", 1664 + required: ["port"], 1665 + properties: { 1666 + port: { 1667 + type: "integer", 1668 + description: "The port number to unexpose.", 1669 + maximum: 65535, 1670 + minimum: 1, 1671 + }, 1672 + }, 1538 1673 }, 1539 1674 }, 1540 1675 }, ··· 2330 2465 IoPocketenvFileGetFile: "io.pocketenv.file.getFile", 2331 2466 IoPocketenvFileGetFiles: "io.pocketenv.file.getFiles", 2332 2467 IoPocketenvFileUpdateFile: "io.pocketenv.file.updateFile", 2468 + IoPocketenvPortDefs: "io.pocketenv.port.defs", 2333 2469 IoPocketenvSandboxClaimSandbox: "io.pocketenv.sandbox.claimSandbox", 2334 2470 IoPocketenvSandboxCreateIntegration: "io.pocketenv.sandbox.createIntegration", 2335 2471 IoPocketenvSandboxCreateSandbox: "io.pocketenv.sandbox.createSandbox", 2336 2472 IoPocketenvSandboxDefs: "io.pocketenv.sandbox.defs", 2337 2473 IoPocketenvSandboxDeleteSandbox: "io.pocketenv.sandbox.deleteSandbox", 2474 + IoPocketenvSandboxExposePort: "io.pocketenv.sandbox.exposePort", 2475 + IoPocketenvSandboxGetExposedPorts: "io.pocketenv.sandbox.getExposedPorts", 2338 2476 IoPocketenvSandboxGetIntegrations: "io.pocketenv.sandbox.getIntegrations", 2339 2477 IoPocketenvSandboxGetPreferences: "io.pocketenv.sandbox.getPreferences", 2340 2478 IoPocketenvSandboxGetSandbox: "io.pocketenv.sandbox.getSandbox", ··· 2351 2489 IoPocketenvSandbox: "io.pocketenv.sandbox", 2352 2490 IoPocketenvSandboxStartSandbox: "io.pocketenv.sandbox.startSandbox", 2353 2491 IoPocketenvSandboxStopSandbox: "io.pocketenv.sandbox.stopSandbox", 2492 + IoPocketenvSandboxUnexposePort: "io.pocketenv.sandbox.unexposePort", 2354 2493 IoPocketenvSandboxUpdateSandboxSettings: 2355 2494 "io.pocketenv.sandbox.updateSandboxSettings", 2356 2495 IoPocketenvSecretAddSecret: "io.pocketenv.secret.addSecret",
+30
apps/api/src/lexicon/types/io/pocketenv/port/defs.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from "@atproto/lexicon"; 5 + import { lexicons } from "../../../../lexicons"; 6 + import { isObj, hasProp } from "../../../../util"; 7 + import { CID } from "multiformats/cid"; 8 + 9 + /** A view of a port exposed by a sandbox. */ 10 + export interface PortView { 11 + /** The port number. */ 12 + port?: number; 13 + /** A description of the port. */ 14 + description?: string; 15 + /** A URL for previewing the service running on the port */ 16 + previewUrl?: string; 17 + [k: string]: unknown; 18 + } 19 + 20 + export function isPortView(v: unknown): v is PortView { 21 + return ( 22 + isObj(v) && 23 + hasProp(v, "$type") && 24 + v.$type === "io.pocketenv.port.defs#portView" 25 + ); 26 + } 27 + 28 + export function validatePortView(v: unknown): ValidationResult { 29 + return lexicons.validate("io.pocketenv.port.defs#portView", v); 30 + }
+45
apps/api/src/lexicon/types/io/pocketenv/sandbox/exposePort.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import type express from "express"; 5 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 6 + import { lexicons } from "../../../../lexicons"; 7 + import { isObj, hasProp } from "../../../../util"; 8 + import { CID } from "multiformats/cid"; 9 + import { type HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 + 11 + export interface QueryParams { 12 + /** The sandbox ID. */ 13 + id: string; 14 + } 15 + 16 + export interface InputSchema { 17 + /** The port number to expose. */ 18 + port: number; 19 + /** A description of the port. */ 20 + description?: string; 21 + [k: string]: unknown; 22 + } 23 + 24 + export interface HandlerInput { 25 + encoding: "application/json"; 26 + body: InputSchema; 27 + } 28 + 29 + export interface HandlerError { 30 + status: number; 31 + message?: string; 32 + } 33 + 34 + export type HandlerOutput = HandlerError | void; 35 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 36 + auth: HA; 37 + params: QueryParams; 38 + input: HandlerInput; 39 + req: express.Request; 40 + res: express.Response; 41 + resetRouteRateLimits: () => Promise<void>; 42 + }; 43 + export type Handler<HA extends HandlerAuth = never> = ( 44 + ctx: HandlerReqCtx<HA>, 45 + ) => Promise<HandlerOutput> | HandlerOutput;
+48
apps/api/src/lexicon/types/io/pocketenv/sandbox/getExposedPorts.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import type express from "express"; 5 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 6 + import { lexicons } from "../../../../lexicons"; 7 + import { isObj, hasProp } from "../../../../util"; 8 + import { CID } from "multiformats/cid"; 9 + import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 + import type * as IoPocketenvPortDefs from "../port/defs"; 11 + 12 + export interface QueryParams { 13 + /** The sandbox ID. */ 14 + id: string; 15 + } 16 + 17 + export type InputSchema = undefined; 18 + 19 + export interface OutputSchema { 20 + ports?: IoPocketenvPortDefs.PortView[]; 21 + [k: string]: unknown; 22 + } 23 + 24 + export type HandlerInput = undefined; 25 + 26 + export interface HandlerSuccess { 27 + encoding: "application/json"; 28 + body: OutputSchema; 29 + headers?: { [key: string]: string }; 30 + } 31 + 32 + export interface HandlerError { 33 + status: number; 34 + message?: string; 35 + } 36 + 37 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 38 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 39 + auth: HA; 40 + params: QueryParams; 41 + input: HandlerInput; 42 + req: express.Request; 43 + res: express.Response; 44 + resetRouteRateLimits: () => Promise<void>; 45 + }; 46 + export type Handler<HA extends HandlerAuth = never> = ( 47 + ctx: HandlerReqCtx<HA>, 48 + ) => Promise<HandlerOutput> | HandlerOutput;
+43
apps/api/src/lexicon/types/io/pocketenv/sandbox/unexposePort.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import type express from "express"; 5 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 6 + import { lexicons } from "../../../../lexicons"; 7 + import { isObj, hasProp } from "../../../../util"; 8 + import { CID } from "multiformats/cid"; 9 + import { type HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 + 11 + export interface QueryParams { 12 + /** The sandbox ID. */ 13 + id: string; 14 + } 15 + 16 + export interface InputSchema { 17 + /** The port number to unexpose. */ 18 + port: number; 19 + [k: string]: unknown; 20 + } 21 + 22 + export interface HandlerInput { 23 + encoding: "application/json"; 24 + body: InputSchema; 25 + } 26 + 27 + export interface HandlerError { 28 + status: number; 29 + message?: string; 30 + } 31 + 32 + export type HandlerOutput = HandlerError | void; 33 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 34 + auth: HA; 35 + params: QueryParams; 36 + input: HandlerInput; 37 + req: express.Request; 38 + res: express.Response; 39 + resetRouteRateLimits: () => Promise<void>; 40 + }; 41 + export type Handler<HA extends HandlerAuth = never> = ( 42 + ctx: HandlerReqCtx<HA>, 43 + ) => Promise<HandlerOutput> | HandlerOutput;
+2
apps/api/src/schema/index.ts
··· 11 11 import sandboxFiles from "./sandbox-files"; 12 12 import tailscaleAuthKeys from "./tailscale-auth-keys"; 13 13 import sshKeys from "./ssh-keys"; 14 + import sandboxPorts from "./sandbox-ports"; 14 15 15 16 export default { 16 17 sandboxes, ··· 26 27 sandboxFiles, 27 28 tailscaleAuthKeys, 28 29 sshKeys, 30 + sandboxPorts, 29 31 };
+2 -3
apps/api/src/schema/sandbox-ports.ts
··· 11 11 const sandboxPorts = pgTable( 12 12 "sandbox_ports", 13 13 { 14 - id: text("id") 15 - .primaryKey() 16 - .default(sql`xata_id()`), 14 + id: text("id").primaryKey().default(sql`xata_id()`), 17 15 sandboxId: text("sandbox_id") 18 16 .notNull() 19 17 .references(() => sandboxes.id), 20 18 exposedPort: integer("exposed_port").notNull(), 21 19 previewUrl: text("preview_url"), 20 + description: text("description"), 22 21 createdAt: timestamp("created_at").defaultNow().notNull(), 23 22 updatedAt: timestamp("updated_at").defaultNow().notNull(), 24 23 },
+6
apps/api/src/xrpc/index.ts
··· 38 38 import updateSecret from "./io/pocketenv/secret/updateSecret"; 39 39 import updateVariable from "./io/pocketenv/variable/updateVariable"; 40 40 import updateVolume from "./io/pocketenv/volume/updateVolume"; 41 + import exposePort from "./io/pocketenv/sandbox/exposePort"; 42 + import getExposedPorts from "./io/pocketenv/sandbox/getExposedPorts"; 43 + import unexposePort from "./io/pocketenv/sandbox/unexposePort"; 41 44 42 45 export default function (server: Server, ctx: Context) { 43 46 // io.pocketenv ··· 82 85 updateSecret(server, ctx); 83 86 updateVariable(server, ctx); 84 87 updateVolume(server, ctx); 88 + exposePort(server, ctx); 89 + getExposedPorts(server, ctx); 90 + unexposePort(server, ctx); 85 91 86 92 return server; 87 93 }
+86
apps/api/src/xrpc/io/pocketenv/sandbox/exposePort.ts
··· 1 + import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 2 + import { Providers } from "consts"; 3 + import type { Context } from "context"; 4 + import { and, eq } from "drizzle-orm"; 5 + import type { Server } from "lexicon"; 6 + import type { 7 + QueryParams, 8 + InputSchema, 9 + } from "lexicon/types/io/pocketenv/sandbox/exposePort"; 10 + import generateJwt from "lib/generateJwt"; 11 + import schema from "schema"; 12 + 13 + export default function (server: Server, ctx: Context) { 14 + const exposePort = async ( 15 + params: QueryParams, 16 + input: InputSchema, 17 + auth: HandlerAuth, 18 + ) => { 19 + if (!auth.credentials) { 20 + throw new XRPCError(401, "Unauthorized"); 21 + } 22 + 23 + await ctx.db.transaction(async (tx) => { 24 + const [record] = await tx 25 + .select() 26 + .from(schema.sandboxes) 27 + .leftJoin(schema.users, eq(schema.sandboxes.userId, schema.users.id)) 28 + .where( 29 + and( 30 + eq(schema.sandboxes.id, params.id), 31 + eq(schema.users.did, auth.credentials.did), 32 + ), 33 + ) 34 + .execute(); 35 + 36 + if (!record) { 37 + throw new XRPCError(404, "Sandbox not found"); 38 + } 39 + 40 + await ctx.db 41 + .insert(schema.sandboxPorts) 42 + .values({ 43 + sandboxId: record.sandboxes.id, 44 + exposedPort: input.port, 45 + description: input.description, 46 + }) 47 + .execute(); 48 + 49 + const sandbox = 50 + record.sandboxes.provider === Providers.CLOUDFLARE 51 + ? ctx.cfsandbox(record.sandboxes.base!) 52 + : ctx.sandbox(); 53 + 54 + const { data } = await sandbox.post<{ previewUrl: string }>( 55 + `/v1/sandboxes/${record.sandboxes.id}/ports`, 56 + { 57 + port: input.port, 58 + }, 59 + { 60 + headers: { 61 + Authorization: `Bearer ${await generateJwt(auth?.credentials?.did || "")}`, 62 + }, 63 + }, 64 + ); 65 + 66 + if (data.previewUrl) { 67 + await ctx.db 68 + .update(schema.sandboxPorts) 69 + .set({ previewUrl: data.previewUrl }) 70 + .where( 71 + and( 72 + eq(schema.sandboxPorts.sandboxId, record.sandboxes.id), 73 + eq(schema.sandboxPorts.exposedPort, input.port), 74 + ), 75 + ) 76 + .execute(); 77 + } 78 + }); 79 + }; 80 + server.io.pocketenv.sandbox.exposePort({ 81 + auth: ctx.authVerifier, 82 + handler: async ({ params, input, auth }) => { 83 + await exposePort(params, input.body, auth); 84 + }, 85 + }); 86 + }
+53
apps/api/src/xrpc/io/pocketenv/sandbox/getExposedPorts.ts
··· 1 + import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 2 + import type { Context } from "context"; 3 + import { and, eq } from "drizzle-orm"; 4 + import type { Server } from "lexicon"; 5 + import type { 6 + QueryParams, 7 + OutputSchema, 8 + } from "lexicon/types/io/pocketenv/sandbox/getExposedPorts"; 9 + import schema from "schema"; 10 + import sandboxPorts from "schema/sandbox-ports"; 11 + 12 + export default function (server: Server, ctx: Context) { 13 + const getExposedPorts = async ( 14 + params: QueryParams, 15 + auth: HandlerAuth, 16 + ): Promise<OutputSchema> => { 17 + if (!auth.credentials) { 18 + throw new XRPCError(401, "Unauthorized"); 19 + } 20 + const records = await ctx.db 21 + .select() 22 + .from(sandboxPorts) 23 + .leftJoin( 24 + schema.sandboxes, 25 + eq(sandboxPorts.sandboxId, schema.sandboxes.id), 26 + ) 27 + .leftJoin(schema.users, eq(schema.sandboxes.userId, schema.users.id)) 28 + .where( 29 + and( 30 + eq(sandboxPorts.sandboxId, params.id), 31 + eq(schema.users.did, auth.credentials.did), 32 + ), 33 + ) 34 + .execute(); 35 + return { 36 + ports: records.map((record) => ({ 37 + port: record.sandbox_ports.exposedPort, 38 + description: record.sandbox_ports.description || undefined, 39 + previewUrl: record.sandbox_ports.previewUrl || undefined, 40 + })), 41 + }; 42 + }; 43 + server.io.pocketenv.sandbox.getExposedPorts({ 44 + auth: ctx.authVerifier, 45 + handler: async ({ params, auth }) => { 46 + const result = await getExposedPorts(params, auth); 47 + return { 48 + encoding: "application/json", 49 + body: result satisfies OutputSchema, 50 + }; 51 + }, 52 + }); 53 + }
+71
apps/api/src/xrpc/io/pocketenv/sandbox/unexposePort.ts
··· 1 + import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 2 + import { Providers } from "consts"; 3 + import type { Context } from "context"; 4 + import { and, eq } from "drizzle-orm"; 5 + import type { Server } from "lexicon"; 6 + import type { 7 + InputSchema, 8 + QueryParams, 9 + } from "lexicon/types/io/pocketenv/sandbox/unexposePort"; 10 + import generateJwt from "lib/generateJwt"; 11 + import schema from "schema"; 12 + 13 + export default function (server: Server, ctx: Context) { 14 + const unexposePort = async ( 15 + params: QueryParams, 16 + input: InputSchema, 17 + auth: HandlerAuth, 18 + ) => { 19 + if (!auth.credentials) { 20 + throw new XRPCError(401, "Unauthorized"); 21 + } 22 + 23 + await ctx.db.transaction(async (tx) => { 24 + const [record] = await tx 25 + .select() 26 + .from(schema.sandboxes) 27 + .leftJoin(schema.users, eq(schema.sandboxes.userId, schema.users.id)) 28 + .where( 29 + and( 30 + eq(schema.sandboxes.id, params.id), 31 + eq(schema.users.did, auth.credentials.did), 32 + ), 33 + ) 34 + .execute(); 35 + 36 + if (!record) { 37 + throw new XRPCError(404, "Sandbox not found"); 38 + } 39 + 40 + await tx 41 + .delete(schema.sandboxPorts) 42 + .where( 43 + and( 44 + eq(schema.sandboxPorts.sandboxId, record.sandboxes.id), 45 + eq(schema.sandboxPorts.exposedPort, input.port), 46 + ), 47 + ) 48 + .execute(); 49 + 50 + const sandbox = 51 + record.sandboxes.provider === Providers.CLOUDFLARE 52 + ? ctx.cfsandbox(record.sandboxes.base!) 53 + : ctx.sandbox(); 54 + 55 + await sandbox.delete(`/v1/sandboxes/${record.sandboxes.id}/ports`, { 56 + params: { 57 + port: input.port, 58 + }, 59 + headers: { 60 + Authorization: `Bearer ${await generateJwt(auth?.credentials?.did || "")}`, 61 + }, 62 + }); 63 + }); 64 + }; 65 + server.io.pocketenv.sandbox.unexposePort({ 66 + auth: ctx.authVerifier, 67 + handler: async ({ params, input, auth }) => { 68 + await unexposePort(params, input.body, auth); 69 + }, 70 + }); 71 + }
+1
apps/cf-sandbox/drizzle/0029_magenta_joystick.sql
··· 1 + ALTER TABLE "sandbox_ports" ADD COLUMN "description" text;
+1409
apps/cf-sandbox/drizzle/meta/0029_snapshot.json
··· 1 + { 2 + "id": "6fc85d4f-c661-459e-9909-77c031b74742", 3 + "prevId": "a4eff7f3-c689-4a08-9bd3-81888d89de31", 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.files": { 61 + "name": "files", 62 + "schema": "", 63 + "columns": { 64 + "id": { 65 + "name": "id", 66 + "type": "text", 67 + "primaryKey": true, 68 + "notNull": true, 69 + "default": "xata_id()" 70 + }, 71 + "content": { 72 + "name": "content", 73 + "type": "text", 74 + "primaryKey": false, 75 + "notNull": true 76 + }, 77 + "created_at": { 78 + "name": "created_at", 79 + "type": "timestamp", 80 + "primaryKey": false, 81 + "notNull": true, 82 + "default": "now()" 83 + }, 84 + "updated_at": { 85 + "name": "updated_at", 86 + "type": "timestamp", 87 + "primaryKey": false, 88 + "notNull": true, 89 + "default": "now()" 90 + } 91 + }, 92 + "indexes": {}, 93 + "foreignKeys": {}, 94 + "compositePrimaryKeys": {}, 95 + "uniqueConstraints": {}, 96 + "policies": {}, 97 + "checkConstraints": {}, 98 + "isRLSEnabled": false 99 + }, 100 + "public.sandbox_files": { 101 + "name": "sandbox_files", 102 + "schema": "", 103 + "columns": { 104 + "id": { 105 + "name": "id", 106 + "type": "text", 107 + "primaryKey": true, 108 + "notNull": true, 109 + "default": "xata_id()" 110 + }, 111 + "sandbox_id": { 112 + "name": "sandbox_id", 113 + "type": "text", 114 + "primaryKey": false, 115 + "notNull": true 116 + }, 117 + "file_id": { 118 + "name": "file_id", 119 + "type": "text", 120 + "primaryKey": false, 121 + "notNull": true 122 + }, 123 + "path": { 124 + "name": "path", 125 + "type": "text", 126 + "primaryKey": false, 127 + "notNull": true 128 + }, 129 + "created_at": { 130 + "name": "created_at", 131 + "type": "timestamp", 132 + "primaryKey": false, 133 + "notNull": true, 134 + "default": "now()" 135 + }, 136 + "updated_at": { 137 + "name": "updated_at", 138 + "type": "timestamp", 139 + "primaryKey": false, 140 + "notNull": true, 141 + "default": "now()" 142 + } 143 + }, 144 + "indexes": { 145 + "unique_sandbox_file": { 146 + "name": "unique_sandbox_file", 147 + "columns": [ 148 + { 149 + "expression": "sandbox_id", 150 + "isExpression": false, 151 + "asc": true, 152 + "nulls": "last" 153 + }, 154 + { 155 + "expression": "file_id", 156 + "isExpression": false, 157 + "asc": true, 158 + "nulls": "last" 159 + } 160 + ], 161 + "isUnique": true, 162 + "concurrently": false, 163 + "method": "btree", 164 + "with": {} 165 + }, 166 + "unique_sandbox_file_path": { 167 + "name": "unique_sandbox_file_path", 168 + "columns": [ 169 + { 170 + "expression": "sandbox_id", 171 + "isExpression": false, 172 + "asc": true, 173 + "nulls": "last" 174 + }, 175 + { 176 + "expression": "path", 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_files_sandbox_id_sandboxes_id_fk": { 190 + "name": "sandbox_files_sandbox_id_sandboxes_id_fk", 191 + "tableFrom": "sandbox_files", 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_files_file_id_files_id_fk": { 203 + "name": "sandbox_files_file_id_files_id_fk", 204 + "tableFrom": "sandbox_files", 205 + "tableTo": "files", 206 + "columnsFrom": [ 207 + "file_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_ports": { 223 + "name": "sandbox_ports", 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 + "exposed_port": { 240 + "name": "exposed_port", 241 + "type": "integer", 242 + "primaryKey": false, 243 + "notNull": true 244 + }, 245 + "preview_url": { 246 + "name": "preview_url", 247 + "type": "text", 248 + "primaryKey": false, 249 + "notNull": false 250 + }, 251 + "description": { 252 + "name": "description", 253 + "type": "text", 254 + "primaryKey": false, 255 + "notNull": false 256 + }, 257 + "created_at": { 258 + "name": "created_at", 259 + "type": "timestamp", 260 + "primaryKey": false, 261 + "notNull": true, 262 + "default": "now()" 263 + }, 264 + "updated_at": { 265 + "name": "updated_at", 266 + "type": "timestamp", 267 + "primaryKey": false, 268 + "notNull": true, 269 + "default": "now()" 270 + } 271 + }, 272 + "indexes": { 273 + "unique_sandbox_port": { 274 + "name": "unique_sandbox_port", 275 + "columns": [ 276 + { 277 + "expression": "sandbox_id", 278 + "isExpression": false, 279 + "asc": true, 280 + "nulls": "last" 281 + }, 282 + { 283 + "expression": "exposed_port", 284 + "isExpression": false, 285 + "asc": true, 286 + "nulls": "last" 287 + } 288 + ], 289 + "isUnique": true, 290 + "concurrently": false, 291 + "method": "btree", 292 + "with": {} 293 + } 294 + }, 295 + "foreignKeys": { 296 + "sandbox_ports_sandbox_id_sandboxes_id_fk": { 297 + "name": "sandbox_ports_sandbox_id_sandboxes_id_fk", 298 + "tableFrom": "sandbox_ports", 299 + "tableTo": "sandboxes", 300 + "columnsFrom": [ 301 + "sandbox_id" 302 + ], 303 + "columnsTo": [ 304 + "id" 305 + ], 306 + "onDelete": "no action", 307 + "onUpdate": "no action" 308 + } 309 + }, 310 + "compositePrimaryKeys": {}, 311 + "uniqueConstraints": {}, 312 + "policies": {}, 313 + "checkConstraints": {}, 314 + "isRLSEnabled": false 315 + }, 316 + "public.sandbox_secrets": { 317 + "name": "sandbox_secrets", 318 + "schema": "", 319 + "columns": { 320 + "id": { 321 + "name": "id", 322 + "type": "text", 323 + "primaryKey": true, 324 + "notNull": true, 325 + "default": "xata_id()" 326 + }, 327 + "sandbox_id": { 328 + "name": "sandbox_id", 329 + "type": "text", 330 + "primaryKey": false, 331 + "notNull": true 332 + }, 333 + "secret_id": { 334 + "name": "secret_id", 335 + "type": "text", 336 + "primaryKey": false, 337 + "notNull": true 338 + }, 339 + "name": { 340 + "name": "name", 341 + "type": "text", 342 + "primaryKey": false, 343 + "notNull": false 344 + }, 345 + "created_at": { 346 + "name": "created_at", 347 + "type": "timestamp", 348 + "primaryKey": false, 349 + "notNull": true, 350 + "default": "now()" 351 + }, 352 + "updated_at": { 353 + "name": "updated_at", 354 + "type": "timestamp", 355 + "primaryKey": false, 356 + "notNull": true, 357 + "default": "now()" 358 + } 359 + }, 360 + "indexes": { 361 + "unique_sandbox_secret": { 362 + "name": "unique_sandbox_secret", 363 + "columns": [ 364 + { 365 + "expression": "sandbox_id", 366 + "isExpression": false, 367 + "asc": true, 368 + "nulls": "last" 369 + }, 370 + { 371 + "expression": "secret_id", 372 + "isExpression": false, 373 + "asc": true, 374 + "nulls": "last" 375 + } 376 + ], 377 + "isUnique": true, 378 + "concurrently": false, 379 + "method": "btree", 380 + "with": {} 381 + }, 382 + "unique_sandbox_secret_by_name": { 383 + "name": "unique_sandbox_secret_by_name", 384 + "columns": [ 385 + { 386 + "expression": "sandbox_id", 387 + "isExpression": false, 388 + "asc": true, 389 + "nulls": "last" 390 + }, 391 + { 392 + "expression": "name", 393 + "isExpression": false, 394 + "asc": true, 395 + "nulls": "last" 396 + } 397 + ], 398 + "isUnique": true, 399 + "concurrently": false, 400 + "method": "btree", 401 + "with": {} 402 + } 403 + }, 404 + "foreignKeys": { 405 + "sandbox_secrets_sandbox_id_sandboxes_id_fk": { 406 + "name": "sandbox_secrets_sandbox_id_sandboxes_id_fk", 407 + "tableFrom": "sandbox_secrets", 408 + "tableTo": "sandboxes", 409 + "columnsFrom": [ 410 + "sandbox_id" 411 + ], 412 + "columnsTo": [ 413 + "id" 414 + ], 415 + "onDelete": "no action", 416 + "onUpdate": "no action" 417 + }, 418 + "sandbox_secrets_secret_id_secrets_id_fk": { 419 + "name": "sandbox_secrets_secret_id_secrets_id_fk", 420 + "tableFrom": "sandbox_secrets", 421 + "tableTo": "secrets", 422 + "columnsFrom": [ 423 + "secret_id" 424 + ], 425 + "columnsTo": [ 426 + "id" 427 + ], 428 + "onDelete": "no action", 429 + "onUpdate": "no action" 430 + } 431 + }, 432 + "compositePrimaryKeys": {}, 433 + "uniqueConstraints": {}, 434 + "policies": {}, 435 + "checkConstraints": {}, 436 + "isRLSEnabled": false 437 + }, 438 + "public.sandbox_variables": { 439 + "name": "sandbox_variables", 440 + "schema": "", 441 + "columns": { 442 + "id": { 443 + "name": "id", 444 + "type": "text", 445 + "primaryKey": true, 446 + "notNull": true, 447 + "default": "xata_id()" 448 + }, 449 + "sandbox_id": { 450 + "name": "sandbox_id", 451 + "type": "text", 452 + "primaryKey": false, 453 + "notNull": true 454 + }, 455 + "variable_id": { 456 + "name": "variable_id", 457 + "type": "text", 458 + "primaryKey": false, 459 + "notNull": true 460 + }, 461 + "name": { 462 + "name": "name", 463 + "type": "text", 464 + "primaryKey": false, 465 + "notNull": true 466 + }, 467 + "created_at": { 468 + "name": "created_at", 469 + "type": "timestamp", 470 + "primaryKey": false, 471 + "notNull": true, 472 + "default": "now()" 473 + }, 474 + "updated_at": { 475 + "name": "updated_at", 476 + "type": "timestamp", 477 + "primaryKey": false, 478 + "notNull": true, 479 + "default": "now()" 480 + } 481 + }, 482 + "indexes": { 483 + "unique_sandbox_variables": { 484 + "name": "unique_sandbox_variables", 485 + "columns": [ 486 + { 487 + "expression": "sandbox_id", 488 + "isExpression": false, 489 + "asc": true, 490 + "nulls": "last" 491 + }, 492 + { 493 + "expression": "variable_id", 494 + "isExpression": false, 495 + "asc": true, 496 + "nulls": "last" 497 + } 498 + ], 499 + "isUnique": true, 500 + "concurrently": false, 501 + "method": "btree", 502 + "with": {} 503 + }, 504 + "unique_sandbox_variables_by_name": { 505 + "name": "unique_sandbox_variables_by_name", 506 + "columns": [ 507 + { 508 + "expression": "sandbox_id", 509 + "isExpression": false, 510 + "asc": true, 511 + "nulls": "last" 512 + }, 513 + { 514 + "expression": "name", 515 + "isExpression": false, 516 + "asc": true, 517 + "nulls": "last" 518 + } 519 + ], 520 + "isUnique": true, 521 + "concurrently": false, 522 + "method": "btree", 523 + "with": {} 524 + } 525 + }, 526 + "foreignKeys": { 527 + "sandbox_variables_sandbox_id_sandboxes_id_fk": { 528 + "name": "sandbox_variables_sandbox_id_sandboxes_id_fk", 529 + "tableFrom": "sandbox_variables", 530 + "tableTo": "sandboxes", 531 + "columnsFrom": [ 532 + "sandbox_id" 533 + ], 534 + "columnsTo": [ 535 + "id" 536 + ], 537 + "onDelete": "no action", 538 + "onUpdate": "no action" 539 + }, 540 + "sandbox_variables_variable_id_variables_id_fk": { 541 + "name": "sandbox_variables_variable_id_variables_id_fk", 542 + "tableFrom": "sandbox_variables", 543 + "tableTo": "variables", 544 + "columnsFrom": [ 545 + "variable_id" 546 + ], 547 + "columnsTo": [ 548 + "id" 549 + ], 550 + "onDelete": "no action", 551 + "onUpdate": "no action" 552 + } 553 + }, 554 + "compositePrimaryKeys": {}, 555 + "uniqueConstraints": {}, 556 + "policies": {}, 557 + "checkConstraints": {}, 558 + "isRLSEnabled": false 559 + }, 560 + "public.sandbox_volumes": { 561 + "name": "sandbox_volumes", 562 + "schema": "", 563 + "columns": { 564 + "id": { 565 + "name": "id", 566 + "type": "text", 567 + "primaryKey": true, 568 + "notNull": true, 569 + "default": "xata_id()" 570 + }, 571 + "sandbox_id": { 572 + "name": "sandbox_id", 573 + "type": "text", 574 + "primaryKey": false, 575 + "notNull": true 576 + }, 577 + "volume_id": { 578 + "name": "volume_id", 579 + "type": "text", 580 + "primaryKey": false, 581 + "notNull": true 582 + }, 583 + "name": { 584 + "name": "name", 585 + "type": "text", 586 + "primaryKey": false, 587 + "notNull": false 588 + }, 589 + "path": { 590 + "name": "path", 591 + "type": "text", 592 + "primaryKey": false, 593 + "notNull": true 594 + }, 595 + "created_at": { 596 + "name": "created_at", 597 + "type": "timestamp", 598 + "primaryKey": false, 599 + "notNull": true, 600 + "default": "now()" 601 + }, 602 + "updated_at": { 603 + "name": "updated_at", 604 + "type": "timestamp", 605 + "primaryKey": false, 606 + "notNull": true, 607 + "default": "now()" 608 + } 609 + }, 610 + "indexes": { 611 + "unique_sandbox_volume": { 612 + "name": "unique_sandbox_volume", 613 + "columns": [ 614 + { 615 + "expression": "sandbox_id", 616 + "isExpression": false, 617 + "asc": true, 618 + "nulls": "last" 619 + }, 620 + { 621 + "expression": "volume_id", 622 + "isExpression": false, 623 + "asc": true, 624 + "nulls": "last" 625 + } 626 + ], 627 + "isUnique": true, 628 + "concurrently": false, 629 + "method": "btree", 630 + "with": {} 631 + }, 632 + "unique_sandbox_volume_path": { 633 + "name": "unique_sandbox_volume_path", 634 + "columns": [ 635 + { 636 + "expression": "sandbox_id", 637 + "isExpression": false, 638 + "asc": true, 639 + "nulls": "last" 640 + }, 641 + { 642 + "expression": "path", 643 + "isExpression": false, 644 + "asc": true, 645 + "nulls": "last" 646 + } 647 + ], 648 + "isUnique": true, 649 + "concurrently": false, 650 + "method": "btree", 651 + "with": {} 652 + } 653 + }, 654 + "foreignKeys": { 655 + "sandbox_volumes_sandbox_id_sandboxes_id_fk": { 656 + "name": "sandbox_volumes_sandbox_id_sandboxes_id_fk", 657 + "tableFrom": "sandbox_volumes", 658 + "tableTo": "sandboxes", 659 + "columnsFrom": [ 660 + "sandbox_id" 661 + ], 662 + "columnsTo": [ 663 + "id" 664 + ], 665 + "onDelete": "no action", 666 + "onUpdate": "no action" 667 + }, 668 + "sandbox_volumes_volume_id_volumes_id_fk": { 669 + "name": "sandbox_volumes_volume_id_volumes_id_fk", 670 + "tableFrom": "sandbox_volumes", 671 + "tableTo": "volumes", 672 + "columnsFrom": [ 673 + "volume_id" 674 + ], 675 + "columnsTo": [ 676 + "id" 677 + ], 678 + "onDelete": "no action", 679 + "onUpdate": "no action" 680 + } 681 + }, 682 + "compositePrimaryKeys": {}, 683 + "uniqueConstraints": {}, 684 + "policies": {}, 685 + "checkConstraints": {}, 686 + "isRLSEnabled": false 687 + }, 688 + "public.sandboxes": { 689 + "name": "sandboxes", 690 + "schema": "", 691 + "columns": { 692 + "id": { 693 + "name": "id", 694 + "type": "text", 695 + "primaryKey": true, 696 + "notNull": true, 697 + "default": "sandbox_id()" 698 + }, 699 + "base": { 700 + "name": "base", 701 + "type": "text", 702 + "primaryKey": false, 703 + "notNull": false 704 + }, 705 + "name": { 706 + "name": "name", 707 + "type": "text", 708 + "primaryKey": false, 709 + "notNull": true 710 + }, 711 + "display_name": { 712 + "name": "display_name", 713 + "type": "text", 714 + "primaryKey": false, 715 + "notNull": false 716 + }, 717 + "uri": { 718 + "name": "uri", 719 + "type": "text", 720 + "primaryKey": false, 721 + "notNull": false 722 + }, 723 + "cid": { 724 + "name": "cid", 725 + "type": "text", 726 + "primaryKey": false, 727 + "notNull": false 728 + }, 729 + "repo": { 730 + "name": "repo", 731 + "type": "text", 732 + "primaryKey": false, 733 + "notNull": false 734 + }, 735 + "provider": { 736 + "name": "provider", 737 + "type": "text", 738 + "primaryKey": false, 739 + "notNull": true, 740 + "default": "'cloudflare'" 741 + }, 742 + "description": { 743 + "name": "description", 744 + "type": "text", 745 + "primaryKey": false, 746 + "notNull": false 747 + }, 748 + "topics": { 749 + "name": "topics", 750 + "type": "text[]", 751 + "primaryKey": false, 752 + "notNull": false 753 + }, 754 + "logo": { 755 + "name": "logo", 756 + "type": "text", 757 + "primaryKey": false, 758 + "notNull": false 759 + }, 760 + "readme": { 761 + "name": "readme", 762 + "type": "text", 763 + "primaryKey": false, 764 + "notNull": false 765 + }, 766 + "public_key": { 767 + "name": "public_key", 768 + "type": "text", 769 + "primaryKey": false, 770 + "notNull": true 771 + }, 772 + "user_id": { 773 + "name": "user_id", 774 + "type": "text", 775 + "primaryKey": false, 776 + "notNull": false 777 + }, 778 + "instance_type": { 779 + "name": "instance_type", 780 + "type": "text", 781 + "primaryKey": false, 782 + "notNull": false 783 + }, 784 + "vcpus": { 785 + "name": "vcpus", 786 + "type": "integer", 787 + "primaryKey": false, 788 + "notNull": false 789 + }, 790 + "memory": { 791 + "name": "memory", 792 + "type": "integer", 793 + "primaryKey": false, 794 + "notNull": false 795 + }, 796 + "disk": { 797 + "name": "disk", 798 + "type": "integer", 799 + "primaryKey": false, 800 + "notNull": false 801 + }, 802 + "status": { 803 + "name": "status", 804 + "type": "text", 805 + "primaryKey": false, 806 + "notNull": true 807 + }, 808 + "keep_alive": { 809 + "name": "keep_alive", 810 + "type": "boolean", 811 + "primaryKey": false, 812 + "notNull": true, 813 + "default": false 814 + }, 815 + "sleep_after": { 816 + "name": "sleep_after", 817 + "type": "text", 818 + "primaryKey": false, 819 + "notNull": false 820 + }, 821 + "sandbox_id": { 822 + "name": "sandbox_id", 823 + "type": "text", 824 + "primaryKey": false, 825 + "notNull": false 826 + }, 827 + "installs": { 828 + "name": "installs", 829 + "type": "integer", 830 + "primaryKey": false, 831 + "notNull": true, 832 + "default": 0 833 + }, 834 + "started_at": { 835 + "name": "started_at", 836 + "type": "timestamp", 837 + "primaryKey": false, 838 + "notNull": false 839 + }, 840 + "created_at": { 841 + "name": "created_at", 842 + "type": "timestamp", 843 + "primaryKey": false, 844 + "notNull": true, 845 + "default": "now()" 846 + }, 847 + "updated_at": { 848 + "name": "updated_at", 849 + "type": "timestamp", 850 + "primaryKey": false, 851 + "notNull": true, 852 + "default": "now()" 853 + } 854 + }, 855 + "indexes": {}, 856 + "foreignKeys": { 857 + "sandboxes_user_id_users_id_fk": { 858 + "name": "sandboxes_user_id_users_id_fk", 859 + "tableFrom": "sandboxes", 860 + "tableTo": "users", 861 + "columnsFrom": [ 862 + "user_id" 863 + ], 864 + "columnsTo": [ 865 + "id" 866 + ], 867 + "onDelete": "no action", 868 + "onUpdate": "no action" 869 + } 870 + }, 871 + "compositePrimaryKeys": {}, 872 + "uniqueConstraints": { 873 + "sandboxes_name_unique": { 874 + "name": "sandboxes_name_unique", 875 + "nullsNotDistinct": false, 876 + "columns": [ 877 + "name" 878 + ] 879 + }, 880 + "sandboxes_uri_unique": { 881 + "name": "sandboxes_uri_unique", 882 + "nullsNotDistinct": false, 883 + "columns": [ 884 + "uri" 885 + ] 886 + }, 887 + "sandboxes_cid_unique": { 888 + "name": "sandboxes_cid_unique", 889 + "nullsNotDistinct": false, 890 + "columns": [ 891 + "cid" 892 + ] 893 + } 894 + }, 895 + "policies": {}, 896 + "checkConstraints": {}, 897 + "isRLSEnabled": false 898 + }, 899 + "public.secrets": { 900 + "name": "secrets", 901 + "schema": "", 902 + "columns": { 903 + "id": { 904 + "name": "id", 905 + "type": "text", 906 + "primaryKey": true, 907 + "notNull": true, 908 + "default": "secret_id()" 909 + }, 910 + "name": { 911 + "name": "name", 912 + "type": "text", 913 + "primaryKey": false, 914 + "notNull": true 915 + }, 916 + "value": { 917 + "name": "value", 918 + "type": "text", 919 + "primaryKey": false, 920 + "notNull": true 921 + }, 922 + "redacted": { 923 + "name": "redacted", 924 + "type": "text", 925 + "primaryKey": false, 926 + "notNull": false 927 + }, 928 + "created_at": { 929 + "name": "created_at", 930 + "type": "timestamp", 931 + "primaryKey": false, 932 + "notNull": true, 933 + "default": "now()" 934 + } 935 + }, 936 + "indexes": {}, 937 + "foreignKeys": {}, 938 + "compositePrimaryKeys": {}, 939 + "uniqueConstraints": {}, 940 + "policies": {}, 941 + "checkConstraints": {}, 942 + "isRLSEnabled": false 943 + }, 944 + "public.snapshots": { 945 + "name": "snapshots", 946 + "schema": "", 947 + "columns": { 948 + "id": { 949 + "name": "id", 950 + "type": "text", 951 + "primaryKey": true, 952 + "notNull": true, 953 + "default": "snapshot_id()" 954 + }, 955 + "slug": { 956 + "name": "slug", 957 + "type": "text", 958 + "primaryKey": false, 959 + "notNull": true 960 + }, 961 + "created_at": { 962 + "name": "created_at", 963 + "type": "timestamp", 964 + "primaryKey": false, 965 + "notNull": true, 966 + "default": "now()" 967 + } 968 + }, 969 + "indexes": {}, 970 + "foreignKeys": {}, 971 + "compositePrimaryKeys": {}, 972 + "uniqueConstraints": { 973 + "snapshots_slug_unique": { 974 + "name": "snapshots_slug_unique", 975 + "nullsNotDistinct": false, 976 + "columns": [ 977 + "slug" 978 + ] 979 + } 980 + }, 981 + "policies": {}, 982 + "checkConstraints": {}, 983 + "isRLSEnabled": false 984 + }, 985 + "public.ssh_keys": { 986 + "name": "ssh_keys", 987 + "schema": "", 988 + "columns": { 989 + "id": { 990 + "name": "id", 991 + "type": "text", 992 + "primaryKey": true, 993 + "notNull": true, 994 + "default": "xata_id()" 995 + }, 996 + "sandbox_id": { 997 + "name": "sandbox_id", 998 + "type": "text", 999 + "primaryKey": false, 1000 + "notNull": true 1001 + }, 1002 + "public_key": { 1003 + "name": "public_key", 1004 + "type": "text", 1005 + "primaryKey": false, 1006 + "notNull": true 1007 + }, 1008 + "private_key": { 1009 + "name": "private_key", 1010 + "type": "text", 1011 + "primaryKey": false, 1012 + "notNull": true 1013 + }, 1014 + "redacted": { 1015 + "name": "redacted", 1016 + "type": "text", 1017 + "primaryKey": false, 1018 + "notNull": false 1019 + }, 1020 + "created_at": { 1021 + "name": "created_at", 1022 + "type": "timestamp", 1023 + "primaryKey": false, 1024 + "notNull": true, 1025 + "default": "now()" 1026 + } 1027 + }, 1028 + "indexes": { 1029 + "unique_sandbox_ssh_key": { 1030 + "name": "unique_sandbox_ssh_key", 1031 + "columns": [ 1032 + { 1033 + "expression": "public_key", 1034 + "isExpression": false, 1035 + "asc": true, 1036 + "nulls": "last" 1037 + }, 1038 + { 1039 + "expression": "sandbox_id", 1040 + "isExpression": false, 1041 + "asc": true, 1042 + "nulls": "last" 1043 + } 1044 + ], 1045 + "isUnique": true, 1046 + "concurrently": false, 1047 + "method": "btree", 1048 + "with": {} 1049 + } 1050 + }, 1051 + "foreignKeys": { 1052 + "ssh_keys_sandbox_id_sandboxes_id_fk": { 1053 + "name": "ssh_keys_sandbox_id_sandboxes_id_fk", 1054 + "tableFrom": "ssh_keys", 1055 + "tableTo": "sandboxes", 1056 + "columnsFrom": [ 1057 + "sandbox_id" 1058 + ], 1059 + "columnsTo": [ 1060 + "id" 1061 + ], 1062 + "onDelete": "no action", 1063 + "onUpdate": "no action" 1064 + } 1065 + }, 1066 + "compositePrimaryKeys": {}, 1067 + "uniqueConstraints": {}, 1068 + "policies": {}, 1069 + "checkConstraints": {}, 1070 + "isRLSEnabled": false 1071 + }, 1072 + "public.tailscale_auth_keys": { 1073 + "name": "tailscale_auth_keys", 1074 + "schema": "", 1075 + "columns": { 1076 + "id": { 1077 + "name": "id", 1078 + "type": "text", 1079 + "primaryKey": true, 1080 + "notNull": true, 1081 + "default": "xata_id()" 1082 + }, 1083 + "sandbox_id": { 1084 + "name": "sandbox_id", 1085 + "type": "text", 1086 + "primaryKey": false, 1087 + "notNull": true 1088 + }, 1089 + "auth_key": { 1090 + "name": "auth_key", 1091 + "type": "text", 1092 + "primaryKey": false, 1093 + "notNull": true 1094 + }, 1095 + "redacted": { 1096 + "name": "redacted", 1097 + "type": "text", 1098 + "primaryKey": false, 1099 + "notNull": true 1100 + }, 1101 + "created_at": { 1102 + "name": "created_at", 1103 + "type": "timestamp", 1104 + "primaryKey": false, 1105 + "notNull": true, 1106 + "default": "now()" 1107 + } 1108 + }, 1109 + "indexes": {}, 1110 + "foreignKeys": { 1111 + "tailscale_auth_keys_sandbox_id_sandboxes_id_fk": { 1112 + "name": "tailscale_auth_keys_sandbox_id_sandboxes_id_fk", 1113 + "tableFrom": "tailscale_auth_keys", 1114 + "tableTo": "sandboxes", 1115 + "columnsFrom": [ 1116 + "sandbox_id" 1117 + ], 1118 + "columnsTo": [ 1119 + "id" 1120 + ], 1121 + "onDelete": "no action", 1122 + "onUpdate": "no action" 1123 + } 1124 + }, 1125 + "compositePrimaryKeys": {}, 1126 + "uniqueConstraints": {}, 1127 + "policies": {}, 1128 + "checkConstraints": {}, 1129 + "isRLSEnabled": false 1130 + }, 1131 + "public.users": { 1132 + "name": "users", 1133 + "schema": "", 1134 + "columns": { 1135 + "id": { 1136 + "name": "id", 1137 + "type": "text", 1138 + "primaryKey": true, 1139 + "notNull": true, 1140 + "default": "xata_id()" 1141 + }, 1142 + "did": { 1143 + "name": "did", 1144 + "type": "text", 1145 + "primaryKey": false, 1146 + "notNull": true 1147 + }, 1148 + "display_name": { 1149 + "name": "display_name", 1150 + "type": "text", 1151 + "primaryKey": false, 1152 + "notNull": false 1153 + }, 1154 + "handle": { 1155 + "name": "handle", 1156 + "type": "text", 1157 + "primaryKey": false, 1158 + "notNull": true 1159 + }, 1160 + "avatar": { 1161 + "name": "avatar", 1162 + "type": "text", 1163 + "primaryKey": false, 1164 + "notNull": false 1165 + }, 1166 + "created_at": { 1167 + "name": "created_at", 1168 + "type": "timestamp", 1169 + "primaryKey": false, 1170 + "notNull": true, 1171 + "default": "now()" 1172 + }, 1173 + "updated_at": { 1174 + "name": "updated_at", 1175 + "type": "timestamp", 1176 + "primaryKey": false, 1177 + "notNull": true, 1178 + "default": "now()" 1179 + } 1180 + }, 1181 + "indexes": {}, 1182 + "foreignKeys": {}, 1183 + "compositePrimaryKeys": {}, 1184 + "uniqueConstraints": { 1185 + "users_did_unique": { 1186 + "name": "users_did_unique", 1187 + "nullsNotDistinct": false, 1188 + "columns": [ 1189 + "did" 1190 + ] 1191 + }, 1192 + "users_handle_unique": { 1193 + "name": "users_handle_unique", 1194 + "nullsNotDistinct": false, 1195 + "columns": [ 1196 + "handle" 1197 + ] 1198 + } 1199 + }, 1200 + "policies": {}, 1201 + "checkConstraints": {}, 1202 + "isRLSEnabled": false 1203 + }, 1204 + "public.variables": { 1205 + "name": "variables", 1206 + "schema": "", 1207 + "columns": { 1208 + "id": { 1209 + "name": "id", 1210 + "type": "text", 1211 + "primaryKey": true, 1212 + "notNull": true, 1213 + "default": "variable_id()" 1214 + }, 1215 + "name": { 1216 + "name": "name", 1217 + "type": "text", 1218 + "primaryKey": false, 1219 + "notNull": true 1220 + }, 1221 + "value": { 1222 + "name": "value", 1223 + "type": "text", 1224 + "primaryKey": false, 1225 + "notNull": true 1226 + }, 1227 + "created_at": { 1228 + "name": "created_at", 1229 + "type": "timestamp", 1230 + "primaryKey": false, 1231 + "notNull": true, 1232 + "default": "now()" 1233 + }, 1234 + "updated_at": { 1235 + "name": "updated_at", 1236 + "type": "timestamp", 1237 + "primaryKey": false, 1238 + "notNull": true, 1239 + "default": "now()" 1240 + } 1241 + }, 1242 + "indexes": {}, 1243 + "foreignKeys": {}, 1244 + "compositePrimaryKeys": {}, 1245 + "uniqueConstraints": {}, 1246 + "policies": {}, 1247 + "checkConstraints": {}, 1248 + "isRLSEnabled": false 1249 + }, 1250 + "public.volumes": { 1251 + "name": "volumes", 1252 + "schema": "", 1253 + "columns": { 1254 + "id": { 1255 + "name": "id", 1256 + "type": "text", 1257 + "primaryKey": true, 1258 + "notNull": true, 1259 + "default": "volume_id()" 1260 + }, 1261 + "slug": { 1262 + "name": "slug", 1263 + "type": "text", 1264 + "primaryKey": false, 1265 + "notNull": true 1266 + }, 1267 + "size": { 1268 + "name": "size", 1269 + "type": "integer", 1270 + "primaryKey": false, 1271 + "notNull": true 1272 + }, 1273 + "size_unit": { 1274 + "name": "size_unit", 1275 + "type": "text", 1276 + "primaryKey": false, 1277 + "notNull": true 1278 + }, 1279 + "created_at": { 1280 + "name": "created_at", 1281 + "type": "timestamp", 1282 + "primaryKey": false, 1283 + "notNull": true, 1284 + "default": "now()" 1285 + }, 1286 + "updated_at": { 1287 + "name": "updated_at", 1288 + "type": "timestamp", 1289 + "primaryKey": false, 1290 + "notNull": true, 1291 + "default": "now()" 1292 + } 1293 + }, 1294 + "indexes": {}, 1295 + "foreignKeys": {}, 1296 + "compositePrimaryKeys": {}, 1297 + "uniqueConstraints": { 1298 + "volumes_slug_unique": { 1299 + "name": "volumes_slug_unique", 1300 + "nullsNotDistinct": false, 1301 + "columns": [ 1302 + "slug" 1303 + ] 1304 + } 1305 + }, 1306 + "policies": {}, 1307 + "checkConstraints": {}, 1308 + "isRLSEnabled": false 1309 + }, 1310 + "public.integrations": { 1311 + "name": "integrations", 1312 + "schema": "", 1313 + "columns": { 1314 + "id": { 1315 + "name": "id", 1316 + "type": "text", 1317 + "primaryKey": true, 1318 + "notNull": true, 1319 + "default": "xata_id()" 1320 + }, 1321 + "sandbox_id": { 1322 + "name": "sandbox_id", 1323 + "type": "text", 1324 + "primaryKey": false, 1325 + "notNull": true 1326 + }, 1327 + "name": { 1328 + "name": "name", 1329 + "type": "text", 1330 + "primaryKey": false, 1331 + "notNull": true 1332 + }, 1333 + "description": { 1334 + "name": "description", 1335 + "type": "text", 1336 + "primaryKey": false, 1337 + "notNull": false 1338 + }, 1339 + "webhook_url": { 1340 + "name": "webhook_url", 1341 + "type": "text", 1342 + "primaryKey": false, 1343 + "notNull": true 1344 + }, 1345 + "created_at": { 1346 + "name": "created_at", 1347 + "type": "timestamp", 1348 + "primaryKey": false, 1349 + "notNull": true, 1350 + "default": "now()" 1351 + } 1352 + }, 1353 + "indexes": { 1354 + "unique_sandbox_integration": { 1355 + "name": "unique_sandbox_integration", 1356 + "columns": [ 1357 + { 1358 + "expression": "sandbox_id", 1359 + "isExpression": false, 1360 + "asc": true, 1361 + "nulls": "last" 1362 + }, 1363 + { 1364 + "expression": "name", 1365 + "isExpression": false, 1366 + "asc": true, 1367 + "nulls": "last" 1368 + } 1369 + ], 1370 + "isUnique": true, 1371 + "concurrently": false, 1372 + "method": "btree", 1373 + "with": {} 1374 + } 1375 + }, 1376 + "foreignKeys": { 1377 + "integrations_sandbox_id_sandboxes_id_fk": { 1378 + "name": "integrations_sandbox_id_sandboxes_id_fk", 1379 + "tableFrom": "integrations", 1380 + "tableTo": "sandboxes", 1381 + "columnsFrom": [ 1382 + "sandbox_id" 1383 + ], 1384 + "columnsTo": [ 1385 + "id" 1386 + ], 1387 + "onDelete": "no action", 1388 + "onUpdate": "no action" 1389 + } 1390 + }, 1391 + "compositePrimaryKeys": {}, 1392 + "uniqueConstraints": {}, 1393 + "policies": {}, 1394 + "checkConstraints": {}, 1395 + "isRLSEnabled": false 1396 + } 1397 + }, 1398 + "enums": {}, 1399 + "schemas": {}, 1400 + "sequences": {}, 1401 + "roles": {}, 1402 + "policies": {}, 1403 + "views": {}, 1404 + "_meta": { 1405 + "columns": {}, 1406 + "schemas": {}, 1407 + "tables": {} 1408 + } 1409 + }
+7
apps/cf-sandbox/drizzle/meta/_journal.json
··· 204 204 "when": 1773901834037, 205 205 "tag": "0028_quiet_korvac", 206 206 "breakpoints": true 207 + }, 208 + { 209 + "idx": 29, 210 + "version": "7", 211 + "when": 1773977053348, 212 + "tag": "0029_magenta_joystick", 213 + "breakpoints": true 207 214 } 208 215 ] 209 216 }
+80
apps/cf-sandbox/src/index.ts
··· 686 686 } 687 687 }); 688 688 689 + app.post("/v1/sandboxes/:sandboxId/ports", async (c) => { 690 + const { sandboxes: record } = await getSandboxById( 691 + c.var.db, 692 + c.req.param("sandboxId"), 693 + ); 694 + 695 + if (!record) { 696 + return c.json({ error: "Sandbox not found" }, 404); 697 + } 698 + 699 + if (record.provider !== "cloudflare") { 700 + return c.json({ error: "Sandbox provider not supported" }, 400); 701 + } 702 + 703 + try { 704 + let sandbox: BaseSandbox | null = null; 705 + 706 + sandbox = await createSandbox("cloudflare", { 707 + id: record.sandboxId ?? c.req.param("sandboxId"), 708 + }); 709 + 710 + const { port } = await c.req.json<{ port: number }>(); 711 + 712 + if (!port || port <= 0 || port > 65535) { 713 + return c.json({ error: "Invalid port number" }, 400); 714 + } 715 + 716 + const { hostname } = new URL(c.req.url); 717 + const previewUrl = await sandbox.expose(port, hostname); 718 + return c.json({ previewUrl }); 719 + } catch (err) { 720 + const errorMessage = err instanceof Error ? err.message : "Unknown error"; 721 + consola.error( 722 + c.req.param("sandboxId"), 723 + "Failed to expose port:", 724 + errorMessage, 725 + ); 726 + return c.json({ error: `Failed to expose port: ${errorMessage}` }, 500); 727 + } 728 + }); 729 + 730 + app.delete("/v1/sandboxes/:sandboxId/ports", async (c) => { 731 + const { sandboxes: record } = await getSandboxById( 732 + c.var.db, 733 + c.req.param("sandboxId"), 734 + ); 735 + 736 + if (!record) { 737 + return c.json({ error: "Sandbox not found" }, 404); 738 + } 739 + 740 + if (record.provider !== "cloudflare") { 741 + return c.json({ error: "Sandbox provider not supported" }, 400); 742 + } 743 + 744 + try { 745 + let sandbox: BaseSandbox | null = null; 746 + 747 + sandbox = await createSandbox("cloudflare", { 748 + id: record.sandboxId ?? c.req.param("sandboxId"), 749 + }); 750 + 751 + const port = parseInt(c.req.query("port") || "0", 10); 752 + 753 + if (!port || port <= 0 || port > 65535) { 754 + return c.json({ error: "Invalid port number" }, 400); 755 + } 756 + 757 + await sandbox.unexpose(port); 758 + } catch (err) { 759 + const errorMessage = err instanceof Error ? err.message : "Unknown error"; 760 + consola.error( 761 + c.req.param("sandboxId"), 762 + "Failed to unexpose port:", 763 + errorMessage, 764 + ); 765 + return c.json({ error: `Failed to unexpose port: ${errorMessage}` }, 500); 766 + } 767 + }); 768 + 689 769 export const getSandboxById = async (db: Context["db"], sandboxId: string) => { 690 770 const [record] = await db 691 771 .select()
+9
apps/cf-sandbox/src/providers/cloudflare/index.ts
··· 119 119 unmount(path: string): Promise<void> { 120 120 return this.sandbox.unmountBucket(path); 121 121 } 122 + 123 + async expose(port: number, hostname: string): Promise<string> { 124 + const { url } = await this.sandbox.exposePort(port, { hostname }); 125 + return url; 126 + } 127 + 128 + async unexpose(port: number): Promise<void> { 129 + await this.sandbox.unexposePort(port); 130 + } 122 131 } 123 132 124 133 class CloudflareProvider implements BaseProvider {
+2
apps/cf-sandbox/src/providers/index.ts
··· 12 12 abstract clone(repoUrl: string): Promise<void>; 13 13 abstract mount(path: string, prefix?: string): Promise<void>; 14 14 abstract unmount(path: string): Promise<void>; 15 + abstract expose(port: number, hostname: string): Promise<string>; 16 + abstract unexpose(port: number): Promise<void>; 15 17 } 16 18 17 19 abstract class BaseProvider {
+1
apps/cf-sandbox/src/schema/sandbox-ports.ts
··· 19 19 .references(() => sandboxes.id), 20 20 exposedPort: integer("exposed_port").notNull(), 21 21 previewUrl: text("preview_url"), 22 + description: text("description"), 22 23 createdAt: timestamp("created_at").defaultNow().notNull(), 23 24 updatedAt: timestamp("updated_at").defaultNow().notNull(), 24 25 },
+10
apps/sandbox/src/index.ts
··· 393 393 return c.json(await sandbox.ssh()); 394 394 }); 395 395 396 + app.post("/v1/sandboxes/:sandboxId/ports", async (c) => { 397 + // TODO: Implement expose port 398 + return c.json({}); 399 + }); 400 + 401 + app.delete("/v1/sandboxes/:sandboxId/ports", async (c) => { 402 + // TODO: Implement unexpose port 403 + return c.json({}); 404 + }); 405 + 396 406 export const getSandbox = async (db: Context["db"], sandboxId: string) => { 397 407 const [record] = await db 398 408 .select()
+1
apps/sandbox/src/schema/sandbox-ports.ts
··· 19 19 .references(() => sandboxes.id), 20 20 exposedPort: integer("exposed_port").notNull(), 21 21 previewUrl: text("preview_url"), 22 + description: text("description"), 22 23 createdAt: timestamp("created_at").defaultNow().notNull(), 23 24 updatedAt: timestamp("updated_at").defaultNow().notNull(), 24 25 },
+48
apps/web/src/api/sandbox.ts
··· 1 1 import { client } from "."; 2 + import type { Port } from "../types/port"; 2 3 import type { Provider } from "../types/providers"; 3 4 import type { Sandbox } from "../types/sandbox"; 4 5 ··· 113 114 Authorization: `Bearer ${localStorage.getItem("token")}`, 114 115 }, 115 116 }); 117 + 118 + export const exposePort = ( 119 + id: string, 120 + { 121 + port, 122 + description, 123 + }: { 124 + port: number; 125 + description?: string; 126 + }, 127 + ) => 128 + client.post( 129 + `/xrpc/io.pocketenv.sandbox.exposePort`, 130 + { port, description }, 131 + { 132 + params: { 133 + id, 134 + }, 135 + headers: { 136 + Authorization: `Bearer ${localStorage.getItem("token")}`, 137 + }, 138 + }, 139 + ); 140 + 141 + export const unexposePort = (id: string, port: number) => 142 + client.post( 143 + `/xrpc/io.pocketenv.sandbox.unexposePort`, 144 + { port }, 145 + { 146 + params: { 147 + id, 148 + }, 149 + headers: { 150 + Authorization: `Bearer ${localStorage.getItem("token")}`, 151 + }, 152 + }, 153 + ); 154 + 155 + export const getExposedPorts = (id: string) => 156 + client.get<{ ports: Port[] }>(`/xrpc/io.pocketenv.sandbox.getExposedPorts`, { 157 + params: { 158 + id, 159 + }, 160 + headers: { 161 + Authorization: `Bearer ${localStorage.getItem("token")}`, 162 + }, 163 + });
+21
apps/web/src/components/contextmenu/ContextMenu.tsx
··· 5 5 import DeleteSandboxModal from "./DeleteSandboxModal"; 6 6 import AddSecretModal from "./AddSecretModal"; 7 7 import { Link } from "@tanstack/react-router"; 8 + import ExposePortModal from "./ExposePortModal"; 8 9 9 10 type ContextMenuProps = { 10 11 sandboxId: string; ··· 23 24 const [isAddFileModalOpen, setIsAddFileModalOpen] = useState(false); 24 25 const [isAddSecretModalOpen, setIsAddSecretModalOpen] = useState(false); 25 26 const [isAddVolumeModalOpen, setIsAddVolumeModalOpen] = useState(false); 27 + const [isExposePortModalOpen, setIsExposePortModalOpen] = useState(false); 26 28 27 29 useEffect(() => { 28 30 if (!open) return; ··· 69 71 setIsAddVolumeModalOpen(true); 70 72 }; 71 73 74 + const onExposePort = (e: React.MouseEvent) => { 75 + e.stopPropagation(); 76 + setOpen(false); 77 + setIsExposePortModalOpen(true); 78 + }; 79 + 72 80 const onDelete = (e: React.MouseEvent) => { 73 81 e.stopPropagation(); 74 82 setOpen(false); ··· 125 133 </div> 126 134 </li> 127 135 <li> 136 + <div 137 + className="dropdown-item cursor-pointer" 138 + onClick={onExposePort} 139 + > 140 + Expose Port 141 + </div> 142 + </li> 143 + <li> 128 144 <Link 129 145 to={`/${uri?.split("at://")[1]?.replace("io.pocketenv.", "")}/settings`} 130 146 className="dropdown-item cursor-pointer" ··· 157 173 <AddSecretModal 158 174 isOpen={isAddSecretModalOpen} 159 175 onClose={() => setIsAddSecretModalOpen(false)} 176 + sandboxId={sandboxId} 177 + /> 178 + <ExposePortModal 179 + isOpen={isExposePortModalOpen} 180 + onClose={() => setIsExposePortModalOpen(false)} 160 181 sandboxId={sandboxId} 161 182 /> 162 183 <DeleteSandboxModal
+185
apps/web/src/components/contextmenu/ExposePortModal/ExposePortMosal.tsx
··· 1 + import { createPortal } from "react-dom"; 2 + import { useForm } from "react-hook-form"; 3 + import { z } from "zod"; 4 + import { zodResolver } from "@hookform/resolvers/zod"; 5 + import { useEffect } from "react"; 6 + import { useExposePortMutation } from "../../../hooks/useSandbox"; 7 + 8 + const schema = z.object({ 9 + port: z.coerce 10 + .number({ error: "Port is required" }) 11 + .int() 12 + .min(1, "Port must be between 1 and 65535") 13 + .max(65535, "Port must be between 1 and 65535"), 14 + description: z.string(), 15 + }); 16 + 17 + export type ExposePortModalProps = { 18 + isOpen: boolean; 19 + onClose: () => void; 20 + sandboxId: string; 21 + portId?: string; 22 + }; 23 + 24 + function ExposePortModal({ isOpen, onClose, sandboxId }: ExposePortModalProps) { 25 + const { mutateAsync: exposePort, isPending: isLoading } = 26 + useExposePortMutation(); 27 + const { 28 + register, 29 + handleSubmit, 30 + reset, 31 + formState: { errors }, 32 + } = useForm({ 33 + resolver: zodResolver(schema), 34 + }); 35 + 36 + const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => { 37 + e.stopPropagation(); 38 + if (e.target === e.currentTarget) { 39 + reset(); 40 + onClose(); 41 + } 42 + }; 43 + 44 + const handleContentClick = (e: React.MouseEvent<HTMLDivElement>) => { 45 + e.stopPropagation(); 46 + }; 47 + 48 + const handleCloseButton = (e: React.MouseEvent<HTMLButtonElement>) => { 49 + e.stopPropagation(); 50 + reset(); 51 + onClose(); 52 + }; 53 + 54 + const onSubmit = async (data: z.infer<typeof schema>) => { 55 + await exposePort({ 56 + id: sandboxId, 57 + port: data.port, 58 + description: data.description || undefined, 59 + }); 60 + reset(); 61 + onClose(); 62 + }; 63 + 64 + useEffect(() => { 65 + const handleEscapeKey = (event: KeyboardEvent) => { 66 + if (event.key === "Escape" && isOpen) { 67 + reset(); 68 + onClose(); 69 + } 70 + }; 71 + 72 + document.addEventListener("keydown", handleEscapeKey); 73 + return () => { 74 + document.removeEventListener("keydown", handleEscapeKey); 75 + }; 76 + }, [isOpen, onClose, reset]); 77 + 78 + if (!isOpen) return null; 79 + 80 + return createPortal( 81 + <> 82 + <div 83 + className="overlay modal modal-middle overlay-open:opacity-100 overlay-open:duration-300 open opened" 84 + role="dialog" 85 + style={{ outline: "none", zIndex: 80 }} 86 + onClick={handleBackdropClick} 87 + onMouseDown={handleBackdropClick} 88 + > 89 + <div 90 + className={`overlay-animation-target modal-dialog overlay-open:duration-300 transition-all ease-out modal-dialog-lg overlay-open:mt-4 mt-12`} 91 + onClick={handleContentClick} 92 + onMouseDown={handleContentClick} 93 + > 94 + <div className="modal-content"> 95 + <div className="modal-header"> 96 + <div className="flex-1">Expose Port</div> 97 + <button 98 + type="button" 99 + className="btn btn-text btn-circle btn-sm absolute end-3 top-3" 100 + aria-label="Close" 101 + onClick={handleCloseButton} 102 + onMouseDown={(e) => e.stopPropagation()} 103 + > 104 + <span className="icon-[tabler--x] size-4"></span> 105 + </button> 106 + </div> 107 + <form onSubmit={handleSubmit(onSubmit)}> 108 + <div className="modal-body"> 109 + <div className="form-control w-full"> 110 + <label className="label"> 111 + <span className="label-text font-bold mb-1 text-[14px]"> 112 + Port 113 + </span> 114 + </label> 115 + <div 116 + className={`input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent`} 117 + > 118 + <input 119 + type="text" 120 + placeholder="Port Number" 121 + className={`grow`} 122 + autoComplete="off" 123 + data-1p-ignore 124 + data-lpignore="true" 125 + data-form-type="other" 126 + style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }} 127 + {...register("port")} 128 + /> 129 + </div> 130 + {errors.port && ( 131 + <span className="text-error text-[12px] mt-1"> 132 + {errors.port.message} 133 + </span> 134 + )} 135 + <div className="mt-5"> 136 + <label className="label"> 137 + <span className="label-text font-bold mb-1 text-[14px]"> 138 + Description 139 + </span> 140 + </label> 141 + <textarea 142 + className={`textarea max-w-full h-[250px] text-[14px] font-semibold`} 143 + aria-label="Textarea" 144 + placeholder="Optional description for this port" 145 + style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }} 146 + {...register("description")} 147 + ></textarea> 148 + {errors.description && ( 149 + <span className="text-error text-[12px] mt-1 block"> 150 + {errors.description.message} 151 + </span> 152 + )} 153 + </div> 154 + </div> 155 + </div> 156 + <div className="modal-footer"> 157 + <button 158 + type="submit" 159 + className="btn btn-primary w-45 font-semibold" 160 + disabled={isLoading} 161 + > 162 + {isLoading && ( 163 + <span className="loading loading-spinner loading-xs mr-1.5"></span> 164 + )} 165 + Expose Port 166 + </button> 167 + </div> 168 + </form> 169 + </div> 170 + </div> 171 + </div> 172 + 173 + <div 174 + data-overlay-backdrop-template="" 175 + style={{ zIndex: 79 }} 176 + className="overlay-backdrop transition duration-300 fixed inset-0 bg-base-300/60 overflow-y-auto opacity-75" 177 + onClick={handleBackdropClick} 178 + onMouseDown={(e) => e.stopPropagation()} 179 + ></div> 180 + </>, 181 + document.body, 182 + ); 183 + } 184 + 185 + export default ExposePortModal;
+3
apps/web/src/components/contextmenu/ExposePortModal/index.tsx
··· 1 + import ExposePortModal from "./ExposePortMosal"; 2 + 3 + export default ExposePortModal;
+43 -1
apps/web/src/hooks/useSandbox.ts
··· 1 - import { useMutation, useQuery } from "@tanstack/react-query"; 1 + import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 2 2 import { 3 3 claimSandbox, 4 4 createSandbox, ··· 8 8 getSandboxes, 9 9 startSandbox, 10 10 stopSandbox, 11 + getExposedPorts, 12 + exposePort, 13 + unexposePort, 11 14 } from "../api/sandbox"; 12 15 import type { Provider } from "../types/providers"; 13 16 ··· 73 76 mutationKey: ["startSandbox"], 74 77 mutationFn: async (id: string) => startSandbox(id), 75 78 }); 79 + 80 + export const useExposedPortsQuery = (id: string) => 81 + useQuery({ 82 + queryKey: ["exposedPorts", id], 83 + queryFn: () => getExposedPorts(id), 84 + select: (response) => response.data, 85 + enabled: !!id, 86 + }); 87 + 88 + export const useExposePortMutation = () => { 89 + const queryClient = useQueryClient(); 90 + return useMutation({ 91 + mutationKey: ["exposePort"], 92 + mutationFn: async (params: { 93 + id: string; 94 + port: number; 95 + description?: string; 96 + }) => 97 + exposePort(params.id, { 98 + port: params.port, 99 + description: params.description, 100 + }), 101 + onSuccess: () => { 102 + queryClient.invalidateQueries({ queryKey: ["exposedPorts"] }); 103 + }, 104 + }); 105 + }; 106 + 107 + export const useUnexposePortMutation = () => { 108 + const queryClient = useQueryClient(); 109 + return useMutation({ 110 + mutationKey: ["unexposePort"], 111 + mutationFn: async (params: { id: string; port: number }) => 112 + unexposePort(params.id, params.port), 113 + onSuccess: () => { 114 + queryClient.invalidateQueries({ queryKey: ["exposedPorts"] }); 115 + }, 116 + }); 117 + };
+1
apps/web/src/layouts/Main.tsx
··· 33 33 /^\/did:plc:[a-z0-9]+\/sandbox\/[a-z0-9]+\/secrets$/.test(path) || 34 34 /^\/did:plc:[a-z0-9]+\/sandbox\/[a-z0-9]+\/integrations$/.test(path) || 35 35 /^\/did:plc:[a-z0-9]+\/sandbox\/[a-z0-9]+\/repository$/.test(path) || 36 + /^\/did:plc:[a-z0-9]+\/sandbox\/[a-z0-9]+\/ports$/.test(path) || 36 37 /^\/did:plc:[a-z0-9]+\/sandbox\/[a-z0-9]+\/tailscale$/.test(path) 37 38 ) 38 39 return "Settings";
+154
apps/web/src/pages/settings/ports/Ports.tsx
··· 1 + import ContentLoader from "react-content-loader"; 2 + import { useRouterState } from "@tanstack/react-router"; 3 + import Main from "../../../layouts/Main"; 4 + import Sidebar from "../sidebar/Sidebar"; 5 + import { 6 + useExposedPortsQuery, 7 + useSandboxQuery, 8 + useUnexposePortMutation, 9 + } from "../../../hooks/useSandbox"; 10 + import ExposePortModal from "../../../components/contextmenu/ExposePortModal"; 11 + import { useState } from "react"; 12 + import { useQueryClient } from "@tanstack/react-query"; 13 + 14 + const SKELETON_ROWS = 4; 15 + 16 + const PortRowSkeleton = ({ index }: { index: number }) => ( 17 + <ContentLoader 18 + speed={1.5} 19 + width="100%" 20 + height={48} 21 + backgroundColor="rgba(255,255,255,0.06)" 22 + foregroundColor="rgba(255,255,255,0.13)" 23 + style={{ width: "100%" }} 24 + uniqueKey={`port-row-skeleton-${index}`} 25 + > 26 + {/* Port column */} 27 + <rect x="16" y="16" rx="6" ry="6" width="8%" height="16" /> 28 + {/* Preview URL column */} 29 + <rect x="18%" y="16" rx="6" ry="6" width="32%" height="16" /> 30 + {/* Description column */} 31 + <rect x="55%" y="16" rx="6" ry="6" width="22%" height="16" /> 32 + {/* Action column */} 33 + <rect x="88%" y="12" rx="6" ry="6" width="8%" height="24" /> 34 + </ContentLoader> 35 + ); 36 + 37 + function Ports() { 38 + const queryClient = useQueryClient(); 39 + const routerState = useRouterState(); 40 + const pathname = routerState.location.pathname; 41 + const { data } = useSandboxQuery( 42 + `at:/${pathname.replace("/ports", "").replace("sandbox", "io.pocketenv.sandbox")}`, 43 + ); 44 + const [isExposePortModalOpen, setIsExposePortModalOpen] = useState(false); 45 + const [unexposingPort, setUnexposingPort] = useState<number | null>(null); 46 + const { data: exposedPorts, isLoading } = useExposedPortsQuery( 47 + data?.sandbox?.id ?? "", 48 + ); 49 + const { mutateAsync: unexposePort } = useUnexposePortMutation(); 50 + 51 + const handleUnexpose = async (port: number) => { 52 + setUnexposingPort(port); 53 + await unexposePort({ id: data?.sandbox?.id ?? "", port }); 54 + queryClient.invalidateQueries({ queryKey: ["exposedPorts"] }); 55 + setUnexposingPort(null); 56 + }; 57 + 58 + return ( 59 + <> 60 + <Main 61 + sidebar={<Sidebar />} 62 + root={data?.sandbox?.name} 63 + rootLink={pathname.replace("/ports", "")} 64 + > 65 + <> 66 + <div className="w-[95%] m-auto"> 67 + <div className="flex flex-row items-center"> 68 + <h1 className="mb-1 text-xl flex-1">Ports</h1> 69 + <button 70 + className="btn btn-md btn-primary font-semibold" 71 + onClick={() => setIsExposePortModalOpen(true)} 72 + > 73 + Expose Port 74 + </button> 75 + </div> 76 + <p className="opacity-60 mb-5"> 77 + Expose sandbox ports to make internal HTTP services accessible 78 + from outside the sandbox. 79 + </p> 80 + <div className="w-full overflow-x-auto"> 81 + <table className="table mb-20"> 82 + {!!exposedPorts?.ports?.length && ( 83 + <thead> 84 + <tr> 85 + <th className="normal-case text-[14px]">Port</th> 86 + <th className="normal-case text-[14px]">Preview URL</th> 87 + <th className="normal-case text-[14px]">Description</th> 88 + <th className="normal-case text-[14px]"></th> 89 + </tr> 90 + </thead> 91 + )} 92 + <tbody> 93 + {isLoading 94 + ? Array.from({ length: SKELETON_ROWS }).map((_, i) => ( 95 + <tr key={`skeleton-${i}`}> 96 + <td colSpan={4} className="p-0"> 97 + <PortRowSkeleton index={i} /> 98 + </td> 99 + </tr> 100 + )) 101 + : exposedPorts?.ports?.map((p) => ( 102 + <tr key={p.port}> 103 + <td className="normal-case text-[14px] font-medium"> 104 + {p.port} 105 + </td> 106 + <td className="normal-case text-[14px] font-medium"> 107 + {p.previewUrl ? ( 108 + <a 109 + href={p.previewUrl} 110 + target="_blank" 111 + rel="noopener noreferrer" 112 + className="link link-primary" 113 + > 114 + {p.previewUrl} 115 + </a> 116 + ) : ( 117 + <span className="opacity-40">—</span> 118 + )} 119 + </td> 120 + <td className="normal-case text-[14px] font-medium"> 121 + {p.description ?? ( 122 + <span className="opacity-40">—</span> 123 + )} 124 + </td> 125 + <td className="normal-case text-[14px] text-right"> 126 + <button 127 + className="btn btn-outline" 128 + onClick={() => handleUnexpose(p.port)} 129 + disabled={unexposingPort === p.port} 130 + > 131 + {unexposingPort === p.port && ( 132 + <span className="loading loading-spinner loading-xs" /> 133 + )} 134 + Unexpose 135 + </button> 136 + </td> 137 + </tr> 138 + ))} 139 + </tbody> 140 + </table> 141 + </div> 142 + </div> 143 + <ExposePortModal 144 + isOpen={isExposePortModalOpen} 145 + onClose={() => setIsExposePortModalOpen(false)} 146 + sandboxId={data?.sandbox?.id ?? ""} 147 + /> 148 + </> 149 + </Main> 150 + </> 151 + ); 152 + } 153 + 154 + export default Ports;
+3
apps/web/src/pages/settings/ports/index.tsx
··· 1 + import Ports from "./Ports"; 2 + 3 + export default Ports;
+16
apps/web/src/pages/settings/sidebar/Sidebar.tsx
··· 170 170 </li> 171 171 <li> 172 172 <Link 173 + to={`/${did}/sandbox/${rkey}/ports`} 174 + className={`${ 175 + isActive("/ports") 176 + ? "active bg-white/7 text-[#00e8c6]! font-semibold rounded-full" 177 + : "rounded-full hover:text-white" 178 + } ${isCollapsed ? "justify-center px-2" : ""}`} 179 + title={isCollapsed ? "Ports" : undefined} 180 + > 181 + <span 182 + className={`icon-[fluent--open-48-filled] size-6 ${isCollapsed ? "" : "mr-2 ml-1"}`} 183 + ></span> 184 + {!isCollapsed && "Ports"} 185 + </Link> 186 + </li> 187 + <li> 188 + <Link 173 189 to={`/${did}/sandbox/${rkey}/ssh-keys`} 174 190 className={`${ 175 191 isActive("/ssh-keys")
+21
apps/web/src/routeTree.gen.ts
··· 26 26 import { Route as DidSandboxRkeySettingsRouteImport } from './routes/$did.sandbox.$rkey/settings' 27 27 import { Route as DidSandboxRkeySecretsRouteImport } from './routes/$did.sandbox.$rkey/secrets' 28 28 import { Route as DidSandboxRkeyRepositoryRouteImport } from './routes/$did.sandbox.$rkey/repository' 29 + import { Route as DidSandboxRkeyPortsRouteImport } from './routes/$did.sandbox.$rkey/ports' 29 30 import { Route as DidSandboxRkeyIntegrationsRouteImport } from './routes/$did.sandbox.$rkey/integrations' 30 31 import { Route as DidSandboxRkeyFilesRouteImport } from './routes/$did.sandbox.$rkey/files' 31 32 ··· 115 116 path: '/repository', 116 117 getParentRoute: () => DidSandboxRkeyRoute, 117 118 } as any) 119 + const DidSandboxRkeyPortsRoute = DidSandboxRkeyPortsRouteImport.update({ 120 + id: '/ports', 121 + path: '/ports', 122 + getParentRoute: () => DidSandboxRkeyRoute, 123 + } as any) 118 124 const DidSandboxRkeyIntegrationsRoute = 119 125 DidSandboxRkeyIntegrationsRouteImport.update({ 120 126 id: '/integrations', ··· 139 145 '/$did/sandbox/$rkey': typeof DidSandboxRkeyRouteWithChildren 140 146 '/$did/sandbox/$rkey/files': typeof DidSandboxRkeyFilesRoute 141 147 '/$did/sandbox/$rkey/integrations': typeof DidSandboxRkeyIntegrationsRoute 148 + '/$did/sandbox/$rkey/ports': typeof DidSandboxRkeyPortsRoute 142 149 '/$did/sandbox/$rkey/repository': typeof DidSandboxRkeyRepositoryRoute 143 150 '/$did/sandbox/$rkey/secrets': typeof DidSandboxRkeySecretsRoute 144 151 '/$did/sandbox/$rkey/settings': typeof DidSandboxRkeySettingsRoute ··· 159 166 '/sandbox/$id': typeof SandboxIdRoute 160 167 '/$did/sandbox/$rkey/files': typeof DidSandboxRkeyFilesRoute 161 168 '/$did/sandbox/$rkey/integrations': typeof DidSandboxRkeyIntegrationsRoute 169 + '/$did/sandbox/$rkey/ports': typeof DidSandboxRkeyPortsRoute 162 170 '/$did/sandbox/$rkey/repository': typeof DidSandboxRkeyRepositoryRoute 163 171 '/$did/sandbox/$rkey/secrets': typeof DidSandboxRkeySecretsRoute 164 172 '/$did/sandbox/$rkey/settings': typeof DidSandboxRkeySettingsRoute ··· 181 189 '/$did/sandbox/$rkey': typeof DidSandboxRkeyRouteWithChildren 182 190 '/$did/sandbox/$rkey/files': typeof DidSandboxRkeyFilesRoute 183 191 '/$did/sandbox/$rkey/integrations': typeof DidSandboxRkeyIntegrationsRoute 192 + '/$did/sandbox/$rkey/ports': typeof DidSandboxRkeyPortsRoute 184 193 '/$did/sandbox/$rkey/repository': typeof DidSandboxRkeyRepositoryRoute 185 194 '/$did/sandbox/$rkey/secrets': typeof DidSandboxRkeySecretsRoute 186 195 '/$did/sandbox/$rkey/settings': typeof DidSandboxRkeySettingsRoute ··· 204 213 | '/$did/sandbox/$rkey' 205 214 | '/$did/sandbox/$rkey/files' 206 215 | '/$did/sandbox/$rkey/integrations' 216 + | '/$did/sandbox/$rkey/ports' 207 217 | '/$did/sandbox/$rkey/repository' 208 218 | '/$did/sandbox/$rkey/secrets' 209 219 | '/$did/sandbox/$rkey/settings' ··· 224 234 | '/sandbox/$id' 225 235 | '/$did/sandbox/$rkey/files' 226 236 | '/$did/sandbox/$rkey/integrations' 237 + | '/$did/sandbox/$rkey/ports' 227 238 | '/$did/sandbox/$rkey/repository' 228 239 | '/$did/sandbox/$rkey/secrets' 229 240 | '/$did/sandbox/$rkey/settings' ··· 245 256 | '/$did/sandbox/$rkey' 246 257 | '/$did/sandbox/$rkey/files' 247 258 | '/$did/sandbox/$rkey/integrations' 259 + | '/$did/sandbox/$rkey/ports' 248 260 | '/$did/sandbox/$rkey/repository' 249 261 | '/$did/sandbox/$rkey/secrets' 250 262 | '/$did/sandbox/$rkey/settings' ··· 388 400 preLoaderRoute: typeof DidSandboxRkeyRepositoryRouteImport 389 401 parentRoute: typeof DidSandboxRkeyRoute 390 402 } 403 + '/$did/sandbox/$rkey/ports': { 404 + id: '/$did/sandbox/$rkey/ports' 405 + path: '/ports' 406 + fullPath: '/$did/sandbox/$rkey/ports' 407 + preLoaderRoute: typeof DidSandboxRkeyPortsRouteImport 408 + parentRoute: typeof DidSandboxRkeyRoute 409 + } 391 410 '/$did/sandbox/$rkey/integrations': { 392 411 id: '/$did/sandbox/$rkey/integrations' 393 412 path: '/integrations' ··· 408 427 interface DidSandboxRkeyRouteChildren { 409 428 DidSandboxRkeyFilesRoute: typeof DidSandboxRkeyFilesRoute 410 429 DidSandboxRkeyIntegrationsRoute: typeof DidSandboxRkeyIntegrationsRoute 430 + DidSandboxRkeyPortsRoute: typeof DidSandboxRkeyPortsRoute 411 431 DidSandboxRkeyRepositoryRoute: typeof DidSandboxRkeyRepositoryRoute 412 432 DidSandboxRkeySecretsRoute: typeof DidSandboxRkeySecretsRoute 413 433 DidSandboxRkeySettingsRoute: typeof DidSandboxRkeySettingsRoute ··· 421 441 const DidSandboxRkeyRouteChildren: DidSandboxRkeyRouteChildren = { 422 442 DidSandboxRkeyFilesRoute: DidSandboxRkeyFilesRoute, 423 443 DidSandboxRkeyIntegrationsRoute: DidSandboxRkeyIntegrationsRoute, 444 + DidSandboxRkeyPortsRoute: DidSandboxRkeyPortsRoute, 424 445 DidSandboxRkeyRepositoryRoute: DidSandboxRkeyRepositoryRoute, 425 446 DidSandboxRkeySecretsRoute: DidSandboxRkeySecretsRoute, 426 447 DidSandboxRkeySettingsRoute: DidSandboxRkeySettingsRoute,
+6
apps/web/src/routes/$did.sandbox.$rkey/ports.ts
··· 1 + import { createFileRoute } from "@tanstack/react-router"; 2 + import PortsPage from "../../pages/settings/ports"; 3 + 4 + export const Route = createFileRoute("/$did/sandbox/$rkey/ports")({ 5 + component: PortsPage, 6 + });
+5
apps/web/src/types/port.ts
··· 1 + export type Port = { 2 + port: number; 3 + description?: string; 4 + previewUrl?: string; 5 + };