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 backup API and Cloudflare support

+889 -17
+48
apps/api/lexicons/sandbox/createBackup.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "io.pocketenv.sandbox.createBackup", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create a backup of a sandbox by id", 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 + "directory" 26 + ], 27 + "properties": { 28 + "directory": { 29 + "type": "string", 30 + "description": "The directory to backup." 31 + }, 32 + "ttl": { 33 + "type": "integer", 34 + "description": "The time-to-live (TTL) for the backup in seconds. After this time, the backup will be automatically deleted. If not provided, the backup will expire after 3 days." 35 + } 36 + } 37 + } 38 + }, 39 + "output": { 40 + "encoding": "application/json", 41 + "schema": { 42 + "type": "ref", 43 + "ref": "io.pocketenv.sandbox.defs#backupViewBasic" 44 + } 45 + } 46 + } 47 + } 48 + }
+22
apps/api/lexicons/sandbox/defs.json
··· 436 436 "description": "An integration connected to the sandbox", 437 437 "ref": "io.pocketenv.sandbox.defs#integrationView" 438 438 } 439 + }, 440 + "backupViewBasic": { 441 + "type": "object", 442 + "properties": { 443 + "id": { 444 + "type": "string", 445 + "description": "Unique identifier of the backup." 446 + }, 447 + "directory": { 448 + "type": "string", 449 + "description": "The directory that was backed up." 450 + }, 451 + "ttl": { 452 + "type": "integer", 453 + "description": "The time-to-live (TTL) for the backup in seconds. After this time, the backup will be automatically deleted." 454 + }, 455 + "createdAt": { 456 + "type": "string", 457 + "description": "datetime when the backup was created.", 458 + "format": "datetime" 459 + } 460 + } 439 461 } 440 462 } 441 463 }
+37
apps/api/lexicons/sandbox/getBackups.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "io.pocketenv.sandbox.getBackups", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get the list of backups 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 + "backups": { 26 + "type": "array", 27 + "items": { 28 + "type": "ref", 29 + "ref": "io.pocketenv.port.defs#backupViewBasic" 30 + } 31 + } 32 + } 33 + } 34 + } 35 + } 36 + } 37 + }
+37
apps/api/lexicons/sandbox/restoreBackup.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "io.pocketenv.sandbox.restoreBackup", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Restore a backup of 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 + "backupId" 26 + ], 27 + "properties": { 28 + "backupId": { 29 + "type": "string", 30 + "description": "The backup ID to restore." 31 + } 32 + } 33 + } 34 + } 35 + } 36 + } 37 + }
+49
apps/api/pkl/defs/sandbox/createBackup.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "io.pocketenv.sandbox.createBackup" 5 + defs = new Mapping<String, Procedure> { 6 + ["main"] { 7 + type = "procedure" 8 + description = "Create a backup of a sandbox by id" 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 = new ObjectType { 22 + type = "object" 23 + required = List("directory") 24 + properties { 25 + ["directory"] = new StringType { 26 + type = "string" 27 + description = "The directory to backup." 28 + } 29 + ["description"] = new StringType { 30 + type = "string" 31 + description = "An optional description for the backup." 32 + } 33 + ["ttl"] = new IntegerType { 34 + type = "integer" 35 + description = 36 + "The time-to-live (TTL) for the backup in seconds. After this time, the backup will be automatically deleted. If not provided, the backup will expire after 3 days." 37 + } 38 + } 39 + } 40 + } 41 + output { 42 + encoding = "application/json" 43 + schema = new Ref { 44 + type = "ref" 45 + ref = "io.pocketenv.sandbox.defs#backupViewBasic" 46 + } 47 + } 48 + } 49 + }
+26
apps/api/pkl/defs/sandbox/defs.pkl
··· 442 442 description = "An integration connected to the sandbox" 443 443 } 444 444 } 445 + ["backupViewBasic"] = new ObjectType { 446 + type = "object" 447 + properties { 448 + ["id"] = new StringType { 449 + type = "string" 450 + description = "Unique identifier of the backup." 451 + } 452 + ["directory"] = new StringType { 453 + type = "string" 454 + description = "The directory that was backed up." 455 + } 456 + ["description"] = new StringType { 457 + type = "string" 458 + description = "An optional description for the backup." 459 + } 460 + ["expiresAt"] = new IntegerType { 461 + type = "integer" 462 + description = "Datetime when the backup will expire and be automatically deleted." 463 + } 464 + ["createdAt"] = new StringType { 465 + type = "string" 466 + description = "datetime when the backup was created." 467 + format = "datetime" 468 + } 469 + } 470 + } 445 471 }
+34
apps/api/pkl/defs/sandbox/getBackups.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "io.pocketenv.sandbox.getBackups" 5 + defs = new Mapping<String, Query> { 6 + ["main"] { 7 + type = "query" 8 + description = "Get the list of backups 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 + ["backups"] = new Array { 25 + type = "array" 26 + items = new Ref { 27 + ref = "io.pocketenv.port.defs#backupViewBasic" 28 + } 29 + } 30 + } 31 + } 32 + } 33 + } 34 + }
+33
apps/api/pkl/defs/sandbox/restoreBackup.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "io.pocketenv.sandbox.restoreBackup" 5 + defs = new Mapping<String, Procedure> { 6 + ["main"] { 7 + type = "procedure" 8 + description = "Restore a backup of 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("backupId") 24 + properties { 25 + ["backupId"] = new StringType { 26 + type = "string" 27 + description = "The backup ID to restore." 28 + } 29 + } 30 + } 31 + } 32 + } 33 + }
+6 -3
apps/api/src/__tests__/xrpc/helpers.ts
··· 23 23 { 24 24 get(_t, key: string | symbol) { 25 25 if (key === "execute") return () => Promise.resolve(results[idx++]); 26 - if (key === "transaction") return (fn: (tx: any) => any) => fn(chain()); 26 + if (key === "transaction") 27 + return (fn: (tx: any) => any) => fn(chain()); 27 28 if ( 28 29 key === "then" || 29 30 key === "catch" || ··· 116 117 }, 117 118 }; 118 119 register(server, ctx); 119 - if (!capturedHandler) throw new Error(`Handler for ${method} was not registered`); 120 + if (!capturedHandler) 121 + throw new Error(`Handler for ${method} was not registered`); 120 122 return capturedHandler; 121 123 } 122 124 ··· 138 140 }, 139 141 }; 140 142 register(server, ctx); 141 - if (!capturedHandler) throw new Error(`Handler for ${method} was not registered`); 143 + if (!capturedHandler) 144 + throw new Error(`Handler for ${method} was not registered`); 142 145 return capturedHandler; 143 146 } 144 147
+8 -2
apps/api/src/__tests__/xrpc/sandbox/getSandbox.test.ts
··· 80 80 execute: () => Promise.reject(new Error("db down")), 81 81 }; 82 82 const ctx = { authVerifier: () => {}, db }; 83 - const handler = captureSandboxHandler("getSandbox", registerGetSandbox, ctx); 83 + const handler = captureSandboxHandler( 84 + "getSandbox", 85 + registerGetSandbox, 86 + ctx, 87 + ); 84 88 85 89 const { body } = await handler({ 86 90 params: { id: "sb-1" }, ··· 92 96 93 97 it("maps baseSandbox from the base column", async () => { 94 98 const sandboxWithBase = { ...MOCK_SANDBOX, base: "at://some/base/ref" }; 95 - const handler = makeHandler([[{ sandboxes: sandboxWithBase, users: null }]]); 99 + const handler = makeHandler([ 100 + [{ sandboxes: sandboxWithBase, users: null }], 101 + ]); 96 102 97 103 const { body } = await handler({ 98 104 params: { id: sandboxWithBase.id },
+1 -4
apps/api/src/__tests__/xrpc/sandbox/getSandboxes.test.ts
··· 15 15 describe("getSandboxes handler", () => { 16 16 it("returns a mapped list of sandboxes and the total count", async () => { 17 17 // First execute() → rows; second execute() → count 18 - const handler = makeHandler( 19 - [{ sandboxes: MOCK_SANDBOX }], 20 - [{ count: 1 }], 21 - ); 18 + const handler = makeHandler([{ sandboxes: MOCK_SANDBOX }], [{ count: 1 }]); 22 19 23 20 const { body } = await handler({ 24 21 params: {},
+6 -1
apps/api/src/__tests__/xrpc/secret/addSecret.test.ts
··· 85 85 // tx.insert(sandboxSecrets).returning().execute() 86 86 [INSERTED_SANDBOX_SECRET], 87 87 // post-tx select for updateSandbox 88 - [{ sandbox_secrets: INSERTED_SANDBOX_SECRET, sandboxes: { uri: "at://did/io.pocketenv.sandbox/tid1" } }], 88 + [ 89 + { 90 + sandbox_secrets: INSERTED_SANDBOX_SECRET, 91 + sandboxes: { uri: "at://did/io.pocketenv.sandbox/tid1" }, 92 + }, 93 + ], 89 94 ); 90 95 91 96 await handler({
+36
apps/api/src/lexicon/index.ts
··· 18 18 import type * as IoPocketenvFileGetFiles from "./types/io/pocketenv/file/getFiles"; 19 19 import type * as IoPocketenvFileUpdateFile from "./types/io/pocketenv/file/updateFile"; 20 20 import type * as IoPocketenvSandboxClaimSandbox from "./types/io/pocketenv/sandbox/claimSandbox"; 21 + import type * as IoPocketenvSandboxCreateBackup from "./types/io/pocketenv/sandbox/createBackup"; 21 22 import type * as IoPocketenvSandboxCreateIntegration from "./types/io/pocketenv/sandbox/createIntegration"; 22 23 import type * as IoPocketenvSandboxCreateSandbox from "./types/io/pocketenv/sandbox/createSandbox"; 23 24 import type * as IoPocketenvSandboxDeleteSandbox from "./types/io/pocketenv/sandbox/deleteSandbox"; 24 25 import type * as IoPocketenvSandboxExec from "./types/io/pocketenv/sandbox/exec"; 25 26 import type * as IoPocketenvSandboxExposePort from "./types/io/pocketenv/sandbox/exposePort"; 26 27 import type * as IoPocketenvSandboxExposeVscode from "./types/io/pocketenv/sandbox/exposeVscode"; 28 + import type * as IoPocketenvSandboxGetBackups from "./types/io/pocketenv/sandbox/getBackups"; 27 29 import type * as IoPocketenvSandboxGetExposedPorts from "./types/io/pocketenv/sandbox/getExposedPorts"; 28 30 import type * as IoPocketenvSandboxGetIntegrations from "./types/io/pocketenv/sandbox/getIntegrations"; 29 31 import type * as IoPocketenvSandboxGetPreferences from "./types/io/pocketenv/sandbox/getPreferences"; ··· 38 40 import type * as IoPocketenvSandboxPutSshKeys from "./types/io/pocketenv/sandbox/putSshKeys"; 39 41 import type * as IoPocketenvSandboxPutTailscaleAuthKey from "./types/io/pocketenv/sandbox/putTailscaleAuthKey"; 40 42 import type * as IoPocketenvSandboxPutTailscaleToken from "./types/io/pocketenv/sandbox/putTailscaleToken"; 43 + import type * as IoPocketenvSandboxRestoreBackup from "./types/io/pocketenv/sandbox/restoreBackup"; 41 44 import type * as IoPocketenvSandboxStartSandbox from "./types/io/pocketenv/sandbox/startSandbox"; 42 45 import type * as IoPocketenvSandboxStopSandbox from "./types/io/pocketenv/sandbox/stopSandbox"; 43 46 import type * as IoPocketenvSandboxUnexposePort from "./types/io/pocketenv/sandbox/unexposePort"; ··· 237 240 return this._server.xrpc.method(nsid, cfg); 238 241 } 239 242 243 + createBackup<AV extends AuthVerifier>( 244 + cfg: ConfigOf< 245 + AV, 246 + IoPocketenvSandboxCreateBackup.Handler<ExtractAuth<AV>>, 247 + IoPocketenvSandboxCreateBackup.HandlerReqCtx<ExtractAuth<AV>> 248 + >, 249 + ) { 250 + const nsid = "io.pocketenv.sandbox.createBackup"; // @ts-ignore 251 + return this._server.xrpc.method(nsid, cfg); 252 + } 253 + 240 254 createIntegration<AV extends AuthVerifier>( 241 255 cfg: ConfigOf< 242 256 AV, ··· 303 317 return this._server.xrpc.method(nsid, cfg); 304 318 } 305 319 320 + getBackups<AV extends AuthVerifier>( 321 + cfg: ConfigOf< 322 + AV, 323 + IoPocketenvSandboxGetBackups.Handler<ExtractAuth<AV>>, 324 + IoPocketenvSandboxGetBackups.HandlerReqCtx<ExtractAuth<AV>> 325 + >, 326 + ) { 327 + const nsid = "io.pocketenv.sandbox.getBackups"; // @ts-ignore 328 + return this._server.xrpc.method(nsid, cfg); 329 + } 330 + 306 331 getExposedPorts<AV extends AuthVerifier>( 307 332 cfg: ConfigOf< 308 333 AV, ··· 454 479 >, 455 480 ) { 456 481 const nsid = "io.pocketenv.sandbox.putTailscaleToken"; // @ts-ignore 482 + return this._server.xrpc.method(nsid, cfg); 483 + } 484 + 485 + restoreBackup<AV extends AuthVerifier>( 486 + cfg: ConfigOf< 487 + AV, 488 + IoPocketenvSandboxRestoreBackup.Handler<ExtractAuth<AV>>, 489 + IoPocketenvSandboxRestoreBackup.HandlerReqCtx<ExtractAuth<AV>> 490 + >, 491 + ) { 492 + const nsid = "io.pocketenv.sandbox.restoreBackup"; // @ts-ignore 457 493 return this._server.xrpc.method(nsid, cfg); 458 494 } 459 495
+139
apps/api/src/lexicon/lexicons.ts
··· 470 470 }, 471 471 }, 472 472 }, 473 + IoPocketenvSandboxCreateBackup: { 474 + lexicon: 1, 475 + id: "io.pocketenv.sandbox.createBackup", 476 + defs: { 477 + main: { 478 + type: "procedure", 479 + description: "Create a backup of a sandbox by id", 480 + parameters: { 481 + type: "params", 482 + required: ["id"], 483 + properties: { 484 + id: { 485 + type: "string", 486 + description: "The sandbox ID.", 487 + }, 488 + }, 489 + }, 490 + input: { 491 + encoding: "application/json", 492 + schema: { 493 + type: "object", 494 + required: ["directory"], 495 + properties: { 496 + directory: { 497 + type: "string", 498 + description: "The directory to backup.", 499 + }, 500 + ttl: { 501 + type: "integer", 502 + description: 503 + "The time-to-live (TTL) for the backup in seconds. After this time, the backup will be automatically deleted. If not provided, the backup will expire after 3 days.", 504 + }, 505 + }, 506 + }, 507 + }, 508 + output: { 509 + encoding: "application/json", 510 + schema: { 511 + type: "ref", 512 + ref: "lex:io.pocketenv.sandbox.defs#backupViewBasic", 513 + }, 514 + }, 515 + }, 516 + }, 517 + }, 473 518 IoPocketenvSandboxCreateIntegration: { 474 519 lexicon: 1, 475 520 id: "io.pocketenv.sandbox.createIntegration", ··· 1110 1155 ref: "lex:io.pocketenv.sandbox.defs#integrationView", 1111 1156 }, 1112 1157 }, 1158 + backupViewBasic: { 1159 + type: "object", 1160 + properties: { 1161 + id: { 1162 + type: "string", 1163 + description: "Unique identifier of the backup.", 1164 + }, 1165 + directory: { 1166 + type: "string", 1167 + description: "The directory that was backed up.", 1168 + }, 1169 + ttl: { 1170 + type: "integer", 1171 + description: 1172 + "The time-to-live (TTL) for the backup in seconds. After this time, the backup will be automatically deleted.", 1173 + }, 1174 + createdAt: { 1175 + type: "string", 1176 + description: "datetime when the backup was created.", 1177 + format: "datetime", 1178 + }, 1179 + }, 1180 + }, 1113 1181 }, 1114 1182 }, 1115 1183 IoPocketenvSandboxDeleteSandbox: { ··· 1279 1347 }, 1280 1348 }, 1281 1349 }, 1350 + IoPocketenvSandboxGetBackups: { 1351 + lexicon: 1, 1352 + id: "io.pocketenv.sandbox.getBackups", 1353 + defs: { 1354 + main: { 1355 + type: "query", 1356 + description: "Get the list of backups for a sandbox.", 1357 + parameters: { 1358 + type: "params", 1359 + required: ["id"], 1360 + properties: { 1361 + id: { 1362 + type: "string", 1363 + description: "The sandbox ID.", 1364 + }, 1365 + }, 1366 + }, 1367 + output: { 1368 + encoding: "application/json", 1369 + schema: { 1370 + type: "object", 1371 + properties: { 1372 + backups: { 1373 + type: "array", 1374 + items: { 1375 + type: "ref", 1376 + ref: "lex:io.pocketenv.port.defs#backupViewBasic", 1377 + }, 1378 + }, 1379 + }, 1380 + }, 1381 + }, 1382 + }, 1383 + }, 1384 + }, 1282 1385 IoPocketenvSandboxGetExposedPorts: { 1283 1386 lexicon: 1, 1284 1387 id: "io.pocketenv.sandbox.getExposedPorts", ··· 1738 1841 schema: { 1739 1842 type: "ref", 1740 1843 ref: "lex:io.pocketenv.sandbox.defs#tailscaleTokenView", 1844 + }, 1845 + }, 1846 + }, 1847 + }, 1848 + }, 1849 + IoPocketenvSandboxRestoreBackup: { 1850 + lexicon: 1, 1851 + id: "io.pocketenv.sandbox.restoreBackup", 1852 + defs: { 1853 + main: { 1854 + type: "procedure", 1855 + description: "Restore a backup of a sandbox", 1856 + parameters: { 1857 + type: "params", 1858 + required: ["id"], 1859 + properties: { 1860 + id: { 1861 + type: "string", 1862 + description: "The sandbox ID.", 1863 + }, 1864 + }, 1865 + }, 1866 + input: { 1867 + encoding: "application/json", 1868 + schema: { 1869 + type: "object", 1870 + required: ["backupId"], 1871 + properties: { 1872 + backupId: { 1873 + type: "string", 1874 + description: "The backup ID to restore.", 1875 + }, 1876 + }, 1741 1877 }, 1742 1878 }, 1743 1879 }, ··· 2999 3135 IoPocketenvFileUpdateFile: "io.pocketenv.file.updateFile", 3000 3136 IoPocketenvPortDefs: "io.pocketenv.port.defs", 3001 3137 IoPocketenvSandboxClaimSandbox: "io.pocketenv.sandbox.claimSandbox", 3138 + IoPocketenvSandboxCreateBackup: "io.pocketenv.sandbox.createBackup", 3002 3139 IoPocketenvSandboxCreateIntegration: "io.pocketenv.sandbox.createIntegration", 3003 3140 IoPocketenvSandboxCreateSandbox: "io.pocketenv.sandbox.createSandbox", 3004 3141 IoPocketenvSandboxDefs: "io.pocketenv.sandbox.defs", ··· 3006 3143 IoPocketenvSandboxExec: "io.pocketenv.sandbox.exec", 3007 3144 IoPocketenvSandboxExposePort: "io.pocketenv.sandbox.exposePort", 3008 3145 IoPocketenvSandboxExposeVscode: "io.pocketenv.sandbox.exposeVscode", 3146 + IoPocketenvSandboxGetBackups: "io.pocketenv.sandbox.getBackups", 3009 3147 IoPocketenvSandboxGetExposedPorts: "io.pocketenv.sandbox.getExposedPorts", 3010 3148 IoPocketenvSandboxGetIntegrations: "io.pocketenv.sandbox.getIntegrations", 3011 3149 IoPocketenvSandboxGetPreferences: "io.pocketenv.sandbox.getPreferences", ··· 3022 3160 IoPocketenvSandboxPutTailscaleAuthKey: 3023 3161 "io.pocketenv.sandbox.putTailscaleAuthKey", 3024 3162 IoPocketenvSandboxPutTailscaleToken: "io.pocketenv.sandbox.putTailscaleToken", 3163 + IoPocketenvSandboxRestoreBackup: "io.pocketenv.sandbox.restoreBackup", 3025 3164 IoPocketenvSandbox: "io.pocketenv.sandbox", 3026 3165 IoPocketenvSandboxStartSandbox: "io.pocketenv.sandbox.startSandbox", 3027 3166 IoPocketenvSandboxStopSandbox: "io.pocketenv.sandbox.stopSandbox",
+54
apps/api/src/lexicon/types/io/pocketenv/sandbox/createBackup.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 IoPocketenvSandboxDefs from "./defs"; 11 + 12 + export interface QueryParams { 13 + /** The sandbox ID. */ 14 + id: string; 15 + } 16 + 17 + export interface InputSchema { 18 + /** The directory to backup. */ 19 + directory: string; 20 + /** The time-to-live (TTL) for the backup in seconds. After this time, the backup will be automatically deleted. If not provided, the backup will expire after 3 days. */ 21 + ttl?: number; 22 + [k: string]: unknown; 23 + } 24 + 25 + export type OutputSchema = IoPocketenvSandboxDefs.BackupViewBasic; 26 + 27 + export interface HandlerInput { 28 + encoding: "application/json"; 29 + body: InputSchema; 30 + } 31 + 32 + export interface HandlerSuccess { 33 + encoding: "application/json"; 34 + body: OutputSchema; 35 + headers?: { [key: string]: string }; 36 + } 37 + 38 + export interface HandlerError { 39 + status: number; 40 + message?: string; 41 + } 42 + 43 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 44 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 45 + auth: HA; 46 + params: QueryParams; 47 + input: HandlerInput; 48 + req: express.Request; 49 + res: express.Response; 50 + resetRouteRateLimits: () => Promise<void>; 51 + }; 52 + export type Handler<HA extends HandlerAuth = never> = ( 53 + ctx: HandlerReqCtx<HA>, 54 + ) => Promise<HandlerOutput> | HandlerOutput;
+24
apps/api/src/lexicon/types/io/pocketenv/sandbox/defs.ts
··· 333 333 } 334 334 335 335 export type IntegrationsView = IntegrationView[]; 336 + 337 + export interface BackupViewBasic { 338 + /** Unique identifier of the backup. */ 339 + id?: string; 340 + /** The directory that was backed up. */ 341 + directory?: string; 342 + /** The time-to-live (TTL) for the backup in seconds. After this time, the backup will be automatically deleted. */ 343 + ttl?: number; 344 + /** datetime when the backup was created. */ 345 + createdAt?: string; 346 + [k: string]: unknown; 347 + } 348 + 349 + export function isBackupViewBasic(v: unknown): v is BackupViewBasic { 350 + return ( 351 + isObj(v) && 352 + hasProp(v, "$type") && 353 + v.$type === "io.pocketenv.sandbox.defs#backupViewBasic" 354 + ); 355 + } 356 + 357 + export function validateBackupViewBasic(v: unknown): ValidationResult { 358 + return lexicons.validate("io.pocketenv.sandbox.defs#backupViewBasic", v); 359 + }
+48
apps/api/src/lexicon/types/io/pocketenv/sandbox/getBackups.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 + backups?: IoPocketenvPortDefs.BackupViewBasic[]; 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/restoreBackup.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 backup ID to restore. */ 18 + backupId: string; 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;
+1 -3
apps/api/src/schema/backups.ts
··· 3 3 import sandboxes from "./sandboxes"; 4 4 5 5 const backups = pgTable("backups", { 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" }),
+6
apps/api/src/xrpc/index.ts
··· 52 52 import stopService from "./io/pocketenv/service/stopService"; 53 53 import pushDirectory from "./io/pocketenv/sandbox/pushDirectory"; 54 54 import pullDirectory from "./io/pocketenv/sandbox/pullDirectory"; 55 + import createBackup from "./io/pocketenv/sandbox/createBackup"; 56 + import getBackups from "./io/pocketenv/sandbox/getBackups"; 57 + import restoreBackup from "./io/pocketenv/sandbox/restoreBackup"; 55 58 56 59 export default function (server: Server, ctx: Context) { 57 60 // io.pocketenv ··· 110 113 stopService(server, ctx); 111 114 pushDirectory(server, ctx); 112 115 pullDirectory(server, ctx); 116 + createBackup(server, ctx); 117 + getBackups(server, ctx); 118 + restoreBackup(server, ctx); 113 119 114 120 return server; 115 121 }
+88
apps/api/src/xrpc/io/pocketenv/sandbox/createBackup.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, or } from "drizzle-orm"; 5 + import type { Server } from "lexicon"; 6 + import type { 7 + InputSchema, 8 + QueryParams, 9 + OutputSchema, 10 + } from "lexicon/types/io/pocketenv/sandbox/createBackup"; 11 + import generateJwt from "lib/generateJwt"; 12 + import schema from "schema"; 13 + import type { SelectBakcup } from "schema/backups"; 14 + 15 + export default function (server: Server, ctx: Context) { 16 + const createBackup = async ( 17 + input: InputSchema, 18 + params: QueryParams, 19 + auth: HandlerAuth, 20 + ) => { 21 + if (!auth.credentials) { 22 + throw new XRPCError(401, "Unauthorized"); 23 + } 24 + 25 + const [record] = await ctx.db 26 + .select() 27 + .from(schema.sandboxes) 28 + .leftJoin(schema.users, eq(schema.sandboxes.userId, schema.users.id)) 29 + .where( 30 + and( 31 + or( 32 + eq(schema.sandboxes.id, params.id), 33 + eq(schema.sandboxes.uri, params.id), 34 + eq(schema.sandboxes.name, params.id), 35 + ), 36 + eq(schema.users.did, auth.credentials.did), 37 + ), 38 + ) 39 + .execute(); 40 + 41 + if (!record) { 42 + throw new XRPCError(404, "Sandbox not found"); 43 + } 44 + 45 + if (record.sandboxes.provider !== Providers.CLOUDFLARE) { 46 + throw new XRPCError(400, "Sandbox provider does not support backup"); 47 + } 48 + 49 + const sandbox = 50 + record.sandboxes.provider === Providers.CLOUDFLARE 51 + ? ctx.cfsandbox(record.sandboxes.base!) 52 + : ctx.sandbox(); 53 + 54 + const { data: backup } = await sandbox.post<SelectBakcup>( 55 + `/v1/sandboxes/${record.sandboxes.id}/backup`, 56 + { 57 + directory: input.directory, 58 + description: input.description, 59 + ttl: input.ttl, 60 + }, 61 + { 62 + headers: { 63 + Authorization: `Bearer ${await generateJwt(auth?.credentials?.did || "")}`, 64 + }, 65 + }, 66 + ); 67 + 68 + return { 69 + id: backup.backupId, 70 + directory: backup.directory, 71 + description: backup.description, 72 + expiresAt: backup.expiresAt 73 + ? new Date(backup.expiresAt).toISOString() 74 + : undefined, 75 + createdAt: new Date(backup.createdAt).toISOString(), 76 + } satisfies OutputSchema; 77 + }; 78 + server.io.pocketenv.sandbox.createBackup({ 79 + auth: ctx.authVerifier, 80 + handler: async ({ input, params, auth }) => { 81 + const result = await createBackup(input.body, params, auth); 82 + return { 83 + encoding: "application/json", 84 + body: result, 85 + }; 86 + }, 87 + }); 88 + }
+63
apps/api/src/xrpc/io/pocketenv/sandbox/getBackups.ts
··· 1 + import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 2 + import type { Context } from "context"; 3 + import { and, eq, or } from "drizzle-orm"; 4 + import type { Server } from "lexicon"; 5 + import type { 6 + QueryParams, 7 + OutputSchema, 8 + } from "lexicon/types/io/pocketenv/sandbox/getBackups"; 9 + import schema from "schema"; 10 + 11 + export default function (server: Server, ctx: Context) { 12 + const getBackups = async (params: QueryParams, auth: HandlerAuth) => { 13 + if (!auth.credentials) { 14 + throw new XRPCError(401, "Unauthorized"); 15 + } 16 + 17 + const [record] = await ctx.db 18 + .select() 19 + .from(schema.sandboxes) 20 + .leftJoin(schema.users, eq(schema.sandboxes.userId, schema.users.id)) 21 + .where( 22 + and( 23 + or( 24 + eq(schema.sandboxes.id, params.id), 25 + eq(schema.sandboxes.uri, params.id), 26 + eq(schema.sandboxes.name, params.id), 27 + ), 28 + eq(schema.users.did, auth.credentials.did), 29 + ), 30 + ) 31 + .execute(); 32 + 33 + if (!record) { 34 + throw new XRPCError(404, "Sandbox not found"); 35 + } 36 + 37 + const backups = await ctx.db 38 + .select() 39 + .from(schema.backups) 40 + .where(eq(schema.backups.sandboxId, record.sandboxes.id)) 41 + .execute(); 42 + 43 + return { 44 + backups: backups.map((backup) => ({ 45 + id: backup.backupId, 46 + directory: backup.directory, 47 + description: backup.description, 48 + expiresAt: backup.expiresAt?.toISOString(), 49 + createdAt: backup.createdAt.toISOString(), 50 + })), 51 + }; 52 + }; 53 + server.io.pocketenv.sandbox.getBackups({ 54 + auth: ctx.authVerifier, 55 + handler: async ({ params, auth }) => { 56 + const result = await getBackups(params, auth); 57 + return { 58 + encoding: "application/json", 59 + body: result satisfies OutputSchema, 60 + }; 61 + }, 62 + }); 63 + }
+72
apps/api/src/xrpc/io/pocketenv/sandbox/restoreBackup.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, or } from "drizzle-orm"; 5 + import type { Server } from "lexicon"; 6 + import type { 7 + InputSchema, 8 + QueryParams, 9 + } from "lexicon/types/io/pocketenv/sandbox/restoreBackup"; 10 + import generateJwt from "lib/generateJwt"; 11 + import schema from "schema"; 12 + 13 + export default function (server: Server, ctx: Context) { 14 + const restoreBackup = async ( 15 + input: InputSchema, 16 + params: QueryParams, 17 + auth: HandlerAuth, 18 + ) => { 19 + if (!auth.credentials) { 20 + throw new XRPCError(401, "Unauthorized"); 21 + } 22 + 23 + const [record] = await ctx.db 24 + .select() 25 + .from(schema.sandboxes) 26 + .leftJoin(schema.users, eq(schema.sandboxes.userId, schema.users.id)) 27 + .where( 28 + and( 29 + or( 30 + eq(schema.sandboxes.id, params.id), 31 + eq(schema.sandboxes.uri, params.id), 32 + eq(schema.sandboxes.name, params.id), 33 + ), 34 + eq(schema.users.did, auth.credentials.did), 35 + ), 36 + ) 37 + .execute(); 38 + 39 + if (!record) { 40 + throw new XRPCError(404, "Sandbox not found"); 41 + } 42 + 43 + if (record.sandboxes.provider !== Providers.CLOUDFLARE) { 44 + throw new XRPCError(400, "Sandbox provider does not support backup"); 45 + } 46 + 47 + const sandbox = 48 + record.sandboxes.provider === Providers.CLOUDFLARE 49 + ? ctx.cfsandbox(record.sandboxes.base!) 50 + : ctx.sandbox(); 51 + 52 + await sandbox.post( 53 + `/v1/sandboxes/${record.sandboxes.id}/restore`, 54 + { 55 + backupId: input.backupId, 56 + }, 57 + { 58 + headers: { 59 + Authorization: `Bearer ${await generateJwt(auth?.credentials?.did || "")}`, 60 + }, 61 + }, 62 + ); 63 + 64 + return {}; 65 + }; 66 + server.io.pocketenv.sandbox.restoreBackup({ 67 + auth: ctx.authVerifier, 68 + handler: async ({ input, params, auth }) => { 69 + await restoreBackup(input.body, params, auth); 70 + }, 71 + }); 72 + }
+7 -4
apps/cf-sandbox/src/routes/sandboxes.ts
··· 542 542 const sandbox = await createSandbox("cloudflare", { id: record.sandboxId! }); 543 543 const backupId = await sandbox.backup(params.directory, params.ttl); 544 544 545 - await c.var.db.insert(backups).values({ 545 + const backup = await c.var.db.insert(backups).values({ 546 546 backupId, 547 547 sandboxId: record.id, 548 548 directory: params.directory, 549 + description: params.description, 549 550 expiresAt: params.ttl ? dayjs().add(params.ttl, "second").toDate() : dayjs().add(3, "days").toDate(), 550 - }).execute(); 551 + }) 552 + .returning() 553 + .execute(); 551 554 552 - return c.json({ backupId }); 555 + return c.json(backup); 553 556 }); 554 557 555 558 sandboxRoutes.post("/v1/sandboxes/:sandboxId/restore", async (c) => { ··· 564 567 const [backup] = await c.var.db.select().from(backups) 565 568 .where( 566 569 and( 567 - eq(backups.id, params.backupId), 570 + eq(backups.backupId, params.backupId), 568 571 eq(backups.sandboxId, record.id), 569 572 ), 570 573 )
+1
apps/cf-sandbox/src/types/backup.ts
··· 2 2 3 3 export const pullSchema = z.object({ 4 4 directory: z.string(), 5 + description: z.string().optional(), 5 6 ttl: z.number().positive().optional(), 6 7 }); 7 8