Документация: обновлён AGENTS.md (API), удалены superpowers-планы, добавлен README.ru.md с шапкой
This commit is contained in:
parent
d1e4068636
commit
f35ee2aaa2
|
|
@ -15,3 +15,4 @@ server-data/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
release/
|
release/
|
||||||
|
pass
|
||||||
|
|
|
||||||
71
AGENTS.md
71
AGENTS.md
|
|
@ -11,16 +11,49 @@
|
||||||
- Плагины явно указывают, какие данные можно синхронизировать.
|
- Плагины явно указывают, какие данные можно синхронизировать.
|
||||||
- Sync — не источник правды, а дополнение.
|
- Sync — не источник правды, а дополнение.
|
||||||
|
|
||||||
## API
|
## API (текущий)
|
||||||
|
|
||||||
|
Desktop sync client:
|
||||||
|
|
||||||
```
|
```
|
||||||
POST /api/v1/pair — создание пары устройство-сервер
|
POST /api/client/pair — pair device with username/password
|
||||||
POST /api/v1/auth — аутентификация
|
POST /api/auth/test — validate username/password
|
||||||
GET /api/v1/devices — список устройств
|
GET /api/client/me — current authenticated device details
|
||||||
POST /api/v1/sync — синхронизация операций
|
POST /api/client/revoke-current — revoke current device token
|
||||||
GET /api/v1/blob/:hash — скачать blob
|
POST /api/client/revoke-device — revoke another device
|
||||||
PUT /api/v1/blob/:hash — загрузить blob
|
POST /api/v1/sync/push — push local operations
|
||||||
GET /api/v1/operations — получить операции с последнего sync point
|
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/
|
cmd/
|
||||||
server/
|
server/
|
||||||
internal/
|
internal/
|
||||||
api/
|
server/
|
||||||
auth/
|
server.go
|
||||||
device/
|
routes.go
|
||||||
vault/
|
handlers_api.go
|
||||||
blob/
|
handlers_auth.go
|
||||||
conflict/
|
handlers_admin.go
|
||||||
migrations/
|
schema.go
|
||||||
...
|
web/
|
||||||
|
...
|
||||||
go.mod
|
go.mod
|
||||||
README.md
|
README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Подробнее
|
||||||
|
|
||||||
|
См. [README.md](README.md) для полной документации по развёртыванию,
|
||||||
|
конфигурации, backup/restore и архитектуре.
|
||||||
|
|
|
||||||
14
README.md
14
README.md
|
|
@ -1,3 +1,17 @@
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# Verstak Sync Server
|
||||||
|
|
||||||
|
### Optional self-hosted synchronization relay for Verstak vaults.
|
||||||
|
|
||||||
|
**English** · [Русский](README.ru.md)
|
||||||
|
|
||||||
|
[](https://github.com/mirivlad/verstak-sync-server/releases)
|
||||||
|

|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
# Verstak Sync Server
|
# Verstak Sync Server
|
||||||
|
|
||||||
Standalone sync server for Verstak2 platform.
|
Standalone sync server for Verstak2 platform.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# Verstak Sync Server
|
||||||
|
|
||||||
|
### Собственный сервер синхронизации для vault-хранилищ Верстака.
|
||||||
|
|
||||||
|
[English](README.md) · **Русский**
|
||||||
|
|
||||||
|
[](https://github.com/mirivlad/verstak-sync-server/releases)
|
||||||
|

|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
> **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).
|
||||||
|
|
@ -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:<user_id>` 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.
|
|
||||||
|
|
@ -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:<user_id>`. 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.
|
|
||||||
Loading…
Reference in New Issue