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 PTY support with pty-tunnel and UI integration

Introduce API /pty router and pty-tunnel listener/messages.
Setup Vercel Sandbox environment to install/run pty-tunnel-server.
Wire CLI and web to select pty vs tty and add POCKETENV_PTY_URL.
Add dependencies: @vercel/sandbox and jsonlines

+460 -19
+44 -3
apps/api/bun.lock
··· 22 22 "@types/prompts": "^2.4.9", 23 23 "@types/ramda": "^0.31.1", 24 24 "@types/ws": "^8.18.1", 25 + "@vercel/sandbox": "^1.9.0", 25 26 "axios": "^1.13.5", 26 27 "better-sqlite3": "^12.6.2", 27 28 "chalk": "^5.6.2", ··· 35 36 "express": "^5.2.1", 36 37 "hono": "^4.11.9", 37 38 "ioredis": "^5.9.3", 39 + "jsonlines": "^0.1.1", 38 40 "jsonwebtoken": "^9.0.3", 39 41 "kysely": "^0.28.11", 40 42 "libsodium-wrappers": "^0.8.2", ··· 57 59 "@types/better-sqlite3": "^7.6.13", 58 60 "@types/cors": "^2.8.19", 59 61 "@types/express": "^5.0.6", 62 + "@types/jsonlines": "^0.1.5", 60 63 "@types/jsonwebtoken": "^9.0.10", 61 64 "@types/lodash": "^4.17.23", 62 65 "@types/morgan": "^1.9.10", ··· 335 338 336 339 "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], 337 340 341 + "@types/jsonlines": ["@types/jsonlines@0.1.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-/zOl7I350g4/G6fEW9dktpTrkcKqZDMRkr2SuDla0utgwkUXrm7OFXq2WZT0W9Jl7BYoisGbn1EZsV/Z2F9LGg=="], 342 + 338 343 "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], 339 344 340 345 "@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="], ··· 365 370 366 371 "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], 367 372 373 + "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], 374 + 375 + "@vercel/sandbox": ["@vercel/sandbox@1.9.0", "", { "dependencies": { "@vercel/oidc": "3.2.0", "async-retry": "1.3.3", "jsonlines": "0.1.1", "ms": "2.1.3", "picocolors": "^1.1.1", "tar-stream": "3.1.7", "undici": "^7.16.0", "xdg-app-paths": "5.1.0", "zod": "3.24.4" } }, "sha512-zgr1ad0tkT1xZn/8Vxo60wOUOLqMAVGo4WqJQ8/UDcUtWynNJsBjI2tiMdWZrAo9EKH1MIqEzJNkcclF0UT1EQ=="], 376 + 368 377 "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], 369 378 370 379 "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], ··· 379 388 380 389 "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], 381 390 391 + "async-retry": ["async-retry@1.3.3", "", { "dependencies": { "retry": "0.13.1" } }, "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw=="], 392 + 382 393 "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], 383 394 384 395 "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], ··· 387 398 388 399 "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], 389 400 401 + "b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="], 402 + 390 403 "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 404 + 405 + "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="], 391 406 392 407 "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], 393 408 ··· 537 552 538 553 "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], 539 554 555 + "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], 556 + 540 557 "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], 541 558 542 559 "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], 543 560 544 561 "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], 545 562 563 + "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], 564 + 546 565 "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], 547 566 548 567 "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], ··· 630 649 "iso-datestring-validator": ["iso-datestring-validator@2.2.2", "", {}, "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="], 631 650 632 651 "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], 652 + 653 + "jsonlines": ["jsonlines@0.1.1", "", {}, "sha512-ekDrAGso79Cvf+dtm+mL8OBI2bmAOt3gssYs833De/C9NmIpWDWyUO4zPgB5x2/OhY366dkhgfPMYfwZF7yOZA=="], 633 654 634 655 "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], 635 656 ··· 735 756 736 757 "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], 737 758 759 + "os-paths": ["os-paths@4.4.0", "", {}, "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg=="], 760 + 738 761 "p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="], 739 762 740 763 "p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], ··· 765 788 766 789 "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], 767 790 791 + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 792 + 768 793 "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 769 794 770 795 "pino": ["pino@8.21.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^1.2.0", "pino-std-serializers": "^6.0.0", "process-warning": "^3.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^3.7.0", "thread-stream": "^2.6.0" }, "bin": { "pino": "bin.js" } }, "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q=="], ··· 837 862 838 863 "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], 839 864 865 + "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], 866 + 840 867 "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], 841 868 842 869 "rollup": ["rollup@4.58.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.58.0", "@rollup/rollup-android-arm64": "4.58.0", "@rollup/rollup-darwin-arm64": "4.58.0", "@rollup/rollup-darwin-x64": "4.58.0", "@rollup/rollup-freebsd-arm64": "4.58.0", "@rollup/rollup-freebsd-x64": "4.58.0", "@rollup/rollup-linux-arm-gnueabihf": "4.58.0", "@rollup/rollup-linux-arm-musleabihf": "4.58.0", "@rollup/rollup-linux-arm64-gnu": "4.58.0", "@rollup/rollup-linux-arm64-musl": "4.58.0", "@rollup/rollup-linux-loong64-gnu": "4.58.0", "@rollup/rollup-linux-loong64-musl": "4.58.0", "@rollup/rollup-linux-ppc64-gnu": "4.58.0", "@rollup/rollup-linux-ppc64-musl": "4.58.0", "@rollup/rollup-linux-riscv64-gnu": "4.58.0", "@rollup/rollup-linux-riscv64-musl": "4.58.0", "@rollup/rollup-linux-s390x-gnu": "4.58.0", "@rollup/rollup-linux-x64-gnu": "4.58.0", "@rollup/rollup-linux-x64-musl": "4.58.0", "@rollup/rollup-openbsd-x64": "4.58.0", "@rollup/rollup-openharmony-arm64": "4.58.0", "@rollup/rollup-win32-arm64-msvc": "4.58.0", "@rollup/rollup-win32-ia32-msvc": "4.58.0", "@rollup/rollup-win32-x64-gnu": "4.58.0", "@rollup/rollup-win32-x64-msvc": "4.58.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wbT0mBmWbIvvq8NeEYWWvevvxnOyhKChir47S66WCxw1SXqhw7ssIYejnQEVt7XYQpsj2y8F9PM+Cr3SNEa0gw=="], ··· 891 918 892 919 "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], 893 920 921 + "streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="], 922 + 894 923 "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], 895 924 896 925 "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], ··· 901 930 902 931 "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], 903 932 904 - "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], 933 + "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], 934 + 935 + "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], 905 936 906 937 "thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw=="], 907 938 ··· 933 964 934 965 "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], 935 966 936 - "undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], 967 + "undici": ["undici@7.24.6", "", {}, "sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA=="], 937 968 938 969 "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 939 970 ··· 957 988 958 989 "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], 959 990 991 + "xdg-app-paths": ["xdg-app-paths@5.1.0", "", { "dependencies": { "xdg-portable": "^7.0.0" } }, "sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA=="], 992 + 993 + "xdg-portable": ["xdg-portable@7.3.0", "", { "dependencies": { "os-paths": "^4.0.1" } }, "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw=="], 994 + 960 995 "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], 961 996 962 997 "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], ··· 970 1005 "@atproto-labs/did-resolver/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 971 1006 972 1007 "@atproto-labs/fetch-node/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], 1008 + 1009 + "@atproto-labs/fetch-node/undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], 973 1010 974 1011 "@atproto-labs/handle-resolver/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 975 1012 ··· 1033 1070 1034 1071 "@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], 1035 1072 1073 + "@vercel/sandbox/zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="], 1074 + 1036 1075 "accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], 1037 1076 1038 1077 "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], ··· 1059 1098 1060 1099 "send/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], 1061 1100 1062 - "tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], 1101 + "tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], 1063 1102 1064 1103 "tsx/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], 1065 1104 ··· 1220 1259 "pkgroll/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.26.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WAckBKaVnmFqbEhbymrPK7M086DQMpL1XoRbpmN0iW8k5JSXjDRQBhcZNa0VweItknLq9eAeCL34jK7/CDcw7A=="], 1221 1260 1222 1261 "send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], 1262 + 1263 + "tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], 1223 1264 1224 1265 "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], 1225 1266
+3
apps/api/package.json
··· 33 33 "@types/prompts": "^2.4.9", 34 34 "@types/ramda": "^0.31.1", 35 35 "@types/ws": "^8.18.1", 36 + "@vercel/sandbox": "^1.9.0", 36 37 "axios": "^1.13.5", 37 38 "better-sqlite3": "^12.6.2", 38 39 "chalk": "^5.6.2", ··· 46 47 "express": "^5.2.1", 47 48 "hono": "^4.11.9", 48 49 "ioredis": "^5.9.3", 50 + "jsonlines": "^0.1.1", 49 51 "jsonwebtoken": "^9.0.3", 50 52 "kysely": "^0.28.11", 51 53 "libsodium-wrappers": "^0.8.2", ··· 68 70 "@types/better-sqlite3": "^7.6.13", 69 71 "@types/cors": "^2.8.19", 70 72 "@types/express": "^5.0.6", 73 + "@types/jsonlines": "^0.1.5", 71 74 "@types/jsonwebtoken": "^9.0.10", 72 75 "@types/lodash": "^4.17.23", 73 76 "@types/morgan": "^1.9.10",
+2
apps/api/src/index.ts
··· 9 9 import API from "./xrpc"; 10 10 import ssh from "./ssh"; 11 11 import tty from "./tty"; 12 + import pty from "./pty"; 12 13 import { createRateLimiter } from "./ratelimiter"; 13 14 14 15 let server = createServer({ ··· 54 55 app.use(server.xrpc.router); 55 56 app.use("/ssh", ssh); 56 57 app.use("/tty", tty); 58 + app.use("/pty", pty); 57 59 58 60 app.listen(process.env.POCKETENV_XPRC_PORT || 8789, () => { 59 61 consola.log(chalk.greenBright(banner));
+230
apps/api/src/pty/index.ts
··· 1 + import { consola } from "consola"; 2 + import type { Context } from "context"; 3 + import * as context from "context"; 4 + import { eq, or } from "drizzle-orm"; 5 + import express, { Router } from "express"; 6 + import { env } from "lib/env"; 7 + import jwt from "jsonwebtoken"; 8 + import schema from "schema"; 9 + import decrypt from "lib/decrypt"; 10 + import path from "node:path"; 11 + import crypto from "node:crypto"; 12 + import fs from "fs/promises"; 13 + import { createListener } from "./pty-tunnel"; 14 + import { Sandbox, type Command } from "@vercel/sandbox"; 15 + import type { ListenerSocket } from "./pty-tunnel/websocket"; 16 + import { $ } from "zx"; 17 + 18 + const router = Router(); 19 + router.use((req, res, next) => { 20 + req.ctx = context.ctx; 21 + next(); 22 + }); 23 + router.use(express.json()); 24 + 25 + router.use((req, res, next) => { 26 + req.sandboxId = req.headers["x-sandbox-id"] as string | undefined; 27 + const authHeader = req.headers.authorization; 28 + const bearer = authHeader?.split("Bearer ")[1]?.trim(); 29 + if (bearer && bearer !== "null") { 30 + try { 31 + const credentials = jwt.verify(bearer, env.JWT_SECRET, { 32 + ignoreExpiration: true, 33 + }) as { did: string }; 34 + 35 + req.did = credentials.did; 36 + } catch (err) { 37 + consola.error("Invalid JWT token:", err); 38 + } 39 + } 40 + 41 + next(); 42 + }); 43 + 44 + type Session = { 45 + socket: ListenerSocket; 46 + clients: Set<express.Response>; 47 + }; 48 + 49 + const sessions = new Map<string, Session>(); 50 + 51 + const TERM = "xterm-256color"; 52 + const PTY_SERVER_DOWNLOAD_URL = 53 + "https://github.com/tsirysndr/pty-tunnel-server/releases/download/v0.0.2/pty-server-linux-x86_64.tar.gz"; 54 + const SERVER_BIN_NAME = "pty-tunnel-server"; 55 + 56 + type SandboxEnvironmentOptions = { 57 + id: string; 58 + vercelApiToken: string; 59 + vercelProjectId: string; 60 + vercelTeamId: string; 61 + }; 62 + 63 + async function checkIfServerInstalled(sandbox: Sandbox) { 64 + const exists = await sandbox.runCommand({ 65 + cmd: "command", 66 + args: ["-v", SERVER_BIN_NAME], 67 + }); 68 + return exists.exitCode === 0; 69 + } 70 + 71 + async function setupSandboxEnvironment( 72 + options: SandboxEnvironmentOptions, 73 + ): Promise<Sandbox> { 74 + const sandbox = await Sandbox.get({ 75 + sandboxId: options.id, 76 + token: options.vercelApiToken, 77 + projectId: options.vercelProjectId, 78 + teamId: options.vercelTeamId, 79 + }); 80 + 81 + if (!(await checkIfServerInstalled(sandbox))) { 82 + await $`bash -c "type /tmp/pty-tunnel-server || curl -L ${PTY_SERVER_DOWNLOAD_URL} | tar xz -C /tmp"`; 83 + 84 + const pathname = path.join("/tmp", `pty-server-${crypto.randomUUID()}`); 85 + await sandbox.writeFiles([ 86 + { 87 + path: pathname, 88 + content: await fs.readFile("/tmp/pty-tunnel-server"), 89 + }, 90 + ]); 91 + 92 + await sandbox.runCommand({ 93 + cmd: "bash", 94 + args: [ 95 + "-c", 96 + `mv "${pathname}" /usr/local/bin/${SERVER_BIN_NAME}; chmod +x /usr/local/bin/${SERVER_BIN_NAME}`, 97 + ], 98 + sudo: true, 99 + }); 100 + } 101 + 102 + await sandbox.runCommand({ 103 + cmd: SERVER_BIN_NAME, 104 + args: [ 105 + `--port=${sandbox.interactivePort}`, 106 + `--mode=client`, 107 + `--cols=${process.stdout.columns}`, 108 + `--rows=${process.stdout.rows}`, 109 + ], 110 + env: { 111 + TERM, 112 + PS1: `▲ \\[\\e[2m\\]\\w/\\[\\e[0m\\] `, 113 + }, 114 + detached: true, 115 + }); 116 + 117 + return sandbox; 118 + } 119 + 120 + async function createTerminalSession(ctx: Context, id: string) { 121 + const [record] = await ctx.db 122 + .select() 123 + .from(schema.sandboxes) 124 + .leftJoin( 125 + schema.vercelAuth, 126 + eq(schema.vercelAuth.sandboxId, schema.sandboxes.id), 127 + ) 128 + .where(or(eq(schema.sandboxes.id, id), eq(schema.sandboxes.sandboxId, id))) 129 + .execute(); 130 + 131 + if (!record?.vercel_auth) { 132 + consola.error("Vercel auth not found for sandbox", { id }); 133 + throw new Error("Vercel auth not found for sandbox " + id); 134 + } 135 + 136 + const sandbox = await setupSandboxEnvironment({ 137 + id, 138 + vercelApiToken: decrypt(record.vercel_auth.vercelToken), 139 + vercelProjectId: record.vercel_auth.projectId, 140 + vercelTeamId: record.vercel_auth.teamId, 141 + }); 142 + 143 + const listener = createListener(); 144 + const details = await listener.connection; 145 + const url = 146 + `wss://${sandbox.domain(sandbox.interactivePort!).replace(/^https?:\/\//, "")}` as const; 147 + consola.info("Connecting to WebSocket URL:", url); 148 + 149 + const socket = details.createClient(url); 150 + 151 + const session: Session = { 152 + socket, 153 + clients: new Set(), 154 + }; 155 + 156 + socket.addEventListener("message", async ({ data }) => { 157 + for (const res of session.clients) { 158 + res.write(`event: output\n`); 159 + res.write( 160 + `data: ${JSON.stringify({ data: data.toString("utf-8") })}\n\n`, 161 + ); 162 + } 163 + }); 164 + 165 + socket.waitForOpen(); 166 + 167 + sessions.set(id, session); 168 + return session; 169 + } 170 + 171 + async function getSession(ctx: Context, id: string) { 172 + return sessions.get(id) ?? (await createTerminalSession(ctx, id)); 173 + } 174 + 175 + router.get("/:id/stream", async (req, res) => { 176 + const { id } = req.params; 177 + const session = await getSession(req.ctx, id); 178 + 179 + res.setHeader("Content-Type", "text/event-stream"); 180 + res.setHeader("Cache-Control", "no-cache, no-transform"); 181 + res.setHeader("Connection", "keep-alive"); 182 + res.flushHeaders?.(); 183 + 184 + session.clients.add(res); 185 + 186 + const keepAlive = setInterval(() => { 187 + res.write(`: ping\n\n`); 188 + }, 15000); 189 + 190 + req.on("close", () => { 191 + clearInterval(keepAlive); 192 + session.clients.delete(res); 193 + }); 194 + }); 195 + 196 + router.post("/:id/input", express.text({ type: "*/*" }), async (req, res) => { 197 + const { id } = req.params; 198 + const session = await getSession(req.ctx, id); 199 + 200 + const input = typeof req.body === "string" ? req.body : ""; 201 + session.socket.sendMessage({ 202 + type: "message", 203 + message: input, 204 + }); 205 + 206 + res.status(204).end(); 207 + }); 208 + 209 + router.post("/:id/resize", async (req, res) => { 210 + const { id } = req.params; 211 + const session = await getSession(req.ctx, id); 212 + 213 + const cols = Number(req.body?.cols); 214 + const rows = Number(req.body?.rows); 215 + 216 + if (!Number.isInteger(cols) || !Number.isInteger(rows)) { 217 + res.status(400).json({ error: "Invalid cols/rows" }); 218 + return; 219 + } 220 + 221 + session.socket.sendMessage({ type: "ready" }); 222 + session.socket.sendMessage({ 223 + type: "resize", 224 + cols, 225 + rows, 226 + }); 227 + res.status(204).end(); 228 + }); 229 + 230 + export default router;
+2
apps/api/src/pty/pty-tunnel/index.ts
··· 1 + export { type Message } from "./messages"; 2 + export { createListener, type Listener } from "./websocket";
+53
apps/api/src/pty/pty-tunnel/messages.ts
··· 1 + export type Message = 2 + | { 3 + type: "message"; 4 + message: string; 5 + } 6 + | { type: "resize"; cols: number; rows: number } 7 + | { type: "ready" }; 8 + 9 + export function parse(buf: Buffer): Message | null { 10 + switch (buf.at(0)) { 11 + case 0: { 12 + // message 13 + return { type: "message", message: buf.subarray(1).toString("utf-8") }; 14 + } 15 + case 1: { 16 + // resize 17 + // must be at least 5 bytes 18 + if (buf.length < 5) return null; 19 + const cols = buf.readUInt16BE(1); 20 + const rows = buf.readUInt16BE(3); 21 + return { type: "resize", cols, rows }; 22 + } 23 + case 2: { 24 + // ready 25 + return { type: "ready" }; 26 + } 27 + } 28 + return null; 29 + } 30 + 31 + export function serialize(msg: Message): Buffer { 32 + switch (msg.type) { 33 + case "message": { 34 + const messageBuf = Buffer.from(msg.message, "utf-8"); 35 + const buf = Buffer.alloc(1 + messageBuf.length); 36 + buf.writeUInt8(0, 0); 37 + messageBuf.copy(buf, 1); 38 + return buf; 39 + } 40 + case "resize": { 41 + const buf = Buffer.alloc(5); 42 + buf.writeUInt8(1, 0); 43 + buf.writeUInt16BE(msg.cols, 1); 44 + buf.writeUInt16BE(msg.rows, 3); 45 + return buf; 46 + } 47 + case "ready": { 48 + const buf = Buffer.alloc(1); 49 + buf.writeUInt8(2, 0); 50 + return buf; 51 + } 52 + } 53 + }
+96
apps/api/src/pty/pty-tunnel/websocket.ts
··· 1 + import { parse, Parser } from "jsonlines"; 2 + import * as Messages from "./messages"; 3 + import type { Writable } from "node:stream"; 4 + import { WebSocket } from "ws"; 5 + 6 + export interface Connection { 7 + port: number; 8 + token: string; 9 + processId: number; 10 + serverProcessId: number; 11 + createClient(origin: `${"ws" | "wss"}://${string}`): ListenerSocket; 12 + } 13 + 14 + async function readConnectionInfo(stream: Parser): Promise<{ 15 + port: number; 16 + processId: number; 17 + token: string; 18 + serverProcessId: number; 19 + }> { 20 + try { 21 + for await (const msg of stream) { 22 + if ( 23 + msg && 24 + typeof msg.port === "number" && 25 + typeof msg.token === "string" && 26 + typeof msg.processId === "number" && 27 + typeof msg.serverProcessId === "number" 28 + ) { 29 + return msg; 30 + } 31 + } 32 + 33 + throw new Error("Did not receive port and token from server"); 34 + } finally { 35 + stream.end(); 36 + stream.destroy(); 37 + } 38 + } 39 + 40 + export function createListener(): { 41 + connection: Promise<Connection>; 42 + stdoutStream: Writable; 43 + } { 44 + const controlFd = parse(); 45 + return { 46 + stdoutStream: controlFd, 47 + connection: (async () => { 48 + const info = await readConnectionInfo(controlFd); 49 + 50 + const qs = new URLSearchParams({ 51 + processId: String(info.processId), 52 + token: info.token, 53 + }); 54 + 55 + return { 56 + port: info.port, 57 + token: info.token, 58 + processId: info.processId, 59 + serverProcessId: info.serverProcessId, 60 + createClient(origin: `${"ws" | "wss"}://${string}`) { 61 + return new ListenerSocket(`${origin}/ws/client?${qs}`); 62 + }, 63 + }; 64 + })(), 65 + }; 66 + } 67 + 68 + export type Listener = ReturnType<typeof createListener>; 69 + 70 + /** 71 + * A typed WebSocket that can send and receive pty-tunnel messages. 72 + */ 73 + export class ListenerSocket extends WebSocket { 74 + async waitForOpen(): Promise<this> { 75 + await waitForOpen(this); 76 + return this; 77 + } 78 + sendMessage(message: Messages.Message): void { 79 + return this.send(Messages.serialize(message)); 80 + } 81 + } 82 + 83 + async function waitForOpen(ws: WebSocket) { 84 + let release: (() => void) | undefined; 85 + await new Promise((resolve, reject) => { 86 + ws.addEventListener("open", resolve, { once: true }); 87 + ws.addEventListener("error", reject, { once: true }); 88 + ws.addEventListener("close", reject, { once: true }); 89 + release = () => { 90 + ws.removeEventListener("open", resolve); 91 + ws.removeEventListener("error", reject); 92 + ws.removeEventListener("close", reject); 93 + }; 94 + }); 95 + release?.(); 96 + }
+2 -2
apps/cli/src/cmd/ssh/index.ts
··· 82 82 await terminal(sandbox); 83 83 break; 84 84 case "vercel": 85 - // await terminal(sandbox); 85 + await tty(sandbox, false); // pty 86 86 break; 87 87 case "sprites": 88 - await tty(sandbox); 88 + await tty(sandbox, true); 89 89 break; 90 90 default: 91 91 consola.error(
+5 -5
apps/cli/src/cmd/ssh/tty.ts
··· 25 25 ): Promise<void> { 26 26 try { 27 27 await axios.post( 28 - `${ttyUrl}/tty/${sandboxId}/input`, 28 + `${ttyUrl}/${sandboxId}/input`, 29 29 data instanceof Buffer ? data.toString("utf-8") : data, 30 30 { 31 31 headers: { ··· 48 48 ): Promise<void> { 49 49 try { 50 50 await axios.post( 51 - `${ttyUrl}/tty/${sandboxId}/resize`, 51 + `${ttyUrl}/${sandboxId}/resize`, 52 52 { cols, rows }, 53 53 { 54 54 headers: { ··· 77 77 }; 78 78 } 79 79 80 - async function ssh(sandbox: Sandbox): Promise<void> { 80 + async function ssh(sandbox: Sandbox, tty: boolean = false): Promise<void> { 81 81 const token = await getAccessToken(); 82 82 const authToken = env.POCKETENV_TOKEN || token; 83 83 84 - const ttyUrl = env.POCKETENV_TTY_URL; 84 + const ttyUrl = tty ? env.POCKETENV_TTY_URL : env.POCKETENV_PTY_URL; 85 85 86 86 let cols = process.stdout.columns ?? 220; 87 87 let rows = process.stdout.rows ?? 50; ··· 157 157 // Open the SSE stream. 158 158 // eventsource v3 is fetch-based, so we inject the Authorization header via 159 159 // a custom fetch implementation instead of an `headers` init option. 160 - es = new EventSource(`${ttyUrl}/tty/${sandbox.id}/stream`, { 160 + es = new EventSource(`${ttyUrl}/${sandbox.id}/stream`, { 161 161 fetch: makeAuthFetch(authToken), 162 162 }); 163 163
+2 -1
apps/cli/src/lib/env.ts
··· 4 4 POCKETENV_TOKEN: str({ default: "" }), 5 5 POCKETENV_API_URL: str({ default: "https://api.pocketenv.io" }), 6 6 POCKETENV_CF_URL: str({ default: "https://sbx.pocketenv.io" }), 7 - POCKETENV_TTY_URL: str({ default: "https://api.pocketenv.io" }), 7 + POCKETENV_TTY_URL: str({ default: "https://api.pocketenv.io/tty" }), 8 + POCKETENV_PTY_URL: str({ default: "https://api.pocketenv.io/pty" }), 8 9 POCKETENV_PUBLIC_KEY: str({ 9 10 default: "2bf96e12d109e6948046a7803ef1696e12c11f04f20a6ce64dbd4bcd93db9341", 10 11 }),
+1
apps/sandbox/src/providers/vercel/mod.ts
··· 2 2 import { Sandbox } from "@vercel/sandbox"; 3 3 import consola from "consola"; 4 4 import path from "node:path"; 5 + import { env } from "node:process"; 5 6 import { Buffer } from "node:buffer"; 6 7 import crypto from "node:crypto"; 7 8
+10 -6
apps/web/src/components/terminal/TtyTerminal.tsx
··· 61 61 isDarkMode: boolean; 62 62 sandboxId: string; 63 63 onClose: () => void; 64 + pty?: boolean; 64 65 } 65 66 66 67 function authHeaders(): Record<string, string> { ··· 72 73 isDarkMode, 73 74 sandboxId, 74 75 onClose, 76 + pty, 75 77 }: TerminalContentProps) { 76 78 // Stable refs so the main effect never re-runs because these changed 77 79 const eventSourceRef = useRef<EventSource | null>(null); 78 80 const fitAddonRef = useRef<FitAddon | null>(null); 79 81 const sandboxIdRef = useRef(sandboxId); 80 82 const onCloseRef = useRef(onClose); 83 + 84 + const BASE_URL = `${API_URL}/${pty ? "pty" : "tty"}`; 81 85 82 86 // Keep refs in sync with the latest props after every render, 83 87 // without listing them as effect deps (which would retrigger connect). ··· 137 141 // --- Helper functions that read latest values from refs --- 138 142 const sendInput = async (data: string) => { 139 143 try { 140 - await fetch(`${API_URL}/tty/${sandboxIdRef.current}/input`, { 144 + await fetch(`${BASE_URL}/${sandboxIdRef.current}/input`, { 141 145 method: "POST", 142 146 headers: { "Content-Type": "text/plain", ...authHeaders() }, 143 147 body: data, ··· 149 153 150 154 const sendResize = async (cols: number, rows: number) => { 151 155 try { 152 - await fetch(`${API_URL}/tty/${sandboxIdRef.current}/resize`, { 156 + await fetch(`${BASE_URL}/${sandboxIdRef.current}/resize`, { 153 157 method: "POST", 154 158 headers: { "Content-Type": "application/json", ...authHeaders() }, 155 159 body: JSON.stringify({ cols, rows }), ··· 177 181 178 182 instance.write(`\x1b[35mConnecting to terminal...\x1b[0m\r\n`); 179 183 180 - const es = new EventSource( 181 - `${API_URL}/tty/${sandboxIdRef.current}/stream`, 182 - ); 184 + const es = new EventSource(`${BASE_URL}/${sandboxIdRef.current}/stream`); 183 185 eventSourceRef.current = es; 184 186 185 187 es.addEventListener("open", () => { ··· 262 264 sandboxId: string; 263 265 worker: string; 264 266 onClose: () => void; 267 + pty?: boolean; 265 268 } 266 269 267 - function TtyTerminal({ sandboxId, onClose }: TtyTerminalProps) { 270 + function TtyTerminal({ sandboxId, onClose, pty }: TtyTerminalProps) { 268 271 const [isDarkMode, setIsDarkMode] = useState( 269 272 document.documentElement.classList.contains("dark"), 270 273 ); ··· 292 295 isDarkMode={isDarkMode} 293 296 sandboxId={sandboxId} 294 297 onClose={onClose} 298 + pty={pty} 295 299 /> 296 300 ); 297 301 }
+1
apps/web/src/components/terminal/index.tsx
··· 8 8 worker: string; 9 9 isCloudflare?: boolean; 10 10 isTty?: boolean; 11 + pty?: boolean; 11 12 }; 12 13 13 14 function Terminal(props: TerminalProps) {
+2 -1
apps/web/src/pages/projects/Project/Project.tsx
··· 121 121 }} 122 122 sandboxId={sandbox.id} 123 123 isCloudflare={sandbox.provider === "cloudflare"} 124 - isTty={sandbox.provider === "sprites"} 124 + isTty={["sprites", "vercel"].includes(sandbox.provider)} 125 + pty={sandbox.provider === "vercel"} 125 126 worker={sandbox.baseSandbox} 126 127 /> 127 128 </td>
+3
apps/web/src/pages/projects/Project/TerminalModal/TerminalModal.tsx
··· 10 10 title?: string; 11 11 isCloudflare?: boolean; 12 12 isTty?: boolean; 13 + pty?: boolean; 13 14 }; 14 15 15 16 function TerminalModal({ ··· 20 21 sandboxId, 21 22 isCloudflare, 22 23 isTty, 24 + pty, 23 25 }: TerminalModalProps) { 24 26 const [isFullscreen, setIsFullscreen] = useState(false); 25 27 ··· 136 138 isCloudflare={isCloudflare} 137 139 worker={worker} 138 140 isTty={isTty} 141 + pty={pty} 139 142 /> 140 143 </div> 141 144 </div>
+4 -1
apps/web/src/pages/sandbox/Sandbox.tsx
··· 327 327 onClose={() => {}} 328 328 worker={data.sandbox.baseSandbox} 329 329 isCloudflare={data.sandbox.provider === "cloudflare"} 330 - isTty={data.sandbox.provider === "sprites"} 330 + isTty={["sprites", "vercel"].includes( 331 + data.sandbox.provider, 332 + )} 333 + pty={data.sandbox.provider === "vercel"} 331 334 /> 332 335 )} 333 336 </div>