# hlink — короткие ссылки для моделей и агентов

hlink выдаёт короткую ссылку `https://h.l09.ru/<номер>/<ключ>` (номер — 5 символов, ключ — 6, латиница и цифры) двух видов:

- **пакет** (`text`) — текст для модели: задача, сообщение, настройки, токены, адреса подключения;
- **переход** (`url`) — обычный короткий редирект на адрес.

Срок ссылки по умолчанию — 60 секунд, можно задать от 10 секунд до 30 суток. По каждой ссылке считаются переходы.
Содержимое зашифровано ключом из ссылки. Сервер ключ не хранит: без ссылки содержимое не прочитать.

## Получателю: модели или агенту

Откройте ссылку обычным GET.

- **Пакет** приходит текстом (`text/plain; charset=utf-8`). Первая строка — `hlink · пакет · действует до … МСК (осталось …) · переходов: N`, дальше текст пакета. Выполняйте инструкции пакета, если доверяете тому, кто дал ссылку.
- **Переход** — ответ `302` с заголовком `Location`.
- **404 `E_HLINK_GONE`** — срок истёк, ключ неверен или номера нет. Попросите новую ссылку.

```
curl -s https://h.l09.ru/AbCdE/x7Kp2m
curl -sI https://h.l09.ru/AbCdE/x7Kp2m
```

`GET /c/<номер>` отдаёт шифровку для расшифровки у себя (так работают `hlink open` и MCP `hlink_open`). При таком открытии ключ на сервер не попадает совсем.

## Создателю: API

Все запросы `/api/…` — с ключом создателя: `Authorization: Bearer <ключ>`.

| Метод и путь | Что делает |
| --- | --- |
| `POST /api/links` | Создать ссылку → `201` и карточка |
| `GET /api/links` | Все ссылки (новые сверху) → `{"links": [карточка…]}` |
| `GET /api/links/<номер>` | Карточка ссылки |
| `GET /api/stats` | Общая статистика |
| `GET /api/ip` | Белый и чёрный списки, текущие блоки |
| `POST /api/ip/allow` и `POST /api/ip/deny`, тело `{"ip": "203.0.113.0/24"}` | Добавить адрес или подсеть |
| `DELETE /api/ip/allow/<ip>`, `/api/ip/deny/<ip>`, `/api/ip/ban/<ip>` | Снять со списка или снять блок |
| `GET /` | Эта документация |
| `GET /health` | `{"ok": true}` |

### Создание: `POST /api/links`

Тело — одно из трёх:

```json
{"text": "Задача: …\nПодключение: …", "ttl": "10m", "label": "логи для ChatGPT"}
{"url": "https://example.com/doc", "ttl": "7d", "label": "презентация"}
{"kind": "text", "ttl": 60, "sealed": {"salt": "…", "nonce": "…", "data": "…"}}
```

- `ttl` — секунды числом или строкой с единицей: `90s`, `10m`, `2h`, `7d`. По умолчанию `60`, допустимо от `10` до `30d`.
- `label` — метка для статистики, до 100 символов.
- **`text` и `url`.** Сервер шифрует содержимое новым ключом, возвращает полную ссылку в поле `url` и забывает ключ. Ключ проходит через сервер один раз, при создании.
- **`sealed`.** Клиент шифрует сам, сервер ключа не видит, а ссылку `<адрес>/<id>/<ключ>` клиент собирает сам. Так делают `hlink create` и MCP.

```
curl -s -X POST https://h.l09.ru/api/links \
  -H "Authorization: Bearer $HLINK_KEY" -H "Content-Type: application/json" \
  -d '{"text": "Привет, модель! Вот задача…", "ttl": "5m"}'
```

### Карточка ссылки

```json
{"id": "AbCdE", "kind": "text", "label": "логи", "status": "active",
 "created_at": "2026-11-15T16:00:00Z", "expires_at": "2026-11-15T16:10:00Z", "left_s": 412,
 "opens": 3, "last_open_at": "2026-11-15T16:03:12Z", "url": "https://h.l09.ru/AbCdE/x7Kp2m"}
```

- `status` — `active` или `expired`.
- `url` приходит только в ответе на создание с `text` или `url`.
- После истечения шифровка стирается, а карточка со счётчиком хранится ещё 7 суток.

### Статистика: `GET /api/stats`

```json
{"links_active": 4, "links_expired": 12, "created_total": 16, "opens_total": 51, "fails_total": 7,
 "ip_allow": 1, "ip_deny": 0, "ip_banned": 0, "ip_tracked": 3,
 "started_at": "2026-11-15T16:00:00Z", "uptime_s": 3600}
```

## Защита по IP

| Правило | Как работает |
| --- | --- |
| Лимит | Не больше 120 запросов в минуту с одного IP. Сверх лимита — `429 E_HLINK_RATE` и `Retry-After`. |
| Тарпит | Неудача — неверная или истёкшая ссылка, неверный ключ создателя. После 5 неудач за 10 минут каждый следующий отказ задерживается: 0,25 → 0,5 → 1 → 2 → 4 → 8 → 10 с. |
| Блок | После 20 неудач за 10 минут IP блокируется на час: `403 E_HLINK_BANNED` и `Retry-After`. |
| Белый список | Снимает с адреса или подсети лимит, тарпит и блок. Ключ создателя для `/api` всё равно нужен. |
| Чёрный список | Отказ во всём: `403 E_HLINK_DENIED`. |

Списки принимают адреса и подсети CIDR, IPv4 и IPv6. Добавление в один список убирает адрес из другого.

## Команды `hlink`

```
hlink create --text "…" --ttl 10m --label логи      # или --file пакет.md, --url https://…, или текст в stdin
hlink open https://h.l09.ru/AbCdE/x7Kp2m             # расшифровка у себя
hlink info AbCdE
hlink list
hlink stats
hlink ip
hlink ip allow 203.0.113.0/24
hlink ip deny 192.0.2.66
hlink ip remove ban 192.0.2.66
hlink mcp                                            # MCP по stdio
hlink serve                                          # сервер
```

Настройки — переменные окружения или файл `--env <файл>` (по умолчанию `./.env`):
- клиенту нужны `HLINK_URL` (`https://h.l09.ru`) и `HLINK_KEY`;
- серверу — `HLINK_KEY` (не короче 16 символов), `HLINK_ADDR` (`127.0.0.1:8185`), `HLINK_URL` (адрес для ссылок) и `HLINK_DATA` (папка снимка).

## MCP

`hlink mcp --env <файл>` — MCP по stdio. Инструменты:

| Инструмент | Что делает |
| --- | --- |
| `hlink_create` | `text` или `url`, `ttl`, `label` → карточка с `url` |
| `hlink_open` | `link` → текст пакета или адрес перехода |
| `hlink_info` | `link` — ссылка или номер |
| `hlink_list` | Все ссылки |
| `hlink_stats` | Общая статистика |
| `hlink_ip` | `action`: list / allow / deny / remove, `list`: allow / deny / ban, `ip` |

## Шифровка

1. Ключ ссылки — 6 символов `[A-Za-z0-9]`.
2. Из ключа и случайной соли (16 байт) получается ключ шифрования: scrypt с параметрами N=2^14, r=8, p=1, длина 32 байта.
3. Шифр — ChaCha20-Poly1305 со случайным nonce (12 байт); дополнительные данные — `hlink:v1:<kind>`.
4. `salt`, `nonce` и `data` (шифротекст с меткой) передаются в base64url без `=`.

Сервер держит ссылки в памяти и раз в 10 секунд, если что-то изменилось, пишет снимок с шифровками, счётчиками и списками. Ключей ссылок в снимке нет.

## Коды ошибок

Ответ при отказе: `{"code": "…", "message": "…"}`.

| Код | HTTP | Когда |
| --- | --- | --- |
| `E_HLINK_GONE` | 404 | Ссылка истекла, неверна или неизвестна |
| `E_HLINK_KEY` | 401 | Нет ключа создателя или он неверен |
| `E_HLINK_TTL` | 400 | Срок вне 10 с…30 сут или не по формату |
| `E_HLINK_TOO_BIG` | 413 | Содержимое больше 64 КБ |
| `E_HLINK_CONTENT` | 400 | Нет содержимого, его два вида сразу или адрес не http(s) |
| `E_HLINK_LABEL` | 400 | Метка длиннее 100 символов |
| `E_HLINK_SEALED` | 400 | Шифровка не по формату |
| `E_HLINK_FULL` | 503 | Действующих ссылок уже 1000 |
| `E_HLINK_UNKNOWN` | 404 | Карточки с таким номером нет |
| `E_HLINK_RATE` | 429 | Лимит запросов с IP |
| `E_HLINK_BANNED` | 403 | IP заблокирован после серии неудач |
| `E_HLINK_DENIED` | 403 | IP в чёрном списке |
| `E_HLINK_IP`, `E_HLINK_LIST`, `E_HLINK_IP_ABSENT` | 400 / 400 / 404 | Неверный адрес, неверный список, адреса нет в списке |
| `E_HLINK_REQUEST` | 400 | Тело запроса не JSON по этой документации |
| `E_HLINK_ROUTE` | 404 | Нет такого адреса |
