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.

Use transaction to replace sandbox keys

Delete existing SSH and Tailscale keys in a transaction before inserting
new rows (use eq from drizzle-orm). Update the SshKeys UI to convert
escaped \n to real newlines when loading, and to return masked private
keys with newlines escaped as \n.

+35 -23
+13 -9
apps/api/src/xrpc/io/pocketenv/sandbox/putSshKeys.ts
··· 1 1 import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 2 2 import type { Context } from "context"; 3 + import { eq } from "drizzle-orm"; 3 4 import type { Server } from "lexicon"; 4 5 import type { InputSchema } from "lexicon/types/io/pocketenv/sandbox/putSshKeys"; 5 6 import sshKeys from "schema/ssh-keys"; ··· 10 11 throw new XRPCError(401, "Unauthorized"); 11 12 } 12 13 13 - await ctx.db 14 - .insert(sshKeys) 15 - .values({ 16 - publicKey: input.publicKey, 17 - privateKey: input.privateKey, 18 - redacted: input.redacted, 19 - sandboxId: input.id, 20 - }) 21 - .execute(); 14 + await ctx.db.transaction(async (tx) => { 15 + await tx.delete(sshKeys).where(eq(sshKeys.sandboxId, input.id)).execute(); 16 + await tx 17 + .insert(sshKeys) 18 + .values({ 19 + publicKey: input.publicKey, 20 + privateKey: input.privateKey, 21 + redacted: input.redacted, 22 + sandboxId: input.id, 23 + }) 24 + .execute(); 25 + }); 22 26 23 27 return {}; 24 28 };
+15 -8
apps/api/src/xrpc/io/pocketenv/sandbox/putTailscaleAuthKey.ts
··· 1 1 import { XRPCError, type HandlerAuth } from "@atproto/xrpc-server"; 2 2 import type { Context } from "context"; 3 + import { eq } from "drizzle-orm"; 3 4 import type { Server } from "lexicon"; 4 5 import type { InputSchema } from "lexicon/types/io/pocketenv/sandbox/putTailscaleAuthKey"; 5 6 import tailscaleAuthKeys from "schema/tailscale-auth-keys"; ··· 10 11 throw new XRPCError(401, "Unauthorized"); 11 12 } 12 13 13 - await ctx.db 14 - .insert(tailscaleAuthKeys) 15 - .values({ 16 - sandboxId: input.id, 17 - authKey: input.authKey, 18 - redacted: input.redacted || "", 19 - }) 20 - .execute(); 14 + await ctx.db.transaction(async (tx) => { 15 + await tx 16 + .delete(tailscaleAuthKeys) 17 + .where(eq(tailscaleAuthKeys.sandboxId, input.id)) 18 + .execute(); 19 + await tx 20 + .insert(tailscaleAuthKeys) 21 + .values({ 22 + sandboxId: input.id, 23 + authKey: input.authKey, 24 + redacted: input.redacted || "", 25 + }) 26 + .execute(); 27 + }); 21 28 22 29 return {}; 23 30 };
+7 -6
apps/web/src/pages/settings/sshkeys/SshKeys.tsx
··· 90 90 91 91 useEffect(() => { 92 92 if (sshKeys?.data) { 93 - setValue("privateKey", sshKeys.data.privateKey); 93 + setValue("privateKey", sshKeys.data.privateKey.replace(/\\n/g, "\n")); 94 94 setValue("publicKey", sshKeys.data.publicKey); 95 95 } 96 96 }, [sshKeys, setValue]); ··· 115 115 const headerIndex = values.privateKey.indexOf(header); 116 116 const footerIndex = values.privateKey.indexOf(footer); 117 117 if (headerIndex === -1 || footerIndex === -1) 118 - return values.privateKey; 119 - const body = values.privateKey 120 - .slice(headerIndex + header.length, footerIndex) 121 - .trim(); 118 + return values.privateKey.replace(/\n/g, "\\n"); 119 + const body = values.privateKey.slice( 120 + headerIndex + header.length, 121 + footerIndex, 122 + ); 122 123 const chars = body.split(""); 123 124 const nonNewlineIndices = chars 124 125 .map((c, i) => (c !== "\n" ? i : -1)) ··· 133 134 return chars.join(""); 134 135 })() 135 136 : body; 136 - return `${header}\n${maskedBody}\n${footer}`; 137 + return `${header}${maskedBody}${footer}`.replace(/\n/g, "\\n"); 137 138 })(), 138 139 }); 139 140 }