From f35ee2aaa2b2c9b996c4246fda4bd3b9cd257787 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Sat, 18 Jul 2026 18:06:44 +0800 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D1=86=D0=B8=D1=8F:=20=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D1=91=D0=BD=20AGENTS.md=20(API),=20=D1=83=D0=B4?= =?UTF-8?q?=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B=20superpowers-=D0=BF=D0=BB=D0=B0?= =?UTF-8?q?=D0=BD=D1=8B,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20README.ru.md=20=D1=81=20=D1=88=D0=B0=D0=BF=D0=BA=D0=BE?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + AGENTS.md | 71 ++++++-- README.md | 14 ++ README.ru.md | 158 ++++++++++++++++++ .../plans/2026-07-10-sync-tenant-isolation.md | 99 ----------- ...2026-07-10-sync-tenant-isolation-design.md | 92 ---------- 6 files changed, 228 insertions(+), 207 deletions(-) create mode 100644 README.ru.md delete mode 100644 docs/superpowers/plans/2026-07-10-sync-tenant-isolation.md delete mode 100644 docs/superpowers/specs/2026-07-10-sync-tenant-isolation-design.md diff --git a/.gitignore b/.gitignore index 4c3e12a..a2771fd 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ server-data/ .DS_Store Thumbs.db release/ +pass diff --git a/AGENTS.md b/AGENTS.md index 16b05a0..1791128 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,16 +11,49 @@ - Плагины явно указывают, какие данные можно синхронизировать. - Sync — не источник правды, а дополнение. -## API +## API (текущий) + +Desktop sync client: ``` -POST /api/v1/pair — создание пары устройство-сервер -POST /api/v1/auth — аутентификация -GET /api/v1/devices — список устройств -POST /api/v1/sync — синхронизация операций -GET /api/v1/blob/:hash — скачать blob -PUT /api/v1/blob/:hash — загрузить blob -GET /api/v1/operations — получить операции с последнего sync point +POST /api/client/pair — pair device with username/password +POST /api/auth/test — validate username/password +GET /api/client/me — current authenticated device details +POST /api/client/revoke-current — revoke current device token +POST /api/client/revoke-device — revoke another device +POST /api/v1/sync/push — push local operations +POST /api/v1/sync/pull — pull operations since server sequence +POST /api/v1/blobs/ — store a blob, return SHA-256 +GET /api/v1/blobs/{sha256} — download a stored blob +``` + +User API: + +``` +POST /api/v1/auth/register — register a user +GET /api/v1/auth/confirm — confirm email (GET shows form, POST confirms) +POST /api/v1/auth/login — user login +POST /api/v1/auth/forgot — request password reset +POST /api/v1/auth/reset — reset password +GET /api/v1/user/devices — list user devices +``` + +Operational: + +``` +GET /api/v1/health — server health and storage status +GET /livez — liveness probe +GET /readyz — readiness probe +``` + +Embedded web console: + +``` +GET / — public page +GET /login, /register, /forgot, /reset, /logout +GET /dashboard — user device management +GET /admin/login — admin console +GET /admin/... — admin sections (users, devices, vaults, storage, audit, SMTP, diagnostics) ``` ## Структура @@ -31,14 +64,20 @@ verstak-sync-server/ cmd/ server/ internal/ - api/ - auth/ - device/ - vault/ - blob/ - conflict/ - migrations/ - ... + server/ + server.go + routes.go + handlers_api.go + handlers_auth.go + handlers_admin.go + schema.go + web/ + ... go.mod README.md ``` + +## Подробнее + +См. [README.md](README.md) для полной документации по развёртыванию, +конфигурации, backup/restore и архитектуре. diff --git a/README.md b/README.md index 56b01d6..48b4069 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ +
+ +# Verstak Sync Server + +### Optional self-hosted synchronization relay for Verstak vaults. + +**English** · [Русский](README.ru.md) + +[![Release](https://img.shields.io/github/v/release/mirivlad/verstak-sync-server?include_prereleases\&label=release)](https://github.com/mirivlad/verstak-sync-server/releases) +![Status](https://img.shields.io/badge/status-alpha-orange) +[![License](https://img.shields.io/github/license/mirivlad/verstak-sync-server)](LICENSE) + +
+ # Verstak Sync Server Standalone sync server for Verstak2 platform. diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 0000000..1891ba6 --- /dev/null +++ b/README.ru.md @@ -0,0 +1,158 @@ +
+ +# Verstak Sync Server + +### Собственный сервер синхронизации для vault-хранилищ Верстака. + +[English](README.md) · **Русский** + +[![Релиз](https://img.shields.io/github/v/release/mirivlad/verstak-sync-server?include_prereleases\&label=release)](https://github.com/mirivlad/verstak-sync-server/releases) +![Статус](https://img.shields.io/badge/status-alpha-orange) +[![Лицензия](https://img.shields.io/github/license/mirivlad/verstak-sync-server)](LICENSE) + +
+ +> **Alpha-версия.** Сервер синхронизации дополняет локальный рабочий процесс. +> Desktop-приложение остаётся основным источником данных. + +## Обзор + +Сервер синхронизации обеспечивает обмен данными между устройствами с Verstak Desktop: + +- Регистрация устройств и аутентификация +- Упорядоченный журнал операций с серверными номерами последовательности +- Передача бинарных и больших файлов через Blob API +- Управление пользователями с подтверждением email +- Встроенная веб-консоль для администратора и пользователей + +## Быстрый старт + +```bash +# Сборка +./scripts/build.sh + +# Запуск +./build/bin/verstak-sync-server --data ./server-data + +# Первый запуск с администратором +printf '%s\n' 'выберите-длинный-пароль' > /tmp/verstak-admin-password +chmod 600 /tmp/verstak-admin-password +./build/bin/verstak-sync-server --admin-user admin --admin-pass-file /tmp/verstak-admin-password +``` + +## Установка + +Соберите бинарник и запустите установочный скрипт: + +```bash +./scripts/build.sh +sudo ./scripts/install.sh \ + --bin ./build/bin/verstak-sync-server \ + --listen 127.0.0.1:47732 \ + --admin-user admin \ + --admin-pass-file /root/verstak-admin-password +``` + +Скрипт создаёт системного пользователя, инициализирует каталог данных и +устанавливает systemd-сервис `verstak-server`. + +Основные операции: + +```bash +sudo systemctl status verstak-server +sudo journalctl -u verstak-server -f +curl http://127.0.0.1:47732/api/v1/health +``` + +## Развёртывание + +В production сервер должен работать за HTTPS. Сам сервер слушает plain HTTP; +TLS терминируется в обратном прокси (nginx, Caddy). + +Пример nginx: + +```nginx +location / { + proxy_pass http://127.0.0.1:47732; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} +``` + +Всегда привязывайте сервер к loopback (`127.0.0.1`), если явно не нужен внешний +доступ. + +## Резервное копирование и восстановление + +Создание резервной копии (сервис должен быть остановлен): + +```bash +sudo systemctl stop verstak-server +sudo tar --xattrs --acls -czf verstak-sync-backup-$(date +%Y%m%d-%H%M%S).tar.gz \ + -C /var/lib verstak-sync-server +sudo systemctl start verstak-server +``` + +Восстановление: + +```bash +sudo systemctl stop verstak-server +sudo tar --xattrs --acls -xzf verstak-sync-backup-ГГГГММДД-ЧЧММСС.tar.gz -C /var/lib +sudo chown -R verstak:verstak /var/lib/verstak-sync-server +sudo systemctl start verstak-server +``` + +## API + +Desktop sync client: + +| Метод | Назначение | +|-------|------------| +| `POST /api/client/pair` | Сопряжение устройства | +| `POST /api/auth/test` | Проверка учётных данных | +| `GET /api/client/me` | Информация о текущем устройстве | +| `POST /api/v1/sync/push` | Отправка локальных операций | +| `POST /api/v1/sync/pull` | Получение операций с сервера | +| `POST /api/v1/blobs/` | Загрузка бинарного файла | +| `GET /api/v1/blobs/{sha256}` | Скачивание бинарного файла | + +User API: + +| Метод | Назначение | +|-------|------------| +| `POST /api/v1/auth/register` | Регистрация | +| `POST /api/v1/auth/login` | Вход | +| `GET /api/v1/user/devices` | Список устройств | + +Операционные: + +| Метод | Назначение | +|-------|------------| +| `GET /api/v1/health` | Состояние сервера | + +Административная веб-консоль доступна по `/admin/login`. + +## Встроенная веб-консоль + +Сервер включает встроенную веб-консоль без внешних зависимостей: + +- Локализованные публичные страницы (System / English / Русский) +- Личный кабинет пользователя: устройства, подтверждение email +- Административная панель: пользователи, устройства, vaults, хранилище, + аудит, SMTP, диагностика +- Защита сессиями, CSRF-токенами и security-заголовками + +## Архитектура + +Сервер не является источником истины для данных. Desktop остаётся local-first. +Сервер передаёт операции и blob-файлы между устройствами, но не сливает +содержимое файлов, не разрешает конфликты и не создаёт замещающих имён. + +Файлы не имеют сквозного шифрования в текущем milestone. Секреты, настройки +плагинов, задачи, журнал, активность и браузерный inbox не синхронизируются. + +## Лицензия + +Copyright © 2026 Verstak contributors. Распространяется на условиях +[GNU AGPLv3 или новее](LICENSE). diff --git a/docs/superpowers/plans/2026-07-10-sync-tenant-isolation.md b/docs/superpowers/plans/2026-07-10-sync-tenant-isolation.md deleted file mode 100644 index 960811a..0000000 --- a/docs/superpowers/plans/2026-07-10-sync-tenant-isolation.md +++ /dev/null @@ -1,99 +0,0 @@ -# Sync Tenant Isolation Implementation Plan - -> **For agentic workers:** execute this plan task by task with focused tests -> before each implementation change. - -**Goal:** prevent cross-user and cross-vault operation visibility while binding -the persisted source device to the authenticated token. - -**Architecture:** the server derives user, device, and vault scope from the -bearer token. SQLite rows carry that scope; desktop sends the immutable vault -ID only while creating a pairing. Existing unscoped devices and operations are -migrated into a deterministic legacy scope. - -**Tech stack:** Go, `database/sql`, SQLite, `net/http`, desktop Go sync client. - -## Task 1: Establish server behaviour tests - -**Files:** -- Modify: `internal/server/server_test.go` - -- [x] Add helpers that create confirmed users and token-authenticated devices - with a specified vault ID. -- [x] Add a failing test where separate users push and pull from the same - vault ID; each pull must contain only its own operation and cursor. -- [x] Add a failing test where one user owns devices in two vaults; pulls must - remain vault-local. -- [x] Add a failing test that sends another device's ID in `push`; assert the - stored and returned operation uses the authenticated device ID. -- [x] Add a failing test for identical idempotency keys in different scopes. -- [x] Run: `go test ./internal/server -run 'TestSync.*Isolation|TestSyncPush'` - and confirm the new assertions fail for the intended missing behaviour. - -## Task 2: Add idempotent SQLite scope migration - -**Files:** -- Modify: `internal/server/schema.go` -- Modify: `internal/server/server.go` -- Test: `internal/server/server_test.go` - -- [x] Define `vault_id` on new devices and `user_id`/`vault_id` on new - operations; define scoped tombstone and idempotency primary keys. -- [x] Add startup migration helpers that inspect columns, add compatible - columns, backfill owner IDs, assign `legacy:` to old scopes, and - rebuild the two tables whose primary keys change. -- [x] Add a failing legacy-schema fixture test, then make it pass by opening - the database through `NewServer` and asserting its operation has the - expected owner and legacy scope. -- [x] Run: `go test ./internal/server -run 'Test.*Migration|TestSync.*'`. - -## Task 3: Apply authenticated scope to sync handlers - -**Files:** -- Modify: `internal/server/middleware.go` -- Modify: `internal/server/handlers_api.go` -- Test: `internal/server/server_test.go` - -- [x] Extend authenticated device lookup to provide the effective vault scope; - missing user ownership must not authorize sync operations. -- [x] Require `vault_id` when creating a new client pairing and store it with - the device. -- [x] Make push use authenticated device/user/vault values for inserts, - conflicts, revisions, tombstones, and idempotency lookup/storage. -- [x] Make pull filter operations and its reported cursor by authenticated - user/vault. -- [x] Run the focused tests from Task 1 until green, then - `go test ./internal/server`. - -## Task 4: Send the current vault ID while pairing - -**Files:** -- Modify: `../verstak-desktop/internal/core/sync/client.go` -- Modify: `../verstak-desktop/internal/core/sync/client_test.go` -- Modify: `../verstak-desktop/internal/api/app.go` -- Modify: `../verstak-desktop/internal/api/app_test.go` - -- [x] Add `vault_id` to the pair request and expose it in the pairing client - method without changing push/pull wire compatibility. -- [x] Read the open vault metadata in `syncConfigure`; reject configuration if - the vault ID is absent. -- [x] Add a failing client/API test that captures the pair request and asserts - the persistent vault ID is sent. -- [x] Rebind desktop sync state, cursor, and persisted device identity whenever - a vault is created, opened, or switched. -- [x] Hydrate missing legacy vault device IDs from the authenticated sync - server before a token-based sync can use a global fallback. -- [x] Run: `go test ./internal/core/sync ./internal/api`. - -## Task 5: Document, verify, and publish - -**Files:** -- Modify: `README.md` -- Modify: `docs/superpowers/specs/2026-07-10-sync-tenant-isolation-design.md` - -- [x] Document that pairing is vault-bound and that sync cursors are scoped. -- [x] Run `gofmt` on all changed Go files. -- [x] Run `go test ./...` in both `verstak-sync-server` and - `verstak-desktop`, then `git diff --check` in both repositories. -- [x] Commit and push the sync-server and desktop changes as coordinated - security commits. diff --git a/docs/superpowers/specs/2026-07-10-sync-tenant-isolation-design.md b/docs/superpowers/specs/2026-07-10-sync-tenant-isolation-design.md deleted file mode 100644 index dc64f86..0000000 --- a/docs/superpowers/specs/2026-07-10-sync-tenant-isolation-design.md +++ /dev/null @@ -1,92 +0,0 @@ -# Sync Tenant Isolation Design - -**Status:** approved for implementation by the project owner on 2026-07-10. - -## Goal - -Make sync operations private to the authenticated user and vault, and make the -server, not the request body, the authority for the originating device. - -## Scope - -- Bind new desktop pairings to the current vault ID. -- Persist `user_id` and `vault_id` with every sync operation. -- Scope push conflict detection, pull cursors, tombstones, and idempotency - responses to that pair of identifiers. -- Ignore the legacy `device_id` field in a push body for authorization and - storage; retain it in the wire format for backward-compatible decoding. -- Upgrade existing SQLite databases without deleting data. - -Blob ownership, API-key retirement, reset-token handling, HTML escaping, and -upload limits are separate security slices and are deliberately not included -in this change. - -## Data model - -`server_devices` gains a nullable `vault_id`. A device created through either -enrollment endpoint must have a non-empty vault ID that does not use the -reserved `legacy:` prefix. The authenticated device therefore identifies one -user and one vault. - -`server_ops` gains `user_id` and `vault_id`. New writes always set both from -the authenticated device. Pull and conflict queries filter both fields. - -`server_tombstones` is rebuilt with a composite key of -`(user_id, vault_id, entity_type, entity_id)`. `server_idempotency_keys` is -rebuilt with a composite key of `(user_id, vault_id, idempotency_key)`. - -Existing devices without `vault_id` use the explicit effective scope -`legacy:`. Existing operations inherit their device owner and that -legacy scope during startup migration. This preserves existing single-vault -accounts while preventing data from crossing account boundaries. New pairings -never use the legacy scope. - -Desktop sync state, operation queues, cursors, and persisted device IDs are -vault-local. The desktop recreates its sync service whenever the active vault -is created, opened, or switched. When it opens a legacy vault state without a -stored device ID, it obtains the authenticated ID from `/api/client/me` before -syncing and persists it locally. - -## API contract - -`POST /api/client/pair` accepts a required `vault_id`. The desktop gets it -from `.verstak/vault.json` and sends it while pairing. - -`POST /api/v1/sync/push` keeps accepting `device_id` for old clients, but the -server ignores it. The stored operation device ID is always the authenticated -device. A request whose token is not associated with a user and effective -vault returns a client error. - -`POST /api/v1/sync/pull` returns only operations from the authenticated -user/vault scope. `server_sequence` is the highest sequence in that scope; -global sequence gaps are not exposed as the caller's cursor. - -## Migration and failure handling - -Startup migration is idempotent. It checks SQLite table columns before adding -new operation/device fields, backfills `user_id` from each operation's device, -and assigns the explicit legacy scope when an old device has no vault ID. -Tables whose primary key must change are rebuilt transactionally. - -The prior global idempotency cache is intentionally discarded during migration: -it is only a replay cache and retaining it could replay one tenant's response -for another tenant. - -If an operation cannot be associated with a user, it remains unscoped and is -not readable through sync APIs. The server must not guess an owner from a -request body. - -## Verification - -Focused server tests must prove: - -1. two users cannot pull each other's operations; -2. two vaults of one user cannot pull each other's operations; -3. a caller cannot forge another device through the push body; -4. scoped idempotency does not replay another tenant's response; -5. a legacy SQLite database is upgraded with its existing operation retained - in the matching legacy scope. -6. switching the active desktop vault rebinds the sync queue, cursor, and - device identity to the new vault. - -Desktop tests must prove that pairing sends the opened vault's persistent ID.