# DSTUcrypt — інтеграційний скіл для LLM

> Самодостатній довідник для інтеграції DSTUcrypt у веб-проєкт. Прочитавши цей
> файл, ти маєш достатньо, щоб коректно підключити КЕП-віджети й відповісти на
> будь-яке питання щодо інтеграції. Джерело правди: https://dstucrypt.com.ua
> Онлайн-версія цього файлу: https://dstucrypt.com.ua/llms.md

## Що це

DSTUcrypt — це набір готових **вбудовуваних iframe-віджетів** для кваліфікованого
електронного підпису (КЕП / ЕЦП) за українськими стандартами ДСТУ (ДСТУ‑4145 тощо),
плюс тонкий JS‑SDK для батьківської сторінки. Криптографія виконується **у браузері
користувача** (WebAssembly) всередині iframe на origin
`dstucrypt.io`, окремому від сторінки інтегратора. Приватний ключ і пароль **ніколи** не потрапляють ні на сайт‑хост,
ні на сервери DSTUcrypt.

Модель:

- Інтегратор робить **один імпорт** SDK з `https://dstucrypt.io/embed/dstucrypt-embed.mjs`.
- SDK сам будує адресу віджета зі свого origin — інтегратор **не пише URL файлів** і
  **нічого не хостить у себе**.
- Кожен `embed()` створює **власний iframe** для одного віджета; спілкування —
  проміси через `postMessage`.
- Дані ходять як `ArrayBuffer` (structured clone). Base64 у коді інтегратора не треба.

## Швидкий старт

```html
<script type="module">
  import { embed } from 'https://dstucrypt.io/embed/dstucrypt-embed.mjs';

  const signer = await embed('sign', { mount: 'modal' });
  const { signature, signerCertId, accredited } = await signer.sign(fileBytes, {
    format: 'CAdES-T',
  });
  signature.download('document.p7s');   // або signature.base64 / .bytes / .blob()
</script>
```

- `fileBytes` може бути `File | Blob | ArrayBuffer | Uint8Array | string`.
- `embed()` повертає екземпляр віджета; методи відкривають модалку (за потреби) і
  повертають проміс з результатом.

## Типи віджетів і їхні методи

`embed(type, opts)` — `type` один із: `'sign' | 'verify' | 'encrypt' | 'decrypt' | 'cert' | 'auth'`.

### sign — підпис
```js
const s = await embed('sign', { mount: 'modal' });
const r = await s.sign(data, { format: 'CAdES-T', digest: 'gost-34311', fileName: 'doc.txt' });
// r = { signature: Bytes, signerCertId: string, accredited: boolean|null }

// Мультипідпис (платна опція «Кілька підписантів»): додати підписанта до готового CMS.
const r2 = await s.coSign(existingSignature, data);   // → { signature: Bytes, accredited }
// Без оплаченої опції: throw з error.code === 'MULTISIGN_NOT_ENABLED'.
```
Опції `sign(data, options)` (усі необов'язкові):

| Опція | Тип / за замовч. | Опис |
|---|---|---|
| `format` | рядок, `'CAdES-BES'` | Повний рядок формату (див. нижче) |
| `fileName` | рядок, `'document'` | Ім'я файлу для PAdES/XAdES/ASiC-контейнерів |
| `detached` | bool, `false` | Відокремлений (detached) CMS-підпис — лише CAdES |
| `checkStatus` | bool, `false` (авто `true` для `CAdES-C`/`CAdES-XL`) | Перевіряти статус сертифіката (OCSP/CRL) і вкладати дані відкликання. Для `-C`/`-XL` вмикається автоматично — інакше рівень неповний; потребує онлайн-OCSP |
| `includeContentTS` | bool, `false` | Додати мітку часу на дані (contentTS) — для CAdES-T і вище |
| `digest` | `'gost-34311'` (деф.) \| `'kupyna-256'` | Геш підпису. За замовч. ГОСТ 34.311-95 (його приймають держвалідатори — Дія/ЦЗО); `'kupyna-256'` — ДСТУ 7564 «Купина». Разом із гешем береться узгоджений signAlgo ДСТУ 4145 |
| `tspUrl` / `proxyUrl` | рядок | Перевизначити TSP/проксі для цього підпису (зазвичай не треба) |

Допустимі значення `format` (повний рядок, не «родина»):

- **CAdES** (`.p7s`, CMS): `CAdES-BES` · `CAdES-T` · `CAdES-C` · `CAdES-XL`
- **PAdES** (`.pdf`): `PAdES-B-B` · `PAdES-B-T` · `PAdES-B-LT` · `PAdES-B-LTA`
- **XAdES** (`.xml`): `XAdES-BES` · `XAdES-B-T` · `XAdES-B-LT` · `XAdES-B-LTA`
- **ASiC-S** (`.asics`): `ASiC-S-BES` · `ASiC-S-T`
- **ASiC-E** (`.asice`): `ASiC-E-BES` · `ASiC-E-T`

> XAdES (XML-підпис) підтримує українські ключі **ДСТУ-4145** з гешами ДСТУ ГОСТ
> 34.311-95 (за замовчуванням) та ДСТУ 7564:2014 «Купина-256». Рівні `-B-T`/`-B-LT`/
> `-B-LTA` потребують онлайн-доступу до TSP (мітка часу) — як і CAdES-T.

- `accredited`: `true` — сертифікат від акредитованого надавача (це КЕП); `false` —
  ні (підпис дійсний, але не КЕП); `null` — не перевірено.
- `s.close()` — закрити відкритий контейнер. `s.session` — див. «Автоматичний режим».

### Дія.Підпис і Smart ID — ключ у застосунку
Працюють і для `sign`, і для `auth`. Вмикаються одним параметром:
```js
const s = await embed('sign', { providers: ['file', 'diia', 'smartid'] });
const { signature, provider } = await s.sign(file, { fileName: file.name });
//  provider — чим підписали: 'file' | 'diia' | 'smartid'
```
Ключове для генерації коду:
- **`digest` для хмарних НЕ задавати** — віджет підставляє потрібний сам
  (Дія — `gost-34311`, Smart ID — `kupyna-256`); передане значення ігнорується.
- **Документ не надсилається** — до надавача йде лише геш (32 байти).
- **`signBatch()`** підписує пачку однією сесією: один QR і одне підтвердження.
  Ліміти: Дія — 10 файлів, Smart ID — 40.
- **QR живе 3 хвилини**, одноразовий. Скасування — `throw` з `CANCELLED`.
- **Доступно оплаченим доменам**: хмарні дії витрачають наш партнерський акаунт
  у надавача, тому домени на автотріалі за замовчуванням не пускаються.

### verify — перевірка підпису
```js
const v = await embed('verify', { mount: 'modal' });
// dataBytes передавайте ЛИШЕ для відокремленого (detached) підпису;
// для вкладеного (attached) — null.
const r = await v.verify(signatureBytes, dataBytes /* або null */);
// r = { valid: boolean, signers: [...], content: Bytes|null }
```
Кожен елемент `signers` містить: `valid`, `status` (`TOTAL-VALID…`),
`accredited` (`true|false|null` — чи виданий сертифікат підписанта акредитованим
АЦСК, тобто це КЕП; `null` — дані довіри недоступні), `format`, `signingTime`,
`timestampTime`, `ocspStatus`, `crlStatus`, `signerCertId`, `signerName`,
`signerOrg`, `signerCode` (РНОКПП/ЄДРПОУ). Формати визначаються автоматично за
вмістом: CMS/CAdES, PAdES (PDF), XAdES (XML), ASiC (ZIP).

> Для **авторитетної** (серверної) перевірки — наприклад, коли рішення про
> допуск ухвалює ваш бекенд — використовуйте `POST /api/verify`: він так само
> повертає `accredited`/`qualified` на кожного підписанта, але нативним ядром на
> сервері, куди фронтенд не має доступу.

### encrypt / decrypt — шифрування
```js
const e = await embed('encrypt', { mount: 'modal' });
const envelope = await e.encrypt(data, recipientCertBytes);   // → Bytes

const d = await embed('decrypt', { mount: 'modal' });
const content = await d.decrypt(envelopeBytes);               // → Bytes
```

### cert — сертифікати
```js
const c = await embed('cert');
await c.inspect(certBytes);                  // розбір сертифіката у структуру
await c.verify(certBytes, { via: 'ocsp' });  // онлайн-статус (ocsp|crl)
await c.loadChain(certBytes);                // підвантажити ланцюжок видавців
```

### auth — вхід за КЕП (Login via КЕП)
**Challenge береться на БЕКЕНДІ** (server-to-server), а не в браузері:
```js
// 1) ваш бекенд: POST https://dstucrypt.io/api/auth/challenge → { challenge, expiresIn }
// 2) фронтенд:
const a = await embed('auth', { mount: 'modal' });
const { signature, subject, accredited } = await a.login({ challenge });
//    signature — рядок base64 (attached CAdES); subject/accredited — best-effort для UX
// 3) надішліть { challenge, signature } на СВІЙ бекенд
// 4) ваш бекенд: POST https://dstucrypt.io/api/auth/verify { challenge, signature }
//    → підтверджена ідентичність. Довіряйте ЛИШЕ відповіді /api/auth/verify
//      (subject/accredited з віджета — тільки для UX, не для авторизації).
```
Вхід через застосунок (Дія.Підпис, Smart ID) — той самий віджет:
```js
const a = await embed('auth', { providers: ['file', 'diia', 'smartid'] });
const r = await a.login({ challenge });
//  ВАЖЛИВО: Дія вимагає challenge у вигляді uuid v4, тож для неї його задає
//  НАШ сервер і повертає в r.challenge. Перевіряти треба підписаний рядок:
const verified = { challenge: r.challenge || challenge, signature: r.signature };
```
Один рядок `r.challenge || challenge` покриває всі три способи: для файлового
ключа й Smart ID поле порожнє, для Дії — містить справжній challenge.
Для хмарних способів `subject` із віджета порожній — дані підписанта віддає
`/api/auth/verify`.

## Підтримувані типи та алгоритми

**Алгоритм ключа підпису:** ДСТУ 4145-2002 (український КЕП/ЕЦП) — основний і
перевірений. Ключ відкривається з контейнерів PKCS#12 (`.p12`/`.pfx`), JKS, `Key-6.dat`.

**Алгоритми гешування підпису** (обираються опцією `digest`):

| Алгоритм | Стандарт | Значення `digest` | За замовчуванням |
|---|---|---|---|
| ГОСТ 34.311-95 | ДСТУ ГОСТ 34.311-95 | `'gost-34311'` | **так** — сумісний із держвалідаторами (Дія/ЦЗО) |
| Купина-256 | ДСТУ 7564:2014 | `'kupyna-256'` | явна опція (сучасний нацстандарт) |

Без опції `digest` підпис гешується **ГОСТ 34.311-95** (для ключів, що його вміють):
саме пару «підпис ДСТУ 4145 + геш ГОСТ 34.311» сьогодні приймають держвалідатори —
Дія відхиляє «Купину»-у-CMS. **«Купину»** (ДСТУ 7564) вмикайте явно —
`digest:'kupyna-256'`. Для RSA/ECDSA-ключів геш обирає ядро. Явно заданий `digest`
завжди має пріоритет; діє для всіх форматів (CAdES/XAdES/PAdES/ASiC). Коли держсистеми
перейдуть на «Купину», її можна зробити дефолтом одним рядком — без перезбирання wasm.

**Явне задавання геша (рекомендовано для передбачуваності):**
```js
await signer.sign(data, { format: 'CAdES-T', digest: 'gost-34311' });  // ГОСТ 34.311
await signer.sign(data, { format: 'CAdES-T', digest: 'kupyna-256' });  // Купина (ДСТУ 7564)
```

**Формати підпису та рівні** (створення `sign()` і перевірка `verify()`):

| Родина | Рівень BES | Рівень T (з міткою часу) | Рівень LT/C | Рівень LTA/XL | Файл |
|---|---|---|---|---|---|
| **CAdES** (CMS) | `CAdES-BES` | `CAdES-T` | `CAdES-C` | `CAdES-XL` | `.p7s` |
| **XAdES** (XML) | `XAdES-BES` | `XAdES-B-T` | `XAdES-B-LT` | `XAdES-B-LTA` | `.xml` |
| **PAdES** (PDF) | `PAdES-B-B` | `PAdES-B-T` | `PAdES-B-LT` | `PAdES-B-LTA` | `.pdf` |
| **ASiC-S** | `ASiC-S-BES` | `ASiC-S-T` | — | — | `.asics` |
| **ASiC-E** | `ASiC-E-BES` | `ASiC-E-T` | — | — | `.asice` |

- **Усі родини підтримують ключі ДСТУ-4145** (у т.ч. XAdES — XML-DSig з
  `dstu4145-gost34311` / `dstu4145-dstu7564-256`).
- Рівні **T / LT / LTA** потребують онлайн-доступу до TSP (мітка часу від
  кваліфікованого надавача) — SDK бере його через вбудований проксі.
- Мітки часу немає лише на рівні **BES** (за визначенням стандарту).

**Повний перелік значень `format` для CAdES** (усі перевірені на бібліотеці):

| `format` | Що це | Потребує |
|---|---|---|
| `RAW` | голий підпис даних, без CMS-обгортки | ключ (сертифікат не обов'язковий) |
| `CMS` | CMS SignedData без CAdES-атрибутів | ключ (сертифікат не обов'язковий) |
| `CAdES-BES` | базовий CAdES | сертифікат |
| `CAdES-T` | + мітка часу підпису | сертифікат + онлайн TSP |
| `CAdES-C` | + референси на сертифікати/CRL | сертифікат + онлайн TSP |
| `CAdES-XL` (= `CAdES-LT`) | + самі сертифікати й CRL/OCSP | сертифікат + онлайн TSP |
| `CAdES-A` (= `CAdES-LTA`) | + архівна мітка часу | сертифікат + онлайн TSP |

> ⚠️ **Номенклатура CAdES ≠ XAdES/PAdES.** CAdES вживає **стару** назву рівня:
> `CAdES-T`, `CAdES-C`, `CAdES-XL`, `CAdES-A` (з аліасами `-LT`/`-LTA`). Baseline-форма
> `CAdES-B-T` **не приймається** (`INVALID_PARAMETER`). Натомість XAdES і PAdES
> вживають baseline-форму: `XAdES-B-T`, `PAdES-B-T` тощо. Тобто рівень «T» — це
> `CAdES-T`, але `XAdES-B-T` / `PAdES-B-T`.

**Підтримка по операціях:**

| Операція | Формати |
|---|---|
| `sign` | CAdES, XAdES, PAdES, ASiC-S, ASiC-E (усі рівні вище) |
| `verify` | ті самі + автовизначення формату за вмістом |
| `encrypt` / `decrypt` | CMS EnvelopedData (`.p7e`) |
| `cert` | розбір / OCSP-CRL-статус / ланцюжок сертифіката |
| `auth` | вхід за КЕП (attached CAdES над challenge) |

## Значення `Bytes` (результати)

Методи повертають об'єкт `Bytes` замість base64:

- `.bytes` — `Uint8Array`
- `.size` — довжина
- `.base64` — рядок base64
- `.text(encoding='utf-8')` — як текст
- `.blob(type='application/octet-stream')` — Blob
- `.download(name='file.bin', type?)` — завантажити файл у користувача

## Опції `embed(type, opts)`

| Опція | Тип | Опис |
|---|---|---|
| `mount` | `'modal'` \| CSS-селектор \| Element | `'modal'` (за замовч.) — модальне вікно; інакше вбудувати в елемент |
| `providers` | масив рядків | Способи підпису/входу у вікні: `'file'` (файловий ключ), `'diia'` (Дія.Підпис), `'smartid'` (Smart ID від ПриватБанку). За замовчуванням лише `['file']` — хмарні вмикаються свідомо. Один спосіб — перемикача немає |
| `session` | `true` \| об'єкт | Автоматичний режим (запам'ятати ключ). Див. нижче |
| `branding` | `false` | Прибрати бейдж «Захищено DSTUcrypt». **Платна опція** «Персональний дизайн» |
| `requestInfo` | `true` | Показати технічний рядок «Застосунок просить підписати N байт (формат)» (за замовч. прихований) |
| `caSelect` | `true` | Показати у віджеті список КНЕДП для ручного вибору. За замовчуванням сертифікат «голого» ключа шукається автоматично по всіх надавачах |
| `caProvider` | рядок (CMP-URL) | Переднастроїти одного надавача — звужує пошук. Дає змогу зробити власний UI вибору на боці інтегратора |
| `certChain` | `false` | Приймати при ручному завантаженні лише один сертифікат, а не ланцюжок `.p7b` (жорсткіше) |
| `theme` / `styles` / `css` / `brand` | об'єкт/рядок | «Персональний дизайн» — власна тема/CSS/бренд. **Платна опція** |
| `allowOrigin` | рядок | Явний origin для postMessage (зазвичай не потрібно) |
| `src` | рядок | Службовий override URL віджета (для локальної розробки) |

## Автоматичний режим (запам'ятати ключ) — `session`

Щоб не обирати файл і не вводити дані щоразу. Вмикає **інтегратор**:

```js
// PIN-режим: запам'ятати ключ + пароль, обгорнуті PIN (за замовчуванням)
await embed('sign', { session: true });

// не питати PIN протягом N хвилин у межах вкладки (тихий підпис):
await embed('sign', { session: { ttlMinutes: 60 } });

// «лише ключ»: запам'ятати ЛИШЕ файл ключа; пароль питати ЩОРАЗУ й НЕ зберігати:
await embed('sign', { session: { mode: 'password' } });

// дозволити користувачу зберегти без PIN (обфускація, менш безпечно):
await embed('sign', { session: { noPin: true } });
```

Модель зберігання:

- **Зашифрований ключ** лежить у `localStorage` origin `dstucrypt.io`, жорстко
  прив'язаний до домену‑хоста (сайт А не дістане ключ, збережений на сайті Б),
  до 30 днів. Шифрування: AES‑GCM‑256, ключ — PBKDF2‑SHA256(310k) від PIN. **PIN
  ніде не зберігається.**
- **Розблокований {ключ+пароль}** живе в `sessionStorage` вкладки протягом
  `ttlMinutes` (у цей час підпис іде тихо, без PIN); за замовчуванням (`ttlMinutes`
  не задано) PIN питається щоразу.
- Режим `mode: 'password'` зберігає лише контейнер, пароль не зберігає ніколи.

API сесії на екземплярі: `w.session.setup() | status() | unlock({pin}) | lock() | forget()`.
Доступний у `sign`, `decrypt`, `auth`.

## «Персональний дизайн» і брендинг — платна опція

`branding:false` (прибрати бейдж) і `theme/styles/css/brand` (власна тема, довільний
CSS, свій бренд) працюють **лише якщо для домену оплачено опцію `custom_design`**
(або протягом випробувального періоду). Без права — бейдж лишається, кастом не
застосовується.

```js
await embed('sign', {
  branding: false,
  theme:  { accent: '#0057ff' },
  styles: { '.file': { borderStyle: 'solid' } },
  css:    '.hero h1 { letter-spacing: 0 }',
  brand:  { name: 'Акме Підпис', href: 'https://example.com' },
});
```

## Серверні API (публічні)

- `GET https://dstucrypt.io/api/license?origin=<origin>` → JSON статусу ліцензії
  домену: `{ active, status, origin, trialEnd?, daysLeft?, whiteLabel, customDesign }`.
  `status`: `licensed | trial | trial_expired | dev | self | standalone | none`.
- `GET https://dstucrypt.io/api/licensed` → `{ "ok": true|false }` — мінімальна
  перевірка **поточного** домену (сервер бачить його з заголовка `Origin`/`Referer`,
  передавати нічого не треба). `ACAO: *`. Для швидкого гейту у своєму фронтенді.
- `POST https://dstucrypt.io/api/auth/challenge` → `{ challenge, expiresIn }`
  (для Login via КЕП; `expiresIn` — час життя challenge у секундах, ~300).
- `POST https://dstucrypt.io/api/auth/verify` `{ challenge, signature }` →
  `{ ok, subject:{ fullName, taxId, orgCode, issuer, … }, signatureFormat, signingTime,
  status, accredited, qualified }`. Криптографічно перевірена ідентичність
  (**авторитетно**; лише цій відповіді довіряти). Challenge одноразовий — повторний
  або прострочений дає `401 { ok:false, reason:'challenge_invalid_or_used' }`.
- `POST https://dstucrypt.io/api/verify` — серверна авторитетна перевірка підпису
  або сертифіката (криптографія + акредитація надавача нативним ядром на сервері):
  - Підпис: `{ signature: base64, content?: base64 }` (`content` — лише для detached) →
    `{ ok, signers:[{ status, signatureValid, digestValid, accredited, qualified,
    revocation, signatureFormat, signingTime, timestampTime, subject }], content? }`.
  - Сертифікат: `{ cert: base64 }` → `{ ok, accredited, qualified, revocation, subject }`.

  Усі три `POST` — server-to-server (виклик із вашого бекенда), тіло — JSON.
  Якщо серверний верифікатор тимчасово недоступний, ці ендпойнти віддають
  `503 { ok:false, reason:'verifier_unavailable' }`.

## Ліцензування та ціни

- Ліцензія — **на домен**, оплата онлайн через **LiqPay** (рекурентна підписка).
- **7 днів безкоштовно** автоматично на кожному новому домені (просто підключіть віджет).
- Базова підписка: **4 500 грн/міс** або **38 880 грн/рік** за домен.
- Опція «Персональний дизайн»: **+1 200 грн/міс** або **+10 368 грн/рік** за домен.
- Оформлення: сторінка `https://dstucrypt.com.ua/buy`. Кабінет: `/login`.
- Гейт реалізовано на сервері (`Content-Security-Policy: frame-ancestors` + віддача
  ассетів лише ліцензованим доменам), тож обійти підміною відповіді не вийде.

## Обробка помилок

`embed()` (лоадер) **завжди** доступний (публічний, без CORS-гейту), тож він сам
показує акуратну модалку у разі проблеми, а проміс операції відхиляється з кодом:

```js
signer.sign(data)
  .then(({ signature }) => { /* ... */ })
  .catch(e => {
    if (e.code === 'not_licensed') return;  // ліцензія неактивна — модалка вже показана
    if (e.code === 'load_failed')  return;  // не вдалося завантажити — модалка вже показана
    showError('Не вдалося підписати: ' + e.message);  // інші помилки (скасовано, невірний пароль тощо)
  });
```

- `not_licensed` — на домені немає активної підписки/тріалу; лоадер показує табличку
  з кнопкою «Оформити підписку».
- `load_failed` — мережа/збій завантаження; показано табличку «Не вдалося завантажити».

## Мережа з коробки

TSP (мітки часу), OCSP і CRL ходять через вбудований проксі `dstucrypt.io`
(українські ЦСК часто без CORS) з автоматичним фолбеком між кількома TSP‑серверами.
Інтегратору нічого не налаштовувати. За потреби — `signer.configure({ tspUrl, proxyUrl, online })`.

## Модель безпеки (стисло)

- Приватний ключ і пароль вводяться **всередині нашого iframe** і не потрапляють ні
  в код сторінки‑хоста (навіть за XSS), ні на сервери.
- Ізоляція реальна лише тому, що iframe завантажується з **іншого origin**
  (`dstucrypt.io`), ніж сайт‑хост.
- Збережений ключ прив'язаний до домену‑хоста; інші сайти його не дістають.

## Часті питання (FAQ)

**Чи потрапляє ключ/пароль на сервер?** Ні — уся криптографія у браузері (WASM), у
нашому iframe; на сайт‑хост і на наші сервери ключ/пароль не передаються.

**Чи треба щось хостити в себе?** Ні. Робите один `import` з
`https://dstucrypt.io/embed/dstucrypt-embed.mjs`; усі файли й ядро вантажаться з
нашого origin.

**Чи можна писати URL файлів вручну / self-host?** Ні. SDK будує адресу віджета сам
(з `import.meta.url`). Self-host заборонений умовами (ізоляція ключа працює лише з
нашого origin).

**Як відрізнити КЕП від просто дійсного підпису?** За полем `accredited`: воно є і в
результаті `sign()`, і в кожного `signer` у результаті `verify()` (`true` — сертифікат
від акредитованого надавача, тобто КЕП; `false` — підпис дійсний, але не КЕП; `null` —
дані довіри недоступні). Коли рішення ухвалює ваш бекенд — беріть авторитетний
серверний `POST /api/verify` (той самий `accredited`/`qualified`, але нативним ядром).

**Які формати підпису?** CAdES (BES/T/C/XL), PAdES (для PDF), XAdES (для XML), ASiC‑S/E.

**Файлові ключі?** Підтримуються файлові контейнери (PKCS#12/PFX, JKS, PKCS#8,
Key-6.dat тощо). Апаратні токени не підтримуються.

**Скільки коштує / як оплатити?** Див. розділ «Ліцензування». 7 днів безкоштовно на
домен, далі 4 500 грн/міс або 38 880 грн/рік; опція дизайну +1 200/+10 368. Оплата LiqPay
на `/buy`.

**Що буде на неоплаченому домені?** Після 7‑денного тріалу віджет показує заглушку
«Оформити підписку», а `sign()` відхиляється з `code: 'not_licensed'`.

**Чи можна прибрати бейдж «Захищено DSTUcrypt»?** Так — `branding:false`, але це
частина платної опції «Персональний дизайн» (у тріалі доступно).

**Як зробити тихий підпис без повторного введення?** `session: { ttlMinutes: N }` —
після одного розблокування підпис іде без PIN/пароля протягом N хвилин у межах вкладки.

**Як запам'ятати ключ, але пароль питати щоразу?** `session: { mode: 'password' }`.

## Типові помилки інтеграції (чого НЕ робити)

- Не передавайте у віджет URL файлів чи `src` (крім локальної розробки) — SDK робить
  це сам.
- Не намагайтеся читати ключ/пароль зі сторінки‑хоста — вони в іншому origin (iframe),
  недоступні за Same-Origin Policy (це навмисно).
- Не хостіть файли віджетів і SDK у себе — це порушує умови використання й ламає origin-ізоляцію ключа.
- Не показуйте власний `alert` для кодів `not_licensed` / `load_failed` — лоадер уже
  показує коректну модалку; ваш алерт дублюватиме.
- Не довіряйте `subject` з відповіді `auth.login()` для авторизації — довіряйте лише
  серверному `POST /api/auth/verify`.

## Реквізити / контакти

Правовласник: **ТОВ «електронний Обіг»** (ЄДРПОУ 46191111), 04077, Київ,
вул. Дніпроводська, 1а, оф. 1. Пошта: `sale@dstucrypt.com.ua`.
Правові документи: `/offer` (оферта), `/terms` (умови), `/refund` (повернення).
Повна документація для людей: `https://dstucrypt.com.ua/docs/start.html`.
