the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
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 }