Пишешь JavaScript-файл, кидаешь в комнату — и он запускается в каждом браузере одновременно, по WebSockets. Мини-игры, рисовалки, квизы — всё, что умещается на один диск.
Это описание продукта с главной страницы. А теперь давай поговорим, зачем всё это.
В интернете сейчас куча сервисов, которые сами по себе маленькие, но каждый живёт отдельно. Gartic Phone, Jackbox, skribbl.io, Tally — под каждую мелочь своя вкладка, свой сайт, свои правила. Отсюда и появилась идея: собрать всю мелочь в одно место! А может, и не только мелочь.
Так и получилась платформа, которая почти не ограничивает воображение в рамках одного JS-файла внутри комнаты. И главное — все копии этого файла соединены по сети. В этом вся сила: не запустить, а запустить вместе.
Но у идеи сразу есть цена. Если файл может написать любой, то ты запускаешь у себя чужой код просто потому, что кто-то кинул ссылку. Это и был главный вопрос всего проекта. Поэтому с него и начну, а дальше по порядку: что вообще умеет один диск, как выглядит полка и чего я делать не обещаю.
Почему чужой код тут можно запускать спокойно
Всё держится на одном вопросе: что именно этот код может сделать. Поэтому сначала список, а потом — из чего он следует.
Сети у диска нет вообще: ни fetch, ни вебсокета, ни картинки со стороннего сайта. Хранилища нет — ни куки, ни localStorage, ничего, что переживёт вкладку. До страницы вокруг он не дотянется: у него свой origin, и ни твоя сессия, ни адресная строка ему не видны. Буфер обмена не отдают ни на чтение, ни на запись.
Получает он твой ник, звук, захват указателя и полноэкранную сцену — причём в фуллскрин уходит рамка платформы вместе с диском, так что весь экран он занять не может.
Разрешений тут нет вообще: ни экрана согласия, ни галочек, ни поля в протоколе. Набор один и тот же всегда, поэтому его не выбирают, а называют — при создании комнаты и ещё раз в лобби, чтобы тот, кто вошёл по чужому коду, тоже это увидел.
Теперь механика. Диск живёт в <iframe sandbox="allow-scripts"> на отдельном origin, а документ фрейма отдаётся с connect-src 'none'. Три вещи тянут тут всю нагрузку.
connect-src 'none'— вот что закрывает сеть. Атрибутsandboxсеть не запрещает: фрейм с opaque origin спокойно сходитfetchкуда угодно, где разрешён CORS.allow-scriptsбезallow-same-origin. Вместе эти два флага снимают песочницу сами с себя. У меня их вместе нет.- Отдельный origin. Site isolation даёт фрейму свой процесс, поэтому зациклившийся диск вешает себя, а не вкладку вместе с кнопкой «Kill script».
Никакого статического анализа кода здесь нет и не будет. Обещание в интерфейсе должно опираться на механизм, а не на то, что кто-то посмотрел на файл глазами.
Есть диск, написанный специально для того, чтобы вылезти наружу: он перебирает два десятка способов — достучаться до сети, до хранилища, до буфера обмена, до страницы вокруг, снять с себя песочницу. Он запускается в настоящем браузере при каждой сборке, и ни один способ не срабатывает. Второй диск делает обратное: заходит в комнату и показывает, что ему на самом деле досталось.
Политику фрейма платформа печатает сама — в студии есть панель, где она показана ровно в том виде, в каком её отдаёт сервер и получает браузер.
Что умеет один диск
Файл я называю диском, хотя по духу это скорее картридж от Денди: вставил — и играешь, ничего не устанавливая. Один .js, а в шапке комментарий с манифестом: имя, автор, версия, сколько игроков, описание, теги. Всё. Никаких package.json, сборки и зависимостей — платформа берёт файл как есть.
Внутри файла тебе доступен ровно один объект. Вот он:
room.me // { id, nick, avatar } — твой настоящий ник
room.players // кто сейчас в комнате
room.isHost // факт, а не роль
room.send(payload) // всем остальным
room.send(payload, { to: playerId }) // одному участнику
room.on('message', (from, payload) => {})
room.on('join', (player) => {})
room.on('leave', (player) => {})
room.on('hostchange', (player) => {})
Это весь API.
Ни тиков, ни состояния, ни синхронизации тут нет. Игровой цикл, кто главный, что делать при расхождении, как всё это рисовать — дело самого диска. Платформа доставляет байты и даёт коробку, из которой не вылезти.
Но этого хватает надолго. Pong, «Чапаев» на физике, рисовалка, квиз — всё в библиотеке написано вот этими шестью строчками.
Что я обещаю про доставку
Три правила.
- Порядок одного отправителя сохраняется. Что ты отправил первым, то первым и придёт.
- Порядок в комнате один на всех. Комната принимает сообщения по одному, и все, кто их получил, получили в одном и том же порядке. А вот угадать, чьё из двух одновременных примут раньше, нельзя.
- Доставка не более одного раза. Оборвалась связь — сообщения потеряны, и переподключение историю не переигрывает. Это сознательно.
Лимиты стоят на соединении, а не на комнате — так шумный участник тормозит себя, а не всех: 4 KiB на сообщение, 60 отправок в секунду, 8 человек в комнате. Превышение не рвёт связь: сообщение отбрасывается, приходит уведомление, соединение живёт дальше.
Как выглядит полка дисков
У каждого диска постоянный адрес — /disk/<hash>. Хеш и есть идентичность: отредактировал файл — получились другие байты, другой хеш и другая запись. Подменить содержимое по чужой ссылке нельзя в принципе, потому что ссылка называет байты, а не имя.
Аккаунтов тут нет вообще. Сессия выдаётся на первом визите, и единственное, что в ней можно поменять, — ник. Имя автора в манифесте ничем не подтверждено: ничто в системе на него не завязано.
Диск можно потрогать, никого не приглашая
Чужой диск не нужно брать на веру и не нужно ради него собирать людей в комнате, ещё не зная, чем он окажется.
На странице каждого диска есть Run it here. Те же самые байты запускаются прямо там, в том же самом фрейме, в который их монтирует комната. Ничего не стартует само — запускаешь ты.
Можно в одиночку — тогда диску говорят правду о том, где он: комнаты нет, room.me равен null. А можно на два места, и это уже та же Multiplayer lab, что живёт в студии: до четырёх локальных мест, которые заходят, выходят и перезапускаются, с задержкой на выбор — мгновенно, 60, 180 или 500 мс. Так проверяются состав, адресные сообщения, поздний вход и передача хоста — до публикации.
Студия и агенты
Манифест читает один и тот же код с обеих сторон: сервер — при загрузке, страница — пока ты печатаешь. Продублировать эти правила на одной стороне нельзя, иначе упрёшься в отказ там, где поле показывает «всё хорошо».
Диск — идеальная задача для агента: всё, что диску позволено, умещается в один файл. Поэтому правила опубликованы одним файлом — diskroom.app/skill.md: грамматика манифеста, весь room API, политика фрейма, гарантии доставки и полностью рабочий диск в конце. Кнопка Prompt for an agent отдаёт готовый текст: адрес гайда и инструкцию сохранить его как скилл. Ничего не устанавливается и ничего не уходит со страницы — диск возвращается текстом и вставляется в тот же редактор.
Живой пример: переписка с агентом, из которой получился вот этот диск. Он лежит в библиотеке и запускается прямо на своей странице.
Чего я делать не обещаю
Есть вещи, которых diskroom не делает:
- Лимитов CPU и памяти нет. Браузер таких ручек не даёт. Потолок — heartbeat и кнопка «Kill script»; бомба по памяти всё ещё уронит вкладку.
- Античита нет. Диск исполняется в браузере игрока, который этот браузер полностью контролирует. Авторитет хоста — это доверие хосту, как в старых P2P-играх.
- От фишингового интерфейса, нарисованного самим диском, защиты нет — кроме того, что фрейм всегда сидит внутри видимой рамки платформы и никогда не занимает весь экран.
Что внутри
Бэкенд на Rust: axum держит HTTP и вебсокеты, он же раздаёт документ фрейма вместе с его политикой. Типы провода лежат на postcard, и там же собраны все лимиты — в одном месте, чтобы они не разъезжались между сервером и страницей. Фронтенд — Leptos, скомпилированный в wasm; вебсокетом владеет именно он, потому что сокет принадлежит платформе, а не диску. Хранилище — Postgres.
Репозиторий пока закрытый, так что показать могу только результат.
Огромную помощь оказали Claude и Codex — без них это заняло бы сильно больше времени.
Попробуй
Заходить можно без регистрации, её тут просто нет. Открываешь diskroom.app, тыкаешь любой диск в библиотеке, жмёшь Run it here — и всё, никого звать не надо.
А если захочется своё — один файл и шапка из шести строк.
Мне интересно узнать ваше мнение! Пишите в Telegram: @Tikitko