[794-2][@ant] feat: скелет монорепы verifier (api+spa+shared) + docker/CI/deploy — единая структура и автономные пайплайны для разработки всех эпиков верификатора
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
@@ -0,0 +1,12 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.tsbuildinfo
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
coverage/
|
||||
.DS_Store
|
||||
*.log
|
||||
.vite/
|
||||
playwright-report/
|
||||
test-results/
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"vueIndentScriptAndStyle": false
|
||||
}
|
||||
@@ -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). Деплой одного приложения не триггерит другое.
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
@@ -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: '^_' }],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"lib": ["ES2022", "DOM"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
Generated
+4082
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- 'apps/*'
|
||||
- 'packages/*'
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user