ProofKit

Документация

API и спецификация

Один адрес, один JSON в ответ. Этот ответ и есть подписанная квитанция.

POST /api/v1/check

Тело: {"text": "…"} — UTF-8, от 5 до 20 000 символов. На бесплатном тарифе ключ не нужен.

curl -X POST https://proof.sapir861.com/api/v1/check \
  -H "content-type: application/json" \
  -d '{"text":"See Roe v. Wade, 410 U.S. 113 (1973)."}'
КодЗначение
200Квитанция: {body, sig, digest, log}
400Нет текста, он слишком короткий или длиннее 20 000 символов
429rate_limited (всплеск) или monthly_limit — бесплатно 100 ссылок в месяц на сеть

Заголовок ответа x-proofkit-remaining показывает остаток бесплатных ссылок в этом месяце. Тарифы Pro и Team получают ключ API с большими лимитами: передавайте его как Authorization: Bearer pk_live_…, в ответе будет x-proofkit-plan. Неверный ключ — 401 invalid_api_key; после окончания тарифа действует бесплатный лимит.

Поля квитанции

ПолеОписание
body.specsapir-receipt/1
body.kindcitation-check
body.tsВремя проверки, ISO-8601 UTC
body.input_sha256SHA-256 исходного текста (доказывает, какой текст проверен, не храня его)
body.checkerМетод и источник
body.results[]cite, name, verdict, note, source, opinion, matched_case
verdictverified · mismatch (настоящая ссылка, другое дело) · not_found · ambiguous · exists_name_unchecked · unverifiable · unchecked
body.summarytotal, verified, flagged
body.keyПубличный ключ подписи (Ed25519, JWK x)
sigПодпись Ed25519 (base64url) канонического JSON body
digestSHA-256 (hex) канонического JSON body
logПозиция n и хеш цепочки hash в публичном журнале

Канонический JSON: ключи объектов отсортированы, без пробелов, стандартное экранирование строк, поля undefined отбрасываются.

Проверка

  1. Сериализовать body канонически.
  2. SHA-256 должен совпасть с digest.
  3. Проверить подпись sig над теми же байтами ключом из /.well-known/proofkit-pubkey; он должен совпасть с body.key.x.
import { readFileSync } from "node:fs";   // node verify.mjs receipt.json  (Node 20+)
const r = JSON.parse(readFileSync(process.argv[2] || "receipt.json", "utf8"));
const canon = o => o === null || typeof o !== "object" ? JSON.stringify(o) : Array.isArray(o) ? "[" + o.map(canon).join(",") + "]"
  : "{" + Object.keys(o).sort().filter(k => o[k] !== undefined).map(k => JSON.stringify(k) + ":" + canon(o[k])).join(",") + "}";
const msg = new TextEncoder().encode(canon(r.body));
const hex = Buffer.from(await crypto.subtle.digest("SHA-256", msg)).toString("hex");
const pub = (await (await fetch("https://proof.sapir861.com/.well-known/proofkit-pubkey")).json()).jwk;
const key = await crypto.subtle.importKey("jwk", pub, { name: "Ed25519" }, false, ["verify"]);
const ok = await crypto.subtle.verify({ name: "Ed25519" }, key, Buffer.from(r.sig, "base64url"), msg);
console.log(ok && hex === r.digest && pub.x === r.body.key.x ? "VALID" : "INVALID");

Ограничения. Прочитайте до того, как полагаться на результат

  • Охват: судебная практика США в CourtListener. Часть штатных и старых сборников неполная.
  • Ссылки Westlaw (WL), Lexis и public-domain нельзя проверить по открытой базе, поэтому они помечаются unverifiable.
  • Сравнение названия идёт по словам (≥ 0.5). Очень короткие или необычные названия могут дать mismatch для настоящего дела. Всегда открывайте ссылку на источник.
  • Мы проверяем существование и идентичность дела, а не то, подтверждает ли оно аргумент.