diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e0c1e3d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +node_modules +.next +.env +.env.* +!.env.example +agents +*.log +*.pem +**/*.env +**/*.env.local +**/*.dump +**/*.key +**/.vk-smoke.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b148550 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM node:22-bookworm-slim AS build +WORKDIR /app +RUN apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates && rm -rf /var/lib/apt/lists/* +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npx prisma generate && npm run build + +FROM node:22-bookworm-slim AS runtime +ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3100 BIND_ADDRESS=0.0.0.0 +WORKDIR /app +RUN apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates && rm -rf /var/lib/apt/lists/* +COPY --from=build --chown=node:node /app /app +USER node +EXPOSE 3100 +CMD ["node", "tools/serve-production.cjs"] diff --git a/agents/claude/inbox/2026-10-19-stage1-pr4-review-and-deploy.md b/agents/claude/inbox/2026-10-19-stage1-pr4-review-and-deploy.md new file mode 100644 index 0000000..fee1aaa --- /dev/null +++ b/agents/claude/inbox/2026-10-19-stage1-pr4-review-and-deploy.md @@ -0,0 +1,142 @@ +# Claude — независимая проверка PR #4 и развёртывание Stage1 + +Дата: 2026-10-19. От: владелец, задание подготовил Codex по прямому запросу. + +## Base SHA и материалы + +- PR: https://git.ochenstarik.ru/ochenstarik/randomayzer/pulls/4 +- Base PR при подготовке: `52fb7587d7dda5ae3d1c5e9ecef96b7f48d147a3`. +- Head PR, подлежащий проверке: `930e6ee5e73c9269088b0a9eff576a58ec6ea158`, ветка `codex/stage1-completion`. +- Production source/tests/dependencies/Prisma/Docker/Compose/tools проверены на `cce70c0b8feb263cc9209332c8c50a44bc97428a`; после этого они не менялись до указанного head. +- R5 templates/runbook независимо перепроверены на `c5ede46bd1c31b1e23434538fdb8a1cd925751a6`. +- Этот handoff добавляется следующим документационным коммитом. При старте получить фактические base/head PR: если изменились runtime/config-файлы, проверить новый diff и заново определить reviewed SHA. Не подменять проверенный snapshot текущим HEAD молча. +- Исходное задание: `agents/codex/inbox/2026-10-19-stage1-completion.md` (включая обновление R5 из base выше). +- Итог Codex: `agents/codex/done/2026-10-19-stage1-completion.md`. +- Обязательный runbook: `docs/STAGE1_DEPLOYMENT_RUNBOOK.md`. +- VK checklist/инструкция smoke: `docs/VK_REAL_SMOKE_TEST.md`; контракты: `docs/VK_ID_LIVE_CONTRACT.md`, `docs/VK_INTEGRATION_PLAN.md`. +- Production guards: `docs/PRODUCTION_GUARDS.md`. +- Review evidence: оба `2026-10-19-auditor*-rereview-cce70c0` / `auditor2-high-rereview` и оба `2026-10-19-auditor*-r5-rereview-c5ede46` в `agents/codex/done/`; точные имена взять из дерева репозитория. + +## Scope / роль + +Провести независимый review PR, воспроизвести gate, затем развернуть проверенный результат по runbook на домашнем сервере и существующем VPS. Выполнить оставшиеся живые VK и сетевые проверки. Зафиксировать evidence и решение о приёмке; зелёные локальные тесты Codex не заменяют эту работу. + +Сначала review, затем deployment только после устранения release-blocking findings и выполнения предусловий. Merge в main выполняет владелец либо Claude по исходному заданию и принятому владельцем порядку; не делать merge автоматически до успешного review. Перед deployment зафиксировать выбранный reviewed/deployed SHA и image ID; при merge записать также merge SHA и убедиться, что развёрнут именно проверенный код. + +В рамках этого задания разрешены операторские изменения только Randomayzer: его app/DB/backup, отдельный доменный блок и маршруты существующих прокси, узкие firewall rules согласно runbook. Изменение production source по найденным замечаниям — отдельный implementation task/commit и повторное review, без смешивания с независимым отчётом. Это задание не разрешает незапрошенный рефакторинг. + +## Do not change / границы + +- Соблюдать `AGENTS.md`, `agents/README.md` и протокол памяти `/srv/projects/AI-Memory/00_SYSTEM/AGENT_PROTOCOL.md`. Прочитать project CURRENT_STATE/HANDOFF/TASKS и релевантные verified lessons/patterns. +- Существующие тесты и проверки не ослаблять. Уже разрешённое владельцем исключение: seed32 bytes/64hex и узкие seed assertions, запись `agents/codex/inbox/2026-10-19-seed-owner-authorization.md`. Нового разрешения менять Randomizer/AuditProof/proof format/snapshot hash/HMAC algorithm нет. +- Сохранить чужие рабочие изменения, в частности локальный `AGENTS.md`; не включать их в свои коммиты. +- Не менять WireGuard, домашний роутер, чужие Apache/Express/Nextcloud/Forgejo/fleet/krona/business-platform/Authelia и их volumes. Не ставить второй proxy на80/443. Не выполнять global prune, production down -v, force-push, restart/recreate общих proxy-контейнеров. +- Не включать Authelia вместо Basic Auth без отдельного подтверждения действующей policy; текущий runbook использует Basic Auth. +- Не публиковать БД, wildcard/LAN bind приложения, credentials или proxy configs. Не выводить `.Config.Env`, raw OAuth callback/logs, response bodies с private data, cookies, bearer tokens или Basic Auth hash в отчёт. + +## 1. Независимый review и gate + +1. Получить PR/ветку, записать полный base/head SHA и проверить diff, отчёт Codex и независимые re-review. Проверить узкие seed-исключения, initial migration, сохранность ownership/public-audit boundaries, отсутствие partial participant saves, proxy socket-peer boundary, local-single-user и строгий domain-профиль, smoke LIVE marker, backup/restore и rollback. +2. Убедиться, что прежние HIGH malformed VK records и MEDIUM mock-smoke / отсутствующий reload закрыты именно на проверяемом SHA. Воспроизводимый PoC сильнее общего PASS; новое CRITICAL/HIGH блокирует deployment, нужен отдельный fix и re-review. +3. На отдельной verification checkout/среде выполнить, записывая exit code и краткий фактический результат: + + ```bash + npm ci + npx prisma generate + npm test + npx tsc --noEmit + npm run lint + npm run build + npm audit --omit=dev + ``` + + Ожидание по Codex: 380/380, lint0errors/6warnings, build/tsc pass, production audit0. Это ориентир, не подстановка результата. Полный dev-inclusive audit ранее содержал19 advisories; не объявлять весь dependency tree чистым. +4. Воспроизвести R2 по разделу5 runbook: отдельная PostgreSQL16, только127.0.0.1:55439 после проверки свободного порта; чистая тестовая БД, migrate deploy (3 migrations), integration12/12, schema diff без различий. Integration tests удаляют данные: production DATABASE_URL запрещён. Для чистоты использовать новый только тестовый volume/DB; не удалять существующие volumes для удобства проверки. +5. Проверить proxy templates: `bash agents/codex/notes/validate-r5-templates.sh`; это локальная syntax-проверка, не доказательство active VPS config. Реальную версию и active config проверить отдельно. + +Оставшиеся LOW не скрывать: pre-existing COMMUNITY adminDetection metadata (текущие routes SERVICE/USER/static guard не позволяют неподдерживаемую проверку) и отсутствие обычного HTTP→HTTPS redirect, если добавлен только HTTP-01 маршрут. Для второго HTTPS/ACME acceptance обязательна, редирект обычного HTTP не заявлен. Не направлять обычный HTTP к приложению без защиты ради закрытия LOW; если требуется отдельный redirect route, согласовать scope, проверить diff и порядок активации. + +## 2. Предусловия и фиксация baseline + +- Проверить доступ к обеим машинам. Домашний сервер — текущий сервер проекта, VPS —178.212.13.102; не предполагать, что локальная shell автоматически на нужной машине. +- Подтвердить у владельца, что WG peer10.77.0.1 — именно этот VPS. На доме ожидается существующий krona-wg10.77.0.3/24; проверить маршруты в обе стороны. Сырые private keys/WG configs не публиковать. +- DNS random.ochenstarik.ru должен указывать на178.212.13.102. Ранее локально наблюдался185.26.122.80: перепроверить, не считать исправленным. Если запись неверна — blocker владельцу, не продолжать выпуск сертификата. +- Проверить `ss`, RAM/CPU и диск (ранее82% занято). Порты80/3000/5432/8080 заняты другими сервисами; выбрать свободный3100. Ничего не освобождать остановкой чужих сервисов. +- Владельцу предоставить реальные VK ID app/service credentials и отдельные AUTH_SECRET/TOKEN_ENCRYPTION_KEY в защищённом0600-файле вне Git; production env и smoke session file — разные файлы. Не генерировать/подменять реальные credentials молча. Если их нет, продолжать независимый review/несекретную подготовку, живые шаги отметить BLOCKED. +- VK ID redirect URI проверять фактически в кабинете: R4 `http://localhost:3100/api/auth/vk/callback`, R5 `https://random.ochenstarik.ru/api/auth/vk/callback`. При смене профиля использовать согласованный canonical origin. Поддержку localhost данным VK-приложением не предполагать. +- На VPS снять other-site baseline для всех действующих доменов на80/443, минимум git.ochenstarik.ru; записать status/latency и доступность ключевых сервисов без sensitive bodies. Подтвердить окно без параллельных правок proxy configs. + +## 3. Домашний сервер: R4, backup/restore, затем R5 overlay + +Выполнять разделы1–4 runbook в их порядке, без копирования placeholder path/user в shell: + +- Env0600 вне checkout; `compose.production.yml`, app127.0.0.1:3100, отдельные randomayzer-сети/volume, PG без host port. Пользователи node/postgres, лимиты app1GiB/1.5CPU и PG512MiB/1CPU проверить в runtime. +- Перед изменением существующей БД сделать backup и clean restore drill. Initial migration не применять CREATE поверх уже существующих таблиц: сверить schema/history и только при доказанном соответствии использовать предусмотренный baseline resolve. Ownership migration не обходить фиктивным applied. +- При первом развёртывании поднять БД/миграции/app, проверить health и anonymous `/api/auth/me`. Не использовать fake env/тестовые volumes Codex для реального deployment. +- Проверить loopback profile через браузер/SSH tunnel и строгие guards вне loopback. Проверить cookie HttpOnly/SameSite и Secure согласно профилю, canonical callback/PKCE, CSRF origin и отказ forged forwarded headers от недоверенного peer. Не менять production credentials для имитации тестов. +- Выполнить `tools/db-backup-daily.sh` с реальным env и приватным каталогом: dump восстановлен в чистую temporary DB, проверены schema и counts, drill DB удалена. Не восстанавливать dump поверх действующей БД. Установить/проверить backup timer/service с реальными operator paths/ownership, следующее срабатывание и успешный ручной oneshot. В отчёт только факт/числа/exit, не данные пользователей. +- Перед domain overlay установить узкие правила через фактически действующий firewall manager, с persistence и rollback; не смешивать iptables/nftables/ufw/firewalld. При неизвестной топологии — STOP. +- Только после этого включить `compose.domain.yml`: host bindings127.0.0.1 и10.77.0.3, без0.0.0.0/LAN/DBports. Domain app guards строгие HTTPS/Secure/canonical origin. С VPS upstream10.77.0.3:3100 должен отвечать. + +## 4. VPS: только additive proxy changes по разделу6 runbook + +- Подтвердить actual images/patch versions/mount paths/includes/process argv и active/source config совпадение у `krona-reverse-proxy` (Caddy2.8) и `krona-proxy-mux` (HAProxy3.0). Не угадывать backend names или предполагать SNI passthrough: выяснить actual topology. +- Сохранить оба active/source configs и необходимые config trees в root-only backup, проверить полноту/совпадение. Не выкладывать их в Git/report. Single-file bind mount редактировать с сохранением inode, сверить содержимое внутри контейнера. +- До правок должны проходить `caddy validate` и `haproxy -c` с реальными путями и всеми active -f; после каждой правки повторить. При baseline error не чинить чужой proxy в рамках Randomayzer. +- Подтвердить штатный reload до правок: PID1 master-worker и -W/-Ws + полный config set для указанной функции USR2; Caddy admin API доступен для штатного reload. Если preconditions не доказаны — STOP, не использовать restart как обход. +- Порядок: при необходимости только HTTP-01 Host/path route → validate/reload HAProxy; защищённый Basic Auth Caddy site → validate/reload Caddy; затем TLS SNI route → validate/reload HAProxy. На каждом этапе сравнить existing-site baselines. Не заменять целиком configs шаблонами репозитория. +- Проверить, что worker handoff HAProxy состоялся, а ID/StartedAt/RestartCount обоих proxy containers не изменились. Отсутствие рестарта подтвердить наблюдением, не только названием команды. +- TLS выдан для правильного домена, chain/hostname/expiry валидны без curl -k, HTTP-01 реально работает; synthetic validate не доказывает certificate issuance. Не форсировать repeated ACME issuance/renewal до rate limit. +- Из внешней точки без proxy auth —401; с proxy auth, но без app session — безопасный unauthenticated JSON; VK callback/login по домену работает, cookies Secure. Basic Auth пароль не передавать в CLI args; использовать interactive prompt/0600-file из smoke инструкции. +- Если отдельный HTTP redirect не настроен, записать фактическое поведение HTTP как LOW; не объявлять его проверенным HTTPS redirect. + +## 5. Живой VK: A–H и ручные I–M + +Использовать owner-approved тестовый аккаунт, публичный пост с заведомо непустыми likes/comments, доступный этому organizer restricted post и группу для subscription. Нужны реальные ответы; MOCK/пустой импорт не доказательство. Для полной приёмки E/H не пропускать из-за необязательности env-полей. + +1. Пройти browser consent вручную, проверить точный callback/ошибки и authenticated session. Экран B никогда не засчитывается по exit code скрипта. +2. Создать отдельный smoke env0600 вне Git по `docs/VK_REAL_SMOKE_TEST.md`. Cookie и proxy credentials только там; `VK_SMOKE_CREATE_GIVEAWAY=YES` означает разрешённое создание тестового giveaway. Использовать только согласованный тестовый контент. +3. Выполнить `./tools/vk-smoke.sh ` в применимых профилях; итоговый domain run обязателен. Записать каждый A–H со статусом PASS/FAIL/SKIP/MANUAL/BLOCKED, временем и безопасным evidence. Exit0 не превращает SKIP/MANUAL в PASS. +4. Проверить смысл пунктов: A PKCE S256/OAuth redirect; B настоящий consent; C session; D public SERVICE; E restricted USER (видимый organizer); F creation; G непустой полный import likes/comments; H полная subscription check, включая участника/неучастника при наличии разрешённых данных. Если реальные scopes/capabilities не позволяют сценарий — фактическая ошибка и BLOCKED/UNVERIFIED, не смена policy ради PASS. +5. Ручные I–M: lock snapshot → один draw → public verification без app session → refresh на естественно истёкшем/разрешённом тестовом credential либо явная reauthentication → logout/unauthenticated. Для public verify сохранить обязательную proxy auth, убрать именно app session; приватные organizer/participant/credential данные не должны попасть в public response. Refresh не имитировать редактированием live credential DB; если нельзя безопасно дождаться expiry — оставить отдельный BLOCKED. +6. В браузере проверить объяснения неподдерживаемых manager/repost правил, понятность отказов и отсутствие ложного статуса участника. Проверить сохранение previous participants при неполном импорте; опасные/malformed сценарии воспроизводить в изолированной fixture, не вмешиваться в live VK и не создавать искусственную нагрузку для rate limit. +7. Для VK API/VK ID/OAuth/scopes/limits/method capabilities сверить актуальную официальную документацию VK/VKCOM, записать прямой источник и дату. Разделить official contract, наблюдавшийся live result и оставшийся UNVERIFIED; не выводить общий контракт из одного успешного запроса. `execute` текущим provider не вызывается: не добавлять его для smoke, отметить неприменимость и неизвестные лимиты. +8. Тестовые giveaways отметить и убрать только согласованным штатным способом после сохранения безопасного evidence; не делать прямой SQL cleanup production данных. Выйти из тестовой session, удалить smoke cookie-file/private temp artifacts безопасным способом. Реальные production env/backup не удалять. + +## 6. Сетевая изоляция и чужие сервисы + +Проверить доступность app3100 из каждой точки; для каждой записать source/destination и результат: + +| Точка | Ожидание | +|---|---| +| Домашний loopback127.0.0.1 | app доступен | +| VPS10.77.0.1 → home10.77.0.3 | app доступен через WG | +| Другая LAN машина → домашний LAN IP и10.77.0.3, если есть маршрут | app недоступен | +| Независимая внешняя машина → домашний публичный IP и VPS public IP:3100 | app не опубликован | +| Другой WG peer, если доступен для проверки | app недоступен | +| Внешняя машина → random.ochenstarik.ru:443 | TLS +401 без proxy auth | + +Отсутствие тестовой точки не равно PASS: отметить UNVERIFIED с конкретным недостающим evidence. Проверить, что PG не имеет published port (не спутать с чужим127.0.0.1:5432). Из разрешённого туннеля подтвердить реальный socket peer10.77.0.1; forged Host/X-Forwarded-* от чужого peer не должны выбирать доверенный контекст. Приложение не должно молча терять peer из-за Docker/NAT; если теряет — blocker, не включать TRUST_PROXY=true для всех. + +После каждого proxy reload и в конце повторить baseline всех прежних сайтов/сервисов: Forgejo, fleet-hub, krona, business-platform и существующий auth flow. Сравнить80/443 и meaningful доступность с собственным baseline; прежний401/redirect не считать сбоем, но изменение поведения расследовать. Не использовать для этого чужие credentials. + +## 7. Условия остановки и rollback + +При новом CRITICAL/HIGH, неизвестной topology/credential policy, невалидном config, отсутствии backup/reload evidence, лишнем bind, failed migration, недоступности app или ухудшении любого чужого сайта остановить дальнейшее продвижение. Не публиковать небезопасный deployment ради завершения checklist. + +Откат строго по разделам4/6.6 runbook: вернуть только свои config edits без затирания параллельных изменений, validate, штатные reload без restart, повторить all-site baselines; убрать только свои firewall rules и domain overlay, оставить loopback. App rollback к сохранённому image. Текущую production DB/volume сохранять; при необходимости DB restore — новая БД, проверка и согласованное переключение, не автоматическое затирание данных. Если безопасный rollback общих proxy не доказан — передать владельцу точную ошибку/backup location без секретов, не перезапускать их наугад. + +## Definition of Done и отчёт + +Сохранить `agents/claude/done/2026-10-19-stage1-pr4-review-and-deploy.md` и собственный worklog; обновить project state согласно memory protocol, сохраняя чужие записи. + +Отчёт обязан содержать: +- PR URL, base/reviewed/result/merge/deployed полные SHA, image ID и фактические версии; когда/на какой машине выполнено. +- Все изменённые repo files и краткое описание operator changes, backup/rollback locations без sensitive contents. +- Реально выполненные команды, exit codes/краткий sanitized вывод; тесты/lint/build/audit и clean migrations/integration/restore отдельно. +- CRITICAL/HIGH/MEDIUM/LOW findings с evidence и CLOSED/OPEN, решения по двум известным LOW; не закрывать своим заявлением finding без повторной проверки. +- Матрицу A–M (каждый пункт отдельно), live contractual sources, proxy/TLS/401/domain login, firewall persistence и все сетевые точки, other-site baseline до/после, отсутствие proxy restart. +- Список UNVERIFIED, blockers и non-blocking debt; если есть SKIP/MANUAL/BLOCKED, итог не «всё PASS». +- Вердикты отдельно: PR review; домашний deployment; VPS/domain; live VK; рекомендация главному координатору. Отсутствие credentials/access — частичный отчёт с конкретным blocker, без придуманного успеха. + +Полная приёмка возможна только при выполнении исходного DoD, фактических gates, отсутствии release-blocking CRITICAL/HIGH, сводке независимых findings и зафиксированном deployed SHA. Исполнитель не объявляет Phase CLOSED/release-ready/production-ready самостоятельно: финальное решение принимает главный координатор по evidence. diff --git a/agents/codex/done/2026-10-19-auditor1-r5-rereview-c5ede46.md b/agents/codex/done/2026-10-19-auditor1-r5-rereview-c5ede46.md new file mode 100644 index 0000000..b43d3b8 --- /dev/null +++ b/agents/codex/done/2026-10-19-auditor1-r5-rereview-c5ede46.md @@ -0,0 +1,48 @@ +# Auditor1 R5 re-review + +## Scope and revision + +- Base SHA: `ca28be6ae629d3d67936475ea5896b6f70f6e1e4` +- Reviewed SHA: `c5ede46bd1c31b1e23434538fdb8a1cd925751a6` +- Scope: re-review of prior auditor1 findings in the HAProxy no-restart reload procedure and active/source backup integrity, plus staged HTTP-01 → Caddy auth → TLS route ordering. +- Resulting code SHA: no code changes by reviewer. +- Files changed by this review: unique assignment and report only; existing `AGENTS.md` modification preserved. + +## Prior findings + +### CLOSED — MEDIUM: HAProxy no-restart reload path was unspecified + +The runbook now has a concrete, gated master-worker path in `docs/STAGE1_DEPLOYMENT_RUNBOOK.md:414-477`. It first requires PID 1 to be `haproxy`, an actual `-W`/`-Ws` argument, and manual confirmation that every live `-f` path matches the validated config set (`:416-425`). The function checks PID 1 and the master-worker argument again immediately before signaling (`:435-440`), sends USR2 specifically to the confirmed PID 1 (`:441-442`), waits for a new HAProxy child worker PID (`:443-451`), and requires unchanged container ID, start time, restart count, and running state (`:452-455`). Unknown process models stop without sending a signal (`:416-420`). Staged activation and failure/rollback instructions are now explicit (`:459-477`, `:518-525`). + +Evidence: source inspection at those lines. The coordinator reports having tested the exact function against an official HAProxy 3.0 fixture: worker handoff occurred with ID/StartedAt/RestartCount unchanged, and non-master PID 1 was rejected before signal. That fixture result was not independently rerun in this review. The live VPS process model remains UNVERIFIED, so this closes the runbook gap only; operation on the VPS remains conditional on its documented preflight. + +### CLOSED — LOW: directory-tree backups lacked completion/integrity checks + +The directory-mount branch now checks that all four active/source backup trees are nonempty, compares each source tree to its saved copy, and compares each active tree copy against its source copy before edits (`docs/STAGE1_DEPLOYMENT_RUNBOOK.md:303-326`). The single-file path also compares each host-source backup to its source (`:289-300`). The root shell is instructed to use `set -euo pipefail` (`:148-155`), so a failed copy command aborts before the later steps; explicit mismatch checks stop with a non-content error. + +Evidence: source inspection at those lines. The coordinator reports directory backups and diff checks reviewed; no VPS backup commands were executed in this review. + +## Remaining finding + +### LOW — When adding a new HTTP-01-only route, the final config never routes ordinary HTTP requests for the hostname to Caddy + +The new conditional route in `deploy/randomayzer.haproxy.cfg:18-27` matches both the requested host and only `/.well-known/acme-challenge/`. The runbook says to add this challenge-only route when the hostname is not already sent to Caddy, then to add Caddy auth and later the TLS SNI route (`docs/STAGE1_DEPLOYMENT_RUNBOOK.md:375-391`, `:459-477`). It never adds or enables a general HTTP host route after the Caddy site and TLS route are ready. In that topology, ordinary `http://random.ochenstarik.ru/` traffic continues to HAProxy's previous catch-all and cannot receive Caddy's automatic HTTP-to-HTTPS redirect. This is conditional on the HTTP host not already being routed to Caddy; the runbook's local/domain route baselines should include an explicit HTTP check for Randomayzer after activation. + +Evidence: the official Caddy Automatic HTTPS documentation says it creates HTTP-to-HTTPS redirects on port 80, while the HAProxy example routes only the challenge path to Caddy: [Caddy Automatic HTTPS](https://caddyserver.com/docs/automatic-https) and the cited source lines. This was not tested against the live topology. A post-TLS stage can route the full host to Caddy after the authenticated site is loaded, preserving the challenge path and other hosts; if broad HTTP forwarding is not intended, document the deliberate behavior instead. + +## Staged ordering assessment + +The runbook now orders a needed HTTP-01 challenge-only route first, Caddy's Basic Auth site second, and the new TLS SNI route third (`docs/STAGE1_DEPLOYMENT_RUNBOOK.md:375-391`). The HTTP-01 route is limited to the challenge prefix and preserves port 80, while the TLS SNI route remains absent until after Caddy has loaded the protected site. The ordering is sound for avoiding a new public TLS route before the auth config is active. The remaining LOW finding above concerns ordinary HTTP requests after activation, not the ordering of challenge/auth/TLS stages. + +## Commands and evidence + +- `git rev-parse HEAD` — `c5ede46bd1c31b1e23434538fdb8a1cd925751a6`. +- `git diff --check ca28be6ae629d3d67936475ea5896b6f70f6e1e4 c5ede46bd1c31b1e23434538fdb8a1cd925751a6` — exit 0. +- Source diff inspected with `git diff --unified=5 ca28be6ae629d3d67936475ea5896b6f70f6e1e4 c5ede46bd1c31b1e23434538fdb8a1cd925751a6 -- deploy/randomayzer.haproxy.cfg docs/STAGE1_DEPLOYMENT_RUNBOOK.md`. +- No Docker, container, VPS, application test, or full-gate commands were run. Coordinator-reported checks: official HAProxy 3.0 fixture behavior and config validation passed; 25 Bash blocks passed syntax checks. These are not claimed as independently run. + +## UNVERIFIED and blockers + +- VPS PID 1, actual HAProxy arguments and complete `-f` config set, mounts, routing rules, Caddy admin endpoint, and HTTP path to Caddy remain UNVERIFIED. The new reload command must not run unless the live preflight proves its preconditions. +- DNS, public certificate issuance, external HTTP/HTTPS behavior, login, WireGuard peer identity, firewall isolation, and preservation of sites on the actual VPS remain UNVERIFIED. +- No R5 phase closure or deployment readiness is claimed. diff --git a/agents/codex/done/2026-10-19-auditor1-r5-review-ca28be6.md b/agents/codex/done/2026-10-19-auditor1-r5-review-ca28be6.md new file mode 100644 index 0000000..008f903 --- /dev/null +++ b/agents/codex/done/2026-10-19-auditor1-r5-review-ca28be6.md @@ -0,0 +1,51 @@ +# Auditor1 R5 runbook review + +## Scope and revision + +- Base SHA: `8284eb5d006943d42133eb665c99dcbc3133d6a6` +- Reviewed SHA: `ca28be6ae629d3d67936475ea5896b6f70f6e1e4` +- Review scope: immutable R5 delta, especially `deploy/randomayzer.haproxy.cfg`, `deploy/randomayzer.Caddyfile`, `docs/STAGE1_DEPLOYMENT_RUNBOOK.md`, and `agents/codex/notes/validate-r5-templates.sh`. +- Resulting code SHA: not applicable; reviewer made no production or test changes. +- Changed files: this report and its assignment only; pre-existing `AGENTS.md` modification left untouched. + +## Findings + +### MEDIUM — HAProxy no-restart reload procedure is not executable from this runbook + +`docs/STAGE1_DEPLOYMENT_RUNBOOK.md:396-403` deliberately leaves out the HAProxy reload command, and requires the operator to independently discover and prove a safe native reload method. The rollback at `:449-453` likewise requires “both confirmed reload commands.” The task's acceptance explicitly requires reloading without restarting the shared proxy. Until the operator has separately established the process model and worked out the command, this runbook cannot perform or roll back that accepted operation; it correctly blocks rather than guessing, but does not yet supply an executable conditional path. + +Reproducer/evidence: inspect the runbook at those lines; there is no HAProxy command or procedure to establish the target process and verify graceful worker handoff. `git diff --check 8284eb5d006943d42133eb665c99dcbc3133d6a6 ca28be6ae629d3d67936475ea5896b6f70f6e1e4` exited 0, but does not address this operational gap. No live VPS process model was inspected. + +### LOW — Directory-tree backup copies have no explicit completion/integrity check + +For directory mounts, `docs/STAGE1_DEPLOYMENT_RUNBOOK.md:289-304` copies both active trees and both host source trees, but unlike the single-file path at `:284-286`, it has no explicit check that all four copies completed or that the saved trees match their source. The later hash guidance at `:306-307` is general and does not provide a tree-backup verification command. A partial `docker cp`/`cp -a` can therefore leave an incomplete private backup before edits proceed, reducing rollback confidence. + +Reproducer/evidence: follow the directory-mount backup block; after its four copy commands it only runs `chmod`. No `diff`, manifest/hash, or copy-result gate verifies the backup trees. `bash -n agents/codex/notes/validate-r5-templates.sh` exited 0; the validator is a syntax/config fixture and does not exercise VPS backup commands. + +## Reviewed controls that appear correctly represented + +- The runbook gates work on confirmed domain/VPS/WireGuard facts and says to stop for unknown topology (`docs/STAGE1_DEPLOYMENT_RUNBOOK.md` R5 preflight and sections 6.1-6.2). +- It requires root context for `/root` backups, avoids printing `.Config.Env`, backs up both active configs and host sources for file mounts, restricts backup permissions, compares active/source content, and preserves single-file bind-mount inode during edits (`:260-332`). The separate directory-tree integrity gap is above. +- HAProxy SNI routing is conditional on confirmed TCP TLS passthrough; the new explicit hostname rule is to follow existing explicit routes and precede catch-all. Port 80 is conditional on existing HTTP routing and must preserve the ACME challenge path unmodified and target Caddy's cleartext HTTP backend (`:352-361`). +- Caddy and HAProxy are validated inside their running containers before and after edits (`:310-323`, `:363-381`). Caddy reload is concrete and gated on its active admin endpoint; container ID/uptime checks and prior-site baseline checks are specified (`:383-415`). +- Rollback limits restoration to changed files, preserves mounted-file inode, validates both configs, and explicitly prohibits restarting/recreating the shared proxy (`:440-456`). +- The local fixture script is syntactically valid and scoped to Docker network none. It was not executed in this review, per coordinator instruction to avoid duplicating the validator run. + +## Commands and results + +- `git rev-parse HEAD` — `ca28be6ae629d3d67936475ea5896b6f70f6e1e4`. +- `git diff --check 8284eb5d006943d42133eb665c99dcbc3133d6a6 ca28be6ae629d3d67936475ea5896b6f70f6e1e4` — exit 0. +- `bash -n agents/codex/notes/validate-r5-templates.sh` — exit 0. +- No application tests/build, Docker template validator, Docker commands, or VPS commands were run by this reviewer. Root owns the full gates and validator. + +## UNVERIFIED and blockers + +- Actual VPS container arguments, mounts, active config paths, HAProxy routing order, HAProxy process model/reload mechanism, Caddy admin endpoint, and source/active inode relationship were not observable from this local review. +- Public DNS, live TLS/ACME issuance, external auth/login, WireGuard peer identity, firewall isolation, and preservation of existing public sites remain UNVERIFIED. +- Parent reported a local fixture experiment with a `haproxy -W -db` master-worker process and USR2 signal; that is not evidence of the actual VPS process model and was not used to close the finding. +- No project phase or deployment is declared closed. + +## Remaining non-blocking debt + +- Add a conditional HAProxy reload branch only when the active PID 1/process model and exact config set are inspected, then specify observable graceful-handoff and unchanged-container checks plus failure/rollback handling. +- Add explicit verification for all active/source directory-tree backup copies before permitting edits. diff --git a/agents/codex/done/2026-10-19-auditor1-rereview-cce70c0.md b/agents/codex/done/2026-10-19-auditor1-rereview-cce70c0.md new file mode 100644 index 0000000..131a6f4 --- /dev/null +++ b/agents/codex/done/2026-10-19-auditor1-rereview-cce70c0.md @@ -0,0 +1,62 @@ +# Independent rereview — cce70c0 + +Base SHA: `17faecd962c2cf40e0a3f6ec4434811cfffdd312` +Reviewed SHA: `cce70c0b8feb263cc9209332c8c50a44bc97428a` +Result SHA: `cce70c0b8feb263cc9209332c8c50a44bc97428a` (review only; no source changes) + +## Prior findings and status + +### auditor1 MEDIUM — live smoke could accept mock-provider data: CLOSED at this SHA + +The previous reproduction set `USE_VK_MOCK=true` in the production environment while a real service token existed, then omitted the optional restricted-post case. This no longer yields live-smoke evidence: + +- [`ProviderFactory`](/srv/projects/Agent%20projects/randomayzer/src/providers/factory.ts:10) now throws in `NODE_ENV=production` when the mock flag is true, before returning the mock provider. The production Compose service also sets `USE_VK_MOCK: "false"` explicitly at [compose.production.yml](/srv/projects/Agent%20projects/randomayzer/compose.production.yml:33), overriding the env-file value. +- The preview and import responses report `providerMode` from the actual `VkProvider` instance at [preview route](/srv/projects/Agent%20projects/randomayzer/src/app/api/posts/preview/route.ts:32) and [participants route](/srv/projects/Agent%20projects/randomayzer/src/app/api/giveaways/%5Bid%5D/participants/route.ts:99). The token is not included in that field or response. +- The smoke script requires `success` and `providerMode === LIVE` for preview; G/H additionally require a safe integer `totalCount > 0` at [vk-smoke.sh](/srv/projects/Agent%20projects/randomayzer/tools/vk-smoke.sh:94). Thus MOCK and empty imports are failures. +- The regression suite passed: production mock refusal even with a service token; mock instance reports MOCK despite SERVICE auth metadata; real provider reports LIVE without exposing a marker token; and actual shell smoke subprocess fixtures reject MOCK and zero-count imports while accepting LIVE nonempty imports. The tests also assert the cookie marker is absent from output. + +Closure is limited to the false-positive code path and regression evidence. No live VK run was performed. + +### auditor2 HIGH — malformed likes/comments could be silently omitted or collapsed: CLOSED at this SHA + +The `VkProvider` validates both payload envelopes and each returned participant record before returning any list. Likes require complete count/items, object items, positive safe-integer IDs, valid optional profile field types, and unique user IDs across pages. Comments require complete count/items, valid profile lists and IDs, and nonzero safe-integer author IDs; negative community author IDs are explicitly excluded as nonparticipants. Malformed responses throw `VkIncompleteDataError`. + +The import route does not call `GiveawayStore.updateParticipants` until provider fetch, enrichment, and filtering all complete. The focused route regression loops through 25 malformed likes/comments payloads and checks HTTP 502, `VK_INCOMPLETE_DATA`, and preservation of the prior participant list for each. A separate test confirms valid negative community comment authors are ignored. Focused suites passed 13 R3 tests including these cases. + +Closure is limited to malformed-item validation and no-partial-save evidence. External VK response schemas were not verified against current official documentation or a live response. + +## Remaining LOW finding + +### LOW — COMMUNITY metadata still advertises admin detection without a manager-list implementation: OPEN / unchanged + +[`resolveEffectiveCapabilities`](/srv/projects/Agent%20projects/randomayzer/src/providers/vk/vk-capabilities.ts:34) still sets `adminDetection: true` for COMMUNITY auth and removes the explanatory note. This commit does not change that code. The prior auditor noted that the participant import route validates against the static capability object, which keeps the current server-side exclusion guard false; the inaccurate dynamic metadata remains a preview/API contract issue and should stay tracked separately. + +## New findings + +CRITICAL: none. + +HIGH: none in the rereviewed fixes. + +MEDIUM: none in the rereviewed fixes. + +LOW: the unchanged COMMUNITY capability-metadata finding above. + +## Commands actually run + +- Initial `npx vitest run tests/vk-smoke-provider-mode.test.ts tests/vk-r3-safety.test.ts` overlapped the coordinator's `npm ci`; Vitest failed to load PostCSS because `dlv` was temporarily unavailable. A second attempt during the same install window reported `vitest: not found`. Per coordinator instruction, these transient install-state errors were not treated as source findings. +- After the coordinator confirmed `npm ci`/Prisma generation had completed: `npx vitest run tests/vk-smoke-provider-mode.test.ts tests/vk-r3-safety.test.ts` — exit 0; 2 files, 19 tests passed (13 R3 safety, 6 provider-mode/smoke tests). +- `bash -n tools/vk-smoke.sh` — exit 0. +- `node --check tools/production-transport.cjs` — exit 0. +- `node --check tools/serve-production.cjs` — exit 0. +- `git diff --check 17faecd962c2cf40e0a3f6ec4434811cfffdd312..cce70c0b8feb263cc9209332c8c50a44bc97428a` — exit 0. + +The coordinator separately reported `npm ci`, Prisma generation, and full `npm test` exit 0 with 380/380 tests at this SHA; I did not run those commands. Lint/build/audit, real PostgreSQL, and Docker remain coordinator-owned. + +## UNVERIFIED and blockers + +- Actual live VK behavior, consent, token scopes, and current official payload contracts remain UNVERIFIED. +- R4 production Docker startup and backup/restore drill remain unverified by this rereview. +- R5 DNS, TLS/basic-auth, live domain login, Docker-preserved peer address, firewall persistence, and internet/LAN/WireGuard isolation remain operator acceptance blockers. +- No phase closure or release readiness is claimed. + +Files changed by this rereview: `agents/codex/inbox/2026-10-19-auditor1-rereview-cce70c0.md`, `agents/codex/done/2026-10-19-auditor1-rereview-cce70c0.md`, and individual memory worklog `/srv/projects/AI-Memory/01_PROJECTS/randomayzer/worklog/2026-10-19_auditor1_cce70c0-rereview.md`. No production code or tests changed; no commit/push was made. Pre-existing `AGENTS.md` modification was preserved. diff --git a/agents/codex/done/2026-10-19-auditor2-high-rereview.md b/agents/codex/done/2026-10-19-auditor2-high-rereview.md new file mode 100644 index 0000000..10a8059 --- /dev/null +++ b/agents/codex/done/2026-10-19-auditor2-high-rereview.md @@ -0,0 +1,61 @@ +# Auditor2 independent re-review — malformed VK data and live smoke + +Reviewed SHA: `cce70c0b8feb263cc9209332c8c50a44bc97428a` +Comparison SHA: `17faecd962c2cf40e0a3f6ec4434811cfffdd312` +Base SHA: `6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60` +Result SHA: `cce70c0b8feb263cc9209332c8c50a44bc97428a` (review only; no code changes). + +## Conclusion + +The previous **HIGH** finding for malformed VK likes/comments participant data is **CLOSED** at the reviewed SHA. The implementation now rejects malformed records before the import route persists any result, and focused route regressions confirm that prior stored participants survive all included malformed cases. + +The follow-up live-smoke false-success issue is also addressed: smoke checks require the actual provider marker `LIVE`; G/H additionally require a positive safe-integer participant count. Production refuses `USE_VK_MOCK=true` and the production Compose profile explicitly sets it false. Focused adversarial fixtures cover mock provider, empty live imports, and nonempty live imports. + +The COMMUNITY admin capability discrepancy remains a **pre-existing, nonblocking debt**, not a finding introduced by this stage. I independently confirm that classification for current server routes: preview passes `fetchPost` only `organizerId` (no explicit COMMUNITY auth), and the resolver's automatic/service/user selection returns only SERVICE or USER. Giveaway detail derives only SERVICE/USER. Participant/snapshot enforcement reads the provider's static capability, which remains `adminDetection: false`. + +No CRITICAL, HIGH, or MEDIUM findings remain from these reviewed issues. This report does not declare the phase closed. + +## Re-reviewed issues and evidence + +### Prior HIGH — CLOSED: malformed likes/comments responses + +At [vk-provider.ts](/srv/projects/Agent%20projects/randomayzer/src/providers/vk/vk-provider.ts:34), profile records now require a plain object with a positive safe-integer user ID and correctly typed returned fields. [Response validation](/srv/projects/Agent%20projects/randomayzer/src/providers/vk/vk-provider.ts:49) requires a response object, an items array, a nonnegative safe-integer count, and object-shaped items. Likes also reject duplicate user IDs at lines 297–303. Comments reject malformed/zero/non-integer/unsafe author IDs at lines 355–359 and validate returned profile arrays and IDs. + +The earlier concrete failures are blocked: a missing `id` cannot become the participant ID `"undefined"`; a string or unsafe ID is rejected; and malformed comment author IDs are no longer silently omitted while the reported count is accepted. Negative integer comment author IDs remain deliberately excluded as community authors, matching prior behavior and now covered by a focused test. + +At [participants route](/srv/projects/Agent%20projects/randomayzer/src/app/api/giveaways/%5Bid%5D/participants/route.ts:86), fetch/enrichment must finish before `updateParticipants` at line 97. The route-level regression at [vk-r3-safety.test.ts](/srv/projects/Agent%20projects/randomayzer/tests/vk-r3-safety.test.ts:200) iterates 25 malformed likes/comments cases, expects `502 VK_INCOMPLETE_DATA`, and checks the previous participant list remains unchanged after each case. + +### Smoke false-success — CLOSED + +[ProviderFactory](/srv/projects/Agent%20projects/randomayzer/src/providers/factory.ts:10) throws in production if mock mode is explicitly enabled, even when a service token exists. Production Compose sets `USE_VK_MOCK: "false"` at [compose.production.yml](/srv/projects/Agent%20projects/randomayzer/compose.production.yml:36). + +Preview and participant-import responses identify the actual provider instance as `LIVE` or `MOCK`. [vk-smoke.sh](/srv/projects/Agent%20projects/randomayzer/tools/vk-smoke.sh:94) requires `providerMode === "LIVE"`; G/H also require `totalCount` to be a safe integer greater than zero. Its test fixture proves MOCK with reported participants fails before creating/importing a giveaway, LIVE with zero participants fails G/H, and LIVE with nonempty participants passes D/G/H: [vk-smoke-provider-mode.test.ts](/srv/projects/Agent%20projects/randomayzer/tests/vk-smoke-provider-mode.test.ts:66). + +### Pre-existing LOW metadata discrepancy — nonblocking, not stage1-introduced + +`resolveEffectiveCapabilities` still sets `adminDetection: true` for COMMUNITY auth at [vk-capabilities.ts](/srv/projects/Agent%20projects/randomayzer/src/providers/vk/vk-capabilities.ts:34), though no manager-list lookup exists. The exact behavior is present in base SHA `6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60`. Current routes do not supply or resolve COMMUNITY auth: preview calls `fetchPost(url, { organizerId })`; its automatic resolver chooses SERVICE or USER; giveaway details likewise derive only SERVICE/USER. Import and snapshot checks use `provider.capabilities`, whose static `adminDetection` remains false, as enforced by [giveaway-schemas.ts](/srv/projects/Agent%20projects/randomayzer/src/core/validation/giveaway-schemas.ts:89). Thus this metadata branch is unreachable through current routes and cannot enable admin exclusion. I agree it is pre-existing nonblocking debt for this phase, not an open stage1 finding. + +## Commands run by this review + +- `git show -s --format='%H%n%P%n%s' cce70c0b8feb263cc9209332c8c50a44bc97428a` — confirmed fixed commit parent is `17faecd962c2cf40e0a3f6ec4434811cfffdd312`. +- `git diff 17faecd962c2cf40e0a3f6ec4434811cfffdd312 cce70c0b8feb263cc9209332c8c50a44bc97428a -- ` — reviewed only the immutable commit delta. +- `USE_VK_MOCK=true npx vitest run tests/vk-r3-safety.test.ts tests/vk-smoke-provider-mode.test.ts` — exit 0; 2 files, 19 tests passed (13 R3 safety, 6 smoke provider-mode). +- `bash -n tools/vk-smoke.sh` — exit 0. +- `git diff --check 17faecd962c2cf40e0a3f6ec4434811cfffdd312 cce70c0b8feb263cc9209332c8c50a44bc97428a` — exit 0; no whitespace errors. + +Coordinator separately reported raw `npm test` at this SHA passed 380/380 across 63 files; I did not independently rerun the full suite, install, Prisma generation, lint, build, audit, or Docker gates. Live VK and target-host acceptance remain operator-dependent and unverified. + +## UNVERIFIED / remaining blockers + +- Actual live VK payload contracts and stable offset-pagination behavior remain **UNVERIFIED**; this review does not assert external VK method contracts. The existing documentation discloses that changing VK data during offset pagination cannot be proven point-in-time consistent. +- Live VK consent, real credentials, and production A–H smoke remain unverified. +- Actual VPS/WireGuard socket peer preservation, firewall persistence, TLS/basic-auth, and LAN/internet isolation require operator checks on target hosts. +- Coordinator's full lint/build/audit/Docker acceptance is outside this rereview; no phase-closure claim is made. + +## Files changed by this rereview session + +- `agents/codex/inbox/2026-10-19-auditor2-high-rereview.md` — task record. +- `agents/codex/done/2026-10-19-auditor2-high-rereview.md` — this report. +- `/srv/projects/AI-Memory/01_PROJECTS/randomayzer/worklog/2026-10-19_auditor2_high-rereview.md` — individual memory worklog. + +No production source/tests were changed, and no commit, push, container mutation, or communication with auditor1 was made. diff --git a/agents/codex/done/2026-10-19-auditor2-high-vk-items.md b/agents/codex/done/2026-10-19-auditor2-high-vk-items.md new file mode 100644 index 0000000..d88bf3f --- /dev/null +++ b/agents/codex/done/2026-10-19-auditor2-high-vk-items.md @@ -0,0 +1,30 @@ +# Result — auditor2 HIGH malformed VK items + +Base SHA: `17faecd962c2cf40e0a3f6ec4434811cfffdd312` +Result SHA: `17faecd962c2cf40e0a3f6ec4434811cfffdd312` (working-tree changes only; no commit) + +## Files changed + +- `src/providers/vk/vk-provider.ts` +- `tests/vk-r3-safety.test.ts` +- `agents/codex/inbox/2026-10-19-auditor2-high-vk-items.md` +- `agents/codex/notes/2026-10-19-auditor2-high-vk-items.md` +- `agents/codex/done/2026-10-19-auditor2-high-vk-items.md` + +## Change and evidence + +VK likes and comments payloads now fail with `VkIncompleteDataError` for missing/invalid response arrays or counts, malformed items, unsafe/noninteger/zero IDs, invalid optional profile fields, and duplicate likes/profile IDs. Negative safe-integer comment author IDs remain explicitly excluded under the documented community-author policy. Route-level regression cases verify each malformed response preserves the existing participant store state. + +Commands actually run: + +- `USE_VK_MOCK=true npx vitest run tests/vk-r3-safety.test.ts` — exit 0; 13 tests passed, including 25 malformed-payload route cases. +- `npx tsc --noEmit` — exit 0. +- `git diff --check -- src/providers/vk/vk-provider.ts tests/vk-r3-safety.test.ts` — exit 0. + +Not run for this narrow follow-up: `npm ci`, `npx prisma generate`, full `npm test`, lint, build, or npm audit. Coordinator must rerun full gates on the integrated working tree. + +CRITICAL/HIGH/MEDIUM/LOW findings: this implementation addresses the auditor2 HIGH finding; independent re-review remains required by project policy. No new issue identified by the focused checks. The auditor2 LOW COMMUNITY capability metadata finding is outside this assignment and remains for coordinator classification. + +UNVERIFIED: external VK response schemas and the documented negative community author behavior have not been verified against current official VK documentation. No live VK credentials/network were used. + +Remaining blockers: independent review and integrated full-gate rerun. Non-blocking tech debt: auditor2 LOW capability-metadata issue as assigned to the coordinator. diff --git a/agents/codex/done/2026-10-19-auditor2-r5-rereview-c5ede46.md b/agents/codex/done/2026-10-19-auditor2-r5-rereview-c5ede46.md new file mode 100644 index 0000000..edb9b6e --- /dev/null +++ b/agents/codex/done/2026-10-19-auditor2-r5-rereview-c5ede46.md @@ -0,0 +1,44 @@ +# Result: independent R5 follow-up review + +Base SHA: `ca28be6ae629d3d67936475ea5896b6f70f6e1e4` +Reviewed SHA: `c5ede46bd1c31b1e23434538fdb8a1cd925751a6` +Result SHA: `c5ede46bd1c31b1e23434538fdb8a1cd925751a6` (review only; no source changes) +Files changed by this reviewer: this report and its inbox task record only. + +## Review result + +The prior **MEDIUM is CLOSED for the runbook/documentation deliverable**. The prior **LOW is CLOSED**. I found no new CRITICAL, HIGH, or MEDIUM finding in the reviewed delta. + +The prior MEDIUM was that the runbook had no executable HAProxy no-restart reload procedure. At `docs/STAGE1_DEPLOYMENT_RUNBOOK.md:414-457`, the revised procedure first requires PID 1 to be `haproxy`, an exact `-W`/`-Ws` argument, and manual confirmation that its `-f` paths match the complete validated config set (`:416-425`). It stops without signalling if those checks fail. The reload function repeats the PID/mode check, snapshots Docker ID/start time/restart count, requires existing worker PIDs, sends USR2 specifically to the confirmed container master, waits for a new worker PID, and verifies the container remains running with unchanged identity/start/restart count (`:435-455`). The operator invokes it only after config/source gates (`:459-464`). This is a concrete, conditionally safe procedure, not an instruction to guess at or alter the deployed process model. + +That signal choice agrees with the [official HAProxy 3.0 Management Guide](https://docs.haproxy.org/3.0/management.html), which documents `-W` as master-worker mode and SIGUSR2 to the master as a reload mechanism. The coordinator reports it separately exercised the exact function against an official HAProxy 3.0 image: worker handoff occurred with Docker ID, StartedAt, and RestartCount unchanged, while a non-master PID 1 was rejected before a signal. I did not rerun that container test. + +The prior LOW was the ordering that could route TLS requests to Caddy before Caddy loaded its site configuration. It is closed by the explicit activation order at `docs/STAGE1_DEPLOYMENT_RUNBOOK.md:374-392`: when needed, first add and reload only the HTTP-01 challenge route; next validate and reload Caddy with Basic Auth; only after that succeeds add and reload the TLS SNI route. The HTTP route is constrained to the exact Host and `/.well-known/acme-challenge/` prefix in `deploy/randomayzer.haproxy.cfg` (lines 18-26), so this staging does not expose the new TLS route prematurely. + +The follow-up also adds explicit source/active config-tree backup comparisons at `docs/STAGE1_DEPLOYMENT_RUNBOOK.md:318-325`, complementing the single-file and inode checks. Rollback now restores and reloads Caddy before using the same gated HAProxy handoff procedure (`:509-525`). + +## Commands actually run + +- `git rev-parse HEAD` — `c5ede46bd1c31b1e23434538fdb8a1cd925751a6`. +- `git status --short` — showed pre-existing `AGENTS.md` modification and shared task artifacts; no source changes by this reviewer. +- `git diff --stat ca28be6ae629d3d67936475ea5896b6f70f6e1e4 c5ede46bd1c31b1e23434538fdb8a1cd925751a6` — scoped follow-up change summary. +- `git diff --no-ext-diff --unified=15 ca28be6ae629d3d67936475ea5896b6f70f6e1e4 c5ede46bd1c31b1e23434538fdb8a1cd925751a6 -- docs/STAGE1_DEPLOYMENT_RUNBOOK.md deploy/randomayzer.haproxy.cfg` — inspected the review delta. +- `git show c5ede46bd1c31b1e23434538fdb8a1cd925751a6:docs/STAGE1_DEPLOYMENT_RUNBOOK.md | nl -ba | sed -n '140,550p'` — inspected final line references. +- `git show c5ede46bd1c31b1e23434538fdb8a1cd925751a6:docs/STAGE1_DEPLOYMENT_RUNBOOK.md | nl -ba | sed -n '275,325p;374,480p;509,528p'` — verified cited reload, ordering, tree-compare and rollback locations. +- `git diff --check ca28be6ae629d3d67936475ea5896b6f70f6e1e4 c5ede46bd1c31b1e23434538fdb8a1cd925751a6` — exit 0. +- Read the official HAProxy 3.0 Management Guide at `https://docs.haproxy.org/3.0/management.html` for master-worker/SIGUSR2 semantics. + +Coordinator reports Caddy/HAProxy config validation and syntax checks for 25 Bash blocks passed. Those checks were not rerun by this reviewer. No full application gates, Docker test, live VPS command, or container mutation was performed by me. + +## UNVERIFIED and remaining blockers + +- Actual VPS PID 1, HAProxy argv/config paths, mounts, active/source equality, master-worker mode, and successful live handoff remain UNVERIFIED. The runbook explicitly stops before editing/signalling unless those preconditions are established on the VPS. +- Actual Caddy admin endpoint, routing topology, HTTP-01 issuance/renewal, DNS/WireGuard/firewall preconditions, and existing-site baselines remain UNVERIFIED. No live acceptance or phase closure is claimed. +- No remaining code-review blocker in the two prior findings; live operation remains gated on the documented operator preconditions and acceptance checks. + +## Severity status + +- CRITICAL: none. +- HIGH: none. +- MEDIUM: prior no-command finding CLOSED for the runbook; deploy-time process model remains UNVERIFIED. +- LOW: prior activation-order window CLOSED by the staged HTTP-01, Caddy, then TLS route sequence. diff --git a/agents/codex/done/2026-10-19-auditor2-r5-review.md b/agents/codex/done/2026-10-19-auditor2-r5-review.md new file mode 100644 index 0000000..daf838c --- /dev/null +++ b/agents/codex/done/2026-10-19-auditor2-r5-review.md @@ -0,0 +1,52 @@ +# Result: independent R5 review + +Base SHA: `8284eb5d006943d42133eb665c99dcbc3133d6a6` +Reviewed SHA: `ca28be6ae629d3d67936475ea5896b6f70f6e1e4` +Result SHA: `ca28be6ae629d3d67936475ea5896b6f70f6e1e4` (review only; no source changes) +Files changed by this reviewer: this report and the task record only; no production or test files. + +## Scope and evidence + +Reviewed the immutable `8284eb5..ca28be6` diff, the updated stage-one owner manifest, and the Caddy/HAProxy templates, removed nginx template, R5 runbook, and local syntax validator. I did not inspect live VPS configuration, alter containers, or run the validator/full gates. The current checkout HEAD matched the reviewed SHA during review. `AGENTS.md` had a pre-existing local modification and was left untouched. + +The runbook does a careful job of distinguishing observed configuration from examples. It requires operator discovery of exact proxy image/version, process arguments, mounts and include files, port 443 mode/SNI ordering, port 80 HTTP-01 path, and source/active configuration identity before editing. It backs up both active proxy files and host source; it also gives separate full-tree backups for directory mounts. Single-file inode preservation, post-edit active/source comparison, validation before reload, a two-protocol baseline for every operator-enumerated site, and scoped rollback are documented. The HAProxy snippets are additive, preserve existing route/default order, and use separate TLS passthrough and cleartext Caddy backends. The Caddy 2.8 site disables TLS-ALPN to require HTTP-01 and does not add access logging. The obsolete nginx alternative is removed. + +## Findings + +### MEDIUM — R5 runbook cannot execute the HAProxy apply step without a separate procedure + +`docs/STAGE1_DEPLOYMENT_RUNBOOK.md:396-403` deliberately leaves the HAProxy reload command unspecified and directs the operator to find a deployment-specific mechanism, otherwise to stop. This is a sound safety guard against guessing at a shared proxy’s process model, but it means this deliverable does not itself provide the requested executable no-restart apply path even for the owner-confirmed Docker/HAProxy 3.0 deployment. The operator must research and supply a process-specific procedure before R5 can proceed; the current text correctly calls that an operational blocker. Parent reported an isolated official HAProxy 3.0 fixture where USR2 preserved the container identity/start time/restart count while replacing worker PIDs, but this does not prove the actual VPS runs master-worker mode or that its PID1/config mechanism accepts that signal. Do not transfer that fixture result to production. + +### LOW — HAProxy route is reloaded before Caddy’s new site configuration + +`docs/STAGE1_DEPLOYMENT_RUNBOOK.md:405-408` instructs applying the HAProxy route first and reloading Caddy second. During that interval, new TLS traffic for the hostname can be sent to a Caddy process that has not loaded the site block yet, producing a transient certificate or routing failure. A safer sequence is to reload Caddy first and then expose the new HAProxy SNI route, with validation/hash gates retained between steps. This is a narrow new-host availability window; it does not imply harm to existing sites. + +No CRITICAL or HIGH findings in the reviewed R5 delta. + +## Commands actually run + +- `git rev-parse HEAD` — `ca28be6ae629d3d67936475ea5896b6f70f6e1e4`. +- `git status --short` — showed the pre-existing `AGENTS.md` modification and task-record files; no source edits from this review. +- `git show --stat --oneline --no-renames 8284eb5d006943d42133eb665c99dcbc3133d6a6..ca28be6ae629d3d67936475ea5896b6f70f6e1e4` — reviewed R5 delta summary. +- `git show ca28be6...:docs/STAGE1_DEPLOYMENT_RUNBOOK.md | nl -ba | sed -n '140,470p'` — inspected runbook and cited line references. +- `git show ca28be6...:deploy/randomayzer.haproxy.cfg | nl -ba` and `git show ca28be6...:deploy/randomayzer.Caddyfile | nl -ba` — inspected proxy snippets. +- `git show ca28be6...:agents/codex/notes/validate-r5-templates.sh | nl -ba` — inspected the local syntax-validator implementation; did not execute it. +- `git diff --check 8284eb5d006943d42133eb665c99dcbc3133d6a6 ca28be6ae629d3d67936475ea5896b6f70f6e1e4` — exit 0. +- `bash -n <(git show ca28be6ae629d3d67936475ea5896b6f70f6e1e4:agents/codex/notes/validate-r5-templates.sh)` — exit 0. + +Coordinator reports that target-version Caddy/HAProxy validation and Docker R5 validator passed, and app gates plus PostgreSQL integration passed at identical runtime code. These are coordinator-reported results, not commands run by this reviewer. This review did not run npm tests, lint, build, the Docker validator, or any VPS operation. + +## UNVERIFIED + +- Actual VPS container IDs, image patch versions, entrypoints, config/include paths, mount sources, inodes and active/source equality. +- Actual HAProxy mode, TLS inspector, SNI precedence, catch-all ordering, cleartext port 80 route, HTTP-01 reachability, and graceful reload mechanism. +- Actual Caddy admin endpoint/reload availability, certificate issuance/renewal, DNS, WireGuard peer identity, firewall policy, login/callback behavior, and all-site baseline health. +- Directory-mount include trees are backed up, but the runbook does not give a concrete recursive active/source tree comparison command after edits; it requires the operator to compare the full set. This is non-blocking documentation debt if that comparison is completed before reload. + +## Remaining blockers and non-blocking debt + +- The MEDIUM runbook gap blocks live R5 application until a conditional, inspection-gated HAProxy reload procedure is documented or an operator supplies and records the verified deployment-specific method. +- The LOW reload-order issue should be corrected before live use. +- Actual topology and live acceptance remain the operator’s responsibility and unverified. No phase-closed or production-ready determination is made here. + +Final verdict: the R5 configuration examples and safeguards are well-scoped, but the current runbook is not yet an executable end-to-end procedure for the HAProxy apply step. No live environment facts were inferred. diff --git a/agents/codex/done/2026-10-19-auditor2.md b/agents/codex/done/2026-10-19-auditor2.md new file mode 100644 index 0000000..1760a8e --- /dev/null +++ b/agents/codex/done/2026-10-19-auditor2.md @@ -0,0 +1,60 @@ +# Independent auditor2 review — stage1 R1–R5 + +Reviewed SHA: `17faecd962c2cf40e0a3f6ec4434811cfffdd312` +Base SHA: `6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60` +Result SHA: `17faecd962c2cf40e0a3f6ec4434811cfffdd312` (review only; no code changes). + +## Scope and conclusion + +Reviewed the immutable base-to-HEAD patch against R1–R5, with emphasis on malformed/incomplete VK responses and persistence, unsupported repost/manager checks, smoke behavior and credential handling, migration bootstrap, production socket trust, deployment binding and operator runbook. + +One **HIGH** issue remains in R3: likes/comments item records are not validated, allowing malformed VK payload elements to be silently accepted or omitted and the resulting list to be persisted as complete. One **LOW** issue remains in capability metadata: COMMUNITY auth is reported as enabling admin detection despite no manager-list fetch implementation. No CRITICAL findings. This is not a phase-closure decision. + +## Findings + +### HIGH — malformed likes/comments can produce a persisted “complete” participant list + +Evidence: [vk-provider.ts](/srv/projects/Agent%20projects/randomayzer/src/providers/vk/vk-provider.ts:272) converts `item.id` directly with `String(item.id)` and inserts the participant without checking that the ID is a valid user identifier. The comments path accepts any `from_id` satisfying JavaScript `> 0` and silently ignores other values at [vk-provider.ts](/srv/projects/Agent%20projects/randomayzer/src/providers/vk/vk-provider.ts:308). The pagination guard at [vk-client.ts](/srv/projects/Agent%20projects/randomayzer/src/integrations/vk/vk-client.ts:249) validates array size and reported totals, but not element shape. The route then saves whatever the provider/pipeline returned at [participants/route.ts](/srv/projects/Agent%20projects/randomayzer/src/app/api/giveaways/%5Bid%5D/participants/route.ts:95). + +For example, a likes page with a reported matching count and an item missing `id` becomes participant ID `"undefined"`; multiple such records collapse in the map. A malformed comment `from_id` is counted by pagination but omitted from participants. Either path can silently change draw eligibility while completing the import and replacing the previous list. The added R3 tests cover missing/short pages and incomplete `groups.isMember` results, but do not exercise malformed likes/comments item payloads. + +The provider should reject malformed participant records with the explicit incomplete-data error before returning; a regression test should assert the existing participant list survives each malformed payload case. + +### LOW — COMMUNITY capability metadata claims admin detection without an implementation + +Evidence: [vk-capabilities.ts](/srv/projects/Agent%20projects/randomayzer/src/providers/vk/vk-capabilities.ts:34) sets `adminDetection: true` whenever the auth type is `COMMUNITY` and clears the explanation. The provider implementation has no manager-list lookup, and [participant-enricher.ts](/srv/projects/Agent%20projects/randomayzer/src/core/pipeline/participant-enricher.ts:28) only enriches subscriptions. Actual participant/snapshot validation currently uses the provider’s static capabilities (`adminDetection: false`), so this metadata discrepancy does not bypass the current server guard; however, the preview API can return a capability consumers could treat as available. + +Keep this capability false until a complete manager-detection implementation exists, or derive it from a separately verified capability; add a test for COMMUNITY auth metadata. Current end-user flow selecting manager exclusion is disabled with a clear explanation, and the server validator rejects it. + +## Areas reviewed without additional findings + +- Pagination rejects invalid/changing totals, short/empty pages before total, oversized pages, missing totals for incomplete data, and max-page truncation. Subscription enrichment validates response shape, unexpected/duplicate/missing IDs and member values. The import route writes only after all fetch/enrichment succeeds. +- Reposts and manager exclusion are rejected by provider capability validation with reasons; the creation UI visibly disables both unsupported options and explains why. The code makes ordinary paginated calls and does not use `execute`. +- Smoke configuration requires a non-symlink regular mode-0600 file, allowlists keys and URL origins, keeps cookie/basic-auth material in curl config under a private temp directory, does not print response bodies, and reports `SKIP`/`MANUAL` distinctly. Exit status is zero when executed checks pass even when others are skipped/manual, but the script prints skip counts and its documentation explicitly says skipped checks are not passed; I did not classify that as a false PASS. +- The new initial migration intentionally models the pre-ownership schema; the following ownership migration aborts on null owner rows, then makes the column non-null and changes FK behavior. The runbook explicitly requires backup/schema/history inspection and says not to apply CREATE TABLE over existing tables. The recorded disposable-PostgreSQL migration evidence includes a zero-difference Prisma schema check. +- The production entrypoint replaces proxy metadata before Next receives the request, derives trust from `socket.remoteAddress`, strips proxy headers from untrusted peers, and allows only the configured `10.77.0.1` peer in the domain profile. Tests cover forged headers from several peers and a real loopback socket. Compose publishes only loopback plus the explicit WireGuard address in the domain overlay; DB has no host port. The runbook gates the actual VPS/WireGuard source identity and firewall persistence on operator checks. +- The deployment runbook includes startup, migration, backup/restore, update/rollback and two-host domain procedures. It requires the correct existing proxy and firewall manager and avoids changing the tunnel or unrelated services. + +## Commands run by this review + +- `git status --short --branch` — branch `codex/stage1-completion`; pre-existing `M AGENTS.md` preserved. +- `git rev-parse HEAD` — `17faecd962c2cf40e0a3f6ec4434811cfffdd312`. +- `USE_VK_MOCK=true npx vitest run tests/vk-r3-safety.test.ts tests/production-transport.test.ts tests/deployment-profile.test.ts` — exit 0; 3 files, 26 tests passed. +- `git diff --check 6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60..17faecd962c2cf40e0a3f6ec4434811cfffdd312` — exit 0, no whitespace errors. + +Not run by this reviewer: `npm ci`, Prisma generation, full unit/integration suites, lint, build, dependency audit, Docker stack rebuild, live VK smoke, live VPS/domain/network acceptance. Coordinator owns the full gates and Docker work. + +## UNVERIFIED and remaining blockers + +- Actual live VK response schemas and official behavior for manager listing, private-profile reposts, pagination consistency and method limits remain unverified. This review makes no claim about external VK contracts. +- Whether the deployed Docker/VPS path preserves the trusted socket peer as `10.77.0.1`, and whether firewall rules persist correctly, still require operator verification on the target hosts. +- Live VK consent, domain login, TLS/basic-auth behavior, and external/LAN/WireGuard isolation remain unverified. +- R3 malformed participant item validation is the release-blocking code issue identified here; regression coverage and repair remain outstanding. + +## Files changed by this review session + +- `agents/codex/inbox/2026-10-19-auditor2.md` — assignment record. +- `agents/codex/done/2026-10-19-auditor2.md` — this report. +- `/srv/projects/AI-Memory/01_PROJECTS/randomayzer/worklog/2026-10-19_auditor2_stage1-review.md` — individual memory worklog. + +No production code or tests were changed; no commit was created. The pre-existing `AGENTS.md` modification was left untouched. diff --git a/agents/codex/done/2026-10-19-claude-handoff-task.md b/agents/codex/done/2026-10-19-claude-handoff-task.md new file mode 100644 index 0000000..6c7d198 --- /dev/null +++ b/agents/codex/done/2026-10-19-claude-handoff-task.md @@ -0,0 +1,20 @@ +# Result — задание Claude +Base SHA: 930e6ee5e73c9269088b0a9eff576a58ec6ea158. +Production code не менялся; result — документационный коммит этого отчёта, определяется git log -1 -- agents/claude/inbox/2026-10-19-stage1-pr4-review-and-deploy.md. + +Изменённые файлы: +- agents/codex/inbox/2026-10-19-claude-handoff-task.md +- agents/claude/inbox/2026-10-19-stage1-pr4-review-and-deploy.md +- agents/codex/done/2026-10-19-claude-handoff-task.md + +Подготовлено исполнимое задание на независимый review PR4, проверку gate и clean PostgreSQL, deployment дома/VPS по runbook, backup/restore/timer, Caddy/HAProxy no-restart reload, live VK A–M, TLS/401/cookies/forwarded-peer/network isolation, existing-site baseline, rollback и итоговый evidence report. Известные LOW и внешние blockers явно сохранены. Другие чаты/люди не уведомлялись; это документ для передачи владельцем. + +Фактическая проверка: +- Forgejo get_pull_request_by_index(4): open, merged=false, mergeable=true; base52fb7587d7dda5ae3d1c5e9ecef96b7f48d147a3 / head930e6ee5e73c9269088b0a9eff576a58ec6ea158. +- Сверка runbook, VK_REAL_SMOKE_TEST, итогового Codex evidence и independent review conclusions. +- Python structure/reference assertions: exit0. +- git diff --check: exit0. +- npm/Prisma/Docker tests не запускались: изменены только задания/отчёт, runtime files не менялись. + +UNVERIFIED: actual VPS/DNS/WG/firewall/TLS, реальные VK credentials/consent/scopes/refresh и deployment остаются задачей Claude/оператора. Развёртывание этим действием не выполнялось; Phase CLOSED не объявлена. +Blockers подготовки документа: нет. Оставшиеся operational blockers/debt перечислены в задании и исходном отчёте. diff --git a/agents/codex/done/2026-10-19-r5-reload-fix.md b/agents/codex/done/2026-10-19-r5-reload-fix.md new file mode 100644 index 0000000..bc3847c --- /dev/null +++ b/agents/codex/done/2026-10-19-r5-reload-fix.md @@ -0,0 +1,19 @@ +# R5 reload and activation repair +Base SHA: ca28be6ae629d3d67936475ea5896b6f70f6e1e4. +Result SHA: accompanying commit (resolved via git log -- agents/codex/done/2026-10-19-r5-reload-fix.md). + +Changed files: deploy/randomayzer.haproxy.cfg; docs/STAGE1_DEPLOYMENT_RUNBOOK.md; this report and associated inbox/review artifacts. + +- Conditional executable HAProxy USR2 reload requires PID1 haproxy with -W/-Ws, complete validated config and worker handoff; unchanged container ID/start/restart count checked. Unknown topology stops. +- HTTP-01-only preparation precedes Caddy Basic Auth activation; TLS SNI route follows it. +- Whole-tree backups require nonempty copies and diff equality; rollback preserves unrelated edits. + +Actual verification: +- bash agents/codex/notes/validate-r5-templates.sh: exit 0, Caddy 2.8.4 and HAProxy 3.0.30 configuration valid in isolated fake fixtures. +- bash -n on all 25 runbook bash blocks: exit 0. +- Exact runbook reload function, official HAProxy 3.0 fixture: exit 0, new worker observed and container identity/start/restart count unchanged. +- Same function against non-master PID1 sleep: rejected without changing container identity/start/restart count or running state. +- Temporary fixtures removed after checks; no VPS mutation. + +UNVERIFIED: actual VPS process model, config mounts/admin API, active backend names, DNS/TLS, WireGuard peer, existing-site baselines and live acceptance. These remain operator gates, not completed deployment. +Blockers: independent rereview pending at report creation. Non-blocking debt: Caddy formatting and explicit forwarded-header warnings in local validation. diff --git a/agents/codex/done/2026-10-19-r5-template-coder2-review.md b/agents/codex/done/2026-10-19-r5-template-coder2-review.md new file mode 100644 index 0000000..459dbf6 --- /dev/null +++ b/agents/codex/done/2026-10-19-r5-template-coder2-review.md @@ -0,0 +1,48 @@ +# Result — coder2 review of amended R5 proxy deployment + +Base SHA: `8284eb5d006943d42133eb665c99dcbc3133d6a6`. +Reviewed/result SHA: `8284eb5d006943d42133eb665c99dcbc3133d6a6` (no commit; shared branch remains uncommitted). + +## Scope + +Reviewed the amended R5 assignment, Caddy 2.8 site fragment, HAProxy 3.0 additive snippet, operator runbook, and `agents/codex/notes/validate-r5-templates.sh`. I did not execute remote VPS/home-server operations, start/stop containers, modify app source/tests, or alter deployment templates. + +The runbook now captures before/after responses for every listed current hostname on both HTTP and HTTPS, explicitly requires a concrete local listener address, and stops if a baseline request cannot connect. It requires a root shell for the VPS backup/Docker steps, compares active-container copies with host sources before edits, records and checks single-file mount device/inode, compares the active files with host source after edits, and uses HAProxy `-c -V` with an accurate success expectation. Existing instructions keep HTTP and TLS backend routes distinct, preserve the HTTP-01 token path, require SNI routing before catch-all, gate reload on actual process/config mechanism, and restore only the two changed configurations before rechecking all sites. + +## Files changed by this review + +- `agents/codex/inbox/TASK-2026-10-19-coder2-r5-review.md` +- `docs/STAGE1_DEPLOYMENT_RUNBOOK.md` +- `agents/codex/done/2026-10-19-r5-template-coder2-review.md` + +The Caddy and HAProxy templates were reviewed but not edited. + +## Verification + +Commands I ran: + +- `python3` extraction of all fenced `bash` blocks from `docs/STAGE1_DEPLOYMENT_RUNBOOK.md`, piping each block to `bash -n` — exit 0; 21 blocks parsed. +- `git diff --check -- docs/STAGE1_DEPLOYMENT_RUNBOOK.md deploy/randomayzer.Caddyfile deploy/randomayzer.haproxy.cfg deploy/randomayzer.nginx.conf` — exit 0. +- `git rev-parse HEAD` — `8284eb5d006943d42133eb665c99dcbc3133d6a6`. + +Coordinator-provided validation evidence (not rerun by me because this assignment forbids container changes): `agents/codex/notes/r5-template-validation.log` records Caddy `Valid configuration`, HAProxy `Configuration file is valid`, and the local syntax validator PASS for Caddy 2.8 / HAProxy 3.0 fake fixtures. The Caddy warnings note formatting and an explicit `X-Forwarded-For` overwrite; the overwrite is intentional for this trust boundary. These fixture checks do not establish live VPS configuration compatibility. + +## Findings + +- CRITICAL: none. +- HIGH: none. +- MEDIUM: none. +- LOW: none open in my reviewed scope. + +This is a scoped runbook review, not the independent R5 security review or live operator acceptance. + +## UNVERIFIED + +- Whether the actual VPS has the expected container names, exact Caddy/HAProxy patch versions, mounts, includes, listener addresses, routing modes, Caddy admin endpoint, and HAProxy graceful reload mechanism. +- Whether all active hostnames and their expected baseline results are correctly enumerated by the operator before change. +- Whether `10.77.0.1` is the confirmed VPS peer, the DNS record points to the VPS, HTTP-01 reaches Caddy unchanged, and the home firewall rules persist safely. +- Certificate issuance, Basic Auth response, domain VK login, and other-site behavior after a real reload. + +## Remaining blockers / non-blocking tech debt + +R5 cannot be accepted until the operator verifies the unknown topology and reload mechanisms, executes the documented backups/validation, and records successful before/after checks for every existing hostname. The parent-run local fixture validator assembles routes before its fixture catch-all to syntax-check them; it does not test ordering in the actual HAProxy config, which must be confirmed during operator inspection and insertion. diff --git a/agents/codex/done/2026-10-19-r5-vps-container-runbook.md b/agents/codex/done/2026-10-19-r5-vps-container-runbook.md new file mode 100644 index 0000000..946b3f1 --- /dev/null +++ b/agents/codex/done/2026-10-19-r5-vps-container-runbook.md @@ -0,0 +1,33 @@ +# Result — R5 runbook for existing proxy containers + +Base SHA: `8284eb5d006943d42133eb665c99dcbc3133d6a6` +Result SHA: `8284eb5d006943d42133eb665c99dcbc3133d6a6` (working-tree edits only; no commit) + +## Files changed + +- `deploy/randomayzer.Caddyfile` — Caddy 2.8 site snippet, Basic Auth fallback, 1 MB request body limit, no access log, safe header overrides, no second proxy/listener. +- `deploy/randomayzer.haproxy.cfg` — additive SNI/HTTP routing examples with separate TLS and cleartext Caddy backends and explicit preservation of existing routes/default behavior. +- `deploy/randomayzer.nginx.conf` — removed obsolete nginx alternative, which does not match the confirmed Docker Caddy/HAProxy setup. +- `docs/STAGE1_DEPLOYMENT_RUNBOOK.md` — R5 operator procedure for actual container topology, redacted inspection, private backups, in-place source updates and inode/hash checks, pre/post validation, no-restart reload gates, other-site baselines, external checks, and rollback. Historical R4 loopback steps remain in place. +- `agents/codex/inbox/2026-10-19-r5-vps-container-runbook.md` +- `agents/codex/notes/2026-10-19-r5-vps-container-runbook.md` +- `agents/codex/done/2026-10-19-r5-vps-container-runbook.md` + +## Verification actually run + +- `docker run --rm --entrypoint caddy caddy:2.8-alpine version` — exit 0, local image reports `v2.8.4`. +- `docker run --rm --entrypoint haproxy haproxy:3.0-alpine -vv` — exit 0, local image reports `3.0.30-47d806b`. +- `docker run --rm -i --entrypoint caddy caddy:2.8-alpine validate --config /dev/stdin --adapter caddyfile` with the proposed site block and an artificial bcrypt hash — exit 0, `Valid configuration`; formatting and explicit X-Forwarded-For warnings only. +- `python3` temporary-fixture harness running `docker run --rm -v :/tmp/config:ro --entrypoint haproxy haproxy:3.0-alpine -c -f /tmp/config/haproxy.cfg` — exit 0. The fixture includes TCP SNI routing plus a distinct HTTP/cleartext backend; it is not the unknown active VPS config. +- `git diff --check -- docs/STAGE1_DEPLOYMENT_RUNBOOK.md deploy/randomayzer.Caddyfile deploy/randomayzer.nginx.conf` — exit 0. +- Python trailing-whitespace scan on the new HAProxy example and task inbox — no trailing whitespace. + +Parent separately reported target-image validation of these templates; evidence files are under `agents/codex/notes/r5-*`. + +No npm, Prisma or application test/lint/build gates were run for this documentation/config-template task. No remote machine was accessed or changed. + +Findings: no new CRITICAL/HIGH/MEDIUM/LOW finding from these local template checks. The runbook intentionally leaves live R5 blocked until actual inspection confirms the host source/config paths, active SNI/HTTP routing, safe HAProxy reload mechanism, Caddy admin endpoint, correct DNS and WG peer. No HAProxy signal/restart is guessed. + +UNVERIFIED: exact deployed patch versions; actual HAProxy topology/backend names/master-worker/control socket/reload procedure; active mounted config source paths and inode semantics; port-80 path to Caddy; whether Authelia policy is usable for this vhost; DNS target and peer identity; live issuance/renewal, Basic Auth/VK callback, other-site health and network isolation. No production claim is made. + +Remaining blockers: operator/owner must perform the gated inspection and live acceptance. Non-blocking tech debt: none identified in this scoped task. diff --git a/agents/codex/done/2026-10-19-smoke-mock-fix.md b/agents/codex/done/2026-10-19-smoke-mock-fix.md new file mode 100644 index 0000000..6c9f0c1 --- /dev/null +++ b/agents/codex/done/2026-10-19-smoke-mock-fix.md @@ -0,0 +1,14 @@ +# Audit-loop implementation — smoke mock false positive +Base/Reviewed SHA: 17faecd962c2cf40e0a3f6ec4434811cfffdd312 +HEAD at report generation: 17faecd962c2cf40e0a3f6ec4434811cfffdd312; follow-up changes uncommitted pending shared fix commit. + +Independent auditor1 MEDIUM is addressed by production factory refusal of explicit VK mocks, Compose USE_VK_MOCK=false, and server-generated actual providerMode in preview/import summary. Smoke requires LIVE mode and a nonempty import for G/H; zero participants cannot prove subscription API execution. All prior existing tests remain untouched; new tests exercise mock-preview labeling, production refusal, live-instance labeling, actual Bash smoke against MOCK/LIVE/zero-count HTTP fixtures and secret-marker suppression. + +Changed files: src/providers/factory.ts; src/app/api/posts/preview/route.ts; src/app/api/giveaways/[id]/participants/route.ts; compose.production.yml; tools/vk-smoke.sh; docs/VK_REAL_SMOKE_TEST.md; docs/PRODUCTION_GUARDS.md; tests/vk-smoke-provider-mode.test.ts; this inbox/result. + +Commands actually run: +- npx vitest run tests/vk-smoke-provider-mode.test.ts: exit0, 6 tests passed (including actual shell subprocess regressions). +- bash -n tools/vk-smoke.sh: exit0. +- git diff --check: exit0. + +Full gate and independent rereview pending at fix SHA. No real VK credentials, no live VK evidence. Finding remains open until independent rereview. Other reviewer HIGH malformed-items fix is separate provider/test work by coder1, to be included in same fix commit. Pre-existing COMMUNITY admin metadata LOW remains nonblocking debt; server static capability guard rejects unsupported manager exclusion. diff --git a/agents/codex/done/2026-10-19-stage1-completion.md b/agents/codex/done/2026-10-19-stage1-completion.md new file mode 100644 index 0000000..c033b6f --- /dev/null +++ b/agents/codex/done/2026-10-19-stage1-completion.md @@ -0,0 +1,157 @@ +# Stage1 — результат локальной реализации и проверки + +Исходный base SHA: 6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60. +Обновление задания: 52fb7587d7dda5ae3d1c5e9ecef96b7f48d147a3; включено merge-коммитом 8284eb5d006943d42133eb665c99dcbc3133d6a6. +Result / проверяемый implementation SHA: c5ede46bd1c31b1e23434538fdb8a1cd925751a6. +Production source, tests, dependencies, Prisma, Docker/Compose и tools совпадают с independently reviewed cce70c0b8feb263cc9209332c8c50a44bc97428a (git diff --exit-code: 0). После него менялись manifest, R5 proxy templates, документация и review artifacts. +Ветка: codex/stage1-completion. Финальный artifacts commit определять через git log -1 -- agents/codex/done/2026-10-19-stage1-completion.md. Этот отчёт фиксирует точный SHA результата кода выше, не самоссылочный SHA. +Main не изменялся. Локальная пользовательская правка AGENTS.md сохранена и не включена в коммиты. + +## Реализация по актуальному заданию + +- R1: next и eslint-config-next 16.3.2 → 16.3.8 (MIT), sharp 0.35.3 → 0.35.5 (Apache-2.0), source-map-js 1.2.1 → 1.2.2 (BSD-3-Clause). Production audit: 0. +- R2: PostgreSQL 16 на 127.0.0.1:55439; добавлена историческая initial migration перед двумя существующими миграциями. Первоначальный P3018/42P01 воспроизведён. Повторный draw возвращает ConflictError. Владелец прямо разрешил 32 случайных байта seed / 64 hex и узкое обновление seed assertions; authorization записана в inbox. Deterministic proof format, snapshot hash и HMAC_SHA256_FY_V1 не менялись. +- R3: неполная пагинация, malformed participant records и неполные subscription data вызывают явную ошибку до сохранения; prior participants сохраняются. Unsupported manager/repost capabilities объяснены. Smoke A–H требует реальный LIVE provider и непустые импорты, MOCK в production запрещён; ручные шаги и I–M сохранены. +- R4: явный local-single-user профиль только loopback, строгий HTTPS/Secure для остальных origin; CSRF привязан к canonical origin. Production entrypoint очищает forwarded headers по реальному socket peer. App 127.0.0.1:3100, PG без host ports, node/postgres users и CPU/RAM limits; backup/timer/clean restore, update/rollback и SSH tunnel в runbook. +- R5 по обновлению владельца: additive Caddy 2.8 / HAProxy 3.0 templates для krona-reverse-proxy и krona-proxy-mux; inspect mounts/process topology, fail-stop backups, validations, условный USR2 reload с worker/container evidence, Caddy auth до TLS route, other-site baselines и rollback. Существующие VPS/Forgejo/fleet/authelia/WireGuard конфигурации не менялись. Authelia integration не заявлена. + +## Фактически выполненные проверки + +Production code gate выполнен на cce70c0, после чего runtime-файлы не менялись: + +| Команда | Результат | +|---|---| +| npm ci | exit 0; полный dev-inclusive tree сообщает 19 advisories | +| npx prisma generate | exit 0, Prisma Client 5.22.0 | +| npm test | exit 0, 63 файла / 380 тестов | +| npx tsc --noEmit | exit 0 | +| npm run lint | exit 0, 0 errors / 6 существующих img warnings | +| npm run build | exit 0, Next production build | +| npm audit --omit=dev | exit 0, found 0 vulnerabilities | +| docker compose up -d --wait | isolated PG16 healthy, loopback55439 | +| DATABASE_URL= npx prisma migrate deploy | exit 0 на чистой randomayzer_stage1_final, применены 3 миграции | +| DATABASE_URL= npm run test:integration | exit 0, 12/12, включая 10 concurrent locks → ровно 1 успех | +| npx prisma migrate diff --from-url --to-schema-datamodel prisma/schema.prisma --exit-code | exit 0, No difference detected | +| docker compose --env-file -f compose.production.yml build app | exit 0 | +| тот же compose up -d --wait postgres / app и run --rm --no-deps app npx prisma migrate deploy | healthy production stack / 3 migrations | +| RANDOMAYZER_ENV_FILE= tools/db-backup-daily.sh | exit 0, clean temporary restore: 3 migrations + 1 fake giveaway, обе PASS строки | +| production HTTP smoke | anonymous session и PKCE/canonical localhost callback при поддельных Host/X-Forwarded-* прошли; не живой VK | +| bash agents/codex/notes/validate-r5-templates.sh | exit 0, Caddy2.8.4 / HAProxy3.0.30 validate; fake fixtures, network none, без опубликованных портов | +| bash -n для всех 25 bash-блоков runbook | exit 0 | +| точная reload_haproxy_without_restart из runbook в isolated HAProxy3.0 fixture | exit 0: новый worker, Id/StartedAt/RestartCount неизменны | +| та же функция с PID1 sleep | отвергнута, контейнер остался running без изменения identity/start/restart count | +| git diff --check | exit 0 | + +Actual production image: sha256:8d6a8b2250cf813d078e24dfc79c743be43ec86533a3d24af87ac9d1ab4ed274. +Проверены runtime binding 127.0.0.1:3100 / DB ports {}, users node/postgres, app1GiB/1.5CPU, PG512MiB/1CPU. +Логи локально: agents/codex/notes/fix-final-*.log, final-schema-diff.log, r5-reload-fix-template-validation.log и r5-runbook-reload-function.log; ignored, не включают реальные credentials и не упакованы в PR. +Все временные app/PG/validator/reload контейнеры остановлены/удалены; удалены только два проверенно созданных в этой сессии throwaway DB volumes. Другие сервисы/volumes не трогали. + +Ранее один raw npm test упал по timeout существующего live-network security.test.ts. Неизменённый focused повтор 7/7 и финальный raw полный прогон 380/380 прошли. Тесты/таймауты не ослаблялись. Начальные 10/12 и 11/12 integration failures устранены кодом и разрешённым seed contract. Неудачные попытки fixture setup и review во время npm ci отражены в промежуточных отчётах; окончательные проверки выше выполнены после исправления среды. + +## Независимое review + +Запрошенный цикл: coder1 → coder2 → auditor1 + auditor2 → исправления → оба re-review → coordinator gates. +- HIGH malformed likes/comments и MEDIUM ложный live-smoke на MOCK: CLOSED обоими независимыми re-review на cce70c0. Новые regression fixtures: 25 malformed route cases сохраняют prior state; smoke subprocess rejects MOCK / zero count. +- R5 MEDIUM отсутствующий executable HAProxy reload и LOW activation order / backup integrity: исправлены на c5ede46bd1c31b1e23434538fdb8a1cd925751a6; CLOSED по двум независимым re-review: 2026-10-19-auditor1-r5-rereview-c5ede46.md и 2026-10-19-auditor2-r5-rereview-c5ede46.md. Проверка только runbook; реальные VPS preconditions остаются открыты. +- CRITICAL: не найдено независимыми аудиторами в рассмотренном scope. +- LOW pre-existing COMMUNITY adminDetection metadata: остаётся OPEN как non-blocking debt; текущие маршруты SERVICE/USER и static server guard не позволяют включить неподдерживаемую проверку. Независимая классификация auditor2 в 2026-10-19-auditor2-high-rereview.md. +- LOW conditional HTTP redirect: если нужен новый challenge-only маршрут80, обычные HTTP-запросы не попадут в Caddy automatic redirect. Coordinator классифицирует как non-blocking: исходная приёмка требует HTTPS/HTTP-01, обычный HTTP redirect не заявлен. Проверять приложение только по HTTPS; при желании general HTTP route — отдельное операторское изменение после активного Caddy auth, с validate/reload/baselines. Finding не объявляется CLOSED. +- 6 lint warnings и dev-only advisories остаются non-blocking debt, не скрыты production audit. + +## UNVERIFIED / оставшиеся внешние blockers + +- Живой VK: credentials/consent, localhost redirect acceptance, актуальные official API contracts/scopes/limits, реальный A–H и ручные I–M. Без реальных ключей эти результаты не заявляются. +- Offset pagination на меняющихся внешних данных не доказывает point-in-time snapshot. +- DNS локально отдавал 185.26.122.80 вместо целевого178.212.13.102. Владелец подтверждает identity peer10.77.0.1; локальные interface10.77.0.3/route наблюдались. +- Actual VPS mount paths, active backends/process flags/Caddy admin API, установленный patch version, TLS certificate, proxy401, VK domain login, forwarded socket peer preservation и firewall persistence/isolation интернет/LAN/tunnel — проверяет оператор по runbook с отчётом agents/claude/done/. +- Нет live VPS изменений, выпуска сертификата или изменения DNS/firewall/tunnel здесь. Deployment templates не считаются принятым deployment. + +Phase CLOSED / release-ready / production-ready не объявляются. Локальная реализация передаётся на PR review; внешние acceptance gates остаются открыты. + +## Изменённые файлы относительно обновлённого main + +Список ниже отражает implementation/review snapshot c5ede46bd1c31b1e23434538fdb8a1cd925751a6; данный итоговый отчёт и последующие review-only artifacts добавляются artifacts-коммитом. + +```text +.dockerignore +Dockerfile +agents/codex/done/2026-10-19-auditor1-r5-review-ca28be6.md +agents/codex/done/2026-10-19-auditor1-rereview-cce70c0.md +agents/codex/done/2026-10-19-auditor2-high-rereview.md +agents/codex/done/2026-10-19-auditor2-high-vk-items.md +agents/codex/done/2026-10-19-auditor2-r5-review.md +agents/codex/done/2026-10-19-auditor2.md +agents/codex/done/2026-10-19-r5-reload-fix.md +agents/codex/done/2026-10-19-r5-template-coder2-review.md +agents/codex/done/2026-10-19-r5-vps-container-runbook.md +agents/codex/done/2026-10-19-smoke-mock-fix.md +agents/codex/done/TASK-2026-10-19-auditor1-independent-review.md +agents/codex/done/TASK-2026-10-19-coder2-review.md +agents/codex/done/TASK-2026-10-19-r1-r3.md +agents/codex/inbox/2026-10-19-auditor1-independent-review.md +agents/codex/inbox/2026-10-19-auditor1-r5-rereview-ca28be6.md +agents/codex/inbox/2026-10-19-auditor1-rereview-cce70c0.md +agents/codex/inbox/2026-10-19-auditor2-high-rereview.md +agents/codex/inbox/2026-10-19-auditor2-high-vk-items.md +agents/codex/inbox/2026-10-19-auditor2-r5-review.md +agents/codex/inbox/2026-10-19-auditor2.md +agents/codex/inbox/2026-10-19-r5-reload-fix.md +agents/codex/inbox/2026-10-19-r5-vps-container-runbook.md +agents/codex/inbox/2026-10-19-seed-owner-authorization.md +agents/codex/inbox/2026-10-19-smoke-mock-fix.md +agents/codex/inbox/2026-10-19-stage1-r2-assignment.md +agents/codex/inbox/2026-10-19-stage1-r4-r5-assignment.md +agents/codex/inbox/TASK-2026-10-19-coder2-r5-review.md +agents/codex/inbox/TASK-2026-10-19-coder2-review.md +agents/codex/inbox/TASK-2026-10-19-r1-dependencies.md +agents/codex/inbox/TASK-2026-10-19-r3-vk-safety.md +agents/codex/notes/.gitignore +agents/codex/notes/2026-10-19-auditor2-high-vk-items.md +agents/codex/notes/2026-10-19-r5-vps-container-runbook.md +agents/codex/notes/2026-10-19-stage1-baseline.md +agents/codex/notes/TASK-2026-10-19-r1-r3-notes.md +agents/codex/notes/stage1-pr-body.md +agents/codex/notes/validate-r5-templates.sh +compose.domain.yml +compose.production.yml +deploy/randomayzer-backup.service +deploy/randomayzer-backup.timer +deploy/randomayzer.Caddyfile +deploy/randomayzer.haproxy.cfg +docker-compose.yml +docs/PRODUCTION_GUARDS.md +docs/STAGE1_DEPLOYMENT_RUNBOOK.md +docs/VK_REAL_SMOKE_TEST.md +package-lock.json +package.json +prisma/migrations/20260818000000_initial/migration.sql +prisma/migrations/migration_lock.toml +src/app/api/giveaways/[id]/participants/route.ts +src/app/api/posts/preview/route.ts +src/app/giveaways/new/page.tsx +src/core/errors/http-errors.ts +src/core/randomizer/hasher.ts +src/core/validation/giveaway-schemas.ts +src/integrations/vk/vk-client.ts +src/integrations/vk/vk-errors.ts +src/lib/auth/app-config.ts +src/lib/auth/csrf-guard.ts +src/lib/auth/session.ts +src/lib/repository/prisma-repository.ts +src/providers/factory.ts +src/providers/vk/vk-capabilities.ts +src/providers/vk/vk-provider.ts +tests/deployment-profile.test.ts +tests/production-transport.test.ts +tests/randomizer.test.ts +tests/seed-precommit-gate.test.ts +tests/vk-r3-safety.test.ts +tests/vk-smoke-provider-mode.test.ts +tools/db-backup-daily.sh +tools/db-backup.sh +tools/db-restore-check.sh +tools/production-transport.cjs +tools/serve-production.cjs +tools/vk-smoke.sh +``` diff --git a/agents/codex/done/TASK-2026-10-19-auditor1-independent-review.md b/agents/codex/done/TASK-2026-10-19-auditor1-independent-review.md new file mode 100644 index 0000000..88669da --- /dev/null +++ b/agents/codex/done/TASK-2026-10-19-auditor1-independent-review.md @@ -0,0 +1,62 @@ +# Result — auditor1 independent review + +Base SHA: `6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60` +Reviewed SHA: `17faecd962c2cf40e0a3f6ec4434811cfffdd312` +Result SHA: `17faecd962c2cf40e0a3f6ec4434811cfffdd312` (review only; no code change) +Working-tree context: pre-existing `AGENTS.md` modification was preserved and excluded from the reviewed commit range. + +Scope: R1–R5 acceptance review with focus on production auth profile and CSRF/Secure cookie strictness, socket-derived proxy trust, deployment networks/backups, R3 fail-closed/no-partial-save behavior, and smoke-report truthfulness. + +## Finding + +### MEDIUM — Live VK smoke can report mock-provider results as real VK checks + +Files/lines: [`tools/vk-smoke.sh`](/srv/projects/Agent%20projects/randomayzer/tools/vk-smoke.sh:105), [`src/providers/factory.ts`](/srv/projects/Agent%20projects/randomayzer/src/providers/factory.ts:8), [`src/providers/vk/vk-mock-provider.ts`](/srv/projects/Agent%20projects/randomayzer/src/providers/vk/vk-mock-provider.ts:113). + +`ProviderFactory.getVkProvider()` selects `VkMockProvider` whenever `USE_VK_MOCK=true`, before it checks for `VK_SERVICE_TOKEN`. The production Compose service inherits arbitrary values from its configured `env_file` and does not reject or override this flag. The smoke script never confirms which provider answered: D accepts a mock `resolvedAuthType: SERVICE` as `PUBLIC_SERVICE`; F/G/H can create a database record, import generated participants, and receive generated subscription statuses. E may be omitted (`VK_SMOKE_PRIVATE_POST_URL` is optional). The script exits successfully whenever there are no `FAIL` lines; `SKIP` does not make the run fail (lines 176–177). + +Reproducer: configure the production env file with `USE_VK_MOCK=true` alongside the normal credentials, complete the real browser consent for A–C, set a public post URL and `VK_SMOKE_CREATE_GIVEAWAY=YES`, and omit `VK_SMOKE_PRIVATE_POST_URL`. The application serves the mock provider despite a service token; A–C can pass from the actual auth flow, D and F/G/H can report `PASS` from generated data, E reports `SKIP`, and the script exits 0. This does not demonstrate public post retrieval, participant import, or subscription checks against VK. It makes the smoke result insufficient evidence for those live checks. + +## Other severity levels + +CRITICAL: none found in the reviewed scope. + +HIGH: none found in the reviewed scope. + +LOW: none found in the reviewed scope. + +## Review notes + +- Production auth configuration uses canonical `APP_BASE_URL`, permits HTTP only when `DEPLOYMENT_PROFILE=local-single-user` and the origin is loopback, rejects local-profile HTTPS/LAN/public origins, requires matching callback origin/path, and makes the session cookie Secure in strict production. CSRF uses configured origin in production, checks scheme as well as host, ignores request Host and forwarded Host, and requires Origin/Referer for production mutations. +- The supplied production entry point removes client proxy identity headers and derives trust from `req.socket.remoteAddress`; only `10.77.0.1` may supply a single valid forwarded IP and HTTPS scheme. It overwrites Host/forwarded host from the canonical origin. The Compose files map the app to loopback and (in the domain overlay) the WG address, leave PostgreSQL unpublished, and declare dedicated named resources and CPU/memory limits. Actual Docker/NAT socket peer behavior and host firewall rules remain operator-side acceptance checks. +- R3 pagination checks totals, malformed/oversized/short pages and page ceilings. Subscription enrichment rejects absent, duplicate, unexpected, or malformed statuses. The participant import route only calls the atomic store update after fetch and enrichment complete, and its focused test proves an incomplete subscription response preserves prior participants. Unsupported repost/admin rules fail with explanatory capability errors. The code uses ordinary VK methods and does not call VK `execute`. +- Backup commands use a 0600 env-file check and restrictive umask; the daily workflow validates archive readability, restores to a new temporary database with `--exit-on-error`, and applies retention only after the restore check succeeds. Actual backup/restore and daily timer execution are not evidenced by this review. +- The migration bootstrap was inspected for scope and the runbook guards existing databases against applying the initial CREATE schema over existing tables. Root owns the clean PostgreSQL migration/integration gate. + +## Commands actually run + +- `git status --short` — showed only pre-existing ` M AGENTS.md` before this review. +- `git rev-parse HEAD` and `git rev-parse ^{commit}` / `git rev-parse ^{commit}` — HEAD and reviewed SHA are `17faecd962c2cf40e0a3f6ec4434811cfffdd312`; base resolved to `6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60`. +- `git diff --stat` and `git diff --name-status ..` — inspected the full 51-file change set. +- `npx vitest run tests/deployment-profile.test.ts tests/production-transport.test.ts tests/vk-r3-safety.test.ts` — exit 0; 3 files and 26 tests passed. +- `bash -n tools/vk-smoke.sh tools/db-backup.sh tools/db-backup-daily.sh tools/db-restore-check.sh` — exit 0. +- `node --check tools/production-transport.cjs` — exit 0. +- `node --check tools/serve-production.cjs` — exit 0. +- `git diff --check ..` — exit 0. +- Full `npm ci`, Prisma generate, full tests, lint, build, npm audit, real PostgreSQL migration/integration and Docker stack/backup-recovery gates were not run by this auditor; root owns them. + +## UNVERIFIED + +- Real VK ID browser consent and the actual support of the loopback redirect URI. +- Live VK behavior for public/restricted posts, pagination, subscription responses and scopes; the smoke false-positive finding above must be addressed before D/F/G/H output can serve as live evidence. +- R5 DNS, VPS identity, existing proxy server/config, certificate issuance, Basic Auth 401, live OAuth over the domain, Docker-preserved socket peer address, host firewall persistence, and internet/LAN/WG isolation. +- Actual daily backup, restore drill, retention, service timer and production rollback. +- Full R1 audit and R2 clean real-PostgreSQL acceptance are outside this review's executed checks. + +## Remaining blockers and non-blocking tech debt + +- Blocker for treating R3 smoke D/F/G/H as live acceptance: prevent `USE_VK_MOCK=true` in production (or make the smoke prove that real-provider mode is active) and rerun against VK. Keep the result for this finding open until the false-positive path is removed and independently rechecked. +- R5 and R4 operator acceptance remain pending per the runbook; this review does not mark any phase closed. +- No additional non-blocking tech debt identified in the reviewed acceptance scope. + +Final verdict: one MEDIUM finding; do not treat live VK smoke checks as accepted evidence until fixed and rerun. No CRITICAL/HIGH finding was established by this review. diff --git a/agents/codex/done/TASK-2026-10-19-coder2-review.md b/agents/codex/done/TASK-2026-10-19-coder2-review.md new file mode 100644 index 0000000..7c3fce7 --- /dev/null +++ b/agents/codex/done/TASK-2026-10-19-coder2-review.md @@ -0,0 +1,61 @@ +# Result — coder2 review and scoped fixes + +Base SHA: `6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60`. +Reviewed/result SHA: `6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60` (no commit; shared working tree remains uncommitted). + +## Scope reviewed + +Reviewed the full stage-one working diff and added deployment/R3 files against the stage-one manifest and R1–R5 assignments. Focused on production ingress trust, auth profiles, Compose isolation, backup/restore and migration bootstrap, VK incomplete-data handling, the A–H smoke script, and runbook correctness. Preserved the existing local `AGENTS.md` modification and the owner-authorized four seed-length assertion changes. No production host, firewall, WireGuard, tunnel, or Docker state was changed by this review. + +## Fixes made + +- Restored the original manual smoke follow-ups I–M for snapshot, draw, public audit, refresh, and logout. The script continues to report only its A–H automation and never claims these follow-ups passed. +- Changed the local smoke example from `127.0.0.1` to canonical `localhost`, matching the production profile and callback origin. +- Documented the production entrypoint's socket-peer sanitization and the distinction from generic `TRUST_PROXY=true` behavior in `docs/PRODUCTION_GUARDS.md`. +- Made the smoke script stop with a `FAIL setup` result if `mktemp` cannot create its private directory, avoiding accidental writes to root paths. +- Returned the internally generated incomplete-data reason to the UI instead of replacing it with a generic message; added a route regression assertion and confirmed the prior participant list remains unchanged on failure. +- Changed backup-directory setup in the runbook to create it with the operator's ownership and mode `0700`. + +## Files changed by this review + +- `agents/codex/inbox/TASK-2026-10-19-coder2-review.md` +- `docs/PRODUCTION_GUARDS.md` +- `docs/VK_REAL_SMOKE_TEST.md` +- `docs/STAGE1_DEPLOYMENT_RUNBOOK.md` +- `tools/vk-smoke.sh` +- `src/core/errors/http-errors.ts` +- `tests/vk-r3-safety.test.ts` +- `agents/codex/done/TASK-2026-10-19-coder2-review.md` + +## Commands actually run + +- `USE_VK_MOCK=true npx vitest run tests/vk-r3-safety.test.ts tests/deployment-profile.test.ts tests/production-transport.test.ts` — exit 0; 3 files, 26 tests passed. +- `bash -n tools/vk-smoke.sh tools/db-backup.sh tools/db-backup-daily.sh tools/db-restore-check.sh` — exit 0. +- `git diff --check` — exit 0. +- `git rev-parse HEAD` — `6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60`. + +The full test/lint/build/audit gates and local Docker/restore acceptance were not rerun by this reviewer; coordinator owns the final shared gates and Docker smoke. No live VK account, production host, or VPS was used. + +## Findings + +- CRITICAL: none found in reviewed scope. +- HIGH: none found in reviewed scope. +- MEDIUM: none found in reviewed scope. +- LOW: fixed in the working diff — smoke temporary-directory creation lacked a failure guard; previous checklist I–M had been dropped from the updated runbook; local smoke example used a host different from the configured canonical origin; incomplete-data UI response did not expose its safe cause; backup directory creation did not establish the service account's ownership. + +These are coder2 review results, not an independent security sign-off. Auth, proxy trust, migrations, and the production Compose boundary still require the requested independent audits. + +## UNVERIFIED + +- Live VK ID redirect acceptance for localhost, consent, token refresh, and real API behavior/scopes/limits. +- Whether Docker ingress on the target host preserves the actual VPS socket peer `10.77.0.1`; the transport tests cover forged headers and an actual loopback socket, not the VPS path. +- VPS identity, existing web server, DNS, certificate, firewall persistence, domain login, and network isolation acceptance. +- The actual Caddy version/directive compatibility on the operator's VPS; deployment snippets must be validated against the installed server. + +## Remaining blockers + +Live VK credentials/consent and operator confirmation/actions for R5 remain external prerequisites. Coordinator-reported local Docker backup-restore success is not independently reproduced in this review. The phase remains open for the two independent auditors and coordinator's final gates. + +## Non-blocking tech debt / environment notes + +`MEMORY_POLICY.md` is at `/srv/projects/AI-Memory/00_SYSTEM/MEMORY_POLICY.md` alongside `AGENT_PROTOCOL.md`; it was read. Per coordinator direction, only an individual worklog is being added; shared project-state files are left untouched. diff --git a/agents/codex/done/TASK-2026-10-19-r1-r3.md b/agents/codex/done/TASK-2026-10-19-r1-r3.md new file mode 100644 index 0000000..e173a83 --- /dev/null +++ b/agents/codex/done/TASK-2026-10-19-r1-r3.md @@ -0,0 +1,50 @@ +# Result — R1 dependencies and R3 VK safety + +Base SHA: `6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60`. +Result SHA: `6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60` (HEAD unchanged; edits remain uncommitted in the shared stage1 branch per coordinator instruction). + +## Changes + +R1 updated the lock and package ranges to next `16.3.8`, sharp `0.35.5`, and source-map-js `1.2.2`; `eslint-config-next` was aligned to `16.3.8`. Before/after licenses: next MIT→MIT, sharp Apache-2.0→Apache-2.0, source-map-js BSD-3-Clause→BSD-3-Clause. + +R3 now rejects incomplete or inconsistent pagination and subscription checks with a safe API error. An import that receives incomplete data does not replace the prior saved participant list. Unsupported repost and manager exclusion rules carry explicit reasons in the UI and are rejected by server capability validation. The live smoke script covers the automatable A-H checks, marks browser consent as MANUAL, stores session/proxy credentials only in a mode-0600 file, limits origins to loopback HTTP or the approved domain, and suppresses response bodies/headers. + +Files changed by this assignment: + +- `package.json` +- `package-lock.json` +- `src/integrations/vk/vk-errors.ts` +- `src/integrations/vk/vk-client.ts` +- `src/providers/vk/vk-provider.ts` +- `src/providers/vk/vk-capabilities.ts` +- `src/core/errors/http-errors.ts` +- `src/core/validation/giveaway-schemas.ts` +- `src/app/giveaways/new/page.tsx` +- `tests/vk-r3-safety.test.ts` (new; existing tests untouched) +- `tools/vk-smoke.sh` (new) +- `docs/VK_REAL_SMOKE_TEST.md` +- `agents/codex/inbox/TASK-2026-10-19-r1-dependencies.md` +- `agents/codex/inbox/TASK-2026-10-19-r3-vk-safety.md` +- `agents/codex/notes/TASK-2026-10-19-r1-r3-notes.md` +- `agents/codex/done/TASK-2026-10-19-r1-r3.md` + +## Commands actually run + +- `npm ci` — exit 0; installed 449 packages. npm reported 19 vulnerabilities in the full dependency tree. Production-only audit below is clean. +- `npx prisma generate` — exit 0; generated Prisma Client v5.22.0. +- `USE_VK_MOCK=true npm test` — exit 0; 62 files, 372 tests passed. +- `npx tsc --noEmit` — exit 0. +- `npm run lint` — exit 0; 0 errors, 6 existing `no-img-element` warnings. +- `npm run build` — exit 0; Next.js 16.3.8 production build completed. It printed the existing in-memory limiter/idempotency deployment warnings. +- `npm audit --omit=dev` — exit 0; `found 0 vulnerabilities`. +- `bash -n tools/vk-smoke.sh` — exit 0. +- `shellcheck tools/vk-smoke.sh` — not run; shellcheck is unavailable. +- `./tools/vk-smoke.sh agents/codex/notes/vk-smoke-fake.env` against a temporary local HTTP fixture — exit 0; A and C-H PASS, B MANUAL. The fake env used a marker cookie and was removed. No real VK account was used. + +## Findings, UNVERIFIED, blockers + +- Production dependency audit has no remaining vulnerabilities. `npm ci` still reports vulnerabilities in the full dependency tree, which includes dev dependencies; production-only audit is the R1 criterion. +- No live VK smoke was run. Current official method behavior for manager enumeration, private-profile reposts, execute limits, and stable offset pagination remains `UNVERIFIED`; the app fails closed where data completeness is uncertain and currently does not use `execute`. +- The owner must provide authorized VK credentials and perform browser consent for a live smoke run. Domain/proxy smoke was not run against the live host. +- Independent coder2 review and the two requested auditors remain pending. This report does not declare the stage or phase closed. +- No review severity findings are claimed; this is an implementation report, not an independent review. diff --git a/agents/codex/inbox/2026-10-19-auditor1-independent-review.md b/agents/codex/inbox/2026-10-19-auditor1-independent-review.md new file mode 100644 index 0000000..4de7ae4 --- /dev/null +++ b/agents/codex/inbox/2026-10-19-auditor1-independent-review.md @@ -0,0 +1,8 @@ +# Task +Base SHA: 6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60 +Reviewed SHA: 17faecd962c2cf40e0a3f6ec4434811cfffdd312 +Scope: Independent read-only audit of R1–R5 acceptance in the specified immutable base..HEAD range. Focus on auth profile/CSRF/Secure strictness, socket-origin and trusted-proxy behavior, deploy network and backup safety, R3 fail-closed/no partial saves, and live smoke truthfulness. +Do not change: Production code/tests, containers, or anything outside this assignment/report and individual memory worklog. No commit/push/delegation. Preserve pre-existing AGENTS.md modification. +Definition of Done: Findings with severity, file/line, impact and reproducible scenario; reviewed SHA; commands and actual results; UNVERIFIED claims and blockers. +Required verification: Static source review and focused, non-conflicting checks only; root owns npm ci/generate/full gates/PostgreSQL/Docker. +Authorization evidence: agents/codex/inbox/2026-10-19-stage1-completion.md and agents/codex/inbox/2026-10-19-seed-owner-authorization.md. diff --git a/agents/codex/inbox/2026-10-19-auditor1-r5-rereview-c5ede46.md b/agents/codex/inbox/2026-10-19-auditor1-r5-rereview-c5ede46.md new file mode 100644 index 0000000..e6d9d1c --- /dev/null +++ b/agents/codex/inbox/2026-10-19-auditor1-r5-rereview-c5ede46.md @@ -0,0 +1,7 @@ +# Auditor1 assignment: R5 reload and backup re-review + +- Base SHA: `ca28be6ae629d3d67936475ea5896b6f70f6e1e4` +- Reviewed SHA: `c5ede46bd1c31b1e23434538fdb8a1cd925751a6` +- Scope: re-review the two findings in `agents/codex/done/2026-10-19-auditor1-r5-review-ca28be6.md`, including the HAProxy reload procedure, backup integrity checks, staged HTTP-01/Caddy-auth/TLS ordering, and any concrete remaining blocker in that diff. +- Read-only: do not edit production source/tests, run Docker/VPS/container commands, commit, or push. +- Report: write a unique auditor1 report under `agents/codex/done` with exact SHA, findings marked CLOSED/OPEN, evidence and commands, UNVERIFIED items, and blockers. diff --git a/agents/codex/inbox/2026-10-19-auditor1-r5-rereview-ca28be6.md b/agents/codex/inbox/2026-10-19-auditor1-r5-rereview-ca28be6.md new file mode 100644 index 0000000..7a6041f --- /dev/null +++ b/agents/codex/inbox/2026-10-19-auditor1-r5-rereview-ca28be6.md @@ -0,0 +1,7 @@ +# Task +Base SHA: 8284eb5 +Reviewed SHA: ca28be6ae629d3d67936475ea5896b6f70f6e1e4 +Scope: Independent review of the owner-updated R5 deployment delta and runbook for the existing Caddy 2.8 plus HAProxy 3.0 Docker topology. +Do not change: Production/source/test code, templates, containers, or host configuration. No commit/push/delegation; do not rerun the coordinator's named Docker validator. Preserve pre-existing AGENTS.md modification. +Definition of Done: Concrete findings with severity/path/line/reproducer; exact SHA; commands/results; UNVERIFIED topology/evidence and blockers. +Required verification: Read-only source/runbook review and safe syntax checks only. No VPS or container access. diff --git a/agents/codex/inbox/2026-10-19-auditor1-rereview-cce70c0.md b/agents/codex/inbox/2026-10-19-auditor1-rereview-cce70c0.md new file mode 100644 index 0000000..104cf21 --- /dev/null +++ b/agents/codex/inbox/2026-10-19-auditor1-rereview-cce70c0.md @@ -0,0 +1,7 @@ +# Task +Base SHA: 17faecd962c2cf40e0a3f6ec4434811cfffdd312 +Reviewed SHA: cce70c0b8feb263cc9209332c8c50a44bc97428a +Scope: Independent rereview of the fixes for the auditor1 smoke-provider finding and auditor2 malformed VK items finding, against the immutable fix commit. +Do not change: Production code/tests or prior review reports; no commit/push, no containers, no delegation. Preserve pre-existing AGENTS.md modification. +Definition of Done: Independently trace the fixes and regression tests; classify prior findings CLOSED or OPEN only with source/test evidence; record any new findings, blockers, UNVERIFIED claims, exact SHA and commands. +Required verification: Focused tests only; root owns final full gates. diff --git a/agents/codex/inbox/2026-10-19-auditor2-high-rereview.md b/agents/codex/inbox/2026-10-19-auditor2-high-rereview.md new file mode 100644 index 0000000..7cc437b --- /dev/null +++ b/agents/codex/inbox/2026-10-19-auditor2-high-rereview.md @@ -0,0 +1,3 @@ +# Auditor2 rereview — malformed VK participant data fix + +Review immutable commit `cce70c0b8feb263cc9209332c8c50a44bc97428a` against prior reviewed commit `17faecd962c2cf40e0a3f6ec4434811cfffdd312`. Reassess auditor2 HIGH finding about malformed likes/comments records and partial participant persistence; inspect/reproduce focused regression tests. Independently assess whether COMMUNITY admin capability metadata discrepancy is pre-existing and nonblocking under current routes/server guards. Read-only: no production/test edits, commit/push, or container mutation. Do not communicate conclusions with auditor1 before report freeze. Write separate report under agents/codex/done and individual memory worklog; exact reviewed SHA, evidence, commands, severity and CLOSED/OPEN status. Do not declare phase closed; root owns final gates and Docker verification. diff --git a/agents/codex/inbox/2026-10-19-auditor2-high-vk-items.md b/agents/codex/inbox/2026-10-19-auditor2-high-vk-items.md new file mode 100644 index 0000000..f2b146f --- /dev/null +++ b/agents/codex/inbox/2026-10-19-auditor2-high-vk-items.md @@ -0,0 +1,5 @@ +# Assignment — auditor2 HIGH: malformed VK likes/comments + +Base SHA: `17faecd962c2cf40e0a3f6ec4434811cfffdd312` + +Fix only `src/providers/vk/vk-provider.ts` and `tests/vk-r3-safety.test.ts` for the auditor2 HIGH finding in `agents/codex/done/2026-10-19-auditor2.md`. Reject malformed likes/comment records, malformed optional comment profile data, and duplicate likes IDs with `VkIncompleteDataError` before participant persistence. Valid positive safe-integer user IDs are accepted. Valid negative integer comment authors may be excluded under an explicit documented community-author policy. Add route-level regression coverage showing old stored participants survive malformed likes/comments responses. No existing tests edits, no commit, no auth/provider capability changes, and no edits to unrelated files. diff --git a/agents/codex/inbox/2026-10-19-auditor2-r5-rereview-c5ede46.md b/agents/codex/inbox/2026-10-19-auditor2-r5-rereview-c5ede46.md new file mode 100644 index 0000000..87b3c3d --- /dev/null +++ b/agents/codex/inbox/2026-10-19-auditor2-r5-rereview-c5ede46.md @@ -0,0 +1,6 @@ +# Task +Base SHA: `ca28be6ae629d3d67936475ea5896b6f70f6e1e4` +Scope: Independently rereview the R5 follow-up at immutable `c5ede46bd1c31b1e23434538fdb8a1cd925751a6`, focusing on whether the HAProxy reload is executable and safely gated to a verified process model, and whether activation ordering closes the prior LOW window. +Do not change: production source/configs, tests, containers, remote VPS, commits, or pushes. Read only; write only this task record and a unique done report. +Definition of Done: Report exact reviewed SHA, findings and CLOSED/OPEN assessment for prior MEDIUM and LOW with file/line evidence, actual commands run, UNVERIFIED blockers, and remaining debt. Do not claim phase closure. +Required verification: Inspect immutable diff and current report scope. Do not rerun full gates or perform live operations. diff --git a/agents/codex/inbox/2026-10-19-auditor2-r5-review.md b/agents/codex/inbox/2026-10-19-auditor2-r5-review.md new file mode 100644 index 0000000..08c9405 --- /dev/null +++ b/agents/codex/inbox/2026-10-19-auditor2-r5-review.md @@ -0,0 +1,3 @@ +# Auditor2 independent R5 review + +Review immutable SHA `ca28be6ae629d3d67936475ea5896b6f70f6e1e4` against `8284eb5` as assigned by coordinator. Read updated owner manifest and scrutinize HAProxy/Caddy templates, removed nginx alternative, actual VPS runbook, and local reproducible validator for route preservation, backups/config-tree/inode provenance, TLS 443 vs HTTP 80 ACME routing, SNI priority, syntax/config validation, graceful reload gating, rollback, and all-site HTTP/HTTPS baselines. No production/source edits, commit/push, VPS access/restarts or container mutations. Do not assume actual topology; unknowns remain UNVERIFIED. Do not rerun full app gates or the Docker validator already run by coordinator. Write unique task/report and individual memory worklog, with exact SHA, severity/evidence, UNVERIFIED blockers, then freeze. diff --git a/agents/codex/inbox/2026-10-19-auditor2.md b/agents/codex/inbox/2026-10-19-auditor2.md new file mode 100644 index 0000000..2520964 --- /dev/null +++ b/agents/codex/inbox/2026-10-19-auditor2.md @@ -0,0 +1,3 @@ +# Auditor2 assignment — stage1 completion + +Independent read-only adversarial/code review of `6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60..17faecd962c2cf40e0a3f6ec4434811cfffdd312` against R1–R5 acceptance. Scope: incomplete/malformed VK data and partial persistence; unsupported managers/reposts/execute safeguards and participant/UI reasons; smoke false PASS cases and credentials; initial migration legacy safety; production socket trust versus spoofed headers; loopback/domain binding and executable operator runbook. Read AGENTS.md, stage1 assignment, seed owner authorization, agents/README, and required project memory. No production/test edits, commit/push, container mutation, or auditor1 communication. Report severity, file/line/evidence, SHA, commands, UNVERIFIED blockers/debt. Root owns full gates. diff --git a/agents/codex/inbox/2026-10-19-claude-handoff-task.md b/agents/codex/inbox/2026-10-19-claude-handoff-task.md new file mode 100644 index 0000000..bb89c65 --- /dev/null +++ b/agents/codex/inbox/2026-10-19-claude-handoff-task.md @@ -0,0 +1,7 @@ +# Task — задание Claude на review и развёртывание stage1 +Base SHA: 930e6ee5e73c9269088b0a9eff576a58ec6ea158. +Запрос владельца: подготовить задание Claude на проверку PR4, развёртывание по runbook и оставшиеся проверки живого VK/VPS. +Scope: сохранить полный исполнимый handoff в agents/claude/inbox/, включить критерии приёмки, prerequisites, границы доступа, evidence и rollback; опубликовать документ в текущей ветке PR4. +Do not change: production source/tests, инфраструктура/credentials, пользовательская правка AGENTS.md. +Definition of Done: документ доступен владельцу и Claude, содержит точные SHA/ссылки/команды и все открытые проверки. Развёртывание в этой сессии не выполняется. +Required verification: сверить задание с актуальным PR, runbook, VK smoke checklist и независимыми review; git diff --check. diff --git a/agents/codex/inbox/2026-10-19-r5-reload-fix.md b/agents/codex/inbox/2026-10-19-r5-reload-fix.md new file mode 100644 index 0000000..032099b --- /dev/null +++ b/agents/codex/inbox/2026-10-19-r5-reload-fix.md @@ -0,0 +1,5 @@ +# R5 independent-review repair +Base/reviewed SHA: ca28be6ae629d3d67936475ea5896b6f70f6e1e4. +Scope: executable conditional HAProxy master-worker reload with process/container evidence and rollback; fail-closed whole-tree backups; explicit HTTP-01/Caddy-auth/TLS-route staging. No actual VPS edits. Preserve existing unrelated routes and require actual runtime/mount preconditions. +Evidence already observed in isolated official HAProxy3.0 fixture: PID1 haproxy -W -db -f..., USR2 changed worker generation while container ID/start/restart count remained unchanged. Actual VPS process model remains UNVERIFIED. +DoD: syntax and fixture checks, independent rereview, explicit unknown-state stop branches. diff --git a/agents/codex/inbox/2026-10-19-r5-vps-container-runbook.md b/agents/codex/inbox/2026-10-19-r5-vps-container-runbook.md new file mode 100644 index 0000000..4b96b73 --- /dev/null +++ b/agents/codex/inbox/2026-10-19-r5-vps-container-runbook.md @@ -0,0 +1,7 @@ +# Assignment — R5 runbook for the actual VPS proxy containers + +Base SHA: `8284eb5d006943d42133eb665c99dcbc3133d6a6` + +Scope: only deploy templates and `docs/STAGE1_DEPLOYMENT_RUNBOOK.md`. Update the R5 procedure for the reported target containers `krona-proxy-mux` (`haproxy:3.0-alpine`) and `krona-reverse-proxy` (`caddy:2.8-alpine`). Add safe in-place additive SNI and site examples, preserve unrelated sites/default routes and the HTTP-01 path, include private backups, version-appropriate validation, non-restart reload only after inspection confirms the actual mechanism, before/after other-site baselines, and a concrete rollback. Use Basic Auth as the supported fallback; Authelia remains optional only if the existing policy is verified. Unknown mount/config paths, routing mode, reload mechanism, DNS/peer identity and live acceptance remain `UNVERIFIED` and must stop the operator until observed. No real remote changes. + +Do not modify application source/tests, existing review reports, compose/auth/deploy app settings, unrelated services or existing user changes. No commit/delegation. Record result and evidence in this agent's `agents/codex/{notes,done}`. diff --git a/agents/codex/inbox/2026-10-19-seed-owner-authorization.md b/agents/codex/inbox/2026-10-19-seed-owner-authorization.md new file mode 100644 index 0000000..cb2f086 --- /dev/null +++ b/agents/codex/inbox/2026-10-19-seed-owner-authorization.md @@ -0,0 +1,10 @@ +# Scope amendment — explicit owner authorization +Base SHA: 6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60 +Date: 2026-10-19 +Owner answer in chat: «Разрешаю увеличить seed до 32 байт». +Scope: generateCryptoSecureSeed uses 32 random bytes (64 hex characters), preserving deterministic proof format, snapshot hash and HMAC_SHA256_FY_V1 algorithms. No existing tests may be changed. +Reason: existing PostgreSQL integration seed contract expects 64 hex characters, actual generator used 16 bytes (32 hex). +Required verification: existing unit/integration suites, tsc/lint/build, independent review. + +Second explicit owner answer: «Разрешаю обновить assertions seed на 64 hex». +Narrow exception to the no-existing-test-edits rule: update seed-length assertions only in tests/randomizer.test.ts and tests/seed-precommit-gate.test.ts (four assertions across the two files; the clarification's count of three omitted the additional precommit assertion). No weakening or unrelated test changes. diff --git a/agents/codex/inbox/2026-10-19-smoke-mock-fix.md b/agents/codex/inbox/2026-10-19-smoke-mock-fix.md new file mode 100644 index 0000000..63395a0 --- /dev/null +++ b/agents/codex/inbox/2026-10-19-smoke-mock-fix.md @@ -0,0 +1,5 @@ +# Audit-loop fix — prevent mock data as live VK evidence +Base/reviewed SHA: 17faecd962c2cf40e0a3f6ec4434811cfffdd312 +Original task authorizes coder/audit repair loop. Independent auditor1 MEDIUM: explicit USE_VK_MOCK=true can make live smoke D/F/G/H report generated data as real VK. +Scope: forbid mock-provider selection in production; expose nonsecret provider mode in preview/import response; require LIVE mode in smoke and nonempty import before reporting subscription check; add exact reproducer regression in new test file. Preserve all existing tests except previously authorized seed assertions. No credentials/production host changes. +Definition of Done: mock PoC fails safely; live-mode fixture passes automated checks, B remains MANUAL; full gates and independent rereview. diff --git a/agents/codex/inbox/2026-10-19-stage1-r2-assignment.md b/agents/codex/inbox/2026-10-19-stage1-r2-assignment.md new file mode 100644 index 0000000..f4d6241 --- /dev/null +++ b/agents/codex/inbox/2026-10-19-stage1-r2-assignment.md @@ -0,0 +1,6 @@ +# Task — coordinator R2 +Base SHA: 6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60 +Scope: isolated PostgreSQL 16 compose, clean migration path, existing integration suite and fixes needed to pass. +Do not change: existing tests, Randomizer/AuditProof/hash/algorithm, local AGENTS.md. +Definition of Done: migrate deploy on empty database; integration suite 0 failures; report evidence and remaining blockers. +Required verification: compose up --wait; prisma migrate deploy; npm run test:integration; final shared gates. diff --git a/agents/codex/inbox/2026-10-19-stage1-r4-r5-assignment.md b/agents/codex/inbox/2026-10-19-stage1-r4-r5-assignment.md new file mode 100644 index 0000000..421a4df --- /dev/null +++ b/agents/codex/inbox/2026-10-19-stage1-r4-r5-assignment.md @@ -0,0 +1,6 @@ +# Task — coordinator R4/R5 preparation +Base SHA: 6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60 +Scope: explicit loopback production profile, strict domain profile, actual socket peer proxy boundary, isolated deploy stack, backup/restore scripts, two-host executable runbook. Actual firewall/WireGuard/VPS changes remain operator work per original manifest. +Do not change: protected draw/proof/hash algorithm, existing tests, pre-existing AGENTS.md. +Definition of Done: local production smoke and clean backup restore; adversarial origin/proxy tests; clear external blockers. +Required verification: all original gates, new profile/transport tests, stack startup and restore drill. diff --git a/agents/codex/inbox/TASK-2026-10-19-coder2-r5-review.md b/agents/codex/inbox/TASK-2026-10-19-coder2-r5-review.md new file mode 100644 index 0000000..f326e54 --- /dev/null +++ b/agents/codex/inbox/TASK-2026-10-19-coder2-r5-review.md @@ -0,0 +1,6 @@ +# Task +Base SHA: 8284eb5d006943d42133eb665c99dcbc3133d6a6 +Scope: Review the amended R5 Caddy 2.8 / HAProxy 3.0 templates, operator runbook, and reproducible local syntax validator. Fix concrete defects only in the R5 runbook/templates and write an individual review report. +Do not change: application source/tests, compose/auth settings, prior review reports, AGENTS.md, existing unrelated services, remote VPS/home-server state, containers, commits, or delegation. +Definition of Done: Templates/runbook provide safe backups, source and active mount checks, correct separate TLS 443 and HTTP 80 routes, HTTP-01 preservation, safe no-restart validation/reload gates, before/after checks for every existing website, and rollback; unknown facts remain operator blockers. +Required verification: review validator evidence, run read-only syntax/diff checks, record exact base/result SHA, changes, findings, UNVERIFIED items, and blockers. diff --git a/agents/codex/inbox/TASK-2026-10-19-coder2-review.md b/agents/codex/inbox/TASK-2026-10-19-coder2-review.md new file mode 100644 index 0000000..9ee1fb0 --- /dev/null +++ b/agents/codex/inbox/TASK-2026-10-19-coder2-review.md @@ -0,0 +1,6 @@ +# Task +Base SHA: 6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60 +Scope: Review the complete shared working diff and added files for the stage-one task, focusing on production ingress trust, strict/local auth profiles, isolated compose, backup/restore/runbook correctness, R3 failure behavior and no-partial-state, truthful A-H smoke checks, and safe migration bootstrap. Fix concrete defects and missing tests/docs within R1-R5 authorized scope. +Do not change: Randomizer, AuditProof, deterministic proof format, snapshot hash algorithm, HMAC_SHA256_FY_V1; existing tests except the four specifically authorized seed-length assertions; preserve the pre-existing AGENTS.md user modification; no production host/firewall/WireGuard changes; no commit or push. +Definition of Done: Concrete scoped defects fixed, meaningful checks run, report includes exact base and current HEAD, changed files, commands/results, findings, UNVERIFIED claims, blockers, and non-blocking tech debt. +Required verification: Inspect whole diff/new files; relevant R3/R4/R5 adversarial tests and scripts; record exact commands and outcomes. Coordinator owns local Docker smoke and shared gates. diff --git a/agents/codex/inbox/TASK-2026-10-19-r1-dependencies.md b/agents/codex/inbox/TASK-2026-10-19-r1-dependencies.md new file mode 100644 index 0000000..700753a --- /dev/null +++ b/agents/codex/inbox/TASK-2026-10-19-r1-dependencies.md @@ -0,0 +1,6 @@ +# Task +Base SHA: 6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60 +Scope: Complete stage-one R1: minimally update vulnerable next, sharp, source-map-js within same major; record license before/after; achieve npm audit --omit=dev with no critical/high; run npm ci, Prisma generate, npm test, TypeScript, lint, build. Continue R3 only after R1, as directed by coordinator. +Do not change: existing tests; Randomizer; AuditProof; deterministic proof/snapshot hash/HMAC algorithm; user changes in AGENTS.md; R2/R4/R5 assigned to coordinator; no commit/push. +Definition of Done: exact vulnerable dependency versions replaced with same-major fixed versions, documented licenses, executed gates recorded; then implement assigned R3 while preserving above boundaries. +Required verification: npm ci; npx prisma generate; npm test; tsc; lint; build; npm audit --omit=dev. R3 tests and smoke checks per task manifest. diff --git a/agents/codex/inbox/TASK-2026-10-19-r3-vk-safety.md b/agents/codex/inbox/TASK-2026-10-19-r3-vk-safety.md new file mode 100644 index 0000000..dc0b302 --- /dev/null +++ b/agents/codex/inbox/TASK-2026-10-19-r3-vk-safety.md @@ -0,0 +1,6 @@ +# Task +Base SHA: 6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60 +Scope: R3 from stage-one manifest, per coordinator follow-up: safe handling for VK unknown/unsupported manager listing, private-profile reposts, pagination limits, execute limits; preserve participants and communicate capability/failure status in UI and participant state; add mock-provider regression tests; add `tools/vk-smoke.(ts|sh)` covering automatable A-H checks with truthful PASS/FAIL and manual browser-consent steps. +Do not change: existing tests; Randomizer, AuditProof, deterministic proof format, snapshot hashing, HMAC algorithm; package files; auth/profile files; R2/R4/R5 and coordinator-owned files; no real credentials; no commit/push. +Definition of Done: failed or unknown VK behavior cannot silently produce a complete participant list or exclude participants without an explicit status; tests cover each requested failure; smoke tool does not print secrets or claim unperformed checks pass; document UNVERIFIED VK contract claims. +Required verification: add/run new R3 regression tests; tsc, relevant test/lint/build gates as practical; all commands and outcomes in report. diff --git a/agents/codex/notes/.gitignore b/agents/codex/notes/.gitignore new file mode 100644 index 0000000..9cbd828 --- /dev/null +++ b/agents/codex/notes/.gitignore @@ -0,0 +1,10 @@ +# Local verification outputs: retain on disk, never package credentials or dumps. +*.env.local +*.dump +*.partial +*.log +stage1-baseline-schema.prisma + +backups/ + +r5-fixtures/ diff --git a/agents/codex/notes/2026-10-19-auditor2-high-vk-items.md b/agents/codex/notes/2026-10-19-auditor2-high-vk-items.md new file mode 100644 index 0000000..e14193d --- /dev/null +++ b/agents/codex/notes/2026-10-19-auditor2-high-vk-items.md @@ -0,0 +1,14 @@ +# Notes — auditor2 HIGH fix (VK participant records) + +Base: `17faecd962c2cf40e0a3f6ec4434811cfffdd312`. No commit created; HEAD remains the base SHA. + +Implementation validates VK response object/count/items before pagination accepts a page. Likes must be profile objects with positive safe-integer IDs and valid optional profile field types; duplicate likes IDs across pages fail. Comment authors must be safe integers and nonzero. Negative safe integers are explicitly treated as community authors and excluded; malformed/zero authors fail. Optional comment profiles, when provided, must be an array of valid positive-ID profiles with no duplicate IDs. All validation failures throw `VkIncompleteDataError` before the provider returns to the import route. + +The route regression table covers 25 malformed response variants (likes and comments, including missing/null items/arrays/counts, invalid IDs, duplicate likes/profile IDs, and invalid profile properties). Every case asserts HTTP `VK_INCOMPLETE_DATA` and that the previously stored participant list remains byte-for-byte equal at the repository result layer. A separate case confirms an otherwise valid negative community author remains intentionally excluded. + +Verification run: +- `USE_VK_MOCK=true npx vitest run tests/vk-r3-safety.test.ts` — exit 0, 1 file / 13 tests passed; the route table exercises 25 response cases. +- `npx tsc --noEmit` — exit 0. +- `git diff --check -- src/providers/vk/vk-provider.ts tests/vk-r3-safety.test.ts` — exit 0. + +No `npm ci`, prisma generation, full test, lint or build was run for this narrow follow-up. Those gates need to be repeated by the coordinator after the complete shared patch is stable. diff --git a/agents/codex/notes/2026-10-19-r5-vps-container-runbook.md b/agents/codex/notes/2026-10-19-r5-vps-container-runbook.md new file mode 100644 index 0000000..97af9d0 --- /dev/null +++ b/agents/codex/notes/2026-10-19-r5-vps-container-runbook.md @@ -0,0 +1,15 @@ +# Notes — R5 Docker proxy runbook + +Base SHA: `8284eb5d006943d42133eb665c99dcbc3133d6a6`. No commit made; current HEAD remains unchanged. + +Scope stayed limited to proxy templates and the deployment runbook. Updated R5 to the reported proxy containers (`krona-proxy-mux`, HAProxy 3.0; `krona-reverse-proxy`, Caddy 2.8), with inspections of command/mounts that omit environment, config backup, same-inode edits/checks, actual config validation, reload gates, host/passthrough and cleartext backends kept separate, other-site before/after baselines, Basic Auth only, and rollback. Exact VPS paths, HAProxy routing/process model/reload, Caddy admin endpoint, DNS and peer identity remain gated UNVERIFIED facts. + +Local image checks run: +- `docker run --rm --entrypoint caddy caddy:2.8-alpine version` — `v2.8.4`. +- `docker run --rm --entrypoint haproxy haproxy:3.0-alpine -vv` — `3.0.30-47d806b`. +- Caddy 2.8.4 `validate --config /dev/stdin --adapter caddyfile` against the proposed site block with an artificial hash — exit 0 / `Valid configuration`; two warnings were formatting and the explicit X-Forwarded-For override. +- HAProxy 3.0.30 `-c -f` against a temporary assembled mode-tcp SNI frontend plus separate mode-http/cleartext frontend fixture — exit 0. This fixture is not the VPS active config. +- `git diff --check -- docs/STAGE1_DEPLOYMENT_RUNBOOK.md deploy/randomayzer.Caddyfile deploy/randomayzer.nginx.conf` — exit 0. +- Python trailing-whitespace scan on the new HAProxy snippet and inbox assignment — no trailing whitespace. + +No remote VPS/home-server changes, npm/app gates, Docker production stack changes, or actual TLS/VK acceptance were performed. Parent independently reported that it validated the submitted Caddy snippet and assembled HAProxy snippets with the cached target images; see `agents/codex/notes/r5-*`. diff --git a/agents/codex/notes/2026-10-19-stage1-baseline.md b/agents/codex/notes/2026-10-19-stage1-baseline.md new file mode 100644 index 0000000..0e518e9 --- /dev/null +++ b/agents/codex/notes/2026-10-19-stage1-baseline.md @@ -0,0 +1,53 @@ +# Stage 1 baseline + +User supplied task: agents/codex/inbox/2026-10-19-stage1-completion.md (retrieved by git fetch forgejo main). +Initial local HEAD: b625d70e8eb5bfb3eb2b6461fe58b08f4b13b1ab. +Task base SHA: 6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60 (origin/main = forgejo/main). +Branch: codex/stage1-completion. Owner merges PR; agent does not merge. +Pre-existing local modification: AGENTS.md, preserve and exclude from commits. +Delegation requested by manifest: cheap coder 1, coder 2, two independent auditors, coordinator verification. +No real VK credentials used. No existing service, firewall, WireGuard, or external VPS configuration to be changed here. + +R2 evidence: +- docker compose up -d --wait: PostgreSQL 16 healthy at 127.0.0.1:55439. +- Initial prisma migrate deploy: P3018 / 42P01 missing Giveaway; log r2-initial-migrate.log. +- Add 20260818000000_initial generated from historical pre-ownership/pre-session schema; existing migrations unchanged. +- Recreated only disposable randomayzer_test database; migrate deploy: 3 migrations applied. +- Initial integration: 10 passed / 2 failed. Fix duplicate-draw error type at Prisma repository: 11 passed / 1 seed-contract failure. +- Owner authorized 32-byte generator and corresponding existing seed assertions; integration 12/12. +- prisma migrate diff --from-url disposable DB --to-schema-datamodel prisma/schema.prisma --exit-code: No difference detected, exit 0. + +R4 evidence before coder2: +- Docker build app completed, Next.js 16.3.8 production build inside image. +- compose.production.yml postgres/app up --wait: both healthy, fake credentials only in ignored 0600 local-stack.env.local. +- Production prisma migrate deploy applied all 3 migrations on empty randomayzer DB. +- Actual port: ss shows 127.0.0.1:3100 only; DB HostConfig.PortBindings={}; app USER=node, PG USER=postgres. +- Actual limits: app1073741824 bytes/1.5 CPUs, PG536870912 bytes/1CPU. +- HTTP /api/auth/me returned unauthenticated safe JSON. Fake VK start returned correct localhost callback and PKCE despite forged Host/X-Forwarded-* headers. +- db-backup-daily.sh restored custom dump to a clean temporary DB, showed 3 migrations / 0 giveaways, printed both PASS messages; repeat with one marker giveaway pending. +- No real VK authorization, actual VPS TLS/basic auth or WG firewall acceptance claimed. +- Repeated backup/restore with one fake giveaway row: clean restored DB reported 3 migration rows and 1 giveaway; both PASS messages. Removed only our marker giveaway/user after drill. + +Gate caveat: +- One raw npm test run timed out in existing tests/security.test.ts unmocked live-network preview case (368/369); a focused unchanged rerun passed7/7, network case725ms. +- USE_VK_MOCK=true full suite later passed372/372. No test timeout or assertions were weakened; final raw run pending. + +Independent review cycle 1 (immutable 17faecd962c2cf40e0a3f6ec4434811cfffdd312): +- auditor1 MEDIUM: smoke may accept generated mock VK results as live evidence; coordinator fixing production refusal + explicit actual-provider mode and subprocess regressions. +- auditor2 HIGH: malformed likes/comments accepted or silently omitted before complete save; coder1 assigned strict element validation + route preservation PoC regressions. +- auditor2 LOW: COMMUNITY effective adminDetection metadata remains true although no manager implementation. This predates our patch; actual static server validation disables manager exclusion, route currently uses SERVICE/USER. Classify as non-blocking pre-existing debt, do not change old tests/expand scope silently. +- Final gates at 17faecd: npm ci/generate/test372/372/tsc/lint(0errors6warnings)/build/audit0 all exit0. Fresh DB randomayzer_stage1_final migration3 applied, integration12/12, schema diff0. Follow-up fixes require new gates/rereview; prior passes do not close findings. + +2026-10-19 owner amendment fetched during final verification: +- Forgejo main advanced to52fb7587d7dda5ae3d1c5e9ecef96b7f48d147a3; GitHub main still6382cb8 at fetch. +- Only change: R5 task now confirms containerized Caddy2.8 krona-reverse-proxy and HAProxy3.0 krona-proxy-mux, requires backups/validation/reload without restart and other-site checks. +- Merged updated manifest into our branch, HEAD8284eb5d006943d42133eb665c99dcbc3133d6a6; production code unchanged from independently re-reviewed cce70c0. +- Coder1 preparing updated R5 templates/runbook; coder2 and independent review to follow. No VPS changes. +- Final cce70c0 production image sha256:8d6a8b2250cf813d078e24dfc79c743be43ec86533a3d24af87ac9d1ab4ed274 up healthy; actual loopback3100 binding, DBports{}, users/limits confirmed. Repeated HTTP PKCE/canonical callback and nonempty backup clean-restore passed. Deleted only our final fake giveaway/user marker after drill. + +Final verification cleanup: +- Final production and integration test stacks stopped with their respective compose down commands. +- Before teardown, production test DB counts were User0/Giveaway0/UserCredential0/Session0; volume creation timestamp and project label proved it was created in this session with FAKE credentials. +- Removed only those two owned throwaway test volumes (randomayzer-database, randomayzer-integration-pgdata) after checking their Compose project labels. This prevents a future real deployment inheriting the fake bootstrap database password. No global Docker prune. +- getent ahostsv4 random.ochenstarik.ru returned185.26.122.80 via local resolver; DNS-to178.212.13.102 remains an external blocker. Local krona-wg address10.77.0.3/24 and route to10.77.0.1 were observed; peer's VPS identity still requires owner confirmation. +- Reproducible R5 validator agents/codex/notes/validate-r5-templates.sh returned exit0 for Caddy2.8.4 and HAProxy3.0.30 assembled configs. Initial nonroot Caddy fixture setup failed with dropped file capabilities/empty fake hash; validator now adds only NET_BIND_SERVICE to satisfy the binary filecap and asserts fake hash generation. No ports or network are exposed by validators. diff --git a/agents/codex/notes/TASK-2026-10-19-r1-r3-notes.md b/agents/codex/notes/TASK-2026-10-19-r1-r3-notes.md new file mode 100644 index 0000000..aa0f177 --- /dev/null +++ b/agents/codex/notes/TASK-2026-10-19-r1-r3-notes.md @@ -0,0 +1,10 @@ +# Working notes — R1 / R3 + +- Base SHA: `6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60`. +- npm lock before changes: `next@16.3.2` (MIT), `sharp@0.35.3` (Apache-2.0), `source-map-js@1.2.1` (BSD-3-Clause). +- Minimal same-major patched versions selected from npm registry metadata: `next@16.3.8` (MIT), `sharp@0.35.5` (Apache-2.0), `source-map-js@1.2.2` (BSD-3-Clause). `eslint-config-next` moved 16.3.2→16.3.8 to match Next. +- Production audit before update found next Critical, sharp High, source-map-js High; after update `npm audit --omit=dev` reported zero vulnerabilities. +- Pagination must be considered incomplete when reported totals are absent/invalid/change mid-read, pages end early, page sizes are invalid, or the safety ceiling is reached. Do not return partial participants for persistence. +- `groups.isMember` only produces an accepted status when every requested ID appears exactly once with `member` exactly 0 or 1. Missing/invalid data aborts the import before repository save. +- Provider currently does not use `execute`; observed execute method limits and live VK method contracts are `UNVERIFIED`. +- A-H smoke was exercised against a fake localhost HTTP server. A, C-H emitted PASS and B emitted MANUAL; no real VK credentials/session were used. diff --git a/agents/codex/notes/stage1-pr-body.md b/agents/codex/notes/stage1-pr-body.md new file mode 100644 index 0000000..99418e8 --- /dev/null +++ b/agents/codex/notes/stage1-pr-body.md @@ -0,0 +1,12 @@ +Неполные и malformed ответы VK теперь останавливают импорт до сохранения участников; live-smoke отвергает MOCK и пустые результаты. Обновлены уязвимые production dependencies, исправлены clean PostgreSQL bootstrap и duplicate-draw conflict. Добавлены loopback production-профиль, socket-based proxy header sanitization, backup/restore scripts и runbook для существующих Caddy/HAProxy контейнеров VPS с условным reload без restart. + +Учтено обновление задания из main52fb758. Владелец прямо разрешил 32-byte seed и узкие seed assertions; proof/hash/HMAC алгоритмы сохранены. Main автором PR не вливается. + +Проверки production code cce70c0 (runtime-файлы далее не менялись): npm ci, prisma generate, npm test380/380, tsc, lint0errors/6warnings, build; npm audit --omit=dev0. PostgreSQL16: clean3migrations, integration12/12, schema diff0. Production Docker healthy; clean restore с3migrations/1fakegiveaway прошёл. Caddy2.8.4/HAProxy3.0.30 configs valid;25 shell blocks syntax pass; exact reload function показала worker handoff без изменения container identity/start/restart count и отказ при неподдерживаемом PID1. + +Два независимых reviewer закрыли найденные HIGH/MEDIUM после исправлений: malformed VK items, ложный live-smoke и отсутствующий executable reload. Открытые non-blocking LOW: pre-existing COMMUNITY capability metadata и отсутствие ordinary HTTP redirect при challenge-only маршруте. Dev-inclusive audit19 advisories; production audit0. + +Живой VK, DNS/TLS, proxy401, domain login и firewall/WireGuard isolation не проверены на VPS: это выполняет Claude/владелец по runbook. Phase CLOSED/release-ready не объявляются. + +Полный evidence, SHA, список файлов и ограничения: agents/codex/done/2026-10-19-stage1-completion.md. +Операторский runbook: docs/STAGE1_DEPLOYMENT_RUNBOOK.md. diff --git a/agents/codex/notes/validate-r5-templates.sh b/agents/codex/notes/validate-r5-templates.sh new file mode 100644 index 0000000..500a235 --- /dev/null +++ b/agents/codex/notes/validate-r5-templates.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Reproducible local syntax check. Fake credentials only; no published ports or VPS access. +set -euo pipefail +root=$(cd "$(dirname "$0")/../../.." && pwd) +cd "$root" +fixtures="$root/agents/codex/notes/r5-fixtures" +mkdir -p "$fixtures" +chmod 755 "$fixtures" +docker run --rm --name randomayzer-caddy-hash-fixture --network none --user 65534:65534 --cap-drop ALL --cap-add NET_BIND_SERVICE --security-opt no-new-privileges:true caddy:2.8-alpine caddy hash-password --plaintext FAKE_R5_CONFIG_TEST_ONLY > "$fixtures/fake-bcrypt.txt" +python3 - <<'PY' +from pathlib import Path +root=Path('agents/codex/notes/r5-fixtures') +h=(root/'fake-bcrypt.txt').read_text().strip() +assert h.startswith('$2'), 'Fake hash generation failed' +s=Path('deploy/randomayzer.Caddyfile').read_text() +assert s.count('owner BASIC_AUTH_HASH') == 1 +p=root/'Caddyfile';p.write_text(s.replace('owner BASIC_AUTH_HASH','owner '+h));p.chmod(0o644) +s=Path('deploy/randomayzer.haproxy.cfg').read_text() +tls='\n'.join(x for x in s.splitlines() if x.startswith(('acl ','use_backend '))).replace('CADDY_TLS_BACKEND_FROM_ACTIVE_CONFIG','fixture_caddy_tls') +http='\n'.join(x[2:] for x in s.splitlines() if x.startswith(('# acl randomayzer_http','# use_backend CADDY_HTTP'))).replace('CADDY_HTTP_BACKEND_FROM_ACTIVE_CONFIG','fixture_caddy_http') +assert 'use_backend fixture_caddy_tls' in tls +assert 'use_backend fixture_caddy_http' in http +config='''global + master-worker +defaults + mode tcp + timeout connect 5s + timeout client 10s + timeout server 10s +frontend fixture_tls + bind :8443 + tcp-request inspect-delay 5s + tcp-request content accept if { req.ssl_hello_type 1 } +'''+''.join(' '+x+'\n' for x in tls.splitlines())+''' default_backend fixture_other_tls +frontend fixture_http + mode http + bind :8080 +'''+''.join(' '+x+'\n' for x in http.splitlines())+''' default_backend fixture_other_http +backend fixture_caddy_tls + mode tcp + server caddy 127.0.0.1:9443 +backend fixture_other_tls + mode tcp + server other 127.0.0.1:9444 +backend fixture_caddy_http + mode http + server caddy 127.0.0.1:9080 +backend fixture_other_http + mode http + server other 127.0.0.1:9081 +''' +p=root/'haproxy.cfg';p.write_text(config);p.chmod(0o644) +PY +docker run --rm --name randomayzer-caddy28-validate --network none --user 65534:65534 --cap-drop ALL --cap-add NET_BIND_SERVICE --security-opt no-new-privileges:true -e XDG_DATA_HOME=/tmp/caddy-data -e XDG_CONFIG_HOME=/tmp/caddy-config -v "$fixtures/Caddyfile:/etc/caddy/Caddyfile:ro" caddy:2.8-alpine caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile +docker run --rm --name randomayzer-haproxy30-validate --network none --cap-drop ALL --security-opt no-new-privileges:true -v "$fixtures/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro" haproxy:3.0-alpine haproxy -c -V -f /usr/local/etc/haproxy/haproxy.cfg +printf 'PASS local Caddy 2.8 / HAProxy 3.0 template syntax (fake fixture, not live VPS acceptance)\n' diff --git a/compose.domain.yml b/compose.domain.yml new file mode 100644 index 0000000..e7edf43 --- /dev/null +++ b/compose.domain.yml @@ -0,0 +1,10 @@ +# Opt-in after VPS identity, DNS, firewall and proxy are verified by the operator. +services: + app: + environment: + DEPLOYMENT_PROFILE: strict + APP_BASE_URL: https://random.ochenstarik.ru + VK_REDIRECT_URI: https://random.ochenstarik.ru/api/auth/vk/callback + TRUSTED_PROXY_IP: 10.77.0.1 + ports: + - "10.77.0.3:${RANDOMAYZER_PORT:-3100}:3100" diff --git a/compose.production.yml b/compose.production.yml new file mode 100644 index 0000000..54f76af --- /dev/null +++ b/compose.production.yml @@ -0,0 +1,67 @@ +name: randomayzer-production +services: + postgres: + image: postgres:16-alpine + container_name: randomayzer-postgres + user: postgres + restart: unless-stopped + environment: + POSTGRES_USER: randomayzer + POSTGRES_DB: randomayzer + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in the 0600 environment file} + volumes: + - database:/var/lib/postgresql/data + networks: [database] + mem_limit: 512m + cpus: 1.0 + security_opt: [no-new-privileges:true] + healthcheck: + test: [CMD-SHELL, pg_isready -U randomayzer -d randomayzer] + interval: 5s + timeout: 3s + retries: 30 + app: + image: ${RANDOMAYZER_IMAGE:-randomayzer-app:stage1} + build: . + container_name: randomayzer-app + user: node + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + env_file: ${RANDOMAYZER_ENV_FILE:?Set absolute path to a 0600 environment file} + environment: + NODE_ENV: production + STORAGE_DRIVER: prisma + USE_VK_MOCK: "false" + DATABASE_URL: postgresql://randomayzer:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@postgres:5432/randomayzer + DEPLOYMENT_PROFILE: local-single-user + APP_BASE_URL: http://localhost:3100 + VK_REDIRECT_URI: http://localhost:3100/api/auth/vk/callback + TRUSTED_PROXY_IP: "" + ALLOW_MEMORY_IDEMPOTENCY: "true" + ALLOW_MEMORY_RATE_LIMITER: "true" + PORT: "3100" + BIND_ADDRESS: 0.0.0.0 + ports: + - "127.0.0.1:${RANDOMAYZER_PORT:-3100}:3100" + networks: [database, egress] + mem_limit: 1g + cpus: 1.5 + pids_limit: 256 + security_opt: [no-new-privileges:true] + cap_drop: [ALL] + healthcheck: + test: [CMD, node, -e, "fetch('http://127.0.0.1:3100/api/auth/me').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 10s + timeout: 5s + retries: 12 +volumes: + database: + name: randomayzer-database +networks: + database: + name: randomayzer-database + internal: true + egress: + name: randomayzer-egress diff --git a/deploy/randomayzer-backup.service b/deploy/randomayzer-backup.service new file mode 100644 index 0000000..3a4f30a --- /dev/null +++ b/deploy/randomayzer-backup.service @@ -0,0 +1,11 @@ +[Unit] +Description=Randomayzer backup and clean restore drill +Requires=docker.service +After=docker.service +[Service] +Type=oneshot +User=RANDOMAYZER_OPERATOR +Environment=RANDOMAYZER_ENV_FILE=/etc/randomayzer/production.env +WorkingDirectory=/opt/randomayzer +ExecStart=/opt/randomayzer/tools/db-backup-daily.sh /var/backups/randomayzer +UMask=0077 diff --git a/deploy/randomayzer-backup.timer b/deploy/randomayzer-backup.timer new file mode 100644 index 0000000..2d6fce3 --- /dev/null +++ b/deploy/randomayzer-backup.timer @@ -0,0 +1,8 @@ +[Unit] +Description=Daily verified Randomayzer backup +[Timer] +OnCalendar=*-*-* 04:15:00 +Persistent=true +RandomizedDelaySec=15m +[Install] +WantedBy=timers.target diff --git a/deploy/randomayzer.Caddyfile b/deploy/randomayzer.Caddyfile new file mode 100644 index 0000000..55d72f0 --- /dev/null +++ b/deploy/randomayzer.Caddyfile @@ -0,0 +1,36 @@ +# Add this site block to the existing Caddy 2.8 Caddyfile (or an already-imported +# site-fragment directory). This does not define a second listener or proxy. +# Replace BASIC_AUTH_HASH with interactive `caddy hash-password` output. +# Basic Auth is the supported access gate; do not add Authelia without confirming +# the existing proxy's auth policy and trusted identity headers. +random.ochenstarik.ru { + tls { + issuer acme { + disable_tlsalpn_challenge + } + } + basic_auth { + owner BASIC_AUTH_HASH + } + # Caddy 2.8 supports this body limit. Keep it ahead of the upstream. + request_body { + max_size 1MB + } + # No access log: OAuth callback query values are sensitive. + reverse_proxy 10.77.0.3:3100 { + header_up Host random.ochenstarik.ru + header_up X-Forwarded-Host random.ochenstarik.ru + header_up X-Forwarded-Proto https + # Replace caller-supplied forwarding metadata; Caddy's peer identity + # depends on the confirmed HAProxy-to-Caddy path. + header_up X-Forwarded-For {remote_host} + header_up -X-Real-IP + header_up -Forwarded + header_up -CF-Connecting-IP + header_up -Authorization + transport http { + dial_timeout 10s + response_header_timeout 120s + } + } +} diff --git a/deploy/randomayzer.haproxy.cfg b/deploy/randomayzer.haproxy.cfg new file mode 100644 index 0000000..98644f5 --- /dev/null +++ b/deploy/randomayzer.haproxy.cfg @@ -0,0 +1,27 @@ +# Additive examples only; this is NOT a standalone HAProxy configuration. +# Do not paste this whole file over the live config. +# +# 443 TLS passthrough example: insert in the existing mode tcp frontend bound +# to :443 after its existing TLS inspection rules and after explicit routes for +# other domains, but before any catch-all use_backend/default_backend. Replace +# CADDY_TLS_BACKEND_FROM_ACTIVE_CONFIG with the exact backend already used for +# krona-reverse-proxy. Do not create backend addresses based on guesses about +# Docker networks. +# If this frontend has no TLS ClientHello inspection, stop and reconcile its +# active routing design before adding these lines. +# +# tcp-request inspect-delay 5s # add only if the frontend has no inspect delay +acl randomayzer_sni req.ssl_sni -i random.ochenstarik.ru +use_backend CADDY_TLS_BACKEND_FROM_ACTIVE_CONFIG if randomayzer_sni +# Keep existing explicit routes and the original default_backend unchanged. + +# HTTP-01 example: only if inspection shows an existing mode http :80 frontend +# does not already route this host to Caddy. Put the host ACL and use_backend +# after existing explicit host routes and before any catch-all rule. This must +# use a distinct cleartext Caddy :80 backend, never the TLS :443 backend above. +# Caddy must receive the full, unchanged /.well-known/acme-challenge/ +# path on port 80; do not redirect, rewrite, or route it elsewhere in HAProxy. +# +# acl randomayzer_http01 path_beg /.well-known/acme-challenge/ +# acl randomayzer_http_host hdr(host),field(1,:) -i random.ochenstarik.ru +# use_backend CADDY_HTTP_BACKEND_FROM_ACTIVE_CONFIG if randomayzer_http_host randomayzer_http01 diff --git a/docker-compose.yml b/docker-compose.yml index fcb4cf9..3cb0ad2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,30 @@ -version: '3.8' - +# Disposable integration database only. Production uses compose.production.yml. +name: randomayzer-integration services: postgres: image: postgres:16-alpine - container_name: randomayzer-postgres - restart: unless-stopped + container_name: randomayzer-integration-postgres + restart: "no" + user: postgres environment: POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: randomayzer + POSTGRES_PASSWORD: randomayzer-integration-only + POSTGRES_DB: randomayzer_test ports: - - "5432:5432" + - "127.0.0.1:${RANDOMAYZER_TEST_DB_PORT:-55439}:5432" volumes: - pgdata:/var/lib/postgresql/data - + networks: [database] + mem_limit: 512m + cpus: 1.0 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d randomayzer_test"] + interval: 2s + timeout: 3s + retries: 30 volumes: pgdata: + name: randomayzer-integration-pgdata +networks: + database: + name: randomayzer-integration-database diff --git a/docs/PRODUCTION_GUARDS.md b/docs/PRODUCTION_GUARDS.md index 3db167a..26618cf 100644 --- a/docs/PRODUCTION_GUARDS.md +++ b/docs/PRODUCTION_GUARDS.md @@ -53,6 +53,22 @@ The API supports the standard `Idempotency-Key` HTTP header on state-mutating en - Normalizes IPv4, IPv6 (including IPv4-mapped IPv6 `::ffff:192.0.2.1` and loopbacks `::1`). - Validates IP syntax against IPv4/IPv6 standards. +### Supported production ingress profiles + +The stage-one Docker deployment uses `tools/serve-production.cjs` as its production entry point. It checks the +actual Node socket peer before forwarding a request to Next, removes client-supplied proxy identity headers, and +rebuilds `Host` and `X-Forwarded-*` from the configured canonical origin. In the strict domain profile, forwarded +client IP and HTTPS scheme are accepted only when `socket.remoteAddress` is the configured VPS peer +`10.77.0.1`; the reverse proxy must overwrite those headers. The local `local-single-user` profile accepts only +HTTP loopback origins and emits a non-Secure session cookie only for that explicit profile. All other production +profiles still require HTTPS and Secure cookies. + +This socket-aware boundary is specific to `tools/serve-production.cjs` and the supplied Compose profiles. The +generic `TRUST_PROXY=true` behavior described above parses forwarded chains and is safe only when an upstream +proxy already sanitizes them. Do not run `next start` with `TRUST_PROXY=true` as a substitute for this entry point, +and do not weaken the HTTPS or cookie checks globally. A deployment using another entry point must separately +establish and test its trusted socket boundary. + ### Memory Limiter vs Multi-Instance Deployments - The built-in `SlidingWindowRateLimiter` is optimized for single-instance, serverless dev, and test environments. - In multi-instance or horizontal cluster deployments, rate limiting must be offloaded to an edge layer (e.g., Cloudflare Rate Limiting, Nginx limit_req) or a shared distributed cache (Redis/Valkey). @@ -101,5 +117,5 @@ If the requested winners count plus reserve exceeds the locked snapshot's eligib | `TRUST_PROXY` | boolean (`true`/`false`) | `false` | Enable only behind trusted upstream proxies | | `ALLOW_MEMORY_IDEMPOTENCY` | boolean | `false` | Silence production warning for memory idempotency in single-instance | | `ALLOW_MEMORY_RATE_LIMITER` | boolean | `false` | Silence production warning for memory rate limiter in single-instance | -| `USE_VK_MOCK` | boolean | `false` | Explicitly enable VkMockProvider (forbidden in production unless true) | +| `USE_VK_MOCK` | boolean | `false` | Explicitly enable VkMockProvider in development/test; always forbidden in production | | `VK_SERVICE_TOKEN` | string | `undefined` | VK Application Service Access Token | diff --git a/docs/STAGE1_DEPLOYMENT_RUNBOOK.md b/docs/STAGE1_DEPLOYMENT_RUNBOOK.md new file mode 100644 index 0000000..9763689 --- /dev/null +++ b/docs/STAGE1_DEPLOYMENT_RUNBOOK.md @@ -0,0 +1,538 @@ +# Randomayzer: операторский runbook R2–R5 + +База задания: `6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60`. Команды выполняет владелец/Claude. +Ниже два альтернативных профиля: R4 только loopback; R5 строго HTTPS через существующий VPS. +Реальный вход VK, актуальное согласие на localhost redirect и возможности token scopes остаются **UNVERIFIED** до smoke. +Не менять существующие Apache/Express/Forgejo/WireGuard. Не устанавливать второй сервер на 80/443. + +## 1. Предварительная проверка на домашнем сервере + +```bash +cd /opt/randomayzer # замените фактическим checkout проекта +ss -ltn '( sport = :3100 or sport = :55439 )' +df -h . /var/lib/docker +docker info --format '{{.ServerVersion}}' +docker compose version +``` + +Порты должны быть свободны. Если 3100 занят — выбрать другой свободный непривилегированный порт и +одновременно изменить mapping, APP_BASE_URL/VK_REDIRECT_URI в compose, proxy upstream и команды ниже. +`RANDOMAYZER_PORT` меняет только host mapping; canonical origin обязательно согласовать отдельно. +Проверить запас диска для Node image, PostgreSQL и 14 бэкапов; при нехватке остановиться, чужие images/volumes не удалять. + +Создать вне checkout файл `/etc/randomayzer/production.env`, доступ только оператору, mode **0600**. +Не использовать `set -x`, `cat` файла, вывод `docker compose config`, `docker inspect` environment, +`docker compose logs` OAuth-запросов или `source` файла. Compose читает данные, не shell-код. +Обязательные имена переменных: + +```text +POSTGRES_PASSWORD= +AUTH_SECRET=<отдельный случайный ключ> +TOKEN_ENCRYPTION_KEY=<отдельный случайный ключ> +VK_APP_ID=<идентификатор приложения> +VK_CLIENT_SECRET=<секрет приложения> +VK_SERVICE_TOKEN=<сервисный токен> +``` + +Для локальной проверки без VK используются очевидные marker values; это не доказательство живого VK. +AUTH_SECRET/TOKEN_ENCRYPTION_KEY — разные криптографически случайные значения. + +```bash +export RANDOMAYZER_ENV_FILE=/etc/randomayzer/production.env +test "$(stat -c '%a' "$RANDOMAYZER_ENV_FILE")" = 600 +# В каждой новой shell определите функцию заново. +ry() { docker compose --env-file "$RANDOMAYZER_ENV_FILE" -f compose.production.yml "$@"; } +ry config --quiet +ry build app +ry up -d --wait postgres +ry run --rm --no-deps app npx prisma migrate deploy +ry up -d --wait app +ry ps +curl --fail --silent http://127.0.0.1:3100/api/auth/me +``` + +Ожидается: 3 миграции применены, оба сервиса healthy; `/api/auth/me` возвращает JSON `authenticated: false`, +`user: null` до входа. PostgreSQL **не имеет host ports**. Приложение на хосте доступно только на 127.0.0.1:3100. +Внутренний контейнерный listener — 0.0.0.0:3100, необходимый для Docker port forwarding; на хосте wildcard запрещён. +Оба контейнера непривилегированные, отдельные randomayzer-сети/том, CPU/RAM limits заданы в Compose. + +Для уже существующей БД: сначала бэкап. Новая `20260818000000_initial` описывает историческую схему до ownership/auth-store +миграций. **Не применять CREATE TABLE поверх существующих таблиц.** Сверить `prisma migrate status`, схему и историю: +если исходные таблицы уже созданы через `db push`, после проверки соответствия оператор выполняет +`ry run --rm --no-deps app npx prisma migrate resolve --applied 20260818000000_initial`, затем deploy. +Не отмечать старые ownership/auth миграции applied без проверки их SQL и фактических ограничений. +NULL organizerId по-прежнему вызывает отказ ownership migration; автоматической перепривязки нет. + +## 2. Доступ владельца / R4 + +На сервере открыть `http://localhost:3100`. Через SSH с компьютера владельца: + +```bash +ssh -N -L 3100:127.0.0.1:3100 OPERATOR@HOME_SERVER +``` + +Открыть `http://localhost:3100` на этом компьютере. В VK ID указать буквально +`http://localhost:3100/api/auth/vk/callback`; поддержку этого URI данным VK-приложением проверить вручную. +`local-single-user` допускает HTTP только для localhost/127.0.0.1/[::1], отключает Secure только в этом профиле. +Cookie остаётся HttpOnly + SameSite=Lax, CSRF проверяет canonical origin. Профиль нельзя использовать с LAN/public hostname. +Не публиковать домен, TLS/firewall/роутер ради R4. + +## 3. Ежедневный бэкап и проверка восстановления + +```bash +sudo install -d -m 700 -o OPERATOR -g OPERATOR /var/backups/randomayzer +tools/db-backup-daily.sh /var/backups/randomayzer +``` + +Replace both `OPERATOR` placeholders with the service account and its primary group. If backups are on a mounted +separate disk, mount it first and verify that this account can write there; keep the directory mode at `0700`. + +Ожидается `PASS backup restored to clean drill database` и `PASS daily backup and restore verification`. +Скрипт создаёт pg_dump custom archive c mode 0600, восстанавливает в новую временную БД `randomayzer_restore_`, +останавливается при первой SQL-ошибке, проверяет таблицы миграций/Giveaway, удаляет только временную БД. +Production DB не перезаписывается. Retention 14 дней применяется только после успешного drill. +Бэкап содержит private данные/зашифрованные credentials: не коммитить, не прикладывать к review. + +Для расписания заменить `RANDOMAYZER_OPERATOR` в `deploy/randomayzer-backup.service` именем оператора; +исправить WorkingDirectory/ExecStart на фактический checkout. Оператор должен иметь доступ к Docker и env-файлу. + +```bash +sudo install -m 644 deploy/randomayzer-backup.service deploy/randomayzer-backup.timer /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now randomayzer-backup.timer +systemctl list-timers randomayzer-backup.timer +sudo systemctl start randomayzer-backup.service +systemctl status randomayzer-backup.service --no-pager +``` + +Ожидается успешный oneshot и следующее ежедневное срабатывание. Проверять failed state ежедневно; +не считать наличие dump доказательством восстановления. + +## 4. Обновление и откат + +До обновления записать полный `git rev-parse HEAD`, сохранить текущий image под отдельным именем и выполнить backup/drill. + +```bash +git rev-parse HEAD +docker tag randomayzer-app:stage1 randomayzer-app:rollback +# Переключить checkout на явно выбранный reviewed SHA, не git pull поверх локальных изменений. +ry build app +ry run --rm --no-deps app npx prisma migrate deploy +ry up -d --wait app +curl --fail --silent http://127.0.0.1:3100/api/auth/me +``` + +Откат приложения: `RANDOMAYZER_IMAGE=randomayzer-app:rollback ry up -d --no-build --wait app`. +Если миграция несовместима с предыдущим приложением: остановить **только** `ry stop app`, восстановить dump +в новую БД, проверить её, затем оператор переключает DATABASE_URL/POSTGRES_DB на восстановленную БД. +Не откатывать production данные автоматически и не удалять текущий том. Для полной остановки приложения +`ry stop app`; `ry down` сохраняет volume. Никогда не использовать `down -v` на production. + +## 5. R2: отдельная уничтожаемая тестовая БД + +```bash +docker compose -f docker-compose.yml up -d --wait +DATABASE_URL=postgresql://postgres:randomayzer-integration-only@127.0.0.1:55439/randomayzer_test npx prisma migrate deploy +DATABASE_URL=postgresql://postgres:randomayzer-integration-only@127.0.0.1:55439/randomayzer_test npm run test:integration +docker compose -f docker-compose.yml down +``` + +Integration tests очищают все данные: **никогда не подставлять production URL**. Имена и volume отделены от production. + +## 6. R5: подготовка и безопасное изменение действующих proxy-контейнеров + +R5 остаётся **BLOCKED** до подтверждения владельцем DNS `random.ochenstarik.ru → 178.212.13.102` +и того, что домашний `10.77.0.1` — этот VPS. Не менять WireGuard, firewall manager, Compose-проекты, +контейнеры или существующие виртуальные хосты других приложений. + +VPS-команды ниже запускай из root shell с доступом к Docker; если SSH-сеанс не root, выполни `sudo -i` +и перейди в checkout. Это нужно для приватного backup под `/root` и управления действующими контейнерами. +В root Bash включить остановку при ошибке; до начала согласовать окно без параллельного редактирования proxy +конфигов. При изменении файлов другим оператором прекратить работу и согласовать diff, не затирать его backup. + +```bash +set -euo pipefail +``` + +Не выводи `.Config.Env`. На домашнем сервере оставайся под своим обычным пользователем, используя `sudo` +только для firewall-команд. + +### 6.1. Установить фактическую топологию без просмотра environment + +На VPS выполняет оператор. Команды намеренно не выводят `.Config.Env`, содержимое конфигов, логи или секреты: + +```bash +docker ps --filter name=krona-proxy-mux --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' +docker ps --filter name=krona-reverse-proxy --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' +docker inspect --format 'image={{.Config.Image}} path={{.Path}} args={{json .Args}} entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}}' krona-proxy-mux +docker inspect --format '{{range .Mounts}}{{printf "%s\t%s\t%s\tRW=%t\n" .Type .Source .Destination .RW}}{{end}}' krona-proxy-mux +docker inspect --format 'image={{.Config.Image}} path={{.Path}} args={{json .Args}} entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}}' krona-reverse-proxy +docker inspect --format '{{range .Mounts}}{{printf "%s\t%s\t%s\tRW=%t\n" .Type .Source .Destination .RW}}{{end}}' krona-reverse-proxy +docker exec krona-proxy-mux haproxy -vv | sed -n '1,3p' +docker exec krona-reverse-proxy caddy version +ss -ltn '( sport = :80 or sport = :443 )' +getent ahostsv4 random.ochenstarik.ru +ip route get 10.77.0.3 +``` + +Ожидается: контейнеры с указанными именами и образами семейств `haproxy:3.0-alpine` / `caddy:2.8-alpine`, +HAProxy 3.0.x и Caddy 2.8.x. Точные patch-версии на VPS зафиксировать из вывода; локальный образ/тег не +доказывает версию удалённого контейнера. `getent` должен вернуть подтверждённый A-record, маршрут к дому — +`src 10.77.0.1`. Если имя/образ/режим процесса отличается, не применять примеры ниже. + +По `Entrypoint`, `Cmd` и mounts оператор определяет фактические config path(ы), все `include`-файлы и source +файлы, которыми контейнер управляется. Не печатать/публиковать raw-конфиги или полные inspect-объекты. +Для HAProxy подтвердить, что порт 443 — `mode tcp` TLS passthrough и где уже выполняются SNI inspection, +`use_backend` и catch-all. Для HTTP-01 установить фактическую цепочку порта 80 до Caddy. Если HAProxy +терминирует TLS, существующий SNI-маршрут не ведёт к Caddy, не определён безопасный путь HTTP-01 до Caddy, +или для редактирования/reload не найден штатный механизм — **остановиться**. Не угадывать backend, адрес +Docker-сети, config path или команду reload. + +На домашнем сервере сначала проверить владельца WG peer и firewall: + +```bash +ip -brief address show krona-wg +ip route get 10.77.0.1 +sudo iptables -S DOCKER-USER +sudo iptables -S INPUT +``` + +Ожидается `10.77.0.3` на существующем `krona-wg`, маршрут через него и подтверждённый владельцем peer +`10.77.0.1`. Если используется nftables, firewalld, ufw или иной manager — не добавлять параллельные +iptables rules; остановиться и согласовать правила в существующем manager. Только после подтверждения +iptables Docker backend, отсутствия chain с таким именем и проверки текущих rules установить ограничение +до публикации порта: + +```bash +sudo iptables -N RANDOMAYZER-WG +sudo iptables -A RANDOMAYZER-WG -s 10.77.0.1 -j RETURN +sudo iptables -A RANDOMAYZER-WG -j DROP +sudo iptables -I DOCKER-USER 1 -p tcp -m conntrack --ctorigdst 10.77.0.3 --ctorigdstport 3100 -j RANDOMAYZER-WG +sudo iptables -I INPUT 1 -p tcp -d 10.77.0.3 --dport 3100 -j RANDOMAYZER-WG +sudo iptables -S RANDOMAYZER-WG +sudo iptables -S DOCKER-USER +sudo iptables -S INPUT +``` + +Ожидается разрешение только source `10.77.0.1` для host destination `10.77.0.3:3100`; остальным source +соединения к этому destination drop. Если chain уже существует или вывод не соответствует, не повторять +команды/не вставлять дубли; разобрать текущие rules с владельцем. После проверки сохранить только новые +rules через уже работающий firewall manager. Без подтверждённой persistence R5 не принят. Не публиковать +порт на LAN, wildcard или роутере. + +В той же безопасной последовательности, из checkout на домашнем сервере, включить только domain overlay: + +```bash +export RANDOMAYZER_ENV_FILE=/etc/randomayzer/production.env +ryd() { docker compose --env-file "$RANDOMAYZER_ENV_FILE" -f compose.production.yml -f compose.domain.yml "$@"; } +ryd config --quiet +ryd up -d --wait app +ryd ps +ss -ltn '( sport = :3100 )' +``` + +Ожидается, что app опубликовал только `127.0.0.1:3100` и `10.77.0.3:3100`; DB по-прежнему без host binding. +Если Docker показывает `0.0.0.0`, другой host address или DB port, немедленно вернуть loopback-only профиль и +не продолжать. На VPS до reload прокси проверить новый WG upstream: + +```bash +curl --connect-timeout 5 --max-time 15 --fail --silent --show-error http://10.77.0.3:3100/api/auth/me +``` + +Ожидается JSON `authenticated: false`, `user: null`; это проверяет путь до приложения, не внешний TLS/login. + +До редактирования прочитать 6.4 и выполнить его read-only preflight; определить функцию reload в той же +root shell. Не начинать изменение, если предусловия стандартного master-worker варианта не выполнены и +не существует другого уже документированного и проверенного оператором механизма. Команда reload при +этом ещё не выполняется. + +### 6.2. Базовый снимок и резервная копия обоих конфигов + +До редактирования сохранить ответы всех уже обслуживаемых сайтов по обоим публичным входам. Адреса listener +80 и 443 оператор находит в `ss`; вводить конкретный локальный адрес listener, например `127.0.0.1`, никогда +`0.0.0.0`. Перечислить в `BASELINE_HOSTS` все имена из текущих HAProxy/Caddy routes, включая +`git.ochenstarik.ru`. Не включать URL, query, заголовки авторизации или bodies: + +```bash +read -r -p 'Local HTTP listener IP for port 80 from ss: ' HTTP_LISTENER_IP +read -r -p 'Local TLS listener IP for port 443 from ss: ' TLS_LISTENER_IP +BASELINE_HOSTS=(git.ochenstarik.ru) # дописать все остальные текущие домены из proxy-конфигов +for host in "${BASELINE_HOSTS[@]}"; do + curl --noproxy '*' --connect-timeout 5 --max-time 15 --silent --show-error \ + --output /dev/null --write-out "$host http %{http_code} %{remote_ip}\n" \ + --resolve "$host:80:$HTTP_LISTENER_IP" "http://$host/" || { echo "STOP: HTTP baseline failed for $host" >&2; exit 1; } + curl --noproxy '*' --connect-timeout 5 --max-time 15 --silent --show-error \ + --output /dev/null --write-out "$host https %{http_code} %{remote_ip}\n" \ + --resolve "$host:443:$TLS_LISTENER_IP" "https://$host/" || { echo "STOP: HTTPS baseline failed for $host" >&2; exit 1; } +done +``` + +Записать именно выведенные HTTP и HTTPS status codes/адреса в operator change record. Для случайного сайта до +добавления route допустим существующий default response; не угадывать ожидаемый статус. Baseline Forgejo и +каждого другого хоста по обоим протоколам после reload должен совпасть с его собственным доизменительным +результатом. + +По mounts определить host source и container path каждого активного Caddy/HAProxy config-файла и всех +подключаемых конфигов. Если конфиг генерируется entrypoint/Compose, работать через его подтверждённый +source/deployment mechanism, не править сгенерированный файл. Создать root-only каталог резервной копии и +снять копии как активных файлов из контейнеров, так и source деревьев конфигурации: + +```bash +umask 077 +BACKUP_DIR="/root/randomayzer-proxy-backup-$(date -u +%Y%m%dT%H%M%SZ)" +install -d -m 0700 "$BACKUP_DIR" +# Задать пути только после inspection. Если mount содержит include-файлы, копировать всё config-дерево. +CADDY_ACTIVE_CONFIG=/path/confirmed/in/container/Caddyfile +CADDY_HOST_SOURCE=/path/confirmed/on/host/Caddyfile +HAPROXY_ACTIVE_CONFIG=/path/confirmed/in/container/haproxy.cfg +HAPROXY_HOST_SOURCE=/path/confirmed/on/host/haproxy.cfg +docker cp "krona-reverse-proxy:$CADDY_ACTIVE_CONFIG" "$BACKUP_DIR/caddy-active" +docker cp "krona-proxy-mux:$HAPROXY_ACTIVE_CONFIG" "$BACKUP_DIR/haproxy-active" +cp -a -- "$CADDY_HOST_SOURCE" "$BACKUP_DIR/caddy-source" +cp -a -- "$HAPROXY_HOST_SOURCE" "$BACKUP_DIR/haproxy-source" +chmod -R go-rwx "$BACKUP_DIR" +cmp -s -- "$CADDY_HOST_SOURCE" "$BACKUP_DIR/caddy-source" || { echo 'STOP: Caddy source backup incomplete'; exit 1; } +cmp -s -- "$HAPROXY_HOST_SOURCE" "$BACKUP_DIR/haproxy-source" || { echo 'STOP: HAProxy source backup incomplete'; exit 1; } +CADDY_INODE_BEFORE=$(stat -Lc '%d:%i' "$CADDY_HOST_SOURCE") +HAPROXY_INODE_BEFORE=$(stat -Lc '%d:%i' "$HAPROXY_HOST_SOURCE") +cmp -s -- "$CADDY_HOST_SOURCE" "$BACKUP_DIR/caddy-active" || { echo 'STOP: Caddy active/source differ'; exit 1; } +cmp -s -- "$HAPROXY_HOST_SOURCE" "$BACKUP_DIR/haproxy-active" || { echo 'STOP: HAProxy active/source differ'; exit 1; } +sha256sum "$CADDY_HOST_SOURCE" "$HAPROXY_HOST_SOURCE" "$BACKUP_DIR/caddy-active" "$BACKUP_DIR/haproxy-active" +``` + +Для directory mounts сделать отдельные backups всего mounted config tree, включая imports (задать подтверждённые +пути directory mounts из `docker inspect`): + +```bash +CADDY_CONFIG_DIR=/confirmed/container/config-dir +CADDY_HOST_CONFIG_DIR=/confirmed/host/config-dir +HAPROXY_CONFIG_DIR=/confirmed/container/config-dir +HAPROXY_HOST_CONFIG_DIR=/confirmed/host/config-dir +install -d -m 0700 "$BACKUP_DIR/caddy-tree-active" "$BACKUP_DIR/caddy-tree-source" +install -d -m 0700 "$BACKUP_DIR/haproxy-tree-active" "$BACKUP_DIR/haproxy-tree-source" +docker cp "krona-reverse-proxy:$CADDY_CONFIG_DIR/." "$BACKUP_DIR/caddy-tree-active" +docker cp "krona-proxy-mux:$HAPROXY_CONFIG_DIR/." "$BACKUP_DIR/haproxy-tree-active" +cp -a -- "$CADDY_HOST_CONFIG_DIR/." "$BACKUP_DIR/caddy-tree-source/" +cp -a -- "$HAPROXY_HOST_CONFIG_DIR/." "$BACKUP_DIR/haproxy-tree-source/" +chmod -R go-rwx "$BACKUP_DIR" +# Любой неполный copy/mismatch — STOP до редактирования. Не выводить содержимое/дифф конфигов. +for tree in caddy-tree-active caddy-tree-source haproxy-tree-active haproxy-tree-source; do + test -n "$(find "$BACKUP_DIR/$tree" -type f -print -quit)" || { echo 'STOP: empty config-tree backup'; exit 1; } +done +diff -qr -- "$CADDY_HOST_CONFIG_DIR" "$BACKUP_DIR/caddy-tree-source" >/dev/null || { echo 'STOP: Caddy source backup differs'; exit 1; } +diff -qr -- "$HAPROXY_HOST_CONFIG_DIR" "$BACKUP_DIR/haproxy-tree-source" >/dev/null || { echo 'STOP: HAProxy source backup differs'; exit 1; } +diff -qr -- "$BACKUP_DIR/caddy-tree-active" "$BACKUP_DIR/caddy-tree-source" >/dev/null || { echo 'STOP: Caddy active/source tree differs'; exit 1; } +diff -qr -- "$BACKUP_DIR/haproxy-tree-active" "$BACKUP_DIR/haproxy-tree-source" >/dev/null || { echo 'STOP: HAProxy active/source tree differs'; exit 1; } +``` + +Сверить хеши активных файлов с source до изменения. Если active path или source не доказан, остановиться. Резервная копия может +содержать proxy credentials и Caddy basic-auth hash: оставить её только в каталоге `0700`, не коммитить, +не отправлять в чат/логи и удалить штатной процедурой владельца после принятия изменения. + +### 6.3. Валидация до и после изменения + +Сначала текущие конфиги должны проходить те же checks до редактирования: + +```bash +docker exec krona-reverse-proxy caddy validate \ + --config "$CADDY_ACTIVE_CONFIG" --adapter caddyfile +docker exec krona-proxy-mux haproxy -c -V -f "$HAPROXY_ACTIVE_CONFIG" +``` + +Если HAProxy стартует с несколькими `-f`, передать в проверку все активные конфиги в том же порядке. +Ожидается `Valid configuration` от Caddy и exit code 0 от HAProxy (`-V` печатает версию; HAProxy может не +печатать отдельную строку успеха). Любая ошибка до изменения — +стоп, не пытаться «починить» baseline конфигурацию в рамках Randomayzer. + +Смонтированный **одиночный файл** может оставаться старым inode при atomic rename/`mv`/некоторых редакторах; +Docker продолжит показывать старое содержимое. Смонтированный каталог имеет другую семантику. Поэтому сначала +определить type mount и текущие file inode/hash. Изменять только подтверждённый host source существующим +способом деплоя. Для single-file bind mount сохранять inode: не использовать `sed -i`, `mv`, `sudoedit` или +full-file copy-over. После правки повторить stat/hash checks, убедиться что device/inode остались прежними, +а host source и active container content совпали. Для directory mount также сверить итоговые хеши из контейнера. +При несовпадении ничего не reload; +сначала вернуть подтверждённый source и добиться совпадения. + +Не заменять active Caddyfile/HAProxy config целиком шаблонами из репозитория. Вставить только новый Caddy site +block из `deploy/randomayzer.Caddyfile` в активный Caddyfile или уже подключённый fragments directory. Если на +VPS установлен Vim и подтверждённый source — обычный файл, редактировать так, чтобы сохранить inode: + +```bash +sudo vim -c 'set backupcopy=yes' "$CADDY_HOST_SOURCE" +sudo vim -c 'set backupcopy=yes' "$HAPROXY_HOST_SOURCE" +``` + +В редакторе добавить только доменный block/правила из snippet в правильные места, проверить diff локально, +сохранить, затем выполнить host/container inode/hash checks ниже. Если Vim отсутствует, файл — symlink, +конфигурация генерируется или source read-only, остановиться и применить подтверждённый deployment mechanism; +не переключаться на `sed -i`/`mv`/atomic rename. Интерактивно +создать hash: `docker exec -it krona-reverse-proxy caddy hash-password`; значение hash поместить только в +новый локальный конфиг. Не писать пароль/hash в CLI args, shell history или Git. Выбран Basic Auth; Authelia +не подключать без подтверждения действующих access-control policy и identity headers. Для Caddy body limit — +1 MB; access log для этого сайта не добавлять, так как OAuth callback может нести чувствительные query values. + +HAProxy snippet `deploy/randomayzer.haproxy.cfg` содержит **только примеры вставок**, а не полный конфиг. +Порядок активации — HTTP-01 при необходимости, затем защищённый Caddy site, затем новый TLS SNI route. +Это три отдельных этапа; не публиковать новый TLS route до успешного Caddy reload с Basic Auth. + +Если текущий порт 80 уже передаёт challenge для этого Host к Caddy, оставить его без изменения. Иначе, +только для подтверждённого `mode http` frontend, сначала добавить **лишь** HTTP-01 ACL/route из snippet к +существующему cleartext Caddy HTTP backend: точный Host **и** path prefix `/.well-known/acme-challenge/`. +Не использовать TLS/443 backend, не менять другие HTTP routes, не добавлять новый listener и не добавлять +пока TLS SNI ACL. Провалидировать полный HAProxy config, выполнить подтверждённый reload из 6.4 и повторить +other-site baseline. Этот этап делает доступным только challenge path; новый TLS route ещё отсутствует. +Если существующий HTTP mode/route/backend не установлен, остановиться — не угадывать топологию. + +Теперь добавить Caddy block с Basic Auth, проверить source/active inode/hash и `caddy validate`, затем +выполнить только Caddy reload из 6.4. Успешный reload доказывает загрузку auth-конфига; выдача сертификата +проверяется отдельно. После успешного Caddy reload, если и только если frontend 443 — TCP TLS passthrough, +добавить точный SNI ACL/`use_backend` к существующему Caddy TLS backend после явных маршрутов других доменов +и перед catch-all. Другие rules/default backend не менять. Повторить source/active checks и полный +`haproxy -c -V`, затем graceful reload HAProxy из 6.4 и baseline всех сайтов. Не добавлять Authelia без +подтверждённого policy. Для каждого из этапов применяются checks ниже; не пропускать их при повторном reload. + +После вносимых дополнений ещё раз проверить хеши и оба конфига: + +```bash +sha256sum "$CADDY_HOST_SOURCE" "$HAPROXY_HOST_SOURCE" +docker exec krona-reverse-proxy sha256sum "$CADDY_ACTIVE_CONFIG" +docker exec krona-proxy-mux sha256sum "$HAPROXY_ACTIVE_CONFIG" +test "$CADDY_INODE_BEFORE" = "$(stat -Lc '%d:%i' "$CADDY_HOST_SOURCE")" || { echo 'STOP: Caddy source inode changed'; exit 1; } +test "$HAPROXY_INODE_BEFORE" = "$(stat -Lc '%d:%i' "$HAPROXY_HOST_SOURCE")" || { echo 'STOP: HAProxy source inode changed'; exit 1; } +docker cp "krona-reverse-proxy:$CADDY_ACTIVE_CONFIG" "$BACKUP_DIR/caddy-active-after" +docker cp "krona-proxy-mux:$HAPROXY_ACTIVE_CONFIG" "$BACKUP_DIR/haproxy-active-after" +cmp -s -- "$CADDY_HOST_SOURCE" "$BACKUP_DIR/caddy-active-after" || { echo 'STOP: Caddy host/active differ'; exit 1; } +cmp -s -- "$HAPROXY_HOST_SOURCE" "$BACKUP_DIR/haproxy-active-after" || { echo 'STOP: HAProxy host/active differ'; exit 1; } +docker exec krona-reverse-proxy caddy validate \ + --config "$CADDY_ACTIVE_CONFIG" --adapter caddyfile +docker exec krona-proxy-mux haproxy -c -V -f "$HAPROXY_ACTIVE_CONFIG" +``` + +Ожидается одинаковый source/active hash для каждого файла плюс успешные Caddy/HAProxy checks. Если include +files edited, сверить и провалидировать весь config set. Не reload при любом mismatch/error. + +### 6.4. Условные команды reload без container restart + +**До изменения** подтвердить, что PID1 именно `haproxy`, в его действующих argv есть отдельный `-W`/`-Ws`, +а все `-f` paths соответствуют полному config set, проверяемому в 6.3. Этот стандартный вариант проверен +локально на official `haproxy:3.0-alpine`, но не доказывает process model VPS. При wrapper/tini, другом PID1, +отсутствии flags или неизвестном config set — STOP, не посылать сигнал и не менять процессную модель. +Read-only preflight (выполняется до 6.2): + +```bash +docker exec krona-proxy-mux sh -c 'test "$(cat /proc/1/comm)" = haproxy && tr "\000" "\n" < /proc/1/cmdline | grep -Eq "^-W(s)?$"' || { echo 'STOP: unconfirmed HAProxy master-worker PID1'; exit 1; } +# Сверить каждый -f с обнаруженными active paths, не угадывать значения. +docker exec krona-proxy-mux sh -c 'tr "\000" "\n" < /proc/1/cmdline' +``` + +Для подтверждённого выше PID1=master определить функции в той же root shell. Они не запускают reload до +явного вызова. Проверка generation не заменяет последующие probes всех сайтов и нового route. + +```bash +haproxy_worker_pids() { + docker exec "$1" sh -c 'for pid in $(cat /proc/1/task/1/children); do if [ "$(cat /proc/$pid/comm 2>/dev/null)" = haproxy ]; then printf "%s " "$pid"; fi; done' +} +reload_haproxy_without_restart() { + local container="$1" before after old_workers new_workers pid attempt handoff + docker exec "$container" sh -c 'test "$(cat /proc/1/comm)" = haproxy && tr "\000" "\n" < /proc/1/cmdline | grep -Eq "^-W(s)?$"' || return 1 + before=$(docker inspect --format '{{.Id}} {{.State.StartedAt}} {{.RestartCount}}' "$container") || return 1 + old_workers=$(haproxy_worker_pids "$container") || return 1 + test -n "$old_workers" || return 1 + # USR2 targets confirmed PID1 master. This is NOT docker restart/kill default signal. + docker kill --signal=USR2 "$container" >/dev/null || return 1 + handoff=no + for attempt in 1 2 3 4 5 6 7 8 9 10; do + new_workers=$(haproxy_worker_pids "$container") || return 1 + for pid in $new_workers; do + case " $old_workers " in *" $pid "*) ;; *) handoff=yes ;; esac + done + test "$handoff" = yes && break + sleep 1 + done + after=$(docker inspect --format '{{.Id}} {{.State.StartedAt}} {{.RestartCount}}' "$container") || return 1 + test "$before" = "$after" && test "$handoff" = yes || return 1 + test "$(docker inspect --format '{{.State.Running}}' "$container")" = true || return 1 + printf 'HAProxy worker handoff observed; container identity/start/restart count unchanged\n' +} +``` + +После **каждого** полного validation/source-match gate из 6.3 активировать только соответствующий этап: + +```bash +# Сначала только если нужен новый HTTP-01 route; затем повторить этот вызов для финального TLS SNI route. +reload_haproxy_without_restart krona-proxy-mux || { echo 'STOP: HAProxy handoff failed; use rollback 6.6'; exit 1; } +``` + +Между HTTP-01 и TLS этапами загрузить Caddy Basic Auth/site (без container restart): + +```bash +CADDY_CONTAINER_BEFORE=$(docker inspect --format '{{.Id}} {{.State.StartedAt}} {{.RestartCount}}' krona-reverse-proxy) +docker exec krona-reverse-proxy caddy reload --config "$CADDY_ACTIVE_CONFIG" --adapter caddyfile || { echo 'STOP: Caddy reload failed; use rollback 6.6'; exit 1; } +test "$CADDY_CONTAINER_BEFORE" = "$(docker inspect --format '{{.Id}} {{.State.StartedAt}} {{.RestartCount}}' krona-reverse-proxy)" || { echo 'STOP: Caddy container identity changed'; exit 1; } +``` + +Если Caddy admin API выключен/имеет другой адрес — STOP; использовать только заранее подтверждённый admin +endpoint и штатный reload, не сигнал/restart как обход. Если worker handoff не подтверждён или service probes +изменились, не повторять USR2 бесконечно: восстановить private backup, validate и применить ту же проверенную +reload-процедуру по 6.6. При неизвестном process model оставить конфиги неизменными и передать blocker владельцу. + +### 6.5. После reload и приёмка + +Повторить baseline loop из 6.2 с теми же `BASELINE_HOSTS` и адресами listener 80/443. Каждый существующий сайт +должен вернуть прежний status code по обоим протоколам и ответить за 15 секунд; минимум Forgejo +`git.ochenstarik.ru` обязателен. Иные +результаты — немедленный rollback. + +После подтверждения DNS и внешнего маршрута: + +```bash +curl --connect-timeout 5 --max-time 15 --silent --show-error \ + --output /dev/null --write-out '%{http_code}\n' https://random.ochenstarik.ru/ +``` + +Ожидается `401` без Basic Auth. Для защищённой проверки `/api/auth/me` использовать интерактивный prompt: + +```bash +curl --connect-timeout 5 --max-time 15 --user owner --fail --silent --show-error \ + https://random.ochenstarik.ru/api/auth/me +``` + +Не указывать пароль в команде. Ожидается безопасный JSON с `authenticated: false` и `user: null` до входа. +Проверить TLS-сертификат извне, ручное VK consent/login и callback в браузере. Путь HTTP-01 удостоверяется +фактической выдачей/renewal сертификата через Caddy; `caddy validate` сам по себе выдачу не подтверждает. +Проверить с домашнего сервера, что порт приложения доступен с VPS через WG, но недоступен с LAN; с внешней +машины и неразрешённого WG peer он также недоступен. Не тестировать порт через интернет, если публичный +bind/firewall ещё не подтверждены. Порт БД не должен появиться. Записать наблюдения/даты/SHA в операторский +отчёт, без secret/config/body/log dumps. Сертификат, 401, VK login, source identity и firewall isolation до +фактического стенда остаются `UNVERIFIED`. + +### 6.6. Откат без воздействия на остальные сервисы + +Если reload не прошёл, TLS/HTTP-01 не работает, Randomayzer не отвечает или любой предыдущий сайт изменил +результат: прекратить дальнейшие проверки. На host source восстановить только изменённые config-файлы из +private backup, записывая в существующий inode (`cat backup-file > existing-mounted-file` после проверки +пути/hash), либо применить подтверждённый штатный config deployment. Не `mv`/atomic replace single-file bind +mount. Не восстанавливать/перезаписывать весь proxy config tree поверх чужих параллельных изменений. Проверить +source и active hashes, затем Caddy `validate` и HAProxy `-c` теми же путями, что выше. + +Загрузить восстановленный Caddy config его reload-командой, затем вызвать `reload_haproxy_without_restart krona-proxy-mux` с теми же предусловиями; обе операции выполняются без рестарта и проверить Caddy/HAProxy container ID/uptime, +затем повторить исходные other-site baselines. Ожидается прежний baseline для Forgejo и каждого перечисленного +домена; Randomayzer снова не должен попадать на новый route. Если безопасный HAProxy reload не найден или +baseline чужого сайта не восстановился, не перезапускать общий proxy: оставить его работающим в доступном +состоянии и немедленно передать владельцу backup, фактический config path, validation output и точную ошибку +без секретов. На домашнем сервере переключить только Randomayzer в loopback-only профиль и удалить только +его добавленные firewall rules через тот manager, которым они были установлены. Существующие containers, +volumes, sites, Authelia, Forgejo, fleet-hub и WireGuard сохраняются. + +## Источники конфигурации + +- [Next.js custom server](https://nextjs.org/docs/app/guides/custom-server) — socket-aware ingress перед Next. +- [Docker Compose services](https://docs.docker.com/reference/compose-file/services/) — host bindings и resource limits. +- [HAProxy 3.0 Configuration Manual](https://docs.haproxy.org/3.0/configuration.html) — TCP inspection, SNI sample fetches, ACL/use_backend and validation options. Использовать семантику для фактической версии 3.0.x; локальный template остаётся примером. +- [Caddy `basic_auth`](https://caddyserver.com/docs/caddyfile/directives/basic_auth) — хеширование пароля; директива переименована в `basic_auth` до версии 2.8. +- [Caddy `reverse_proxy`](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy) — upstream и HTTP transport timeouts. +- [Caddy automatic HTTPS](https://caddyserver.com/docs/automatic-https) — HTTP-01/redirect/certificate behavior. Доступность пути подтверждается реальной конфигурацией/сертификатом, не статическим предположением. +- [Caddy command line](https://caddyserver.com/docs/command-line) — `validate` и `reload`; admin endpoint и успешный reload должны быть подтверждены на существующем контейнере. +- [Docker inspect](https://docs.docker.com/reference/cli/docker/container/inspect/) and [Docker cp](https://docs.docker.com/reference/cli/docker/container/cp/) — узкий вывод конфигурации контейнера, mounts и резервное копирование. Никогда не выводить `.Config.Env`. + +- [HAProxy 3.0 Management Guide](https://docs.haproxy.org/3.0/management.html) — `-W`/`-Ws`, SIGUSR2 для master и exit status `-c`/`-V`. [Official Docker entrypoint](https://github.com/docker-library/haproxy/blob/master/3.0/alpine/docker-entrypoint.sh) — пример образа; действующий PID1/argv VPS всё равно проверяется отдельно. diff --git a/docs/VK_REAL_SMOKE_TEST.md b/docs/VK_REAL_SMOKE_TEST.md index 76a3beb..6c98004 100644 --- a/docs/VK_REAL_SMOKE_TEST.md +++ b/docs/VK_REAL_SMOKE_TEST.md @@ -1,43 +1,71 @@ -# Real VK ID & API Live Smoke Test Runbook +# VK live smoke test -This runbook outlines the live verification steps for testing VK ID OAuth 2.1 and authenticated VK operations without committing credentials into version control or CI. +The smoke tool checks the configured application and VK account without placing tokens or cookies in command arguments or output. It only accepts loopback HTTP or the approved `https://random.ochenstarik.ru` origin and a regular environment file with mode `0600`. The domain run requires proxy Basic Auth in that file. ---- +## Prepare the local run -## 1. Local Environment Preparation +1. Start the app with its normal local credentials, then log in from a browser at the configured loopback URL. +2. Complete the browser consent flow described in **B** below. +3. Create a file outside the repository, for example `~/.vk-smoke.env`, and restrict it to the owner: -Set in your `.env.local` file: -```bash -# VK ID Web Application Credentials -VK_APP_ID="" -VK_CLIENT_SECRET="" + ```sh + umask 077 + touch ~/.vk-smoke.env + chmod 600 ~/.vk-smoke.env + ``` -# Service Token for Public Operations -VK_SERVICE_TOKEN="" +4. Add one unquoted `KEY=value` per line. Do not add VK access tokens or client secrets. The session cookie is sensitive and must remain in this `0600` file. -# Canonical Local Configuration -APP_BASE_URL="http://localhost:3000" -VK_REDIRECT_URI="http://localhost:3000/api/auth/vk/callback" + ```text + VK_SMOKE_BASE_URL=http://localhost:3100 + VK_SMOKE_SESSION_COOKIE=randomayzer_session= + VK_SMOKE_PROXY_AUTH=: + VK_SMOKE_PUBLIC_POST_URL=https://vk.com/wall-_ + VK_SMOKE_PRIVATE_POST_URL=https://vk.com/wall-_ + VK_SMOKE_GROUP_ID= + VK_SMOKE_CREATE_GIVEAWAY=YES + ``` -# Cryptographic Keys (min 32 chars) -AUTH_SECRET="" -TOKEN_ENCRYPTION_KEY="" -``` + For the domain run, set `VK_SMOKE_BASE_URL=https://random.ochenstarik.ru` and provide `VK_SMOKE_PROXY_AUTH`. Proxy credentials are used only by curl from the protected temporary config. `VK_SMOKE_PRIVATE_POST_URL` and `VK_SMOKE_GROUP_ID` are optional when those checks are not part of the run. Remove their lines to skip the related checks. Set `VK_SMOKE_CREATE_GIVEAWAY=YES` only when a smoke-test giveaway may be created. ---- +5. Run: -## 2. Verification Checklist + ```sh + ./tools/vk-smoke.sh ~/.vk-smoke.env + ``` -- [ ] **A. OAuth Login Start**: Visit `/api/auth/vk/start` $\rightarrow$ Redirects to `https://id.vk.com/authorize` with PKCE `code_challenge` (S256). -- [ ] **B. OAuth Callback**: Authorize on VK screen $\rightarrow$ Redirected to `/api/auth/vk/callback`, sets HttpOnly cookie `randomayzer_session`. -- [ ] **C. Session Inspection**: Visit `/api/auth/me` $\rightarrow$ Returns authenticated user profile (name, avatar). -- [ ] **D. Public Post Preview**: Paste public VK post URL in `/giveaways/new` $\rightarrow$ Preview loads with `accessMode: "PUBLIC_SERVICE"`. -- [ ] **E. Private/Restricted Post Preview**: Paste post URL from closed group where organizer is member $\rightarrow$ Resolver falls back to `ORGANIZER_USER`. -- [ ] **F. Create Giveaway**: Submit giveaway form $\rightarrow$ Giveaway created with `organizerId: sessionUser.id`. -- [ ] **G. Import Participants**: Click Import Participants $\rightarrow$ Likes and comments fetched via `VkAuthContextResolver`. -- [ ] **H. Subscription Verification**: Run community subscription filter $\rightarrow$ Batch `groups.isMember` executed successfully. -- [ ] **I. Snapshot Locking**: Lock snapshot $\rightarrow$ Canonical hashes computed. -- [ ] **J. Deterministic Draw**: Execute draw $\rightarrow$ Winner selected via unbiased CSPRNG rejection sampling. -- [ ] **K. Public Audit**: Open `/api/giveaways/[id]/verify` in incognito window $\rightarrow$ Audit passes without authentication. -- [ ] **L. Token Expiry & Refresh**: Wait for access token expiry or simulate $\rightarrow$ Next API request automatically triggers server-side refresh without user interruption. -- [ ] **M. Logout**: Click logout $\rightarrow$ Session terminated, cookie destroyed. +The server reports the actual provider mode; previews/imports must report `LIVE` for these checks to pass. Production refuses `USE_VK_MOCK=true`, and the production Compose profile explicitly disables it. G/H require nonempty participants: an empty import cannot prove that subscription calls ran. + +The script prints `PASS`, `FAIL`, `SKIP`, or `MANUAL` for checks A–H and exits nonzero when any executed check fails. It keeps response bodies and HTTP headers in a private temporary directory and removes that directory on exit. It does not print profile or participant data. A successful run of F creates a real giveaway record; G imports participants and H reimports them with the subscription condition. Choose a known test post and community. + +## Checks + +- **A. OAuth start:** verifies that `/api/auth/vk/start` redirects to VK ID and requests PKCE S256. +- **B. Browser consent:** manual. Open `/api/auth/vk/start`, approve the consent screen, and confirm the browser returns to `/api/auth/vk/callback`. Confirm the app session is authenticated before copying the session cookie into the protected smoke file. The script never treats the browser step as passed. +- **C. Session:** confirms `/api/auth/me` returns an authenticated profile. +- **D. Public post:** checks authenticated preview for the configured public post and requires `effectiveCapabilities.accessMode` to equal `PUBLIC_SERVICE`. +- **E. Restricted post:** checks preview for the configured restricted post and requires the resolved auth mode to be `USER`. The chosen post must be visible to the organizer. +- **F. Giveaway creation:** creates a test giveaway from the successful public preview when explicitly enabled. +- **G. Participant import:** checks that likes and comments import successfully. +- **H. Subscription check:** reimports participants with `groups.isMember` for the configured group and accepts only a complete successful response. + +## Follow-up checks retained from the original checklist + +These checks are intentionally manual. A–H are the script's current automation boundary; later checks are never +reported as passed by the script. + +- **I. Snapshot locking:** lock the imported participant snapshot and verify the returned counts and hashes are present. Do not change or infer the deterministic proof/hash contract from this smoke run. +- **J. Deterministic draw:** run one draw from the locked snapshot and confirm the result is returned. The implementation's algorithm is outside this smoke checklist; do not use this as evidence to alter or revalidate the protected proof format. +- **K. Public audit:** open `/api/giveaways//verify` in a private browser window without a session and confirm the public verification response succeeds without exposing organizer or participant-private data. +- **L. Token refresh:** use an authorized test account after its access token expires, or a controlled test credential with a known expiry, and confirm the next authorized VK request either refreshes successfully or reports reauthentication. Do not force expiry by editing stored credentials on a live database. +- **M. Logout:** log out, confirm the session cookie is cleared, then confirm `/api/auth/me` returns unauthenticated. + +## Safe behavior for unsupported and unknown VK behavior + +- Community manager detection is disabled unless a complete, supported check is implemented. Choosing a manager-exclusion rule is rejected with an explanation; the app does not mark unknown users as non-managers. +- Repost eligibility remains unsupported. The app rejects a repost requirement and explains that it cannot verify it for private profiles. +- Pagination fails the import when an empty or short page arrives before VK's reported total, when the total is missing, or when the page limit is reached before the total. The import route saves participants only after all fetch and enrichment work succeeds, so a failed import does not replace the previous participant list. +- The current provider does not call VK `execute`; it fetches pages with ordinary methods. Its batching limits and behavior remain **UNVERIFIED** and are not inferred from third-party examples. +- VK can change data while an offset-based import is running. Completeness checks detect short or missing pages against the returned total but cannot prove a stable point-in-time set. That behavior remains **UNVERIFIED** pending official contract confirmation or a live test. + +No test credentials, cookies, or tokens belong in the repository. Use only a throwaway session cookie in the protected local file and delete that file after testing. diff --git a/package-lock.json b/package-lock.json index 937a63c..0aa17d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@prisma/client": "^5.20.0", "clsx": "^2.1.1", "lucide-react": "^0.453.0", - "next": "^16.3.2", + "next": "^16.3.8", "react": "^19.2.8", "react-dom": "^19.2.8", "tailwind-merge": "^2.5.4", @@ -23,7 +23,7 @@ "@types/react-dom": "^19.2.4", "autoprefixer": "^10.4.20", "eslint": "^9.20.0", - "eslint-config-next": "^16.3.2", + "eslint-config-next": "^16.3.8", "postcss": "^8.5.26", "prisma": "^5.20.0", "tailwindcss": "^3.4.13", @@ -988,9 +988,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", - "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -1006,13 +1006,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.2" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", - "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -1028,20 +1028,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.2" + "@img/sharp-libvips-darwin-x64": "1.3.4" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", - "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.3" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -1051,9 +1051,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", - "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -1067,9 +1067,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", - "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -1083,15 +1083,12 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", - "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1102,15 +1099,12 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", - "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1121,15 +1115,12 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", - "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1140,15 +1131,12 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", - "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1159,15 +1147,12 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", - "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1178,15 +1163,12 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", - "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1197,15 +1179,12 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", - "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1216,15 +1195,12 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", - "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1235,15 +1211,12 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", - "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1256,19 +1229,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.2" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", - "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1281,19 +1251,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.2" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", - "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1306,19 +1273,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.2" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", - "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1331,19 +1295,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.2" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", - "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1356,19 +1317,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.2" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", - "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1381,19 +1339,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.2" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", - "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1406,19 +1361,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", - "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1431,17 +1383,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", - "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.11.1" + "@emnapi/runtime": "^1.11.3" }, "engines": { "node": ">=20.9.0" @@ -1451,16 +1403,16 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", - "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.3" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -1470,9 +1422,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", - "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -1489,9 +1441,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", - "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -1508,9 +1460,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", - "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -1584,9 +1536,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1616,15 +1565,15 @@ } }, "node_modules/@next/env": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.2.tgz", - "integrity": "sha512-8k4YoG8cM7LWlkfzGNYCRBbFNlernLiMw4s0btVl+CmmWqn3VpYypA72/5Feb1UWdxe6tHqr5KHP4p4Y4m9luA==", + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.8.tgz", + "integrity": "sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.3.2.tgz", - "integrity": "sha512-z+HW1cZgt8QhByw8p2EbxF94AImgsKIYUbtSkA7Zld2T9yrKAlys4jNOcAOCtv6csX2CoA/5qCVyesL5pHmJ0A==", + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.3.8.tgz", + "integrity": "sha512-eCR9RcLTZrVS+K3+VOhi0xMdUUGNuriCHHhU1ZoABC/QQ/Gp0OWbLKQxV2/9DkmihYdYRcQWFmZXW8M1AMUXYw==", "dev": true, "license": "MIT", "dependencies": { @@ -1682,9 +1631,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.2.tgz", - "integrity": "sha512-ib5Llm93YCKoKWDh6ZaHq6QWTuOZ2bRkSnUwMmX8dsRIOkBNL1vVlSiUKSfixPL9SSh9pvukzqajk/klkn5vqg==", + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.8.tgz", + "integrity": "sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA==", "cpu": [ "arm64" ], @@ -1698,9 +1647,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.2.tgz", - "integrity": "sha512-qd98fX2+I5nYJDioW2o7nSjoxM5KvWdeDefM80igia4+C/qSIEhH4MhTE+hO/7qKM7W37/Mq+dOWp8UePSyLHw==", + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.8.tgz", + "integrity": "sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q==", "cpu": [ "x64" ], @@ -1714,15 +1663,12 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.2.tgz", - "integrity": "sha512-vqsgb6FAOzcrCccsLXiKtAy5t8EzO+uOazuFaSkQxeY0tNONG3vpHYy8pyBafcI5SNFPTeyard6yTr6SzNGo2A==", + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.8.tgz", + "integrity": "sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA==", "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1733,15 +1679,12 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.2.tgz", - "integrity": "sha512-xIe1eujfHUB2XcxHGddxJyu6TJRPjC5NpIkQYB/32ESkt5VkQyIAjmLRS38c+s6QY+qjtY/4KarVDzXRuD7lZQ==", + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.8.tgz", + "integrity": "sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg==", "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1752,15 +1695,12 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.2.tgz", - "integrity": "sha512-Fe0SA2j8X0kmc3aveuHD7UktO3AE2+mH3LguP60vGbz7u0z+MrDXbeb5iZFYAwR7EzzzXJ2Yk966w9mGTFMqfA==", + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.8.tgz", + "integrity": "sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ==", "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1771,15 +1711,12 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.2.tgz", - "integrity": "sha512-TFBipb+gyesI/2Ve4zVu7kGltBWN/R466G5/1gtt2lECfc22G1pjkTxu68Q9aFcOaXiRGTQfvDbQQFe7mYgxiQ==", + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.8.tgz", + "integrity": "sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA==", "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1790,9 +1727,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.2.tgz", - "integrity": "sha512-rVtmnNpBYIosDnKD/96dKxFsJnwnn1WRGG/HioSe8XCm2ksSHNrd2R6+hSjvTBxeMNhJ9pYeu/90cWB1nQLuNA==", + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.8.tgz", + "integrity": "sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw==", "cpu": [ "arm64" ], @@ -1806,9 +1743,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.2.tgz", - "integrity": "sha512-H4Y2o2/JcHu8LtwzD5CXfHhwxwz8gfsx2HXDEw46Mtev5xHnEmB7HNtZtmriw5ReUOjRtcDqo7XSbU01FT9NlA==", + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.8.tgz", + "integrity": "sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg==", "cpu": [ "x64" ], @@ -2029,9 +1966,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2046,9 +1980,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2063,9 +1994,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2080,9 +2008,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2097,9 +2022,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2114,9 +2036,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2131,9 +2050,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2148,9 +2064,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2165,9 +2078,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2182,9 +2092,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2199,9 +2106,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2216,9 +2120,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2233,9 +2134,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2766,9 +2664,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2783,9 +2678,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2800,9 +2692,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2817,9 +2706,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2834,9 +2720,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2851,9 +2734,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2868,9 +2748,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2885,9 +2762,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2902,9 +2776,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2919,9 +2790,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4387,13 +4255,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.3.2.tgz", - "integrity": "sha512-gTABOJmyc6pEgSX1Z1VOjBxkSmo5Hkdj+ePclDf8HLGTsnVWzgtDdrOeQrAnUkf0JNJJhwPuXrsIwmFZRKJLoQ==", + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.3.8.tgz", + "integrity": "sha512-81vovwMe6NGnoFsl0KUJWzlS+y239i3fdsxs49gSEX3pDQW2cuKy+fxUPXNkQqqx6OCPheYPtBlSufc9aZ0k+w==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.3.2", + "@next/eslint-plugin-next": "16.3.8", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -5034,6 +4902,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -6233,12 +6102,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/next/-/next-16.3.2.tgz", - "integrity": "sha512-/ZCaubUy17Lld1SiPWxuPbCk2ihqAxF2QNQaPZeEaEb7t1I58qhsJN187D7AfpapHAqUPXH0f/thtdW9dWgWFg==", + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/next/-/next-16.3.8.tgz", + "integrity": "sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw==", "license": "MIT", "dependencies": { - "@next/env": "16.3.2", + "@next/env": "16.3.8", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -6252,15 +6121,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.3.2", - "@next/swc-darwin-x64": "16.3.2", - "@next/swc-linux-arm64-gnu": "16.3.2", - "@next/swc-linux-arm64-musl": "16.3.2", - "@next/swc-linux-x64-gnu": "16.3.2", - "@next/swc-linux-x64-musl": "16.3.2", - "@next/swc-win32-arm64-msvc": "16.3.2", - "@next/swc-win32-x64-msvc": "16.3.2", - "sharp": "^0.35.3" + "@next/swc-darwin-arm64": "16.3.8", + "@next/swc-darwin-x64": "16.3.8", + "@next/swc-linux-arm64-gnu": "16.3.8", + "@next/swc-linux-arm64-musl": "16.3.8", + "@next/swc-linux-x64-gnu": "16.3.8", + "@next/swc-linux-x64-musl": "16.3.8", + "@next/swc-win32-arm64-msvc": "16.3.8", + "@next/swc-win32-x64-msvc": "16.3.8", + "sharp": "^0.35.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -7235,9 +7104,9 @@ } }, "node_modules/sharp": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", - "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -7252,31 +7121,31 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.3", - "@img/sharp-darwin-x64": "0.35.3", - "@img/sharp-freebsd-wasm32": "0.35.3", - "@img/sharp-libvips-darwin-arm64": "1.3.2", - "@img/sharp-libvips-darwin-x64": "1.3.2", - "@img/sharp-libvips-linux-arm": "1.3.2", - "@img/sharp-libvips-linux-arm64": "1.3.2", - "@img/sharp-libvips-linux-ppc64": "1.3.2", - "@img/sharp-libvips-linux-riscv64": "1.3.2", - "@img/sharp-libvips-linux-s390x": "1.3.2", - "@img/sharp-libvips-linux-x64": "1.3.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", - "@img/sharp-libvips-linuxmusl-x64": "1.3.2", - "@img/sharp-linux-arm": "0.35.3", - "@img/sharp-linux-arm64": "0.35.3", - "@img/sharp-linux-ppc64": "0.35.3", - "@img/sharp-linux-riscv64": "0.35.3", - "@img/sharp-linux-s390x": "0.35.3", - "@img/sharp-linux-x64": "0.35.3", - "@img/sharp-linuxmusl-arm64": "0.35.3", - "@img/sharp-linuxmusl-x64": "0.35.3", - "@img/sharp-webcontainers-wasm32": "0.35.3", - "@img/sharp-win32-arm64": "0.35.3", - "@img/sharp-win32-ia32": "0.35.3", - "@img/sharp-win32-x64": "0.35.3" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" }, "peerDependenciesMeta": { "@types/node": { @@ -7404,9 +7273,9 @@ "license": "ISC" }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" diff --git a/package.json b/package.json index 219c662..e861a53 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@prisma/client": "^5.20.0", "clsx": "^2.1.1", "lucide-react": "^0.453.0", - "next": "^16.3.2", + "next": "^16.3.8", "react": "^19.2.8", "react-dom": "^19.2.8", "tailwind-merge": "^2.5.4", @@ -31,7 +31,7 @@ "@types/react-dom": "^19.2.4", "autoprefixer": "^10.4.20", "eslint": "^9.20.0", - "eslint-config-next": "^16.3.2", + "eslint-config-next": "^16.3.8", "postcss": "^8.5.26", "prisma": "^5.20.0", "tailwindcss": "^3.4.13", diff --git a/prisma/migrations/20260818000000_initial/migration.sql b/prisma/migrations/20260818000000_initial/migration.sql new file mode 100644 index 0000000..ba314e5 --- /dev/null +++ b/prisma/migrations/20260818000000_initial/migration.sql @@ -0,0 +1,201 @@ +-- CreateEnum +CREATE TYPE "Platform" AS ENUM ('VK', 'TELEGRAM', 'YOUTUBE'); + +-- CreateEnum +CREATE TYPE "GiveawayStatus" AS ENUM ('DRAFT', 'FETCHING', 'READY', 'SNAPSHOT_LOCKED', 'DRAWN', 'PUBLISHED', 'CANCELLED'); + +-- CreateEnum +CREATE TYPE "ParticipantSource" AS ENUM ('LIKES', 'COMMENTS', 'REPOSTS', 'COMBINED'); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "vkUserId" TEXT NOT NULL, + "firstName" TEXT, + "lastName" TEXT, + "username" TEXT, + "avatarUrl" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserCredential" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "encryptedAccessToken" TEXT NOT NULL, + "encryptedRefreshToken" TEXT, + "expiresAt" TIMESTAMP(3), + "scope" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "UserCredential_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Giveaway" ( + "id" TEXT NOT NULL, + "platform" "Platform" NOT NULL DEFAULT 'VK', + "sourceUrl" TEXT NOT NULL, + "platformOwnerId" TEXT NOT NULL, + "platformPostId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "postImageUrl" TEXT, + "postLikesCount" INTEGER NOT NULL DEFAULT 0, + "postCommentsCount" INTEGER NOT NULL DEFAULT 0, + "postRepostsCount" INTEGER NOT NULL DEFAULT 0, + "status" "GiveawayStatus" NOT NULL DEFAULT 'DRAFT', + "filterRules" JSONB NOT NULL, + "winnersCount" INTEGER NOT NULL DEFAULT 1, + "reserveWinnersCount" INTEGER NOT NULL DEFAULT 0, + "seed" TEXT, + "organizerId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "drawnAt" TIMESTAMP(3), + + CONSTRAINT "Giveaway_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Participant" ( + "id" TEXT NOT NULL, + "giveawayId" TEXT NOT NULL, + "platformUserId" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "username" TEXT, + "avatarUrl" TEXT, + "source" "ParticipantSource" NOT NULL DEFAULT 'LIKES', + "liked" BOOLEAN NOT NULL DEFAULT false, + "commented" BOOLEAN NOT NULL DEFAULT false, + "commentsCount" INTEGER NOT NULL DEFAULT 0, + "reposted" BOOLEAN NOT NULL DEFAULT false, + "subscribed" BOOLEAN NOT NULL DEFAULT false, + "eligible" BOOLEAN NOT NULL DEFAULT true, + "exclusionReason" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Participant_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ParticipantSnapshot" ( + "id" TEXT NOT NULL, + "giveawayId" TEXT NOT NULL, + "version" INTEGER NOT NULL DEFAULT 1, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "eligibleParticipants" JSONB NOT NULL, + "filterRulesSnapshot" JSONB NOT NULL, + "participantCount" INTEGER NOT NULL, + "participantsSnapshotHash" TEXT NOT NULL, + "conditionsHash" TEXT NOT NULL, + + CONSTRAINT "ParticipantSnapshot_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DrawResult" ( + "id" TEXT NOT NULL, + "drawId" TEXT NOT NULL, + "giveawayId" TEXT NOT NULL, + "snapshotId" TEXT NOT NULL, + "winners" JSONB NOT NULL, + "reserveWinners" JSONB NOT NULL, + "winnerIds" JSONB NOT NULL, + "reserveWinnerIds" JSONB NOT NULL, + "totalEligibleCount" INTEGER NOT NULL, + "totalLoadedCount" INTEGER NOT NULL, + "seedUsed" TEXT NOT NULL, + "algorithmVersion" TEXT NOT NULL DEFAULT 'HMAC_SHA256_FY_V1', + "deterministicProofHash" TEXT NOT NULL, + "auditEventHash" TEXT NOT NULL, + "drawnAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "DrawResult_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AuditRecord" ( + "id" TEXT NOT NULL, + "giveawayId" TEXT NOT NULL, + "snapshotId" TEXT NOT NULL, + "algorithmVersion" TEXT NOT NULL DEFAULT 'HMAC_SHA256_FY_V1', + "seed" TEXT NOT NULL, + "participantsSnapshotHash" TEXT NOT NULL, + "conditionsHash" TEXT NOT NULL, + "deterministicProofHash" TEXT NOT NULL, + "auditEventHash" TEXT NOT NULL, + "winnerIds" JSONB NOT NULL, + "reserveWinnerIds" JSONB NOT NULL, + "eligibleCount" INTEGER NOT NULL, + "drawId" TEXT NOT NULL, + "drawnAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "verifiedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AuditRecord_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_vkUserId_key" ON "User"("vkUserId"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserCredential_userId_key" ON "UserCredential"("userId"); + +-- CreateIndex +CREATE INDEX "Giveaway_platform_platformOwnerId_platformPostId_idx" ON "Giveaway"("platform", "platformOwnerId", "platformPostId"); + +-- CreateIndex +CREATE INDEX "Giveaway_organizerId_idx" ON "Giveaway"("organizerId"); + +-- CreateIndex +CREATE INDEX "Participant_giveawayId_eligible_idx" ON "Participant"("giveawayId", "eligible"); + +-- CreateIndex +CREATE UNIQUE INDEX "Participant_giveawayId_platformUserId_key" ON "Participant"("giveawayId", "platformUserId"); + +-- CreateIndex +CREATE INDEX "ParticipantSnapshot_giveawayId_idx" ON "ParticipantSnapshot"("giveawayId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ParticipantSnapshot_giveawayId_version_key" ON "ParticipantSnapshot"("giveawayId", "version"); + +-- CreateIndex +CREATE UNIQUE INDEX "DrawResult_giveawayId_key" ON "DrawResult"("giveawayId"); + +-- CreateIndex +CREATE UNIQUE INDEX "DrawResult_snapshotId_key" ON "DrawResult"("snapshotId"); + +-- CreateIndex +CREATE UNIQUE INDEX "AuditRecord_giveawayId_key" ON "AuditRecord"("giveawayId"); + +-- CreateIndex +CREATE UNIQUE INDEX "AuditRecord_snapshotId_key" ON "AuditRecord"("snapshotId"); + +-- AddForeignKey +ALTER TABLE "UserCredential" ADD CONSTRAINT "UserCredential_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Giveaway" ADD CONSTRAINT "Giveaway_organizerId_fkey" FOREIGN KEY ("organizerId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Participant" ADD CONSTRAINT "Participant_giveawayId_fkey" FOREIGN KEY ("giveawayId") REFERENCES "Giveaway"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ParticipantSnapshot" ADD CONSTRAINT "ParticipantSnapshot_giveawayId_fkey" FOREIGN KEY ("giveawayId") REFERENCES "Giveaway"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DrawResult" ADD CONSTRAINT "DrawResult_giveawayId_fkey" FOREIGN KEY ("giveawayId") REFERENCES "Giveaway"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DrawResult" ADD CONSTRAINT "DrawResult_snapshotId_fkey" FOREIGN KEY ("snapshotId") REFERENCES "ParticipantSnapshot"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AuditRecord" ADD CONSTRAINT "AuditRecord_giveawayId_fkey" FOREIGN KEY ("giveawayId") REFERENCES "Giveaway"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AuditRecord" ADD CONSTRAINT "AuditRecord_snapshotId_fkey" FOREIGN KEY ("snapshotId") REFERENCES "ParticipantSnapshot"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..2fe25d8 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1 @@ +provider = "postgresql" diff --git a/src/app/api/giveaways/[id]/participants/route.ts b/src/app/api/giveaways/[id]/participants/route.ts index f5b2140..03cec15 100644 --- a/src/app/api/giveaways/[id]/participants/route.ts +++ b/src/app/api/giveaways/[id]/participants/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { GiveawayStore } from '@/lib/giveaway-store'; import { ProviderFactory } from '@/providers/factory'; +import { VkProvider } from '@/providers/vk/vk-provider'; import { executeParticipantPipeline } from '@/core/pipeline/participant-enricher'; import { fetchParticipantsSchema, validateProviderCapabilities } from '@/core/validation/giveaway-schemas'; import { handleApiError } from '@/core/errors/http-errors'; @@ -99,6 +100,7 @@ export async function POST( const responseBody = { success: true, giveawayId: updated.id, + providerMode: provider instanceof VkProvider ? 'LIVE' : 'MOCK', totalCount: allParticipants.length, eligibleCount: eligibleParticipants.length, excludedCount: excludedParticipants.length, diff --git a/src/app/api/posts/preview/route.ts b/src/app/api/posts/preview/route.ts index e630c79..41f80a9 100644 --- a/src/app/api/posts/preview/route.ts +++ b/src/app/api/posts/preview/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { ProviderFactory } from '@/providers/factory'; +import { VkProvider } from '@/providers/vk/vk-provider'; import { postPreviewSchema } from '@/core/validation/giveaway-schemas'; import { handleApiError } from '@/core/errors/http-errors'; import { generalApiRateLimiter } from '@/lib/rate-limiter'; @@ -31,6 +32,7 @@ export async function POST(req: NextRequest) { return NextResponse.json({ success: true, post, + providerMode: provider instanceof VkProvider ? 'LIVE' : 'MOCK', effectiveCapabilities, }); } catch (error: any) { diff --git a/src/app/giveaways/new/page.tsx b/src/app/giveaways/new/page.tsx index fb0f1db..3ea0287 100644 --- a/src/app/giveaways/new/page.tsx +++ b/src/app/giveaways/new/page.tsx @@ -597,7 +597,7 @@ export default function NewGiveawayWizardPage() {

- Не поддерживается VK API для закрытых профилей сторонними приложениями + Проверка не подключена. Репосты закрытых профилей нельзя подтвердить, поэтому это условие недоступно.

@@ -619,7 +619,7 @@ export default function NewGiveawayWizardPage() {

- Требует авторизации организатора через VK ID для доступа к списку контактов + Список руководителей сообщества не загружается. Администраторы не исключаются автоматически.

diff --git a/src/core/errors/http-errors.ts b/src/core/errors/http-errors.ts index 8bd9d13..5447743 100644 --- a/src/core/errors/http-errors.ts +++ b/src/core/errors/http-errors.ts @@ -209,6 +209,19 @@ export function handleApiError(error: unknown): NextResponse { { status: 504 } ); + case 'INCOMPLETE_DATA': + case 'PAGINATION_LIMIT_REACHED': + return NextResponse.json( + { + success: false, + error: { + code: 'VK_INCOMPLETE_DATA', + message: error.message, + }, + }, + { status: 502 } + ); + default: return NextResponse.json( { diff --git a/src/core/randomizer/hasher.ts b/src/core/randomizer/hasher.ts index 2acb1cd..255ce49 100644 --- a/src/core/randomizer/hasher.ts +++ b/src/core/randomizer/hasher.ts @@ -15,11 +15,11 @@ export { }; /** - * Generates a cryptographically secure random seed (128-bit / 32 hex chars) using CSPRNG. + * Generates a cryptographically secure random seed (256-bit / 64 hex chars) using CSPRNG. * Math.random() is strictly forbidden in security-sensitive giveaway workflows. */ export function generateCryptoSecureSeed(): string { - return randomBytes(16).toString('hex'); + return randomBytes(32).toString('hex'); } /** diff --git a/src/core/validation/giveaway-schemas.ts b/src/core/validation/giveaway-schemas.ts index 2816da0..f2993fb 100644 --- a/src/core/validation/giveaway-schemas.ts +++ b/src/core/validation/giveaway-schemas.ts @@ -74,7 +74,7 @@ export function validateProviderCapabilities( ): void { if (rules.requireRepost && !capabilities.reposts) { throw new ValidationError( - 'Repost verification is not supported due to VK API privacy limitations', + `Repost verification is unavailable: ${capabilities.repostsNote || 'the requirement cannot be evaluated safely.'}`, { condition: 'requireRepost' } ); } @@ -88,7 +88,7 @@ export function validateProviderCapabilities( if (rules.excludeAdmins && !capabilities.adminDetection) { throw new ValidationError( - 'Admin detection requires VK ID organizer authorization', + `Admin detection is unavailable: ${capabilities.adminDetectionNote || 'community managers cannot be excluded safely.'}`, { condition: 'excludeAdmins' } ); } diff --git a/src/integrations/vk/vk-client.ts b/src/integrations/vk/vk-client.ts index 93df967..63aac5c 100644 --- a/src/integrations/vk/vk-client.ts +++ b/src/integrations/vk/vk-client.ts @@ -11,7 +11,8 @@ import { VkCancelledError, VkNetworkError, VkValidationError, - VkPaginationLimitError + VkPaginationLimitError, + VkIncompleteDataError } from './vk-errors'; import { validateAuthContext } from './vk-auth'; import { IVkRateLimiter, defaultVkRateLimiter } from './vk-rate-limit'; @@ -219,7 +220,6 @@ export async function fetchPaginatedVk( ): Promise { const pageSize = options.pageSize ?? 100; const maxPages = options.maxPages ?? 10000; - const throwOnTruncation = options.throwOnTruncation ?? true; const allItems: TItem[] = []; let offset = 0; let page = 0; @@ -232,10 +232,38 @@ export async function fetchPaginatedVk( const { items, totalCount } = await options.fetchPage(offset, pageSize, options.signal); if (totalCount !== undefined) { + if (!Number.isSafeInteger(totalCount) || totalCount < 0) { + throw new VkIncompleteDataError('VK returned an invalid pagination total.', { + details: { loaded: allItems.length, page }, + }); + } + if (recordedTotalCount !== undefined && totalCount !== recordedTotalCount) { + throw new VkIncompleteDataError( + `VK pagination total changed during import (${recordedTotalCount} to ${totalCount}); no complete result can be confirmed.`, + { details: { loaded: allItems.length, previousTotal: recordedTotalCount, total: totalCount, page } } + ); + } recordedTotalCount = totalCount; } + if (!Array.isArray(items) || items.length > pageSize) { + throw new VkIncompleteDataError('VK returned an invalid page size; no complete result can be confirmed.', { + details: { loaded: allItems.length, received: Array.isArray(items) ? items.length : null, pageSize, page }, + }); + } + if (recordedTotalCount !== undefined && allItems.length + items.length > recordedTotalCount) { + throw new VkIncompleteDataError('VK returned more items than its reported pagination total.', { + details: { loaded: allItems.length, received: items.length, total: recordedTotalCount, page }, + }); + } + if (!items || items.length === 0) { + if (recordedTotalCount === undefined || allItems.length < recordedTotalCount) { + throw new VkIncompleteDataError( + `VK pagination ended at ${allItems.length} items before a complete result could be confirmed.`, + { details: { loaded: allItems.length, total: recordedTotalCount ?? null, page } } + ); + } break; } @@ -252,22 +280,22 @@ export async function fetchPaginatedVk( } if (items.length < pageSize) { + if (recordedTotalCount === undefined || allItems.length < recordedTotalCount) { + throw new VkIncompleteDataError( + `VK returned a short page at ${allItems.length} items before its reported total was reached.`, + { details: { loaded: allItems.length, total: recordedTotalCount ?? null, page } } + ); + } break; } } // Safety check against silent truncation - if ( - page >= maxPages && - recordedTotalCount !== undefined && - allItems.length < recordedTotalCount - ) { - if (throwOnTruncation) { - throw new VkPaginationLimitError( - `VK pagination reached maxPages safety ceiling (${maxPages} pages) with only ${allItems.length}/${recordedTotalCount} items loaded. Truncation detected.`, - { details: { loaded: allItems.length, total: recordedTotalCount, maxPages } } - ); - } + if (page >= maxPages && (recordedTotalCount === undefined || allItems.length < recordedTotalCount)) { + throw new VkPaginationLimitError( + `VK pagination reached maxPages safety ceiling (${maxPages}) before a complete result was confirmed.`, + { details: { loaded: allItems.length, total: recordedTotalCount ?? null, maxPages } } + ); } return allItems; diff --git a/src/integrations/vk/vk-errors.ts b/src/integrations/vk/vk-errors.ts index b430f89..499f36c 100644 --- a/src/integrations/vk/vk-errors.ts +++ b/src/integrations/vk/vk-errors.ts @@ -117,6 +117,12 @@ export class VkPaginationLimitError extends VkClientError { readonly category = 'PAGINATION_LIMIT_REACHED'; } +/** VK stopped returning pages before its reported total was collected. */ +export class VkIncompleteDataError extends VkClientError { + readonly isRetryable = false; + readonly category = 'INCOMPLETE_DATA'; +} + // Alias for backwards compatibility export const VkPaginationTruncatedError = VkPaginationLimitError; diff --git a/src/lib/auth/app-config.ts b/src/lib/auth/app-config.ts index 1a9a5de..8443fb7 100644 --- a/src/lib/auth/app-config.ts +++ b/src/lib/auth/app-config.ts @@ -1,55 +1,60 @@ -/** - * Application environment configuration and trusted origin resolver. - * Enforces strict fail-fast validation in production for OAuth base URLs. - */ +/** Canonical origins are server configuration; request headers never choose them. */ +function parseConfiguredUrl(value: string, name: string): URL { + let url: URL; + try { url = new URL(value); } catch { throw new Error(`FATAL CONFIGURATION ERROR: ${name} must be a valid URL.`); } + if (url.username || url.password || url.search || url.hash) { + throw new Error(`FATAL CONFIGURATION ERROR: ${name} cannot contain credentials, query or fragment.`); + } + return url; +} + +export function isLocalSingleUserProfile(): boolean { + if (process.env.DEPLOYMENT_PROFILE !== 'local-single-user') return false; + const value = process.env.APP_BASE_URL?.trim(); + if (!value) throw new Error('FATAL CONFIGURATION ERROR: APP_BASE_URL is required for local-single-user.'); + const url = parseConfiguredUrl(value, 'APP_BASE_URL'); + if (!['localhost', '127.0.0.1', '[::1]'].includes(url.hostname) || url.protocol !== 'http:' || url.pathname !== '/') { + throw new Error('FATAL CONFIGURATION ERROR: local-single-user requires an HTTP loopback origin.'); + } + return true; +} export function getAppBaseUrl(): string { const envUrl = process.env.APP_BASE_URL?.trim(); - if (process.env.NODE_ENV === 'production') { - if (!envUrl) { - throw new Error( - 'FATAL CONFIGURATION ERROR: APP_BASE_URL environment variable is strictly required in production.' - ); + if (!envUrl) throw new Error('FATAL CONFIGURATION ERROR: APP_BASE_URL environment variable is strictly required in production.'); + const url = parseConfiguredUrl(envUrl, 'APP_BASE_URL'); + if (url.pathname !== '/' || (url.protocol !== 'https:' && !isLocalSingleUserProfile())) { + throw new Error('FATAL CONFIGURATION ERROR: APP_BASE_URL must be a valid HTTPS URL in production.'); } - if (!envUrl.startsWith('https://')) { - throw new Error( - 'FATAL CONFIGURATION ERROR: APP_BASE_URL must be a valid HTTPS URL in production.' - ); - } - return envUrl.replace(/\/+$/, ''); + // Validate explicitly selected local profile even when URL happens to use HTTPS. + if (process.env.DEPLOYMENT_PROFILE === 'local-single-user') isLocalSingleUserProfile(); + return url.origin; } - - // Development/Test environment fallback return (envUrl || 'http://localhost:3000').replace(/\/+$/, ''); } export function getVkRedirectUri(): string { const envUri = process.env.VK_REDIRECT_URI?.trim(); - if (process.env.NODE_ENV === 'production') { - if (!envUri) { - throw new Error( - 'FATAL CONFIGURATION ERROR: VK_REDIRECT_URI environment variable is strictly required in production.' - ); + if (!envUri) throw new Error('FATAL CONFIGURATION ERROR: VK_REDIRECT_URI environment variable is strictly required in production.'); + const url = parseConfiguredUrl(envUri, 'VK_REDIRECT_URI'); + const base = getAppBaseUrl(); + if (url.origin !== base || url.pathname !== '/api/auth/vk/callback') { + throw new Error('FATAL CONFIGURATION ERROR: VK_REDIRECT_URI must match the canonical origin and callback path.'); } - if (!envUri.startsWith('https://')) { - throw new Error( - 'FATAL CONFIGURATION ERROR: VK_REDIRECT_URI must be a valid HTTPS URL in production.' - ); - } - return envUri; + return url.href; } - - // Development/Test environment fallback return envUri || `${getAppBaseUrl()}/api/auth/vk/callback`; } -export function getTrustedHost(): string { - try { - const url = new URL(getAppBaseUrl()); - return url.host.toLowerCase(); - } catch { - return 'localhost:3000'; - } +export function getSecureSessionCookieFlag(): boolean { + if (process.env.NODE_ENV !== 'production') return false; + if (process.env.DEPLOYMENT_PROFILE === 'local-single-user') return !isLocalSingleUserProfile(); + return true; +} + +export function getTrustedHost(): string { + // Production configuration failures must never fall back to a different trust boundary. + return new URL(getAppBaseUrl()).host.toLowerCase(); } diff --git a/src/lib/auth/csrf-guard.ts b/src/lib/auth/csrf-guard.ts index e084482..dcabe6b 100644 --- a/src/lib/auth/csrf-guard.ts +++ b/src/lib/auth/csrf-guard.ts @@ -1,6 +1,6 @@ import { NextRequest } from 'next/server'; import { ForbiddenError } from '@/core/errors/http-errors'; -import { getTrustedHost } from './app-config'; +import { getAppBaseUrl, getTrustedHost } from './app-config'; /** * Validates Origin and Referer headers for cookie-authenticated mutating requests (POST/PUT/DELETE/PATCH) @@ -30,7 +30,8 @@ export function validateCsrfOrigin(req: NextRequest): void { if (origin) { try { const originUrl = new URL(origin); - if (originUrl.host.toLowerCase() !== trustedHost.toLowerCase()) { + if (originUrl.host.toLowerCase() !== trustedHost.toLowerCase() || + (process.env.NODE_ENV === 'production' && originUrl.origin !== getAppBaseUrl())) { throw new ForbiddenError(`CSRF origin mismatch: origin "${originUrl.host}" does not match trusted host "${trustedHost}"`); } } catch (e: any) { @@ -43,7 +44,8 @@ export function validateCsrfOrigin(req: NextRequest): void { if (referer) { try { const refererUrl = new URL(referer); - if (refererUrl.host.toLowerCase() !== trustedHost.toLowerCase()) { + if (refererUrl.host.toLowerCase() !== trustedHost.toLowerCase() || + (process.env.NODE_ENV === 'production' && refererUrl.origin !== getAppBaseUrl())) { throw new ForbiddenError(`CSRF referer mismatch: referer "${refererUrl.host}" does not match trusted host "${trustedHost}"`); } } catch (e: any) { diff --git a/src/lib/auth/session.ts b/src/lib/auth/session.ts index 00a0663..fc02770 100644 --- a/src/lib/auth/session.ts +++ b/src/lib/auth/session.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { randomBytes } from 'crypto'; import { prisma } from '@/lib/prisma'; +import { getSecureSessionCookieFlag } from './app-config'; export const SESSION_COOKIE_NAME = 'randomayzer_session'; export const SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60; // 30 days @@ -269,12 +270,11 @@ export async function getSessionFromRequest( * Sets session cookie on a NextResponse */ export function setSessionCookie(res: NextResponse, sessionId: string): void { - const isProd = process.env.NODE_ENV === 'production'; res.cookies.set({ name: SESSION_COOKIE_NAME, value: sessionId, httpOnly: true, - secure: isProd, + secure: getSecureSessionCookieFlag(), sameSite: 'lax', path: '/', maxAge: SESSION_MAX_AGE_SECONDS, @@ -289,7 +289,7 @@ export function clearSessionCookie(res: NextResponse): void { name: SESSION_COOKIE_NAME, value: '', httpOnly: true, - secure: process.env.NODE_ENV === 'production', + secure: getSecureSessionCookieFlag(), sameSite: 'lax', path: '/', maxAge: 0, diff --git a/src/lib/repository/prisma-repository.ts b/src/lib/repository/prisma-repository.ts index 9f28bd5..6148996 100644 --- a/src/lib/repository/prisma-repository.ts +++ b/src/lib/repository/prisma-repository.ts @@ -536,6 +536,9 @@ export class PrismaGiveawayRepository implements IGiveawayRepository { const current = await this.getGiveawayById(id); if (!current) throw new NotFoundError(`Giveaway with id "${id}" not found`); + if (current.status === 'DRAWN' || current.status === 'PUBLISHED') { + throw new ConflictError('Giveaway has already been drawn. Duplicate draw is strictly forbidden.'); + } GiveawayFSM.assertCanDraw(current.status); try { diff --git a/src/providers/factory.ts b/src/providers/factory.ts index 68e8981..be805bd 100644 --- a/src/providers/factory.ts +++ b/src/providers/factory.ts @@ -8,6 +8,9 @@ export class ProviderFactory { public static getVkProvider(): SocialMediaProvider { // 1. Explicit mock configuration if (process.env.USE_VK_MOCK === 'true') { + if (process.env.NODE_ENV === 'production') { + throw new DependencyUnavailableError('VK mock provider is forbidden in production. Remove USE_VK_MOCK.'); + } return new VkMockProvider(); } diff --git a/src/providers/vk/vk-capabilities.ts b/src/providers/vk/vk-capabilities.ts index 73a1405..49296ce 100644 --- a/src/providers/vk/vk-capabilities.ts +++ b/src/providers/vk/vk-capabilities.ts @@ -13,10 +13,10 @@ export const STATIC_VK_CAPABILITIES: ProviderCapabilities = { likes: true, comments: true, reposts: false, - repostsNote: 'Сбор репостов ограничен политикой приватности VK для закрытых профилей', + repostsNote: 'Проверка репостов не подключена; данные о репостах закрытых профилей нельзя подтвердить.', subscriptions: true, adminDetection: false, - adminDetectionNote: 'Требует расширенных прав администратора сообщества', + adminDetectionNote: 'Список руководителей сообщества не загружается; администраторы не исключаются из розыгрыша.', }; /** @@ -39,7 +39,7 @@ export function resolveEffectiveCapabilities( adminDetection: isCommunityAdmin, adminDetectionNote: isCommunityAdmin ? undefined - : 'Требует прямого подключения токена сообщества с правами администратора', + : 'Список руководителей сообщества не загружается без отдельного доступа.', accessMode, ...(credentialStatus ? { credentialStatus } : {}), }; diff --git a/src/providers/vk/vk-provider.ts b/src/providers/vk/vk-provider.ts index 246f78f..31410a7 100644 --- a/src/providers/vk/vk-provider.ts +++ b/src/providers/vk/vk-provider.ts @@ -17,11 +17,48 @@ import { VkNotFoundError, VkAuthError, VkPrivateResourceError, - VkPermissionError + VkPermissionError, + VkIncompleteDataError } from '@/integrations/vk/vk-errors'; import { STATIC_VK_CAPABILITIES } from './vk-capabilities'; import { VkAuthContextResolver, defaultVkAuthContextResolver } from '@/integrations/vk/vk-auth-resolver'; +function incompleteVkItems(method: string, reason: string): VkIncompleteDataError { + return new VkIncompleteDataError(`VK ${method} returned incomplete participant data: ${reason}. No result was saved.`, { method }); +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function validateProfile(value: unknown, method: string): VkUserProfile { + if (!isRecord(value) || !Number.isSafeInteger(value.id) || (value.id as number) <= 0) { + throw incompleteVkItems(method, 'profile has an invalid user ID'); + } + for (const key of ['first_name', 'last_name', 'screen_name', 'photo_50', 'photo_100', 'photo_200', 'deactivated']) { + if (value[key] !== undefined && typeof value[key] !== 'string') { + throw incompleteVkItems(method, `profile has an invalid ${key}`); + } + } + if (value.is_closed !== undefined && typeof value.is_closed !== 'boolean') { + throw incompleteVkItems(method, 'profile has an invalid is_closed value'); + } + return value as unknown as VkUserProfile; +} + +function validateResponseItems(value: unknown, method: string): Record[] { + if (!isRecord(value) || !Array.isArray(value.items)) { + throw incompleteVkItems(method, 'response items are missing or invalid'); + } + if (!Number.isSafeInteger(value.count) || (value.count as number) < 0) { + throw incompleteVkItems(method, 'response count is missing or invalid'); + } + for (const item of value.items) { + if (!isRecord(item)) throw incompleteVkItems(method, 'response contains a malformed item'); + } + return value.items as Record[]; +} + export interface ExtendedFetchParticipantsParams extends FetchParticipantsParams { authContext?: VkAuthContext; organizerId?: string; @@ -243,6 +280,7 @@ export class VkProvider implements SocialMediaProvider { // 1. Fetch Likes with full pagination if (params.includeLikes !== false) { + const seenLikeIds = new Set(); const likeProfiles = await fetchPaginatedVk({ pageSize: 100, fetchPage: async (offset, count) => { @@ -256,10 +294,13 @@ export class VkProvider implements SocialMediaProvider { offset, }, authContext); - return { - items: (res.items || []) as VkUserProfile[], - totalCount: res.count, - }; + const items = validateResponseItems(res, 'likes.getList'); + const profiles = items.map(item => validateProfile(item, 'likes.getList')); + for (const profile of profiles) { + if (seenLikeIds.has(profile.id)) throw incompleteVkItems('likes.getList', 'duplicate user ID'); + seenLikeIds.add(profile.id); + } + return { items: profiles, totalCount: res.count }; }, onProgress: (loaded, total) => { if (params.onProgress) { @@ -300,14 +341,26 @@ export class VkProvider implements SocialMediaProvider { fields: 'photo_100,photo_200,screen_name', }, authContext); - const profileMap = new Map( - (res.profiles || []).map(p => [p.id, p]) - ); + const items = validateResponseItems(res, 'wall.getComments'); + const profileMap = new Map(); + if (res.profiles !== undefined) { + if (!Array.isArray(res.profiles)) throw incompleteVkItems('wall.getComments', 'profiles are malformed'); + for (const rawProfile of res.profiles) { + const profile = validateProfile(rawProfile, 'wall.getComments'); + if (profileMap.has(profile.id)) throw incompleteVkItems('wall.getComments', 'duplicate profile user ID'); + profileMap.set(profile.id, profile); + } + } - for (const item of res.items || []) { - if (item.from_id && item.from_id > 0) { - const userId = String(item.from_id); - const prof = profileMap.get(item.from_id); + for (const item of items) { + const fromId = item.from_id; + if (!Number.isSafeInteger(fromId) || fromId === 0) { + throw incompleteVkItems('wall.getComments', 'comment has an invalid author ID'); + } + // VK group/community authors have negative IDs and are intentionally not participants. + if ((fromId as number) > 0) { + const userId = String(fromId); + const prof = profileMap.get(fromId as number); const existing = participantsMap.get(userId); if (existing) { @@ -332,7 +385,7 @@ export class VkProvider implements SocialMediaProvider { } return { - items: res.items || [], + items: items as unknown as { from_id: number }[], totalCount: res.count, }; }, @@ -381,8 +434,31 @@ export class VkProvider implements SocialMediaProvider { activeAuth ); - for (const item of res || []) { - resultMap.set(String(item.user_id), item.member === 1); + if (!Array.isArray(res)) { + throw new VkIncompleteDataError( + 'VK subscription check returned an invalid response shape. No result was saved.', + { method: 'groups.isMember', details: { requested: chunk.length } } + ); + } + const chunkStatuses = new Set(); + for (const item of res) { + const userId = String(item?.user_id ?? ''); + if (!/^\d+$/.test(userId) || !chunk.includes(userId) || chunkStatuses.has(userId) || (item.member !== 0 && item.member !== 1)) { + throw new VkIncompleteDataError( + 'VK subscription check returned an invalid or unexpected user status. No result was saved.', + { method: 'groups.isMember', details: { requested: chunk.length, received: chunkStatuses.size } } + ); + } + chunkStatuses.add(userId); + resultMap.set(userId, item.member === 1); + } + + const missingStatuses = chunk.filter(userId => !chunkStatuses.has(String(userId))); + if (missingStatuses.length > 0) { + throw new VkIncompleteDataError( + `VK subscription check returned status for ${chunk.length - missingStatuses.length}/${chunk.length} users. No result was saved.`, + { method: 'groups.isMember', details: { requested: chunk.length, received: chunk.length - missingStatuses.length } } + ); } } diff --git a/tests/deployment-profile.test.ts b/tests/deployment-profile.test.ts new file mode 100644 index 0000000..0085ecd --- /dev/null +++ b/tests/deployment-profile.test.ts @@ -0,0 +1,56 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { NextRequest, NextResponse } from 'next/server'; +import { getAppBaseUrl, getVkRedirectUri } from '../src/lib/auth/app-config'; +import { setSessionCookie, clearSessionCookie } from '../src/lib/auth/session'; +import { validateCsrfOrigin } from '../src/lib/auth/csrf-guard'; + +afterEach(() => vi.unstubAllEnvs()); +function config(base: string, profile = 'local-single-user') { + vi.stubEnv('NODE_ENV', 'production'); + vi.stubEnv('DEPLOYMENT_PROFILE', profile); + vi.stubEnv('APP_BASE_URL', base); + vi.stubEnv('VK_REDIRECT_URI', `${base}/api/auth/vk/callback`); +} +describe('production deployment profiles', () => { + it('allows HTTP only in the explicit loopback profile and uses HttpOnly Lax cookies', () => { + config('http://localhost:3100'); + expect(getAppBaseUrl()).toBe('http://localhost:3100'); + expect(getVkRedirectUri()).toBe('http://localhost:3100/api/auth/vk/callback'); + const res = NextResponse.json({}); + setSessionCookie(res, 'fake-session'); + expect(res.headers.get('set-cookie')).toContain('HttpOnly'); + expect(res.headers.get('set-cookie')).toContain('SameSite=lax'); + expect(res.headers.get('set-cookie')).not.toContain('Secure'); + clearSessionCookie(res); + expect(res.headers.get('set-cookie')).not.toContain('Secure'); + }); + it.each(['http://evil.test', 'http://localhost.evil.test', 'http://192.168.1.81:3100', 'https://localhost:3100', 'http://localhost:3100/path'])('rejects invalid local origin %s', (base) => { + config(base); + expect(() => getAppBaseUrl()).toThrow(); + }); + it('keeps strict HTTPS and Secure cookies outside the local profile', () => { + config('http://localhost:3100', 'strict'); + expect(() => getAppBaseUrl()).toThrow(); + config('https://random.ochenstarik.ru', 'strict'); + expect(getAppBaseUrl()).toBe('https://random.ochenstarik.ru'); + const res = NextResponse.json({}); + setSessionCookie(res, 'fake-session'); + expect(res.headers.get('set-cookie')).toContain('Secure'); + }); + it('rejects redirect origin and path mismatch', () => { + config('http://localhost:3100'); + vi.stubEnv('VK_REDIRECT_URI', 'http://localhost:3101/api/auth/vk/callback'); + expect(() => getVkRedirectUri()).toThrow(); + vi.stubEnv('VK_REDIRECT_URI', 'http://localhost:3100/other'); + expect(() => getVkRedirectUri()).toThrow(); + }); + it('ignores forged Host/forwarded headers and rejects scheme downgrade for CSRF', () => { + config('https://random.ochenstarik.ru', 'strict'); + const request = (origin: string) => new NextRequest('https://random.ochenstarik.ru/api/giveaways', { + method: 'POST', headers: { origin, host: 'evil.test', 'x-forwarded-host': 'evil.test' }, + }); + expect(() => validateCsrfOrigin(request('https://evil.test'))).toThrow(); + expect(() => validateCsrfOrigin(request('http://random.ochenstarik.ru'))).toThrow(); + expect(() => validateCsrfOrigin(request('https://random.ochenstarik.ru'))).not.toThrow(); + }); +}); diff --git a/tests/production-transport.test.ts b/tests/production-transport.test.ts new file mode 100644 index 0000000..b6dd0e1 --- /dev/null +++ b/tests/production-transport.test.ts @@ -0,0 +1,54 @@ +import { createRequire } from 'node:module'; +import { createServer, request } from 'node:http'; +import { describe, expect, it } from 'vitest'; +const require = createRequire(import.meta.url); +const { sanitizeProxyHeaders } = require('../tools/production-transport.cjs'); +const origin = new URL('https://random.ochenstarik.ru'); +function fake(peer: string) { + return { socket: { remoteAddress: peer }, headers: { + host: 'evil.test', 'x-forwarded-for': '203.0.113.8', 'x-forwarded-proto': 'https', + 'x-forwarded-host': 'evil.test', 'x-real-ip': '203.0.113.9', forwarded: 'for=evil', + 'cf-connecting-ip': '203.0.113.10', 'x-forwarded-unknown': 'evil', + } as Record }; +} +describe('production socket trust boundary', () => { + it.each(['192.168.1.2', '10.77.0.2', '127.0.0.1', '::ffff:192.168.1.2'])('ignores all forwarded headers from foreign peer %s', (peer) => { + const req = fake(peer); + sanitizeProxyHeaders(req, { trustedProxyIp: '10.77.0.1', origin }); + expect(req.headers['x-forwarded-for']).toBe(peer.replace('::ffff:', '')); + expect(req.headers['x-forwarded-proto']).toBe('http'); + expect(req.headers.host).toBe(origin.host); + expect(req.headers.forwarded).toBeUndefined(); + expect(req.headers['x-real-ip']).toBeUndefined(); + expect(req.headers['cf-connecting-ip']).toBeUndefined(); + expect(req.headers['x-forwarded-unknown']).toBeUndefined(); + }); + it('accepts one overwritten client IP only from the VPS socket peer', () => { + const req = fake('::ffff:10.77.0.1'); + sanitizeProxyHeaders(req, { trustedProxyIp: '10.77.0.1', origin }); + expect(req.headers['x-forwarded-for']).toBe('203.0.113.8'); + expect(req.headers['x-forwarded-proto']).toBe('https'); + req.headers['x-forwarded-for'] = '203.0.113.8, 192.0.2.3'; + sanitizeProxyHeaders(req, { trustedProxyIp: '10.77.0.1', origin }); + expect(req.headers['x-forwarded-for']).toBe('10.77.0.1'); + }); + it('sanitizes a real loopback HTTP connection before the downstream handler', async () => { + const server = createServer((req, res) => { + sanitizeProxyHeaders(req, { trustedProxyIp: '10.77.0.1', origin }); + res.end(JSON.stringify(req.headers)); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + try { + const address = server.address() as { port: number }; + const body = await new Promise((resolve, reject) => { + const req = request({ host: '127.0.0.1', port: address.port, headers: fake('ignored').headers }, res => { + let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => resolve(data)); + }); req.on('error', reject); req.end(); + }); + const headers = JSON.parse(body); + expect(headers['x-forwarded-for']).toBe('127.0.0.1'); + expect(headers['x-forwarded-proto']).toBe('http'); + expect(headers['x-forwarded-host']).toBe(origin.host); + } finally { await new Promise((resolve, reject) => server.close(err => err ? reject(err) : resolve())); } + }); +}); diff --git a/tests/randomizer.test.ts b/tests/randomizer.test.ts index 2afde63..8a530d7 100644 --- a/tests/randomizer.test.ts +++ b/tests/randomizer.test.ts @@ -41,10 +41,10 @@ describe('Deterministic Randomizer V1 (HMAC_SHA256_FY_V1)', () => { const seed1 = generateCryptoSecureSeed(); const seed2 = generateCryptoSecureSeed(); - expect(seed1).toHaveLength(32); // 16 bytes in hex = 32 chars - expect(seed2).toHaveLength(32); + expect(seed1).toHaveLength(64); // 32 bytes in hex = 64 chars + expect(seed2).toHaveLength(64); expect(seed1).not.toBe(seed2); - expect(/^[0-9a-f]{32}$/.test(seed1)).toBe(true); + expect(/^[0-9a-f]{64}$/.test(seed1)).toBe(true); }); it('should guarantee deterministic replay given the same snapshot and seed', () => { diff --git a/tests/seed-precommit-gate.test.ts b/tests/seed-precommit-gate.test.ts index c96900d..faf0040 100644 --- a/tests/seed-precommit-gate.test.ts +++ b/tests/seed-precommit-gate.test.ts @@ -316,7 +316,7 @@ describe('Phase 2.4 — Seed Pre-Commit Gate (Seed Grinding Elimination)', () => const lockedGw = await repo.getGiveawayById(gw.id); expect(lockedGw?.status).toBe('SNAPSHOT_LOCKED'); expect(lockedGw?.seed).toBeDefined(); - expect(lockedGw?.seed).toHaveLength(32); + expect(lockedGw?.seed).toHaveLength(64); expect(lockedGw?.seedCommitment).toBe(computeSeedCommitment(lockedGw!.seed!)); expect(seedCommitment).toBe(lockedGw?.seedCommitment); }); diff --git a/tests/vk-r3-safety.test.ts b/tests/vk-r3-safety.test.ts new file mode 100644 index 0000000..2806928 --- /dev/null +++ b/tests/vk-r3-safety.test.ts @@ -0,0 +1,292 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { NextRequest } from 'next/server'; +import { fetchPaginatedVk } from '../src/integrations/vk/vk-client'; +import { VkIncompleteDataError } from '../src/integrations/vk/vk-errors'; +import { VkProvider } from '../src/providers/vk/vk-provider'; +import { STATIC_VK_CAPABILITIES } from '../src/providers/vk/vk-capabilities'; +import { IVkClient } from '../src/integrations/vk/vk-client'; +import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway'; +import { validateProviderCapabilities } from '../src/core/validation/giveaway-schemas'; +import { POST as importParticipants } from '../src/app/api/giveaways/[id]/participants/route'; +import { GiveawayStore } from '../src/lib/giveaway-store'; +import { MemoryGiveawayRepository } from '../src/lib/repository/memory-repository'; +import { defaultSessionStore, SESSION_COOKIE_NAME } from '../src/lib/auth/session'; +import { ProviderFactory } from '../src/providers/factory'; +import { generalApiRateLimiter, expensiveApiRateLimiter } from '../src/lib/rate-limiter'; + +describe('R3 VK incomplete and unsupported behavior', () => { + it('rejects an empty page before the reported total instead of returning a partial list', async () => { + await expect(fetchPaginatedVk({ + pageSize: 2, + fetchPage: vi.fn().mockResolvedValue({ items: [], totalCount: 2 }), + })).rejects.toBeInstanceOf(VkIncompleteDataError); + }); + + it('rejects a short page before the reported total instead of treating it as completion', async () => { + await expect(fetchPaginatedVk({ + pageSize: 3, + fetchPage: vi.fn().mockResolvedValue({ items: [1, 2], totalCount: 4 }), + })).rejects.toBeInstanceOf(VkIncompleteDataError); + }); + + it('rejects an empty result without a reported total because completeness is unknown', async () => { + await expect(fetchPaginatedVk({ + pageSize: 3, + fetchPage: vi.fn().mockResolvedValue({ items: [] }), + })).rejects.toBeInstanceOf(VkIncompleteDataError); + }); + + it('accepts a complete short final page when it reaches the reported total', async () => { + const result = await fetchPaginatedVk({ + pageSize: 3, + fetchPage: vi.fn() + .mockResolvedValueOnce({ items: [1, 2, 3], totalCount: 5 }) + .mockResolvedValueOnce({ items: [4, 5], totalCount: 5 }), + }); + expect(result).toEqual([1, 2, 3, 4, 5]); + }); + + it('rejects invalid totals, totals that change, and oversized pages', async () => { + await expect(fetchPaginatedVk({ + pageSize: 2, + fetchPage: vi.fn().mockResolvedValue({ items: [], totalCount: -1 }), + })).rejects.toBeInstanceOf(VkIncompleteDataError); + await expect(fetchPaginatedVk({ + pageSize: 2, + fetchPage: vi.fn() + .mockResolvedValueOnce({ items: [1, 2], totalCount: 4 }) + .mockResolvedValueOnce({ items: [3], totalCount: 5 }), + })).rejects.toBeInstanceOf(VkIncompleteDataError); + await expect(fetchPaginatedVk({ + pageSize: 2, + fetchPage: vi.fn().mockResolvedValue({ items: [1, 2, 3], totalCount: 3 }), + })).rejects.toBeInstanceOf(VkIncompleteDataError); + }); + + it('fails subscription enrichment when VK omits any requested user status', async () => { + const client = { + call: vi.fn().mockResolvedValue([{ user_id: 101, member: 1 }]), + } as unknown as IVkClient; + const provider = new VkProvider('fake-service-token-r3', client); + + await expect(provider.checkSubscription(['101', '202'], '42')) + .rejects.toBeInstanceOf(VkIncompleteDataError); + }); + + it('fails subscription enrichment for malformed or unexpected VK statuses', async () => { + for (const response of [ + { items: [{ user_id: 101, member: 1 }] }, + [{ user_id: 101, member: 2 }], + [{ user_id: 999, member: 1 }], + [{ user_id: 101, member: 1 }, { user_id: 101, member: 0 }], + ]) { + const client = { call: vi.fn().mockResolvedValue(response) } as unknown as IVkClient; + const provider = new VkProvider('fake-service-token-r3', client); + await expect(provider.checkSubscription(['101'], '42')).rejects.toBeInstanceOf(VkIncompleteDataError); + } + }); + + it('keeps unsupported repost and manager checks explicit', () => { + expect(STATIC_VK_CAPABILITIES.reposts).toBe(false); + expect(STATIC_VK_CAPABILITIES.repostsNote).toMatch(/не подключена/i); + expect(STATIC_VK_CAPABILITIES.adminDetection).toBe(false); + expect(STATIC_VK_CAPABILITIES.adminDetectionNote).toMatch(/не загружается/i); + }); + + it('rejects repost and manager rules with clear capability reasons', () => { + expect(() => validateProviderCapabilities( + { ...DEFAULT_FILTER_RULES, requireRepost: true }, + STATIC_VK_CAPABILITIES + )).toThrow(/репост/i); + expect(() => validateProviderCapabilities( + { ...DEFAULT_FILTER_RULES, excludeAdmins: true }, + STATIC_VK_CAPABILITIES + )).toThrow(/руководителей/i); + }); + + it('uses ordinary page requests and never attempts execute batching', async () => { + const calls: string[] = []; + const client = { + call: vi.fn(async (method: string) => { + calls.push(method); + return { count: 0, items: [] }; + }), + } as unknown as IVkClient; + const provider = new VkProvider('fake-service-token-r3', client); + + await provider.fetchParticipants({ ownerId: '-42', postId: '7', includeLikes: true, includeComments: true }); + + expect(calls).toEqual(['likes.getList', 'wall.getComments']); + expect(calls).not.toContain('execute'); + }); +}); + +describe('R3 import route persistence on incomplete VK data', () => { + const user = { id: 'usr_r3_safety', vkUserId: '101' }; + let sessionId = ''; + + beforeEach(async () => { + GiveawayStore.setRepository(new MemoryGiveawayRepository()); + defaultSessionStore.clear(); + generalApiRateLimiter.reset(); + expensiveApiRateLimiter.reset(); + sessionId = await defaultSessionStore.createSession(user); + }); + + afterEach(() => vi.restoreAllMocks()); + + it('returns an explicit error and preserves the previous participant list when membership status is incomplete', async () => { + const giveaway = await GiveawayStore.create({ + sourceUrl: 'https://vk.com/wall-42_7', + post: { + platform: 'VK', ownerId: '-42', postId: '7', sourceUrl: 'https://vk.com/wall-42_7', + title: 'R3 test', text: '', likesCount: 1, commentsCount: 0, repostsCount: 0, + }, + filterRules: { ...DEFAULT_FILTER_RULES }, + winnersCount: 1, + reserveWinnersCount: 0, + organizerId: user.id, + }); + const previousParticipants = [{ + platformUserId: 'old-participant', firstName: 'Existing', lastName: 'Entry', + source: 'LIKES' as const, liked: true, commented: false, commentsCount: 0, + reposted: false, subscribed: false, eligible: true, exclusionReason: null, + }]; + await GiveawayStore.updateParticipants(giveaway.id, previousParticipants); + + const client = { + call: vi.fn(async (method: string) => { + if (method === 'likes.getList') { + return { count: 2, items: [ + { id: 101, first_name: 'One', last_name: 'User' }, + { id: 202, first_name: 'Two', last_name: 'User' }, + ] }; + } + if (method === 'wall.getComments') return { count: 0, items: [] }; + if (method === 'groups.isMember') return [{ user_id: 101, member: 1 }]; + throw new Error(`Unexpected VK method: ${method}`); + }), + } as unknown as IVkClient; + vi.spyOn(ProviderFactory, 'getVkProvider').mockReturnValue( + new VkProvider('fake-service-token-r3-route', client) + ); + + const req = new NextRequest(`http://localhost/api/giveaways/${giveaway.id}/participants`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Cookie: `${SESSION_COOKIE_NAME}=${sessionId}`, + }, + body: JSON.stringify({ + filterRules: { + ...DEFAULT_FILTER_RULES, + requireLike: true, + requireComment: true, + requireSubscription: true, + targetGroupId: '42', + }, + }), + }); + const response = await importParticipants(req, { params: { id: giveaway.id } }); + const body = await response.json(); + + expect(response.status).toBe(502); + expect(body.error.code).toBe('VK_INCOMPLETE_DATA'); + expect(body.error.message).toMatch(/subscription check returned status/i); + const stored = await GiveawayStore.getParticipantsPaginated(giveaway.id, 1, 50); + expect(stored.participants).toEqual(previousParticipants); + }); + + it('rejects malformed likes/comments records at the import route without replacing stored participants', async () => { + const giveaway = await GiveawayStore.create({ + sourceUrl: 'https://vk.com/wall-42_7', + post: { + platform: 'VK', ownerId: '-42', postId: '7', sourceUrl: 'https://vk.com/wall-42_7', + title: 'Malformed response test', text: '', likesCount: 1, commentsCount: 0, repostsCount: 0, + }, + filterRules: { ...DEFAULT_FILTER_RULES }, + winnersCount: 1, + reserveWinnersCount: 0, + organizerId: user.id, + }); + const previousParticipants = [{ + platformUserId: 'previous-user', firstName: 'Previous', lastName: 'Participant', + source: 'LIKES' as const, liked: true, commented: false, commentsCount: 0, + reposted: false, subscribed: false, eligible: true, exclusionReason: null, + }]; + await GiveawayStore.updateParticipants(giveaway.id, previousParticipants); + + const invalidCases: Array<{ label: string; method: 'likes.getList' | 'wall.getComments'; response: unknown }> = [ + { label: 'missing likes ID', method: 'likes.getList', response: { count: 1, items: [{ first_name: 'No', last_name: 'ID' }] } }, + { label: 'undefined likes ID', method: 'likes.getList', response: { count: 1, items: [{ id: undefined }] } }, + { label: 'zero likes ID', method: 'likes.getList', response: { count: 1, items: [{ id: 0 }] } }, + { label: 'negative likes ID', method: 'likes.getList', response: { count: 1, items: [{ id: -3 }] } }, + { label: 'fractional likes ID', method: 'likes.getList', response: { count: 1, items: [{ id: 1.5 }] } }, + { label: 'unsafe likes ID', method: 'likes.getList', response: { count: 1, items: [{ id: Number.MAX_SAFE_INTEGER + 1 }] } }, + { label: 'string likes ID', method: 'likes.getList', response: { count: 1, items: [{ id: '101' }] } }, + { label: 'null likes item', method: 'likes.getList', response: { count: 1, items: [null] } }, + { label: 'duplicate likes IDs', method: 'likes.getList', response: { count: 2, items: [{ id: 101 }, { id: 101 }] } }, + { label: 'malformed optional likes profile field', method: 'likes.getList', response: { count: 1, items: [{ id: 101, first_name: {} }] } }, + { label: 'missing likes items', method: 'likes.getList', response: { count: 0 } }, + { label: 'missing likes count', method: 'likes.getList', response: { items: [] } }, + { label: 'missing comments items', method: 'wall.getComments', response: { count: 0 } }, + { label: 'null comment item', method: 'wall.getComments', response: { count: 1, items: [null] } }, + { label: 'comment missing author ID', method: 'wall.getComments', response: { count: 1, items: [{ id: 1 }] } }, + { label: 'zero comment author ID', method: 'wall.getComments', response: { count: 1, items: [{ from_id: 0 }] } }, + { label: 'string comment author ID', method: 'wall.getComments', response: { count: 1, items: [{ from_id: '101' }] } }, + { label: 'fractional comment author ID', method: 'wall.getComments', response: { count: 1, items: [{ from_id: 1.5 }] } }, + { label: 'unsafe comment author ID', method: 'wall.getComments', response: { count: 1, items: [{ from_id: Number.MAX_SAFE_INTEGER + 1 }] } }, + { label: 'null comments profiles', method: 'wall.getComments', response: { count: 0, items: [], profiles: null } }, + { label: 'null comments profile item', method: 'wall.getComments', response: { count: 0, items: [], profiles: [null] } }, + { label: 'invalid comments profile ID', method: 'wall.getComments', response: { count: 0, items: [], profiles: [{ id: -101 }] } }, + { label: 'malformed comments profile field', method: 'wall.getComments', response: { count: 0, items: [], profiles: [{ id: 101, photo_100: 42 }] } }, + { label: 'duplicate comments profiles', method: 'wall.getComments', response: { count: 0, items: [], profiles: [{ id: 101 }, { id: 101 }] } }, + { label: 'missing comments count', method: 'wall.getComments', response: { items: [] } }, + ]; + + for (const invalidCase of invalidCases) { + expensiveApiRateLimiter.reset(); + const client = { + call: vi.fn(async (method: string) => { + if (method === invalidCase.method) return invalidCase.response; + return { count: 0, items: [] }; + }), + } as unknown as IVkClient; + vi.spyOn(ProviderFactory, 'getVkProvider').mockReturnValue( + new VkProvider('fake-service-token-malformed-r3', client) + ); + const req = new NextRequest(`http://localhost/api/giveaways/${giveaway.id}/participants`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Cookie: `${SESSION_COOKIE_NAME}=${sessionId}`, + }, + body: JSON.stringify({ + filterRules: { + ...DEFAULT_FILTER_RULES, + requireLike: invalidCase.method === 'likes.getList', + requireComment: invalidCase.method === 'wall.getComments', + }, + }), + }); + const response = await importParticipants(req, { params: { id: giveaway.id } }); + const body = await response.json(); + + expect(response.status, invalidCase.label).toBe(502); + expect(body.error.code, invalidCase.label).toBe('VK_INCOMPLETE_DATA'); + const stored = await GiveawayStore.getParticipantsPaginated(giveaway.id, 1, 50); + expect(stored.participants, invalidCase.label).toEqual(previousParticipants); + } + }); + + it('allows valid negative comment authors as explicitly excluded community authors', async () => { + const client = { + call: vi.fn().mockResolvedValue({ count: 1, items: [{ id: 1, from_id: -42 }] }), + } as unknown as IVkClient; + const provider = new VkProvider('fake-service-token-negative-community', client); + + await expect(provider.fetchParticipants({ + ownerId: '-42', postId: '7', includeLikes: false, includeComments: true, + })).resolves.toEqual([]); + }); +}); diff --git a/tests/vk-smoke-provider-mode.test.ts b/tests/vk-smoke-provider-mode.test.ts new file mode 100644 index 0000000..5d3aeda --- /dev/null +++ b/tests/vk-smoke-provider-mode.test.ts @@ -0,0 +1,115 @@ +import { execFile } from 'node:child_process'; +import { createServer } from 'node:http'; +import { mkdtemp, writeFile, rm } from 'node:fs/promises'; +import path from 'node:path'; +import { promisify } from 'node:util'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { NextRequest } from 'next/server'; +import { ProviderFactory } from '../src/providers/factory'; +import { VkProvider } from '../src/providers/vk/vk-provider'; +import { POST as previewPost } from '../src/app/api/posts/preview/route'; +import { defaultSessionStore, SESSION_COOKIE_NAME } from '../src/lib/auth/session'; +import { generalApiRateLimiter } from '../src/lib/rate-limiter'; + +const run = promisify(execFile); +afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs(); generalApiRateLimiter.reset(); }); + +describe('live smoke cannot accept generated VK data', () => { + it('rejects explicit mock configuration in production even with a service token', () => { + vi.stubEnv('NODE_ENV', 'production'); + vi.stubEnv('USE_VK_MOCK', 'true'); + vi.stubEnv('VK_SERVICE_TOKEN', 'FAKE_UNUSED_SERVICE_TOKEN'); + expect(() => ProviderFactory.getVkProvider()).toThrow(/forbidden in production/); + }); + + it('labels an actual mock provider preview despite its SERVICE auth metadata', async () => { + vi.stubEnv('NODE_ENV', 'test'); + vi.stubEnv('USE_VK_MOCK', 'true'); + const id = await defaultSessionStore.createSession({ id: 'fake-smoke-mode-user', vkUserId: '9001' }); + try { + const response = await previewPost(new NextRequest('http://localhost/api/posts/preview', { + method: 'POST', headers: { cookie: `${SESSION_COOKIE_NAME}=${id}`, 'content-type': 'application/json' }, + body: JSON.stringify({ url: 'https://vk.com/wall-1_1' }), + })); + const body = await response.json(); + expect(response.status).toBe(200); + expect(body.effectiveCapabilities.accessMode).toBe('PUBLIC_SERVICE'); + expect(body.providerMode).toBe('MOCK'); + } finally { await defaultSessionStore.destroySession(id); } + }); + + it('labels a real provider instance without exposing its token', async () => { + vi.stubEnv('NODE_ENV', 'test'); + vi.stubEnv('USE_VK_MOCK', 'false'); + vi.stubEnv('VK_SERVICE_TOKEN', 'FAKE_LIVE_MODE_MARKER_TOKEN'); + vi.spyOn(VkProvider.prototype, 'fetchPost').mockResolvedValue({ + platform: 'VK', ownerId: '-1', postId: '1', sourceUrl: 'https://vk.com/wall-1_1', + title: 'Fake response', text: '', likesCount: 2, commentsCount: 0, repostsCount: 0, resolvedAuthType: 'SERVICE', + }); + const id = await defaultSessionStore.createSession({ id: 'fake-live-mode-user', vkUserId: '9002' }); + try { + const response = await previewPost(new NextRequest('http://localhost/api/posts/preview', { + method: 'POST', headers: { cookie: `${SESSION_COOKIE_NAME}=${id}`, 'content-type': 'application/json' }, + body: JSON.stringify({ url: 'https://vk.com/wall-1_1' }), + })); + const text = await response.text(); + expect(response.status).toBe(200); + expect(JSON.parse(text).providerMode).toBe('LIVE'); + expect(text).not.toContain('FAKE_LIVE_MODE_MARKER_TOKEN'); + } finally { await defaultSessionStore.destroySession(id); } + }); + + it.each([ + { mode: 'MOCK', count: 2, expected: 1 }, + { mode: 'LIVE', count: 0, expected: 1 }, + { mode: 'LIVE', count: 2, expected: 0 }, + ])('smoke exits $expected for $mode mode with $count participants', async ({ mode, count, expected }) => { + let createCalls = 0; + let importCalls = 0; + const server = createServer((req, res) => { + req.resume(); + if (req.url === '/api/auth/vk/start') { + res.writeHead(307, { location: 'https://id.vk.com/authorize?code_challenge_method=S256' }); res.end(); return; + } + res.setHeader('content-type', 'application/json'); + if (req.url === '/api/auth/me') { res.end(JSON.stringify({ authenticated: true, user: { id: 'fake-user' } })); return; } + if (req.url === '/api/posts/preview') { + res.end(JSON.stringify({ success: true, providerMode: mode, + effectiveCapabilities: { accessMode: 'PUBLIC_SERVICE' }, post: { sourceUrl: 'https://vk.com/wall-1_1', resolvedAuthType: 'SERVICE' } })); return; + } + if (req.url === '/api/giveaways') { + createCalls++; res.statusCode = 201; res.end(JSON.stringify({ success: true, giveaway: { id: 'fake-giveaway' } })); return; + } + if (req.url === '/api/giveaways/fake-giveaway/participants') { + importCalls++; res.end(JSON.stringify({ success: true, providerMode: mode, totalCount: count })); return; + } + res.statusCode = 404; res.end('{}'); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + // Keep all local scratch material under the agent's permitted notes area. + const dir = await mkdtemp(path.resolve('agents/codex/notes/.smoke-regression-')); + try { + const port = (server.address() as { port: number }).port; + const file = path.join(dir, '.env.local'); + await writeFile(file, `VK_SMOKE_BASE_URL=http://127.0.0.1:${port}\nVK_SMOKE_SESSION_COOKIE=randomayzer_session=FAKE_SMOKE_COOKIE\nVK_SMOKE_PUBLIC_POST_URL=https://vk.com/wall-1_1\nVK_SMOKE_GROUP_ID=42\nVK_SMOKE_CREATE_GIVEAWAY=YES\n`, { mode: 0o600 }); + let stdout = '', exitCode = 0; + try { stdout = (await run('bash', [path.resolve('tools/vk-smoke.sh'), file], { timeout: 20000 })).stdout; } + catch (error) { const failure = error as { code: number; stdout: string }; exitCode = failure.code; stdout = failure.stdout; } + expect(exitCode).toBe(expected); + expect(stdout).toContain('MANUAL B'); + expect(stdout).not.toContain('FAKE_SMOKE_COOKIE'); + if (mode === 'MOCK') { + expect(stdout).toContain('FAIL D'); expect(stdout).not.toContain('PASS G'); expect(stdout).not.toContain('PASS H'); + expect(createCalls).toBe(0); expect(importCalls).toBe(0); + } else if (count === 0) { + expect(stdout).toContain('FAIL G'); expect(stdout).toContain('FAIL H'); + } else { + expect(stdout).toContain('PASS D'); expect(stdout).toContain('PASS G'); expect(stdout).toContain('PASS H'); + expect(importCalls).toBe(2); + } + } finally { + await rm(dir, { recursive: true, force: true }); + await new Promise((resolve, reject) => server.close(err => err ? reject(err) : resolve())); + } + }); +}); diff --git a/tools/db-backup-daily.sh b/tools/db-backup-daily.sh new file mode 100755 index 0000000..3fd5ded --- /dev/null +++ b/tools/db-backup-daily.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail +umask 077 +[[ $# == 1 && -d $1 ]] || { echo 'Usage: db-backup-daily.sh /absolute/backup-directory' >&2; exit 1; } +root=$(cd "$(dirname "$0")/.." && pwd) +# Serialize daily runs to bound space usage. On failure retain dump for diagnosis. +exec 9>"$1/.randomayzer-backup.lock" +flock -n 9 || exit 0 +backup=$("$root/tools/db-backup.sh" "$1") +"$root/tools/db-restore-check.sh" "$backup" +# Retain fourteen daily backups; only this application's named dumps are eligible. +find "$1" -maxdepth 1 -type f -name 'randomayzer-*.dump' -mtime +14 -delete +echo 'PASS daily backup and restore verification' diff --git a/tools/db-backup.sh b/tools/db-backup.sh new file mode 100755 index 0000000..5c229ce --- /dev/null +++ b/tools/db-backup.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail +umask 077 +# Environment file is read by Docker Compose, never sourced as executable shell code. +: "${RANDOMAYZER_ENV_FILE:?Set absolute 0600 env file path}" +[[ $(stat -c '%a' "$RANDOMAYZER_ENV_FILE") == 600 ]] || { echo 'Environment file must have mode 0600' >&2; exit 1; } +[[ $# == 1 && -d $1 ]] || { echo 'Usage: tools/db-backup.sh /absolute/backup-directory' >&2; exit 1; } +root=$(cd "$(dirname "$0")/.." && pwd) +backup_dir=$(realpath "$1") +file="$backup_dir/randomayzer-$(date -u +%Y%m%dT%H%M%S)-$$.dump" +args=(--env-file "$RANDOMAYZER_ENV_FILE" -f "$root/compose.production.yml") +export RANDOMAYZER_ENV_FILE +trap 'rm -f "$file.partial"' EXIT +docker compose "${args[@]}" exec -T postgres pg_dump -U randomayzer -d randomayzer -Fc > "$file.partial" +# Validate archive readability; full restore drill is a separate mandatory daily step. +docker compose "${args[@]}" exec -T postgres pg_restore --list < "$file.partial" > /dev/null +mv "$file.partial" "$file" +echo "$file" diff --git a/tools/db-restore-check.sh b/tools/db-restore-check.sh new file mode 100755 index 0000000..c1198a6 --- /dev/null +++ b/tools/db-restore-check.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail +: "${RANDOMAYZER_ENV_FILE:?Set absolute 0600 env file path}" +[[ $(stat -c '%a' "$RANDOMAYZER_ENV_FILE") == 600 ]] || { echo 'Environment file must have mode 0600' >&2; exit 1; } +[[ $# == 1 && -f $1 ]] || { echo 'Usage: tools/db-restore-check.sh backup.dump' >&2; exit 1; } +root=$(cd "$(dirname "$0")/.." && pwd) +args=(--env-file "$RANDOMAYZER_ENV_FILE" -f "$root/compose.production.yml") +export RANDOMAYZER_ENV_FILE +# A fresh temporary DB, never the production database. Avoid concurrent drill collisions. +drill="randomayzer_restore_${$}" +cleanup() { docker compose "${args[@]}" exec -T postgres dropdb -U randomayzer --if-exists "$drill" > /dev/null; } +trap cleanup EXIT +docker compose "${args[@]}" exec -T postgres createdb -U randomayzer "$drill" +docker compose "${args[@]}" exec -T postgres pg_restore -U randomayzer -d "$drill" --exit-on-error --no-owner --no-privileges < "$1" +docker compose "${args[@]}" exec -T postgres psql -U randomayzer -d "$drill" -v ON_ERROR_STOP=1 -c 'SELECT COUNT(*) FROM "_prisma_migrations";' -c 'SELECT COUNT(*) FROM "Giveaway";' +echo 'PASS backup restored to clean drill database' diff --git a/tools/production-transport.cjs b/tools/production-transport.cjs new file mode 100644 index 0000000..8e0c45a --- /dev/null +++ b/tools/production-transport.cjs @@ -0,0 +1,21 @@ +'use strict'; +const { isIP } = require('node:net'); +function normalizePeer(ip) { return (ip || '').replace(/^::ffff:/, ''); } +/** Run before handing the raw HTTP request to Next. Never use a header as peer identity. */ +function sanitizeProxyHeaders(req, { trustedProxyIp, origin }) { + const peer = normalizePeer(req.socket.remoteAddress); + const trusted = !!trustedProxyIp && peer === trustedProxyIp; + const forwarded = req.headers['x-forwarded-for']; + const proto = req.headers['x-forwarded-proto']; + for (const key of Object.keys(req.headers)) { + if (key.startsWith('x-forwarded-') || ['forwarded', 'x-real-ip', 'cf-connecting-ip'].includes(key)) delete req.headers[key]; + } + // Our VPS overwrites this with one client address; chains are deliberately unsupported. + const client = trusted && typeof forwarded === 'string' && isIP(forwarded) ? forwarded : peer; + req.headers['x-forwarded-for'] = isIP(client) ? client : 'unknown-client'; + req.headers['x-forwarded-proto'] = trusted && proto === 'https' ? 'https' : 'http'; + req.headers['x-forwarded-host'] = origin.host; + req.headers['x-forwarded-port'] = origin.port || (origin.protocol === 'https:' ? '443' : '80'); + req.headers.host = origin.host; +} +module.exports = { sanitizeProxyHeaders }; diff --git a/tools/serve-production.cjs b/tools/serve-production.cjs new file mode 100644 index 0000000..85cfb62 --- /dev/null +++ b/tools/serve-production.cjs @@ -0,0 +1,29 @@ +'use strict'; +const http = require('node:http'); +const next = require('next'); +const { sanitizeProxyHeaders } = require('./production-transport.cjs'); +async function main() { + if (process.env.NODE_ENV !== 'production') throw new Error('Production server requires NODE_ENV=production'); + const origin = new URL(process.env.APP_BASE_URL); + const local = process.env.DEPLOYMENT_PROFILE === 'local-single-user'; + if (origin.username || origin.password || origin.pathname !== '/' || origin.search || origin.hash) throw new Error('Invalid canonical origin'); + if (local ? !(origin.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(origin.hostname)) : origin.protocol !== 'https:') throw new Error('Invalid production origin/profile'); + if (process.env.VK_REDIRECT_URI !== `${origin.origin}/api/auth/vk/callback`) throw new Error('Invalid VK redirect URI'); + const trustedProxyIp = process.env.TRUSTED_PROXY_IP || ''; + if (trustedProxyIp && (local || trustedProxyIp !== '10.77.0.1')) throw new Error('Only the configured VPS peer 10.77.0.1 is supported'); + // TRUST_PROXY is safe only after this socket-aware ingress sanitizes the request. + process.env.TRUST_PROXY = 'true'; + const app = next({ dev: false }); + await app.prepare(); + const handle = app.getRequestHandler(); + const server = http.createServer({ maxHeaderSize: 16384 }, (req, res) => { + sanitizeProxyHeaders(req, { trustedProxyIp, origin }); + handle(req, res).catch(() => { if (!res.headersSent) res.writeHead(500); res.end('Request failed'); }); + }); + server.requestTimeout = 120000; + server.headersTimeout = 20000; + // Container binds internally; Compose publishes only the exact host loopback/WG addresses. + server.listen(Number(process.env.PORT || 3100), process.env.BIND_ADDRESS || '127.0.0.1', () => console.log('Randomayzer production listener ready')); + for (const signal of ['SIGTERM', 'SIGINT']) process.on(signal, () => server.close(() => process.exit(0))); +} +main().catch(() => { console.error('Production startup failed: check canonical origin, redirect, profile and required environment'); process.exitCode = 1; }); diff --git a/tools/vk-smoke.sh b/tools/vk-smoke.sh new file mode 100755 index 0000000..ee8d6ff --- /dev/null +++ b/tools/vk-smoke.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +set -u -o pipefail + +ENV_FILE="${1:-.vk-smoke.env}" +failures=0 +skips=0 +TMP_DIR="" + +result() { + local name="$1" status="$2" detail="${3:-}" + printf '%s %s' "$status" "$name" + [[ -z "$detail" ]] || printf ' — %s' "$detail" + printf '\n' + [[ "$status" != FAIL ]] || failures=$((failures + 1)) + [[ "$status" != SKIP && "$status" != MANUAL ]] || skips=$((skips + 1)) +} + +if [[ ! -f "$ENV_FILE" || -L "$ENV_FILE" ]]; then + printf 'FAIL config — expected a regular, non-symlink 0600 file: %s\n' "$ENV_FILE" + exit 2 +fi +mode="$(stat -c '%a' -- "$ENV_FILE" 2>/dev/null || true)" +if [[ "$mode" != 600 ]]; then + printf 'FAIL config — permissions must be 0600 (found %s)\n' "${mode:-unknown}" + exit 2 +fi + +declare -A config=() +while IFS= read -r line || [[ -n "$line" ]]; do + [[ "$line" =~ ^[[:space:]]*(#|$) ]] && continue + [[ "$line" == *=* ]] || { printf 'FAIL config — malformed line\n'; exit 2; } + key="${line%%=*}" + value="${line#*=}" + case "$key" in + VK_SMOKE_BASE_URL|VK_SMOKE_SESSION_COOKIE|VK_SMOKE_PROXY_AUTH|VK_SMOKE_PUBLIC_POST_URL|VK_SMOKE_PRIVATE_POST_URL|VK_SMOKE_GROUP_ID|VK_SMOKE_CREATE_GIVEAWAY) ;; + *) printf 'FAIL config — unsupported key %s\n' "$key"; exit 2 ;; + esac + [[ -z "${config[$key]+x}" ]] || { printf 'FAIL config — duplicate key %s\n' "$key"; exit 2; } + [[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || { printf 'FAIL config — multiline values are forbidden\n'; exit 2; } + config[$key]="$value" +done < "$ENV_FILE" + +BASE_URL="${config[VK_SMOKE_BASE_URL]:-}" +SESSION_COOKIE="${config[VK_SMOKE_SESSION_COOKIE]:-}" +PUBLIC_POST_URL="${config[VK_SMOKE_PUBLIC_POST_URL]:-}" +PRIVATE_POST_URL="${config[VK_SMOKE_PRIVATE_POST_URL]:-}" +GROUP_ID="${config[VK_SMOKE_GROUP_ID]:-}" +ALLOW_CREATE="${config[VK_SMOKE_CREATE_GIVEAWAY]:-NO}" + +if [[ ! "$BASE_URL" =~ ^http://(localhost|127\.0\.0\.1)(:[0-9]+)?$ && "$BASE_URL" != 'https://random.ochenstarik.ru' ]]; then + printf 'FAIL config — base URL must be loopback HTTP or https://random.ochenstarik.ru\n' + exit 2 +fi +if [[ -n "$SESSION_COOKIE" && ! "$SESSION_COOKIE" =~ ^randomayzer_session=[A-Za-z0-9._~-]+$ ]]; then + printf 'FAIL config — session cookie format is invalid\n' + exit 2 +fi +PROXY_AUTH="${config[VK_SMOKE_PROXY_AUTH]:-}" +if [[ -n "$PROXY_AUTH" && ! "$PROXY_AUTH" =~ ^[A-Za-z0-9._~-]+:[A-Za-z0-9._~-]+$ ]]; then + printf 'FAIL config — proxy auth must use safe username:password characters\n' + exit 2 +fi +if [[ "$BASE_URL" == 'https://random.ochenstarik.ru' && -z "$PROXY_AUTH" ]]; then + printf 'FAIL config — VK_SMOKE_PROXY_AUTH is required for the domain smoke test\n' + exit 2 +fi + +umask 077 +TMP_DIR="$(mktemp -d)" || { + printf 'FAIL setup — could not create a private temporary directory\n' + exit 2 +} +trap 'rm -rf -- "$TMP_DIR"' EXIT +BODY="$TMP_DIR/body.json" +HEADERS="$TMP_DIR/headers.txt" +CURL_CONFIG="$TMP_DIR/curl.cfg" +printf 'silent\nshow-error\n' > "$CURL_CONFIG" +[[ -z "$SESSION_COOKIE" ]] || printf 'header = "Cookie: %s"\n' "$SESSION_COOKIE" >> "$CURL_CONFIG" +[[ -z "$PROXY_AUTH" ]] || printf 'user = "%s"\n' "$PROXY_AUTH" >> "$CURL_CONFIG" + +request() { + local method="$1" path="$2" payload="${3:-}" + local args=(--config "$CURL_CONFIG" --connect-timeout 5 --max-time 120 --request "$method" --output "$BODY" --dump-header "$HEADERS" --write-out '%{http_code}') + if [[ "$method" != GET ]]; then + args+=(--header "Origin: $BASE_URL" --header 'Content-Type: application/json' --data "$payload") + fi + HTTP_CODE="$(curl "${args[@]}" "$BASE_URL$path" 2>/dev/null || true)" +} + +json_success() { + node -e 'const d=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")); process.exit(d?.success === true ? 0 : 1)' "$BODY" >/dev/null 2>&1 +} + +# Marker is emitted by the server for the actual provider instance, not inferred from access mode. +json_live_success() { + node -e 'const d=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")); process.exit(d?.success === true && d.providerMode === "LIVE" ? 0 : 1)' "$BODY" >/dev/null 2>&1 +} +json_live_import_success() { + node -e 'const d=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")); process.exit(d?.success === true && d.providerMode === "LIVE" && Number.isSafeInteger(d.totalCount) && d.totalCount > 0 ? 0 : 1)' "$BODY" >/dev/null 2>&1 +} + +# A: OAuth start creates a VK ID authorize redirect with PKCE S256. +request GET /api/auth/vk/start +if [[ "$HTTP_CODE" =~ ^30[1278]$ ]] && grep -Eiq '^location: https://id\.vk\.com/authorize[^\r]*code_challenge_method=S256' "$HEADERS"; then + result A PASS 'VK ID authorize redirect and PKCE S256 found' +else + result A FAIL "expected VK ID 3xx authorize redirect with PKCE S256 (HTTP ${HTTP_CODE:-curl-error})" +fi + +# B: consent, redirect completion, and cookie delivery require a human in a browser. +result B MANUAL 'open /api/auth/vk/start, approve consent in VK, and confirm callback returns to the app with a session cookie' + +# C: authenticated session lookup. No response body or cookie is printed. +if [[ -z "$SESSION_COOKIE" ]]; then + result C SKIP 'set VK_SMOKE_SESSION_COOKIE after completing manual consent' +else + request GET /api/auth/me + if [[ "$HTTP_CODE" == 200 ]] && node -e 'const d=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")); process.exit(d?.authenticated === true ? 0 : 1)' "$BODY" >/dev/null 2>&1; then result C PASS 'authenticated session endpoint returned success'; else result C FAIL "session endpoint returned HTTP ${HTTP_CODE:-curl-error}"; fi +fi + +POST_JSON="" +GIVEAWAY_ID="" +if [[ -z "$SESSION_COOKIE" || -z "$PUBLIC_POST_URL" ]]; then + result D SKIP 'authenticated session and VK_SMOKE_PUBLIC_POST_URL are required' +else + payload="$(node -e 'process.stdout.write(JSON.stringify({url:process.argv[1],platform:"VK"}))' "$PUBLIC_POST_URL")" + request POST /api/posts/preview "$payload" + if [[ "$HTTP_CODE" == 200 ]] && json_live_success; then + access_mode="$(node -e 'const d=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")); process.stdout.write(d.effectiveCapabilities?.accessMode||"")' "$BODY")" + if [[ "$access_mode" != PUBLIC_SERVICE ]]; then + result D FAIL 'public post preview did not report PUBLIC_SERVICE access mode' + else + result D PASS 'public VK post preview succeeded with PUBLIC_SERVICE access mode' + POST_JSON="$(node -e 'const d=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")); process.stdout.write(JSON.stringify(d.post))' "$BODY")" + fi + else result D FAIL "public post preview requires LIVE provider mode (HTTP ${HTTP_CODE:-curl-error})"; fi +fi + +if [[ -z "$SESSION_COOKIE" || -z "$PRIVATE_POST_URL" ]]; then + result E SKIP 'authenticated session and VK_SMOKE_PRIVATE_POST_URL are required' +else + payload="$(node -e 'process.stdout.write(JSON.stringify({url:process.argv[1],platform:"VK"}))' "$PRIVATE_POST_URL")" + request POST /api/posts/preview "$payload" + if [[ "$HTTP_CODE" == 200 ]] && json_live_success; then + mode="$(node -e 'const d=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")); process.stdout.write(d.post?.resolvedAuthType||"")' "$BODY")" + if [[ "$mode" == USER ]]; then result E PASS 'restricted post preview used organizer authorization'; else result E FAIL 'restricted post did not resolve with USER authorization'; fi + else result E FAIL "restricted post preview requires LIVE provider mode (HTTP ${HTTP_CODE:-curl-error})"; fi +fi + +if [[ "$ALLOW_CREATE" != YES ]]; then + result F SKIP 'set VK_SMOKE_CREATE_GIVEAWAY=YES to create a test giveaway' + result G SKIP 'requires a giveaway created by check F' + result H SKIP 'requires a giveaway and VK_SMOKE_GROUP_ID' +elif [[ -z "$SESSION_COOKIE" || -z "$POST_JSON" ]]; then + result F FAIL 'cannot create without a successful public preview and authenticated session' + result G SKIP 'requires a giveaway created by check F' + result H SKIP 'requires a giveaway and VK_SMOKE_GROUP_ID' +else + create_payload="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(JSON.stringify({sourceUrl:p.sourceUrl,post:p,filterRules:{requireLike:true,requireComment:false,requireRepost:false,requireSubscription:false,excludeAdmins:false,excludeBlacklistedIds:[],minEligibleParticipants:1}}))' "$POST_JSON")" + request POST /api/giveaways "$create_payload" + if [[ "$HTTP_CODE" == 201 ]] && json_success; then + GIVEAWAY_ID="$(node -e 'const d=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")); process.stdout.write(d.giveaway?.id||"")' "$BODY")" + if [[ -n "$GIVEAWAY_ID" ]]; then result F PASS 'test giveaway created'; else result F FAIL 'create response omitted giveaway id'; fi + else result F FAIL "giveaway create returned HTTP ${HTTP_CODE:-curl-error}"; fi + + if [[ -n "$GIVEAWAY_ID" ]]; then + import_payload='{"filterRules":{"requireLike":true,"requireComment":true,"requireRepost":false,"requireSubscription":false,"excludeAdmins":false,"excludeBlacklistedIds":[],"minEligibleParticipants":1}}' + request POST "/api/giveaways/$GIVEAWAY_ID/participants" "$import_payload" + if [[ "$HTTP_CODE" == 200 ]] && json_live_import_success; then result G PASS 'participant import succeeded'; else result G FAIL "participant import requires LIVE mode and nonempty participants (HTTP ${HTTP_CODE:-curl-error})"; fi + + if [[ -z "$GROUP_ID" ]]; then + result H SKIP 'set VK_SMOKE_GROUP_ID to run subscription verification' + else + subscription_payload="$(node -e 'const p=JSON.parse(process.argv[1]); p.filterRules.requireSubscription=true; p.filterRules.targetGroupId=process.argv[2]; process.stdout.write(JSON.stringify(p))' "$import_payload" "$GROUP_ID")" + request POST "/api/giveaways/$GIVEAWAY_ID/participants" "$subscription_payload" + if [[ "$HTTP_CODE" == 200 ]] && json_live_import_success; then result H PASS 'subscription verification returned a complete result'; else result H FAIL "subscription verification requires LIVE mode and nonempty participants (HTTP ${HTTP_CODE:-curl-error})"; fi + fi + else + result G SKIP 'requires a giveaway created by check F' + result H SKIP 'requires a giveaway and VK_SMOKE_GROUP_ID' + fi +fi + +printf 'Smoke checks: %d failure(s), %d skipped/manual check(s).\n' "$failures" "$skips" +[[ "$failures" -eq 0 ]]