first init
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+142
@@ -0,0 +1,142 @@
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
const list = document.querySelector("#endpoint-list");
|
||||
const template = document.querySelector("#endpoint-template");
|
||||
const search = document.querySelector("#search");
|
||||
const filters = document.querySelector("#service-filters");
|
||||
const count = document.querySelector("#endpoint-count");
|
||||
const empty = document.querySelector("#empty");
|
||||
const sidebar = document.querySelector("#sidebar");
|
||||
const menu = document.querySelector("#menu");
|
||||
let manifest;
|
||||
let activeService = "all";
|
||||
|
||||
const escapeHtml = (value) => String(value)
|
||||
.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
||||
|
||||
function curlFor(service, endpoint) {
|
||||
const url = `${service.base_url}${endpoint.path.replaceAll(/{([^}]+)}/g, "<$1>")}`;
|
||||
const lines = [`curl -X ${endpoint.method} '${url}'`];
|
||||
if (endpoint.auth === "bearer" || endpoint.auth === "admin") {
|
||||
lines.push(" -H 'Authorization: Bearer $TOKEN'");
|
||||
} else if (endpoint.auth === "hmac") {
|
||||
lines.push(" -H 'X-Api-Key: $API_KEY'",
|
||||
" -H 'X-Api-Timestamp: $TIMESTAMP'",
|
||||
" -H 'X-Api-Nonce: $UUID'",
|
||||
" -H 'X-Api-Signature: $SIGNATURE'");
|
||||
if (endpoint.method !== "GET") lines.push(" -H 'Idempotency-Key: $UUID'");
|
||||
}
|
||||
if (endpoint.body) {
|
||||
lines.push(" -H 'Content-Type: application/json'",
|
||||
` -d '${JSON.stringify(endpoint.body)}'`);
|
||||
}
|
||||
return lines.join(" \\\n");
|
||||
}
|
||||
|
||||
function endpointId(service, endpoint) {
|
||||
return `${service.id}-${endpoint.method.toLowerCase()}-${endpoint.path
|
||||
.replace(/[{}]/g, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "")}`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
const query = search.value.trim().toLowerCase();
|
||||
list.replaceChildren();
|
||||
let shown = 0;
|
||||
manifest.services.forEach((service) => {
|
||||
service.endpoints.forEach((endpoint) => {
|
||||
const haystack = `${service.name} ${endpoint.method} ${endpoint.path} ${endpoint.title} ${endpoint.description} ${endpoint.group}`.toLowerCase();
|
||||
if ((activeService !== "all" && activeService !== service.id) || !haystack.includes(query)) return;
|
||||
shown += 1;
|
||||
const node = template.content.firstElementChild.cloneNode(true);
|
||||
node.id = endpointId(service, endpoint);
|
||||
node.dataset.service = service.id;
|
||||
const summary = node.querySelector(".endpoint-summary");
|
||||
const method = node.querySelector(".method");
|
||||
method.textContent = endpoint.method;
|
||||
method.classList.add(endpoint.method);
|
||||
node.querySelector(".path").textContent = endpoint.path;
|
||||
node.querySelector(".endpoint-title").textContent = endpoint.title;
|
||||
node.querySelector(".auth-badge").textContent = endpoint.auth_label;
|
||||
node.querySelector(".endpoint-description").textContent = endpoint.description;
|
||||
node.querySelector(".endpoint-meta").innerHTML =
|
||||
`<code>${escapeHtml(service.base_url)}</code><code>${escapeHtml(endpoint.group)}</code>`;
|
||||
const curl = curlFor(service, endpoint);
|
||||
node.querySelector(".endpoint-example").innerHTML =
|
||||
`<div class="code-card"><div class="code-head"><span>cURL</span><button class="copy">Копировать</button></div><pre><code>${escapeHtml(curl)}</code></pre></div>`;
|
||||
summary.addEventListener("click", () => {
|
||||
const open = node.classList.toggle("open");
|
||||
summary.setAttribute("aria-expanded", String(open));
|
||||
if (open) history.replaceState(null, "", `#${node.id}`);
|
||||
});
|
||||
node.querySelector(".copy").addEventListener("click", (event) => copyText(event.currentTarget, curl));
|
||||
list.append(node);
|
||||
});
|
||||
});
|
||||
count.textContent = `${shown} / ${manifest.total_endpoints}`;
|
||||
empty.hidden = shown !== 0;
|
||||
}
|
||||
|
||||
function copyText(button, text) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const old = button.textContent;
|
||||
button.textContent = "Скопировано";
|
||||
window.setTimeout(() => { button.textContent = old; }, 1300);
|
||||
});
|
||||
}
|
||||
|
||||
function makeFilters() {
|
||||
const choices = [["all", "Все"], ...manifest.services.map((service) => [service.id, service.name])];
|
||||
choices.forEach(([id, label]) => {
|
||||
const button = document.createElement("button");
|
||||
button.className = `filter${id === "all" ? " active" : ""}`;
|
||||
button.textContent = label;
|
||||
button.addEventListener("click", () => {
|
||||
activeService = id;
|
||||
filters.querySelectorAll(".filter").forEach((item) => item.classList.toggle("active", item === button));
|
||||
render();
|
||||
});
|
||||
filters.append(button);
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll("[data-copy-target]").forEach((button) => {
|
||||
button.addEventListener("click", () => copyText(button, document.querySelector(`#${button.dataset.copyTarget}`).innerText));
|
||||
});
|
||||
search.addEventListener("input", render);
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "/" && document.activeElement !== search) {
|
||||
event.preventDefault();
|
||||
search.focus();
|
||||
}
|
||||
if (event.key === "Escape") sidebar.classList.remove("open");
|
||||
});
|
||||
menu.addEventListener("click", () => {
|
||||
const open = sidebar.classList.toggle("open");
|
||||
menu.setAttribute("aria-expanded", String(open));
|
||||
});
|
||||
sidebar.querySelectorAll("a").forEach((link) => link.addEventListener("click", () => sidebar.classList.remove("open")));
|
||||
|
||||
fetch("/api-manifest.json")
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error(`manifest ${response.status}`);
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
manifest = data;
|
||||
makeFilters();
|
||||
render();
|
||||
if (location.hash.startsWith("#messenger-") || location.hash.startsWith("#gateway-")) {
|
||||
const target = document.querySelector(location.hash);
|
||||
if (target) {
|
||||
target.classList.add("open");
|
||||
target.querySelector(".endpoint-summary").setAttribute("aria-expanded", "true");
|
||||
target.scrollIntoView();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
empty.hidden = false;
|
||||
empty.textContent = "Не удалось загрузить API catalog.";
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,209 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Публичная документация API сервисов OVE">
|
||||
<title>OVE API Reference</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
<script src="/app.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="noise" aria-hidden="true"></div>
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<a class="brand" href="#top" aria-label="OVE API">
|
||||
<span class="brand-mark">O</span>
|
||||
<span><strong>OVE</strong><small>API reference</small></span>
|
||||
</a>
|
||||
<nav aria-label="Основная навигация">
|
||||
<a href="#overview">Обзор</a>
|
||||
<a href="#quickstart">Быстрый старт</a>
|
||||
<a href="#auth">Авторизация</a>
|
||||
<a href="#messenger">Messenger API</a>
|
||||
<a href="#bots">Bot API</a>
|
||||
<a href="#oauth">OAuth / OIDC</a>
|
||||
<a href="#gateway">Crypto Gateway</a>
|
||||
<a href="#donations">Donations + OBS</a>
|
||||
<a href="#webhooks">Webhooks</a>
|
||||
<a href="#errors">Ошибки</a>
|
||||
</nav>
|
||||
<div class="sidebar-foot">
|
||||
<span class="status-dot"></span> Production polygon
|
||||
<a href="/openapi/messenger.json" download>Messenger OpenAPI ↗</a>
|
||||
<a href="/openapi/gateway.json" download>Gateway OpenAPI ↗</a>
|
||||
<a href="/openapi/donations.json" download>Donations OpenAPI ↗</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main id="top">
|
||||
<header class="topbar">
|
||||
<button class="menu" id="menu" aria-label="Открыть меню" aria-expanded="false">☰</button>
|
||||
<label class="search">
|
||||
<span>⌕</span>
|
||||
<input id="search" type="search" placeholder="Поиск endpoint, пути или метода…" autocomplete="off">
|
||||
<kbd>/</kbd>
|
||||
</label>
|
||||
<a class="spec-link" href="/openapi/gateway.json">OpenAPI</a>
|
||||
</header>
|
||||
|
||||
<section class="hero" id="overview">
|
||||
<div class="eyebrow">PUBLIC API · OVE.RS</div>
|
||||
<h1>Один вход.<br><em>Все сервисы OVE.</em></h1>
|
||||
<p class="lead">Документация публичных HTTP API Messenger, Crypto Gateway и Donation Service: авторизация, сообщения, боты, платежи, OBS alerts, webhooks и операторские методы.</p>
|
||||
<div class="hero-actions">
|
||||
<a class="button primary" href="#quickstart">Начать интеграцию</a>
|
||||
<a class="button secondary" href="#endpoints">Все endpoints</a>
|
||||
</div>
|
||||
<div class="service-strip">
|
||||
<article><span class="service-icon messenger">M</span><div><b>Messenger</b><code>https://ms.ove.rs</code></div><i>REST · OAuth · WS</i></article>
|
||||
<article><span class="service-icon gateway">C</span><div><b>Crypto Gateway</b><code>https://cr.ove.rs</code></div><i>REST · HMAC</i></article>
|
||||
<article><span class="service-icon donations">D</span><div><b>Donations</b><code>https://do.ove.rs</code></div><i>REST · SSE · OBS</i></article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="doc-section" id="quickstart">
|
||||
<div class="section-label">01 — Quick start</div>
|
||||
<h2>Первый запрос</h2>
|
||||
<p>Все ответы API — JSON, если endpoint явно не обозначен как HTML, WebSocket или файл. Для Messenger получите токен через email-код, затем передавайте его в заголовке Bearer.</p>
|
||||
<div class="code-card">
|
||||
<div class="code-head"><span>Email login</span><button class="copy" data-copy-target="quick-code">Копировать</button></div>
|
||||
<pre id="quick-code"><code>curl -X POST https://ms.ove.rs/auth/email/start \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"email":"you@example.com"}'
|
||||
|
||||
curl -X POST https://ms.ove.rs/auth/email/verify \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"email":"you@example.com","code":"123456"}'</code></pre>
|
||||
</div>
|
||||
<div class="callout"><strong>HTTPS в тестовом полигоне.</strong> Сертификаты выпущены локальным CA OVE. Добавьте CA в доверенные на устройстве; не отключайте проверку TLS в production-клиенте.</div>
|
||||
</section>
|
||||
|
||||
<section class="doc-section" id="auth">
|
||||
<div class="section-label">02 — Security</div>
|
||||
<h2>Три схемы авторизации</h2>
|
||||
<div class="auth-grid">
|
||||
<article><span>01</span><h3>Messenger Bearer</h3><p>Пользовательский или bot token передается как <code>Authorization: Bearer <token></code>.</p></article>
|
||||
<article><span>02</span><h3>Gateway HMAC</h3><p>Каждый merchant-запрос подписывается ключом, timestamp и одноразовым UUID nonce.</p></article>
|
||||
<article><span>03</span><h3>Operator Bearer</h3><p><code>/admin/v1/*</code> принимает отдельный секрет <code>GATEWAY_ADMIN_TOKEN</code>.</p></article>
|
||||
</div>
|
||||
<h3>Каноническая строка Gateway</h3>
|
||||
<pre><code>timestamp + "\n" +
|
||||
nonce + "\n" +
|
||||
HTTP_METHOD + "\n" +
|
||||
path_and_query + "\n" +
|
||||
hex(sha256(raw_request_body))</code></pre>
|
||||
<p>Результат подпишите HMAC-SHA256 и отправьте lowercase hex в <code>X-Api-Signature</code>. Также обязательны <code>X-Api-Key</code>, <code>X-Api-Timestamp</code>, <code>X-Api-Nonce</code>; для изменяющих запросов — <code>Idempotency-Key</code>.</p>
|
||||
</section>
|
||||
|
||||
<section class="doc-section" id="messenger">
|
||||
<div class="section-label">03 — Messenger</div>
|
||||
<h2>Клиенты, сообщения и real-time</h2>
|
||||
<p>HTTP API покрывает аккаунты, чаты, E2E-ключи, файлы, голосовые сессии, Dastars и очередь обновлений. Native Android транспорт MST5 работает на <code>ms.ove.rs:8080</code>: после защищённого handshake он передаёт CBOR-команды и мультиплексированные ответы внутри зашифрованного потока.</p>
|
||||
<div class="facts">
|
||||
<div><b>Updates</b><span>long polling + ACK</span></div>
|
||||
<div><b>Voice</b><span>WebSocket ticket</span></div>
|
||||
<div><b>E2E</b><span>не зависит от username</span></div>
|
||||
<div><b>Files</b><span>multipart upload</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="doc-section" id="bots">
|
||||
<div class="section-label">04 — Bots</div>
|
||||
<h2>Bot API</h2>
|
||||
<p>Создайте бота через проверенного <code>botfather</code> или <code>POST /bots</code>. Bot token использует ту же Bearer-схему. Обновления хранятся до подтверждения через <code>POST /updates/ack</code>.</p>
|
||||
<div class="two-col">
|
||||
<div><h3>Кнопки</h3><p><code>url</code>, <code>callback</code> и <code>pay_dsr</code>; не более 12 кнопок, текст до 64 символов.</p></div>
|
||||
<div><h3>Реакции</h3><p>Бот получает callback для обычных и платных реакций. Пакет платной реакции может содержать <code>amount > 1</code>.</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="doc-section" id="oauth">
|
||||
<div class="section-label">05 — Identity</div>
|
||||
<h2>OAuth 2.0 Device Flow + OIDC</h2>
|
||||
<p>Сервис запрашивает device code у Messenger, показывает QR или user code, а пользователь подтверждает вход в Android-клиенте. Токен обменивается только после решения пользователя.</p>
|
||||
<div class="flow"><span>1 · device_authorization</span><b>→</b><span>2 · QR / code</span><b>→</b><span>3 · approve</span><b>→</b><span>4 · token</span></div>
|
||||
</section>
|
||||
|
||||
<section class="doc-section" id="gateway">
|
||||
<div class="section-label">06 — Payments</div>
|
||||
<h2>Crypto Gateway</h2>
|
||||
<p>Создавайте invoice и withdrawal, сверяйте баланс и принимайте подписанные callbacks. Денежные величины всегда передаются строками в атомарных единицах — без float.</p>
|
||||
<div class="asset-table" role="table" aria-label="Поддерживаемые активы">
|
||||
<div class="thead"><span>Asset</span><span>Network</span><span>Decimals</span></div>
|
||||
<div><b>BTC</b><span>bitcoin</span><code>8</code></div>
|
||||
<div><b>LTC</b><span>litecoin</span><code>8</code></div>
|
||||
<div><b>TON</b><span>ton</span><code>9</code></div>
|
||||
<div><b>TRX</b><span>tron</span><code>6</code></div>
|
||||
<div><b>USDT</b><span>ton / tron</span><code>6</code></div>
|
||||
</div>
|
||||
<div class="callout"><strong>Hosted checkout.</strong> URL из ответа invoice открывается как <code>/pay/{token}</code>; JSON-статус доступен в <code>/pay/{token}/status</code>. Таймер оплаты синхронизирован с серверным временем.</div>
|
||||
</section>
|
||||
|
||||
<section class="doc-section" id="webhooks">
|
||||
<div class="section-label">07 — Events</div>
|
||||
<h2>Подпись callbacks</h2>
|
||||
<pre><code>signature = hex(HMAC-SHA256(
|
||||
webhook_secret,
|
||||
timestamp + "." + event_id + "." + raw_body
|
||||
))</code></pre>
|
||||
<p>Проверяйте <code>X-Gateway-Timestamp</code>, <code>X-Gateway-Event-Id</code> и <code>X-Gateway-Signature</code>. Доставка at-least-once повторяется до 24 часов — дедуплицируйте события по <code>event_id</code>.</p>
|
||||
</section>
|
||||
|
||||
<section class="doc-section" id="donations">
|
||||
<div class="section-label">08 — Donations</div>
|
||||
<h2>Страница доната и OBS Browser Source</h2>
|
||||
<p>Стример входит через Messenger, получает публичный адрес <code>do.ove.rs/u/{slug}</code> и секретную ссылку виджета. Доноры авторизоваться не обязаны: Donation Service создаёт invoice в Crypto Gateway и показывает его hosted checkout.</p>
|
||||
<div class="flow"><span>1 · public donation</span><b>→</b><span>2 · Gateway checkout</span><b>→</b><span>3 · signed webhook</span><b>→</b><span>4 · OBS SSE alert</span></div>
|
||||
<div class="callout"><strong>Вывод средств.</strong> Каждый запрос резервирует доступный баланс и отдельно подтверждается свайпом в Messenger. В окне подтверждения показаны актив, сумма, сеть и сокращённый адрес.</div>
|
||||
</section>
|
||||
|
||||
<section class="doc-section endpoints-section" id="endpoints">
|
||||
<div class="section-label">09 — Reference</div>
|
||||
<h2>Все endpoints</h2>
|
||||
<div class="endpoint-toolbar">
|
||||
<div id="service-filters" class="filters"></div>
|
||||
<span id="endpoint-count"></span>
|
||||
</div>
|
||||
<div id="endpoint-list" class="endpoint-list" aria-live="polite"></div>
|
||||
<div id="empty" class="empty" hidden>Ничего не найдено. Попробуйте другой запрос.</div>
|
||||
</section>
|
||||
|
||||
<section class="doc-section" id="errors">
|
||||
<div class="section-label">10 — Errors</div>
|
||||
<h2>Ошибки и повторные запросы</h2>
|
||||
<div class="error-grid">
|
||||
<div><code>400</code><span>Неверные поля или состояние</span></div>
|
||||
<div><code>401</code><span>Нет или неверна авторизация</span></div>
|
||||
<div><code>403</code><span>Недостаточно прав</span></div>
|
||||
<div><code>404</code><span>Объект не найден</span></div>
|
||||
<div><code>409</code><span>Конфликт / повтор</span></div>
|
||||
<div><code>429</code><span>Превышен лимит</span></div>
|
||||
<div><code>5xx</code><span>Временная ошибка сервиса</span></div>
|
||||
</div>
|
||||
<p>Для безопасного повтора merchant POST используйте тот же <code>Idempotency-Key</code>. Не повторяйте запрос с новым ключом, пока результат предыдущего неизвестен.</p>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<span>OVE API · public contract</span>
|
||||
<span>Internal wallet gRPC и системные bot nodes не публикуются.</span>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<template id="endpoint-template">
|
||||
<article class="endpoint">
|
||||
<button class="endpoint-summary" type="button" aria-expanded="false">
|
||||
<span class="method"></span>
|
||||
<code class="path"></code>
|
||||
<span class="endpoint-title"></span>
|
||||
<span class="auth-badge"></span>
|
||||
<span class="chevron">⌄</span>
|
||||
</button>
|
||||
<div class="endpoint-body">
|
||||
<p class="endpoint-description"></p>
|
||||
<div class="endpoint-meta"></div>
|
||||
<div class="endpoint-example"></div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
# OVE public API
|
||||
|
||||
Canonical human documentation: https://docs.ove.rs/
|
||||
Messenger OpenAPI: https://docs.ove.rs/openapi/messenger.json
|
||||
Crypto Gateway OpenAPI: https://docs.ove.rs/openapi/gateway.json
|
||||
Donation Service OpenAPI: https://docs.ove.rs/openapi/donations.json
|
||||
|
||||
Public services:
|
||||
- Messenger: https://ms.ove.rs
|
||||
- Crypto Gateway: https://cr.ove.rs
|
||||
- Donation Service: https://do.ove.rs
|
||||
|
||||
Messenger uses Bearer tokens for users and bots. Crypto Gateway merchant API
|
||||
uses request-specific HMAC-SHA256 signatures. Gateway operator API uses a
|
||||
Bearer admin token. Amounts in Crypto Gateway are decimal strings in atomic
|
||||
units.
|
||||
@@ -0,0 +1,660 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "OVE Donations API",
|
||||
"version": "1.0.0",
|
||||
"description": "OVE donation pages, Messenger login, Crypto Gateway payments and OBS alerts."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://do.ove.rs"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/health": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Сервис"
|
||||
],
|
||||
"summary": "Состояние Gateway",
|
||||
"description": "Состояние Gateway. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "get_health",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/assets": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Сервис"
|
||||
],
|
||||
"summary": "Активы для донатов",
|
||||
"description": "Активы для донатов. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "get_api_assets",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/auth/start": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Авторизация"
|
||||
],
|
||||
"summary": "Вход стримера через Messenger",
|
||||
"description": "Вход стримера через Messenger. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "post_api_auth_start",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
},
|
||||
"example": {
|
||||
"email": "streamer@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/auth/poll/{flow}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Авторизация"
|
||||
],
|
||||
"summary": "Статус входа стримера",
|
||||
"description": "Статус входа стримера. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "get_api_auth_poll_flow",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "flow",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/logout": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Авторизация"
|
||||
],
|
||||
"summary": "Выход из Donation Service",
|
||||
"description": "Выход из Donation Service. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "post_api_logout",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionCookie": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/me": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Стример"
|
||||
],
|
||||
"summary": "Панель стримера",
|
||||
"description": "Панель стримера. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "get_api_me",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionCookie": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/profile": {
|
||||
"patch": {
|
||||
"tags": [
|
||||
"Стример"
|
||||
],
|
||||
"summary": "Настройки страницы и OBS",
|
||||
"description": "Настройки страницы и OBS. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "patch_api_profile",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionCookie": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
},
|
||||
"example": {
|
||||
"display_name": "My channel",
|
||||
"slug": "my-channel",
|
||||
"accent_color": "#b9f541",
|
||||
"duration_seconds": 8,
|
||||
"show_donor_name": true,
|
||||
"show_message": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/sound": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"OBS"
|
||||
],
|
||||
"summary": "Загрузка звука",
|
||||
"description": "Загрузка звука. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "put_api_sound",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionCookie": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"OBS"
|
||||
],
|
||||
"summary": "Удаление звука",
|
||||
"description": "Удаление звука. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "delete_api_sound",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionCookie": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/widget/rotate": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"OBS"
|
||||
],
|
||||
"summary": "Ротация секретной OBS-ссылки",
|
||||
"description": "Ротация секретной OBS-ссылки. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "post_api_widget_rotate",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionCookie": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/widget/test": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"OBS"
|
||||
],
|
||||
"summary": "Тестовый OBS alert",
|
||||
"description": "Тестовый OBS alert. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "post_api_widget_test",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionCookie": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/widget/{token}/config": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"OBS"
|
||||
],
|
||||
"summary": "Конфигурация OBS-виджета",
|
||||
"description": "Конфигурация OBS-виджета. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "get_api_widget_token_config",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "token",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/widget/{token}/events": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"OBS"
|
||||
],
|
||||
"summary": "SSE-события OBS-виджета",
|
||||
"description": "Долгоживущий SSE-поток для OBS Browser Source. Виджет сам ставит alerts в очередь.",
|
||||
"operationId": "get_api_widget_token_events",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "token",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/public/creators/{slug}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Донаты"
|
||||
],
|
||||
"summary": "Публичная страница стримера",
|
||||
"description": "Публичная страница стримера. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "get_api_public_creators_slug",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "slug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/public/donations": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Донаты"
|
||||
],
|
||||
"summary": "Создание доната",
|
||||
"description": "Создает Gateway invoice с комиссией 5%. cover_fee=false удерживает комиссию из суммы; cover_fee=true точно увеличивает платеж так, чтобы стример получил введенную сумму после комиссии. Все расчеты выполняются в atomic без float.",
|
||||
"operationId": "post_api_public_donations",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
},
|
||||
"example": {
|
||||
"slug": "my-channel",
|
||||
"donor_name": "Anonymous",
|
||||
"message": "Great stream!",
|
||||
"asset": "USDT",
|
||||
"network": "tron",
|
||||
"amount": "25.5",
|
||||
"cover_fee": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/public/donations/{token}/status": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Донаты"
|
||||
],
|
||||
"summary": "Статус доната",
|
||||
"description": "Статус доната. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "get_api_public_donations_token_status",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "token",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/withdrawals": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Вывод"
|
||||
],
|
||||
"summary": "Запрос вывода",
|
||||
"description": "Резервирует баланс и запускает новый Messenger Device Flow с описанием суммы и адреса.",
|
||||
"operationId": "post_api_withdrawals",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionCookie": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
},
|
||||
"example": {
|
||||
"asset": "USDT",
|
||||
"network": "tron",
|
||||
"amount": "10",
|
||||
"address": "TExampleRecipientAddress"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/withdrawals/{flow}/poll": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Вывод"
|
||||
],
|
||||
"summary": "Подтверждение вывода через Messenger",
|
||||
"description": "После свайпа тем же Messenger-пользователем идемпотентно отправляет withdrawal в Gateway.",
|
||||
"operationId": "get_api_withdrawals_flow_poll",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionCookie": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "flow",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/admin/status": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Оператор"
|
||||
],
|
||||
"summary": "Статус Donation Service",
|
||||
"description": "Статус Donation Service. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "get_api_admin_status",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionCookie": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/admin/creators/{creator_id}/blocked": {
|
||||
"patch": {
|
||||
"tags": [
|
||||
"Оператор"
|
||||
],
|
||||
"summary": "Блокировка страницы стримера",
|
||||
"description": "Блокировка страницы стримера. Возвращает JSON-ответ или стандартную JSON-ошибку.",
|
||||
"operationId": "patch_api_admin_creators_creator_id_blocked",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Успешный ответ"
|
||||
},
|
||||
"400": {
|
||||
"description": "Ошибка запроса"
|
||||
},
|
||||
"401": {
|
||||
"description": "Ошибка авторизации"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionCookie": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "creator_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
},
|
||||
"example": {
|
||||
"blocked": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"sessionCookie": {
|
||||
"type": "apiKey",
|
||||
"in": "cookie",
|
||||
"name": "ove_donations_session",
|
||||
"description": "HttpOnly session; changing requests also require X-CSRF-Token."
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"Error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"error"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
@@ -0,0 +1,137 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #0b0d10;
|
||||
--panel: #111419;
|
||||
--panel-2: #171b21;
|
||||
--line: #262c34;
|
||||
--muted: #929ba8;
|
||||
--text: #f3f5f7;
|
||||
--lime: #b9f541;
|
||||
--cyan: #61ddff;
|
||||
--violet: #a78bfa;
|
||||
--orange: #ffb86b;
|
||||
--sidebar: 260px;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; scroll-padding-top: 88px; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 15px/1.65 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
body::before {
|
||||
content: ""; position: fixed; inset: 0; pointer-events: none;
|
||||
background: radial-gradient(circle at 80% 5%, rgba(97,221,255,.07), transparent 28%),
|
||||
radial-gradient(circle at 30% 40%, rgba(185,245,65,.035), transparent 25%);
|
||||
}
|
||||
.noise { position: fixed; inset: 0; opacity: .025; pointer-events: none; z-index: 10;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
}
|
||||
a { color: inherit; }
|
||||
code, pre { font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; }
|
||||
code { font-size: .9em; }
|
||||
.sidebar {
|
||||
position: fixed; inset: 0 auto 0 0; z-index: 20; width: var(--sidebar);
|
||||
display: flex; flex-direction: column; padding: 26px 20px;
|
||||
background: rgba(12,14,17,.96); border-right: 1px solid var(--line); backdrop-filter: blur(18px);
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; margin-bottom: 38px; }
|
||||
.brand-mark { width: 38px; height: 38px; display: grid; place-items: center; border-radius: 11px;
|
||||
background: var(--lime); color: #0a0d08; font-weight: 900; font-size: 20px; box-shadow: 0 0 30px rgba(185,245,65,.15); }
|
||||
.brand strong { display: block; letter-spacing: .08em; }
|
||||
.brand small { display: block; color: var(--muted); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; }
|
||||
nav { display: grid; gap: 3px; }
|
||||
nav a { padding: 8px 11px; border-radius: 8px; color: #aab2bd; text-decoration: none; font-size: 13px; }
|
||||
nav a:hover, nav a.active { color: var(--text); background: var(--panel-2); }
|
||||
.sidebar-foot { margin-top: auto; display: grid; gap: 8px; color: var(--muted); font-size: 11px; }
|
||||
.sidebar-foot a { text-decoration: none; color: #c5cbd3; }
|
||||
.status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--lime); display: inline-block; box-shadow: 0 0 10px var(--lime); }
|
||||
main { margin-left: var(--sidebar); min-width: 0; }
|
||||
.topbar { height: 66px; position: sticky; top: 0; z-index: 15; display: flex; align-items: center; gap: 18px;
|
||||
padding: 0 max(32px, calc((100vw - var(--sidebar) - 980px) / 2));
|
||||
border-bottom: 1px solid rgba(38,44,52,.75); background: rgba(11,13,16,.82); backdrop-filter: blur(20px); }
|
||||
.menu { display: none; background: none; color: white; border: 0; font-size: 20px; }
|
||||
.search { flex: 1; max-width: 620px; height: 38px; display: flex; align-items: center; gap: 9px; padding: 0 11px;
|
||||
border: 1px solid var(--line); border-radius: 9px; background: #111419; color: var(--muted); }
|
||||
.search:focus-within { border-color: #53606e; }
|
||||
.search input { width: 100%; border: 0; outline: 0; color: var(--text); background: transparent; font: inherit; font-size: 13px; }
|
||||
kbd { border: 1px solid #343b45; border-radius: 5px; padding: 0 6px; font: 11px/19px inherit; color: var(--muted); }
|
||||
.spec-link { margin-left: auto; color: var(--lime); text-decoration: none; font-size: 12px; letter-spacing: .05em; text-transform: uppercase; }
|
||||
.hero, .doc-section, footer { width: min(980px, calc(100% - 64px)); margin-inline: auto; }
|
||||
.hero { min-height: 690px; display: flex; flex-direction: column; justify-content: center; padding: 100px 0 72px; }
|
||||
.eyebrow, .section-label { color: var(--lime); font: 700 11px/1.3 "SFMono-Regular", Consolas, monospace; letter-spacing: .14em; text-transform: uppercase; }
|
||||
h1 { margin: 24px 0 20px; font-size: clamp(50px, 7vw, 88px); line-height: .98; letter-spacing: -.065em; font-weight: 720; }
|
||||
h1 em { color: var(--muted); font-style: normal; }
|
||||
.lead { max-width: 670px; margin: 0; color: #b6bec8; font-size: 19px; line-height: 1.7; }
|
||||
.hero-actions { display: flex; gap: 12px; margin: 32px 0 66px; }
|
||||
.button { padding: 11px 17px; border-radius: 9px; text-decoration: none; font-weight: 650; font-size: 13px; }
|
||||
.button.primary { background: var(--lime); color: #10140a; }
|
||||
.button.secondary { border: 1px solid var(--line); background: var(--panel); }
|
||||
.service-strip { display: grid; grid-template-columns: 1fr 1fr; border: 1px solid var(--line); border-radius: 14px; overflow: hidden; }
|
||||
.service-strip article { display: flex; align-items: center; gap: 13px; padding: 18px; background: rgba(17,20,25,.7); }
|
||||
.service-strip article + article { border-left: 1px solid var(--line); }
|
||||
.service-icon { width: 36px; height: 36px; display: grid; place-items: center; border-radius: 9px; font-weight: 800; color: #0b0d10; }
|
||||
.service-icon.messenger { background: var(--cyan); }.service-icon.gateway { background: var(--violet); }
|
||||
.service-strip b, .service-strip code { display: block; }
|
||||
.service-strip code { color: var(--muted); font-size: 11px; }
|
||||
.service-strip i { margin-left: auto; color: var(--muted); font: normal 10px/1.2 monospace; }
|
||||
.doc-section { padding: 100px 0; border-top: 1px solid var(--line); }
|
||||
h2 { margin: 14px 0 20px; font-size: clamp(32px, 4vw, 49px); line-height: 1.1; letter-spacing: -.045em; }
|
||||
h3 { margin: 28px 0 10px; font-size: 17px; }
|
||||
p { color: #b2bac5; max-width: 760px; }
|
||||
pre { position: relative; overflow-x: auto; margin: 22px 0; padding: 22px; border: 1px solid var(--line); border-radius: 12px;
|
||||
background: #0d1014; color: #dce4ec; font-size: 12px; line-height: 1.75; }
|
||||
.code-card { border: 1px solid var(--line); border-radius: 13px; overflow: hidden; margin-top: 28px; background: #0d1014; }
|
||||
.code-card pre { border: 0; border-radius: 0; margin: 0; }
|
||||
.code-head { display: flex; justify-content: space-between; align-items: center; padding: 9px 13px; border-bottom: 1px solid var(--line); color: var(--muted); font-size: 11px; }
|
||||
.copy { border: 0; color: var(--lime); background: none; cursor: pointer; font: inherit; }
|
||||
.callout { margin-top: 24px; padding: 18px 20px; border-left: 3px solid var(--lime); background: rgba(185,245,65,.055); color: #abb4bf; }
|
||||
.callout strong { color: var(--text); }
|
||||
.auth-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 32px; }
|
||||
.auth-grid article { min-height: 190px; padding: 20px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); }
|
||||
.auth-grid article > span { color: var(--lime); font: 11px monospace; }.auth-grid h3 { margin-top: 32px; }.auth-grid p { font-size: 13px; }
|
||||
.facts { display: grid; grid-template-columns: repeat(4, 1fr); margin-top: 34px; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
|
||||
.facts div { padding: 18px; }.facts div + div { border-left: 1px solid var(--line); }.facts b,.facts span { display:block; }.facts span { color:var(--muted);font-size:11px; }
|
||||
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }.two-col > div { padding: 0 20px 12px; border-left: 1px solid var(--line); }
|
||||
.flow { display: flex; align-items: center; gap: 12px; margin-top: 30px; overflow-x: auto; padding-bottom: 8px; }
|
||||
.flow span { flex: 0 0 auto; padding: 12px 15px; border: 1px solid var(--line); border-radius: 9px; background: var(--panel); font: 12px monospace; }.flow b { color: var(--lime); }
|
||||
.asset-table { max-width: 670px; margin-top: 28px; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
|
||||
.asset-table > div { display: grid; grid-template-columns: 1fr 2fr 1fr; padding: 10px 15px; border-top: 1px solid var(--line); }
|
||||
.asset-table > div:first-child { border: 0; }.asset-table .thead { color: var(--muted); background: var(--panel); font-size: 11px; text-transform: uppercase; }
|
||||
.asset-table code { color: var(--lime); }
|
||||
.endpoint-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 15px; margin: 28px 0 14px; }
|
||||
.filters { display: flex; gap: 7px; flex-wrap: wrap; }
|
||||
.filter { border: 1px solid var(--line); border-radius: 99px; background: var(--panel); color: #b7bec7; padding: 6px 11px; cursor: pointer; font: 11px inherit; }
|
||||
.filter.active { background: var(--text); color: var(--bg); border-color: var(--text); }
|
||||
#endpoint-count { color: var(--muted); font: 11px monospace; white-space: nowrap; }
|
||||
.endpoint-list { display: grid; gap: 7px; }
|
||||
.endpoint { border: 1px solid var(--line); border-radius: 11px; background: rgba(17,20,25,.74); overflow: hidden; scroll-margin-top: 80px; }
|
||||
.endpoint-summary { width: 100%; min-height: 57px; display: grid; grid-template-columns: 54px minmax(175px, 1.25fr) 1fr auto 20px; align-items: center; gap: 12px;
|
||||
border: 0; padding: 9px 14px; background: none; color: var(--text); text-align: left; cursor: pointer; }
|
||||
.method { width: 48px; padding: 3px 0; border-radius: 5px; text-align:center; font: 800 10px monospace; }
|
||||
.method.GET { color: var(--cyan); background: rgba(97,221,255,.09); }.method.POST { color: var(--lime); background: rgba(185,245,65,.09); }.method.DELETE { color: #ff718a; background: rgba(255,113,138,.09); }
|
||||
.path { overflow-wrap: anywhere; color: #dce2e9; font-size: 12px; }
|
||||
.endpoint-title { color: var(--muted); font-size: 12px; }
|
||||
.auth-badge { border: 1px solid #343b45; border-radius: 99px; padding: 2px 8px; color: var(--muted); font: 9px monospace; text-transform: uppercase; }
|
||||
.chevron { color: var(--muted); transition: transform .2s; }.endpoint.open .chevron { transform: rotate(180deg); }
|
||||
.endpoint-body { display: none; padding: 0 18px 18px 80px; border-top: 1px solid var(--line); }.endpoint.open .endpoint-body { display: block; }
|
||||
.endpoint-meta { display: flex; gap: 8px; flex-wrap: wrap; margin: 14px 0; }.endpoint-meta code { padding: 4px 8px; background:#0d1014; border-radius:5px;color:#aeb7c2; }
|
||||
.endpoint-example pre { margin-bottom: 0; }.empty { padding: 50px; text-align:center; color:var(--muted); }
|
||||
.error-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; overflow:hidden; margin: 25px 0; border:1px solid var(--line); border-radius:12px; background:var(--line); }
|
||||
.error-grid div { display:flex; gap:15px; padding:12px 15px; background:var(--panel); }.error-grid code{color:var(--orange)}.error-grid span{color:var(--muted)}
|
||||
footer { display: flex; justify-content: space-between; gap: 20px; padding: 40px 0 60px; border-top: 1px solid var(--line); color: var(--muted); font-size: 11px; }
|
||||
@media (max-width: 900px) {
|
||||
:root { --sidebar: 0px; }
|
||||
.sidebar { width: 260px; transform: translateX(-100%); transition: transform .25s; box-shadow: 20px 0 50px rgba(0,0,0,.4); }
|
||||
.sidebar.open { transform: translateX(0); }
|
||||
.menu { display:block; }.topbar{padding:0 20px}.hero,.doc-section,footer{width:min(100% - 38px,760px)}
|
||||
.auth-grid{grid-template-columns:1fr}.facts{grid-template-columns:1fr 1fr}.facts div:nth-child(3){border-left:0;border-top:1px solid var(--line)}.facts div:nth-child(4){border-top:1px solid var(--line)}
|
||||
}
|
||||
@media (max-width: 620px) {
|
||||
.hero{min-height:auto;padding-top:75px}.service-strip{grid-template-columns:1fr}.service-strip article+article{border-left:0;border-top:1px solid var(--line)}
|
||||
.two-col{grid-template-columns:1fr}.endpoint-summary{grid-template-columns:48px 1fr 18px}.endpoint-title,.auth-badge{display:none}.endpoint-body{padding-left:18px}
|
||||
.facts{grid-template-columns:1fr}.facts div+div{border-left:0;border-top:1px solid var(--line)}.error-grid{grid-template-columns:1fr}.spec-link,kbd{display:none}
|
||||
footer{flex-direction:column}h1{font-size:48px}.lead{font-size:16px}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) { html{scroll-behavior:auto}*{transition:none!important} }
|
||||
Reference in New Issue
Block a user