From 55f9d43ea221027c3d90c8274802dceaf42f79d8 Mon Sep 17 00:00:00 2001 From: MIEM DEV Date: Thu, 29 Oct 2026 17:22:39 +0300 Subject: [PATCH 01/13] feat: add course literature audit with scheduled checks --- .dockerignore | 22 + .gitignore | 41 + .npmrc | 3 + CHANGELOG.md | 30 + DESIGN.md | 2 + Dockerfile | 24 + PRODUCT.md | 6 + README.md | 53 ++ app/api/audit/route.ts | 15 + app/api/health/route.ts | 5 + app/dashboard.tsx | 35 + app/globals.css | 8 + app/layout.tsx | 23 + app/page.tsx | 2 + app/schedule-panel.tsx | 30 + compose.yaml | 30 + deploy/nginx-courses.conf | 17 + lib/checker.ts | 99 +++ lib/model.ts | 17 + lib/scanner.ts | 46 ++ lib/schedule-worker.ts | 10 + lib/scheduler.ts | 66 ++ lib/store.ts | 26 + next.config.ts | 9 + package.json | 32 + pnpm-lock.yaml | 1291 ++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 5 + postcss.config.mjs | 1 + public/favicon.svg | 1 + public/fonts/GolosText-OFL.txt | 93 +++ public/fonts/Lora-OFL.txt | 93 +++ public/fonts/font-0.woff2 | Bin 0 -> 64240 bytes public/fonts/font-1.woff2 | Bin 0 -> 64232 bytes public/fonts/font-2.woff2 | Bin 0 -> 64308 bytes public/fonts/font-3.woff2 | Bin 0 -> 64160 bytes public/fonts/font-4.woff2 | Bin 0 -> 132188 bytes public/fonts/font-5.woff2 | Bin 0 -> 132320 bytes public/fonts/fonts.css | 42 ++ server/runtime.mjs | 14 + tests/checker.test.ts | 19 + tests/origin.test.ts | 15 + tests/scanner.test.ts | 25 + tests/scheduler.test.ts | 72 ++ tests/test-database.ts | 23 + tsconfig.json | 59 ++ 45 files changed, 2404 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 CHANGELOG.md create mode 100644 DESIGN.md create mode 100644 Dockerfile create mode 100644 PRODUCT.md create mode 100644 app/api/audit/route.ts create mode 100644 app/api/health/route.ts create mode 100644 app/dashboard.tsx create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 app/schedule-panel.tsx create mode 100644 compose.yaml create mode 100644 deploy/nginx-courses.conf create mode 100644 lib/checker.ts create mode 100644 lib/model.ts create mode 100644 lib/scanner.ts create mode 100644 lib/schedule-worker.ts create mode 100644 lib/scheduler.ts create mode 100644 lib/store.ts create mode 100644 next.config.ts create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 postcss.config.mjs create mode 100644 public/favicon.svg create mode 100644 public/fonts/GolosText-OFL.txt create mode 100644 public/fonts/Lora-OFL.txt create mode 100644 public/fonts/font-0.woff2 create mode 100644 public/fonts/font-1.woff2 create mode 100644 public/fonts/font-2.woff2 create mode 100644 public/fonts/font-3.woff2 create mode 100644 public/fonts/font-4.woff2 create mode 100644 public/fonts/font-5.woff2 create mode 100644 public/fonts/fonts.css create mode 100644 server/runtime.mjs create mode 100644 tests/checker.test.ts create mode 100644 tests/origin.test.ts create mode 100644 tests/scanner.test.ts create mode 100644 tests/scheduler.test.ts create mode 100644 tests/test-database.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..99b6407 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +.git +node_modules +.next +.wrangler +.sites-runtime +dist +work +outputs +data +.env* +*.sqlite* +*.tsbuildinfo +components +hooks +examples +db +drizzle +scripts +worker +vendor +.openai +app/chatgpt-auth.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f02c096 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Next.js / Node +node_modules/ +.next/ +next-env.d.ts +*.tsbuildinfo +# Persistent runtime data and local state +data/ +*.sqlite* +.env* +.wrangler/ +.sites-runtime/ +.vinext/ +dist/ +work/ +outputs/ +# Retained original Sites scaffold (not part of Node deployment) +components/ +hooks/ +examples/ +db/ +drizzle/ +scripts/ +worker/ +vendor/ +.openai/ +vite.config.ts +drizzle.config.ts +cloudflare-env.d.ts +components.json +eslint.config.mjs +lib/utils.ts +lib/connectors.ts +lib/connector-preview.d.ts +lib/connector-errors.mts +lib/connector-contract.mts +lib/connector-context.ts +app/chatgpt-auth.ts +# OS +.DS_Store +AGENTS.md +CLAUDE.md diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9f274d6 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +audit=false +fund=false +update-notifier=false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..347e14c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +## 1.2.0 — 2026-10-29 + +- Хранилище перенесено на PostgreSQL для использования существующего сервера VPS; приложению предназначены отдельная база и роль. +- Добавлено общее редактируемое расписание: ежедневная или еженедельная проверка по московскому времени, выбор режима кэша, включение и отключение. +- Проверки по расписанию выполняются на сервере при закрытом браузере и возобновляются после перезапуска. +- Следующий запуск, пропуск совпавшего запуска и ошибки фоновой проверки отображаются в интерфейсе. +- Расписание и запуск сохраняются атомарно; совпавшие запуски не дублируются, после простоя выполняется один пропущенный запуск. +- При старте приложения одновременно запускаются веб-сервер и worker с общим завершением при сбое одного процесса. + +## 1.1.1 — 2026-10-29 + +- Ответы 302/304 без материала, включая бинарный Content-Type, отмечаются как требующие ручной проверки. + +## 1.1.0 — 2026-10-29 + +- Перенесён сервер с Cloudflare Workers/D1 на Node.js24 и постоянную SQLite для VPS. +- Добавлен путь `/courses` для интерфейса, API и шрифтов; production Origin настроен для miem.space. +- DNS-адрес закрепляется в сетевом соединении, включая повторную проверку перенаправлений. +- Добавлены standalone-сборка, непривилегированный Docker runtime, постоянный volume, healthcheck и Nginx-сниппет. +- Сохранение порций и очереди атомарно; добавлены проверки восстановления после ошибки записи, блокировки одновременных порций и Origin за reverse proxy. + +## 1.0.1 — 2026-10-29 + +- Добавлен общий реестр доступности литературы пяти программ НИУ ВШЭ без регистрации. +- Ручной запуск с продолжением, обход пагинации, чтение основной и дополнительной литературы, включая текстовые URL. +- Сохранение результатов и контрольных сумм курсов; повторное использование неизменившихся курсов и полная проверка без кэша. +- Отдельные причины недоступности, переходы к материалам и исходным курсам, поиск, фильтры и экспорт CSV. +- Явные отметки неполного охвата, JavaScript-заглушек, ограничений доступа и устаревших результатов. diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..02f5a35 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,2 @@ +# Визуальная система +Режим Operate. Академический реестр: тёплая бумага, глубокий зелёный, киноварь для проблем. Боковая навигация по программам, заголовок с засечками, плотный список результатов. Состояния подписаны словами. Без иллюстративных статистик и декоративных карточек. Адаптивность, клавиатура, заметный фокус, reduced motion. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..98f4ea3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM node:24-bookworm-slim@sha256:c86ecead9e55a5210ac1727de38a4ca840bf03dcaf34dc9749addbf383b1920c AS build +WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npm install --global pnpm@11.25.0 +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +RUN pnpm install --frozen-lockfile +COPY app ./app +COPY lib/model.ts lib/checker.ts lib/scanner.ts lib/store.ts lib/scheduler.ts lib/schedule-worker.ts ./lib/ +COPY public ./public +COPY next.config.ts tsconfig.json postcss.config.mjs ./ +RUN pnpm build + +FROM node:24-bookworm-slim@sha256:c86ecead9e55a5210ac1727de38a4ca840bf03dcaf34dc9749addbf383b1920c AS runtime +WORKDIR /app +ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 HOSTNAME=0.0.0.0 PORT=3000 +COPY --from=build --chown=node:node /app/.next/standalone ./ +COPY --from=build --chown=node:node /app/.next/static ./.next/static +COPY --from=build --chown=node:node /app/public ./public +COPY --from=build --chown=node:node /app/lib ./lib +COPY server ./server +USER node +EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD node -e "fetch('http://127.0.0.1:3000/courses/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))" +CMD ["node","server/runtime.mjs","--standalone"] diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..d061b77 --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,6 @@ +# Сверка +Общий реестр доступности литературы пяти программ НИУ ВШЭ. Без входа. Только реальные результаты; начальное состояние пустое. Node.js24 и PostgreSQL хранят результаты, контрольные суммы курсов и прогресс в постоянном volume. Проверка порциями, пока открыта вкладка; продолжение вручную. Неизменившиеся курсы используют кэш; доступна полная проверка. Успешный HTTP-ответ не доказывает доступ к полному тексту. Целевой путь — https://miem.space/courses. + +Расписание: одно общее, ежедневно или еженедельно по Москве, выключено до сохранения пользователем. Запланированный аудит работает фоновым процессом при закрытом браузере и продолжается после перезапуска. Совпавший слот пропускается, если предыдущая ручная или автоматическая проверка ещё running. Изменение/отключение влияет на будущие запуски. + +Для VPS используется отдельная база/роль на существующем PostgreSQL. Пароль только в окружении, не в исходниках. diff --git a/README.md b/README.md index e69de29..65d0ab3 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,53 @@ +# Сверка + +Проверка доступности литературы пяти образовательных программ НИУ ВШЭ. Целевой адрес: `https://miem.space/courses`. Авторизация не требуется. + +## Запуск + +Требуется Node.js24 и pnpm11.25.0. + +```sh +pnpm install --frozen-lockfile +TEST_DATABASE_URL=<изолированная-локальная-БД-sverka_test> pnpm test +pnpm typecheck +pnpm dev +``` + +Открыть `http://127.0.0.1:5173/courses`. Для сервера требуется `DATABASE_URL` с подключением к отдельной базе PostgreSQL. Там хранятся результаты, очередь и расписание; новое пустое хранилище инициализируется при первом подключении. Пароль передаётся только через окружение или защищённый файл окружения вне Git. `COURSES_ORIGIN` задаёт допустимый Origin для POST за reverse proxy, в production — `https://miem.space`. + +Ручная проверка выполняется порциями, пока вкладка открыта. После её закрытия или перезапуска сервера кнопка «Продолжить» использует сохранённую очередь. Глобальная аренда не допускает одновременную обработку одной порции. Каждая порция сохраняет результаты и очередь в одной транзакции PostgreSQL; при ошибке записи задания остаются в очереди. + +Страницы курсов загружаются заново, но литература неизменившихся курсов берётся из кэша вместе с исходной датой проверки. Полная проверка без кэша нужна для проверки изменений на внешних сайтах. HTTP200 не доказывает доступ к полному тексту; JavaScript-заглушки, CAPTCHA и признаки подписки отмечаются отдельно. Литература только во вложении отмечается как неполный охват. + +## Расписание + +В интерфейсе откройте «Проверка по расписанию»: включите автоматический запуск, выберите ежедневное или еженедельное повторение, время по Москве, при необходимости день недели и режим кэша. Сохранение и отключение доступны там же. По умолчанию расписание выключено; оно общее для всех посетителей и хранится в той же базе PostgreSQL. + +`pnpm dev`, `pnpm start` и Docker запускают веб-сервер и отдельный фоновый процесс одним управляемым runtime. Проверка по расписанию выполняется без открытой вкладки, при перезапуске продолжается по сохранённой очереди. Расписание проверяется раз в пять секунд. Ручные проверки сохраняют прежний режим с открытой вкладкой. + +Если любая предыдущая проверка ещё имеет статус running, совпавший слот пропускается с отметкой в интерфейсе: параллельных запусков нет. После простоя сервера выполняется один просроченный запуск; пропущенные дни не образуют очередь. Изменение или отключение расписания не прерывает уже начатую автоматическую проверку. Если один из двух процессов runtime завершится, второй останавливается; restart policy Docker поднимает их вместе. + +## Production + +```sh +docker compose build +docker compose up -d +``` + +Эти команды изменяют production: выполнять только через разрешённый deployment-процесс после независимого review. Сервис слушает только `127.0.0.1:3011`, работает от UID1000 и использует существующий сервер PostgreSQL через внешнюю Docker-сеть `miem_workers_default`. Отдельный контейнер БД или SQLite volume не создаётся. Healthcheck: `GET /courses/api/health`. Порт3011 — предлагаемый; перед запуском проверить, что он свободен. Код не зависит от Cloudflare Workers, D1 или Sites hosting. + +Добавить `deploy/nginx-courses.conf` в существующий TLS-server `miem.space`, проверить `nginx -t`, затем выполнить разрешённый reload. Конфиг передаёт исходный URI без удаления `/courses`; `/courses-other` не захватывается. Фактический путь Nginx-конфига должен быть подтверждён read-only preflight, а не угадан. + +## Порядок первого deployment + +1. Подтвердить актуальные branch/head, независимое review, тесты, сборку, Docker daemon/диск, свободный порт и отсутствие существующей локации `/courses`. +2. Сохранить текущую конфигурацию Nginx. Использовать отдельную базу `miem_courses_links` и отдельную LOGIN-роль без SUPERUSER/CREATEDB/CREATEROLE. Не использовать учётную запись и таблицы существующих приложений. Перед изменением уже заполненной базы сделать pg_dump и проверить возможность восстановления; на первом размещении новая база пустая. +3. Собрать exact commit, запустить только новый сервис. Проверить localhost healthcheck и `/courses`, статические файлы и шрифты. +4. Включить Nginx location, выполнить syntax check/reload через разрешённый канал. Проверить HTTPS страницу, healthcheck и ручной шаг проверки, затем перезапуск сервиса с сохранением данных. +5. При неудаче вернуть прежний Nginx-конфиг, проверить и reload; остановить только новый сервис. Базу и роль не удалять. При обновлении вернуть предыдущий совместимый image tag, сохранив PostgreSQL. Не применять `compose down -v` или общий prune. + +Обновление Nginx обычно не требует остановки существующих сервисов; фактический риск и разрешённый способ reload определяются инфраструктурным runbook. DATABASE_URL передаётся Docker Compose из окружения либо через защищённый deployment env-file; его содержимое не публикуется и не коммитится. В этом checkout production deployment не выполнен. + +## Проверки + +`TEST_DATABASE_URL` должен вести только на локальный `sverka_test`; DB-тесты создают и удаляют собственные случайные схемы и отказываются работать с другой базой. `pnpm test` проверяет извлечение ссылок, классификацию, URL со скобками, запрет непубличных IP, атомарный rollback/replay очереди, единственный общий запуск, lease, создание/изменение/отключение расписания, московское время и переходы календаря, однократный claim, skip пересекающихся запусков и выполнение worker без браузера. Node HTTP-загрузчик закрепляет проверенный DNS-адрес в сокете и проверяет каждый redirect; TLS проверяет исходное имя хоста. Прокси из environment не используются загрузчиком. diff --git a/app/api/audit/route.ts b/app/api/audit/route.ts new file mode 100644 index 0000000..d2c770a --- /dev/null +++ b/app/api/audit/route.ts @@ -0,0 +1,15 @@ +import {dashboard,start,step} from '../../../lib/scanner.ts'; +import {saveSchedule} from '../../../lib/scheduler.ts'; +export const dynamic='force-dynamic'; +export const runtime='nodejs'; +export async function GET(){try{return Response.json(await dashboard(),{headers:{'Cache-Control':'no-store'}});}catch{return Response.json({error:'Не удалось прочитать результаты. Попробуйте обновить страницу.'},{status:503});}} +export async function POST(request:Request){ +const origin=request.headers.get('origin'); +// Production uses a fixed public origin. Next's internal URL may use localhost +// during development even when the browser connected to 127.0.0.1. +const host=request.headers.get('host'); +const expectedOrigin=process.env.COURSES_ORIGIN??(process.env.NODE_ENV!=='production'&&host?new URL(request.url).protocol+'//'+host:null); +if(!origin||!expectedOrigin||origin!==expectedOrigin)return Response.json({error:'Запрос с другого сайта запрещён'},{status:403}); +try{const body=await request.json() as {action?:string;force?:boolean;schedule?:unknown};if(body.action==='schedule'){ + try{return Response.json(await saveSchedule(body.schedule));}catch(error){if(error instanceof RangeError)return Response.json({error:error.message},{status:400});throw error;} +}if(body.action==='start')return Response.json(await start(body.force===true));if(body.action==='step')return Response.json(await step());return Response.json({error:'Неизвестное действие'},{status:400});}catch{return Response.json({error:'Не удалось выполнить шаг. Сохранённый прогресс можно продолжить.'},{status:503});}} diff --git a/app/api/health/route.ts b/app/api/health/route.ts new file mode 100644 index 0000000..2db5b83 --- /dev/null +++ b/app/api/health/route.ts @@ -0,0 +1,5 @@ +import {database} from '../../../lib/store'; +import {VERSION} from '../../../lib/model'; +export const dynamic='force-dynamic'; +export const runtime='nodejs'; +export async function GET(){try{await (await database()).query('SELECT 1');return Response.json({status:'ok',version:VERSION},{headers:{'Cache-Control':'no-store'}});}catch{return Response.json({status:'error'},{status:503});}} diff --git a/app/dashboard.tsx b/app/dashboard.tsx new file mode 100644 index 0000000..6c42451 --- /dev/null +++ b/app/dashboard.tsx @@ -0,0 +1,35 @@ +'use client'; +import {useEffect,useRef,useState} from 'react'; +import {ArrowUpRight,Download,BookOpen,Check,ChevronDown,ExternalLink,FileSearch,Link2,LoaderCircle,Pause,Play,Search,ShieldCheck,SlidersHorizontal,X,RefreshCw} from 'lucide-react'; +import SchedulePanel from './schedule-panel'; +import {PROGRAMS,LABELS,VERSION,DEFAULT_SCHEDULE,type Dashboard,type Run} from '../lib/model'; +const date=(v?:string)=>v?new Date(v).toLocaleString('ru-RU',{timeZone:'Europe/Moscow',day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit'}):'Ещё не проверено'; +export default function Audit(){ +const [data,setData]=useState({run:null,courses:[],checks:{},programs:PROGRAMS,schedule:{...DEFAULT_SCHEDULE}}),[loaded,setLoaded]=useState(false); +const [loading,setLoading]=useState(true),[active,setActive]=useState(false),[error,setError]=useState(''),[program,setProgram]=useState('all'),[query,setQuery]=useState(''),[status,setStatus]=useState('issues'),[view,setView]=useState('links'),[force,setForce]=useState(false),[expanded,setExpanded]=useState(''),[limit,setLimit]=useState(40); +const running=useRef(false),mounted=useRef(true); +async function refresh(){const r=await fetch('/courses/api/audit',{cache:'no-store'}),d=await r.json() as Dashboard & {error?:string};if(!r.ok)throw new Error(d.error);if(mounted.current){setData(d);setLoaded(true);}} +useEffect(()=>{mounted.current=true;refresh().catch(e=>setError(e.message)).finally(()=>setLoading(false));return()=>{mounted.current=false;running.current=false;};},[]); +async function post(action:string){const r=await fetch('/courses/api/audit',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action,force})}),d=await r.json() as Run & {error?:string};if(!r.ok)throw new Error(d.error);return d as Run;} +async function launch(){if(running.current)return;running.current=true;setActive(true);setError('');try{let run=data.run?.status==='running'?data.run:await post('start');setData(d=>({...d,run}));while(running.current&&run?.status==='running'){run=await post('step');if(!mounted.current)break;setData(d=>({...d,run}));await refresh();await new Promise(r=>setTimeout(r,350));}}catch(e){setError((e as Error).message);}finally{running.current=false;if(mounted.current){setActive(false);refresh().catch(()=>{});}}} +function pause(){running.current=false;} +const serverActive=data.run?.source==='schedule'&&data.run.status==='running'; +useEffect(()=>{if(active||(!data.schedule.enabled&&!serverActive))return;const timer=setInterval(()=>{refresh().catch(e=>setError(e.message));},3000);return()=>clearInterval(timer);},[active,data.schedule.enabled,serverActive]); +const run=data.run,courses=data.courses.filter(c=>program==='all'||c.program===program); +const rows=courses.flatMap(course=>course.refs.map(ref=>({course,ref,result:data.checks[ref.url]}))),problems=rows.filter(r=>r.result&&r.result.status!=='ok'); +const visible=rows.filter(r=>(status==='all'||status==='issues'&&r.result&&r.result.status!=='ok'||status==='pending'&&!r.result||r.result?.status===status)&&(!query||(r.ref.title+' '+r.ref.url+' '+r.course.name).toLowerCase().includes(query.toLowerCase()))); +const shownCourses=courses.filter(c=>!query||(c.name+' '+c.url).toLowerCase().includes(query.toLowerCase())); +useEffect(()=>setLimit(40),[query,status,program,view]); +function exportCsv(){const rows=[['Программа','Курс','Ссылка на курс','Литература','URL','Статус','Причина','Проверено'],...visible.map(r=>[PROGRAMS.find(p=>p.id===r.course.program)?.name??'',r.course.name,r.course.url,r.ref.title,r.ref.url,r.result?LABELS[r.result.status]:'Не проверено',r.result?.reason??'',r.result?.checkedAt??''])];const csv='\uFEFF'+rows.map(row=>row.map(v=>'"'+String(v).replace(/^[=+@-]/,"'$&").replaceAll('"','""')+'"').join(';')).join('\r\n');const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv;charset=utf-8'}));a.download='sverka-links.csv';a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1000);} +const selected=PROGRAMS.find(p=>p.id===program),percent=run?.phase==='links'?Math.round(run.checked/Math.max(1,run.totalLinks)*100):run?.phase==='courses'?Math.round(run.processed/Math.max(1,run.totalCourses)*100):0; +return
К результатам
НИУ ВШЭ / Учебная литература

{view==='method'?'Как устроена сверка':view==='courses'?'Учебные курсы':'Ссылки под наблюдением'}

{view==='method'?'Что проверяется и как читать результаты.':'Проверяйте литературу. Находите недоступные материалы.'}

5 программединый реестр
+{error&&
{error}
} +{view==='method'?

От программы — к материалу

Сверка читает текущий учебный год на пяти страницах ВШЭ, обходит пагинацию и открывает каждый курс. Из разделов основной и дополнительной литературы извлекаются кликабельные ссылки и адреса, записанные обычным текстом.

Вы управляете проверкой

Нажмите «Запустить проверку» и оставьте вкладку открытой. Можно приостановить проверку, закрыть вкладку и позже продолжить сохранённый запуск. Одновременно выполняется один общий запуск. Приостановка в этой вкладке не останавливает проверку у другого посетителя.

Проверка по расписанию

Настройте ежедневный или еженедельный запуск и время по Москве. Расписание общее для всех посетителей и сохраняется на сервере. Автоматическая проверка работает без браузера. Изменение или отключение расписания влияет на будущие запуски; уже начатая проверка продолжается. Если другая проверка ещё идёт, совпавший запуск пропускается. После простоя сервера выполняется один просроченный запуск, без очереди пропущенных повторений.

Изменения вместо повторений

Страницы курсов загружаются и сравниваются по контрольной сумме. Неизменившиеся курсы сохраняют результаты и исходные даты проверки ссылок. Одинаковые адреса в изменившихся курсах проверяются один раз за запуск. Полная проверка игнорирует кэш: используйте её, чтобы обнаружить изменения на внешних сайтах при неизменной программе курса.

Успешный ответ — ещё не полный текст

Различаются 404/410, вход, ограничения доступа, CAPTCHA, ошибки сервера и тайм-ауты. «Доступна» означает успешный ответ сервера. Карточка книги может открываться, а текст — требовать подписку. Ограничения определяются по признакам на странице и требуют ручного подтверждения. Сверка не обходит вход и защиту сайта.

Границы проверки

Проверяется литература в HTML-программах. Если раздел отсутствует, доступен только во вложении или не читается, курс получает отдельную отметку. Архивные годы и навигационные ссылки не проверяются. Старые результаты сохраняются с датами; непрочитанные и исчезнувшие курсы отмечаются как не обновлённые.

:<> +

{serverActive?'Выполняется проверка по расписанию':active?'Сверяем источники':run?.status==='running'?'Проверка ожидает продолжения':run?'Результаты сохранены':'Начните с первой проверки'}

{active||serverActive?run?.message:run?`Последний запуск: ${date(run.startedAt)} · МСК`:'Все курсы пяти программ. Только реальные результаты.'}

{serverActive?:active?:}
{run?.status==='running'&&
{run.phase==='catalogs'?`Каталоги: ${run.visited.length}`:run.phase==='courses'?`Курсы: ${run.processed} / ${run.totalCourses}`:`Адреса: ${run.checked} / ${run.totalLinks}`}{serverActive?'Выполняется без открытой вкладки':'Оставьте вкладку открытой'}
}
+{loaded?setData(d=>({...d,schedule}))}/>:

{loading?'Загружаем настройки расписания…':'Расписание недоступно. Повторите загрузку через сообщение об ошибке выше.'}

}
{problems.length||'—'}ссылок требуют внимания
{courses.length||'—'}курсов в реестре
{rows.filter(r=>r.result?.status==='ok').length||'—'}ссылок доступны
{run?.reused??'—'}курсов взято из кэша
+{run&&run.errors.length>0&&
Не удалось прочитать {run.errors.length} источников — охват неполный{run.errors.map((e,i)=>

{e.url}
{e.reason}

)}
} +

{view==='courses'?'Каталог курсов':'Результаты проверки'}{view==='courses'?shownCourses.length:visible.length}

{view==='links'&&}
{selected&&
{selected.name}Каталог ВШЭ
} +{loading?

Загружаем реестр

:view==='courses'?shownCourses.length?
{shownCourses.slice(0,limit).map(c=>
{c.name}

{PROGRAMS.find(p=>p.id===c.program)?.name}

{c.issue&&

{c.issue}

}{date(c.checkedAt)} · {c.refs.length} адресов {c.reused?'· из кэша':''}{run&&c.seen!==run.id?' · не обновлён в текущем запуске':''}
{c.issue?'Нужна проверка':c.refs.length?'Литература найдена':'Нет ссылок'}
)}
::visible.length?<>
Материал и источникРезультатПроверено, МСК
{visible.slice(0,limit).map(r=>{const key=r.course.url+r.ref.url;return
{r.ref.title||r.ref.url}
{r.ref.url}
{r.course.name}{PROGRAMS.find(p=>p.id===r.course.program)?.name}{run&&r.course.seen!==run.id?' · прежний результат':''}
{r.result?LABELS[r.result.status]:'Не проверено'}
{date(r.result?.checkedAt)}{r.result?.code&&HTTP {r.result.code}}
{expanded===key&&

{r.result?.reason??'Адрес ещё не проверен. Запустите или продолжите сверку.'}

{r.result?.finalUrl&&r.result.finalUrl!==r.ref.url&&После перенаправления: {r.result.finalUrl}}{r.course.reused&&Содержание курса не изменилось. Использован сохранённый результат.}
}
;})}
:} +{(view==='courses'?shownCourses.length:visible.length)>limit&&}
Блокировка проверки не означает, что материал удалён. Причину можно уточнить по исходной ссылке.
Проверяем изменения, сохраняем результаты.Неизменившиеся курсы — из кэша
} +
сверка. Независимый инструмент · источники: hse.ruИнтерфейс {VERSION} · Сервер {VERSION}
;} +function Empty({hasRun,filtered,launch}:{hasRun:boolean;filtered:boolean;launch:()=>void}){return

{filtered?'По этим условиям ничего нет':hasRun?'Проблемных ссылок пока нет':'Пока ни одна ссылка не проверена'}

{filtered?'Измените запрос, причину или выбранную программу.':hasRun?'Смотрите охват и список курсов. Непрочитанные источники отмечаются отдельно.':'Запустите сверку, чтобы найти недоступную литературу и увидеть, в каких курсах она используется.'}

{!hasRun&&!filtered&&}
404 / удаленаТребуется входОграничен доступ
;} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..8ddff7b --- /dev/null +++ b/app/globals.css @@ -0,0 +1,8 @@ +@import url('/courses/fonts/fonts.css'); +:root{--paper:#f7f7f2;--white:#fff;--ink:#243a32;--muted:#69756d;--forest:#203e32;--line:#dfe3da;--red:#a54735;--accent:#d9ebbb;--sans:'Golos Text',sans-serif;--serif:'Lora',Georgia,serif} +*{box-sizing:border-box}body{margin:0;color:var(--ink);background:var(--paper);font:14px/1.55 var(--sans);-webkit-font-smoothing:antialiased}button,input,select{font:inherit}button,a,input,select{touch-action:manipulation}button{cursor:pointer;color:inherit}button:disabled{opacity:.45;cursor:default}a{color:inherit;text-decoration:none;text-underline-offset:4px}a:hover{text-decoration:underline}button{border:0;background:none}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid #85a457;outline-offset:4px}::selection{background:#d9ebbb;color:#203e32}input{caret-color:var(--forest)}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-thumb{background:#b7c2b5;border-radius:8px}svg{flex-shrink:0;vertical-align:middle}.shell{min-height:100vh;display:grid;grid-template-columns:275px minmax(0,1fr)}.sidebar{background:var(--forest);color:#f2f5ed;padding:38px 21px 24px;display:flex;flex-direction:column;position:sticky;top:0;height:100vh}.brand{display:flex;align-items:center;font-size:32px;font-weight:600;letter-spacing:-1.3px;padding:0 10px;line-height:1}.brand:hover{text-decoration:none}.brand__mark{border:1px solid #b9ccad;width:38px;height:38px;display:grid;place-items:center;border-radius:50%;margin-right:11px;color:var(--accent)}.brand__dot{color:var(--accent)}.sidebar__description{color:#c1cfbf;font-size:12px;line-height:1.7;margin:24px 10px 39px}.primary-nav{display:grid;gap:6px}.primary-nav button{display:flex;align-items:center;text-align:left;gap:11px;padding:13px 11px;font-size:12px;border-radius:5px;color:#d6dfd1}.primary-nav button.selected{background:#3d5743;color:#f6f9f0}.primary-nav button:hover,.program-nav button:hover{background:#35513e}.primary-nav button span{margin-left:auto;background:#50664c;color:#e0edce;border-radius:3px;padding:0 6px;font-size:11px}.sidebar__label{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:#b6c6af;margin:37px 10px 14px}.program-nav{display:grid;gap:3px}.program-nav button{text-align:left;color:#d9e3d3;display:flex;align-items:flex-start;justify-content:space-between;gap:9px;font-size:12px;line-height:1.55;padding:12px 11px;border-radius:5px}.program-nav button.selected{background:#d9ebbb;color:#223c2e}.program-nav small{display:block;font-size:10px;opacity:.75;margin-top:4px}.program-nav svg{margin-top:2px}.program-nav b{font-weight:500}.sidebar__bottom{border-top:1px solid #4b604a;display:flex;gap:12px;align-items:center;margin: auto 10px 0;padding-top:20px;color:#c5d5b9}.sidebar__bottom p{font-size:11px;line-height:1.7;margin:0}.sidebar__bottom span{color:#b2c1a9}.workspace{min-width:0}.topbar{height:76px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 44px;font-size:11px}.topbar>span{font-weight:500}.topbar__divider{color:#a7b3a2;margin:0 14px}.topbar button{display:flex;gap:10px;align-items:center;color:var(--muted);font-size:11px}main{max-width:1530px;margin:0 auto;padding:44px 44px 0}.page-title{display:flex;justify-content:space-between;gap:20px;align-items:center;margin:0 0 32px}h1{font-family:var(--serif);font-weight:400;font-size:clamp(28px,3vw,43px);line-height:1.2;letter-spacing:-1.3px;margin:0 0 12px}.page-title p{margin:0;color:var(--muted);font-size:12px}.edition{font-size:12px;white-space:nowrap;text-align:right;padding-left:22px;border-left:1px solid var(--line)}.edition span{display:block;color:var(--muted);font-size:10px;margin-top:4px}.control{border:1px solid #ced8c2;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:17px;background:#eef2e7;padding:26px 25px;border-radius:7px}.control__icon{width:47px;height:47px;display:grid;place-items:center;background:#e0e8d5;border-radius:50%;color:#4b6340}.control h2{font-size:15px;margin:0 0 6px;font-weight:550}.control p{font-size:11px;color:#5f7058;margin:0;overflow-wrap:anywhere}.control__actions{display:grid;gap:10px;justify-items:center}.button{display:inline-flex;align-items:center;justify-content:center;gap:9px;border-radius:5px;padding:12px 16px;font-size:12px;font-weight:500;white-space:nowrap;transition:background .15s}.button--primary{background:var(--forest);color:#f4f7ef}.button--primary:hover{background:#345b42}.force{display:flex;align-items:center;font-size:10px;color:#596950;gap:6px;cursor:pointer}.force input{accent-color:#34553a;width:12px;height:12px;margin:0}.scan-progress{grid-column:1/-1;border-top:1px solid #d4dfcb;padding-top:14px;font-size:11px;color:#536a48}.scan-progress>div{display:flex;justify-content:space-between;gap:10px}progress{width:100%;height:5px;accent-color:var(--forest);margin-top:10px;display:block}.summary{display:grid;grid-template-columns:1.2fr 1fr 1fr 1fr;padding:30px 0 28px;border-bottom:1px solid var(--line);gap:18px;margin-bottom:34px}.summary>div{display:flex;flex-direction:column;padding-left:24px;border-left:1px solid var(--line)}.summary>div:first-child{padding-left:0;border:0}.summary__number{font-size:31px;font-weight:450;letter-spacing:-1px;line-height:1.2;font-variant-numeric:tabular-nums}.summary__number--problem{color:var(--red)}.summary>div>span:last-child{font-size:10px;color:var(--muted);margin-top:8px}.results{border:1px solid var(--line);border-radius:6px;background:white;overflow:hidden}.results__heading{display:flex;justify-content:space-between;align-items:center;padding:23px 23px 18px;gap:15px}.results__heading h2{display:flex;align-items:center;gap:10px;margin:0;font-size:17px;font-weight:550}.results__heading h2 span{color:var(--muted);background:var(--paper);border:1px solid var(--line);padding:0 6px;border-radius:4px;font-size:10px;font-weight:400}.text-button{display:inline-flex;gap:8px;align-items:center;color:#47633c;font-size:11px;padding:4px}.text-button:hover{color:#172d21;text-decoration:underline;text-underline-offset:4px}.filters{display:flex;gap:12px;padding:0 23px 20px}.search{display:flex;align-items:center;gap:10px;border:1px solid var(--line);background:#fbfcf9;border-radius:4px;padding:10px 12px;flex:1;min-width:0;color:#82907b}.search input{min-width:0;width:100%;border:0;background:transparent;outline:none;color:var(--ink);font-size:11px}.search:focus-within{outline:2px solid #85a457;outline-offset:2px}.search input::placeholder{color:#6e7c65}.search button{padding:0}.status-select{display:flex;align-items:center;gap:7px;border:1px solid var(--line);padding:0 10px;border-radius:4px;font-size:11px}.status-select select{appearance:none;border:0;max-width:200px;background:transparent;color:var(--ink);cursor:pointer;font-size:11px;padding:11px 0}.table-head,.link-row{display:grid;grid-template-columns:minmax(0,1fr) 155px 112px;gap:15px}.table-head{padding:11px 23px;font-size:10px;color:var(--muted);background:#f5f7f1;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.link-row{padding:20px 23px;border-bottom:1px solid #e7ebe3;align-items:start}.link-row:hover{background:#fcfdf9}.link-row__main{display:flex;gap:12px;min-width:0}.link-row__main>div{min-width:0}.status-dot{width:7px;height:7px;border-radius:50%;background:#8b9585;display:inline-block;flex-shrink:0}.link-row__main>.status-dot{margin-top:7px}.status-dot--broken{background:#b24e3c}.status-dot--auth{background:#ad7929}.status-dot--blocked,.status-dot--restricted{background:#9c6a43}.status-dot--network,.status-dot--timeout,.status-dot--unknown{background:#7b668e}.status-dot--ok{background:#4e7847}.material-name{font-size:12px;font-weight:500;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;line-height:1.65;overflow-wrap:anywhere}.material-name svg{margin-left:4px}.material-url{font-size:10px;color:#718068;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;margin:4px 0 10px}.source-course{font-size:10px;display:flex;gap:5px;align-items:center;color:#486640}.program-name{display:block;font-size:9px;color:var(--muted);margin-top:4px}.badge{font-size:9px;line-height:1.4;padding:5px 7px;border-radius:4px;display:inline-block;background:#f0f1ec;color:#5a6755;white-space:nowrap}.badge--broken{background:#f9e8e2;color:#9b402f}.badge--auth,.badge--restricted,.badge--blocked{background:#f6eddb;color:#80571c}.badge--network,.badge--timeout,.badge--unknown{background:#f0eaf4;color:#705080}.badge--ok{background:#eaf2df;color:#42602f}.reason-toggle{font-size:10px;display:flex;align-items:center;gap:5px;padding:7px 0;color:var(--muted)}.link-row__date{font-size:10px;color:#69765f;font-variant-numeric:tabular-nums}.link-row__date small{display:block;font-size:9px;color:#829079;margin-top:5px}.reason{grid-column:1/-1;padding:14px 18px;background:#f5f7f0;border-radius:4px;font-size:12px;overflow-wrap:anywhere}.reason p{margin:0}.reason a,.reason small{display:block;margin-top:8px;font-size:11px}.empty{min-height:300px;padding:36px 25px 27px;display:flex;flex-direction:column;align-items:center;text-align:center}.empty__drawing{position:relative;width:72px;height:72px;background:#f0f4e8;border-radius:50%;display:grid;place-items:center;color:#7a9563;margin-bottom:17px}.empty__drawing>span{position:absolute;bottom:3px;right:0;background:#dce9cc;color:#4e7037;display:grid;place-items:center;width:25px;height:25px;border:3px solid white;border-radius:50%}.empty h3{font-size:17px;font-weight:500;margin:0 0 9px}.empty p{font-size:11px;color:var(--muted);max-width:380px;line-height:1.8;margin:0 0 15px}.empty__legend{display:flex;flex-wrap:wrap;justify-content:center;gap:22px;margin-top:27px;color:#6b7764;font-size:9px}.empty__legend>span{display:flex;gap:6px;align-items:center}.results__note{border-top:1px solid var(--line);padding:13px 20px;display:flex;align-items:center;gap:8px;color:#6d7b63;background:#fafbf7;font-size:9px}.bottom-note{display:flex;justify-content:space-between;gap:15px;margin-top:20px;font-size:10px;color:#74816b}.bottom-note>span:last-child{display:flex;gap:7px;align-items:center}footer{display:flex;justify-content:space-between;gap:20px;align-items:center;border-top:1px solid var(--line);padding:22px 0;margin-top:60px;font-size:10px;color:#738068}footer>span:first-child{font-size:13px;font-weight:600;color:#4c6342}footer>span>span{font-size:9px;font-weight:400;color:#74816b;margin-left:12px}.error-banner{padding:15px;border:1px solid #e2b4a4;background:#fff1e9;display:flex;justify-content:space-between;gap:20px;font-size:12px;margin-bottom:20px;border-radius:5px;color:#873d2a}.error-banner button{text-decoration:underline}.source-errors{background:#fff4e0;padding:14px 18px;margin-bottom:20px;color:#825b25;font-size:12px;border-radius:5px}.source-errors summary{cursor:pointer}.source-errors p{font-size:11px;overflow-wrap:anywhere}.scope{padding:0 23px 18px;display:flex;align-items:center;gap:10px;color:#566f45;font-size:11px}.scope a{margin-left:auto;font-size:10px}.course-list article{padding:20px 23px;border-top:1px solid var(--line);display:flex;justify-content:space-between;gap:20px}.course-list article>div{min-width:0}.course-name{font-size:13px;font-weight:500}.course-name svg{margin-left:6px}.course-list p{font-size:10px;color:var(--muted);margin:5px 0}.course-list .course-issue{color:#9b5b2c;font-size:11px}.course-list small{font-size:9px;color:#76846c}.course-list .badge{align-self:start}.load-more{display:block;margin:15px auto;padding:10px 20px;border:1px solid var(--line);border-radius:4px;font-size:11px}.method{max-width:720px;padding:10px 0}.method h2{font-family:var(--serif);font-size:25px;font-weight:400;margin:28px 0 10px}.method p{color:#52654b;line-height:1.9;margin:0 0 24px}.spin{animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.skip{position:absolute;left:15px;top:-100px;background:var(--accent);color:var(--forest);padding:10px;z-index:99}.skip:focus{top:12px} +@media(min-width:1500px){.shell{grid-template-columns:290px minmax(0,1fr)}main{padding:55px 64px 0}.topbar{padding:0 64px}.sidebar{padding-top:42px}.control{padding:30px}.summary{padding:35px 0}.empty{min-height:340px}} +@media(max-width:1150px){.shell{grid-template-columns:235px minmax(0,1fr)}.sidebar{padding:28px 13px 20px}.sidebar__description{margin-bottom:28px}.sidebar__label{font-size:9px}.program-nav button{font-size:11px;padding:10px}.primary-nav button{font-size:11px}.topbar{padding:0 27px}main{padding:35px 27px 0}.control{grid-template-columns:auto minmax(0,1fr);padding:20px}.control__actions{grid-column:2;justify-items:start}.edition{display:none}.summary{gap:10px}.summary>div{padding-left:14px}.table-head,.link-row{grid-template-columns:minmax(0,1fr) 130px}.link-row__date{grid-column:2}.table-head>span:last-child{display:none}.filters{flex-wrap:wrap}.search{flex-basis:260px}footer{flex-wrap:wrap;gap:8px}.sidebar__bottom{margin-top:25px}.sidebar{overflow-y:auto}.bottom-note{flex-wrap:wrap}} +@media(max-width:760px){.shell{display:block}.sidebar{height:auto;position:relative;padding:22px 20px 14px;overflow:visible}.brand{font-size:28px;padding:0}.brand__mark{width:32px;height:32px}.sidebar__description,.sidebar__label,.sidebar__bottom{display:none}.primary-nav{display:flex;margin-top:23px;gap:10px}.primary-nav button{font-size:11px;padding:10px;flex:1;justify-content:center}.program-nav{display:flex;overflow-x:auto;gap:7px;margin-top:15px;padding-bottom:5px}.program-nav button{white-space:nowrap;flex-shrink:0;padding:7px 11px;max-width:235px;overflow:hidden;align-items:center;background:#2d4c38;font-size:10px}.program-nav button>span{overflow:hidden;text-overflow:ellipsis}.program-nav small,.program-nav svg{display:none}.program-nav b{margin-left:4px}.topbar{height:54px;padding:0 20px;font-size:9px}.topbar__divider{margin:0 8px}.topbar button{font-size:9px;gap:5px}main{padding:28px 20px 0}.page-title{margin-bottom:23px}h1{font-size:31px;letter-spacing:-.8px}.page-title p{font-size:11px;max-width:290px;line-height:1.8}.control{gap:12px;padding:19px 16px;grid-template-columns:auto minmax(0,1fr)}.control__icon{width:38px;height:38px}.control h2{font-size:13px}.control p{font-size:10px}.control__actions{grid-column:1/-1;justify-items:stretch;margin-top:6px}.force{justify-content:center}.summary{grid-template-columns:1fr 1fr;gap:23px 10px;margin-bottom:25px;padding:25px 0}.summary>div{padding-left:17px}.summary>div:nth-child(3){padding-left:0;border:0}.summary__number{font-size:27px}.summary>div>span:last-child{font-size:10px}.results__heading{padding:18px 14px 15px;gap:6px}.results__heading h2{font-size:14px;gap:7px}.text-button{font-size:10px;gap:5px}.filters{padding:0 14px 16px;gap:8px}.search{padding:10px;font-size:10px}.status-select{width:100%}.status-select select{flex:1;max-width:none}.table-head{display:none}.link-row{padding:18px 14px;grid-template-columns:minmax(0,1fr) auto;gap:8px 12px}.link-row__main{grid-column:1/-1}.link-row__result{grid-column:1;padding-left:19px;display:flex;gap:10px;align-items:center}.link-row__date{grid-column:2;font-size:9px}.link-row__date small{display:none}.material-name{font-size:12px}.program-name{font-size:9px}.empty{padding:29px 16px 24px;min-height:320px}.empty h3{font-size:15px}.empty p{font-size:11px}.empty__legend{gap:13px;font-size:9px}.results__note{font-size:9px;padding:12px 14px;align-items:start}.results__note svg{margin-top:2px}.bottom-note{font-size:9px}.course-list article{padding:17px 14px;flex-direction:column;gap:9px}.course-list .badge{align-self:flex-start}.scope{padding:0 14px 15px;flex-wrap:wrap;font-size:10px}.scope a{margin-left:0}footer{margin-top:35px;padding:18px 0;font-size:9px;display:grid}footer>span>span{margin-left:6px;font-size:8px}.scan-progress>div{flex-direction:column;gap:4px}.error-banner{font-size:11px}.method h2{font-size:23px}.method p{font-size:13px}} +@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}} +.schedule{margin-top:16px;border-bottom:1px solid var(--line)}.schedule summary{display:flex;align-items:center;gap:12px;cursor:pointer;list-style:none;padding:17px 2px;color:var(--forest)}.schedule summary::-webkit-details-marker{display:none}.schedule__title{font-size:13px;font-weight:500}.schedule__title small{display:block;font-size:10px;font-weight:400;color:var(--muted);margin-top:3px}.schedule__state{margin-left:auto;font-size:10px;color:var(--muted);white-space:nowrap}.schedule__state--on{color:#42602f}.schedule__chevron{transition:transform .15s}.schedule details[open] .schedule__chevron{transform:rotate(180deg)}.schedule__form{padding:8px 0 24px}.schedule__toggle{display:flex;align-items:center;gap:9px;font-size:12px;font-weight:500;cursor:pointer}.schedule input[type=checkbox]{accent-color:var(--forest);width:15px;height:15px;margin:0}.schedule__fields{display:flex;flex-wrap:wrap;gap:16px;margin:20px 0 14px}.schedule__fields label{display:flex;flex-direction:column;gap:7px;font-size:10px;color:var(--muted);min-width:140px;flex:1}.schedule__fields select,.schedule__fields input{border:1px solid #c9d2c3;border-radius:4px;padding:11px 10px;background:var(--white);color:var(--ink);font-size:12px;min-width:0;width:100%;height:42px}.schedule__hint{font-size:11px;line-height:1.8;color:var(--muted);max-width:80ch;margin:10px 0}.schedule__history{font-size:11px;margin:12px 0;color:#52654b}.schedule__error{font-size:11px;line-height:1.7;color:var(--red);margin:12px 0;overflow-wrap:anywhere}.schedule__actions{display:flex;flex-wrap:wrap;gap:16px;align-items:center;margin-top:19px}.schedule__actions>span{display:flex;align-items:center;gap:7px;color:#52654b;font-size:11px}@media(max-width:760px){.schedule summary{gap:9px}.schedule__title{font-size:12px}.schedule__title small{font-size:10px;max-width:230px}.schedule__state{font-size:9px}.schedule__fields{gap:12px}.schedule__fields label{flex-basis:calc(50% - 12px);min-width:120px}.schedule__actions{gap:12px}.schedule__actions .button{width:100%}} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..dcf9c61 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,23 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Сверка — доступность учебной литературы", + description: "Проверка ссылок на литературу в учебных курсах пяти программ НИУ ВШЭ.", + icons: { + icon: "/courses/favicon.svg", + shortcut: "/courses/favicon.svg", + }, +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..1a38709 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,2 @@ +import Audit from './dashboard'; +export default function Home(){return ;} diff --git a/app/schedule-panel.tsx b/app/schedule-panel.tsx new file mode 100644 index 0000000..2fd2235 --- /dev/null +++ b/app/schedule-panel.tsx @@ -0,0 +1,30 @@ +'use client'; +import {useEffect,useState} from 'react'; +import {CalendarClock,ChevronDown,LoaderCircle,Check} from 'lucide-react'; +import {type Schedule,type ScheduleSettings} from '../lib/model'; + +const DAYS=['Воскресенье','Понедельник','Вторник','Среда','Четверг','Пятница','Суббота']; +const date=(value:string|null)=>value?new Date(value).toLocaleString('ru-RU',{timeZone:'Europe/Moscow',day:'numeric',month:'long',hour:'2-digit',minute:'2-digit'}):'—'; +export default function SchedulePanel({schedule,onSaved}:{schedule:Schedule;onSaved:(schedule:Schedule)=>void}){ + const [form,setForm]=useState(schedule),[dirty,setDirty]=useState(false),[busy,setBusy]=useState(false),[error,setError]=useState(''),[saved,setSaved]=useState(false); + useEffect(()=>{if(!dirty&&!busy)setForm(schedule);},[schedule,dirty,busy]); + function update(value:Partial){setForm(f=>({...f,...value}));setDirty(true);setSaved(false);setError('');} + async function save(event:React.FormEvent){event.preventDefault();setBusy(true);setError('');setSaved(false); + try{const response=await fetch('/courses/api/audit',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'schedule',schedule:{enabled:form.enabled,frequency:form.frequency,time:form.time,weekday:form.weekday,force:form.force}})}); + const result=await response.json() as Schedule & {error?:string};if(!response.ok)throw new Error(result.error??'Не удалось сохранить расписание.');setForm(result);setDirty(false);setSaved(true);onSaved(result); + }catch(e){setError(e instanceof Error?e.message:'Не удалось сохранить расписание.');}finally{setBusy(false);} + } + return
Проверка по расписанию{schedule.enabled?`Следующий запуск: ${date(schedule.nextRunAt)} · МСК`:'Автоматическая проверка выключена'}{schedule.enabled?'Включено':'Настроить'} +
+ +
+ {form.frequency==='weekly'&&} + +
+

Запланированная проверка выполняется на сервере — вкладку можно закрыть. Новые настройки не прерывают уже начатую проверку. Если другая проверка ещё идёт, совпавший запуск пропускается.

+ {schedule.lastAttemptAt&&

{schedule.lastOutcome==='skipped'?'Последний запуск пропущен: предыдущая проверка ещё не завершена.':'Последний автоматический запуск:'} {date(schedule.lastAttemptAt)} · МСК

} + {schedule.lastError&&

Ошибка фоновой проверки: {schedule.lastError}

} + {error&&

{error}

} +
{saved?<>Расписание сохранено:dirty?'Есть несохранённые изменения':''}
+
; +} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..4fdc7a2 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,30 @@ +name: miem-courses +services: + courses: + build: . + image: miem-courses:1.2.0 + restart: unless-stopped + init: true + user: "1000:1000" + ports: + - "127.0.0.1:${COURSES_PORT:-3011}:3000" + environment: + NODE_ENV: production + NEXT_TELEMETRY_DISABLED: "1" + COURSES_ORIGIN: https://miem.space + DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL for the dedicated courses PostgreSQL database} + networks: + - database + read_only: true + tmpfs: + - /tmp:size=64m,mode=1777 + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + mem_limit: 512m + cpus: 1.0 +networks: + database: + external: true + name: ${COURSES_DATABASE_NETWORK:-miem_workers_default} diff --git a/deploy/nginx-courses.conf b/deploy/nginx-courses.conf new file mode 100644 index 0000000..14e2955 --- /dev/null +++ b/deploy/nginx-courses.conf @@ -0,0 +1,17 @@ +# Include inside the existing TLS server for miem.space; preserve the /courses prefix. +location = /courses { + proxy_pass http://127.0.0.1:3011; + include /etc/nginx/proxy_params; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_read_timeout 35s; +} +location ^~ /courses/ { + proxy_pass http://127.0.0.1:3011; + include /etc/nginx/proxy_params; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_read_timeout 35s; +} diff --git a/lib/checker.ts b/lib/checker.ts new file mode 100644 index 0000000..1cb8780 --- /dev/null +++ b/lib/checker.ts @@ -0,0 +1,99 @@ +import {parseHTML} from 'linkedom'; +import {lookup} from 'node:dns/promises'; +import {request as httpRequest} from 'node:http'; +import {request as httpsRequest} from 'node:https'; +import {createUnzip,createBrotliDecompress} from 'node:zlib'; +import type {Check,Reference} from './model'; +export function safeUrl(value:string,base?:string):string|null{ +try{const u=new URL(value,base),h=u.hostname.toLowerCase(); +if(!['https:','http:'].includes(u.protocol)||u.username||u.password||(u.port&&!['80','443'].includes(u.port)))return null; +if(!h.includes('.')||h.includes(':')||/^\d+[.\d]*$/.test(h)||h.endsWith('.')||/(^|\.)(localhost|local|internal|test|invalid|example|onion|arpa)$/.test(h))return null; +u.hash='';return u.href;}catch{return null;}} +export function clean(text:string){return text.replace(/\s+/g,' ').trim();} +export function trimUrl(value:string){let v=value.replace(/[.,;:!?]+$/g,'');for(const [open,close] of [['(',')'],['[',']'],['{','}']])while(v.endsWith(close)&&v.split(close).length>v.split(open).length)v=v.slice(0,-1);return v;} +export function parseCatalog(html:string,url:string,root:string){ +const {document}=parseHTML(html),courses=new Map(),pages=new Set(); +for(const a of document.querySelectorAll('a[href]')){const href=safeUrl(a.getAttribute('href')!,url);if(!href)continue; +const u=new URL(href),r=new URL(root);if(u.origin!==r.origin)continue; +if(new RegExp('^'+r.pathname+'/[0-9]+\\.html$').test(u.pathname))courses.set(u.origin+u.pathname,{url:u.origin+u.pathname,name:clean(a.textContent??'')}); +if(new RegExp('^'+r.pathname+'/page[0-9]+\\.html$').test(u.pathname)&&(url===root||u.search===new URL(url).search))pages.add(href);} +return {courses:[...courses.values()],pages:[...pages]};} +export function parseCourse(html:string,url:string){ +const {document}=parseHTML(html),refs=new Map(); +const sections=[...document.querySelectorAll('.pud__section')].filter(s=>/список литературы|основная литература|дополнительная литература|reading list|bibliography/i.test(s.querySelector('h4,h3,h2')?.textContent??'')); +let found=sections.length>0; +if(!found){const h=[...document.querySelectorAll('h2,h3,h4')].find(h=>/^(список литературы|литература|reading list|bibliography)$/i.test(clean(h.textContent??'')));if(h){found=true;let n=h.nextElementSibling;while(n&&!/^H[1-4]$/.test(n.tagName)){sections.push(n);n=n.nextElementSibling;}}} +for(const s of sections){const items=[...s.querySelectorAll('li')];for(const item of items.length?items:[s]){ +const title=clean(item.textContent??''),urls=[...item.querySelectorAll('a[href]')].map(a=>a.getAttribute('href')!); +urls.push(...(title.match(/https?:\/\/[^\s<>"«»]+/gi)??[]).map(trimUrl)); +for(const raw of urls){const normalized=safeUrl(raw,url);if(normalized&&normalized!==url)refs.set(normalized,{url:normalized,title:title.slice(0,2000)});}}} +const content=[...document.querySelectorAll('.pud__section')].map(s=>clean(s.textContent??'')).join('\n')||clean(document.querySelector('main')?.textContent??document.querySelector('h1')?.textContent??''); +return {name:clean(document.querySelector('h1')?.textContent??''),refs:[...refs.values()],literature:found,content};} +export async function fingerprint(v:string){return [...new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(v)))].map(n=>n.toString(16).padStart(2,'0')).join('');} +export function publicIp(ip:string){ +if(ip.includes(':')){const fields=ip.toLowerCase().split(':');return /^[23][0-9a-f]{3}:/i.test(ip)&&fields[0]!=='2002'&&fields[0]!=='3fff'&&!(fields[0]==='2001'&&(parseInt(fields[1]||'0',16)<=0x1ff||parseInt(fields[1],16)===0xdb8));} +const p=ip.split('.').map(Number);if(p.length!==4||p.some(n=>!Number.isInteger(n)||n<0||n>255))return false; +const [a,b,c]=p;return !(a===0||a===10||a===127||a>=224||a===169&&b===254||a===172&&b>=16&&b<=31||a===192&&(b===168||b===0||b===88&&c===99)||a===100&&b>=64&&b<=127||a===198&&(b===18||b===19||b===51&&c===100)||a===203&&b===0&&c===113); +} +export async function resolvePublicHost(host:string){ + const addresses=await lookup(host,{all:true,verbatim:true}); + if(!addresses.length)throw new Error('DNS: адрес сервера не найден'); + if(addresses.some(a=>!publicIp(a.address)))throw new Error('DNS указывает на непубличный адрес; запрос запрещён'); + return addresses.find(a=>a.family===4)??addresses[0]; +} +export async function retrieve(raw:string,source=false){ + let url=safeUrl(raw);if(!url)throw new Error('Адрес не разрешён политикой безопасности'); + const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),12000); + const aborted=new Promise((_,reject)=>controller.signal.addEventListener('abort',()=>reject(new DOMException('Истекло время ожидания','AbortError')),{once:true})); + try{ + for(let hop=0;hop<5;hop++){ + const target=new URL(url),ip=await Promise.race([resolvePublicHost(target.hostname),aborted]); + // Pin the validated address into this socket. Redirects create a new socket and repeat DNS validation. + const result=await new Promise<{code:number;location?:string;body:string;type:string}>((resolve,reject)=>{ + const request=(target.protocol==='https:'?httpsRequest:httpRequest)(target,{ + agent:false,family:ip.family,signal:controller.signal, + lookup:(_hostname,_options,callback)=>callback(null,ip.address,ip.family), + headers:{'User-Agent':'Sverka/1.1 (academic bibliography check)','Accept':'text/html,application/pdf;q=0.9,*/*;q=0.8','Accept-Encoding':'identity'}, + },response=>{ + const code=response.statusCode??0,type=String(response.headers['content-type']??''),location=response.headers.location; + if(location&&code>=300&&code<400){response.destroy();resolve({code,location,body:'',type});return;} + if(!source&&!/html|text\//i.test(type)){response.destroy();resolve({code,body:'',type});return;} + void(async()=>{ + const encoding=String(response.headers['content-encoding']??''); + const stream=/gzip|deflate/i.test(encoding)?response.pipe(createUnzip()):/br/i.test(encoding)?response.pipe(createBrotliDecompress()):response; + // pipe() does not forward source errors to a decompressor by itself. + response.on('error',error=>stream.destroy(error)); + response.on('aborted',()=>stream.destroy(new Error('Соединение прервано'))); + request.once('error',error=>{response.destroy();stream.destroy(error);}); + const chunks:Buffer[]=[];let size=0; + try{for await(const chunk of stream){const bytes=Buffer.from(chunk);size+=bytes.length;if(size>(source?3000000:1500000))throw new Error('Превышен допустимый размер загрузки');chunks.push(bytes);} + const charset=/charset=([\w-]+)/i.exec(type)?.[1]??'utf-8';let body:string;try{body=new TextDecoder(charset).decode(Buffer.concat(chunks));}catch{body=Buffer.concat(chunks).toString('utf8');}resolve({code,body,type}); + }catch(error){response.destroy();stream.destroy();reject(error);} + })(); + });request.on('error',reject);request.end(); + }); + if(result.location&&result.code>=300&&result.code<400){const next=safeUrl(result.location,url);if(!next)throw new Error('Небезопасное перенаправление');url=next;continue;} + return {...result,url}; + } + throw new Error('Слишком много перенаправлений'); + }finally{clearTimeout(timer);} +} +export function classify(url:string,r:{code:number;url:string;body:string;type:string}):Check{ +const out:Check={url,finalUrl:r.url,code:r.code,checkedAt:new Date().toISOString(),status:'ok',reason:'Сервер вернул материал. Доступ к полному тексту может потребовать ручной проверки.'}; +if(r.code===404||r.code===410)return {...out,status:'broken',reason:`HTTP ${r.code}: ${r.code===404?'страница не найдена':'материал удалён'}.`}; +if(r.code===401||/(?:\/|\.)(?:login|signin|sign-in|sso|auth)(?:[/.?]|$)/i.test(r.url))return {...out,status:'auth',reason:'Требуется авторизация или выполнено перенаправление на вход.'}; +if(r.code===403||r.code===429)return {...out,status:'blocked',reason:`HTTP ${r.code}: сервер ограничил автоматический доступ. Проверьте вручную.`}; +if(r.code>=400)return {...out,status:'network',reason:`HTTP ${r.code}: сервер не смог предоставить материал.`}; +if(r.code<200||r.code>=300)return {...out,status:'unknown',reason:`Необычный ответ HTTP ${r.code}; нужна ручная проверка.`}; +if(r.code===204||r.code===205)return {...out,status:'unknown',reason:`HTTP ${r.code}: сервер вернул ответ без содержимого. Доступ к материалу не подтверждён.`}; +if(!/html|text\//i.test(r.type))return out; +if(!r.body.trim())return {...out,status:'unknown',reason:'Сервер вернул пустую страницу. Доступ к материалу не подтверждён.'}; +const {document}=parseHTML(r.body);const scripted=!!document.querySelector('script');document.querySelectorAll('script,style,nav,footer,header').forEach(n=>n.remove()); +const title=clean(document.querySelector('title,h1')?.textContent??''),text=clean(document.querySelector('body')?.textContent??document.documentElement?.textContent??r.body); +if(/captcha|verify you are human|checking your browser|just a moment|докажите, что вы|проверка браузера|доступ ограничен/i.test(title+' '+text.slice(0,2500)))return {...out,status:'blocked',reason:'CAPTCHA или антибот-защита: автоматически подтвердить доступность нельзя.'}; +if(/^(404|страница не найдена|page not found|not found)/i.test(title))return {...out,status:'broken',reason:'Сайт показывает «не найдено» при успешном HTTP-ответе.'}; +if(document.querySelector('input[type="password"]')&&text.length<10000)return {...out,status:'auth',reason:'Обнаружена форма входа с паролем; доступ к материалу не подтверждён.'}; +if(/(для (?:чтения|доступа|просмотра).{0,80}(?:авториз|войд|подписк)|(?:авторизуйтесь|войдите).{0,80}(?:чита|доступ|просмотр)|sign in to (?:read|access|view)|purchase (?:this|access)|institutional access|доступ (?:к полному тексту|по подписке)|читать по подписке)/i.test(text))return {...out,status:'restricted',reason:'Для полного текста может потребоваться вход, подписка или доступ организации. Проверьте вручную.'}; +if(/html/i.test(r.type)&&!text)return {...out,status:'unknown',reason:'Получена пустая HTML-страница. Доступ к материалу не подтверждён.'}; +if(/html/i.test(r.type)&&((scripted&&text.length<200)||/^(загрузка|loading)[.\s…]*$/i.test(text)))return {...out,status:'unknown',reason:'Получена оболочка страницы с загрузкой через JavaScript. Доступ к материалу не подтверждён; откройте ссылку вручную.'};return out;} +export async function checkLink(url:string):Promise{try{return classify(url,await retrieve(url));}catch(e){const error=e as Error;return {url,status:error.name==='AbortError'?'timeout':'network',reason:error.name==='AbortError'?'Сервер не ответил за 12 секунд.':`Не удалось загрузить: ${error.message.slice(0,180)}`,checkedAt:new Date().toISOString()};}} diff --git a/lib/model.ts b/lib/model.ts new file mode 100644 index 0000000..3b0c588 --- /dev/null +++ b/lib/model.ts @@ -0,0 +1,17 @@ +export const VERSION='1.2.0'; +export const PROGRAMS=[ +{id:'it',name:'Цифровые технологии и телекоммуникации',level:'Бакалавриат',url:'https://www.hse.ru/ba/it/courses'}, +{id:'is',name:'Информационная безопасность',level:'Бакалавриат',url:'https://www.hse.ru/ba/is/courses'}, +{id:'cosec',name:'Компьютерная безопасность',level:'Специалитет',url:'https://www.hse.ru/ba/cosec/courses'}, +{id:'internet',name:'Интернет вещей и киберфизические системы',level:'Магистратура',url:'https://www.hse.ru/ma/internet/courses'}, +{id:'cyber',name:'Информационная безопасность и технологии искусственного интеллекта',level:'Магистратура',url:'https://www.hse.ru/ma/cyber/courses'}]; +export type Status='ok'|'broken'|'auth'|'blocked'|'timeout'|'network'|'restricted'|'unknown'; +export const LABELS:Record={ok:'Доступна',broken:'Не найдена',auth:'Требуется вход',blocked:'Доступ запрещён',timeout:'Нет ответа',network:'Ошибка соединения',restricted:'Ограничен доступ',unknown:'Нужна проверка'}; +export type Check={url:string;finalUrl?:string;status:Status;reason:string;code?:number;checkedAt:string}; +export type Reference={url:string;title:string}; +export type Course={url:string;name:string;program:string;hash?:string;refs:Reference[];issue?:string;checkedAt:string;seen:string;reused:boolean;literature:boolean}; +export type Run={id:string;source?:'manual'|'schedule';status:'running'|'complete';startedAt:string;finishedAt?:string;force:boolean;phase:'catalogs'|'courses'|'links'|'done';catalogs:{url:string;program:string}[];visited:string[];courses:{url:string;name:string;program:string}[];links:string[];seen:string[];errors:{url:string;reason:string}[];processed:number;reused:number;checked:number;totalCourses:number;totalLinks:number;message:string}; +export type ScheduleSettings={enabled:boolean;frequency:'daily'|'weekly';time:string;weekday:number;force:boolean}; +export type Schedule=ScheduleSettings & {nextRunAt:string|null;updatedAt:string|null;lastRunAt:string|null;lastAttemptAt:string|null;lastOutcome:'started'|'skipped'|null;lastError:string|null}; +export const DEFAULT_SCHEDULE:Schedule={enabled:false,frequency:'daily',time:'09:00',weekday:1,force:false,nextRunAt:null,updatedAt:null,lastRunAt:null,lastAttemptAt:null,lastOutcome:null,lastError:null}; +export type Dashboard={run:Run|null;courses:Course[];checks:Record;programs:typeof PROGRAMS;schedule:Schedule}; diff --git a/lib/scanner.ts b/lib/scanner.ts new file mode 100644 index 0000000..d17a38b --- /dev/null +++ b/lib/scanner.ts @@ -0,0 +1,46 @@ +import {PROGRAMS,DEFAULT_SCHEDULE,type Schedule,type Run,type Course,type Check} from './model.ts'; +import {checkLink,retrieve,parseCatalog,parseCourse,fingerprint} from './checker.ts'; +import {database,transaction,type Connection} from './store.ts'; +async function read(key:string):Promise{const {rows}=await (await database()).query<{value:string}>('SELECT value FROM records WHERE key=$1',[key]);return rows[0]?JSON.parse(rows[0].value):null;} +export async function getRun(connection?:Connection):Promise{const {rows}=await (connection??await database()).query<{data:string}>('SELECT data FROM scan WHERE id=1');return rows[0]?JSON.parse(rows[0].data):null;} +export async function dashboard(){const run=await getRun(),{rows}=await (await database()).query<{key:string;value:string}>('SELECT key,value FROM records'),courses:Course[]=[],checks:Record={}; +let schedule:Schedule={...DEFAULT_SCHEDULE};for(const row of rows){if(row.key.startsWith('course:'))courses.push(JSON.parse(row.value));else if(row.key.startsWith('link:'))checks[row.key.slice(5)]=JSON.parse(row.value);else if(row.key==='schedule')schedule=JSON.parse(row.value);}return {run,courses,checks,programs:PROGRAMS,schedule};} +export async function start(force:boolean,source:'manual'|'schedule'='manual',connection?:Connection){const run:Run={id:crypto.randomUUID(),source,status:'running',phase:'catalogs',startedAt:new Date().toISOString(),force,catalogs:PROGRAMS.map(p=>({url:p.url,program:p.id})),visited:[],courses:[],links:[],seen:[],errors:[],processed:0,reused:0,checked:0,totalCourses:0,totalLinks:0,message:'Читаем каталоги пяти программ'}; +const db=connection??await database();await db.query("INSERT INTO scan(id,data,lease,token) VALUES(1,$1,0,'') ON CONFLICT(id) DO UPDATE SET data=excluded.data,lease=0,token='' WHERE scan.data::jsonb->>'status'!='running'",[JSON.stringify(run)]);return getRun(db);} +export async function step(){const token=crypto.randomUUID(),now=Date.now(),db=await database(); +const {rows}=await db.query<{data:string}>('UPDATE scan SET lease=$1,token=$2 WHERE id=1 AND lease<$3 RETURNING data',[now+90000,token,now]);if(!rows[0])return getRun();const run:Run=JSON.parse(rows[0].data);const pending:{key:string;value:unknown}[]=[];let succeeded=false; +try{if(run.status!=='running')return run; +if(run.phase==='catalogs'){ +const task=run.catalogs.shift();if(task&&!run.visited.includes(task.url)){run.visited.push(task.url);run.message=`Читаем каталог: ${PROGRAMS.find(p=>p.id===task.program)?.name}`; +try{const page=await retrieve(task.url,true);if(page.code!==200)throw new Error(`Каталог вернул HTTP ${page.code}`); +const root=PROGRAMS.find(p=>p.id===task.program)!.url,parsed=parseCatalog(page.body,task.url,root);if(!parsed.courses.length)throw new Error('Курсы не найдены: каталог недоступен или его структура изменилась'); +for(const c of parsed.courses)if(!run.seen.includes(c.url)){run.seen.push(c.url);run.courses.push({...c,program:task.program});} +for(const url of parsed.pages)if(!run.visited.includes(url)&&!run.catalogs.some(p=>p.url===url))run.catalogs.push({url,program:task.program});run.totalCourses=run.seen.length; +}catch(e){run.errors.push({url:task.url,reason:(e as Error).message});}} +if(!run.catalogs.length)run.phase='courses'; +}else if(run.phase==='courses'){ +const task=run.courses.shift();if(task){run.message=`Читаем курс: ${task.name}`;const old=await read('course:'+task.url); +let course:Course={...task,refs:old?.refs??[],checkedAt:old?.checkedAt??'',seen:run.id,reused:false,literature:false}; +try{const page=await retrieve(task.url,true);if(page.code!==200)throw new Error(`Страница курса вернула HTTP ${page.code}`); +const parsed=parseCourse(page.body,task.url);if(!parsed.name)throw new Error('Не удалось прочитать программу курса');const hash=await fingerprint(parsed.name+parsed.content+JSON.stringify(parsed.refs)); +if(!run.force&&old?.hash===hash&&!old.issue){course={...old,seen:run.id,reused:true};run.reused++;} +else{course={...course,name:parsed.name,hash,refs:parsed.refs,literature:parsed.literature,checkedAt:new Date().toISOString()};if(!parsed.literature)course.issue='Раздел литературы не найден. Проверьте программу курса вручную.'; +for(const ref of course.refs)if(!run.links.includes(ref.url))run.links.push(ref.url);} +}catch(e){course.issue=(e as Error).message;} +pending.push({key:'course:'+task.url,value:course});run.processed++;} +if(!run.courses.length){run.phase='links';run.totalLinks=run.links.length;} +}else if(run.phase==='links'){ +const urls=run.links.splice(0,3);run.message='Проверяем доступность материалов';const results=await Promise.all(urls.map(checkLink));for(const result of results){pending.push({key:'link:'+result.url,value:result});run.checked++;} +if(!run.links.length){run.status='complete';run.phase='done';run.finishedAt=new Date().toISOString();run.message=run.errors.length?'Проверка завершена с ошибками чтения каталогов':'Проверка завершена';} +} +succeeded=true; +}finally{ +if(succeeded){ +try{await transaction(async client=>{ +const {rows}=await client.query<{token:string}>('SELECT token FROM scan WHERE id=1 FOR UPDATE'); +if(rows[0]?.token!==token)throw new Error('Порция проверки уже выполняется другим процессом'); +for(const record of pending)await client.query('INSERT INTO records(key,value) VALUES($1,$2) ON CONFLICT(key) DO UPDATE SET value=excluded.value',[record.key,JSON.stringify(record.value)]); +await client.query("UPDATE scan SET data=$1,lease=0,token='' WHERE id=1 AND token=$2",[JSON.stringify(run),token]); +});}catch(error){await db.query("UPDATE scan SET lease=0,token='' WHERE id=1 AND token=$1",[token]);throw error;} +}else await db.query("UPDATE scan SET lease=0,token='' WHERE id=1 AND token=$1",[token]); +}return run;} diff --git a/lib/schedule-worker.ts b/lib/schedule-worker.ts new file mode 100644 index 0000000..3d24c02 --- /dev/null +++ b/lib/schedule-worker.ts @@ -0,0 +1,10 @@ +import {setTimeout as sleep} from 'node:timers/promises'; +import {schedulerStep,recordSchedulerError} from './scheduler.ts'; + +// The worker shares SQLite leases with the API; restarting the container resumes scheduled runs. +for(;;){ + let busy=false; + try{busy=await schedulerStep();} + catch(error){console.error('Scheduled audit failed; retrying in five seconds');try{await recordSchedulerError(error);}catch(storageError){console.error('Cannot save scheduler error');}} + await sleep(busy?350:5000); +} diff --git a/lib/scheduler.ts b/lib/scheduler.ts new file mode 100644 index 0000000..f9d2c16 --- /dev/null +++ b/lib/scheduler.ts @@ -0,0 +1,66 @@ +import {DEFAULT_SCHEDULE,type Schedule,type ScheduleSettings} from './model.ts'; +import {database,transaction,type Connection} from './store.ts'; +import {getRun,start,step} from './scanner.ts'; + +export function validateSchedule(value:unknown):ScheduleSettings{ + if(!value||typeof value!=='object')throw new RangeError('Укажите настройки расписания.'); + const s=value as Record; + if(typeof s.enabled!=='boolean'||typeof s.force!=='boolean')throw new RangeError('Укажите состояние расписания и режим кэша.'); + if(s.frequency!=='daily'&&s.frequency!=='weekly')throw new RangeError('Выберите ежедневную или еженедельную проверку.'); + if(typeof s.time!=='string'||!/^([01]\d|2[0-3]):[0-5]\d$/.test(s.time))throw new RangeError('Укажите время в формате ЧЧ:ММ.'); + if(!Number.isInteger(s.weekday)||Number(s.weekday)<0||Number(s.weekday)>6)throw new RangeError('Выберите день недели.'); + return {enabled:s.enabled,frequency:s.frequency,time:s.time,weekday:s.weekday as number,force:s.force}; +} +const clock=new Intl.DateTimeFormat('en-CA',{timeZone:'Europe/Moscow',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit',hourCycle:'h23'}); +function moscowWallTime(instant:number){const parts=Object.fromEntries(clock.formatToParts(instant).map(p=>[p.type,p.value]));return Date.UTC(+parts.year,+parts.month-1,+parts.day,+parts.hour,+parts.minute,+parts.second);} +export function nextOccurrence(settings:ScheduleSettings,now=Date.now()):string{ + const local=new Date(moscowWallTime(now)),[hour,minute]=settings.time.split(':').map(Number); + for(let days=0;days<=7;days++){ + const wall=Date.UTC(local.getUTCFullYear(),local.getUTCMonth(),local.getUTCDate()+days,hour,minute); + if(settings.frequency==='weekly'&&new Date(wall).getUTCDay()!==settings.weekday)continue; + let instant=wall;for(let i=0;i<3;i++)instant+=wall-moscowWallTime(instant); + if(instant>now)return new Date(instant).toISOString(); + } + throw new Error('Не удалось рассчитать следующий запуск.'); +} +export async function getSchedule(connection?:Connection):Promise{ + const {rows}=await (connection??await database()).query<{value:string}>("SELECT value FROM records WHERE key='schedule'"); + return rows[0]?JSON.parse(rows[0].value):{...DEFAULT_SCHEDULE}; +} +async function persist(schedule:Schedule,client:Connection){await client.query("INSERT INTO records(key,value) VALUES('schedule',$1) ON CONFLICT(key) DO UPDATE SET value=excluded.value",[JSON.stringify(schedule)]);} +export async function saveSchedule(value:unknown,now=Date.now()):Promise{ + const settings=validateSchedule(value); + return transaction(async client=>{ + await client.query('SELECT pg_advisory_xact_lock(72847322)'); + const schedule:Schedule={...await getSchedule(client),...settings,updatedAt:new Date(now).toISOString(),nextRunAt:settings.enabled?nextOccurrence(settings,now):null}; + await persist(schedule,client);return schedule; + }); +} +export async function claimScheduledRun(now=Date.now()){ + await transaction(async client=>{ + await client.query('SELECT pg_advisory_xact_lock(72847322)'); + const schedule=await getSchedule(client); + if(schedule.enabled&&schedule.nextRunAt&&Date.parse(schedule.nextRunAt)<=now){ + const run=await getRun(client);schedule.lastAttemptAt=new Date(now).toISOString();schedule.nextRunAt=nextOccurrence(schedule,now); + if(run?.status==='running')schedule.lastOutcome='skipped'; + else{ + const started=await start(schedule.force,'schedule',client); + if(started?.source==='schedule'){schedule.lastRunAt=new Date(now).toISOString();schedule.lastOutcome='started';schedule.lastError=null;} + else schedule.lastOutcome='skipped'; + } + await persist(schedule,client); + } + }); +} +export async function schedulerStep(now=Date.now()){ + await claimScheduledRun(now); + const run=await getRun(); + if(run?.source==='schedule'&&run.status==='running'){await step();return true;} + return false; +} +export async function recordSchedulerError(error:unknown){ + await transaction(async client=>{ + await client.query('SELECT pg_advisory_xact_lock(72847322)'); + const schedule=await getSchedule(client);schedule.lastError='Не удалось обработать очередь. Сервер повторит попытку через пять секунд.';await persist(schedule,client); + }); +} diff --git a/lib/store.ts b/lib/store.ts new file mode 100644 index 0000000..92c382d --- /dev/null +++ b/lib/store.ts @@ -0,0 +1,26 @@ +import {Pool,type PoolClient} from 'pg'; +export type Connection=Pool|PoolClient; +const runtime=globalThis as typeof globalThis & {coursesPool?:Pool;coursesReady?:Promise}; +async function inTransaction(pool:Pool,work:(client:PoolClient)=>Promise):Promise{ + const client=await pool.connect(); + try{await client.query('BEGIN');const result=await work(client);await client.query('COMMIT');return result;} + catch(error){await client.query('ROLLBACK').catch(()=>{});throw error;} + finally{client.release();} +} +export async function database():Promise{ + if(!runtime.coursesPool){ + if(!process.env.DATABASE_URL)throw new Error('DATABASE_URL is required'); + runtime.coursesPool=new Pool({connectionString:process.env.DATABASE_URL,max:4,connectionTimeoutMillis:5000,idleTimeoutMillis:30000,statement_timeout:15000}); + runtime.coursesPool.on('error',()=>console.error('PostgreSQL connection unavailable')); + } + const pool=runtime.coursesPool; + runtime.coursesReady??=inTransaction(pool,async client=>{ + await client.query('SELECT pg_advisory_xact_lock(72847321)'); + await client.query('CREATE TABLE IF NOT EXISTS records(key TEXT PRIMARY KEY NOT NULL,value TEXT NOT NULL)'); + await client.query("CREATE TABLE IF NOT EXISTS scan(id INTEGER PRIMARY KEY,data TEXT NOT NULL,lease BIGINT NOT NULL DEFAULT 0,token TEXT NOT NULL DEFAULT '')"); + }); + try{await runtime.coursesReady;}catch(error){runtime.coursesReady=undefined;throw error;} + return pool; +} +export async function transaction(work:(client:PoolClient)=>Promise):Promise{return inTransaction(await database(),work);} +export async function closeDatabase(){const pool=runtime.coursesPool;runtime.coursesPool=undefined;runtime.coursesReady=undefined;if(pool)await pool.end();} diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..d4c02be --- /dev/null +++ b/next.config.ts @@ -0,0 +1,9 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + output: 'standalone', + basePath: '/courses', + serverExternalPackages: ['linkedom','pg'], +}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 0000000..6a3cc2f --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "sverka", + "version": "1.2.0", + "private": true, + "engines": { + "node": ">=24.0.0" + }, + "scripts": { + "dev": "node server/runtime.mjs --dev", + "build": "next build --webpack", + "start": "node server/runtime.mjs", + "test": "node --experimental-strip-types --test tests/*.test.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "next": "16.3.4", + "react": "19.2.6", + "react-dom": "19.2.6", + "linkedom": "0.18.14", + "lucide-react": "1.63.0", + "pg": "8.16.3" + }, + "devDependencies": { + "@types/node": "22.19.19", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "typescript": "5.9.3", + "@types/pg": "8.15.5" + }, + "type": "module", + "packageManager": "pnpm@11.25.0" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..996d89f --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1291 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + linkedom: + specifier: 0.18.14 + version: 0.18.14 + lucide-react: + specifier: 1.63.0 + version: 1.63.0(@types/react@19.2.14)(react@19.2.6) + next: + specifier: 16.3.4 + version: 16.3.4(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@22.19.19)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + pg: + specifier: 8.16.3 + version: 8.16.3 + react: + specifier: 19.2.6 + version: 19.2.6 + react-dom: + specifier: 19.2.6 + version: 19.2.6(react@19.2.6) + devDependencies: + '@types/node': + specifier: 22.19.19 + version: 22.19.19 + '@types/pg': + specifier: 8.15.5 + version: 8.15.5 + '@types/react': + specifier: 19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.14) + typescript: + specifier: 5.9.3 + version: 5.9.3 + +packages: + + '@acemir/cssom@0.9.32': + resolution: {integrity: sha512-OijrAUu7rrJhSIoHNg6LMPS5e344nwUHA+1EFYYjrh64D+GmytUsfBQjci45TjT+rd5Spsp8B5xsZDT+TgyXXQ==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.29.11': + resolution: {integrity: sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.10': + resolution: {integrity: sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.11': + resolution: {integrity: sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.10': + resolution: {integrity: sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.11': + resolution: {integrity: sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==} + engines: {node: '>=6.9.0'} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@next/env@16.3.4': + resolution: {integrity: sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==} + + '@next/swc-darwin-arm64@16.3.4': + resolution: {integrity: sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.3.4': + resolution: {integrity: sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.3.4': + resolution: {integrity: sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@16.3.4': + resolution: {integrity: sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@16.3.4': + resolution: {integrity: sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@16.3.4': + resolution: {integrity: sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@16.3.4': + resolution: {integrity: sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.3.4': + resolution: {integrity: sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@types/node@22.19.19': + resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} + + '@types/pg@8.15.5': + resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + baseline-browser-mapping@2.11.34: + resolution: {integrity: sha512-Aqr8mA1kIvyJ3UY5YAY7ib8LSo1DmDMbMnVqGPtsKW1LxaFQFwn9Eb3yvq2fUKJsRvaXjnAV7R5osqETapzNiA==} + engines: {node: '>=6.0.0'} + hasBin: true + + boolbase@2.0.0: + resolution: {integrity: sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==} + engines: {node: '>=20.19.0'} + + browserslist@4.29.5: + resolution: {integrity: sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001819: + resolution: {integrity: sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + css-select@7.0.0: + resolution: {integrity: sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==} + engines: {node: '>=20.19.0'} + + css-what@8.0.0: + resolution: {integrity: sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==} + engines: {node: '>=20.19.0'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + dom-serializer@3.1.1: + resolution: {integrity: sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==} + engines: {node: '>=20.19.0'} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domelementtype@3.0.0: + resolution: {integrity: sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==} + engines: {node: '>=20.19.0'} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domhandler@6.0.1: + resolution: {integrity: sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==} + engines: {node: '>=20.19.0'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + domutils@4.0.2: + resolution: {integrity: sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==} + engines: {node: '>=20.19.0'} + + electron-to-chromium@1.5.467: + resolution: {integrity: sha512-Nw9X371rtZmYcBSnEijnCDONILjs93biCZO34juC7MBOmK8E+GFnyD4T/WvyrlJkppul7oxg6TwvtKst9SODTw==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} + engines: {node: '>=20.19.0'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + linkedom@0.18.14: + resolution: {integrity: sha512-BRUnkOm4YR5vo+qI3j0EbungB0vZPSCue5AdnTQRaxAGSbsOZiBFRO0rh4zlo8qXUhli8pxvnpEXDklMnlq2Rg==} + engines: {node: '>=16'} + peerDependencies: + canvas: '>= 2' + peerDependenciesMeta: + canvas: + optional: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@1.63.0: + resolution: {integrity: sha512-GkBfQG+FUJ41x6y3/lNXiOziVU0Rcqqq0TCzhHS8avsFXHZmOdmfBqsVTvHSaVmbI+wbdGkxHRmhYWhD3N4/yA==} + peerDependencies: + '@types/react': '*' + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + next@16.3.4: + resolution: {integrity: sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-releases@2.0.60: + resolution: {integrity: sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==} + engines: {node: '>=18'} + + nth-check@3.0.1: + resolution: {integrity: sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==} + engines: {node: '>=20.19.0'} + + pg-cloudflare@1.4.1: + resolution: {integrity: sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==} + + pg-connection-string@2.14.1: + resolution: {integrity: sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.16.1: + resolution: {integrity: sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.16.3: + resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uhyphen@0.2.0: + resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + update-browserslist-db@1.3.4: + resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@acemir/cssom@0.9.32': {} + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + optional: true + + '@babel/compat-data@7.29.7': + optional: true + + '@babel/core@7.29.7(supports-color@10.2.2)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) + '@babel/helpers': 7.29.10 + '@babel/parser': 7.29.11 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.10(supports-color@10.2.2) + '@babel/types': 7.29.11 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@10.2.2) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.11 + '@babel/types': 7.29.11 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + optional: true + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.29.5 + lru-cache: 5.1.1 + semver: 6.3.1 + optional: true + + '@babel/helper-globals@7.29.7': + optional: true + + '@babel/helper-module-imports@7.29.7(supports-color@10.2.2)': + dependencies: + '@babel/traverse': 7.29.10(supports-color@10.2.2) + '@babel/types': 7.29.11 + transitivePeerDependencies: + - supports-color + optional: true + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)': + dependencies: + '@babel/core': 7.29.7(supports-color@10.2.2) + '@babel/helper-module-imports': 7.29.7(supports-color@10.2.2) + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.10(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + optional: true + + '@babel/helper-string-parser@7.29.11': + optional: true + + '@babel/helper-validator-identifier@7.29.7': + optional: true + + '@babel/helper-validator-option@7.29.7': + optional: true + + '@babel/helpers@7.29.10': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.11 + optional: true + + '@babel/parser@7.29.11': + dependencies: + '@babel/types': 7.29.11 + optional: true + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.11 + '@babel/types': 7.29.11 + optional: true + + '@babel/traverse@7.29.10(supports-color@10.2.2)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.11 + '@babel/template': 7.29.7 + '@babel/types': 7.29.11 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + optional: true + + '@babel/types@7.29.11': + dependencies: + '@babel/helper-string-parser': 7.29.11 + '@babel/helper-validator-identifier': 7.29.7 + optional: true + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.4 + optional: true + + '@img/sharp-darwin-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.4 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + optional: true + + '@img/sharp-linux-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.4 + optional: true + + '@img/sharp-linux-arm@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.4 + optional: true + + '@img/sharp-linux-ppc64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.4 + optional: true + + '@img/sharp-linux-riscv64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.4 + optional: true + + '@img/sharp-linux-s390x@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.4 + optional: true + + '@img/sharp-linux-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + optional: true + + '@img/sharp-wasm32@0.35.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-win32-arm64@0.35.5': + optional: true + + '@img/sharp-win32-ia32@0.35.5': + optional: true + + '@img/sharp-win32-x64@0.35.5': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + optional: true + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + optional: true + + '@jridgewell/resolve-uri@3.1.2': + optional: true + + '@jridgewell/sourcemap-codec@1.6.0': + optional: true + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + optional: true + + '@next/env@16.3.4': {} + + '@next/swc-darwin-arm64@16.3.4': + optional: true + + '@next/swc-darwin-x64@16.3.4': + optional: true + + '@next/swc-linux-arm64-gnu@16.3.4': + optional: true + + '@next/swc-linux-arm64-musl@16.3.4': + optional: true + + '@next/swc-linux-x64-gnu@16.3.4': + optional: true + + '@next/swc-linux-x64-musl@16.3.4': + optional: true + + '@next/swc-win32-arm64-msvc@16.3.4': + optional: true + + '@next/swc-win32-x64-msvc@16.3.4': + optional: true + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@types/node@22.19.19': + dependencies: + undici-types: 6.21.0 + + '@types/pg@8.15.5': + dependencies: + '@types/node': 22.19.19 + pg-protocol: 1.16.1 + pg-types: 2.2.0 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + baseline-browser-mapping@2.11.34: {} + + boolbase@2.0.0: {} + + browserslist@4.29.5: + dependencies: + baseline-browser-mapping: 2.11.34 + caniuse-lite: 1.0.30001819 + electron-to-chromium: 1.5.467 + node-releases: 2.0.60 + update-browserslist-db: 1.3.4(browserslist@4.29.5) + optional: true + + caniuse-lite@1.0.30001819: {} + + client-only@0.0.1: {} + + convert-source-map@2.0.0: + optional: true + + css-select@7.0.0: + dependencies: + boolbase: 2.0.0 + css-what: 8.0.0 + domhandler: 6.0.1 + domutils: 4.0.2 + nth-check: 3.0.1 + + css-what@8.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3(supports-color@10.2.2): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 + optional: true + + detect-libc@2.1.2: + optional: true + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + dom-serializer@3.1.1: + dependencies: + domelementtype: 3.0.0 + domhandler: 6.0.1 + entities: 8.1.0 + + domelementtype@2.3.0: {} + + domelementtype@3.0.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domhandler@6.0.1: + dependencies: + domelementtype: 3.0.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + domutils@4.0.2: + dependencies: + dom-serializer: 3.1.1 + domelementtype: 3.0.0 + domhandler: 6.0.1 + + electron-to-chromium@1.5.467: + optional: true + + entities@4.5.0: {} + + entities@7.0.1: {} + + entities@8.1.0: {} + + escalade@3.2.0: + optional: true + + gensync@1.0.0-beta.2: + optional: true + + html-escaper@3.0.3: {} + + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + + js-tokens@4.0.0: + optional: true + + jsesc@3.1.0: + optional: true + + json5@2.2.3: + optional: true + + linkedom@0.18.14: + dependencies: + '@acemir/cssom': 0.9.32 + css-select: 7.0.0 + html-escaper: 3.0.3 + htmlparser2: 10.1.0 + uhyphen: 0.2.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + optional: true + + lucide-react@1.63.0(@types/react@19.2.14)(react@19.2.6): + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + ms@2.1.3: + optional: true + + nanoid@3.3.20: {} + + next@16.3.4(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@22.19.19)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + '@next/env': 16.3.4 + '@swc/helpers': 0.5.23 + baseline-browser-mapping: 2.11.34 + caniuse-lite: 1.0.30001819 + postcss: 8.5.23 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.6) + optionalDependencies: + '@next/swc-darwin-arm64': 16.3.4 + '@next/swc-darwin-x64': 16.3.4 + '@next/swc-linux-arm64-gnu': 16.3.4 + '@next/swc-linux-arm64-musl': 16.3.4 + '@next/swc-linux-x64-gnu': 16.3.4 + '@next/swc-linux-x64-musl': 16.3.4 + '@next/swc-win32-arm64-msvc': 16.3.4 + '@next/swc-win32-x64-msvc': 16.3.4 + sharp: 0.35.5(@types/node@22.19.19) + transitivePeerDependencies: + - '@babel/core' + - '@types/node' + - babel-plugin-macros + + node-releases@2.0.60: + optional: true + + nth-check@3.0.1: + dependencies: + boolbase: 2.0.0 + + pg-cloudflare@1.4.1: + optional: true + + pg-connection-string@2.14.1: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.16.3): + dependencies: + pg: 8.16.3 + + pg-protocol@1.16.1: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.16.3: + dependencies: + pg-connection-string: 2.14.1 + pg-pool: 3.14.0(pg@8.16.3) + pg-protocol: 1.16.1 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.1 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + postcss@8.5.23: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + react-dom@19.2.6(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + + react@19.2.6: {} + + scheduler@0.27.0: {} + + semver@6.3.1: + optional: true + + semver@7.8.5: + optional: true + + sharp@0.35.5(@types/node@22.19.19): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@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 + '@types/node': 22.19.19 + optional: true + + source-map-js@1.2.2: {} + + split2@4.2.0: {} + + styled-jsx@5.1.6(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.6): + dependencies: + client-only: 0.0.1 + react: 19.2.6 + optionalDependencies: + '@babel/core': 7.29.7(supports-color@10.2.2) + + supports-color@10.2.2: + optional: true + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + uhyphen@0.2.0: {} + + undici-types@6.21.0: {} + + update-browserslist-db@1.3.4(browserslist@4.29.5): + dependencies: + browserslist: 4.29.5 + escalade: 3.2.0 + picocolors: 1.1.1 + optional: true + + xtend@4.0.2: {} + + yallist@3.1.1: + optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..9be25d9 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +allowBuilds: + esbuild: true + sharp: true + unrs-resolver: true + workerd: true diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..f61e600 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1 @@ +export default {plugins:{}}; diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..46fc005 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + diff --git a/public/fonts/GolosText-OFL.txt b/public/fonts/GolosText-OFL.txt new file mode 100644 index 0000000..4db2dc9 --- /dev/null +++ b/public/fonts/GolosText-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2019 The Golos Text Project Authors (https://github.com/googlefonts/golos-text) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/fonts/Lora-OFL.txt b/public/fonts/Lora-OFL.txt new file mode 100644 index 0000000..d0ce7fe --- /dev/null +++ b/public/fonts/Lora-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Lora Project Authors (https://github.com/cyrealtype/Lora-Cyrillic), with Reserved Font Name "Lora". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/fonts/font-0.woff2 b/public/fonts/font-0.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..f7e94e02681eec683ddb130d81de572cd0ed2620 GIT binary patch literal 64240 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 literal 0 HcmV?d00001 diff --git a/public/fonts/font-1.woff2 b/public/fonts/font-1.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..02c1aa4bf345fd4a8de11e8d5d3272f81e4b6361 GIT binary patch literal 64232 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 literal 0 HcmV?d00001 diff --git a/public/fonts/font-4.woff2 b/public/fonts/font-4.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..bd2b7cb70e023eb657c1814291262b55ea525329 GIT binary patch literal 132188 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 AMgRZ+ literal 0 HcmV?d00001 diff --git a/public/fonts/font-5.woff2 b/public/fonts/font-5.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..913c1dd9f6623be1724e2071ab399b85dc50a114 GIT binary patch literal 132320 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 child of children)if(child.exitCode===null)child.kill('SIGKILL');},5000);timer.unref();} +for(const child of children){child.on('error',error=>{console.error(error);stop(1);});child.on('exit',(code,signal)=>{if(!stopping)stop(code||1);});} +process.on('SIGINT',()=>stop(0));process.on('SIGTERM',()=>stop(0)); diff --git a/tests/checker.test.ts b/tests/checker.test.ts new file mode 100644 index 0000000..33f5814 --- /dev/null +++ b/tests/checker.test.ts @@ -0,0 +1,19 @@ +import assert from 'node:assert/strict'; +import {readFileSync} from 'node:fs'; +import {test} from 'node:test'; +import {parseCatalog,parseCourse,classify,safeUrl,fingerprint} from '../lib/checker.ts'; +const url='https://www.hse.ru/ba/is/courses/1.html'; +const result=(body='',code=200,finalUrl='https://public.org/book')=>({body,code,url:finalUrl,type:'text/html'}); +const html='Book

Algorithms

'; +test('extracts linked and plain-text bibliography, normalizes and excludes footer',()=>{const p=parseCourse(html,url);assert.equal(p.name,'Algorithms');assert.deepEqual(p.refs.map(r=>r.url),['https://books.org/a','https://books.org/b']);assert.equal(p.literature,true);}); +test('missing bibliography is explicit',()=>assert.equal(parseCourse('

Course

',url).literature,false)); +test('pagination follows current program and excludes archived years',()=>{const root='https://www.hse.ru/ba/is/courses',p=parseCatalog('A2archiveother',root,root);assert.equal(p.courses.length,1);assert.deepEqual(p.pages,[root+'/page2.html?year=2026']);}); +test('classifies errors, login, protection and subscription separately',()=>{assert.equal(classify(url,result('',404)).status,'broken');assert.equal(classify(url,result('',403)).status,'blocked');assert.equal(classify(url,result('',401)).status,'auth');assert.equal(classify(url,result('',200,'https://public.org/login')).status,'auth');assert.equal(classify(url,result('Just a momentChecking your browser')).status,'blocked');assert.equal(classify(url,result('Sign in to read this book')).status,'restricted');assert.equal(classify(url,result('')).status,'auth');assert.equal(classify(url,result('Open book')).status,'ok');}); +test('rejects dangerous and non-http URLs',()=>{for(const v of ['http://127.0.0.1','http://[::1]','http://localhost/a','https://foo.internal','file:///a','https://a:b@public.org','https://public.org:8080'])assert.equal(safeUrl(v),null);assert.equal(safeUrl('https://public.org/book#x'),'https://public.org/book');}); +test('hash detects content changes and remains stable',async()=>{assert.equal(await fingerprint(html),await fingerprint(html));assert.notEqual(await fingerprint(html),await fingerprint(html+'x'));}); +import {publicIp,trimUrl} from '../lib/checker.ts'; +test('preserves balanced parentheses in URLs',()=>{assert.equal(trimUrl('https://public.org/Foo_(bar).'),'https://public.org/Foo_(bar)');assert.equal(trimUrl('https://public.org/book).'),'https://public.org/book');}); +test('rejects private, reserved and loopback DNS answers',()=>{for(const ip of ['127.0.0.1','10.1.2.3','169.254.169.254','100.64.0.1','172.16.2.2','192.168.1.1','192.0.2.1','198.18.1.1','203.0.113.1','::1','fc00::1','fe80::1','2001:db8::1'])assert.equal(publicIp(ip),false,ip);assert.equal(publicIp('1.1.1.1'),true);assert.equal(publicIp('2606:4700:4700::1111'),true);}); +test('does not mark a JavaScript loading shell as accessible',()=>{assert.equal(classify(url,result('
Загрузка...
')).status,'unknown');}); +test('empty HTML and No Content responses do not prove material access',()=>{for(const code of [200,204,205])assert.equal(classify(url,result('',code)).status,'unknown');}); +test('non-successful binary responses do not count as available material',()=>{for(const code of [302,304])assert.equal(classify(url,{...result('',code),type:'application/pdf'}).status,'unknown');assert.equal(classify(url,{...result(),type:'application/pdf'}).status,'ok');}); diff --git a/tests/origin.test.ts b/tests/origin.test.ts new file mode 100644 index 0000000..285954c --- /dev/null +++ b/tests/origin.test.ts @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict'; +import {test} from 'node:test'; +import {POST} from '../app/api/audit/route.ts'; +const request=(origin:string,host:string)=>new Request('http://localhost:5173/courses/api/audit',{method:'POST',headers:{Origin:origin,Host:host,'Content-Type':'application/json'},body:JSON.stringify({action:'invalid'})}); +test('development accepts the actual browser Host, not Next internal localhost',async()=>{ +delete process.env.COURSES_ORIGIN; +assert.equal((await POST(request('http://127.0.0.1:5173','127.0.0.1:5173'))).status,400); +assert.equal((await POST(request('https://other.example','127.0.0.1:5173'))).status,403); +}); +test('canonical production origin works behind a proxy and rejects another site',async()=>{ +process.env.COURSES_ORIGIN='https://miem.space'; +assert.equal((await POST(request('https://miem.space','internal:3000'))).status,400); +assert.equal((await POST(request('https://other.example','internal:3000'))).status,403); +delete process.env.COURSES_ORIGIN; +}); diff --git a/tests/scanner.test.ts b/tests/scanner.test.ts new file mode 100644 index 0000000..8f9519e --- /dev/null +++ b/tests/scanner.test.ts @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict'; +import {test} from 'node:test'; +import {database} from '../lib/store.ts'; +import {dashboard,getRun,start,step} from '../lib/scanner.ts'; +import type {Run} from '../lib/model.ts'; +import {useTestDatabase} from './test-database.ts'; +useTestDatabase('scanner'); +test('only one shared manual run can be started',async()=>{ + const [first,second]=await Promise.all([start(false),start(false)]); + assert.equal(first?.id,second?.id);assert.equal(second?.force,false); +}); +test('a failed record transaction preserves its queue and can resume',async()=>{ + const run=(await getRun())!;run.phase='links';run.links=['http://127.0.0.1/'];run.totalLinks=1;run.checked=0; + const db=await database();await db.query("UPDATE scan SET data=$1,lease=0,token='' WHERE id=1",[JSON.stringify(run)]); + await db.query("CREATE FUNCTION reject_record() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'simulated disk failure'; END $$"); + await db.query('CREATE TRIGGER fail_record BEFORE INSERT ON records FOR EACH ROW EXECUTE FUNCTION reject_record()'); + try{await assert.rejects(step(),/simulated disk failure/);const afterFailure=(await getRun())!;assert.deepEqual(afterFailure.links,run.links);assert.equal(afterFailure.checked,0);assert.equal((await dashboard()).checks['http://127.0.0.1/'],undefined);} + finally{await db.query('DROP TRIGGER fail_record ON records');await db.query('DROP FUNCTION reject_record()');} + const resumed=(await step())!;assert.equal(resumed.status,'complete');assert.equal(resumed.checked,1);assert.equal((await dashboard()).checks['http://127.0.0.1/'].status,'network'); +}); +test('an active lease prevents two workers from processing the same queue',async()=>{ + const run=(await start(false))!;run.phase='links';run.links=['http://127.0.0.1/']; + await (await database()).query('UPDATE scan SET data=$1,lease=$2,token=$3 WHERE id=1',[JSON.stringify(run),Date.now()+60000,'another-worker']); + const unchanged=(await step()) as Run;assert.deepEqual(unchanged.links,run.links);assert.equal(unchanged.checked,0); +}); diff --git a/tests/scheduler.test.ts b/tests/scheduler.test.ts new file mode 100644 index 0000000..fec1398 --- /dev/null +++ b/tests/scheduler.test.ts @@ -0,0 +1,72 @@ +import assert from 'node:assert/strict'; +import {test} from 'node:test'; +import {fileURLToPath} from 'node:url'; +import {spawn} from 'node:child_process'; +import {setTimeout as sleep} from 'node:timers/promises'; +import {database} from '../lib/store.ts'; +import {DEFAULT_SCHEDULE} from '../lib/model.ts'; +import {getRun,start,dashboard} from '../lib/scanner.ts'; +import {nextOccurrence,validateSchedule,saveSchedule,getSchedule,claimScheduledRun} from '../lib/scheduler.ts'; +import {POST} from '../app/api/audit/route.ts'; + +import {useTestDatabase} from './test-database.ts'; +useTestDatabase('scheduler'); +const settings={...DEFAULT_SCHEDULE,enabled:true}; +const now=Date.parse('2026-10-29T05:00:00Z'); +async function reset(){const db=await database();await db.query('DELETE FROM records');await db.query('DELETE FROM scan');} + +test('daily and weekly times use Moscow including midnight, month rollover and exact boundary',()=>{ + assert.equal(nextOccurrence(settings,now),'2026-10-29T06:00:00.000Z'); + assert.equal(nextOccurrence(settings,Date.parse('2026-10-29T06:00:00Z')),'2026-10-30T06:00:00.000Z'); + assert.equal(nextOccurrence({...settings,time:'00:15'},Date.parse('2026-10-31T21:00:00Z')),'2026-10-31T21:15:00.000Z'); + assert.equal(nextOccurrence({...settings,frequency:'weekly',weekday:1},now),'2026-11-02T06:00:00.000Z'); +}); +test('strict validation rejects impossible times and malformed configuration',()=>{ + for(const bad of [null,{}, {...settings,time:'25:00'},{...settings,time:'9:00'},{...settings,time:'12:90'},{...settings,enabled:'true'},{...settings,force:1},{...settings,weekday:7},{...settings,weekday:1.5},{...settings,frequency:'hourly'}])assert.throws(()=>validateSchedule(bad),RangeError); +}); +test('schedule can be created, edited and disabled without dropping history',async()=>{ + await reset();assert.equal((await getSchedule()).enabled,false); + const first=await saveSchedule(settings,now);assert.equal(first.nextRunAt,'2026-10-29T06:00:00.000Z'); + const changed=await saveSchedule({...settings,time:'11:30',frequency:'weekly',weekday:5,force:true},now); + assert.equal(changed.nextRunAt,'2026-10-30T08:30:00.000Z');assert.equal((await dashboard()).schedule.force,true); + const disabled=await saveSchedule({...changed,enabled:false},now);assert.equal(disabled.nextRunAt,null);assert.equal((await getSchedule()).time,'11:30'); +}); +test('a due slot is claimed once and persists a scheduled source and cache choice',async()=>{ + await reset();await saveSchedule({...settings,force:true},now); + const due=Date.parse('2026-10-29T06:00:00Z');await Promise.all([claimScheduledRun(due),claimScheduledRun(due)]); + const run=await getRun();assert.equal(run?.source,'schedule');assert.equal(run?.force,true); + const id=run?.id;await claimScheduledRun(due);assert.equal((await getRun())?.id,id); + assert.equal((await getSchedule()).lastOutcome,'started');assert.equal((await getSchedule()).nextRunAt,'2026-10-30T06:00:00.000Z'); +}); +test('an existing manual run is preserved and the conflicting slot is skipped',async()=>{ + await reset();await saveSchedule(settings,now);const manual=await start(false); + await claimScheduledRun(Date.parse('2026-10-29T06:00:00Z')); + assert.equal((await getRun())?.id,manual?.id);assert.equal((await getRun())?.source,'manual');assert.equal((await getSchedule()).lastOutcome,'skipped'); +}); +test('downtime catches up once rather than replaying every missed day',async()=>{ + await reset();await saveSchedule(settings,now);const later=Date.parse('2026-11-05T08:00:00Z'); + await claimScheduledRun(later);const id=(await getRun())?.id;await claimScheduledRun(later); + assert.equal((await getRun())?.id,id);assert.equal((await getSchedule()).nextRunAt,'2026-11-06T06:00:00.000Z'); +}); +test('failed schedule claim rolls back both the new run and the consumed slot',async()=>{ + await reset();await saveSchedule(settings,now);const original=(await getSchedule()); + const db=await database();await db.query("CREATE FUNCTION reject_schedule() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'schedule disk failure'; END $$"); + await db.query("CREATE TRIGGER fail_schedule BEFORE UPDATE ON records FOR EACH ROW WHEN (NEW.key='schedule') EXECUTE FUNCTION reject_schedule()"); + try{await assert.rejects(claimScheduledRun(Date.parse('2026-10-29T06:00:00Z')),/schedule disk failure/);assert.equal(await getRun(),null);assert.deepEqual((await getSchedule()),original);} + finally{await db.query('DROP TRIGGER fail_schedule ON records');await db.query('DROP FUNCTION reject_schedule()');} +}); +test('schedule API rejects a foreign Origin and invalid input, then persists valid settings',async()=>{ + await reset();process.env.COURSES_ORIGIN='https://miem.space'; + const request=(origin:string,schedule:unknown)=>new Request('http://internal:3000/courses/api/audit',{method:'POST',headers:{Origin:origin,'Content-Type':'application/json'},body:JSON.stringify({action:'schedule',schedule})}); + try{assert.equal((await POST(request('https://other.example',settings))).status,403);assert.equal((await POST(request('https://miem.space',{...settings,time:'99:99'}))).status,400);assert.equal((await POST(request('https://miem.space',settings))).status,200);assert.equal((await getSchedule()).enabled,true);} + finally{delete process.env.COURSES_ORIGIN;} +}); +test('the standalone worker resumes a saved scheduled run without any browser or API client',async()=>{ + await reset();const run=(await start(false,'schedule'))!;run.phase='links';run.links=['http://127.0.0.1/'];run.totalLinks=1; + await (await database()).query('UPDATE scan SET data=$1 WHERE id=1',[JSON.stringify(run)]); + const worker=spawn(process.execPath,['--experimental-strip-types',fileURLToPath(new URL('../lib/schedule-worker.ts',import.meta.url))],{env:{...process.env},stdio:'ignore'}); + const exited=new Promise(resolve=>worker.once('exit',()=>resolve())); + try{const deadline=Date.now()+8000;while((await getRun())?.status!=='complete'&&Date.now(){ + if(!process.env.TEST_DATABASE_URL)throw new Error('Tests require a dedicated local TEST_DATABASE_URL'); + const target=new URL(process.env.TEST_DATABASE_URL); + if(!['127.0.0.1','localhost'].includes(target.hostname)||target.pathname!=='/sverka_test')throw new Error('Tests must use the isolated local sverka_test database'); + schema='sverka_test_'+label+'_'+randomUUID().replaceAll('-','');url=target.href; + const admin=new Pool({connectionString:url}); + try{await admin.query(`CREATE SCHEMA "${schema}"`);}finally{await admin.end();} + target.searchParams.set('options','-c search_path='+schema);process.env.DATABASE_URL=target.href; + }); + after(async()=>{ + await closeDatabase(); + if(!url||!schema)return; + const admin=new Pool({connectionString:url}); + try{await admin.query(`DROP SCHEMA "${schema}" CASCADE`);}finally{await admin.end();} + }); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c757461 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,59 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + }, + "types": [ + "node" + ], + "allowImportingTsExtensions": true + }, + "include": [ + "next-env.d.ts", + "app/**/*.ts", + "app/**/*.tsx", + "lib/model.ts", + "lib/checker.ts", + "lib/scanner.ts", + "lib/store.ts", + "tests/**/*.ts", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "lib/scheduler.ts", + "lib/schedule-worker.ts" + ], + "exclude": [ + "node_modules", + "examples", + "dist", + ".vinext", + ".wrangler", + ".sites-runtime", + "outputs", + "work" + ] +} -- 2.54.0 From 1b87f38c21edcf2a099b18c8662266a5314db284 Mon Sep 17 00:00:00 2001 From: MIEM DEV Date: Thu, 29 Oct 2026 17:35:26 +0300 Subject: [PATCH 02/13] docs: clarify schedule default and active run behavior --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 347e14c..5af3fae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Хранилище перенесено на PostgreSQL для использования существующего сервера VPS; приложению предназначены отдельная база и роль. - Добавлено общее редактируемое расписание: ежедневная или еженедельная проверка по московскому времени, выбор режима кэша, включение и отключение. - Проверки по расписанию выполняются на сервере при закрытом браузере и возобновляются после перезапуска. +- Расписание выключено по умолчанию; изменение или отключение не прерывает уже начатую автоматическую проверку. - Следующий запуск, пропуск совпавшего запуска и ошибки фоновой проверки отображаются в интерфейсе. - Расписание и запуск сохраняются атомарно; совпавшие запуски не дублируются, после простоя выполняется один пропущенный запуск. - При старте приложения одновременно запускаются веб-сервер и worker с общим завершением при сбое одного процесса. -- 2.54.0 From 8534fd27750288510bde330e3a94ff13d1ab7bb0 Mon Sep 17 00:00:00 2001 From: MIEM DEV Date: Thu, 29 Oct 2026 18:20:39 +0300 Subject: [PATCH 03/13] fix: pace outbound requests and respect server cooldowns --- Dockerfile | 2 +- README.md | 4 ++++ compose.yaml | 2 +- lib/checker.ts | 12 +++++++++--- lib/model.ts | 2 +- lib/scanner.ts | 2 +- lib/throttle.ts | 41 +++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- tests/scanner.test.ts | 6 ++++++ tests/throttle.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 lib/throttle.ts create mode 100644 tests/throttle.test.ts diff --git a/Dockerfile b/Dockerfile index 98f4ea3..f3248ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ RUN npm install --global pnpm@11.25.0 COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ RUN pnpm install --frozen-lockfile COPY app ./app -COPY lib/model.ts lib/checker.ts lib/scanner.ts lib/store.ts lib/scheduler.ts lib/schedule-worker.ts ./lib/ +COPY lib/model.ts lib/checker.ts lib/scanner.ts lib/store.ts lib/scheduler.ts lib/schedule-worker.ts lib/throttle.ts ./lib/ COPY public ./public COPY next.config.ts tsconfig.json postcss.config.mjs ./ RUN pnpm build diff --git a/README.md b/README.md index 65d0ab3..a017bc7 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ pnpm dev Страницы курсов загружаются заново, но литература неизменившихся курсов берётся из кэша вместе с исходной датой проверки. Полная проверка без кэша нужна для проверки изменений на внешних сайтах. HTTP200 не доказывает доступ к полному тексту; JavaScript-заглушки, CAPTCHA и признаки подписки отмечаются отдельно. Литература только во вложении отмечается как неполный охват. +## Частота запросов + +Материалы проверяются последовательно. Между запросами к одному хосту выдерживается 3–4 секунды, включая каталоги, курсы и перенаправления. Ограничение общее для API и worker и сохраняется в PostgreSQL. После HTTP429/503 действует пауза из `Retry-After` (не менее минуты, не более суток), после HTTP403 — пять минут. Повторных попыток в цикле нет. Ссылки, пропущенные во время паузы, отмечаются как ограниченные с датой окончания паузы; это не подтверждает, что материал удалён. Их можно обновить последующей полной проверкой. Паузы снижают нагрузку, но не гарантируют отсутствие блокировки со стороны сайта. + ## Расписание В интерфейсе откройте «Проверка по расписанию»: включите автоматический запуск, выберите ежедневное или еженедельное повторение, время по Москве, при необходимости день недели и режим кэша. Сохранение и отключение доступны там же. По умолчанию расписание выключено; оно общее для всех посетителей и хранится в той же базе PostgreSQL. diff --git a/compose.yaml b/compose.yaml index 4fdc7a2..297d790 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,7 +2,7 @@ name: miem-courses services: courses: build: . - image: miem-courses:1.2.0 + image: miem-courses:1.2.1 restart: unless-stopped init: true user: "1000:1000" diff --git a/lib/checker.ts b/lib/checker.ts index 1cb8780..3d43fa4 100644 --- a/lib/checker.ts +++ b/lib/checker.ts @@ -4,6 +4,7 @@ import {request as httpRequest} from 'node:http'; import {request as httpsRequest} from 'node:https'; import {createUnzip,createBrotliDecompress} from 'node:zlib'; import type {Check,Reference} from './model'; +import {waitForHost,limitHost,HostCooldown} from './throttle.ts'; export function safeUrl(value:string,base?:string):string|null{ try{const u=new URL(value,base),h=u.hostname.toLowerCase(); if(!['https:','http:'].includes(u.protocol)||u.username||u.password||(u.port&&!['80','443'].includes(u.port)))return null; @@ -43,19 +44,23 @@ export async function resolvePublicHost(host:string){ } export async function retrieve(raw:string,source=false){ let url=safeUrl(raw);if(!url)throw new Error('Адрес не разрешён политикой безопасности'); + await waitForHost(new URL(url).hostname); const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),12000); const aborted=new Promise((_,reject)=>controller.signal.addEventListener('abort',()=>reject(new DOMException('Истекло время ожидания','AbortError')),{once:true})); try{ for(let hop=0;hop<5;hop++){ - const target=new URL(url),ip=await Promise.race([resolvePublicHost(target.hostname),aborted]); + const target=new URL(url); + if(hop)await waitForHost(target.hostname,controller.signal); + const ip=await Promise.race([resolvePublicHost(target.hostname),aborted]); // Pin the validated address into this socket. Redirects create a new socket and repeat DNS validation. - const result=await new Promise<{code:number;location?:string;body:string;type:string}>((resolve,reject)=>{ + const result=await new Promise<{code:number;location?:string;body:string;type:string;retryAfter?:string}>((resolve,reject)=>{ const request=(target.protocol==='https:'?httpsRequest:httpRequest)(target,{ agent:false,family:ip.family,signal:controller.signal, lookup:(_hostname,_options,callback)=>callback(null,ip.address,ip.family), headers:{'User-Agent':'Sverka/1.1 (academic bibliography check)','Accept':'text/html,application/pdf;q=0.9,*/*;q=0.8','Accept-Encoding':'identity'}, },response=>{ const code=response.statusCode??0,type=String(response.headers['content-type']??''),location=response.headers.location; + if([403,429,503].includes(code)){response.destroy();resolve({code,body:'',type,retryAfter:response.headers['retry-after']});return;} if(location&&code>=300&&code<400){response.destroy();resolve({code,location,body:'',type});return;} if(!source&&!/html|text\//i.test(type)){response.destroy();resolve({code,body:'',type});return;} void(async()=>{ @@ -72,6 +77,7 @@ export async function retrieve(raw:string,source=false){ })(); });request.on('error',reject);request.end(); }); + await limitHost(target.hostname,result.code,result.retryAfter); if(result.location&&result.code>=300&&result.code<400){const next=safeUrl(result.location,url);if(!next)throw new Error('Небезопасное перенаправление');url=next;continue;} return {...result,url}; } @@ -96,4 +102,4 @@ if(document.querySelector('input[type="password"]')&&text.length<10000)return {. if(/(для (?:чтения|доступа|просмотра).{0,80}(?:авториз|войд|подписк)|(?:авторизуйтесь|войдите).{0,80}(?:чита|доступ|просмотр)|sign in to (?:read|access|view)|purchase (?:this|access)|institutional access|доступ (?:к полному тексту|по подписке)|читать по подписке)/i.test(text))return {...out,status:'restricted',reason:'Для полного текста может потребоваться вход, подписка или доступ организации. Проверьте вручную.'}; if(/html/i.test(r.type)&&!text)return {...out,status:'unknown',reason:'Получена пустая HTML-страница. Доступ к материалу не подтверждён.'}; if(/html/i.test(r.type)&&((scripted&&text.length<200)||/^(загрузка|loading)[.\s…]*$/i.test(text)))return {...out,status:'unknown',reason:'Получена оболочка страницы с загрузкой через JavaScript. Доступ к материалу не подтверждён; откройте ссылку вручную.'};return out;} -export async function checkLink(url:string):Promise{try{return classify(url,await retrieve(url));}catch(e){const error=e as Error;return {url,status:error.name==='AbortError'?'timeout':'network',reason:error.name==='AbortError'?'Сервер не ответил за 12 секунд.':`Не удалось загрузить: ${error.message.slice(0,180)}`,checkedAt:new Date().toISOString()};}} +export async function checkLink(url:string):Promise{try{return classify(url,await retrieve(url));}catch(e){const error=e as Error;return {url,status:error instanceof HostCooldown?'blocked':error.name==='AbortError'?'timeout':'network',reason:error.name==='AbortError'?'Сервер не ответил за 12 секунд.':`Не удалось загрузить: ${error.message.slice(0,180)}`,checkedAt:new Date().toISOString()};}} diff --git a/lib/model.ts b/lib/model.ts index 3b0c588..b210bd6 100644 --- a/lib/model.ts +++ b/lib/model.ts @@ -1,4 +1,4 @@ -export const VERSION='1.2.0'; +export const VERSION='1.2.1'; export const PROGRAMS=[ {id:'it',name:'Цифровые технологии и телекоммуникации',level:'Бакалавриат',url:'https://www.hse.ru/ba/it/courses'}, {id:'is',name:'Информационная безопасность',level:'Бакалавриат',url:'https://www.hse.ru/ba/is/courses'}, diff --git a/lib/scanner.ts b/lib/scanner.ts index d17a38b..c4a3360 100644 --- a/lib/scanner.ts +++ b/lib/scanner.ts @@ -30,7 +30,7 @@ for(const ref of course.refs)if(!run.links.includes(ref.url))run.links.push(ref. pending.push({key:'course:'+task.url,value:course});run.processed++;} if(!run.courses.length){run.phase='links';run.totalLinks=run.links.length;} }else if(run.phase==='links'){ -const urls=run.links.splice(0,3);run.message='Проверяем доступность материалов';const results=await Promise.all(urls.map(checkLink));for(const result of results){pending.push({key:'link:'+result.url,value:result});run.checked++;} +const url=run.links.shift();run.message='Проверяем доступность материалов';if(url){const result=await checkLink(url);pending.push({key:'link:'+result.url,value:result});run.checked++;} if(!run.links.length){run.status='complete';run.phase='done';run.finishedAt=new Date().toISOString();run.message=run.errors.length?'Проверка завершена с ошибками чтения каталогов':'Проверка завершена';} } succeeded=true; diff --git a/lib/throttle.ts b/lib/throttle.ts new file mode 100644 index 0000000..6bc9ebc --- /dev/null +++ b/lib/throttle.ts @@ -0,0 +1,41 @@ +import {setTimeout as sleep} from 'node:timers/promises'; +import {transaction} from './store.ts'; + +type HostState={nextAt:number;blockedUntil:number}; +export class HostCooldown extends Error{ + constructor(until:number){super(`Запрос отложен: сервер ограничил доступ. Пауза до ${new Date(until).toISOString()}; доступность материала не подтверждена.`);} +} +// Persist reservations across the API and worker, including container restarts. +async function updateHost(host:string,update:(state:HostState,now:number)=>T){ + return transaction(async client=>{ + const key='throttle:'+host; + await client.query('SELECT pg_advisory_xact_lock(hashtextextended($1,0))',[key]); + const {rows}=await client.query<{value:string}>('SELECT value FROM records WHERE key=$1',[key]); + const state:HostState=rows[0]?JSON.parse(rows[0].value):{nextAt:0,blockedUntil:0}; + const result=update(state,Date.now()); + await client.query('INSERT INTO records(key,value) VALUES($1,$2) ON CONFLICT(key) DO UPDATE SET value=excluded.value',[key,JSON.stringify(state)]); + return result; + }); +} +export async function reserveRequest(host:string){ + return updateHost(host,(state,now)=>{ + if(state.blockedUntil>now)throw new HostCooldown(state.blockedUntil); + const at=Math.max(now,state.nextAt);state.nextAt=at+3000+Math.floor(Math.random()*1001);return at; + }); +} +export async function waitForHost(host:string,signal?:AbortSignal){ + const at=await reserveRequest(host);await sleep(Math.max(0,at-Date.now()),undefined,{signal}); + await updateHost(host,(state,now)=>{if(state.blockedUntil>now)throw new HostCooldown(state.blockedUntil);}); +} +export function retryDelay(value:string|undefined,now:number){ + const milliseconds=value&&/^\d+$/.test(value.trim())?Number(value)*1000:value?Date.parse(value)-now:0; + // Ignore malformed values; keep a minimum pause even if the server says zero. + return Math.min(86400000,Math.max(60000,Number.isFinite(milliseconds)?milliseconds:0)); +} +export async function limitHost(host:string,code:number,retryAfter?:string){ + if(![403,429,503].includes(code))return; + await updateHost(host,(state,now)=>{ + state.blockedUntil=Math.max(state.blockedUntil,now+(code===403?300000:retryDelay(retryAfter,now))); + state.nextAt=Math.max(state.nextAt,state.blockedUntil); + }); +} diff --git a/package.json b/package.json index 6a3cc2f..0fd1760 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sverka", - "version": "1.2.0", + "version": "1.2.1", "private": true, "engines": { "node": ">=24.0.0" diff --git a/tests/scanner.test.ts b/tests/scanner.test.ts index 8f9519e..52bf9f5 100644 --- a/tests/scanner.test.ts +++ b/tests/scanner.test.ts @@ -23,3 +23,9 @@ test('an active lease prevents two workers from processing the same queue',async await (await database()).query('UPDATE scan SET data=$1,lease=$2,token=$3 WHERE id=1',[JSON.stringify(run),Date.now()+60000,'another-worker']); const unchanged=(await step()) as Run;assert.deepEqual(unchanged.links,run.links);assert.equal(unchanged.checked,0); }); + +test('a step consumes only one bibliography link',async()=>{ + const db=await database(),run=(await getRun())!;run.phase='links';run.links=['http://127.0.0.1/a','http://127.0.0.1/b'];run.checked=0; + await db.query("UPDATE scan SET data=$1,lease=0,token='' WHERE id=1",[JSON.stringify(run)]); + const after=(await step())!;assert.equal(after.checked,1);assert.deepEqual(after.links,['http://127.0.0.1/b']);assert.equal(after.status,'running'); +}); diff --git a/tests/throttle.test.ts b/tests/throttle.test.ts new file mode 100644 index 0000000..49a0bb8 --- /dev/null +++ b/tests/throttle.test.ts @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict'; +import {test} from 'node:test'; +import {database,closeDatabase} from '../lib/store.ts'; +import {reserveRequest,limitHost,retryDelay,waitForHost,HostCooldown} from '../lib/throttle.ts'; +import {checkLink} from '../lib/checker.ts'; +import {useTestDatabase} from './test-database.ts'; +useTestDatabase('throttle'); + +test('concurrent reservations space one host by 3–4 seconds; other hosts are independent',async()=>{ + const slots=(await Promise.all([reserveRequest('one.org'),reserveRequest('one.org'),reserveRequest('one.org')])).sort((a,b)=>a-b); + for(let i=1;i=3000&&slots[i]-slots[i-1]<=4000); + assert.ok(await reserveRequest('two.org'){ + const now=Date.parse('2026-10-29T00:00:00Z'); + assert.equal(retryDelay('120',now),120000);assert.equal(retryDelay('Thu, 29 Oct 2026 00:02:00 GMT',now),120000); + for(const value of [undefined,'garbage','0','-1','Wed, 28 Oct 2026 00:00:00 GMT'])assert.equal(retryDelay(value,now),60000); + assert.equal(retryDelay('99999999999999999999999999',now),86400000); +}); +test('cooldown survives reconnect, cannot be shortened and prevents a network attempt',async()=>{ + await limitHost('limited.org',429,'600');await limitHost('limited.org',503,'1');await closeDatabase(); + await assert.rejects(waitForHost('limited.org'),HostCooldown); + const result=await checkLink('https://limited.org/book');assert.equal(result.status,'blocked');assert.match(result.reason,/Пауза до/); + const {rows}=await (await database()).query("SELECT value FROM records WHERE key='throttle:limited.org'"); + assert.ok(JSON.parse(rows[0].value).blockedUntil>Date.now()+590000); +}); +test('403 backs off for five minutes and successful responses do not block hosts',async()=>{ + await limitHost('forbidden.org',403);await assert.rejects(reserveRequest('forbidden.org'),HostCooldown); + await limitHost('available.org',200);await reserveRequest('available.org'); +}); +test('expired cooldown permits requests and waiting is abortable',async()=>{ + await limitHost('expired.org',429);await (await database()).query("UPDATE records SET value=$1 WHERE key='throttle:expired.org'",[JSON.stringify({nextAt:0,blockedUntil:Date.now()-1})]); + await reserveRequest('expired.org'); + const controller=new AbortController();controller.abort();await assert.rejects(waitForHost('expired.org',controller.signal),{name:'AbortError'}); +}); +test('the shared gate actually waits before allowing a second request',async()=>{ + await waitForHost('paced.org');const started=Date.now();await waitForHost('paced.org');assert.ok(Date.now()-started>=2900); +}); -- 2.54.0 From 3cceb1a453ea11e3c75346f684d7a69fb8a5d570 Mon Sep 17 00:00:00 2001 From: MIEM DEV Date: Thu, 29 Oct 2026 18:23:43 +0300 Subject: [PATCH 04/13] fix: allow redirect pauses and honor long server cooldowns --- README.md | 2 +- lib/checker.ts | 5 +++-- lib/throttle.ts | 2 +- tests/throttle.test.ts | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a017bc7..a517a2d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ pnpm dev ## Частота запросов -Материалы проверяются последовательно. Между запросами к одному хосту выдерживается 3–4 секунды, включая каталоги, курсы и перенаправления. Ограничение общее для API и worker и сохраняется в PostgreSQL. После HTTP429/503 действует пауза из `Retry-After` (не менее минуты, не более суток), после HTTP403 — пять минут. Повторных попыток в цикле нет. Ссылки, пропущенные во время паузы, отмечаются как ограниченные с датой окончания паузы; это не подтверждает, что материал удалён. Их можно обновить последующей полной проверкой. Паузы снижают нагрузку, но не гарантируют отсутствие блокировки со стороны сайта. +Материалы проверяются последовательно. Между запросами к одному хосту выдерживается 3–4 секунды, включая каталоги, курсы и перенаправления. Ограничение общее для API и worker и сохраняется в PostgreSQL. После HTTP429/503 действует пауза из `Retry-After` (не менее минуты), после HTTP403 — пять минут. Повторных попыток в цикле нет. Ссылки, пропущенные во время паузы, отмечаются как ограниченные с датой окончания паузы; это не подтверждает, что материал удалён. Их можно обновить последующей полной проверкой. Общий таймаут загрузки с перенаправлениями — 30 секунд, включая паузы. Паузы снижают нагрузку, но не гарантируют отсутствие блокировки со стороны сайта. ## Расписание diff --git a/lib/checker.ts b/lib/checker.ts index 3d43fa4..773d73d 100644 --- a/lib/checker.ts +++ b/lib/checker.ts @@ -45,7 +45,8 @@ export async function resolvePublicHost(host:string){ export async function retrieve(raw:string,source=false){ let url=safeUrl(raw);if(!url)throw new Error('Адрес не разрешён политикой безопасности'); await waitForHost(new URL(url).hostname); - const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),12000); + // Four redirects can add up to 16 seconds of intentional host pacing. + const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),30000); const aborted=new Promise((_,reject)=>controller.signal.addEventListener('abort',()=>reject(new DOMException('Истекло время ожидания','AbortError')),{once:true})); try{ for(let hop=0;hop<5;hop++){ @@ -102,4 +103,4 @@ if(document.querySelector('input[type="password"]')&&text.length<10000)return {. if(/(для (?:чтения|доступа|просмотра).{0,80}(?:авториз|войд|подписк)|(?:авторизуйтесь|войдите).{0,80}(?:чита|доступ|просмотр)|sign in to (?:read|access|view)|purchase (?:this|access)|institutional access|доступ (?:к полному тексту|по подписке)|читать по подписке)/i.test(text))return {...out,status:'restricted',reason:'Для полного текста может потребоваться вход, подписка или доступ организации. Проверьте вручную.'}; if(/html/i.test(r.type)&&!text)return {...out,status:'unknown',reason:'Получена пустая HTML-страница. Доступ к материалу не подтверждён.'}; if(/html/i.test(r.type)&&((scripted&&text.length<200)||/^(загрузка|loading)[.\s…]*$/i.test(text)))return {...out,status:'unknown',reason:'Получена оболочка страницы с загрузкой через JavaScript. Доступ к материалу не подтверждён; откройте ссылку вручную.'};return out;} -export async function checkLink(url:string):Promise{try{return classify(url,await retrieve(url));}catch(e){const error=e as Error;return {url,status:error instanceof HostCooldown?'blocked':error.name==='AbortError'?'timeout':'network',reason:error.name==='AbortError'?'Сервер не ответил за 12 секунд.':`Не удалось загрузить: ${error.message.slice(0,180)}`,checkedAt:new Date().toISOString()};}} +export async function checkLink(url:string):Promise{try{return classify(url,await retrieve(url));}catch(e){const error=e as Error;return {url,status:error instanceof HostCooldown?'blocked':error.name==='AbortError'?'timeout':'network',reason:error.name==='AbortError'?'Превышен общий лимит ожидания: 30 секунд, включая паузы между перенаправлениями.':`Не удалось загрузить: ${error.message.slice(0,180)}`,checkedAt:new Date().toISOString()};}} diff --git a/lib/throttle.ts b/lib/throttle.ts index 6bc9ebc..c514f2c 100644 --- a/lib/throttle.ts +++ b/lib/throttle.ts @@ -30,7 +30,7 @@ export async function waitForHost(host:string,signal?:AbortSignal){ export function retryDelay(value:string|undefined,now:number){ const milliseconds=value&&/^\d+$/.test(value.trim())?Number(value)*1000:value?Date.parse(value)-now:0; // Ignore malformed values; keep a minimum pause even if the server says zero. - return Math.min(86400000,Math.max(60000,Number.isFinite(milliseconds)?milliseconds:0)); + return Math.min(8640000000000000-now,Math.max(60000,Number.isNaN(milliseconds)?0:milliseconds)); } export async function limitHost(host:string,code:number,retryAfter?:string){ if(![403,429,503].includes(code))return; diff --git a/tests/throttle.test.ts b/tests/throttle.test.ts index 49a0bb8..3b08c91 100644 --- a/tests/throttle.test.ts +++ b/tests/throttle.test.ts @@ -15,7 +15,8 @@ test('Retry-After handles seconds, dates, invalid and excessive values',()=>{ const now=Date.parse('2026-10-29T00:00:00Z'); assert.equal(retryDelay('120',now),120000);assert.equal(retryDelay('Thu, 29 Oct 2026 00:02:00 GMT',now),120000); for(const value of [undefined,'garbage','0','-1','Wed, 28 Oct 2026 00:00:00 GMT'])assert.equal(retryDelay(value,now),60000); - assert.equal(retryDelay('99999999999999999999999999',now),86400000); + assert.equal(retryDelay('172800',now),172800000); + for(const value of ['99999999999999999999999999','9'.repeat(400)])assert.equal(new Date(now+retryDelay(value,now)).toISOString(),'+275760-09-13T00:00:00.000Z'); }); test('cooldown survives reconnect, cannot be shortened and prevents a network attempt',async()=>{ await limitHost('limited.org',429,'600');await limitHost('limited.org',503,'1');await closeDatabase(); -- 2.54.0 From cd2b9934028d261f44fdeb0f46e6bdec8f0a01dc Mon Sep 17 00:00:00 2001 From: MIEM DEV Date: Thu, 29 Oct 2026 18:26:36 +0300 Subject: [PATCH 05/13] docs: record request pacing release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5af3fae..48f5226 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.2.1 — 2026-10-29 + +- Материалы проверяются последовательно; запросы к одному хосту разделены паузой 3–4 секунды, включая перенаправления. +- Паузы и ограничения общие для ручных и автоматических проверок и сохраняются в PostgreSQL после перезапуска. +- HTTP429/503 учитывают срок Retry-After с минимальной паузой в минуту; HTTP403 приостанавливает обращения на пять минут. +- Пропущенные из-за ограничения ссылки показывают причину и срок паузы; повторные запросы в цикле не выполняются. +- Общий таймаут загрузки увеличен до 30 секунд с учётом пауз на перенаправлениях. + ## 1.2.0 — 2026-10-29 - Хранилище перенесено на PostgreSQL для использования существующего сервера VPS; приложению предназначены отдельная база и роль. -- 2.54.0 From 423a4782c0d274c664fe867bc0beb056d3ac60de Mon Sep 17 00:00:00 2001 From: MIEM DEV Date: Thu, 29 Oct 2026 18:36:33 +0300 Subject: [PATCH 06/13] fix: set request spacing to two seconds --- README.md | 2 +- compose.yaml | 2 +- lib/checker.ts | 2 +- lib/model.ts | 2 +- lib/throttle.ts | 2 +- package.json | 2 +- tests/throttle.test.ts | 6 +++--- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a517a2d..7e7f3c9 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ pnpm dev ## Частота запросов -Материалы проверяются последовательно. Между запросами к одному хосту выдерживается 3–4 секунды, включая каталоги, курсы и перенаправления. Ограничение общее для API и worker и сохраняется в PostgreSQL. После HTTP429/503 действует пауза из `Retry-After` (не менее минуты), после HTTP403 — пять минут. Повторных попыток в цикле нет. Ссылки, пропущенные во время паузы, отмечаются как ограниченные с датой окончания паузы; это не подтверждает, что материал удалён. Их можно обновить последующей полной проверкой. Общий таймаут загрузки с перенаправлениями — 30 секунд, включая паузы. Паузы снижают нагрузку, но не гарантируют отсутствие блокировки со стороны сайта. +Материалы проверяются последовательно. Между запросами к одному хосту выдерживается 2 секунды, включая каталоги, курсы и перенаправления. Ограничение общее для API и worker и сохраняется в PostgreSQL. После HTTP429/503 действует пауза из `Retry-After` (не менее минуты), после HTTP403 — пять минут. Повторных попыток в цикле нет. Ссылки, пропущенные во время паузы, отмечаются как ограниченные с датой окончания паузы; это не подтверждает, что материал удалён. Их можно обновить последующей полной проверкой. Общий таймаут загрузки с перенаправлениями — 30 секунд, включая паузы. Паузы снижают нагрузку, но не гарантируют отсутствие блокировки со стороны сайта. ## Расписание diff --git a/compose.yaml b/compose.yaml index 297d790..1398a7a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,7 +2,7 @@ name: miem-courses services: courses: build: . - image: miem-courses:1.2.1 + image: miem-courses:1.2.2 restart: unless-stopped init: true user: "1000:1000" diff --git a/lib/checker.ts b/lib/checker.ts index 773d73d..cc47c9d 100644 --- a/lib/checker.ts +++ b/lib/checker.ts @@ -45,7 +45,7 @@ export async function resolvePublicHost(host:string){ export async function retrieve(raw:string,source=false){ let url=safeUrl(raw);if(!url)throw new Error('Адрес не разрешён политикой безопасности'); await waitForHost(new URL(url).hostname); - // Four redirects can add up to 16 seconds of intentional host pacing. + // Four redirects can add up to eight seconds of intentional host pacing. const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),30000); const aborted=new Promise((_,reject)=>controller.signal.addEventListener('abort',()=>reject(new DOMException('Истекло время ожидания','AbortError')),{once:true})); try{ diff --git a/lib/model.ts b/lib/model.ts index b210bd6..98fd68f 100644 --- a/lib/model.ts +++ b/lib/model.ts @@ -1,4 +1,4 @@ -export const VERSION='1.2.1'; +export const VERSION='1.2.2'; export const PROGRAMS=[ {id:'it',name:'Цифровые технологии и телекоммуникации',level:'Бакалавриат',url:'https://www.hse.ru/ba/it/courses'}, {id:'is',name:'Информационная безопасность',level:'Бакалавриат',url:'https://www.hse.ru/ba/is/courses'}, diff --git a/lib/throttle.ts b/lib/throttle.ts index c514f2c..85bc67b 100644 --- a/lib/throttle.ts +++ b/lib/throttle.ts @@ -20,7 +20,7 @@ async function updateHost(host:string,update:(state:HostState,now:number)=>T) export async function reserveRequest(host:string){ return updateHost(host,(state,now)=>{ if(state.blockedUntil>now)throw new HostCooldown(state.blockedUntil); - const at=Math.max(now,state.nextAt);state.nextAt=at+3000+Math.floor(Math.random()*1001);return at; + const at=Math.max(now,state.nextAt);state.nextAt=at+2000;return at; }); } export async function waitForHost(host:string,signal?:AbortSignal){ diff --git a/package.json b/package.json index 0fd1760..6dcd378 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sverka", - "version": "1.2.1", + "version": "1.2.2", "private": true, "engines": { "node": ">=24.0.0" diff --git a/tests/throttle.test.ts b/tests/throttle.test.ts index 3b08c91..bb6ef5c 100644 --- a/tests/throttle.test.ts +++ b/tests/throttle.test.ts @@ -6,9 +6,9 @@ import {checkLink} from '../lib/checker.ts'; import {useTestDatabase} from './test-database.ts'; useTestDatabase('throttle'); -test('concurrent reservations space one host by 3–4 seconds; other hosts are independent',async()=>{ +test('concurrent reservations space one host by two seconds; other hosts are independent',async()=>{ const slots=(await Promise.all([reserveRequest('one.org'),reserveRequest('one.org'),reserveRequest('one.org')])).sort((a,b)=>a-b); - for(let i=1;i=3000&&slots[i]-slots[i-1]<=4000); + for(let i=1;i{ @@ -35,5 +35,5 @@ test('expired cooldown permits requests and waiting is abortable',async()=>{ const controller=new AbortController();controller.abort();await assert.rejects(waitForHost('expired.org',controller.signal),{name:'AbortError'}); }); test('the shared gate actually waits before allowing a second request',async()=>{ - await waitForHost('paced.org');const started=Date.now();await waitForHost('paced.org');assert.ok(Date.now()-started>=2900); + await waitForHost('paced.org');const started=Date.now();await waitForHost('paced.org');assert.ok(Date.now()-started>=1900); }); -- 2.54.0 From 4dbb662ec8902a25f90e40a14433cea57c64f9bf Mon Sep 17 00:00:00 2001 From: MIEM DEV Date: Thu, 29 Oct 2026 18:38:05 +0300 Subject: [PATCH 07/13] docs: record two-second pacing release --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48f5226..1cf0733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.2.2 — 2026-10-29 + +- Интервал между запросами к одному хосту установлен в две секунды вместо случайной паузы 3–4 секунды. + ## 1.2.1 — 2026-10-29 - Материалы проверяются последовательно; запросы к одному хосту разделены паузой 3–4 секунды, включая перенаправления. -- 2.54.0 From 3abe7e26ec368beb7b086951e638dd4e74184ac1 Mon Sep 17 00:00:00 2001 From: MIEM DEV Date: Fri, 30 Oct 2026 18:21:55 +0300 Subject: [PATCH 08/13] feat: verify anonymous full-text access with provider evidence --- Dockerfile | 7 +- PRODUCT.md | 2 + README.md | 16 +- app/dashboard.tsx | 6 +- app/globals.css | 6 +- compose.yaml | 8 +- deploy/PLAYWRIGHT-LICENSE | 202 +++++++++ deploy/README.md | 8 + deploy/chromium-seccomp.json | 831 +++++++++++++++++++++++++++++++++++ deploy/nginx-courses.conf | 4 +- lib/access.ts | 65 +++ lib/browser.ts | 95 ++++ lib/checker.ts | 93 +--- lib/http.ts | 63 +++ lib/model.ts | 6 +- lib/provider.ts | 1 + lib/scanner.ts | 8 +- next.config.ts | 2 +- package.json | 18 +- pnpm-lock.yaml | 19 + tests/access.test.ts | 49 +++ tests/browser-policy.test.ts | 10 + tests/browser.security.ts | 38 ++ tests/checker.test.ts | 10 +- tests/network.test.ts | 21 + tests/scanner.test.ts | 11 +- 26 files changed, 1485 insertions(+), 114 deletions(-) create mode 100644 deploy/PLAYWRIGHT-LICENSE create mode 100644 deploy/README.md create mode 100644 deploy/chromium-seccomp.json create mode 100644 lib/access.ts create mode 100644 lib/browser.ts create mode 100644 lib/http.ts create mode 100644 lib/provider.ts create mode 100644 tests/access.test.ts create mode 100644 tests/browser-policy.test.ts create mode 100644 tests/browser.security.ts create mode 100644 tests/network.test.ts diff --git a/Dockerfile b/Dockerfile index f3248ab..2010e85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,8 +4,10 @@ ENV NEXT_TELEMETRY_DISABLED=1 RUN npm install --global pnpm@11.25.0 COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ RUN pnpm install --frozen-lockfile +ENV PLAYWRIGHT_BROWSERS_PATH=/opt/chromium +RUN node node_modules/playwright/cli.js install chromium --only-shell COPY app ./app -COPY lib/model.ts lib/checker.ts lib/scanner.ts lib/store.ts lib/scheduler.ts lib/schedule-worker.ts lib/throttle.ts ./lib/ +COPY lib ./lib COPY public ./public COPY next.config.ts tsconfig.json postcss.config.mjs ./ RUN pnpm build @@ -13,11 +15,14 @@ RUN pnpm build FROM node:24-bookworm-slim@sha256:c86ecead9e55a5210ac1727de38a4ca840bf03dcaf34dc9749addbf383b1920c AS runtime WORKDIR /app ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 HOSTNAME=0.0.0.0 PORT=3000 +ENV PLAYWRIGHT_BROWSERS_PATH=/opt/chromium COPY --from=build --chown=node:node /app/.next/standalone ./ COPY --from=build --chown=node:node /app/.next/static ./.next/static COPY --from=build --chown=node:node /app/public ./public COPY --from=build --chown=node:node /app/lib ./lib COPY server ./server +COPY --from=build /opt/chromium /opt/chromium +RUN apt-get update && apt-get install -y --no-install-recommends libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcairo2 libcups2 libdbus-1-3 libdrm2 libgbm1 libglib2.0-0 libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 fonts-liberation fonts-noto-color-emoji && chmod -R a+rX /opt/chromium && rm -rf /var/lib/apt/lists/* USER node EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD node -e "fetch('http://127.0.0.1:3000/courses/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))" diff --git a/PRODUCT.md b/PRODUCT.md index d061b77..0cdc077 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -4,3 +4,5 @@ Расписание: одно общее, ежедневно или еженедельно по Москве, выключено до сохранения пользователем. Запланированный аудит работает фоновым процессом при закрытом браузере и продолжается после перезапуска. Совпавший слот пропускается, если предыдущая ручная или автоматическая проверка ещё running. Изменение/отключение влияет на будущие запуски. Для VPS используется отдельная база/роль на существующем PostgreSQL. Пароль только в окружении, не в исходниках. + +Полный текст: авторизация, подписка, покупка или гостевой фрагмент означают недоступность книги с конкретным доказательством. Аннотация и успешный HTTP не дают зелёный статус. Неопределённость и остановка проверки защитой сайта показываются отдельно. Для известных ЭБС доступ проверяется после JS и перехода в читалку без авторизации. Кэш внешних ссылок имеет собственный TTL и версию правил. diff --git a/README.md b/README.md index 7e7f3c9..87948d9 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,23 @@ pnpm dev Ручная проверка выполняется порциями, пока вкладка открыта. После её закрытия или перезапуска сервера кнопка «Продолжить» использует сохранённую очередь. Глобальная аренда не допускает одновременную обработку одной порции. Каждая порция сохраняет результаты и очередь в одной транзакции PostgreSQL; при ошибке записи задания остаются в очереди. -Страницы курсов загружаются заново, но литература неизменившихся курсов берётся из кэша вместе с исходной датой проверки. Полная проверка без кэша нужна для проверки изменений на внешних сайтах. HTTP200 не доказывает доступ к полному тексту; JavaScript-заглушки, CAPTCHA и признаки подписки отмечаются отдельно. Литература только во вложении отмечается как неполный охват. +Страницы курсов сравниваются по контрольной сумме. Неизменившийся список литературы переиспользуется, но доступность внешних ссылок проверяется отдельно: кэш сутки для подтверждённых ограничений, десять минут для неопределённых результатов и временных ошибок. Версия правил входит в проверку свежести; старые HTTP-only результаты показываются как устаревшие и при следующем запуске пересчитываются. Полная проверка игнорирует кэш. Литература только во вложении отмечается как неполный охват. + +## Доступ к полному тексту + +Требование авторизации, подписки или покупки онлайн-доступа означает недоступность книги. Аннотация, оглавление и ознакомительный фрагмент не подтверждают доступ к полному тексту. HTTP200 и Content-Type PDF сами по себе не дают зелёный статус; при недостатке доказательств возвращается `unknown`. Даже полученный PDF может быть фрагментом издания. + +Правила учитывают сообщения читалок Лани, Юрайта и iBooks, изъятие публикации и предложение покупки онлайн-доступа Znanium, покупку именно eBook Springer и гостевое ограничение BOOK.ru. Цена печатной книги и общий запрет скачивания PDF не доказывают закрытое чтение. Результат содержит короткое доказательство, URL без токенов, время, версию правил и профиль `anonymous`. + +Статического HTML достаточно для явных ограничений. Неопределённые страницы известных ЭБС открываются в Chromium/Playwright; проверяется видимый текст, при необходимости адрес читалки, а у BOOK.ru — подсказка к гостевому просмотру. Для остальных сайтов неопределённость сохраняется. Проверка не выполняет вход, покупку и обход CAPTCHA. + +Docker включает Chromium с включённой песочницей, UID1000, read-only rootfs, no-new-privileges и cap-drop ALL. Профиль seccomp разрешает необходимые для песочницы user namespaces, остальные ограничения остаются; источник и лицензия указаны в deploy/README.md. Браузер получает минимальное окружение без DATABASE_URL. Все HTTP-запросы перехватываются, DNS проверяется и закрепляется в Node-сокете. Прямой выход блокируется нерабочим proxy, WebSocket/SW отключены, непроходящий через proxy WebRTC UDP запрещён. Методы записи, внешние домены, изображения/медиа/шрифты блокируются. Лимиты:100 запросов,32MB на задачу,8MB на ресурс,10 одновременных запросов; browser deadline30s и общий link deadline60s. Ошибка/лимит оставляет доступ неподтверждённым. + +Локальная разработка браузерной части рекомендована в Docker с тем же seccomp-профилем. `pnpm test:browser` с локальной изолированной TEST_DATABASE_URL проверяет реальный Chromium и запрет обращений к локальному HTTP/UDP серверу через fetch, redirect, iframe, popup, worker, WebSocket и WebRTC. Обычные `pnpm test` также проверяют DNS/IP pinning и правила провайдеров. ## Частота запросов -Материалы проверяются последовательно. Между запросами к одному хосту выдерживается 2 секунды, включая каталоги, курсы и перенаправления. Ограничение общее для API и worker и сохраняется в PostgreSQL. После HTTP429/503 действует пауза из `Retry-After` (не менее минуты), после HTTP403 — пять минут. Повторных попыток в цикле нет. Ссылки, пропущенные во время паузы, отмечаются как ограниченные с датой окончания паузы; это не подтверждает, что материал удалён. Их можно обновить последующей полной проверкой. Общий таймаут загрузки с перенаправлениями — 30 секунд, включая паузы. Паузы снижают нагрузку, но не гарантируют отсутствие блокировки со стороны сайта. +Материалы проверяются последовательно. Между запросами к одному хосту выдерживается 2 секунды, включая каталоги, курсы и перенаправления. В браузерной проверке пауза применяется к навигациям; JS/CSS и anonymous API загружаются с ограничением параллелизма, без двухсекундной паузы на каждый ресурс. Ограничение общее для API и worker и сохраняется в PostgreSQL. После HTTP429/503 действует пауза из `Retry-After` (не менее минуты), после HTTP403 — пять минут. Повторных попыток в цикле нет. Ссылки, пропущенные во время паузы, отмечаются как ограниченные с датой окончания паузы; это не подтверждает, что материал удалён. Их можно обновить последующей полной проверкой. Статическая загрузка ограничена30 секундами, вся проверка ссылки с браузером —60 секундами. Proxy read timeout85s оставляет время на сохранение результата в пределах90s lease. Паузы снижают нагрузку, но не гарантируют отсутствие блокировки со стороны сайта. ## Расписание diff --git a/app/dashboard.tsx b/app/dashboard.tsx index 6c42451..a9df7e9 100644 --- a/app/dashboard.tsx +++ b/app/dashboard.tsx @@ -24,12 +24,12 @@ function exportCsv(){const rows=[['Программа','Курс','Ссылка const selected=PROGRAMS.find(p=>p.id===program),percent=run?.phase==='links'?Math.round(run.checked/Math.max(1,run.totalLinks)*100):run?.phase==='courses'?Math.round(run.processed/Math.max(1,run.totalCourses)*100):0; return
К результатам
НИУ ВШЭ / Учебная литература

{view==='method'?'Как устроена сверка':view==='courses'?'Учебные курсы':'Ссылки под наблюдением'}

{view==='method'?'Что проверяется и как читать результаты.':'Проверяйте литературу. Находите недоступные материалы.'}

5 программединый реестр
{error&&
{error}
} -{view==='method'?

От программы — к материалу

Сверка читает текущий учебный год на пяти страницах ВШЭ, обходит пагинацию и открывает каждый курс. Из разделов основной и дополнительной литературы извлекаются кликабельные ссылки и адреса, записанные обычным текстом.

Вы управляете проверкой

Нажмите «Запустить проверку» и оставьте вкладку открытой. Можно приостановить проверку, закрыть вкладку и позже продолжить сохранённый запуск. Одновременно выполняется один общий запуск. Приостановка в этой вкладке не останавливает проверку у другого посетителя.

Проверка по расписанию

Настройте ежедневный или еженедельный запуск и время по Москве. Расписание общее для всех посетителей и сохраняется на сервере. Автоматическая проверка работает без браузера. Изменение или отключение расписания влияет на будущие запуски; уже начатая проверка продолжается. Если другая проверка ещё идёт, совпавший запуск пропускается. После простоя сервера выполняется один просроченный запуск, без очереди пропущенных повторений.

Изменения вместо повторений

Страницы курсов загружаются и сравниваются по контрольной сумме. Неизменившиеся курсы сохраняют результаты и исходные даты проверки ссылок. Одинаковые адреса в изменившихся курсах проверяются один раз за запуск. Полная проверка игнорирует кэш: используйте её, чтобы обнаружить изменения на внешних сайтах при неизменной программе курса.

Успешный ответ — ещё не полный текст

Различаются 404/410, вход, ограничения доступа, CAPTCHA, ошибки сервера и тайм-ауты. «Доступна» означает успешный ответ сервера. Карточка книги может открываться, а текст — требовать подписку. Ограничения определяются по признакам на странице и требуют ручного подтверждения. Сверка не обходит вход и защиту сайта.

Границы проверки

Проверяется литература в HTML-программах. Если раздел отсутствует, доступен только во вложении или не читается, курс получает отдельную отметку. Архивные годы и навигационные ссылки не проверяются. Старые результаты сохраняются с датами; непрочитанные и исчезнувшие курсы отмечаются как не обновлённые.

:<> +{view==='method'?

От программы — к материалу

Сверка читает текущий учебный год на пяти страницах ВШЭ, обходит пагинацию и открывает каждый курс. Из разделов основной и дополнительной литературы извлекаются кликабельные ссылки и адреса, записанные обычным текстом.

Вы управляете проверкой

Нажмите «Запустить проверку» и оставьте вкладку открытой. Можно приостановить проверку, закрыть вкладку и позже продолжить сохранённый запуск. Одновременно выполняется один общий запуск. Приостановка в этой вкладке не останавливает проверку у другого посетителя.

Проверка по расписанию

Настройте ежедневный или еженедельный запуск и время по Москве. Расписание общее для всех посетителей и сохраняется на сервере. Автоматическая проверка работает без браузера. Изменение или отключение расписания влияет на будущие запуски; уже начатая проверка продолжается. Если другая проверка ещё идёт, совпавший запуск пропускается. После простоя сервера выполняется один просроченный запуск, без очереди пропущенных повторений.

Изменения вместо повторений

Страницы курсов сравниваются по контрольной сумме. Неизменившийся список литературы используется повторно. Доступность ссылок имеет отдельный срок кэша: сутки для подтверждённых ограничений и десять минут для неопределённых результатов или временных ошибок. После изменения правил ссылки проверяются заново. Полная проверка игнорирует кэш.

Проверяется доступ без авторизации

Различаются 404/410, вход, ограничения доступа, CAPTCHA, ошибки сервера и тайм-ауты. Если для чтения требуется авторизация, подписка или покупка, книга считается недоступной. Причина подтверждается сообщением сайта или предложением покупки онлайн-доступа. Карточка, аннотация и фрагмент не подтверждают доступ ко всей книге. Для поддерживаемых ЭБС проверяется содержимое после загрузки JavaScript и, при необходимости, читалка. При недостатке доказательств показывается «Доступ не подтверждён». Сверка не обходит вход и защиту сайта.

Границы проверки

Проверяется литература в HTML-программах. Если раздел отсутствует, доступен только во вложении или не читается, курс получает отдельную отметку. Архивные годы и навигационные ссылки не проверяются. Старые результаты сохраняются с датами; непрочитанные и исчезнувшие курсы отмечаются как не обновлённые.

:<>

{serverActive?'Выполняется проверка по расписанию':active?'Сверяем источники':run?.status==='running'?'Проверка ожидает продолжения':run?'Результаты сохранены':'Начните с первой проверки'}

{active||serverActive?run?.message:run?`Последний запуск: ${date(run.startedAt)} · МСК`:'Все курсы пяти программ. Только реальные результаты.'}

{serverActive?:active?:}
{run?.status==='running'&&
{run.phase==='catalogs'?`Каталоги: ${run.visited.length}`:run.phase==='courses'?`Курсы: ${run.processed} / ${run.totalCourses}`:`Адреса: ${run.checked} / ${run.totalLinks}`}{serverActive?'Выполняется без открытой вкладки':'Оставьте вкладку открытой'}
}
-{loaded?setData(d=>({...d,schedule}))}/>:

{loading?'Загружаем настройки расписания…':'Расписание недоступно. Повторите загрузку через сообщение об ошибке выше.'}

}
{problems.length||'—'}ссылок требуют внимания
{courses.length||'—'}курсов в реестре
{rows.filter(r=>r.result?.status==='ok').length||'—'}ссылок доступны
{run?.reused??'—'}курсов взято из кэша
+{loaded?setData(d=>({...d,schedule}))}/>:

{loading?'Загружаем настройки расписания…':'Расписание недоступно. Повторите загрузку через сообщение об ошибке выше.'}

}
{problems.length||'—'}ссылок требуют внимания
{courses.length||'—'}курсов в реестре
{rows.filter(r=>r.result?.status==='ok').length||'—'}полных текстов доступны
{run?.reused??'—'}курсов взято из кэша
{run&&run.errors.length>0&&
Не удалось прочитать {run.errors.length} источников — охват неполный{run.errors.map((e,i)=>

{e.url}
{e.reason}

)}
}

{view==='courses'?'Каталог курсов':'Результаты проверки'}{view==='courses'?shownCourses.length:visible.length}

{view==='links'&&}
{selected&&
{selected.name}Каталог ВШЭ
} -{loading?

Загружаем реестр

:view==='courses'?shownCourses.length?
{shownCourses.slice(0,limit).map(c=>
{c.name}

{PROGRAMS.find(p=>p.id===c.program)?.name}

{c.issue&&

{c.issue}

}{date(c.checkedAt)} · {c.refs.length} адресов {c.reused?'· из кэша':''}{run&&c.seen!==run.id?' · не обновлён в текущем запуске':''}
{c.issue?'Нужна проверка':c.refs.length?'Литература найдена':'Нет ссылок'}
)}
::visible.length?<>
Материал и источникРезультатПроверено, МСК
{visible.slice(0,limit).map(r=>{const key=r.course.url+r.ref.url;return
{r.ref.title||r.ref.url}
{r.ref.url}
{r.course.name}{PROGRAMS.find(p=>p.id===r.course.program)?.name}{run&&r.course.seen!==run.id?' · прежний результат':''}
{r.result?LABELS[r.result.status]:'Не проверено'}
{date(r.result?.checkedAt)}{r.result?.code&&HTTP {r.result.code}}
{expanded===key&&

{r.result?.reason??'Адрес ещё не проверен. Запустите или продолжите сверку.'}

{r.result?.finalUrl&&r.result.finalUrl!==r.ref.url&&После перенаправления: {r.result.finalUrl}}{r.course.reused&&Содержание курса не изменилось. Использован сохранённый результат.}
}
;})}
:} +{loading?

Загружаем реестр

:view==='courses'?shownCourses.length?
{shownCourses.slice(0,limit).map(c=>
{c.name}

{PROGRAMS.find(p=>p.id===c.program)?.name}

{c.issue&&

{c.issue}

}{date(c.checkedAt)} · {c.refs.length} адресов {c.reused?'· из кэша':''}{run&&c.seen!==run.id?' · не обновлён в текущем запуске':''}
{c.issue?'Нужна проверка':c.refs.length?'Литература найдена':'Нет ссылок'}
)}
::visible.length?<>
Материал и источникРезультатПроверено, МСК
{visible.slice(0,limit).map(r=>{const key=r.course.url+r.ref.url;return
{r.ref.title||r.ref.url}
{r.ref.url}
{r.course.name}{PROGRAMS.find(p=>p.id===r.course.program)?.name}{run&&r.course.seen!==run.id?' · прежний результат':''}
{r.result?LABELS[r.result.status]:'Не проверено'}{r.result&&['auth','restricted'].includes(r.result.status)&&

{r.result.reason}

}
{date(r.result?.checkedAt)}
{expanded===key&&

{r.result?.reason??'Адрес ещё не проверен. Запустите или продолжите сверку.'}

{r.result?.evidence&&
{r.result.evidence.text}
Сообщение на сайте Проверено без авторизации
}{r.result?.code&&Технические сведения: HTTP {r.result.code}}{r.result?.finalUrl&&r.result.finalUrl!==r.ref.url&&После перенаправления: {r.result.finalUrl}}{r.course.reused&&Список литературы курса не изменился. Доступность ссылок проверяется отдельно.}
}
;})}
:} {(view==='courses'?shownCourses.length:visible.length)>limit&&}
Блокировка проверки не означает, что материал удалён. Причину можно уточнить по исходной ссылке.
Проверяем изменения, сохраняем результаты.Неизменившиеся курсы — из кэша
}
сверка. Независимый инструмент · источники: hse.ruИнтерфейс {VERSION} · Сервер {VERSION}
;} function Empty({hasRun,filtered,launch}:{hasRun:boolean;filtered:boolean;launch:()=>void}){return

{filtered?'По этим условиям ничего нет':hasRun?'Проблемных ссылок пока нет':'Пока ни одна ссылка не проверена'}

{filtered?'Измените запрос, причину или выбранную программу.':hasRun?'Смотрите охват и список курсов. Непрочитанные источники отмечаются отдельно.':'Запустите сверку, чтобы найти недоступную литературу и увидеть, в каких курсах она используется.'}

{!hasRun&&!filtered&&}
404 / удаленаТребуется входОграничен доступ
;} diff --git a/app/globals.css b/app/globals.css index 8ddff7b..92dae59 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,5 +4,7 @@ @media(min-width:1500px){.shell{grid-template-columns:290px minmax(0,1fr)}main{padding:55px 64px 0}.topbar{padding:0 64px}.sidebar{padding-top:42px}.control{padding:30px}.summary{padding:35px 0}.empty{min-height:340px}} @media(max-width:1150px){.shell{grid-template-columns:235px minmax(0,1fr)}.sidebar{padding:28px 13px 20px}.sidebar__description{margin-bottom:28px}.sidebar__label{font-size:9px}.program-nav button{font-size:11px;padding:10px}.primary-nav button{font-size:11px}.topbar{padding:0 27px}main{padding:35px 27px 0}.control{grid-template-columns:auto minmax(0,1fr);padding:20px}.control__actions{grid-column:2;justify-items:start}.edition{display:none}.summary{gap:10px}.summary>div{padding-left:14px}.table-head,.link-row{grid-template-columns:minmax(0,1fr) 130px}.link-row__date{grid-column:2}.table-head>span:last-child{display:none}.filters{flex-wrap:wrap}.search{flex-basis:260px}footer{flex-wrap:wrap;gap:8px}.sidebar__bottom{margin-top:25px}.sidebar{overflow-y:auto}.bottom-note{flex-wrap:wrap}} @media(max-width:760px){.shell{display:block}.sidebar{height:auto;position:relative;padding:22px 20px 14px;overflow:visible}.brand{font-size:28px;padding:0}.brand__mark{width:32px;height:32px}.sidebar__description,.sidebar__label,.sidebar__bottom{display:none}.primary-nav{display:flex;margin-top:23px;gap:10px}.primary-nav button{font-size:11px;padding:10px;flex:1;justify-content:center}.program-nav{display:flex;overflow-x:auto;gap:7px;margin-top:15px;padding-bottom:5px}.program-nav button{white-space:nowrap;flex-shrink:0;padding:7px 11px;max-width:235px;overflow:hidden;align-items:center;background:#2d4c38;font-size:10px}.program-nav button>span{overflow:hidden;text-overflow:ellipsis}.program-nav small,.program-nav svg{display:none}.program-nav b{margin-left:4px}.topbar{height:54px;padding:0 20px;font-size:9px}.topbar__divider{margin:0 8px}.topbar button{font-size:9px;gap:5px}main{padding:28px 20px 0}.page-title{margin-bottom:23px}h1{font-size:31px;letter-spacing:-.8px}.page-title p{font-size:11px;max-width:290px;line-height:1.8}.control{gap:12px;padding:19px 16px;grid-template-columns:auto minmax(0,1fr)}.control__icon{width:38px;height:38px}.control h2{font-size:13px}.control p{font-size:10px}.control__actions{grid-column:1/-1;justify-items:stretch;margin-top:6px}.force{justify-content:center}.summary{grid-template-columns:1fr 1fr;gap:23px 10px;margin-bottom:25px;padding:25px 0}.summary>div{padding-left:17px}.summary>div:nth-child(3){padding-left:0;border:0}.summary__number{font-size:27px}.summary>div>span:last-child{font-size:10px}.results__heading{padding:18px 14px 15px;gap:6px}.results__heading h2{font-size:14px;gap:7px}.text-button{font-size:10px;gap:5px}.filters{padding:0 14px 16px;gap:8px}.search{padding:10px;font-size:10px}.status-select{width:100%}.status-select select{flex:1;max-width:none}.table-head{display:none}.link-row{padding:18px 14px;grid-template-columns:minmax(0,1fr) auto;gap:8px 12px}.link-row__main{grid-column:1/-1}.link-row__result{grid-column:1;padding-left:19px;display:flex;gap:10px;align-items:center}.link-row__date{grid-column:2;font-size:9px}.link-row__date small{display:none}.material-name{font-size:12px}.program-name{font-size:9px}.empty{padding:29px 16px 24px;min-height:320px}.empty h3{font-size:15px}.empty p{font-size:11px}.empty__legend{gap:13px;font-size:9px}.results__note{font-size:9px;padding:12px 14px;align-items:start}.results__note svg{margin-top:2px}.bottom-note{font-size:9px}.course-list article{padding:17px 14px;flex-direction:column;gap:9px}.course-list .badge{align-self:flex-start}.scope{padding:0 14px 15px;flex-wrap:wrap;font-size:10px}.scope a{margin-left:0}footer{margin-top:35px;padding:18px 0;font-size:9px;display:grid}footer>span>span{margin-left:6px;font-size:8px}.scan-progress>div{flex-direction:column;gap:4px}.error-banner{font-size:11px}.method h2{font-size:23px}.method p{font-size:13px}} -@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}} -.schedule{margin-top:16px;border-bottom:1px solid var(--line)}.schedule summary{display:flex;align-items:center;gap:12px;cursor:pointer;list-style:none;padding:17px 2px;color:var(--forest)}.schedule summary::-webkit-details-marker{display:none}.schedule__title{font-size:13px;font-weight:500}.schedule__title small{display:block;font-size:10px;font-weight:400;color:var(--muted);margin-top:3px}.schedule__state{margin-left:auto;font-size:10px;color:var(--muted);white-space:nowrap}.schedule__state--on{color:#42602f}.schedule__chevron{transition:transform .15s}.schedule details[open] .schedule__chevron{transform:rotate(180deg)}.schedule__form{padding:8px 0 24px}.schedule__toggle{display:flex;align-items:center;gap:9px;font-size:12px;font-weight:500;cursor:pointer}.schedule input[type=checkbox]{accent-color:var(--forest);width:15px;height:15px;margin:0}.schedule__fields{display:flex;flex-wrap:wrap;gap:16px;margin:20px 0 14px}.schedule__fields label{display:flex;flex-direction:column;gap:7px;font-size:10px;color:var(--muted);min-width:140px;flex:1}.schedule__fields select,.schedule__fields input{border:1px solid #c9d2c3;border-radius:4px;padding:11px 10px;background:var(--white);color:var(--ink);font-size:12px;min-width:0;width:100%;height:42px}.schedule__hint{font-size:11px;line-height:1.8;color:var(--muted);max-width:80ch;margin:10px 0}.schedule__history{font-size:11px;margin:12px 0;color:#52654b}.schedule__error{font-size:11px;line-height:1.7;color:var(--red);margin:12px 0;overflow-wrap:anywhere}.schedule__actions{display:flex;flex-wrap:wrap;gap:16px;align-items:center;margin-top:19px}.schedule__actions>span{display:flex;align-items:center;gap:7px;color:#52654b;font-size:11px}@media(max-width:760px){.schedule summary{gap:9px}.schedule__title{font-size:12px}.schedule__title small{font-size:10px;max-width:230px}.schedule__state{font-size:9px}.schedule__fields{gap:12px}.schedule__fields label{flex-basis:calc(50% - 12px);min-width:120px}.schedule__actions{gap:12px}.schedule__actions .button{width:100%}} +@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}} +.schedule{margin-top:16px;border-bottom:1px solid var(--line)}.schedule summary{display:flex;align-items:center;gap:12px;cursor:pointer;list-style:none;padding:17px 2px;color:var(--forest)}.schedule summary::-webkit-details-marker{display:none}.schedule__title{font-size:13px;font-weight:500}.schedule__title small{display:block;font-size:10px;font-weight:400;color:var(--muted);margin-top:3px}.schedule__state{margin-left:auto;font-size:10px;color:var(--muted);white-space:nowrap}.schedule__state--on{color:#42602f}.schedule__chevron{transition:transform .15s}.schedule details[open] .schedule__chevron{transform:rotate(180deg)}.schedule__form{padding:8px 0 24px}.schedule__toggle{display:flex;align-items:center;gap:9px;font-size:12px;font-weight:500;cursor:pointer}.schedule input[type=checkbox]{accent-color:var(--forest);width:15px;height:15px;margin:0}.schedule__fields{display:flex;flex-wrap:wrap;gap:16px;margin:20px 0 14px}.schedule__fields label{display:flex;flex-direction:column;gap:7px;font-size:10px;color:var(--muted);min-width:140px;flex:1}.schedule__fields select,.schedule__fields input{border:1px solid #c9d2c3;border-radius:4px;padding:11px 10px;background:var(--white);color:var(--ink);font-size:12px;min-width:0;width:100%;height:42px}.schedule__hint{font-size:11px;line-height:1.8;color:var(--muted);max-width:80ch;margin:10px 0}.schedule__history{font-size:11px;margin:12px 0;color:#52654b}.schedule__error{font-size:11px;line-height:1.7;color:var(--red);margin:12px 0;overflow-wrap:anywhere}.schedule__actions{display:flex;flex-wrap:wrap;gap:16px;align-items:center;margin-top:19px}.schedule__actions>span{display:flex;align-items:center;gap:7px;color:#52654b;font-size:11px}@media(max-width:760px){.schedule summary{gap:9px}.schedule__title{font-size:12px}.schedule__title small{font-size:10px;max-width:230px}.schedule__state{font-size:9px}.schedule__fields{gap:12px}.schedule__fields label{flex-basis:calc(50% - 12px);min-width:120px}.schedule__actions{gap:12px}.schedule__actions .button{width:100%}} + +.link-row__reason{font-size:10px;line-height:1.6;margin:6px 0 0;color:var(--ink);overflow-wrap:anywhere}.reason__evidence{margin-top:12px}.reason__evidence blockquote{margin:0;padding:10px 12px;border:1px solid var(--line);border-radius:4px;background:var(--paper);font-size:11px;line-height:1.7} diff --git a/compose.yaml b/compose.yaml index 1398a7a..0167945 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,7 +2,7 @@ name: miem-courses services: courses: build: . - image: miem-courses:1.2.2 + image: miem-courses:1.3.0 restart: unless-stopped init: true user: "1000:1000" @@ -17,12 +17,14 @@ services: - database read_only: true tmpfs: - - /tmp:size=64m,mode=1777 + - /tmp:size=128m,mode=1777 cap_drop: - ALL security_opt: - no-new-privileges:true - mem_limit: 512m + - seccomp=./deploy/chromium-seccomp.json + shm_size: 128m + mem_limit: 1g cpus: 1.0 networks: database: diff --git a/deploy/PLAYWRIGHT-LICENSE b/deploy/PLAYWRIGHT-LICENSE new file mode 100644 index 0000000..df11237 --- /dev/null +++ b/deploy/PLAYWRIGHT-LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Portions Copyright (c) Microsoft Corporation. + Portions Copyright 2017 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..644b95c --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,8 @@ +# Chromium sandbox + +chromium-seccomp.json — неизменённый профиль Microsoft Playwright v1.64.1: +https://github.com/microsoft/playwright/blob/v1.64.1/utils/docker/seccomp_profile.json + +Лицензия Apache-2.0 сохранена в PLAYWRIGHT-LICENSE. Профиль разрешает clone/setns/unshare для Chromium user namespaces и используется с chromiumSandbox:true, UID1000, no-new-privileges и cap-drop ALL. Не заменять seccomp на unconfined или Chromium sandbox на --no-sandbox. Read-only rootfs и временные /tmp,/dev/shm сохраняются. + +Nginx-сниппет повышает read timeout только для /courses до85s, чтобы browser deadline60s и сохранение данных завершались до90s lease. Другие маршруты и сервисы не меняются. diff --git a/deploy/chromium-seccomp.json b/deploy/chromium-seccomp.json new file mode 100644 index 0000000..fddc05f --- /dev/null +++ b/deploy/chromium-seccomp.json @@ -0,0 +1,831 @@ +{ + "defaultAction": "SCMP_ACT_ERRNO", + "archMap": [ + { + "architecture": "SCMP_ARCH_X86_64", + "subArchitectures": [ + "SCMP_ARCH_X86", + "SCMP_ARCH_X32" + ] + }, + { + "architecture": "SCMP_ARCH_AARCH64", + "subArchitectures": [ + "SCMP_ARCH_ARM" + ] + }, + { + "architecture": "SCMP_ARCH_MIPS64", + "subArchitectures": [ + "SCMP_ARCH_MIPS", + "SCMP_ARCH_MIPS64N32" + ] + }, + { + "architecture": "SCMP_ARCH_MIPS64N32", + "subArchitectures": [ + "SCMP_ARCH_MIPS", + "SCMP_ARCH_MIPS64" + ] + }, + { + "architecture": "SCMP_ARCH_MIPSEL64", + "subArchitectures": [ + "SCMP_ARCH_MIPSEL", + "SCMP_ARCH_MIPSEL64N32" + ] + }, + { + "architecture": "SCMP_ARCH_MIPSEL64N32", + "subArchitectures": [ + "SCMP_ARCH_MIPSEL", + "SCMP_ARCH_MIPSEL64" + ] + }, + { + "architecture": "SCMP_ARCH_S390X", + "subArchitectures": [ + "SCMP_ARCH_S390" + ] + } + ], + "syscalls": [ + { + "comment": "Allow create user namespaces", + "names": [ + "clone", + "setns", + "unshare" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "includes": {}, + "excludes": {} + }, + { + "names": [ + "accept", + "accept4", + "access", + "adjtimex", + "alarm", + "bind", + "brk", + "capget", + "capset", + "chdir", + "chmod", + "chown", + "chown32", + "clock_adjtime", + "clock_adjtime64", + "clock_getres", + "clock_getres_time64", + "clock_gettime", + "clock_gettime64", + "clock_nanosleep", + "clock_nanosleep_time64", + "close", + "connect", + "copy_file_range", + "creat", + "dup", + "dup2", + "dup3", + "epoll_create", + "epoll_create1", + "epoll_ctl", + "epoll_ctl_old", + "epoll_pwait", + "epoll_wait", + "epoll_wait_old", + "eventfd", + "eventfd2", + "execve", + "execveat", + "exit", + "exit_group", + "faccessat", + "fadvise64", + "fadvise64_64", + "fallocate", + "fanotify_mark", + "fchdir", + "fchmod", + "fchmodat", + "fchown", + "fchown32", + "fchownat", + "fcntl", + "fcntl64", + "fdatasync", + "fgetxattr", + "flistxattr", + "flock", + "fork", + "fremovexattr", + "fsetxattr", + "fstat", + "fstat64", + "fstatat64", + "fstatfs", + "fstatfs64", + "fsync", + "ftruncate", + "ftruncate64", + "futex", + "futex_time64", + "futimesat", + "getcpu", + "getcwd", + "getdents", + "getdents64", + "getegid", + "getegid32", + "geteuid", + "geteuid32", + "getgid", + "getgid32", + "getgroups", + "getgroups32", + "getitimer", + "getpeername", + "getpgid", + "getpgrp", + "getpid", + "getppid", + "getpriority", + "getrandom", + "getresgid", + "getresgid32", + "getresuid", + "getresuid32", + "getrlimit", + "get_robust_list", + "getrusage", + "getsid", + "getsockname", + "getsockopt", + "get_thread_area", + "gettid", + "gettimeofday", + "getuid", + "getuid32", + "getxattr", + "inotify_add_watch", + "inotify_init", + "inotify_init1", + "inotify_rm_watch", + "io_cancel", + "ioctl", + "io_destroy", + "io_getevents", + "io_pgetevents", + "io_pgetevents_time64", + "ioprio_get", + "ioprio_set", + "io_setup", + "io_submit", + "io_uring_enter", + "io_uring_register", + "io_uring_setup", + "ipc", + "kill", + "lchown", + "lchown32", + "lgetxattr", + "link", + "linkat", + "listen", + "listxattr", + "llistxattr", + "_llseek", + "lremovexattr", + "lseek", + "lsetxattr", + "lstat", + "lstat64", + "madvise", + "membarrier", + "memfd_create", + "mincore", + "mkdir", + "mkdirat", + "mknod", + "mknodat", + "mlock", + "mlock2", + "mlockall", + "mmap", + "mmap2", + "mprotect", + "mq_getsetattr", + "mq_notify", + "mq_open", + "mq_timedreceive", + "mq_timedreceive_time64", + "mq_timedsend", + "mq_timedsend_time64", + "mq_unlink", + "mremap", + "msgctl", + "msgget", + "msgrcv", + "msgsnd", + "msync", + "munlock", + "munlockall", + "munmap", + "nanosleep", + "newfstatat", + "_newselect", + "open", + "openat", + "pause", + "pipe", + "pipe2", + "poll", + "ppoll", + "ppoll_time64", + "prctl", + "pread64", + "preadv", + "preadv2", + "prlimit64", + "pselect6", + "pselect6_time64", + "pwrite64", + "pwritev", + "pwritev2", + "read", + "readahead", + "readlink", + "readlinkat", + "readv", + "recv", + "recvfrom", + "recvmmsg", + "recvmmsg_time64", + "recvmsg", + "remap_file_pages", + "removexattr", + "rename", + "renameat", + "renameat2", + "restart_syscall", + "rmdir", + "rseq", + "rt_sigaction", + "rt_sigpending", + "rt_sigprocmask", + "rt_sigqueueinfo", + "rt_sigreturn", + "rt_sigsuspend", + "rt_sigtimedwait", + "rt_sigtimedwait_time64", + "rt_tgsigqueueinfo", + "sched_getaffinity", + "sched_getattr", + "sched_getparam", + "sched_get_priority_max", + "sched_get_priority_min", + "sched_getscheduler", + "sched_rr_get_interval", + "sched_rr_get_interval_time64", + "sched_setaffinity", + "sched_setattr", + "sched_setparam", + "sched_setscheduler", + "sched_yield", + "seccomp", + "select", + "semctl", + "semget", + "semop", + "semtimedop", + "semtimedop_time64", + "send", + "sendfile", + "sendfile64", + "sendmmsg", + "sendmsg", + "sendto", + "setfsgid", + "setfsgid32", + "setfsuid", + "setfsuid32", + "setgid", + "setgid32", + "setgroups", + "setgroups32", + "setitimer", + "setpgid", + "setpriority", + "setregid", + "setregid32", + "setresgid", + "setresgid32", + "setresuid", + "setresuid32", + "setreuid", + "setreuid32", + "setrlimit", + "set_robust_list", + "setsid", + "setsockopt", + "set_thread_area", + "set_tid_address", + "setuid", + "setuid32", + "setxattr", + "shmat", + "shmctl", + "shmdt", + "shmget", + "shutdown", + "sigaltstack", + "signalfd", + "signalfd4", + "sigprocmask", + "sigreturn", + "socket", + "socketcall", + "socketpair", + "splice", + "stat", + "stat64", + "statfs", + "statfs64", + "statx", + "symlink", + "symlinkat", + "sync", + "sync_file_range", + "syncfs", + "sysinfo", + "tee", + "tgkill", + "time", + "timer_create", + "timer_delete", + "timer_getoverrun", + "timer_gettime", + "timer_gettime64", + "timer_settime", + "timer_settime64", + "timerfd_create", + "timerfd_gettime", + "timerfd_gettime64", + "timerfd_settime", + "timerfd_settime64", + "times", + "tkill", + "truncate", + "truncate64", + "ugetrlimit", + "umask", + "uname", + "unlink", + "unlinkat", + "utime", + "utimensat", + "utimensat_time64", + "utimes", + "vfork", + "vmsplice", + "wait4", + "waitid", + "waitpid", + "write", + "writev" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": {}, + "excludes": {} + }, + { + "names": [ + "ptrace" + ], + "action": "SCMP_ACT_ALLOW", + "args": null, + "comment": "", + "includes": { + "minKernel": "4.8" + }, + "excludes": {} + }, + { + "names": [ + "personality" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 0, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + } + ], + "comment": "", + "includes": {}, + "excludes": {} + }, + { + "names": [ + "personality" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 8, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + } + ], + "comment": "", + "includes": {}, + "excludes": {} + }, + { + "names": [ + "personality" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 131072, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + } + ], + "comment": "", + "includes": {}, + "excludes": {} + }, + { + "names": [ + "personality" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 131080, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + } + ], + "comment": "", + "includes": {}, + "excludes": {} + }, + { + "names": [ + "personality" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 4294967295, + "valueTwo": 0, + "op": "SCMP_CMP_EQ" + } + ], + "comment": "", + "includes": {}, + "excludes": {} + }, + { + "names": [ + "sync_file_range2" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "arches": [ + "ppc64le" + ] + }, + "excludes": {} + }, + { + "names": [ + "arm_fadvise64_64", + "arm_sync_file_range", + "sync_file_range2", + "breakpoint", + "cacheflush", + "set_tls" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "arches": [ + "arm", + "arm64" + ] + }, + "excludes": {} + }, + { + "names": [ + "arch_prctl" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "arches": [ + "amd64", + "x32" + ] + }, + "excludes": {} + }, + { + "names": [ + "modify_ldt" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "arches": [ + "amd64", + "x32", + "x86" + ] + }, + "excludes": {} + }, + { + "names": [ + "s390_pci_mmio_read", + "s390_pci_mmio_write", + "s390_runtime_instr" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "arches": [ + "s390", + "s390x" + ] + }, + "excludes": {} + }, + { + "names": [ + "open_by_handle_at" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_DAC_READ_SEARCH" + ] + }, + "excludes": {} + }, + { + "names": [ + "bpf", + "clone", + "fanotify_init", + "lookup_dcookie", + "mount", + "name_to_handle_at", + "perf_event_open", + "quotactl", + "setdomainname", + "sethostname", + "setns", + "syslog", + "umount", + "umount2", + "unshare" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_ADMIN" + ] + }, + "excludes": {} + }, + { + "names": [ + "clone" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 2114060288, + "valueTwo": 0, + "op": "SCMP_CMP_MASKED_EQ" + } + ], + "comment": "", + "includes": {}, + "excludes": { + "caps": [ + "CAP_SYS_ADMIN" + ], + "arches": [ + "s390", + "s390x" + ] + } + }, + { + "names": [ + "clone" + ], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 1, + "value": 2114060288, + "valueTwo": 0, + "op": "SCMP_CMP_MASKED_EQ" + } + ], + "comment": "s390 parameter ordering for clone is different", + "includes": { + "arches": [ + "s390", + "s390x" + ] + }, + "excludes": { + "caps": [ + "CAP_SYS_ADMIN" + ] + } + }, + { + "names": [ + "reboot" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_BOOT" + ] + }, + "excludes": {} + }, + { + "names": [ + "chroot" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_CHROOT" + ] + }, + "excludes": {} + }, + { + "names": [ + "delete_module", + "init_module", + "finit_module" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_MODULE" + ] + }, + "excludes": {} + }, + { + "names": [ + "acct" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_PACCT" + ] + }, + "excludes": {} + }, + { + "names": [ + "kcmp", + "process_vm_readv", + "process_vm_writev", + "ptrace" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_PTRACE" + ] + }, + "excludes": {} + }, + { + "names": [ + "iopl", + "ioperm" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_RAWIO" + ] + }, + "excludes": {} + }, + { + "names": [ + "settimeofday", + "stime", + "clock_settime" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_TIME" + ] + }, + "excludes": {} + }, + { + "names": [ + "vhangup" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_TTY_CONFIG" + ] + }, + "excludes": {} + }, + { + "names": [ + "get_mempolicy", + "mbind", + "set_mempolicy" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYS_NICE" + ] + }, + "excludes": {} + }, + { + "names": [ + "syslog" + ], + "action": "SCMP_ACT_ALLOW", + "args": [], + "comment": "", + "includes": { + "caps": [ + "CAP_SYSLOG" + ] + }, + "excludes": {} + } + ] +} diff --git a/deploy/nginx-courses.conf b/deploy/nginx-courses.conf index 14e2955..477fc82 100644 --- a/deploy/nginx-courses.conf +++ b/deploy/nginx-courses.conf @@ -5,7 +5,7 @@ location = /courses { proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; - proxy_read_timeout 35s; + proxy_read_timeout 85s; } location ^~ /courses/ { proxy_pass http://127.0.0.1:3011; @@ -13,5 +13,5 @@ location ^~ /courses/ { proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; - proxy_read_timeout 35s; + proxy_read_timeout 85s; } diff --git a/lib/access.ts b/lib/access.ts new file mode 100644 index 0000000..3588091 --- /dev/null +++ b/lib/access.ts @@ -0,0 +1,65 @@ +import {parseHTML} from 'linkedom'; +import type {Check} from './model.ts'; + +export const RULES_VERSION='access-1'; +export const CHECK_TTL=24*60*60*1000; +export function checkCurrent(check:Check|undefined,now=Date.now()){ + return !!check&&check.rulesVersion===RULES_VERSION&&check.profile==='anonymous'&&now-Date.parse(check.checkedAt)<(['unknown','blocked','network','timeout'].includes(check.status)?600000:CHECK_TTL); +} +export function evidenceUrl(raw:string){const u=new URL(raw);for(const key of [...u.searchParams.keys()])if(!['pid','id','bcode'].includes(key))u.searchParams.delete(key);u.hash='';return u.href;} +export function result(url:string,status:Check['status'],reason:string,extra:Partial={}):Check{ + return {url,status,reason,checkedAt:new Date().toISOString(),rulesVersion:RULES_VERSION,profile:'anonymous',...extra}; +} +function evidence(url:string,text:string,rule:string){const normalized=text.replace(/\s+/g,' ').trim(),at=normalized.search(/публикация\s+изъята|для\s+(?:получения\s+)?(?:доступа|чтения|просмотра)|чтобы\s+получить\s+полный\s+доступ|sign in to|чтение\s+книги\s+недоступно|книга\s+недоступна/i);return {text:normalized.slice(Math.max(0,at),Math.max(0,at)+500),url:evidenceUrl(url),rule};} +export function accessMessage(url:string,text:string):Check|null{ + const normalized=text.replace(/\s+/g,' ').trim(); + const preview=/доступн[а-я]*\s+(?:только\s+|первые\s+)?(\d{1,4})\s+страниц/i.exec(normalized); + if(preview&&/гостев|ознакомительн|фрагмент/i.test(normalized))return result(url,'restricted',`Доступен только ознакомительный фрагмент — до ${preview[1]} страниц; полный текст недоступен.`,{evidence:evidence(url,normalized.slice(Math.max(0,preview.index-150),preview.index+200),'reader-preview-pages')}); + if(/публикация\s+изъята\s+из\s+фонда/i.test(normalized))return result(url,'broken','Публикация изъята из фонда.',{evidence:evidence(url,normalized,'publication-withdrawn')}); + if(/\/bookshelf\/\d+\/reading/.test(new URL(url).pathname)&&/для продолжения работы с сайтом.{0,60}войдите в систему/i.test(normalized))return result(url,'auth','Для чтения книги требуется авторизация.',{evidence:evidence(url,normalized,'ibooks-reader-login')}); + const subscription=/(?:для\s+(?:полного\s+)?(?:доступа|чтения)|чтобы\s+(?:получить\s+)?полный\s+доступ|полный\s+текст)[^.]{0,250}(?:подписк|оплат|купить)|доступ\s+(?:к\s+полному\s+тексту\s+)?по\s+подписке|читать\s+по\s+подписке|sign in to (?:read|access)[^.]{0,100}subscri/i.test(normalized); + const login=/(?:для\s+(?:получения\s+)?(?:доступа\s+к\s+)?(?:чтения|просмотра|доступа)|чтобы\s+(?:получить\s+)?полный\s+доступ)[^.]{0,220}(?:авториз|войти|войдите|вход)|(?:авторизуйтесь|войдите)[^.]{0,100}(?:чтения|полного\s+доступа|чита)|sign in to (?:read|access|view)/i.test(normalized); + if(login||subscription)return result(url,subscription?'restricted':'auth',login&&subscription?'Для чтения книги требуются авторизация и подписка.':login?'Для чтения книги требуется авторизация.':'Для чтения книги требуется подписка или оплата.',{evidence:evidence(url,normalized,login&&subscription?'reader-login-subscription':login?'reader-login':'reader-subscription')}); + if(/(?:доступен|доступно)\s+только\s+(?:ознакомительный\s+)?фрагмент|только\s+ознакомительный\s+фрагмент/i.test(normalized))return result(url,'restricted','Доступен только ознакомительный фрагмент; полный текст недоступен.',{evidence:evidence(url,normalized,'reader-preview')}); + if(/книга\s+недоступна\s+для\s+чтения|чтение\s+книги\s+недоступно/i.test(normalized))return result(url,'restricted','Чтение книги недоступно. Сайт не указал подтверждённую причину.',{evidence:evidence(url,normalized,'reader-denied')}); + return null; +} +export function purchaseOffer(url:string,html:string):Check|null{ + const {document}=parseHTML(html),host=new URL(url).hostname; + document.querySelectorAll('script,style,[hidden],[aria-hidden="true"],.hidden,[style*="display: none"],[style*="display:none"]').forEach(n=>n.remove()); + if(host==='znanium.ru'||host.endsWith('.znanium.ru')){ + const id=new URL(url).searchParams.get('id'); + for(const item of document.querySelectorAll('.book-offers__item')){ + const text=item.textContent??'',control=item.querySelector('.book-offers__ctrl'); + if(/доступ\s+онлайн/i.test(text)&&/[1-9]\d*(?:[.,]\d+)?\s*₽/.test(text)&&/в корзину/i.test(control?.textContent??'')&&(!id||control?.getAttribute('data-id')===id))return result(url,'restricted','Для чтения книги требуется покупка онлайн-доступа или подписка.',{evidence:evidence(url,text,'znanium-online-purchase')}); + } + } + if(/(^|\.)(springer\.com|springernature\.com)$/.test(host)){ + for(const access of document.querySelectorAll('.app-article-access')){ + if(/this is an open access book|open access/i.test(access.textContent??''))continue; + for(const option of access.querySelectorAll('.buying-option')){ + if(/ebook/i.test(option.textContent??'')&&/[1-9]/.test(option.querySelector('.price-amount')?.textContent??'')&&/buy\s+ebook/i.test(option.querySelector('button')?.textContent??''))return result(url,'restricted','Для чтения требуется покупка электронной книги или доступ по подписке организации.',{evidence:evidence(url,'Access this book. '+(option.textContent??'').replace(/\s+/g,' '),'springer-ebook-purchase')}); + } + } + } + return null; +} +export function classifyResponse(url:string,r:{code:number;url:string;body:string;type:string}):Check{ + const base={finalUrl:evidenceUrl(r.url),code:r.code}; + const {document}=parseHTML(/html|text\//i.test(r.type)?r.body:''); + document.querySelectorAll('script,style,nav,footer,header,[hidden],[aria-hidden="true"],.hidden').forEach(n=>n.remove()); + const title=(document.querySelector('title')?.textContent??'').trim(); + const text=(document.querySelector('main,[role="main"]')??document.querySelector('body')??document.documentElement)?.textContent??''; + const message=accessMessage(r.url,text);if(message?.status==='broken')return {...message,url,...base}; + if(r.code===404||r.code===410)return result(url,'broken',r.code===404?'Страница материала не найдена.':'Материал удалён.',base); + if(r.code===401||/(?:\/|\.)(?:login|signin|sign-in|sso|auth)(?:[/.?]|$)/i.test(r.url))return result(url,'auth','Для чтения материала требуется авторизация.',base); + if(r.code===403&&message)return {...message,url,...base}; + if(r.code===403||r.code===429||/captcha|verify you are human|checking your browser|just a moment|докажите, что вы|проверка браузера/i.test(title+' '+text.slice(0,2000)))return result(url,'blocked','Сайт ограничил автоматическую проверку. Доступ к полному тексту не определён.',base); + if(r.code>=400)return result(url,'network',`Сайт временно не предоставил материал (HTTP ${r.code}).`,base); + if(r.code<200||r.code>=300)return result(url,'unknown',`Ответ HTTP ${r.code} не подтвердил доступ к материалу.`,base); + if(message)return {...message,url,...base}; + const purchase=purchaseOffer(r.url,r.body);if(purchase)return {...purchase,url,...base}; + if(/^(404|страница не найдена|page not found|not found)/i.test(title))return result(url,'broken','Сайт показывает страницу «не найдено».',{...base,evidence:evidence(r.url,title,'page-not-found')}); + if(/application\/pdf/i.test(r.type)&&r.body.startsWith('%PDF-'))return result(url,'unknown','PDF получен без авторизации, но не установлено, содержит ли он полный текст или только фрагмент.',base); + return result(url,'unknown','Страница открывается, но доступ к полному тексту не подтверждён.',base); +} diff --git a/lib/browser.ts b/lib/browser.ts new file mode 100644 index 0000000..787c7a2 --- /dev/null +++ b/lib/browser.ts @@ -0,0 +1,95 @@ +import {chromium,type Route} from 'playwright'; +import {requestPublic,safeUrl} from './http.ts'; +import {waitForHost,limitHost,HostCooldown} from './throttle.ts'; +import {accessMessage,purchaseOffer,classifyResponse,result,evidenceUrl} from './access.ts'; +import type {Check} from './model.ts'; + +import {provider} from './provider.ts'; +export function browserRequestAllowed(raw:string,root:string,method:string,resource:string){ + const url=safeUrl(raw);if(!url)return false;const u=new URL(url); + return u.protocol==='https:'&&provider(u.href)===root&&['GET','HEAD','OPTIONS'].includes(method)&&!['image','media','font','manifest'].includes(resource); +} +export async function checkBrowser(url:string,parentSignal?:AbortSignal):Promise{ + const root=provider(url);if(!root)return result(url,'unknown','Для этого сайта нет правил проверки полного текста.'); + const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),30000); + const abort=()=>controller.abort();parentSignal?.addEventListener('abort',abort,{once:true});if(parentSignal?.aborted)controller.abort(); + let browser:Awaited>|undefined; + let requests=0,totalBytes=0,failed=false,navigations=0,mainCode=200,limit:Check|null=null; + const active=new Set>(); + try{ + controller.signal.throwIfAborted();browser=await chromium.launch({headless:true,chromiumSandbox:true,timeout:10000,env:{PATH:process.env.PATH??'/usr/bin:/bin',HOME:'/tmp',TMPDIR:'/tmp',LANG:'C.UTF-8'},args:[ + '--proxy-server=http://127.0.0.1:9','--proxy-bypass-list=<-loopback>','--disable-quic','--force-webrtc-ip-handling-policy=disable_non_proxied_udp', + ]}); + controller.signal.throwIfAborted();const context=await browser.newContext({serviceWorkers:'block',acceptDownloads:false,viewport:{width:1280,height:900}}); + controller.signal.addEventListener('abort',()=>{void context.close().catch(()=>{});},{once:true}); + await context.routeWebSocket('**/*',socket=>socket.close()); + const handle=async(route:Route)=>{ + const request=route.request(),target=request.url(),navigation=request.isNavigationRequest(); + try{ + if(controller.signal.aborted||limit||!browserRequestAllowed(target,root,request.method(),request.resourceType())){await route.abort();return;} + if(++requests>100||totalBytes>32000000||active.size>=10)throw new Error('Превышен лимит ресурсов страницы'); + if(navigation){if(++navigations>8)throw new Error('Слишком много переходов');await waitForHost(new URL(target).hostname,controller.signal);} + const headers:Record={};for(const key of ['accept','accept-language','origin','referer','cookie','content-type']){const value=request.headers()[key];if(value)headers[key]=value;} + const response=await requestPublic(target,{signal:controller.signal,maxBytes:8000000,method:request.method() as 'GET'|'HEAD'|'OPTIONS',headers}); + totalBytes+=response.body.length;if(totalBytes>32000000)throw new Error('Превышен лимит ресурсов страницы'); + if([429,503].includes(response.code)){ + await limitHost(new URL(target).hostname,response.code,response.headers['retry-after']); + limit=result(url,'blocked',`Сайт остановил автоматическую проверку (HTTP ${response.code}). Доступ к полному тексту не определён.`); + } + if(navigation&&request.frame()===page.mainFrame())mainCode=response.code; + const fulfilled:Record={}; + for(const [key,value] of Object.entries(response.headers))if(value!==undefined&&!['content-encoding','content-length','transfer-encoding','connection'].includes(key))fulfilled[key]=Array.isArray(value)?value.join('\n'):String(value); + await route.fulfill({status:response.code,headers:fulfilled,body:response.body}); + }catch(error){ + if(error instanceof HostCooldown)limit=result(url,'blocked',error.message); + else if(navigation||['script','xhr','fetch','stylesheet'].includes(request.resourceType()))failed=true; + await route.abort().catch(()=>{}); + } + }; + // Every request is fulfilled by the pinned Node transport. The dead proxy also prevents direct browser egress. + await context.route('**/*',route=>{const task=handle(route);active.add(task);void task.finally(()=>active.delete(task));return task;}); + const page=await context.newPage(); + context.on('page',other=>{if(other!==page)void other.close().catch(()=>{});}); + page.setDefaultTimeout(5000);page.setDefaultNavigationTimeout(20000); + const capture=async()=>{ + await page.locator('body').waitFor({state:'attached'}); + // Only rendered text is evidence; hidden login templates and site chrome cannot decide book access. + const text=await page.locator('body').innerText(); + const offers=await page.locator('.book-offers__item,.app-article-access').evaluateAll(nodes=>nodes.filter(node=>{const style=getComputedStyle(node);return style.display!=='none'&&style.visibility!=='hidden'&&node.getClientRects().length>0;}).map(node=>node.outerHTML).join('')); + return {text,offers,url:page.url(),title:await page.title()}; + }; + const settle=async()=>{ + await page.waitForFunction(()=>{ + const text=document.body?.innerText??''; + return /чтение книги недоступно|необходимо авторизоваться|публикация изъята|чтобы получить полный доступ|книга недоступна для чтения|для продолжения работы с сайтом|библиографическое описание|not found|captcha|проверка браузера/i.test(text)||!!document.querySelector('a[href*="/course-viewer/"],a[href*="/viewer/"],a[href*="reader.new.book.ru"],.book-offers__item,.app-article-access'); + },undefined,{timeout:12000}).catch(()=>{}); + }; + await page.goto(url,{waitUntil:'domcontentloaded'});await settle();let snapshot=await capture(); + if(root==='book.ru'){ + const preview=page.getByRole('link',{name:'Ознакомиться',exact:true}); + if(await preview.count()){ + await preview.hover({timeout:3000}).catch(()=>{}); + await page.waitForFunction(()=>/гостевом\s+режиме|доступны\s+только\s+\d+\s+страниц/i.test(document.body?.innerText??''),undefined,{timeout:3000}).catch(()=>{});snapshot=await capture(); + } + } + const known=accessMessage(snapshot.url,snapshot.text)??purchaseOffer(snapshot.url,snapshot.offers); + if(known)return {...known,url,code:mainCode,finalUrl:evidenceUrl(snapshot.url)}; + if(root==='urait.ru'||root==='ibooks.ru'){ + // Follow only a book reader link already present on this exact card, never subscription/purchase controls. + const href=root==='urait.ru'?await page.locator('a[href*="/course-viewer/"],a[href*="/viewer/"]').first().getAttribute('href').catch(()=>null):snapshot.text.match(/https:\/\/ibooks\.ru\/bookshelf\/\d+\/reading/)?.[0]??null; + const reader=href?safeUrl(href,snapshot.url):null; + if(reader&&provider(reader)===root){await page.goto(reader,{waitUntil:'domcontentloaded'});await settle();snapshot=await capture(); + const message=accessMessage(snapshot.url,snapshot.text);if(message)return {...message,url,code:mainCode,finalUrl:evidenceUrl(snapshot.url)}; + } + } + if(limit)return limit; + const escape=(text:string)=>text.replaceAll('&','&').replaceAll('<','<'); + const classified=classifyResponse(url,{code:mainCode,url:snapshot.url,body:''+escape(snapshot.title)+''+escape(snapshot.text)+'',type:'text/html'}); + if(mainCode===403&&classified.status==='blocked')await limitHost(new URL(snapshot.url).hostname,403); + if(failed&&classified.status==='unknown')classified.reason='Страница загрузилась не полностью. Доступ к полному тексту не подтверждён.'; + return classified; + }catch(error){ + if(limit)return limit; + return result(url,'unknown',controller.signal.aborted?'Не удалось определить доступ к полному тексту за отведённое время.':'Не удалось завершить браузерную проверку. Доступ к полному тексту не подтверждён.'); + }finally{clearTimeout(timer);parentSignal?.removeEventListener('abort',abort);controller.abort();await browser?.close().catch(()=>{});await Promise.allSettled(active);} +} diff --git a/lib/checker.ts b/lib/checker.ts index cc47c9d..c0aa71b 100644 --- a/lib/checker.ts +++ b/lib/checker.ts @@ -1,15 +1,9 @@ import {parseHTML} from 'linkedom'; -import {lookup} from 'node:dns/promises'; -import {request as httpRequest} from 'node:http'; -import {request as httpsRequest} from 'node:https'; -import {createUnzip,createBrotliDecompress} from 'node:zlib'; import type {Check,Reference} from './model'; -import {waitForHost,limitHost,HostCooldown} from './throttle.ts'; -export function safeUrl(value:string,base?:string):string|null{ -try{const u=new URL(value,base),h=u.hostname.toLowerCase(); -if(!['https:','http:'].includes(u.protocol)||u.username||u.password||(u.port&&!['80','443'].includes(u.port)))return null; -if(!h.includes('.')||h.includes(':')||/^\d+[.\d]*$/.test(h)||h.endsWith('.')||/(^|\.)(localhost|local|internal|test|invalid|example|onion|arpa)$/.test(h))return null; -u.hash='';return u.href;}catch{return null;}} +import {HostCooldown,limitHost} from './throttle.ts'; +import {safeUrl,retrieve} from './http.ts'; +import {classifyResponse,result} from './access.ts'; +import {provider} from './provider.ts'; export function clean(text:string){return text.replace(/\s+/g,' ').trim();} export function trimUrl(value:string){let v=value.replace(/[.,;:!?]+$/g,'');for(const [open,close] of [['(',')'],['[',']'],['{','}']])while(v.endsWith(close)&&v.split(close).length>v.split(open).length)v=v.slice(0,-1);return v;} export function parseCatalog(html:string,url:string,root:string){ @@ -31,76 +25,13 @@ for(const raw of urls){const normalized=safeUrl(raw,url);if(normalized&&normaliz const content=[...document.querySelectorAll('.pud__section')].map(s=>clean(s.textContent??'')).join('\n')||clean(document.querySelector('main')?.textContent??document.querySelector('h1')?.textContent??''); return {name:clean(document.querySelector('h1')?.textContent??''),refs:[...refs.values()],literature:found,content};} export async function fingerprint(v:string){return [...new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(v)))].map(n=>n.toString(16).padStart(2,'0')).join('');} -export function publicIp(ip:string){ -if(ip.includes(':')){const fields=ip.toLowerCase().split(':');return /^[23][0-9a-f]{3}:/i.test(ip)&&fields[0]!=='2002'&&fields[0]!=='3fff'&&!(fields[0]==='2001'&&(parseInt(fields[1]||'0',16)<=0x1ff||parseInt(fields[1],16)===0xdb8));} -const p=ip.split('.').map(Number);if(p.length!==4||p.some(n=>!Number.isInteger(n)||n<0||n>255))return false; -const [a,b,c]=p;return !(a===0||a===10||a===127||a>=224||a===169&&b===254||a===172&&b>=16&&b<=31||a===192&&(b===168||b===0||b===88&&c===99)||a===100&&b>=64&&b<=127||a===198&&(b===18||b===19||b===51&&c===100)||a===203&&b===0&&c===113); -} -export async function resolvePublicHost(host:string){ - const addresses=await lookup(host,{all:true,verbatim:true}); - if(!addresses.length)throw new Error('DNS: адрес сервера не найден'); - if(addresses.some(a=>!publicIp(a.address)))throw new Error('DNS указывает на непубличный адрес; запрос запрещён'); - return addresses.find(a=>a.family===4)??addresses[0]; -} -export async function retrieve(raw:string,source=false){ - let url=safeUrl(raw);if(!url)throw new Error('Адрес не разрешён политикой безопасности'); - await waitForHost(new URL(url).hostname); - // Four redirects can add up to eight seconds of intentional host pacing. - const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),30000); - const aborted=new Promise((_,reject)=>controller.signal.addEventListener('abort',()=>reject(new DOMException('Истекло время ожидания','AbortError')),{once:true})); +export {safeUrl,publicIp,resolvePublicHost,retrieve} from './http.ts'; +export {classifyResponse as classify} from './access.ts'; +export async function checkLink(url:string):Promise{ try{ - for(let hop=0;hop<5;hop++){ - const target=new URL(url); - if(hop)await waitForHost(target.hostname,controller.signal); - const ip=await Promise.race([resolvePublicHost(target.hostname),aborted]); - // Pin the validated address into this socket. Redirects create a new socket and repeat DNS validation. - const result=await new Promise<{code:number;location?:string;body:string;type:string;retryAfter?:string}>((resolve,reject)=>{ - const request=(target.protocol==='https:'?httpsRequest:httpRequest)(target,{ - agent:false,family:ip.family,signal:controller.signal, - lookup:(_hostname,_options,callback)=>callback(null,ip.address,ip.family), - headers:{'User-Agent':'Sverka/1.1 (academic bibliography check)','Accept':'text/html,application/pdf;q=0.9,*/*;q=0.8','Accept-Encoding':'identity'}, - },response=>{ - const code=response.statusCode??0,type=String(response.headers['content-type']??''),location=response.headers.location; - if([403,429,503].includes(code)){response.destroy();resolve({code,body:'',type,retryAfter:response.headers['retry-after']});return;} - if(location&&code>=300&&code<400){response.destroy();resolve({code,location,body:'',type});return;} - if(!source&&!/html|text\//i.test(type)){response.destroy();resolve({code,body:'',type});return;} - void(async()=>{ - const encoding=String(response.headers['content-encoding']??''); - const stream=/gzip|deflate/i.test(encoding)?response.pipe(createUnzip()):/br/i.test(encoding)?response.pipe(createBrotliDecompress()):response; - // pipe() does not forward source errors to a decompressor by itself. - response.on('error',error=>stream.destroy(error)); - response.on('aborted',()=>stream.destroy(new Error('Соединение прервано'))); - request.once('error',error=>{response.destroy();stream.destroy(error);}); - const chunks:Buffer[]=[];let size=0; - try{for await(const chunk of stream){const bytes=Buffer.from(chunk);size+=bytes.length;if(size>(source?3000000:1500000))throw new Error('Превышен допустимый размер загрузки');chunks.push(bytes);} - const charset=/charset=([\w-]+)/i.exec(type)?.[1]??'utf-8';let body:string;try{body=new TextDecoder(charset).decode(Buffer.concat(chunks));}catch{body=Buffer.concat(chunks).toString('utf8');}resolve({code,body,type}); - }catch(error){response.destroy();stream.destroy();reject(error);} - })(); - });request.on('error',reject);request.end(); - }); - await limitHost(target.hostname,result.code,result.retryAfter); - if(result.location&&result.code>=300&&result.code<400){const next=safeUrl(result.location,url);if(!next)throw new Error('Небезопасное перенаправление');url=next;continue;} - return {...result,url}; - } - throw new Error('Слишком много перенаправлений'); - }finally{clearTimeout(timer);} + const signal=AbortSignal.timeout(60000),page=await retrieve(url,false,signal),check=classifyResponse(url,page); + if(page.code===403&&check.status==='blocked')await limitHost(new URL(page.url).hostname,403); + if(check.status==='unknown'&&provider(url))return await (await import('./browser.ts')).checkBrowser(url,signal); + return check; + }catch(e){const error=e as Error;return result(url,error instanceof HostCooldown?'blocked':error.name==='AbortError'?'timeout':'network',error instanceof HostCooldown?error.message:'Не удалось загрузить материал. Доступ к полному тексту не подтверждён.');} } -export function classify(url:string,r:{code:number;url:string;body:string;type:string}):Check{ -const out:Check={url,finalUrl:r.url,code:r.code,checkedAt:new Date().toISOString(),status:'ok',reason:'Сервер вернул материал. Доступ к полному тексту может потребовать ручной проверки.'}; -if(r.code===404||r.code===410)return {...out,status:'broken',reason:`HTTP ${r.code}: ${r.code===404?'страница не найдена':'материал удалён'}.`}; -if(r.code===401||/(?:\/|\.)(?:login|signin|sign-in|sso|auth)(?:[/.?]|$)/i.test(r.url))return {...out,status:'auth',reason:'Требуется авторизация или выполнено перенаправление на вход.'}; -if(r.code===403||r.code===429)return {...out,status:'blocked',reason:`HTTP ${r.code}: сервер ограничил автоматический доступ. Проверьте вручную.`}; -if(r.code>=400)return {...out,status:'network',reason:`HTTP ${r.code}: сервер не смог предоставить материал.`}; -if(r.code<200||r.code>=300)return {...out,status:'unknown',reason:`Необычный ответ HTTP ${r.code}; нужна ручная проверка.`}; -if(r.code===204||r.code===205)return {...out,status:'unknown',reason:`HTTP ${r.code}: сервер вернул ответ без содержимого. Доступ к материалу не подтверждён.`}; -if(!/html|text\//i.test(r.type))return out; -if(!r.body.trim())return {...out,status:'unknown',reason:'Сервер вернул пустую страницу. Доступ к материалу не подтверждён.'}; -const {document}=parseHTML(r.body);const scripted=!!document.querySelector('script');document.querySelectorAll('script,style,nav,footer,header').forEach(n=>n.remove()); -const title=clean(document.querySelector('title,h1')?.textContent??''),text=clean(document.querySelector('body')?.textContent??document.documentElement?.textContent??r.body); -if(/captcha|verify you are human|checking your browser|just a moment|докажите, что вы|проверка браузера|доступ ограничен/i.test(title+' '+text.slice(0,2500)))return {...out,status:'blocked',reason:'CAPTCHA или антибот-защита: автоматически подтвердить доступность нельзя.'}; -if(/^(404|страница не найдена|page not found|not found)/i.test(title))return {...out,status:'broken',reason:'Сайт показывает «не найдено» при успешном HTTP-ответе.'}; -if(document.querySelector('input[type="password"]')&&text.length<10000)return {...out,status:'auth',reason:'Обнаружена форма входа с паролем; доступ к материалу не подтверждён.'}; -if(/(для (?:чтения|доступа|просмотра).{0,80}(?:авториз|войд|подписк)|(?:авторизуйтесь|войдите).{0,80}(?:чита|доступ|просмотр)|sign in to (?:read|access|view)|purchase (?:this|access)|institutional access|доступ (?:к полному тексту|по подписке)|читать по подписке)/i.test(text))return {...out,status:'restricted',reason:'Для полного текста может потребоваться вход, подписка или доступ организации. Проверьте вручную.'}; -if(/html/i.test(r.type)&&!text)return {...out,status:'unknown',reason:'Получена пустая HTML-страница. Доступ к материалу не подтверждён.'}; -if(/html/i.test(r.type)&&((scripted&&text.length<200)||/^(загрузка|loading)[.\s…]*$/i.test(text)))return {...out,status:'unknown',reason:'Получена оболочка страницы с загрузкой через JavaScript. Доступ к материалу не подтверждён; откройте ссылку вручную.'};return out;} -export async function checkLink(url:string):Promise{try{return classify(url,await retrieve(url));}catch(e){const error=e as Error;return {url,status:error instanceof HostCooldown?'blocked':error.name==='AbortError'?'timeout':'network',reason:error.name==='AbortError'?'Превышен общий лимит ожидания: 30 секунд, включая паузы между перенаправлениями.':`Не удалось загрузить: ${error.message.slice(0,180)}`,checkedAt:new Date().toISOString()};}} diff --git a/lib/http.ts b/lib/http.ts new file mode 100644 index 0000000..0d7ba72 --- /dev/null +++ b/lib/http.ts @@ -0,0 +1,63 @@ +import {lookup} from 'node:dns/promises'; +import {request as httpRequest,type IncomingHttpHeaders} from 'node:http'; +import {request as httpsRequest} from 'node:https'; +import {createUnzip,createBrotliDecompress} from 'node:zlib'; +import {waitForHost,limitHost} from './throttle.ts'; + +export function safeUrl(value:string,base?:string):string|null{ + try{const u=new URL(value,base),h=u.hostname.toLowerCase(); + if(!['https:','http:'].includes(u.protocol)||u.username||u.password||(u.port&&!['80','443'].includes(u.port)))return null; + if(!h.includes('.')||h.includes(':')||/^\d+[.\d]*$/.test(h)||h.endsWith('.')||/(^|\.)(localhost|local|internal|test|invalid|example|onion|arpa)$/.test(h))return null; + u.hash='';return u.href; + }catch{return null;} +} +export function publicIp(ip:string){ + if(ip.includes(':')){const fields=ip.toLowerCase().split(':');return /^[23][0-9a-f]{3}:/i.test(ip)&&fields[0]!=='2002'&&fields[0]!=='3fff'&&!(fields[0]==='2001'&&(parseInt(fields[1]||'0',16)<=0x1ff||parseInt(fields[1],16)===0xdb8));} + const p=ip.split('.').map(Number);if(p.length!==4||p.some(n=>!Number.isInteger(n)||n<0||n>255))return false; + const [a,b,c]=p;return !(a===0||a===10||a===127||a>=224||a===169&&b===254||a===172&&b>=16&&b<=31||a===192&&(b===168||b===0||b===88&&c===99)||a===100&&b>=64&&b<=127||a===198&&(b===18||b===19||b===51&&c===100)||a===203&&b===0&&c===113); +} +export async function resolvePublicHost(host:string){ + const addresses=await lookup(host,{all:true,verbatim:true}); + if(!addresses.length)throw new Error('DNS: адрес сервера не найден'); + if(addresses.some(a=>!publicIp(a.address)))throw new Error('DNS указывает на непубличный адрес; запрос запрещён'); + return addresses.find(a=>a.family===4)??addresses[0]; +} +export type PublicResponse={code:number;headers:IncomingHttpHeaders;body:Buffer;type:string}; +// One pinned socket, no implicit redirects/proxies; browser redirects pass this boundary again. +export async function requestPublic(raw:string,options:{signal?:AbortSignal;maxBytes?:number;headers?:Record;method?:'GET'|'HEAD'|'OPTIONS'}={}):Promise{ + const url=safeUrl(raw);if(!url)throw new Error('Адрес не разрешён политикой безопасности'); + const signal=AbortSignal.any([AbortSignal.timeout(12000),...(options.signal?[options.signal]:[])]); + const aborted=new Promise((_,reject)=>{if(signal.aborted)reject(new DOMException('Истекло время ожидания','AbortError'));else signal.addEventListener('abort',()=>reject(new DOMException('Истекло время ожидания','AbortError')),{once:true});}); + const target=new URL(url),ip=await Promise.race([resolvePublicHost(target.hostname),aborted]); + return new Promise((resolve,reject)=>{ + const request=(target.protocol==='https:'?httpsRequest:httpRequest)(target,{ + agent:false,family:ip.family,signal,lookup:(_hostname,_options,callback)=>callback(null,ip.address,ip.family),method:options.method??'GET', + headers:{'User-Agent':'Sverka/1.3 (academic bibliography access check)','Accept':'text/html,application/pdf;q=0.9,*/*;q=0.8','Accept-Encoding':'identity',...options.headers}, + },response=>{ + const code=response.statusCode??0,headers=response.headers,type=String(headers['content-type']??''); + if(code>=300&&code<400||[429,503].includes(code)||options.method==='HEAD'){response.destroy();resolve({code,headers,type,body:Buffer.alloc(0)});return;} + void(async()=>{ + const encoding=String(headers['content-encoding']??''),stream=/gzip|deflate/i.test(encoding)?response.pipe(createUnzip()):/br/i.test(encoding)?response.pipe(createBrotliDecompress()):response; + response.on('error',error=>stream.destroy(error));response.on('aborted',()=>stream.destroy(new Error('Соединение прервано'))); + request.once('error',error=>{response.destroy();stream.destroy(error);}); + const chunks:Buffer[]=[];let size=0; + try{for await(const chunk of stream){const bytes=Buffer.from(chunk);size+=bytes.length;if(size>(options.maxBytes??3000000))throw new Error('Превышен допустимый размер загрузки');chunks.push(bytes);}resolve({code,headers,type,body:Buffer.concat(chunks)});} + catch(error){response.destroy();stream.destroy();reject(error);} + })(); + });request.once('error',reject);request.end(); + }); +} +export function decodeBody(body:Buffer,type:string){try{return new TextDecoder(/charset=([\w-]+)/i.exec(type)?.[1]??'utf-8').decode(body);}catch{return body.toString('utf8');}} +export async function retrieve(raw:string,source=false,parentSignal?:AbortSignal){ + let url=safeUrl(raw);if(!url)throw new Error('Адрес не разрешён политикой безопасности'); + const signal=AbortSignal.any([AbortSignal.timeout(30000),...(parentSignal?[parentSignal]:[])]); + for(let hop=0;hop<5;hop++){ + await waitForHost(new URL(url).hostname,signal); + const result=await requestPublic(url,{signal,maxBytes:source?3000000:1500000}); + if(result.code!==403||source)await limitHost(new URL(url).hostname,result.code,result.headers['retry-after']); + const location=result.headers.location; + if(location&&result.code>=300&&result.code<400){const next=safeUrl(location,url);if(!next)throw new Error('Небезопасное перенаправление');url=next;continue;} + return {code:result.code,url,type:result.type,body:decodeBody(result.body,result.type)}; + } + throw new Error('Слишком много перенаправлений'); +} diff --git a/lib/model.ts b/lib/model.ts index 98fd68f..f345ba3 100644 --- a/lib/model.ts +++ b/lib/model.ts @@ -1,4 +1,4 @@ -export const VERSION='1.2.2'; +export const VERSION='1.3.0'; export const PROGRAMS=[ {id:'it',name:'Цифровые технологии и телекоммуникации',level:'Бакалавриат',url:'https://www.hse.ru/ba/it/courses'}, {id:'is',name:'Информационная безопасность',level:'Бакалавриат',url:'https://www.hse.ru/ba/is/courses'}, @@ -6,8 +6,8 @@ export const PROGRAMS=[ {id:'internet',name:'Интернет вещей и киберфизические системы',level:'Магистратура',url:'https://www.hse.ru/ma/internet/courses'}, {id:'cyber',name:'Информационная безопасность и технологии искусственного интеллекта',level:'Магистратура',url:'https://www.hse.ru/ma/cyber/courses'}]; export type Status='ok'|'broken'|'auth'|'blocked'|'timeout'|'network'|'restricted'|'unknown'; -export const LABELS:Record={ok:'Доступна',broken:'Не найдена',auth:'Требуется вход',blocked:'Доступ запрещён',timeout:'Нет ответа',network:'Ошибка соединения',restricted:'Ограничен доступ',unknown:'Нужна проверка'}; -export type Check={url:string;finalUrl?:string;status:Status;reason:string;code?:number;checkedAt:string}; +export const LABELS:Record={ok:'Полный текст доступен',broken:'Недоступна: удалена',auth:'Недоступна: нужен вход',blocked:'Проверка ограничена',timeout:'Нет ответа',network:'Ошибка соединения',restricted:'Недоступна',unknown:'Доступ не подтверждён'}; +export type Check={url:string;finalUrl?:string;status:Status;reason:string;code?:number;checkedAt:string;rulesVersion?:string;profile?:'anonymous';evidence?:{text:string;url:string;rule:string}}; export type Reference={url:string;title:string}; export type Course={url:string;name:string;program:string;hash?:string;refs:Reference[];issue?:string;checkedAt:string;seen:string;reused:boolean;literature:boolean}; export type Run={id:string;source?:'manual'|'schedule';status:'running'|'complete';startedAt:string;finishedAt?:string;force:boolean;phase:'catalogs'|'courses'|'links'|'done';catalogs:{url:string;program:string}[];visited:string[];courses:{url:string;name:string;program:string}[];links:string[];seen:string[];errors:{url:string;reason:string}[];processed:number;reused:number;checked:number;totalCourses:number;totalLinks:number;message:string}; diff --git a/lib/provider.ts b/lib/provider.ts new file mode 100644 index 0000000..39b3192 --- /dev/null +++ b/lib/provider.ts @@ -0,0 +1 @@ +export function provider(raw:string){try{const h=new URL(raw).hostname;if(h==='springernature.com'||h.endsWith('.springernature.com'))return 'springer.com';return ['lanbook.com','urait.ru','znanium.ru','ibooks.ru','springer.com','book.ru'].find(root=>h===root||h.endsWith('.'+root))??null;}catch{return null;}} diff --git a/lib/scanner.ts b/lib/scanner.ts index c4a3360..12ce15a 100644 --- a/lib/scanner.ts +++ b/lib/scanner.ts @@ -1,10 +1,11 @@ import {PROGRAMS,DEFAULT_SCHEDULE,type Schedule,type Run,type Course,type Check} from './model.ts'; import {checkLink,retrieve,parseCatalog,parseCourse,fingerprint} from './checker.ts'; import {database,transaction,type Connection} from './store.ts'; +import {checkCurrent} from './access.ts'; async function read(key:string):Promise{const {rows}=await (await database()).query<{value:string}>('SELECT value FROM records WHERE key=$1',[key]);return rows[0]?JSON.parse(rows[0].value):null;} export async function getRun(connection?:Connection):Promise{const {rows}=await (connection??await database()).query<{data:string}>('SELECT data FROM scan WHERE id=1');return rows[0]?JSON.parse(rows[0].data):null;} export async function dashboard(){const run=await getRun(),{rows}=await (await database()).query<{key:string;value:string}>('SELECT key,value FROM records'),courses:Course[]=[],checks:Record={}; -let schedule:Schedule={...DEFAULT_SCHEDULE};for(const row of rows){if(row.key.startsWith('course:'))courses.push(JSON.parse(row.value));else if(row.key.startsWith('link:'))checks[row.key.slice(5)]=JSON.parse(row.value);else if(row.key==='schedule')schedule=JSON.parse(row.value);}return {run,courses,checks,programs:PROGRAMS,schedule};} +let schedule:Schedule={...DEFAULT_SCHEDULE};for(const row of rows){if(row.key.startsWith('course:'))courses.push(JSON.parse(row.value));else if(row.key.startsWith('link:')){const check:Check=JSON.parse(row.value);checks[row.key.slice(5)]=checkCurrent(check)?check:{...check,status:'unknown',reason:'Результат устарел или получен по прежним правилам. Запустите проверку доступа к полному тексту.',evidence:undefined};}else if(row.key==='schedule')schedule=JSON.parse(row.value);}return {run,courses,checks,programs:PROGRAMS,schedule};} export async function start(force:boolean,source:'manual'|'schedule'='manual',connection?:Connection){const run:Run={id:crypto.randomUUID(),source,status:'running',phase:'catalogs',startedAt:new Date().toISOString(),force,catalogs:PROGRAMS.map(p=>({url:p.url,program:p.id})),visited:[],courses:[],links:[],seen:[],errors:[],processed:0,reused:0,checked:0,totalCourses:0,totalLinks:0,message:'Читаем каталоги пяти программ'}; const db=connection??await database();await db.query("INSERT INTO scan(id,data,lease,token) VALUES(1,$1,0,'') ON CONFLICT(id) DO UPDATE SET data=excluded.data,lease=0,token='' WHERE scan.data::jsonb->>'status'!='running'",[JSON.stringify(run)]);return getRun(db);} export async function step(){const token=crypto.randomUUID(),now=Date.now(),db=await database(); @@ -25,7 +26,10 @@ try{const page=await retrieve(task.url,true);if(page.code!==200)throw new Error( const parsed=parseCourse(page.body,task.url);if(!parsed.name)throw new Error('Не удалось прочитать программу курса');const hash=await fingerprint(parsed.name+parsed.content+JSON.stringify(parsed.refs)); if(!run.force&&old?.hash===hash&&!old.issue){course={...old,seen:run.id,reused:true};run.reused++;} else{course={...course,name:parsed.name,hash,refs:parsed.refs,literature:parsed.literature,checkedAt:new Date().toISOString()};if(!parsed.literature)course.issue='Раздел литературы не найден. Проверьте программу курса вручную.'; -for(const ref of course.refs)if(!run.links.includes(ref.url))run.links.push(ref.url);} +} +const keys=course.refs.map(ref=>'link:'+ref.url),{rows}=await db.query<{key:string;value:string}>('SELECT key,value FROM records WHERE key=ANY($1::text[])',[keys]); +const checks=new Map(rows.map(row=>[row.key,JSON.parse(row.value) as Check])); +for(const ref of course.refs)if((run.force||!checkCurrent(checks.get('link:'+ref.url)))&&!run.links.includes(ref.url))run.links.push(ref.url); }catch(e){course.issue=(e as Error).message;} pending.push({key:'course:'+task.url,value:course});run.processed++;} if(!run.courses.length){run.phase='links';run.totalLinks=run.links.length;} diff --git a/next.config.ts b/next.config.ts index d4c02be..74c1395 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,7 +3,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: 'standalone', basePath: '/courses', - serverExternalPackages: ['linkedom','pg'], + serverExternalPackages: ['linkedom','pg','playwright','playwright-core'], }; export default nextConfig; diff --git a/package.json b/package.json index 6dcd378..3a4730a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sverka", - "version": "1.2.2", + "version": "1.3.0", "private": true, "engines": { "node": ">=24.0.0" @@ -9,23 +9,25 @@ "dev": "node server/runtime.mjs --dev", "build": "next build --webpack", "start": "node server/runtime.mjs", - "test": "node --experimental-strip-types --test tests/*.test.ts", + "test": "node --experimental-strip-types --experimental-test-module-mocks --test tests/*.test.ts", + "test:browser": "node --experimental-strip-types --experimental-test-module-mocks --test tests/browser.security.ts", "typecheck": "tsc --noEmit" }, "dependencies": { - "next": "16.3.4", - "react": "19.2.6", - "react-dom": "19.2.6", "linkedom": "0.18.14", "lucide-react": "1.63.0", - "pg": "8.16.3" + "next": "16.3.4", + "pg": "8.16.3", + "playwright": "1.64.1", + "react": "19.2.6", + "react-dom": "19.2.6" }, "devDependencies": { "@types/node": "22.19.19", + "@types/pg": "8.15.5", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", - "typescript": "5.9.3", - "@types/pg": "8.15.5" + "typescript": "5.9.3" }, "type": "module", "packageManager": "pnpm@11.25.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 996d89f..ce6ada5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: pg: specifier: 8.16.3 version: 8.16.3 + playwright: + specifier: 1.64.1 + version: 1.64.1 react: specifier: 19.2.6 version: 19.2.6 @@ -581,6 +584,16 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + playwright-core@1.64.1: + resolution: {integrity: sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.64.1: + resolution: {integrity: sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==} + engines: {node: '>=20'} + hasBin: true + postcss@8.5.23: resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} @@ -1191,6 +1204,12 @@ snapshots: picocolors@1.1.1: {} + playwright-core@1.64.1: {} + + playwright@1.64.1: + dependencies: + playwright-core: 1.64.1 + postcss@8.5.23: dependencies: nanoid: 3.3.20 diff --git a/tests/access.test.ts b/tests/access.test.ts new file mode 100644 index 0000000..62111db --- /dev/null +++ b/tests/access.test.ts @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import {test} from 'node:test'; +import {accessMessage,classifyResponse,purchaseOffer,checkCurrent,result,evidenceUrl,RULES_VERSION} from '../lib/access.ts'; +const lan='https://e.lanbook.com/book/111431',urait='https://urait.ru/course-viewer/6BDE5BCD-49B3-4DCD-B6BF-F91BF5F4657E'; +const html=(body:string)=>'Book
'+body+'
'; +const classify=(body:string,url=lan)=>classifyResponse(url,{url,body:html(body),code:200,type:'text/html'}); +const offer='
Доступ онлайн
от 156 ₽
В корзину
'; + +test('Lanbook denial is unavailable with an explicit login reason and evidence',()=>{ + const check=classify('

Чтение книги недоступно

Для доступа к чтению книги необходимо авторизоваться.

'); + assert.equal(check.status,'auth');assert.equal(check.reason,'Для чтения книги требуется авторизация.');assert.match(check.evidence!.text,/необходимо авторизоваться/);assert.equal(check.profile,'anonymous'); +}); +test('Urait requires both login and subscription; PDF prohibition alone is insufficient',()=>{ + const check=accessMessage(urait,'Чтобы получить полный доступ к курсам, авторизуйтесь на аккаунте с подпиской учебного заведения или личной подпиской.')!; + assert.equal(check.status,'restricted');assert.equal(check.reason,'Для чтения книги требуются авторизация и подписка.'); + assert.equal(accessMessage(urait,'Оплаченный доступ к контенту предоставляется только на платформе. Скачивание контента в PDF не доступно.'),null); +}); +test('Znanium withdrawn and priced online access are different evidence',()=>{ + assert.equal(classify('Данная публикация изъята из фонда.','https://znanium.ru/catalog/document?pid=1040849').reason,'Публикация изъята из фонда.'); + const paid=purchaseOffer('https://znanium.ru/catalog/document?id=372956',offer)!;assert.equal(paid.status,'restricted');assert.match(paid.reason,/покупка онлайн-доступа/);assert.match(paid.evidence!.url,/id=372956/); + assert.equal(purchaseOffer('https://znanium.ru/catalog/document?id=372956',offer.replace('Доступ онлайн','Печатная книга')),null); + assert.equal(purchaseOffer('https://znanium.ru/catalog/document?id=999',offer),null); + assert.equal(purchaseOffer('https://znanium.ru/catalog/document?id=372956',offer.replace('156','0')),null); +}); +test('iBooks uses the reader denial rather than a login button on the catalogue card',()=>{ + const check=accessMessage('https://ibooks.ru/bookshelf/356956/reading','Для продолжения работы с сайтом, пожалуйста, войдите в систему')!; + assert.equal(check.status,'auth');assert.equal(classify('

Linux API

Аннотация','https://ibooks.ru/products/356956').status,'unknown'); + const url='https://ibooks.ru/bookshelf/356956/reading';assert.equal(classifyResponse(url,{url,body:html('Для продолжения работы с сайтом, пожалуйста, войдите в систему'),code:403,type:'text/html'}).status,'auth'); +}); +test('Springer requires an eBook purchase offer, not the paper edition price',()=>{ + const url='https://link.springer.com/book/10.1007/978-3-642-17545-9',block='

Access this book

eBookEUR 117.69
'; + assert.equal(purchaseOffer(url,block)?.status,'restricted'); + assert.equal(purchaseOffer(url,block.replaceAll('eBook','Hardcover Book')),null); + assert.equal(purchaseOffer(url,block.replace('Access this book','This is an open access book')),null); +}); +test('hidden templates, general login, annotations and successful HTTP do not confirm access',()=>{ + for(const body of ['

Book

Аннотация','
Для чтения авторизуйтесь

Book card

','',''])assert.equal(classify(body).status,'unknown'); +}); +test('old rule versions and expired checks are invalidated independently of course content',()=>{ + const now=Date.now(),fresh=result(lan,'auth','Login');assert.equal(checkCurrent(fresh,now),true); + assert.equal(checkCurrent({...fresh,rulesVersion:undefined},now),false);assert.equal(checkCurrent({...fresh,rulesVersion:'old'},now),false); + assert.equal(checkCurrent({...fresh,checkedAt:new Date(now-86400001).toISOString()},now),false); + assert.equal(checkCurrent({...fresh,status:'unknown',checkedAt:new Date(now-600001).toISOString()},now),false);assert.equal(fresh.rulesVersion,RULES_VERSION); +}); +test('evidence URLs retain book identifiers while discarding tokens and tracking',()=>assert.equal(evidenceUrl('https://znanium.ru/catalog/document?id=372956&token=secret&utm_source=x#foo'),'https://znanium.ru/catalog/document?id=372956')); +test('BOOK.ru guest preview is unavailable even when twenty pages can be read',()=>{ + const check=accessMessage('https://book.ru/book/940586','Перейти в модуль чтения в гостевом режиме, где доступны только 20 страниц книги.')!; + assert.equal(check.status,'restricted');assert.match(check.reason,/до 20 страниц/);assert.match(check.reason,/полный текст недоступен/); +}); diff --git a/tests/browser-policy.test.ts b/tests/browser-policy.test.ts new file mode 100644 index 0000000..19cc1e7 --- /dev/null +++ b/tests/browser-policy.test.ts @@ -0,0 +1,10 @@ +import assert from 'node:assert/strict'; +import {test} from 'node:test'; +import {browserRequestAllowed} from '../lib/browser.ts'; +test('browser policy blocks other providers, internal targets, writes and heavy resources',()=>{ + for(const url of ['http://127.0.0.1/','https://127.0.0.1/','https://[::1]/','http://e.lanbook.com/','https://e.lanbook.com.evil.org/','https://urait.ru/','file:///etc/passwd','https://user:pass@e.lanbook.com/'])assert.equal(browserRequestAllowed(url,'lanbook.com','GET','document'),false,url); + assert.equal(browserRequestAllowed('https://e.lanbook.com/book/111431','lanbook.com','POST','fetch'),false); + assert.equal(browserRequestAllowed('https://e.lanbook.com/cover.jpg','lanbook.com','GET','image'),false); + assert.equal(browserRequestAllowed('https://e.lanbook.com/main.js','lanbook.com','GET','script'),true); + assert.equal(browserRequestAllowed('https://link.springernature.com/book/a','springer.com','GET','document'),true); +}); diff --git a/tests/browser.security.ts b/tests/browser.security.ts new file mode 100644 index 0000000..c6365d0 --- /dev/null +++ b/tests/browser.security.ts @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict'; +import {test,mock} from 'node:test'; +import {createServer} from 'node:http'; +import {createSocket} from 'node:dgram'; +import {useTestDatabase} from './test-database.ts'; +useTestDatabase('browser'); +const original=await import('../lib/http.ts'); +let fixture='',proxied:string[]=[]; +mock.module(new URL('../lib/http.ts',import.meta.url).href,{namedExports:{...original,requestPublic:async(url:string)=>{ + proxied.push(url); + if(url.endsWith('/redirect'))return {code:302,headers:{location:target},body:Buffer.alloc(0),type:'text/html'}; + if(!url.startsWith('https://e.lanbook.com/'))throw new Error('Unexpected transport destination'); + return {code:200,headers:{'content-type':'text/html; charset=utf-8'},body:Buffer.from(fixture),type:'text/html'}; +}}}); +const {checkBrowser}=await import('../lib/browser.ts'); +let target=''; +test('sandboxed Chromium blocks private HTTP, redirects, frames, popups, workers, WebSockets and WebRTC UDP',async()=>{ + let httpHits=0,udpHits=0; + const server=createServer((_req,res)=>{httpHits++;res.end('private');}),udp=createSocket('udp4');udp.on('message',()=>udpHits++); + await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));await new Promise(resolve=>udp.bind(0,'127.0.0.1',resolve)); + const address=server.address();assert.ok(address&&typeof address!=='string');target='http://127.0.0.1:'+address.port+'/private'; + const udpAddress=udp.address(); + fixture=`

Book

Loading
`; + try{ + const check=await checkBrowser('https://e.lanbook.com/book/111431'); + assert.equal(check.status,'auth',check.reason);assert.equal(httpHits,0);assert.equal(udpHits,0); + assert.ok(proxied.every(url=>url.startsWith('https://e.lanbook.com/')));assert.ok(proxied.includes('https://e.lanbook.com/redirect')); + }finally{await new Promise(resolve=>server.close(()=>resolve()));udp.close();} +}); diff --git a/tests/checker.test.ts b/tests/checker.test.ts index 33f5814..959ab02 100644 --- a/tests/checker.test.ts +++ b/tests/checker.test.ts @@ -8,12 +8,12 @@ const html='Book

Algorithms

{const p=parseCourse(html,url);assert.equal(p.name,'Algorithms');assert.deepEqual(p.refs.map(r=>r.url),['https://books.org/a','https://books.org/b']);assert.equal(p.literature,true);}); test('missing bibliography is explicit',()=>assert.equal(parseCourse('

Course

',url).literature,false)); test('pagination follows current program and excludes archived years',()=>{const root='https://www.hse.ru/ba/is/courses',p=parseCatalog('A2archiveother',root,root);assert.equal(p.courses.length,1);assert.deepEqual(p.pages,[root+'/page2.html?year=2026']);}); -test('classifies errors, login, protection and subscription separately',()=>{assert.equal(classify(url,result('',404)).status,'broken');assert.equal(classify(url,result('',403)).status,'blocked');assert.equal(classify(url,result('',401)).status,'auth');assert.equal(classify(url,result('',200,'https://public.org/login')).status,'auth');assert.equal(classify(url,result('Just a momentChecking your browser')).status,'blocked');assert.equal(classify(url,result('Sign in to read this book')).status,'restricted');assert.equal(classify(url,result('')).status,'auth');assert.equal(classify(url,result('Open book')).status,'ok');}); +test('classifies errors, login, protection and subscription separately',()=>{assert.equal(classify(url,result('',404)).status,'broken');assert.equal(classify(url,result('',403)).status,'blocked');assert.equal(classify(url,result('',401)).status,'auth');assert.equal(classify(url,result('',200,'https://public.org/login')).status,'auth');assert.equal(classify(url,result('Just a momentChecking your browser')).status,'blocked');assert.equal(classify(url,result('Sign in to read this book')).status,'auth');assert.equal(classify(url,result('')).status,'unknown');assert.equal(classify(url,result('Open book')).status,'unknown');}); test('rejects dangerous and non-http URLs',()=>{for(const v of ['http://127.0.0.1','http://[::1]','http://localhost/a','https://foo.internal','file:///a','https://a:b@public.org','https://public.org:8080'])assert.equal(safeUrl(v),null);assert.equal(safeUrl('https://public.org/book#x'),'https://public.org/book');}); -test('hash detects content changes and remains stable',async()=>{assert.equal(await fingerprint(html),await fingerprint(html));assert.notEqual(await fingerprint(html),await fingerprint(html+'x'));}); +test('hash detects content changes and remains stable',async()=>{assert.equal(await fingerprint(html),await fingerprint(html));assert.notEqual(await fingerprint(html),await fingerprint(html+'x'));}); import {publicIp,trimUrl} from '../lib/checker.ts'; test('preserves balanced parentheses in URLs',()=>{assert.equal(trimUrl('https://public.org/Foo_(bar).'),'https://public.org/Foo_(bar)');assert.equal(trimUrl('https://public.org/book).'),'https://public.org/book');}); -test('rejects private, reserved and loopback DNS answers',()=>{for(const ip of ['127.0.0.1','10.1.2.3','169.254.169.254','100.64.0.1','172.16.2.2','192.168.1.1','192.0.2.1','198.18.1.1','203.0.113.1','::1','fc00::1','fe80::1','2001:db8::1'])assert.equal(publicIp(ip),false,ip);assert.equal(publicIp('1.1.1.1'),true);assert.equal(publicIp('2606:4700:4700::1111'),true);}); -test('does not mark a JavaScript loading shell as accessible',()=>{assert.equal(classify(url,result('
Загрузка...
')).status,'unknown');}); +test('rejects private, reserved and loopback DNS answers',()=>{for(const ip of ['127.0.0.1','10.1.2.3','169.254.169.254','100.64.0.1','172.16.2.2','192.168.1.1','192.0.2.1','198.18.1.1','203.0.113.1','::1','fc00::1','fe80::1','2001:db8::1'])assert.equal(publicIp(ip),false,ip);assert.equal(publicIp('1.1.1.1'),true);assert.equal(publicIp('2606:4700:4700::1111'),true);}); +test('does not mark a JavaScript loading shell as accessible',()=>{assert.equal(classify(url,result('
Загрузка...
')).status,'unknown');}); test('empty HTML and No Content responses do not prove material access',()=>{for(const code of [200,204,205])assert.equal(classify(url,result('',code)).status,'unknown');}); -test('non-successful binary responses do not count as available material',()=>{for(const code of [302,304])assert.equal(classify(url,{...result('',code),type:'application/pdf'}).status,'unknown');assert.equal(classify(url,{...result(),type:'application/pdf'}).status,'ok');}); +test('non-successful binary responses do not count as available material',()=>{for(const code of [302,304])assert.equal(classify(url,{...result('',code),type:'application/pdf'}).status,'unknown');assert.equal(classify(url,{...result(),type:'application/pdf'}).status,'unknown');assert.equal(classify(url,{...result('%PDF-1.4 book file\n%%EOF'),type:'application/pdf'}).status,'unknown');}); diff --git a/tests/network.test.ts b/tests/network.test.ts new file mode 100644 index 0000000..904822c --- /dev/null +++ b/tests/network.test.ts @@ -0,0 +1,21 @@ +import assert from 'node:assert/strict'; +import {test,mock} from 'node:test'; +import {EventEmitter} from 'node:events'; +import {Readable} from 'node:stream'; +let connections=0,pinned='',code=200; +mock.module('node:dns/promises',{namedExports:{lookup:async(host:string)=>[{address:host.startsWith('private')?'127.0.0.1':'1.1.1.1',family:4}]}}); +mock.module('node:https',{namedExports:{request:(_url:URL,options:{lookup:(h:string,o:object,cb:(e:unknown,ip:string)=>void)=>void},callback:(r:unknown)=>void)=>{ + connections++;options.lookup('unused',{},(_e,ip)=>{pinned=ip;}); + const request=new EventEmitter() as EventEmitter&{end:()=>void};request.end=()=>queueMicrotask(()=>{const response=Readable.from(['Book']) as Readable&{statusCode:number;headers:object};response.statusCode=code;response.headers={'content-type':'text/html',location:code===302?'http://127.0.0.1/private':undefined};callback(response);});return request; +}}}); +const {requestPublic}=await import('../lib/http.ts'); +test('outbound transport rejects unsafe URLs and private DNS before creating a socket',async()=>{ + for(const url of ['http://127.0.0.1/','http://[::1]/','file:///etc/passwd','https://private.provider.org/'])await assert.rejects(requestPublic(url)); + assert.equal(connections,0); +}); +test('a public DNS address is pinned into the socket lookup',async()=>{ + const response=await requestPublic('https://public.provider.org/book');assert.equal(pinned,'1.1.1.1');assert.equal(response.code,200);assert.equal(connections,1); +}); +test('a redirect is returned to the caller without following it past the trust boundary',async()=>{ + code=302;const before=connections,response=await requestPublic('https://public.provider.org/redirect');assert.equal(response.code,302);assert.equal(response.headers.location,'http://127.0.0.1/private');assert.equal(connections,before+1); +}); diff --git a/tests/scanner.test.ts b/tests/scanner.test.ts index 52bf9f5..3a2c505 100644 --- a/tests/scanner.test.ts +++ b/tests/scanner.test.ts @@ -3,6 +3,7 @@ import {test} from 'node:test'; import {database} from '../lib/store.ts'; import {dashboard,getRun,start,step} from '../lib/scanner.ts'; import type {Run} from '../lib/model.ts'; +import {result} from '../lib/access.ts'; import {useTestDatabase} from './test-database.ts'; useTestDatabase('scanner'); test('only one shared manual run can be started',async()=>{ @@ -22,10 +23,18 @@ test('an active lease prevents two workers from processing the same queue',async const run=(await start(false))!;run.phase='links';run.links=['http://127.0.0.1/']; await (await database()).query('UPDATE scan SET data=$1,lease=$2,token=$3 WHERE id=1',[JSON.stringify(run),Date.now()+60000,'another-worker']); const unchanged=(await step()) as Run;assert.deepEqual(unchanged.links,run.links);assert.equal(unchanged.checked,0); -}); +}); test('a step consumes only one bibliography link',async()=>{ const db=await database(),run=(await getRun())!;run.phase='links';run.links=['http://127.0.0.1/a','http://127.0.0.1/b'];run.checked=0; await db.query("UPDATE scan SET data=$1,lease=0,token='' WHERE id=1",[JSON.stringify(run)]); const after=(await step())!;assert.equal(after.checked,1);assert.deepEqual(after.links,['http://127.0.0.1/b']);assert.equal(after.status,'running'); }); + +test('previous HTTP-only results are not presented as available after a rules upgrade',async()=>{ + const db=await database(),old={url:'https://books.org/old',status:'ok',reason:'HTTP 200',checkedAt:new Date().toISOString()}; + await db.query('INSERT INTO records(key,value) VALUES($1,$2)', ['link:'+old.url,JSON.stringify(old)]); + assert.equal((await dashboard()).checks[old.url].status,'unknown'); + const current=result(old.url,'auth','Для чтения книги требуется авторизация.');await db.query('UPDATE records SET value=$1 WHERE key=$2',[JSON.stringify(current),'link:'+old.url]); + assert.equal((await dashboard()).checks[old.url].status,'auth'); +}); -- 2.54.0 From 71bf5fd9266d44d982dfa743b12d4e7f9df81db0 Mon Sep 17 00:00:00 2001 From: MIEM DEV Date: Fri, 30 Oct 2026 18:26:10 +0300 Subject: [PATCH 09/13] fix: preserve anonymous sessions and ignore hidden access templates --- lib/access.ts | 8 ++++++-- lib/browser.ts | 8 ++++++-- tests/access.test.ts | 2 +- tests/browser.security.ts | 10 ++++++---- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/access.ts b/lib/access.ts index 3588091..ef706cd 100644 --- a/lib/access.ts +++ b/lib/access.ts @@ -11,6 +11,10 @@ export function result(url:string,status:Check['status'],reason:string,extra:Par return {url,status,reason,checkedAt:new Date().toISOString(),rulesVersion:RULES_VERSION,profile:'anonymous',...extra}; } function evidence(url:string,text:string,rule:string){const normalized=text.replace(/\s+/g,' ').trim(),at=normalized.search(/публикация\s+изъята|для\s+(?:получения\s+)?(?:доступа|чтения|просмотра)|чтобы\s+получить\s+полный\s+доступ|sign in to|чтение\s+книги\s+недоступно|книга\s+недоступна/i);return {text:normalized.slice(Math.max(0,at),Math.max(0,at)+500),url:evidenceUrl(url),rule};} +function removeHidden(document:ReturnType['document']){ + document.querySelectorAll('script,style,[hidden],[aria-hidden="true"],.hidden').forEach(n=>n.remove()); + document.querySelectorAll('[style]').forEach(n=>{if(/(?:^|;)\s*(?:display\s*:\s*none|visibility\s*:\s*(?:hidden|collapse))\s*(?:!important)?\s*(?:;|$)/i.test(n.getAttribute('style')??''))n.remove();}); +} export function accessMessage(url:string,text:string):Check|null{ const normalized=text.replace(/\s+/g,' ').trim(); const preview=/доступн[а-я]*\s+(?:только\s+|первые\s+)?(\d{1,4})\s+страниц/i.exec(normalized); @@ -26,7 +30,7 @@ export function accessMessage(url:string,text:string):Check|null{ } export function purchaseOffer(url:string,html:string):Check|null{ const {document}=parseHTML(html),host=new URL(url).hostname; - document.querySelectorAll('script,style,[hidden],[aria-hidden="true"],.hidden,[style*="display: none"],[style*="display:none"]').forEach(n=>n.remove()); + removeHidden(document); if(host==='znanium.ru'||host.endsWith('.znanium.ru')){ const id=new URL(url).searchParams.get('id'); for(const item of document.querySelectorAll('.book-offers__item')){ @@ -47,7 +51,7 @@ export function purchaseOffer(url:string,html:string):Check|null{ export function classifyResponse(url:string,r:{code:number;url:string;body:string;type:string}):Check{ const base={finalUrl:evidenceUrl(r.url),code:r.code}; const {document}=parseHTML(/html|text\//i.test(r.type)?r.body:''); - document.querySelectorAll('script,style,nav,footer,header,[hidden],[aria-hidden="true"],.hidden').forEach(n=>n.remove()); + removeHidden(document);document.querySelectorAll('nav,footer,header').forEach(n=>n.remove()); const title=(document.querySelector('title')?.textContent??'').trim(); const text=(document.querySelector('main,[role="main"]')??document.querySelector('body')??document.documentElement)?.textContent??''; const message=accessMessage(r.url,text);if(message?.status==='broken')return {...message,url,...base}; diff --git a/lib/browser.ts b/lib/browser.ts index 787c7a2..f8652cc 100644 --- a/lib/browser.ts +++ b/lib/browser.ts @@ -29,7 +29,7 @@ export async function checkBrowser(url:string,parentSignal?:AbortSignal):Promise if(controller.signal.aborted||limit||!browserRequestAllowed(target,root,request.method(),request.resourceType())){await route.abort();return;} if(++requests>100||totalBytes>32000000||active.size>=10)throw new Error('Превышен лимит ресурсов страницы'); if(navigation){if(++navigations>8)throw new Error('Слишком много переходов');await waitForHost(new URL(target).hostname,controller.signal);} - const headers:Record={};for(const key of ['accept','accept-language','origin','referer','cookie','content-type']){const value=request.headers()[key];if(value)headers[key]=value;} + const allHeaders=await request.allHeaders(),headers:Record={};for(const key of ['accept','accept-language','origin','referer','cookie','content-type']){const value=allHeaders[key];if(value)headers[key]=value;} const response=await requestPublic(target,{signal:controller.signal,maxBytes:8000000,method:request.method() as 'GET'|'HEAD'|'OPTIONS',headers}); totalBytes+=response.body.length;if(totalBytes>32000000)throw new Error('Превышен лимит ресурсов страницы'); if([429,503].includes(response.code)){ @@ -54,7 +54,11 @@ export async function checkBrowser(url:string,parentSignal?:AbortSignal):Promise const capture=async()=>{ await page.locator('body').waitFor({state:'attached'}); // Only rendered text is evidence; hidden login templates and site chrome cannot decide book access. - const text=await page.locator('body').innerText(); + const text=await page.locator('body').evaluate(body=>{ + const walker=document.createTreeWalker(body,NodeFilter.SHOW_TEXT),parts:string[]=[];let node:Node|null,size=0; + while((node=walker.nextNode())&&size<100000){const el=node.parentElement;if(!el||el.closest('header,footer,nav,script,style,[role="navigation"],[role="banner"],[role="contentinfo"]'))continue;const style=getComputedStyle(el);if(style.visibility==='hidden'||style.display==='none'||!el.getClientRects().length)continue;const text=node.textContent??'';parts.push(text);size+=text.length;} + return parts.join(' '); + }); const offers=await page.locator('.book-offers__item,.app-article-access').evaluateAll(nodes=>nodes.filter(node=>{const style=getComputedStyle(node);return style.display!=='none'&&style.visibility!=='hidden'&&node.getClientRects().length>0;}).map(node=>node.outerHTML).join('')); return {text,offers,url:page.url(),title:await page.title()}; }; diff --git a/tests/access.test.ts b/tests/access.test.ts index 62111db..4beb9ee 100644 --- a/tests/access.test.ts +++ b/tests/access.test.ts @@ -34,7 +34,7 @@ test('Springer requires an eBook purchase offer, not the paper edition price',() assert.equal(purchaseOffer(url,block.replace('Access this book','This is an open access book')),null); }); test('hidden templates, general login, annotations and successful HTTP do not confirm access',()=>{ - for(const body of ['

Book

Аннотация','
Для чтения авторизуйтесь

Book card

','',''])assert.equal(classify(body).status,'unknown'); + for(const body of ['

Book

Аннотация','
Для чтения авторизуйтесь

Book card

','','','
Для чтения необходимо авторизоваться
','
Для чтения необходимо авторизоваться
'])assert.equal(classify(body).status,'unknown'); }); test('old rule versions and expired checks are invalidated independently of course content',()=>{ const now=Date.now(),fresh=result(lan,'auth','Login');assert.equal(checkCurrent(fresh,now),true); diff --git a/tests/browser.security.ts b/tests/browser.security.ts index c6365d0..1ae3460 100644 --- a/tests/browser.security.ts +++ b/tests/browser.security.ts @@ -5,12 +5,13 @@ import {createSocket} from 'node:dgram'; import {useTestDatabase} from './test-database.ts'; useTestDatabase('browser'); const original=await import('../lib/http.ts'); -let fixture='',proxied:string[]=[]; -mock.module(new URL('../lib/http.ts',import.meta.url).href,{namedExports:{...original,requestPublic:async(url:string)=>{ +let fixture='',proxied:string[]=[],cookieSeen=false; +mock.module(new URL('../lib/http.ts',import.meta.url).href,{namedExports:{...original,requestPublic:async(url:string,options:{headers?:Record})=>{ proxied.push(url); + if(url.endsWith('/session')){cookieSeen=options.headers?.cookie?.includes('anonymous_session=fixture')??false;return {code:200,headers:{'content-type':'text/plain'},body:Buffer.from('ok'),type:'text/plain'};} if(url.endsWith('/redirect'))return {code:302,headers:{location:target},body:Buffer.alloc(0),type:'text/html'}; if(!url.startsWith('https://e.lanbook.com/'))throw new Error('Unexpected transport destination'); - return {code:200,headers:{'content-type':'text/html; charset=utf-8'},body:Buffer.from(fixture),type:'text/html'}; + return {code:200,headers:{'content-type':'text/html; charset=utf-8','set-cookie':['anonymous_session=fixture; Path=/; Secure; SameSite=Lax']},body:Buffer.from(fixture),type:'text/html'}; }}}); const {checkBrowser}=await import('../lib/browser.ts'); let target=''; @@ -22,6 +23,7 @@ test('sandboxed Chromium blocks private HTTP, redirects, frames, popups, workers const udpAddress=udp.address(); fixture=`

Book

Loading
`; try{ const check=await checkBrowser('https://e.lanbook.com/book/111431'); - assert.equal(check.status,'auth',check.reason);assert.equal(httpHits,0);assert.equal(udpHits,0); + assert.equal(check.status,'auth',check.reason);assert.equal(httpHits,0);assert.equal(udpHits,0);assert.equal(cookieSeen,true,'The fresh anonymous cookie must survive the pinned transport'); assert.ok(proxied.every(url=>url.startsWith('https://e.lanbook.com/')));assert.ok(proxied.includes('https://e.lanbook.com/redirect')); }finally{await new Promise(resolve=>server.close(()=>resolve()));udp.close();} }); -- 2.54.0 From 6d5b920287adb53fa86b178d34b5721162bfaa2b Mon Sep 17 00:00:00 2001 From: MIEM DEV Date: Fri, 30 Oct 2026 18:30:01 +0300 Subject: [PATCH 10/13] fix: require rendered evidence for library access restrictions --- README.md | 2 +- lib/browser.ts | 6 +++++- lib/checker.ts | 7 +++++-- tests/browser.security.ts | 4 ++++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 87948d9..6a94bd3 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ pnpm dev Правила учитывают сообщения читалок Лани, Юрайта и iBooks, изъятие публикации и предложение покупки онлайн-доступа Znanium, покупку именно eBook Springer и гостевое ограничение BOOK.ru. Цена печатной книги и общий запрет скачивания PDF не доказывают закрытое чтение. Результат содержит короткое доказательство, URL без токенов, время, версию правил и профиль `anonymous`. -Статического HTML достаточно для явных ограничений. Неопределённые страницы известных ЭБС открываются в Chromium/Playwright; проверяется видимый текст, при необходимости адрес читалки, а у BOOK.ru — подсказка к гостевому просмотру. Для остальных сайтов неопределённость сохраняется. Проверка не выполняет вход, покупку и обход CAPTCHA. +Страницы известных ЭБС открываются в Chromium/Playwright для подтверждения видимых ограничений: скрытые CSS-шаблоны не считаются доказательством; проверяется видимый текст, при необходимости адрес читалки, а у BOOK.ru — подсказка к гостевому просмотру. Для остальных сайтов неопределённость сохраняется. Проверка не выполняет вход, покупку и обход CAPTCHA. Docker включает Chromium с включённой песочницей, UID1000, read-only rootfs, no-new-privileges и cap-drop ALL. Профиль seccomp разрешает необходимые для песочницы user namespaces, остальные ограничения остаются; источник и лицензия указаны в deploy/README.md. Браузер получает минимальное окружение без DATABASE_URL. Все HTTP-запросы перехватываются, DNS проверяется и закрепляется в Node-сокете. Прямой выход блокируется нерабочим proxy, WebSocket/SW отключены, непроходящий через proxy WebRTC UDP запрещён. Методы записи, внешние домены, изображения/медиа/шрифты блокируются. Лимиты:100 запросов,32MB на задачу,8MB на ресурс,10 одновременных запросов; browser deadline30s и общий link deadline60s. Ошибка/лимит оставляет доступ неподтверждённым. diff --git a/lib/browser.ts b/lib/browser.ts index f8652cc..222a777 100644 --- a/lib/browser.ts +++ b/lib/browser.ts @@ -59,7 +59,11 @@ export async function checkBrowser(url:string,parentSignal?:AbortSignal):Promise while((node=walker.nextNode())&&size<100000){const el=node.parentElement;if(!el||el.closest('header,footer,nav,script,style,[role="navigation"],[role="banner"],[role="contentinfo"]'))continue;const style=getComputedStyle(el);if(style.visibility==='hidden'||style.display==='none'||!el.getClientRects().length)continue;const text=node.textContent??'';parts.push(text);size+=text.length;} return parts.join(' '); }); - const offers=await page.locator('.book-offers__item,.app-article-access').evaluateAll(nodes=>nodes.filter(node=>{const style=getComputedStyle(node);return style.display!=='none'&&style.visibility!=='hidden'&&node.getClientRects().length>0;}).map(node=>node.outerHTML).join('')); + const offers=await page.locator('.book-offers__item,.app-article-access').evaluateAll(nodes=>nodes.filter(node=>{const style=getComputedStyle(node);return style.display!=='none'&&style.visibility!=='hidden'&&node.getClientRects().length>0;}).map(node=>{ + const clone=node.cloneNode(true) as Element,original=[...node.querySelectorAll('*')],copied=[...clone.querySelectorAll('*')]; + original.forEach((el,i)=>{const style=getComputedStyle(el);if(style.display==='none'||style.visibility==='hidden'||!el.getClientRects().length)copied[i].remove();}); + return clone.outerHTML; + }).join('')); return {text,offers,url:page.url(),title:await page.title()}; }; const settle=async()=>{ diff --git a/lib/checker.ts b/lib/checker.ts index c0aa71b..4aa5f47 100644 --- a/lib/checker.ts +++ b/lib/checker.ts @@ -29,9 +29,12 @@ export {safeUrl,publicIp,resolvePublicHost,retrieve} from './http.ts'; export {classifyResponse as classify} from './access.ts'; export async function checkLink(url:string):Promise{ try{ - const signal=AbortSignal.timeout(60000),page=await retrieve(url,false,signal),check=classifyResponse(url,page); + const signal=AbortSignal.timeout(60000); + // Known libraries contain hidden auth/paywall templates; only rendered evidence can decide their access. + if(provider(url))return await (await import('./browser.ts')).checkBrowser(url,signal); + const page=await retrieve(url,false,signal),check=classifyResponse(url,page); if(page.code===403&&check.status==='blocked')await limitHost(new URL(page.url).hostname,403); - if(check.status==='unknown'&&provider(url))return await (await import('./browser.ts')).checkBrowser(url,signal); + if(check.evidence&&['auth','restricted'].includes(check.status))return {...check,status:'unknown',reason:'В HTML есть признаки ограничения, но доступ к полному тексту не подтверждён отрисованной страницей.',evidence:undefined}; return check; }catch(e){const error=e as Error;return result(url,error instanceof HostCooldown?'blocked':error.name==='AbortError'?'timeout':'network',error instanceof HostCooldown?error.message:'Не удалось загрузить материал. Доступ к полному тексту не подтверждён.');} } diff --git a/tests/browser.security.ts b/tests/browser.security.ts index 1ae3460..5de0dbc 100644 --- a/tests/browser.security.ts +++ b/tests/browser.security.ts @@ -38,3 +38,7 @@ test('sandboxed Chromium blocks private HTTP, redirects, frames, popups, workers assert.ok(proxied.every(url=>url.startsWith('https://e.lanbook.com/')));assert.ok(proxied.includes('https://e.lanbook.com/redirect')); }finally{await new Promise(resolve=>server.close(()=>resolve()));udp.close();} }); +test('a CSS-hidden login template does not make a known library book unavailable',async()=>{ + fixture='

Book

Only the catalogue card has loaded.

'; + const check=await checkBrowser('https://e.lanbook.com/book/2');assert.equal(check.status,'unknown',check.reason); +}); -- 2.54.0 From b14a5413441ed2bc2dc5dd3280b6796c9035d040 Mon Sep 17 00:00:00 2001 From: MIEM DEV Date: Fri, 30 Oct 2026 19:26:19 +0300 Subject: [PATCH 11/13] fix: verify rendered library restrictions in sandboxed Chromium --- app/globals.css | 2 ++ deploy/README.md | 4 ++-- deploy/chromium-seccomp.json | 5 ++-- lib/access.ts | 2 +- lib/browser.ts | 46 +++++++++++++++++++++++++++++------- tests/browser.security.ts | 30 +++++++++++++++++++++-- 6 files changed, 73 insertions(+), 16 deletions(-) diff --git a/app/globals.css b/app/globals.css index 92dae59..41784fa 100644 --- a/app/globals.css +++ b/app/globals.css @@ -8,3 +8,5 @@ .schedule{margin-top:16px;border-bottom:1px solid var(--line)}.schedule summary{display:flex;align-items:center;gap:12px;cursor:pointer;list-style:none;padding:17px 2px;color:var(--forest)}.schedule summary::-webkit-details-marker{display:none}.schedule__title{font-size:13px;font-weight:500}.schedule__title small{display:block;font-size:10px;font-weight:400;color:var(--muted);margin-top:3px}.schedule__state{margin-left:auto;font-size:10px;color:var(--muted);white-space:nowrap}.schedule__state--on{color:#42602f}.schedule__chevron{transition:transform .15s}.schedule details[open] .schedule__chevron{transform:rotate(180deg)}.schedule__form{padding:8px 0 24px}.schedule__toggle{display:flex;align-items:center;gap:9px;font-size:12px;font-weight:500;cursor:pointer}.schedule input[type=checkbox]{accent-color:var(--forest);width:15px;height:15px;margin:0}.schedule__fields{display:flex;flex-wrap:wrap;gap:16px;margin:20px 0 14px}.schedule__fields label{display:flex;flex-direction:column;gap:7px;font-size:10px;color:var(--muted);min-width:140px;flex:1}.schedule__fields select,.schedule__fields input{border:1px solid #c9d2c3;border-radius:4px;padding:11px 10px;background:var(--white);color:var(--ink);font-size:12px;min-width:0;width:100%;height:42px}.schedule__hint{font-size:11px;line-height:1.8;color:var(--muted);max-width:80ch;margin:10px 0}.schedule__history{font-size:11px;margin:12px 0;color:#52654b}.schedule__error{font-size:11px;line-height:1.7;color:var(--red);margin:12px 0;overflow-wrap:anywhere}.schedule__actions{display:flex;flex-wrap:wrap;gap:16px;align-items:center;margin-top:19px}.schedule__actions>span{display:flex;align-items:center;gap:7px;color:#52654b;font-size:11px}@media(max-width:760px){.schedule summary{gap:9px}.schedule__title{font-size:12px}.schedule__title small{font-size:10px;max-width:230px}.schedule__state{font-size:9px}.schedule__fields{gap:12px}.schedule__fields label{flex-basis:calc(50% - 12px);min-width:120px}.schedule__actions{gap:12px}.schedule__actions .button{width:100%}} .link-row__reason{font-size:10px;line-height:1.6;margin:6px 0 0;color:var(--ink);overflow-wrap:anywhere}.reason__evidence{margin-top:12px}.reason__evidence blockquote{margin:0;padding:10px 12px;border:1px solid var(--line);border-radius:4px;background:var(--paper);font-size:11px;line-height:1.7} + +@media(max-width:760px){.link-row__result{grid-column:1/-1;flex-wrap:wrap}.link-row__reason{flex-basis:100%;order:3;margin-top:0}.link-row__date{grid-column:1/-1;padding-left:19px}} diff --git a/deploy/README.md b/deploy/README.md index 644b95c..41582df 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,8 +1,8 @@ # Chromium sandbox -chromium-seccomp.json — неизменённый профиль Microsoft Playwright v1.64.1: +chromium-seccomp.json — адаптация профиля Microsoft Playwright v1.64.1: https://github.com/microsoft/playwright/blob/v1.64.1/utils/docker/seccomp_profile.json -Лицензия Apache-2.0 сохранена в PLAYWRIGHT-LICENSE. Профиль разрешает clone/setns/unshare для Chromium user namespaces и используется с chromiumSandbox:true, UID1000, no-new-privileges и cap-drop ALL. Не заменять seccomp на unconfined или Chromium sandbox на --no-sandbox. Read-only rootfs и временные /tmp,/dev/shm сохраняются. +Лицензия Apache-2.0 сохранена в PLAYWRIGHT-LICENSE. Профиль разрешает clone/setns/unshare/chroot для Chromium user namespaces и используется с chromiumSandbox:true, UID1000, no-new-privileges и cap-drop ALL. Не заменять seccomp на unconfined или Chromium sandbox на --no-sandbox. Дополнительно разрешён syscall chroot: он нужен Chromium внутри собственного user namespace; host/container capability SYS_CHROOT не добавляется, kernel permission checks сохраняются. Read-only rootfs и временные /tmp,/dev/shm сохраняются. Nginx-сниппет повышает read timeout только для /courses до85s, чтобы browser deadline60s и сохранение данных завершались до90s lease. Другие маршруты и сервисы не меняются. diff --git a/deploy/chromium-seccomp.json b/deploy/chromium-seccomp.json index fddc05f..1fa1f1a 100644 --- a/deploy/chromium-seccomp.json +++ b/deploy/chromium-seccomp.json @@ -51,11 +51,12 @@ ], "syscalls": [ { - "comment": "Allow create user namespaces", + "comment": "Allow Chromium user namespaces and chroot inside its namespace", "names": [ "clone", "setns", - "unshare" + "unshare", + "chroot" ], "action": "SCMP_ACT_ALLOW", "args": [], diff --git a/lib/access.ts b/lib/access.ts index ef706cd..97d816d 100644 --- a/lib/access.ts +++ b/lib/access.ts @@ -17,7 +17,7 @@ function removeHidden(document:ReturnType['document']){ } export function accessMessage(url:string,text:string):Check|null{ const normalized=text.replace(/\s+/g,' ').trim(); - const preview=/доступн[а-я]*\s+(?:только\s+|первые\s+)?(\d{1,4})\s+страниц/i.exec(normalized); + const preview=/гостев[а-я]*\s+режим[а-я]*[^.]{0,180}?(\d{1,4})\s*страниц/i.exec(normalized)??/доступн[а-я]*\s+(?:только\s+|первые\s+)?(\d{1,4})\s+страниц/i.exec(normalized); if(preview&&/гостев|ознакомительн|фрагмент/i.test(normalized))return result(url,'restricted',`Доступен только ознакомительный фрагмент — до ${preview[1]} страниц; полный текст недоступен.`,{evidence:evidence(url,normalized.slice(Math.max(0,preview.index-150),preview.index+200),'reader-preview-pages')}); if(/публикация\s+изъята\s+из\s+фонда/i.test(normalized))return result(url,'broken','Публикация изъята из фонда.',{evidence:evidence(url,normalized,'publication-withdrawn')}); if(/\/bookshelf\/\d+\/reading/.test(new URL(url).pathname)&&/для продолжения работы с сайтом.{0,60}войдите в систему/i.test(normalized))return result(url,'auth','Для чтения книги требуется авторизация.',{evidence:evidence(url,normalized,'ibooks-reader-login')}); diff --git a/lib/browser.ts b/lib/browser.ts index 222a777..d34846d 100644 --- a/lib/browser.ts +++ b/lib/browser.ts @@ -14,22 +14,23 @@ export async function checkBrowser(url:string,parentSignal?:AbortSignal):Promise const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),30000); const abort=()=>controller.abort();parentSignal?.addEventListener('abort',abort,{once:true});if(parentSignal?.aborted)controller.abort(); let browser:Awaited>|undefined; - let requests=0,totalBytes=0,failed=false,navigations=0,mainCode=200,limit:Check|null=null; + let requests=0,totalBytes=0,failed=false,navigations=0,mainCode=200,redirectTarget:string|null=null,limit:Check|null=null; const active=new Set>(); + const slots:Promise[]=Array.from({length:10},()=>Promise.resolve()); try{ controller.signal.throwIfAborted();browser=await chromium.launch({headless:true,chromiumSandbox:true,timeout:10000,env:{PATH:process.env.PATH??'/usr/bin:/bin',HOME:'/tmp',TMPDIR:'/tmp',LANG:'C.UTF-8'},args:[ '--proxy-server=http://127.0.0.1:9','--proxy-bypass-list=<-loopback>','--disable-quic','--force-webrtc-ip-handling-policy=disable_non_proxied_udp', ]}); - controller.signal.throwIfAborted();const context=await browser.newContext({serviceWorkers:'block',acceptDownloads:false,viewport:{width:1280,height:900}}); + controller.signal.throwIfAborted();const context=await browser.newContext({serviceWorkers:'block',acceptDownloads:false,javaScriptEnabled:!['znanium.ru','ibooks.ru','springer.com'].includes(root),viewport:{width:1280,height:900}}); controller.signal.addEventListener('abort',()=>{void context.close().catch(()=>{});},{once:true}); await context.routeWebSocket('**/*',socket=>socket.close()); const handle=async(route:Route)=>{ const request=route.request(),target=request.url(),navigation=request.isNavigationRequest(); try{ if(controller.signal.aborted||limit||!browserRequestAllowed(target,root,request.method(),request.resourceType())){await route.abort();return;} - if(++requests>100||totalBytes>32000000||active.size>=10)throw new Error('Превышен лимит ресурсов страницы'); + if(totalBytes>32000000)throw new Error('Превышен лимит ресурсов страницы'); if(navigation){if(++navigations>8)throw new Error('Слишком много переходов');await waitForHost(new URL(target).hostname,controller.signal);} - const allHeaders=await request.allHeaders(),headers:Record={};for(const key of ['accept','accept-language','origin','referer','cookie','content-type']){const value=allHeaders[key];if(value)headers[key]=value;} + const allHeaders=await request.allHeaders(),headers:Record={};for(const key of ['accept','accept-language','origin','referer','cookie','content-type','user-agent']){const value=allHeaders[key];if(value)headers[key]=value;} const response=await requestPublic(target,{signal:controller.signal,maxBytes:8000000,method:request.method() as 'GET'|'HEAD'|'OPTIONS',headers}); totalBytes+=response.body.length;if(totalBytes>32000000)throw new Error('Превышен лимит ресурсов страницы'); if([429,503].includes(response.code)){ @@ -39,6 +40,13 @@ export async function checkBrowser(url:string,parentSignal?:AbortSignal):Promise if(navigation&&request.frame()===page.mainFrame())mainCode=response.code; const fulfilled:Record={}; for(const [key,value] of Object.entries(response.headers))if(value!==undefined&&!['content-encoding','content-length','transfer-encoding','connection'].includes(key))fulfilled[key]=Array.isArray(value)?value.join('\n'):String(value); + if([301,302,303,307,308].includes(response.code)&&response.headers.location){ + if(!navigation||request.frame()!==page.mainFrame())throw new Error('Перенаправление ресурса не подтверждено'); + const next=safeUrl(response.headers.location,target);if(!next||!browserRequestAllowed(next,root,'GET','document'))throw new Error('Перенаправление не разрешено'); + redirectTarget=next;delete fulfilled.location;delete fulfilled['content-type']; + // Chromium routing may skip native redirect hops. Start each hop explicitly through the pinned boundary. + await route.fulfill({status:200,headers:fulfilled,contentType:'text/html',body:''});return; + } await route.fulfill({status:response.code,headers:fulfilled,body:response.body}); }catch(error){ if(error instanceof HostCooldown)limit=result(url,'blocked',error.message); @@ -47,10 +55,16 @@ export async function checkBrowser(url:string,parentSignal?:AbortSignal):Promise } }; // Every request is fulfilled by the pinned Node transport. The dead proxy also prevents direct browser egress. - await context.route('**/*',route=>{const task=handle(route);active.add(task);void task.finally(()=>active.delete(task));return task;}); + await context.route('**/*',route=>{ + const request=route.request(); + if(controller.signal.aborted||limit||!browserRequestAllowed(request.url(),root,request.method(),request.resourceType()))return route.abort().catch(()=>{}); + if(++requests>100){failed=true;return route.abort().catch(()=>{});} + const index=(requests-1)%slots.length,task=slots[index].then(()=>handle(route));slots[index]=task.catch(()=>{});active.add(task);void task.finally(()=>active.delete(task));return task; + }); const page=await context.newPage(); context.on('page',other=>{if(other!==page)void other.close().catch(()=>{});}); page.setDefaultTimeout(5000);page.setDefaultNavigationTimeout(20000); + const navigate=async(raw:string)=>{let next:string=raw;for(let hop=0;hop<5;hop++){redirectTarget=null;await page.goto(next,{waitUntil:'domcontentloaded'});if(!redirectTarget)return;next=redirectTarget;}throw new Error('Слишком много перенаправлений');}; const capture=async()=>{ await page.locator('body').waitFor({state:'attached'}); // Only rendered text is evidence; hidden login templates and site chrome cannot decide book access. @@ -72,21 +86,35 @@ export async function checkBrowser(url:string,parentSignal?:AbortSignal):Promise return /чтение книги недоступно|необходимо авторизоваться|публикация изъята|чтобы получить полный доступ|книга недоступна для чтения|для продолжения работы с сайтом|библиографическое описание|not found|captcha|проверка браузера/i.test(text)||!!document.querySelector('a[href*="/course-viewer/"],a[href*="/viewer/"],a[href*="reader.new.book.ru"],.book-offers__item,.app-article-access'); },undefined,{timeout:12000}).catch(()=>{}); }; - await page.goto(url,{waitUntil:'domcontentloaded'});await settle();let snapshot=await capture(); + await navigate(url);await settle();if(['urait.ru','book.ru'].includes(root))await page.waitForLoadState('networkidle',{timeout:4000}).catch(()=>{});let snapshot=await capture(); if(root==='book.ru'){ - const preview=page.getByRole('link',{name:'Ознакомиться',exact:true}); + // CSS tooltips become part of the accessible name on hover; retain a stable DOM-text locator. + const preview=page.locator('a[href*="reader.new.book.ru"]').filter({hasText:'Ознакомиться'}).first(); if(await preview.count()){ await preview.hover({timeout:3000}).catch(()=>{}); await page.waitForFunction(()=>/гостевом\s+режиме|доступны\s+только\s+\d+\s+страниц/i.test(document.body?.innerText??''),undefined,{timeout:3000}).catch(()=>{});snapshot=await capture(); + const tooltip=await preview.evaluate(el=>[el,el.parentElement].filter((node):node is HTMLElement=>!!node).flatMap(node=>['::before','::after'].map(pseudo=>{const style=getComputedStyle(node,pseudo);return style.display!=='none'&&style.visibility!=='hidden'&&style.opacity!=='0'&&!['none','normal'].includes(style.content)?style.content.replace(/^["']|["']$/g,''):'';})).join(' ')).catch(()=>''); + snapshot.text+=' '+tooltip; + const message=accessMessage(snapshot.url,snapshot.text);if(message)return {...message,url,code:mainCode,finalUrl:evidenceUrl(snapshot.url)}; + // BOOK.ru distinguishes its locked orange preview action from a full reading action. + const lockedPreview=await preview.evaluate(el=>el.className.includes('book_mode_orange__')&&!![...el.querySelectorAll('[class*="book_marker2__"]')].find(node=>{const style=getComputedStyle(node);return style.display!=='none'&&style.visibility!=='hidden'&&node.getClientRects().length>0;})); + const fullRead=await page.locator('a[href*="reader.new.book.ru"]').filter({hasText:/^\s*Читать(?: полностью)?\s*$/i}).count(); + if(lockedPreview&&!fullRead)return result(url,'restricted','Доступен только ознакомительный режим; полный текст книги недоступен.',{code:mainCode,finalUrl:evidenceUrl(snapshot.url),evidence:{url:evidenceUrl(snapshot.url),text:'Ознакомиться',rule:'bookru-locked-preview'}}); + } } const known=accessMessage(snapshot.url,snapshot.text)??purchaseOffer(snapshot.url,snapshot.offers); if(known)return {...known,url,code:mainCode,finalUrl:evidenceUrl(snapshot.url)}; if(root==='urait.ru'||root==='ibooks.ru'){ // Follow only a book reader link already present on this exact card, never subscription/purchase controls. - const href=root==='urait.ru'?await page.locator('a[href*="/course-viewer/"],a[href*="/viewer/"]').first().getAttribute('href').catch(()=>null):snapshot.text.match(/https:\/\/ibooks\.ru\/bookshelf\/\d+\/reading/)?.[0]??null; + const modern=root==='urait.ru'?await page.locator('a[href*="/course-viewer/"]').first().getAttribute('href').catch(()=>null):null; + const href=root==='urait.ru'?modern??await page.locator('a[href*="/viewer/"]').first().getAttribute('href').catch(()=>null):snapshot.text.match(/https:\/\/ibooks\.ru\/bookshelf\/\d+\/reading/)?.[0]??null; const reader=href?safeUrl(href,snapshot.url):null; - if(reader&&provider(reader)===root){await page.goto(reader,{waitUntil:'domcontentloaded'});await settle();snapshot=await capture(); + if(reader&&provider(reader)===root){await navigate(reader);await settle();snapshot=await capture(); + if(root==='urait.ru'&&new URL(snapshot.url).pathname.startsWith('/viewer/')){ + const nextHref=await page.locator('a[href*="/course-viewer/"]').first().getAttribute('href').catch(()=>null),next=nextHref?safeUrl(nextHref,snapshot.url):null; + if(next&&provider(next)===root){await navigate(next);await settle();snapshot=await capture();} + } const message=accessMessage(snapshot.url,snapshot.text);if(message)return {...message,url,code:mainCode,finalUrl:evidenceUrl(snapshot.url)}; } } diff --git a/tests/browser.security.ts b/tests/browser.security.ts index 5de0dbc..4857e70 100644 --- a/tests/browser.security.ts +++ b/tests/browser.security.ts @@ -2,15 +2,22 @@ import assert from 'node:assert/strict'; import {test,mock} from 'node:test'; import {createServer} from 'node:http'; import {createSocket} from 'node:dgram'; +import {chromium} from 'playwright'; import {useTestDatabase} from './test-database.ts'; useTestDatabase('browser'); +const launch=chromium.launch.bind(chromium); +mock.method(chromium,'launch',async(options:Parameters[0])=>{ + try{return await launch(options);}catch(error){console.error('Chromium launch diagnostic:',(error as Error).message);throw error;} +}); const original=await import('../lib/http.ts'); let fixture='',proxied:string[]=[],cookieSeen=false; mock.module(new URL('../lib/http.ts',import.meta.url).href,{namedExports:{...original,requestPublic:async(url:string,options:{headers?:Record})=>{ proxied.push(url); + if(url.endsWith('/go-book'))return {code:302,headers:{location:'/book/111431'},body:Buffer.alloc(0),type:'text/html'}; if(url.endsWith('/session')){cookieSeen=options.headers?.cookie?.includes('anonymous_session=fixture')??false;return {code:200,headers:{'content-type':'text/plain'},body:Buffer.from('ok'),type:'text/plain'};} + if(url.includes('/probe/'))return {code:200,headers:{'content-type':'application/javascript'},body:Buffer.from('window.loadedChunks=(window.loadedChunks||0)+1;'),type:'application/javascript'}; if(url.endsWith('/redirect'))return {code:302,headers:{location:target},body:Buffer.alloc(0),type:'text/html'}; - if(!url.startsWith('https://e.lanbook.com/'))throw new Error('Unexpected transport destination'); + if(!/^https:\/\/(e\.lanbook\.com|book\.ru)\//.test(url))throw new Error('Unexpected transport destination'); return {code:200,headers:{'content-type':'text/html; charset=utf-8','set-cookie':['anonymous_session=fixture; Path=/; Secure; SameSite=Lax']},body:Buffer.from(fixture),type:'text/html'}; }}}); const {checkBrowser}=await import('../lib/browser.ts'); @@ -40,5 +47,24 @@ test('sandboxed Chromium blocks private HTTP, redirects, frames, popups, workers }); test('a CSS-hidden login template does not make a known library book unavailable',async()=>{ fixture='

Book

Only the catalogue card has loaded.

'; - const check=await checkBrowser('https://e.lanbook.com/book/2');assert.equal(check.status,'unknown',check.reason); + const check=await checkBrowser('https://e.lanbook.com/book/2');assert.equal(check.status,'unknown',check.reason);assert.equal(check.code,200,'The browser must actually render the fixture'); +}); +test('a burst of essential chunks is queued instead of dropping requests over the parallelism cap',async()=>{ + fixture='

Book

Loading
'; + const check=await checkBrowser('https://e.lanbook.com/book/3');assert.equal(check.status,'auth',check.reason); +}); +test('a public document redirect is explicitly revalidated and rendered at its final URL',async()=>{ + fixture='

Book

Для доступа к чтению книги необходимо авторизоваться.'; + const check=await checkBrowser('https://e.lanbook.com/go-book');assert.equal(check.status,'auth',check.reason);assert.equal(check.finalUrl,'https://e.lanbook.com/book/111431'); +}); +test('BOOK.ru CSS pseudo-element tooltip proves that only a guest fragment is available',async()=>{ + fixture='

Book

Ознакомиться'; + const check=await checkBrowser('https://book.ru/book/940586');assert.equal(check.status,'restricted',check.reason);assert.match(check.reason,/до 20 страниц/); +}); + +test('BOOK.ru locked orange preview is unavailable even without a loaded tooltip',async()=>{ + fixture='

Book

LockОзнакомиться'; + const check=await checkBrowser('https://book.ru/book/940586');assert.equal(check.status,'restricted',check.reason);assert.equal(check.evidence?.rule,'bookru-locked-preview');assert.doesNotMatch(check.reason,/20/); + fixture=fixture.replace('','Читать полностью'); + assert.equal((await checkBrowser('https://book.ru/book/940586')).status,'unknown','A full-reading control contradicts the locked-preview-only signal'); }); -- 2.54.0 From ef243cceff7c80b76327fc159255368dc6e276b3 Mon Sep 17 00:00:00 2001 From: MIEM DEV Date: Fri, 30 Oct 2026 19:28:28 +0300 Subject: [PATCH 12/13] fix: ignore hidden book preview controls --- lib/browser.ts | 2 +- tests/browser.security.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/browser.ts b/lib/browser.ts index d34846d..75bc6e8 100644 --- a/lib/browser.ts +++ b/lib/browser.ts @@ -89,7 +89,7 @@ export async function checkBrowser(url:string,parentSignal?:AbortSignal):Promise await navigate(url);await settle();if(['urait.ru','book.ru'].includes(root))await page.waitForLoadState('networkidle',{timeout:4000}).catch(()=>{});let snapshot=await capture(); if(root==='book.ru'){ // CSS tooltips become part of the accessible name on hover; retain a stable DOM-text locator. - const preview=page.locator('a[href*="reader.new.book.ru"]').filter({hasText:'Ознакомиться'}).first(); + const preview=page.locator('a[href*="reader.new.book.ru"]').filter({hasText:'Ознакомиться',visible:true}).first(); if(await preview.count()){ await preview.hover({timeout:3000}).catch(()=>{}); await page.waitForFunction(()=>/гостевом\s+режиме|доступны\s+только\s+\d+\s+страниц/i.test(document.body?.innerText??''),undefined,{timeout:3000}).catch(()=>{});snapshot=await capture(); diff --git a/tests/browser.security.ts b/tests/browser.security.ts index 4857e70..d75e44e 100644 --- a/tests/browser.security.ts +++ b/tests/browser.security.ts @@ -68,3 +68,8 @@ test('BOOK.ru locked orange preview is unavailable even without a loaded tooltip fixture=fixture.replace('','Читать полностью'); assert.equal((await checkBrowser('https://book.ru/book/940586')).status,'unknown','A full-reading control contradicts the locked-preview-only signal'); }); + +test('a hidden BOOK.ru preview cannot prove a restriction through its pseudo-element',async()=>{ + fixture='

Book

'; + const check=await checkBrowser('https://book.ru/book/940586');assert.equal(check.status,'unknown',check.reason);assert.equal(check.code,200); +}); -- 2.54.0 From 200a1e2810e279d4c6ac61f89802b30b20bf2cfc Mon Sep 17 00:00:00 2001 From: MIEM DEV Date: Fri, 30 Oct 2026 19:30:09 +0300 Subject: [PATCH 13/13] docs: record full-text access checks in changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cf0733..fff6d80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.3.0 — 2026-10-30 + +- Проверка литературы учитывает доступ к полному тексту без авторизации: вход, подписка, покупка онлайн-доступа и ознакомительный режим отмечаются как причины недоступности. +- Добавлены правила для Лань, Юрайт, Znanium, iBooks, Springer и BOOK.ru с браузерной проверкой видимого содержимого и ограниченными переходами в читатель. +- Причина и подтверждающая цитата доступны рядом с результатом; HTTP 200, аннотация и PDF сами по себе больше не подтверждают доступность полного текста. +- Старые результаты и истёкшие проверки перепроверяются независимо от изменения курса; версия правил и срок кэша предотвращают повторное использование прежних HTTP-оценок. +- Chromium работает в изолированном контейнере с проверкой публичных адресов, ограничениями ресурсов и сохранением двухсекундной паузы между переходами к одному хосту. +- Увеличен таймаут только маршрута /courses для браузерной проверки; причины недоступности адаптированы для мобильных экранов. + ## 1.2.2 — 2026-10-29 - Интервал между запросами к одному хосту установлен в две секунды вместо случайной паузы 3–4 секунды. -- 2.54.0