[794-2][@ant] feat: скелет монорепы verifier (api+spa+shared) + docker/CI/deploy — единая структура и автономные пайплайны для разработки всех эпиков верификатора

This commit is contained in:
coopops
2026-06-03 07:30:53 +00:00
commit cc2ebc4196
25 changed files with 4525 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
+9
View File
@@ -0,0 +1,9 @@
# verifier-api
DATABASE_URL=postgres://verifier:verifier@localhost:5433/verifier
REDIS_URL=redis://localhost:6380
SHIP_URL=ws://127.0.0.1:8080
START_BLOCK=1
API_PORT=3030
# verifier-spa
VITE_API_BASE=http://localhost:3030
+24
View File
@@ -0,0 +1,24 @@
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm -r build
- run: pnpm lint
- run: pnpm -r typecheck
- run: pnpm -r test
+24
View File
@@ -0,0 +1,24 @@
name: Deploy API
# Автономный пайплайн api (ADR-003/004): не триггерит deploy-spa.
on:
push:
tags:
- 'api-v*'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build & push image
run: |
echo "docker build -f apps/api/Dockerfile -t $IMAGE ."
echo "docker push $IMAGE"
env:
IMAGE: ${{ vars.API_IMAGE }}
- name: Deploy over ssh
run: |
echo "ssh $DEPLOY_HOST 'cd /srv/verifier && docker-compose pull && docker-compose up -d'"
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
+24
View File
@@ -0,0 +1,24 @@
name: Deploy SPA
# Автономный пайплайн spa (ADR-003/004): не триггерит deploy-api.
on:
push:
tags:
- 'spa-v*'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm --filter @verifier/shared build && pnpm --filter @verifier/spa build
- name: Publish static
run: |
echo "scp -r apps/spa/dist/* $DEPLOY_HOST:/var/www/verify"
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
+12
View File
@@ -0,0 +1,12 @@
node_modules/
dist/
*.tsbuildinfo
.env
.env.*
!.env.example
coverage/
.DS_Store
*.log
.vite/
playwright-report/
test-results/
+7
View File
@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"vueIndentScriptAndStyle": false
}
+46
View File
@@ -0,0 +1,46 @@
# Верификатор документов Coopenomics v1
Монорепа доказательного верификатора документов: проверка целостности, криптоподписи (secp256k1/SHA-256)
и исторической привязки ключа к аккаунту в блокчейне Coopenomics.
## Структура (pnpm workspace)
| Пакет | Назначение |
|---|---|
| `packages/shared` (`@verifier/shared`) | Контракт `.sig` v2.0 + чистые проверки C1–C5 (браузер + Node). Не публикуется. |
| `apps/api` (`@verifier/api`) | Fastify + Kysely + Postgres. Demux-индексер permissions (native-delta из `@coopenomics/parser2`) + REST `/v1/active-permission/*`. |
| `apps/spa` (`@verifier/spa`) | Vue 3 + Vite + Tailwind v4 + Reka UI. Загрузка PDF+`.sig`/ZIP, оркестрация C1–C8, вердикт. |
## Проверки
| # | Проверка | Где |
|---|---|---|
| C1 | Целостность (`SHA-256(pdf) == doc_hash`) | shared, локально |
| C2 | `meta_hash` (канонизация JCS) | shared, локально |
| C3 | Композитный хэш | shared, локально |
| C4 | Подпись secp256k1 | shared, локально |
| C5 | Наличие сертификата | shared, локально |
| C6 | Историческая привязка ключа к аккаунту | api `/v1/active-permission/check` |
| C7 | Самоподписанность (⚠ предупреждение) | spa |
| C8 | Доказательство в цепи (опц.) | spa через публичный RPC |
## Разработка
```bash
pnpm install
docker compose up -d # postgres:5433 + redis:6380
pnpm -r build
pnpm dev:api # :3030
pnpm dev:spa # :5173
```
## Дизайн-язык SPA
Минималистичный пространственный дизайн на плоскостях, один ведущий цвет — **бордо** `#9f1239`
(бренд кооперативной экономики). Бордо только в chrome; вердикт — отдельная семантика
(✅ зелёный / ❌ красный / ⚠ amber). Reka UI + Tailwind v4 + Radix Colors. Эталоны: Linear, Vercel, Raycast, Resend.
## Деплой
Автономные пайплайны из одной репы: `deploy-api.yml` (docker image → ssh) и `deploy-spa.yml`
(vite build → nginx static). Деплой одного приложения не триггерит другое.
+19
View File
@@ -0,0 +1,19 @@
# Сборка verifier-api из монорепы (контекст сборки — корень репы).
FROM node:22-alpine AS build
RUN corepack enable
WORKDIR /repo
COPY pnpm-workspace.yaml package.json ./
COPY packages/shared/package.json packages/shared/
COPY apps/api/package.json apps/api/
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm --filter @verifier/shared build && pnpm --filter @verifier/api build
RUN pnpm --filter @verifier/api deploy --prod /app
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app .
COPY --from=build /repo/apps/api/migrations ./migrations
ENV NODE_ENV=production
EXPOSE 3030
CMD ["node", "dist/index.js"]
+32
View File
@@ -0,0 +1,32 @@
{
"name": "@verifier/api",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"dev": "node --watch --experimental-strip-types src/index.ts",
"start": "node dist/index.js",
"migrate": "node dist/db/migrate.js",
"test": "vitest run"
},
"dependencies": {
"@coopenomics/parser2": "^1.1.0",
"@fastify/cors": "^10.0.1",
"@fastify/rate-limit": "^10.1.1",
"@fastify/swagger": "^9.4.0",
"@sinclair/typebox": "^0.34.9",
"@verifier/shared": "workspace:*",
"fastify": "^5.1.0",
"kysely": "^0.27.4",
"pg": "^8.13.1",
"pino": "^9.5.0",
"prom-client": "^15.1.3"
},
"devDependencies": {
"@types/pg": "^8.11.10",
"typescript": "^5.7.2",
"vitest": "^2.1.8"
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src"],
"exclude": ["src/**/*.test.ts"]
}
+17
View File
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- CSP: wasm-unsafe-eval нужен wharfkit secp256k1; контент документов не уходит на сервер -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' http://localhost:3030; frame-src 'self' blob:;"
/>
<title>Верификатор документов · Кооперативная экономика</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@verifier/spa",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"typecheck": "vue-tsc --noEmit -p tsconfig.json",
"preview": "vite preview",
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"@verifier/shared": "workspace:*",
"@wharfkit/antelope": "^1.0.13",
"fflate": "^0.8.2",
"reka-ui": "^2.0.0",
"vue": "^3.5.13",
"vue-i18n": "^10.0.5"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@vitejs/plugin-vue": "^5.2.1",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.2",
"vite": "^6.0.5",
"vitest": "^2.1.8",
"vue-tsc": "^2.1.10"
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "preserve",
"types": ["vite/client"],
"noEmit": true
},
"include": ["src", "vite.config.ts"]
}
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [vue(), tailwindcss()],
server: { port: 5173 },
});
+16
View File
@@ -0,0 +1,16 @@
# verifier-api — reverse-proxy на Fastify (:3030).
server {
listen 443 ssl;
server_name api.verify.example.coop;
location /v1/ {
proxy_pass http://127.0.0.1:3030;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
}
location /metrics {
allow 127.0.0.1;
deny all;
proxy_pass http://127.0.0.1:3030;
}
}
+15
View File
@@ -0,0 +1,15 @@
# verifier-spa — статика + CSP-заголовки (wasm-unsafe-eval для wharfkit secp256k1).
server {
listen 443 ssl;
server_name verify.example.coop;
root /var/www/verify;
index index.html;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://api.verify.example.coop; frame-src 'self' blob:;" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
location / {
try_files $uri $uri/ /index.html;
}
}
+29
View File
@@ -0,0 +1,29 @@
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: verifier
POSTGRES_PASSWORD: verifier
POSTGRES_DB: verifier
ports:
- '5433:5432'
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U verifier']
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:7-alpine
ports:
- '6380:6379'
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
timeout: 3s
retries: 10
volumes:
pgdata:
+14
View File
@@ -0,0 +1,14 @@
// @ts-check
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ ignores: ['**/dist/**', '**/node_modules/**', '**/*.d.ts', '**/coverage/**'] },
eslint.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
},
},
);
+25
View File
@@ -0,0 +1,25 @@
{
"name": "verificator",
"version": "0.0.0",
"private": true,
"description": "Верификатор документов Coopenomics v1 — монорепа (api + spa + shared)",
"packageManager": "pnpm@9.15.9",
"engines": {
"node": ">=22"
},
"scripts": {
"build": "pnpm -r build",
"lint": "eslint .",
"typecheck": "pnpm -r typecheck",
"test": "pnpm -r test",
"dev:api": "pnpm --filter @verifier/api dev",
"dev:spa": "pnpm --filter @verifier/spa dev"
},
"devDependencies": {
"@types/node": "^22.10.0",
"eslint": "^9.17.0",
"prettier": "^3.4.2",
"typescript": "^5.7.2",
"typescript-eslint": "^8.18.0"
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@verifier/shared",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./schemas/sig-file.schema.json": "./schemas/sig-file.schema.json"
},
"files": ["dist", "schemas"],
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run"
},
"dependencies": {
"@wharfkit/antelope": "^1.0.13",
"canonicalize": "^2.0.0"
},
"devDependencies": {
"typescript": "^5.7.2",
"vitest": "^2.1.8"
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"lib": ["ES2022", "DOM"]
},
"include": ["src"],
"exclude": ["src/**/*.test.ts"]
}
+4082
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
packages:
- 'apps/*'
- 'packages/*'
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true
}
}