docs: restructure development guide and move agent rules to .cursor
test / test (push) Has been cancelled
test / test (push) Has been cancelled
Reorganize development.md (stack, deps, build, release, testing, CI); relocate agent rules to .cursor/rules; remove dev-host and mixfed.ru references from docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
---
|
||||
description: Правила работы агента в репозитории SelfPost (ранее spec §12)
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Правила для агентов
|
||||
|
||||
1. **Без git-коммитов** без явной инструкции в промпте.
|
||||
2. После правок Go: `go build`, `go vet`; исправить все проблемы. `go test`,
|
||||
когда тесты есть.
|
||||
3. Перед «готово» для контейнерной задачи: образ собирается, контейнер стартует.
|
||||
4. Итерации: минимальный скелет, затем фичи.
|
||||
5. Требования безопасности — `docs/security.md`; внедрять с фичей, не откладывать.
|
||||
6. Не делать out-of-scope (`docs/product.md`); не менять зафиксированные
|
||||
допущения без согласования.
|
||||
7. Крупные задачи — план до кода, если пользователь не утвердил план ранее.
|
||||
8. Совместимость лицензий новых Go-зависимостей (permissive или GPL-family для
|
||||
AGPL-3.0).
|
||||
|
||||
Маршрутизация моделей — `docs/progress.md` § «Модель по типу работы».
|
||||
Предрелизная **ревизия** безопасности (не авторство) — Fable.
|
||||
|
||||
Протокол коммитов, CHANGELOG и закрытия фазы — `docs/progress.md` § «Коммиты»
|
||||
и «Протокол закрытия фазы».
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# Development-only material (secrets, server access notes) — never published.
|
||||
dev/
|
||||
|
||||
# Build artifacts (binaries are built on the dev server, not committed).
|
||||
# Build artifacts (not committed).
|
||||
/bin/
|
||||
*.exe
|
||||
|
||||
|
||||
+5
-2
@@ -39,6 +39,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
||||
and outcome are the line under it, so what the message was and how it ended
|
||||
are both on the first line. The page takes the whole column rather than the
|
||||
reading measure, as the other three monitoring pages already did.
|
||||
- `docs/development.md` restructured into stack, dependencies, build, release,
|
||||
testing, and CI; agent rules moved to `.cursor/rules/agent-rules.mdc`;
|
||||
dev-host-specific workflow and `mixfed.ru` references removed from docs.
|
||||
|
||||
## [0.6.0] - 2026-08-08
|
||||
|
||||
@@ -617,5 +620,5 @@ of `docs/implementation-plan.md`).
|
||||
- Security pass against spec 7.6 (exec safety, config-write sanitization,
|
||||
server-side validation, rate limiting, session/cookie hardening, output
|
||||
escaping, non-root panel) — full compliance, no code changes required.
|
||||
- Live production deployment on `selfpost.mixfed.ru` with a real Let's
|
||||
Encrypt certificate; end-to-end delivery confirmed (DKIM pass, SPF pass).
|
||||
- Live production deployment with a real Let's Encrypt certificate;
|
||||
end-to-end delivery confirmed (DKIM pass, SPF pass).
|
||||
|
||||
+155
-103
@@ -1,146 +1,198 @@
|
||||
# SelfPost — development
|
||||
# SelfPost — разработка
|
||||
|
||||
**What this file is.** How to build, test, and ship changes. Current sprint
|
||||
state lives in [progress.md](progress.md) — read that first after `/clear`.
|
||||
**Что это.** Как собрать, проверить и выкатить изменения. Текущий спринт —
|
||||
[progress.md](progress.md) (читать первым после `/clear`).
|
||||
|
||||
Product boundaries: [product.md](product.md). As-built layout:
|
||||
Границы продукта: [product.md](product.md). As-built устройство:
|
||||
[architecture.md](architecture.md).
|
||||
|
||||
---
|
||||
|
||||
## Repository layout
|
||||
## Технологический стек и инструменты
|
||||
|
||||
- `cmd/panel` — panel HTTP server + milter + log-tailer
|
||||
- `cmd/selfpost-backup` — CLI backup (`docker exec … selfpost-backup`)
|
||||
- `internal/` — domain logic, store, web handlers, health checks
|
||||
- `build/` — Dockerfile, supervisord, Postfix/OpenDKIM wiring, entrypoint
|
||||
- `deploy/` — `docker-compose.yml`, proxy examples, `.env.example`
|
||||
- `test/e2e/` — **separate Go module**; container integration tests
|
||||
| Компонент | Версия / заметки |
|
||||
|---|---|
|
||||
| **Go** | 1.26+ (`go.mod`); `CGO_ENABLED=0` — чистый Go, статическая линковка |
|
||||
| **SQLite** | `modernc.org/sqlite` (pure Go, без cgo) |
|
||||
| **Сборка** | [Makefile](../Makefile): `vet`, `test`, `build`, `e2e` |
|
||||
| **Контейнер** | Docker + Compose v2 на хосте разработки и в CI |
|
||||
| **Образ (build stage)** | `golang:1.26-bookworm` — [build/Dockerfile](../build/Dockerfile) |
|
||||
| **Образ (runtime)** | `debian:bookworm-slim` + Postfix, OpenDKIM, supervisord, SASL, logrotate |
|
||||
| **CI** | GitHub Actions — [.github/workflows/](../.github/workflows/) |
|
||||
| **Реестр образов** | `ghcr.io/mixeme/selfpost` |
|
||||
|
||||
**Структура репозитория** (кратко; детали процессов — в [architecture.md](architecture.md)):
|
||||
|
||||
- `cmd/panel` — HTTP-панель + journal-milter + log-tailer
|
||||
- `cmd/selfpost-backup` — CLI бэкапа (`docker exec … selfpost-backup`)
|
||||
- `internal/` — доменная логика, store, web, health
|
||||
- `build/` — Dockerfile, supervisord, Postfix/OpenDKIM, entrypoint
|
||||
- `deploy/` — `docker-compose.yml`, примеры прокси, `.env.example`
|
||||
- `test/e2e/` — **отдельный Go-модуль**; контейнерные интеграционные тесты
|
||||
|
||||
---
|
||||
|
||||
## Local Go workflow
|
||||
## Внешние библиотеки
|
||||
|
||||
Requires Go 1.26+ and `CGO_ENABLED=0` (pure Go SQLite).
|
||||
Проект — **AGPL-3.0** ([LICENSE](../LICENSE)). Новые Go-зависимости — только
|
||||
permissive или GPL-family (см. [.cursor/rules/agent-rules.mdc](../.cursor/rules/agent-rules.mdc)).
|
||||
|
||||
### Основной модуль (`go.mod`)
|
||||
|
||||
| Пакет | Версия | Репозиторий | Лицензия |
|
||||
|---|---|---|---|
|
||||
| `github.com/emersion/go-milter` | v0.4.1 | <https://github.com/emersion/go-milter> | BSD-2-Clause |
|
||||
| `golang.org/x/crypto` | v0.54.0 | <https://github.com/golang/crypto> | BSD-3-Clause |
|
||||
| `modernc.org/sqlite` | v1.53.0 | <https://gitlab.com/cznic/sqlite> (зеркало: <https://github.com/modernc-org/sqlite>) | BSD-3-Clause |
|
||||
|
||||
Транзитивные зависимости — `go mod graph` / `go.sum`; все indirect в дереве
|
||||
совместимы с AGPL-3.0.
|
||||
|
||||
### Модуль e2e (`test/e2e/go.mod`)
|
||||
|
||||
| Пакет | Версия | Репозиторий | Лицензия |
|
||||
|---|---|---|---|
|
||||
| `github.com/emersion/go-msgauth` | v0.6.8 | <https://github.com/emersion/go-msgauth> | BSD-2-Clause |
|
||||
|
||||
Тестовый модуль не входит в граф основного `go build` и не попадает в образ.
|
||||
|
||||
### Пакеты Debian в runtime-образе
|
||||
|
||||
Postfix, OpenDKIM, `supervisord`, `sasl2-bin`, `logrotate` и др. —
|
||||
из репозиториев Debian bookworm; лицензии — в `copyright` соответствующих
|
||||
пакетов на <https://packages.debian.org/bookworm/>.
|
||||
|
||||
---
|
||||
|
||||
## Сборка исполняемого файла и образа
|
||||
|
||||
### Локальные бинарники
|
||||
|
||||
Требуется Go 1.26+ и `CGO_ENABLED=0`.
|
||||
|
||||
```sh
|
||||
make build # bin/panel, bin/selfpost-backup (VERSION=dev по умолчанию)
|
||||
make build VERSION=1.0.0
|
||||
```
|
||||
|
||||
Или напрямую:
|
||||
|
||||
```sh
|
||||
go build -trimpath -ldflags "-X github.com/mixeme/selfpost/internal/buildinfo.Version=dev" -o bin/panel ./cmd/panel
|
||||
```
|
||||
|
||||
Версия вшивается в оба бинарники через `-ldflags` и **должна совпадать с тегом
|
||||
Docker-образа** — restore проверяет совместимость версий бэкапа.
|
||||
|
||||
### Docker-образ
|
||||
|
||||
Из корня репозитория:
|
||||
|
||||
```sh
|
||||
docker build -f build/Dockerfile -t selfpost:dev --build-arg VERSION=dev .
|
||||
```
|
||||
|
||||
В Dockerfile: build stage (`go vet`, `go build` с `VERSION`), runtime stage
|
||||
(Debian + почтовый стек). См. [architecture.md](architecture.md) § Image and
|
||||
processes.
|
||||
|
||||
---
|
||||
|
||||
## Сборка релиза
|
||||
|
||||
Релизный образ публикуется **только по тегу** `vX.Y.Z` (не на каждый push в
|
||||
`main`). Тег — единственный источник версии: из него берутся тег образа и
|
||||
`-ldflags` в бинарниках, чтобы они не расходились.
|
||||
|
||||
**Шаги (по явному запросу):**
|
||||
|
||||
1. Закрыть `[Unreleased]` в [CHANGELOG.md](../CHANGELOG.md).
|
||||
2. Создать и запушить git-тег `vX.Y.Z`.
|
||||
3. Workflow [release.yml](../.github/workflows/release.yml) собирает, гейтит
|
||||
e2e и публикует `ghcr.io/mixeme/selfpost:X.Y.Z`.
|
||||
4. Обновить закреплённый тег в [deploy/docker-compose.yml](../deploy/docker-compose.yml)
|
||||
(см. [roadmap.md](roadmap.md) § «v1.x — хвост документации и деплоя»).
|
||||
|
||||
Обычные коммиты **не** публикуют образ.
|
||||
|
||||
---
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Статический анализ и unit-тесты
|
||||
|
||||
Основной модуль (`go test ./...`); e2e — отдельный модуль, см. ниже.
|
||||
|
||||
```sh
|
||||
make vet # go vet ./...
|
||||
make test # go test ./...
|
||||
make build # bin/panel, bin/selfpost-backup (VERSION=dev by default)
|
||||
make build VERSION=1.0.0
|
||||
```
|
||||
|
||||
Or directly:
|
||||
Или напрямую:
|
||||
|
||||
```sh
|
||||
gofmt -l . # в CI — fail при расхождении
|
||||
go vet ./...
|
||||
go test ./...
|
||||
go build -trimpath -ldflags "-X github.com/mixeme/selfpost/internal/buildinfo.Version=dev" -o bin/panel ./cmd/panel
|
||||
```
|
||||
|
||||
**Env documentation regression:** `go test ./cmd/panel -run TestLoadConfig` —
|
||||
new `loadConfig` keys must appear in guide.md env lists
|
||||
### Регресс документации env
|
||||
|
||||
`go test ./cmd/panel -run TestLoadConfig` — каждый новый ключ `loadConfig`
|
||||
должен появиться в списках env в [guide.md](guide.md)
|
||||
([cmd/panel/envdoc_test.go](../cmd/panel/envdoc_test.go)).
|
||||
|
||||
---
|
||||
### End-to-end (контейнерный suite)
|
||||
|
||||
## End-to-end tests
|
||||
|
||||
Hermetic container suite (implemented as `test/e2e/`, separate Go module):
|
||||
Отдельный Go-модуль `test/e2e/`; **не** входит в `go test ./...` основного
|
||||
модуля.
|
||||
|
||||
```sh
|
||||
make e2e
|
||||
# equivalent: cd test/e2e && go test -v -timeout 20m ./...
|
||||
# то же: cd test/e2e && go test -v -timeout 20m ./...
|
||||
```
|
||||
|
||||
**Stack:** shipped [deploy/docker-compose.yml](../deploy/docker-compose.yml) +
|
||||
[test/e2e/compose.override.yml](../test/e2e/compose.override.yml) — same
|
||||
`cap_drop`/`cap_add`/`no-new-privileges` as production. Override uses high
|
||||
ports (`20465`/`20587`/`20080`), test hostname, self-signed TLS,
|
||||
`PANEL_COOKIE_SECURE=false`, isolated compose project. Mail is hermetic: CoreDNS
|
||||
fake zone + Postfix `smtp-sink` as sink-MX; DKIM TXT is scraped from the panel
|
||||
and published into the zone so the test verifies the record the operator would
|
||||
use.
|
||||
**Стек:** [deploy/docker-compose.yml](../deploy/docker-compose.yml) +
|
||||
[test/e2e/compose.override.yml](../test/e2e/compose.override.yml) — те же
|
||||
`cap_drop`/`cap_add`/`no-new-privileges`, что в production. Override: высокие
|
||||
порты (`20465`/`20587`/`20080`), тестовый hostname, self-signed TLS,
|
||||
`PANEL_COOKIE_SECURE=false`, изолированный compose-проект. Почта герметична:
|
||||
CoreDNS (fake zone) + Postfix `smtp-sink` как sink-MX; DKIM TXT скрапится с
|
||||
панели и публикуется в зону — тест проверяет записи, которые оператор реально
|
||||
использует.
|
||||
|
||||
**Coverage (summary):** full bootstrap → SMTP AUTH → delivery → DKIM verify →
|
||||
send-log `queued → sent`; negatives (no AUTH, relay, sender/login mismatch, L1/L2
|
||||
limits, milter fail-open, bad `SELFPOST_HOSTNAME`, session survives
|
||||
`docker restart`). Polling with timeouts only — no fixed `sleep`.
|
||||
**Покрытие (сводка):** bootstrap → SMTP AUTH → доставка → DKIM verify →
|
||||
send-log `queued → sent`; негативы (без AUTH, relay, sender/login mismatch,
|
||||
L1/L2 лимиты, fail-open milter, неверный `SELFPOST_HOSTNAME`, сессия после
|
||||
`docker restart`). Только polling с таймаутами — без фиксированных `sleep`.
|
||||
|
||||
Requires **Docker + Compose v2** on the test host. Not included in `go test ./...`
|
||||
of the main module.
|
||||
|
||||
**CI** ([release.yml](../.github/workflows/release.yml)): tag `vX.Y.Z` triggers
|
||||
`prepare` (version from tag) → native matrix `[ubuntu-latest, ubuntu-24.04-arm]`
|
||||
— build `--load`, e2e, push per-arch tag → `merge` publishes unified
|
||||
`ghcr.io/...:X.Y.Z` via `docker buildx imagetools create`. Failed e2e blocks the
|
||||
image. Ordinary pushes still run only `vet`/`test` in [test.yml](../.github/workflows/test.yml).
|
||||
Требует **Docker + Compose v2** на машине, где гоняется suite.
|
||||
|
||||
---
|
||||
|
||||
## Dev server (full container)
|
||||
## CI
|
||||
|
||||
When unit tests are not enough — Postfix, OpenDKIM, supervisord, real SMTP:
|
||||
Workflows в [.github/workflows/](../.github/workflows/). Что именно гоняется —
|
||||
в [§ Тестирование](#тестирование) выше.
|
||||
|
||||
**Typical setup:** edit locally → sync tree to dev host → build/test there.
|
||||
### `test.yml` — каждый push и PR в `main`
|
||||
|
||||
Documented dev host: `selfpost.mixfed.ru` (Debian 12). Sync example from
|
||||
[progress.md](progress.md):
|
||||
`gofmt -l` → `go vet ./...` → `go test ./...` (основной модуль, без e2e).
|
||||
|
||||
```sh
|
||||
tar -czf - --exclude=.git . | ssh root@selfpost.mixfed.ru \
|
||||
'rm -rf /root/selfpost-src && mkdir -p /root/selfpost-src && tar -xzf - -C /root/selfpost-src'
|
||||
### `release.yml` — push тега `vX.Y.Z` или `workflow_dispatch`
|
||||
|
||||
```
|
||||
prepare (версия из тега)
|
||||
→ build [matrix: ubuntu-latest / ubuntu-24.04-arm]
|
||||
→ docker build --load (VERSION из тега)
|
||||
→ e2e (test/e2e)
|
||||
→ push ghcr.io/...:X.Y.Z-amd64 | X.Y.Z-arm64
|
||||
→ merge
|
||||
→ docker buildx imagetools create → единый манифест X.Y.Z
|
||||
```
|
||||
|
||||
On the server (Go in `/usr/local/go/bin` if not in PATH):
|
||||
Нативная матрица per-arch (без QEMU): полный стек Postfix/OpenDKIM под
|
||||
эмуляцией для e2e непрактичен. Сначала e2e, затем push — в registry попадают
|
||||
байты, прошедшие гейт.
|
||||
|
||||
```sh
|
||||
cd /root/selfpost-src
|
||||
/usr/local/go/bin/go vet ./...
|
||||
/usr/local/go/bin/go test ./...
|
||||
docker build -f build/Dockerfile -t selfpost:dev --build-arg VERSION=dev .
|
||||
```
|
||||
|
||||
Manual smoke on production-like host: panel at `https://selfpost.mixfed.ru`,
|
||||
real LE cert, live deliverability — not replaceable by e2e alone (no outbound
|
||||
25 on CI runners, no real PTR/reputation).
|
||||
|
||||
---
|
||||
|
||||
## Commit and changelog protocol
|
||||
|
||||
From [progress.md](progress.md):
|
||||
|
||||
1. Meaningful step → entry under `[Unreleased]` in [CHANGELOG.md](../CHANGELOG.md)
|
||||
(Keep a Changelog format).
|
||||
2. Commit on `main` unless asked for a branch. **Do not commit unless the user
|
||||
asks.**
|
||||
3. Before `/clear` at end of a phase: update `progress.md`, verify acceptance
|
||||
criteria, CHANGELOG, final commit.
|
||||
|
||||
Documentation changes that add/rename env vars, panel routes, or observable
|
||||
mail behaviour ship in the **same commit** as the code change.
|
||||
|
||||
Release tagging and image push — only on explicit request.
|
||||
|
||||
---
|
||||
|
||||
## Agent rules (formerly spec §12)
|
||||
|
||||
1. **No git commits** without explicit instruction in the prompt.
|
||||
2. After Go changes: `go build`, `go vet`; fix all issues. Run `go test` when
|
||||
tests exist.
|
||||
3. Before calling a container task done: image builds and container starts.
|
||||
4. Iterate: minimal skeleton first, then features.
|
||||
5. Security requirements in [security.md](security.md) — implement with the feature,
|
||||
not deferred.
|
||||
6. Do not implement out-of-scope items ([product.md](product.md)) or change
|
||||
fixed assumptions without agreement.
|
||||
7. For large tasks: propose a plan before coding unless the user already approved
|
||||
one.
|
||||
8. Check licence compatibility of new Go dependencies (permissive or
|
||||
GPL-family for AGPL-3.0 project).
|
||||
|
||||
Model routing (from progress): security/infra → Opus; UI/docs → Sonnet;
|
||||
trivial mechanics → Haiku. Pre-release security **review** (not authorship) →
|
||||
Fable.
|
||||
Провал e2e **блокирует** публикацию образа.
|
||||
|
||||
+12
-9
@@ -43,21 +43,24 @@
|
||||
|
||||
- **Выполнено и принято:** базовый линейный план 0→11 (v1.0; аудит безопасности ТЗ 7.6 — полное соответствие), Фаза 12 (UI/UX), Фаза 13 (страница `/status`, DNS-проверки домена) и Фаза 14 (security-заголовки, проверка origin, cookie `__Host-` + обнаружение дублей, документация про `/data/setup-token`). Что именно сделано — в `git log` и `CHANGELOG.md`, здесь не дублируется.
|
||||
- **B.1 реализован** (не выкачен на прод): сессии переехали в SQLite (`internal/store/migrations/0002_sessions.sql`, `internal/store/sessions.go`, `internal/web/session.go`) — хранится SHA-256 токена, не сам токен; скользящий срок бездействия `PANEL_SESSION_IDLE_DAYS` (по умолчанию 7 дней, без абсолютного потолка); запись в БД продлевается не чаще раза в час (`renewThreshold`); опросы мониторинга (`GET` с `HX-Request`) продление не триггерят (`isSessionActivity` в `internal/web/middleware.go`); `Max-Age` cookie выставляется тем же значением при логине и при продлении (`setSessionCookie`); смена пароля разлогинивает все сессии кроме текущей (уже было, теперь через БД). Проверено на стенде: логин → рестарт процесса панели → сессия жива по старой cookie; HX-Request-опрос и повторный GET внутри часового окна не шлют `Set-Cookie`. `go vet`/`go test ./...`/`gofmt -l .` чистые.
|
||||
- **B.2 реализован** (не выкачен на прод): ротация `mail.log` ушла с `copytruncate` на «переименовать + `postfix reload`» — `build/logrotate-mail.conf` (`nocreate` заменён на `create 0644 root root` **не по плану, а по стендовой проверке**: после reload Postfix пересоздаёт лог сам только в момент следующей фактической записи и с режимом `0600`, недоступным непривилегированной панели, — `create` в logrotate закрывает это, отдавая файл ей же на 644 сразу после переименования); `follow()` в `internal/logtail/logtail.go` при обнаружении смены inode дочитывает старый дескриптор ещё раз перед переключением; `readLogTail()` в `internal/web/handlers_monitor.go` считает отсутствующий файл пустым экраном, а не ошибкой. Проверено на стенде (`selfpost.mixfed.ru`, отдельный контейнер `selfpost:b2test2`): цикл трафик → принудительная ротация → файл пуст и сразу читаем непривилегированным uid панели (0 читает `mail.log` сразу после rename, без окна недоступности) → новый трафик после ротации уходит в новый файл на 644, ничего не потеряно по обе стороны rename. `go vet`/`go test ./...`/`gofmt -l .` чистые (на dev-сервере; локально на Windows `TestFollowTailsAndRotates` падает — rename открытого файла запрещён ОС, к делу не относится).
|
||||
- **B.3 реализован** (не выкачен на прод): `build/entrypoint.sh` проверяет `SELFPOST_HOSTNAME` до `postfix-config.sh` и до `supervisord` — при пустом значении `exit 1` с развёрнутым текстом ошибки (что это за имя, почему обязательно, пример, где задаётся); плюс синтаксическая проверка через `case`: минимум одна точка, без схемы/порта/пробелов (`*://*`, `*:*`, пробел/таб — тот же класс тихого спам-отказа, что и пустое значение). `saslRealm()` и fallback в `postfix-config.sh` не тронуты — после гейта эти ветки мертвы. Заодно отмечена обязательность переменной в `README.md` и `deploy/.env.example`. Проверено на стенде (`selfpost.mixfed.ru`, отдельный образ `selfpost:b3test`, cap-list как в поставляемом compose): без переменной — `exit 1` с ожидаемым текстом, без бесконечного тихого retry; `https://mail.example.com:465` и `localhost` отклонены с понятными сообщениями; валидный `mail.example.com` — обычный старт, все процессы supervisord поднимаются. `go vet`/`go test ./...` чистые.
|
||||
- **C.4 реализован** (не выкачен на прод — это CI/тестовая инфраструктура, а не образ): герметичный контейнерный e2e отдельным Go-модулем `test/e2e/` (свой `go.mod`, не подхватывается `go test ./...` основного модуля) поверх поставляемого `deploy/docker-compose.yml` плюс `test/e2e/compose.override.yml` (самоподписанный сертификат, `PANEL_COOKIE_SECURE=false`, `SELFPOST_HOSTNAME=mail.e2e.test`, высокие порты `20465/20587/20080`, изолированный compose-проект `selfpost-e2e`, свой `--project-directory` — прод на том же хосте не задет). Герметичная почта: CoreDNS (`test/e2e/dns/Corefile` — авторитетна только для `e2e.test`, `file`-плагин с саб-директивой `reload` перечитывает `db.zone` по mtime, без сигналов) плюс `smtp-sink` из пакета postfix (`test/e2e/sink/`) как sink-MX. Сценарий (`test/e2e/*_test.go`): старт контейнера → все supervisord-программы `RUNNING` (`postfix-reload` — `STOPPED`) → токен из `/data/setup-token` → setup → login → добавление домена → DKIM-запись **скраплена со страницы панели** и опубликована в фейковую зону → добавление приложения → SMTP AUTH на 465 → письмо на sink → DKIM-подпись проверена (`go-msgauth/dkim` с кастомным `LookupTXT` через CoreDNS) против ключа **из DNS**, не из панели напрямую → send-log `queued → sent`. Негативы: без AUTH, relay на чужой домен без AUTH, sender/login mismatch (`reject_sender_login_mismatch` репортится Postfix'ом на RCPT, не MAIL — `smtpd_delay_reject=yes` по умолчанию), L1-лимит (anvil, override `RATE_LIMIT_MESSAGES_PER_IP=50` — специально высокий, чтобы остальные под-тесты не расходовали общий бюджет по IP раньше времени; сам тест шлёт до 60 раз, ждёт отказа), L2-лимит через панель (домен/приложение → `rejected`-строка в send-log), fail-open journal-milter'а (`supervisorctl stop panel`, письмо всё равно принято, контейнер жив), пустой/синтаксически неверный `SELFPOST_HOSTNAME` (отдельный один-разовый контейнер, не общий стенд), сессия переживает `docker restart` (плюс явное ожидание готовности smtps-порта после рестарта — панель и Postfix поднимаются независимо). `make e2e` — локальный/dev-server прогон. Найдено и исправлено по ходу стендовой проверки: `reload` — саб-директива `file`-плагина CoreDNS, а не отдельный топ-левел плагин (топ-левел `reload` следит за самим Corefile, не за зоной); `docker compose build.context` резолвится относительно `--project-directory`, а не относительно файла, где объявлен; `smtp-sink` отказывается стартовать от root без `-u`; `html/template` эскейпит `+` в `+` даже в тексте — скрапер значений со страницы обязан `html.UnescapeString`; проверки состояния сразу после `up`/`restart` должны поллиться, а не разово опрашиваться (supervisord/postfix поднимаются не мгновенно). **Проверено на dev-сервере (`selfpost.mixfed.ru`)**: `make e2e` — зелёный (`go vet`/`gofmt -l` тоже чистые в обоих модулях). `release.yml` переработан: job `prepare` (версия из тега) → матрица `[ubuntu-latest, ubuntu-24.04-arm]` — каждая нативно собирает образ (`--load`), прогоняет e2e, пушит тег `X.Y.Z-amd64`/`X.Y.Z-arm64` → job `merge` — `docker buildx imagetools create` в единый тег `X.Y.Z`; `setup-qemu-action` убран. Не проверено вживую (нельзя без реального тега): сам workflow на GitHub Actions — синтаксис вычитан, логика идентична локальному `make e2e` пути.
|
||||
- **B.2 реализован** (не выкачен на прод): ротация `mail.log` ушла с `copytruncate` на «переименовать + `postfix reload`» — `build/logrotate-mail.conf` (`nocreate` заменён на `create 0644 root root` **не по плану, а по стендовой проверке**: после reload Postfix пересоздаёт лог сам только в момент следующей фактической записи и с режимом `0600`, недоступным непривилегированной панели, — `create` в logrotate закрывает это, отдавая файл ей же на 644 сразу после переименования); `follow()` в `internal/logtail/logtail.go` при обнаружении смены inode дочитывает старый дескриптор ещё раз перед переключением; `readLogTail()` в `internal/web/handlers_monitor.go` считает отсутствующий файл пустым экраном, а не ошибкой. Проверено на стенде (отдельный контейнер `selfpost:b2test2`): цикл трафик → принудительная ротация → файл пуст и сразу читаем непривилегированным uid панели (0 читает `mail.log` сразу после rename, без окна недоступности) → новый трафик после ротации уходит в новый файл на 644, ничего не потеряно по обе стороны rename. `go vet`/`go test ./...`/`gofmt -l .` чистые (на стенде; локально на Windows `TestFollowTailsAndRotates` падает — rename открытого файла запрещён ОС, к делу не относится).
|
||||
- **B.3 реализован** (не выкачен на прод): `build/entrypoint.sh` проверяет `SELFPOST_HOSTNAME` до `postfix-config.sh` и до `supervisord` — при пустом значении `exit 1` с развёрнутым текстом ошибки (что это за имя, почему обязательно, пример, где задаётся); плюс синтаксическая проверка через `case`: минимум одна точка, без схемы/порта/пробелов (`*://*`, `*:*`, пробел/таб — тот же класс тихого спам-отказа, что и пустое значение). `saslRealm()` и fallback в `postfix-config.sh` не тронуты — после гейта эти ветки мертвы. Заодно отмечена обязательность переменной в `README.md` и `deploy/.env.example`. Проверено на стенде (отдельный образ `selfpost:b3test`, cap-list как в поставляемом compose): без переменной — `exit 1` с ожидаемым текстом, без бесконечного тихого retry; `https://mail.example.com:465` и `localhost` отклонены с понятными сообщениями; валидный `mail.example.com` — обычный старт, все процессы supervisord поднимаются. `go vet`/`go test ./...` чистые.
|
||||
- **C.4 реализован** (не выкачен на прод — это CI/тестовая инфраструктура, а не образ): герметичный контейнерный e2e отдельным Go-модулем `test/e2e/` (свой `go.mod`, не подхватывается `go test ./...` основного модуля) поверх поставляемого `deploy/docker-compose.yml` плюс `test/e2e/compose.override.yml` (самоподписанный сертификат, `PANEL_COOKIE_SECURE=false`, `SELFPOST_HOSTNAME=mail.e2e.test`, высокие порты `20465/20587/20080`, изолированный compose-проект `selfpost-e2e`, свой `--project-directory` — прод на том же хосте не задет). Герметичная почта: CoreDNS (`test/e2e/dns/Corefile` — авторитетна только для `e2e.test`, `file`-плагин с саб-директивой `reload` перечитывает `db.zone` по mtime, без сигналов) плюс `smtp-sink` из пакета postfix (`test/e2e/sink/`) как sink-MX. Сценарий (`test/e2e/*_test.go`): старт контейнера → все supervisord-программы `RUNNING` (`postfix-reload` — `STOPPED`) → токен из `/data/setup-token` → setup → login → добавление домена → DKIM-запись **скраплена со страницы панели** и опубликована в фейковую зону → добавление приложения → SMTP AUTH на 465 → письмо на sink → DKIM-подпись проверена (`go-msgauth/dkim` с кастомным `LookupTXT` через CoreDNS) против ключа **из DNS**, не из панели напрямую → send-log `queued → sent`. Негативы: без AUTH, relay на чужой домен без AUTH, sender/login mismatch (`reject_sender_login_mismatch` репортится Postfix'ом на RCPT, не MAIL — `smtpd_delay_reject=yes` по умолчанию), L1-лимит (anvil, override `RATE_LIMIT_MESSAGES_PER_IP=50` — специально высокий, чтобы остальные под-тесты не расходовали общий бюджет по IP раньше времени; сам тест шлёт до 60 раз, ждёт отказа), L2-лимит через панель (домен/приложение → `rejected`-строка в send-log), fail-open journal-milter'а (`supervisorctl stop panel`, письмо всё равно принято, контейнер жив), пустой/синтаксически неверный `SELFPOST_HOSTNAME` (отдельный один-разовый контейнер, не общий стенд), сессия переживает `docker restart` (плюс явное ожидание готовности smtps-порта после рестарта — панель и Postfix поднимаются независимо). `make e2e` — локальный прогон. Найдено и исправлено по ходу стендовой проверки: `reload` — саб-директива `file`-плагина CoreDNS, а не отдельный топ-левел плагин (топ-левел `reload` следит за самим Corefile, не за зоной); `docker compose build.context` резолвится относительно `--project-directory`, а не относительно файла, где объявлен; `smtp-sink` отказывается стартовать от root без `-u`; `html/template` эскейпит `+` в `+` даже в тексте — скрапер значений со страницы обязан `html.UnescapeString`; проверки состояния сразу после `up`/`restart` должны поллиться, а не разово опрашиваться (supervisord/postfix поднимаются не мгновенно). **Проверено на стенде**: `make e2e` — зелёный (`go vet`/`gofmt -l` тоже чистые в обоих модулях). `release.yml` переработан: job `prepare` (версия из тега) → матрица `[ubuntu-latest, ubuntu-24.04-arm]` — каждая нативно собирает образ (`--load`), прогоняет e2e, пушит тег `X.Y.Z-amd64`/`X.Y.Z-arm64` → job `merge` — `docker buildx imagetools create` в единый тег `X.Y.Z`; `setup-qemu-action` убран. Не проверено вживую (нельзя без реального тега): сам workflow на GitHub Actions — синтаксис вычитан, логика идентична локальному `make e2e` пути.
|
||||
- **Документация:** план D1–D9 закрыт ([documentation-plan.md](documentation-plan.md) — только метод и правила поддержки). Хвост v1.x — [roadmap.md](roadmap.md) § «v1.x — хвост документации и деплоя»; из него остался только бамп тега образа (Quick start и `docs/logo` закрыты).
|
||||
- **Рецензирование кодовой базы** (2026-08-05, `522425a`): 10 разделов (архитектура, качество, docs, GUI, legacy, риски) плюс приоритизированный план доработок фазами 0–3. Критичных багов не найдено; единственным блокером релиза названа § D. **План выполнен целиком** (см. записи ниже), поэтому сам документ `docs/code-review.md` удалён — незакрытые пункты унесены в [roadmap.md](roadmap.md) (разбиение `internal/web`, индекс документации в README, адаптивный интервал опроса, `CONTRIBUTING.md`), остальное либо сделано, либо уже описано в architecture.md / security.md / комментариях кода. Текст ревизии — в git-истории.
|
||||
- **§ D выполнен (2026-08-06):** предрелизная ревизия безопасности моделью Fable — диф от аудита v1.0 (Фаза 11, `bd64e80`) до HEAD + полный проход по чек-листу [security.md](security.md) (бывшее ТЗ 7.6). Эксплуатируемых находок нет; одна правка defence-in-depth (`--` перед логином в argv `saslpasswd2`, `internal/app/sasl.go` + тест). Принятые риски не пополнились. Детали — [implementation-plan.md](implementation-plan.md) § D и CHANGELOG `[Unreleased]/Security`. Локально `go vet`/`go test ./internal/app/...` чистые; падения `internal/domain` (`TestWriteLoadPrivateKeyRoundtrip`, `TestRenderTables`) и `internal/logtail` (`TestFollowTailsAndRotates`) — Windows-специфика (права файлов/`\` в путях/rename открытого файла), на Linux CI зелено.
|
||||
- **Фаза 1 плана ревизии выполнена (2026-08-06)** (doc/code hygiene, P1): cleanup ~30 stale «Phase N» комментариев в коде и shell-скриптах; исправлен stale-комментарий в `handlers_domains.go`; ADR CSRF (Origin vs токены) добавлен в [security.md](security.md); known-limitations по log-tailer уже был в [architecture.md](architecture.md) § Log tailer — отдельного действия не потребовалось; `docs/logo` в [roadmap.md](roadmap.md) закрыт (каталога нет, критерию соответствует); `gofmt -l` добавлен в CI (`.github/workflows/test.yml`). `gofmt`/`go vet`/`go test ./...` чистые в обоих модулях (dev-server).
|
||||
- **Фаза 1.5 плана ревизии выполнена (2026-08-06)** (шифрование резервных копий, P1): новый пакет `internal/secretfile` — конверт `magic SELFPOST1 | type | scrypt-параметры | salt | nonce-prefix` + поток 64 KiB чанков AES-256-GCM, каждый с AAD `header+counter+last`, поэтому обрезка, перестановка и подмена не открываются (стриминг в обе стороны — полный бэкап не держится в памяти). Панель: чекбокс «Encrypt with a password» в форме полного бэкапа и экспорта домена (общий партиал `templates/encrypt_fields.html`, показ/очистка полей — `panel.js`, без inline-скриптов), импорт домена принимает `.spde` (шифрование определяется по magic, не по расширению) с полем пароля. CLI `selfpost-backup`: пишет `.spbk` при заданном пароле и умеет `-decrypt` (иначе зашифрованный бэкап нечем распаковать при restore); пароль — только `SELFPOST_BACKUP_PASSWORD` / `-password-file`, никогда argv. Умолчание не изменилось: галочка снята — прежние `.tar.gz` / `.json` байт в байт. Тесты: round-trip по размерам (0, границы чанка, несколько чанков), неверный пароль, обрезка, перестановка чанков, порча байта, чужие KDF-параметры; валидация формы пароля; round-trip CLI create→decrypt→tar. Docs: README § *Encrypting a backup or export*, [security.md](security.md) § «Резервная копия и экспорт домена» + принятый риск (шифрование опционально), [architecture.md](architecture.md) § Persistence. `gofmt`/`go vet`/`go test ./...` чистые (кроме известных Windows-падений `internal/domain`, `internal/logtail`). E2E-сценарий не добавлялся: в `test/e2e/` бэкапа не было и раньше, а прогнать новый тест локально нечем (нет Docker) — кандидат при следующем прогоне на dev-сервере.
|
||||
- **Фаза 2 плана ревизии выполнена (2026-08-06)** (GUI polish, P2): опрос мониторинговых страниц не уходит на сервер, пока вкладка скрыта — фильтр повешен на `htmx:beforeRequest` в `panel.js`, а не на встроенный в htmx фильтр триггера (тот вычисляется через `new Function`, что CSP панели `default-src 'self'` без `unsafe-eval` молча ломает); тёмная тема переписана с каскада `!important` на переопределение CSS-переменных в одном блоке `prefers-color-scheme: dark`; дублирующее правило `main { max-width }` сведено к одному базовому плюс задокументированные постраничные оверрайды. Только CSS/JS, поведения сервера не касается; вживую не проверялось (нет Docker локально) — кандидат на следующий прогон на dev-сервере.
|
||||
- **Фаза 3 плана ревизии выполнена (2026-08-06)** (operational improvements, P2–P3): (1) log-tailer сохраняет позицию чтения — таблица `logtail_state` (миграция `0003`, `internal/store/logtail.go`) хранит offset + отпечаток первых 512 байт лога, `internal/logtail/offset.go` решает откуда стартовать: отпечаток совпал → продолжаем с offset (дочитывается хвост, написанный пока панель лежала); не совпал (лог сменился/пересоздан) → читаем файл с начала (повторный разбор безвреден, `UpdateStatus` идемпотентен); записи нет вовсе (первый запуск) → с конца, как раньше. Запись offset — не чаще раза в 5 с, плюс форс при ротации и на выключении; сохраняется позиция *потреблённых* байт (минус недочитанная частичная строка). (2) L2-лимит перестал промахиваться при параллельных сессиях: между проверкой на MAIL FROM и вставкой строки на end-of-message сообщение не видно в БД, поэтому N одновременных сессий пропускали друг друга — теперь к счёту из БД добавляются «в полёте» (`internal/milter/inflight.go`, общий на процесс реестр резерваций); резервация освобождается после записи в send-log, на ABORT и по TTL 10 минут (у go-milter нет колбэка на закрытие соединения, а вечная резервация — это fail-closed-дрейф, которого у лимитера быть не должно). Транзакция «count+insert», как предлагал review, невозможна буквально: эти два шага разнесены по разным стадиям SMTP-транзакции. Тесты: restart/rotation-resume для tailer'а, четыре сценария резерваций для лимита. `gofmt`/`go vet` чистые; `go test ./...` — падения только известные Windows-специфичные (`internal/domain`, `TestFollowTailsAndRotates`). Не проверено на стенде (нет Docker локально) — кандидат на следующий прогон на dev-сервере.
|
||||
- **Фаза 1 плана ревизии выполнена (2026-08-06)** (doc/code hygiene, P1): cleanup ~30 stale «Phase N» комментариев в коде и shell-скриптах; исправлен stale-комментарий в `handlers_domains.go`; ADR CSRF (Origin vs токены) добавлен в [security.md](security.md); known-limitations по log-tailer уже был в [architecture.md](architecture.md) § Log tailer — отдельного действия не потребовалось; `docs/logo` в [roadmap.md](roadmap.md) закрыт (каталога нет, критерию соответствует); `gofmt -l` добавлен в CI (`.github/workflows/test.yml`). `gofmt`/`go vet`/`go test ./...` чистые в обоих модулях.
|
||||
- **Фаза 1.5 плана ревизии выполнена (2026-08-06)** (шифрование резервных копий, P1): новый пакет `internal/secretfile` — конверт `magic SELFPOST1 | type | scrypt-параметры | salt | nonce-prefix` + поток 64 KiB чанков AES-256-GCM, каждый с AAD `header+counter+last`, поэтому обрезка, перестановка и подмена не открываются (стриминг в обе стороны — полный бэкап не держится в памяти). Панель: чекбокс «Encrypt with a password» в форме полного бэкапа и экспорта домена (общий партиал `templates/encrypt_fields.html`, показ/очистка полей — `panel.js`, без inline-скриптов), импорт домена принимает `.spde` (шифрование определяется по magic, не по расширению) с полем пароля. CLI `selfpost-backup`: пишет `.spbk` при заданном пароле и умеет `-decrypt` (иначе зашифрованный бэкап нечем распаковать при restore); пароль — только `SELFPOST_BACKUP_PASSWORD` / `-password-file`, никогда argv. Умолчание не изменилось: галочка снята — прежние `.tar.gz` / `.json` байт в байт. Тесты: round-trip по размерам (0, границы чанка, несколько чанков), неверный пароль, обрезка, перестановка чанков, порча байта, чужие KDF-параметры; валидация формы пароля; round-trip CLI create→decrypt→tar. Docs: README § *Encrypting a backup or export*, [security.md](security.md) § «Резервная копия и экспорт домена» + принятый риск (шифрование опционально), [architecture.md](architecture.md) § Persistence. `gofmt`/`go vet`/`go test ./...` чистые (кроме известных Windows-падений `internal/domain`, `internal/logtail`). E2E-сценарий не добавлялся: в `test/e2e/` бэкапа не было и раньше, а прогнать новый тест локально нечем (нет Docker) — кандидат при следующем прогоне на стенде.
|
||||
- **Фаза 2 плана ревизии выполнена (2026-08-06)** (GUI polish, P2): опрос мониторинговых страниц не уходит на сервер, пока вкладка скрыта — фильтр повешен на `htmx:beforeRequest` в `panel.js`, а не на встроенный в htmx фильтр триггера (тот вычисляется через `new Function`, что CSP панели `default-src 'self'` без `unsafe-eval` молча ломает); тёмная тема переписана с каскада `!important` на переопределение CSS-переменных в одном блоке `prefers-color-scheme: dark`; дублирующее правило `main { max-width }` сведено к одному базовому плюс задокументированные постраничные оверрайды. Только CSS/JS, поведения сервера не касается; вживую не проверялось (нет Docker локально) — кандидат на следующий прогон на стенде.
|
||||
- **Фаза 3 плана ревизии выполнена (2026-08-06)** (operational improvements, P2–P3): (1) log-tailer сохраняет позицию чтения — таблица `logtail_state` (миграция `0003`, `internal/store/logtail.go`) хранит offset + отпечаток первых 512 байт лога, `internal/logtail/offset.go` решает откуда стартовать: отпечаток совпал → продолжаем с offset (дочитывается хвост, написанный пока панель лежала); не совпал (лог сменился/пересоздан) → читаем файл с начала (повторный разбор безвреден, `UpdateStatus` идемпотентен); записи нет вовсе (первый запуск) → с конца, как раньше. Запись offset — не чаще раза в 5 с, плюс форс при ротации и на выключении; сохраняется позиция *потреблённых* байт (минус недочитанная частичная строка). (2) L2-лимит перестал промахиваться при параллельных сессиях: между проверкой на MAIL FROM и вставкой строки на end-of-message сообщение не видно в БД, поэтому N одновременных сессий пропускали друг друга — теперь к счёту из БД добавляются «в полёте» (`internal/milter/inflight.go`, общий на процесс реестр резерваций); резервация освобождается после записи в send-log, на ABORT и по TTL 10 минут (у go-milter нет колбэка на закрытие соединения, а вечная резервация — это fail-closed-дрейф, которого у лимитера быть не должно). Транзакция «count+insert», как предлагал review, невозможна буквально: эти два шага разнесены по разным стадиям SMTP-транзакции. Тесты: restart/rotation-resume для tailer'а, четыре сценария резерваций для лимита. `gofmt`/`go vet` чистые; `go test ./...` — падения только известные Windows-специфичные (`internal/domain`, `TestFollowTailsAndRotates`). Не проверено на стенде (нет Docker локально) — кандидат на следующий прогон на стенде.
|
||||
- **Добор по плану ревизии выполнен (2026-08-06):** (1) проект переехал на единственную площадку — GitHub (Codeberg уходит): вместе с URL, лицензионными шапками SVG/HTML и docs переехал путь Go-модуля на `github.com/mixeme/selfpost` (`go.mod`, `test/e2e/go.mod`, все импорты, `MODULE` в Makefile, `-ldflags` в Dockerfile и development.md) — оставлять импорты на исчезающем хосте нельзя, `go get`/`go install` сломались бы; (2) ссылки на архивную спецификацию убраны из кода целиком — не только «spec 7.x», как просило ревью, но и «spec 4/5/6/8/9», страдавшие тем же, каждая заменена на живой документ с секцией там, где документ большой; (3) [architecture.md](architecture.md) § Code layers — диаграмма слоёв (A2); (4) `TestParseDelivery` расширен экзотикой mail.log — и **вскрыл реальный баг**: шаблон брал `status=` жадно, то есть последнее вхождение в строке, а Postfix дописывает ответ удалённого сервера дословно, поэтому отказ с `status=sent` в тексте ответа попадал в журнал как доставленный (исправлено на ленивый разбор); (5) `CONTRIBUTING.md` перенесён в 2.x, бамп тега образа и git-тег оставлены в [roadmap.md](roadmap.md) § v1.x. `gofmt`/`go vet` чистые в обоих модулях, `go test ./...` — падения только известные Windows-специфичные (`internal/domain`, `TestFollowTailsAndRotates`). На стенде не проверялось (нет Docker локально).
|
||||
- **Дальше:** релизный гейт (Фаза 0) закрыт по существу — e2e C.4 и ревизия § D пройдены; остаются только шаги, которые делаются в момент резки версии (бамп тега образа в compose, git tag) по явной команде пользователя. Все polish-фазы плана ревизии (1, 1.5, 2, 3) закрыты.
|
||||
- **Принятые риски** — [security.md](security.md). **Опционально v1.x / 2.x** — [roadmap.md](roadmap.md) (хвост документации, send-log gaps, Фаза O1+, роль администратора домена).
|
||||
- **Прод:** `selfpost.mixfed.ru`, реальный Let's Encrypt сертификат, живой e2e (DKIM/SPF pass). Контейнер там всё ещё на образе v1.0 — Фаза 14 в него не выкатывалась. При апгрейде: админа один раз разлогинит (сменилось имя cookie), а от reverse-proxy требуется передача исходного `Host` (Apache-фрагмент из `deploy/` это делает).
|
||||
- **Прод:** инстанс с реальным Let's Encrypt сертификатом и живым deliverability (DKIM/SPF pass). Контейнер там всё ещё на образе v1.0 — Фаза 14 в него не выкатывалась. При апгрейде: админа один раз разлогинит (сменилось имя cookie), а от reverse-proxy требуется передача исходного `Host` (Apache-фрагмент из `deploy/` это делает).
|
||||
|
||||
## Рабочая петля (dev loop) — ВАЖНО
|
||||
|
||||
Локально (Windows, `D:\Local\Git\selfpost`) **нет Go и Docker** — только редактирование и git. Вся сборка/тесты идут на dev-сервере `selfpost.mixfed.ru` (Debian 12 bookworm, тот же, что базовый образ; провижён под разработку). Цикл: править локально → залить дерево на сервер → `go build`/`go vet`/`docker build`/тесты там. `rsync` в локальном git-bash нет, поэтому дерево едет tar'ом по ssh: `tar -czf - --exclude=.git . | ssh root@selfpost.mixfed.ru 'rm -rf /root/selfpost-src && mkdir -p /root/selfpost-src && tar -xzf - -C /root/selfpost-src'`; Go на сервере — в `/usr/local/go/bin` (не в `PATH` по умолчанию); образ — `docker build -f build/Dockerfile -t selfpost:dev --build-arg VERSION=dev .`. Источник истины и git-история — локальный репозиторий; сервер — только исполнитель сборки/тестов. Подключение: `ssh root@selfpost.mixfed.ru` (по ключу).
|
||||
Сборка, unit-тесты и e2e требуют Go 1.26+ и Docker + Compose v2 — см.
|
||||
[development.md](development.md). Конкретная связка машин (всё локально,
|
||||
отдельный сервер, только CI) у каждого разработчика своя; источник истины —
|
||||
git-репозиторий.
|
||||
|
||||
+6
-6
@@ -2,7 +2,8 @@
|
||||
|
||||
**Статус:** здесь собран объём, отнесённый к релизной линии **2.x.x** — вне
|
||||
базового объёма v1.0/v1.x (v1.x — только исходящий релей). Реализация —
|
||||
только после явного согласования ([product.md](product.md), [development.md](development.md) § Agent rules):
|
||||
только после явного согласования ([product.md](product.md),
|
||||
[.cursor/rules/agent-rules.mdc](../.cursor/rules/agent-rules.mdc)):
|
||||
[product.md](product.md) явно исключает часть этого объёма (приём входящей
|
||||
почты; несколько пользователей/роли), поэтому включение — сознательное
|
||||
расширение границ проекта, а не доработка по своей инициативе. Присутствие
|
||||
@@ -128,7 +129,7 @@ git-тег `vX.Y.Z`; `implementation-plan.md` в `docs/archive/`, ссылок
|
||||
|
||||
**Риски:** open relay/backscatter (снимается `relay_domains` + `relay_recipient_maps` + `reject_unauth_destination`); потеря origin IP для фильтрации на backend'е при пересылке (снимается milter-хуком антиспама + нативным DNSBL на входном хопе, где origin IP ещё виден); порт 25 на приём расширяет поверхность атаки (по умолчанию выключено). **Модель:** Opus (инфра/безопасность, риск open relay). **Внешняя зависимость деплоя:** опциональный antispam-контейнер — вне образа SelfPost, поднимается оператором при включении опции.
|
||||
|
||||
**Зависимости:** не является частью v1.0, зависит только от готового исходящего тракта (уже реализован) и требует отдельного согласования ([development.md](development.md)) до кодирования.
|
||||
**Зависимости:** не является частью v1.0, зависит только от готового исходящего тракта (уже реализован) и требует отдельного согласования ([.cursor/rules/agent-rules.mdc](../.cursor/rules/agent-rules.mdc)) до кодирования.
|
||||
|
||||
---
|
||||
|
||||
@@ -139,7 +140,7 @@ git-тег `vX.Y.Z`; `implementation-plan.md` в `docs/archive/`, ссылок
|
||||
**Почему 2.x, а не v1.x.** [product.md](product.md) относит «несколько пользователей
|
||||
панели, роли» к out of scope (один администратор), поэтому появление второго
|
||||
субъекта — расширение границ проекта, как и Фаза O1: сначала согласование
|
||||
([development.md](development.md)), только потом код. Цена — уровня фазы, а не патча: таблица пользователей и их привязка к доменам, роль в сессии, авторизация в каждом хендлере (а не только на маршруте — сейчас `{id}`/`{aid}` не сверяются ни с чем, кроме существования), пересмотр первичного setup'а и смены пароля под нескольких пользователей, учёт нового субъекта в бэкапе и экспорте домена.
|
||||
([.cursor/rules/agent-rules.mdc](../.cursor/rules/agent-rules.mdc)), только потом код. Цена — уровня фазы, а не патча: таблица пользователей и их привязка к доменам, роль в сессии, авторизация в каждом хендлере (а не только на маршруте — сейчас `{id}`/`{aid}` не сверяются ни с чем, кроме существования), пересмотр первичного setup'а и смены пароля под нескольких пользователей, учёт нового субъекта в бэкапе и экспорте домена.
|
||||
|
||||
*(Прежняя формулировка этого пункта — «2FA и несколько администраторов» — заменена: 2FA снята с рассмотрения, а «несколько администраторов» уточнено до одной конкретной роли, потому что нужна не вторая копия всевластного админа, а ограниченный доступ владельца отдельного домена.)*
|
||||
|
||||
@@ -147,9 +148,8 @@ git-тег `vX.Y.Z`; `implementation-plan.md` в `docs/archive/`, ссылок
|
||||
|
||||
## `CONTRIBUTING.md` — кандидат на 2.x
|
||||
|
||||
**Что это.** Точка входа для стороннего контрибьютора: dev loop (правка на
|
||||
Windows → сборка и прогон на Debian-сервере, потому что Docker локально нет),
|
||||
маршрутизация моделей по типу работы, протокол коммитов, требование
|
||||
**Что это.** Точка входа для стороннего контрибьютора: dev loop, маршрутизация
|
||||
моделей по типу работы, протокол коммитов, требование
|
||||
`gofmt`/`vet`/`test`/`make e2e` до PR. Сейчас всё это есть, но в
|
||||
[development.md](development.md) и [progress.md](progress.md) — то есть на
|
||||
русском и вперемешку с внутренним состоянием проекта.
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
)
|
||||
|
||||
// smtpsAddr is where compose.override.yml publishes the primary implicit-TLS
|
||||
// submission port (465 in the shipped compose, remapped here to stay off the
|
||||
// dev server's real ports).
|
||||
// submission port (465 in the shipped compose, remapped here to avoid clashing
|
||||
// with a local deployment on the same host).
|
||||
const smtpsAddr = "127.0.0.1:20465"
|
||||
|
||||
// sendAttempt is one SMTP transaction against the stand. An empty authLogin
|
||||
|
||||
Reference in New Issue
Block a user