---
url: 'https://www.corbado.com/ru/blog/kak-sozdat-verifiable-credential-issuer'
title: 'Как создать эмитент цифровых удостоверений (руководство для разработчиков)'
description: 'Узнайте, как создать эмитент верифицируемых удостоверений W3C с использованием протокола OpenID4VCI. В этом пошаговом руководстве мы покажем, как создать приложение на Next.js, которое выпускает криптографически подписанные удостоверения, совместимые с ци'
lang: 'ru'
author: 'Amine'
date: '2025-08-20T15:39:11.528Z'
lastModified: '2026-03-25T10:07:13.830Z'
keywords: 'эмитент цифровых удостоверений, руководство по созданию эмитента, создать эмитент'
category: 'Digital Credentials'
---

# Как создать эмитент цифровых удостоверений (руководство для разработчиков)

## 1. Введение

Цифровые удостоверения — это мощный способ подтверждения личности и заявлений безопасным и
конфиденциальным образом. Но как пользователи получают эти удостоверения? Именно здесь
решающую роль играет **эмитент**. Эмитент — это доверенная организация, например
[государственное учреждение](https://www.corbado.com/passkeys-for-public-sector), университет или банк, которая
отвечает за создание и выдачу пользователям удостоверений с цифровой подписью.

Это руководство представляет собой подробное пошаговое пособие по созданию эмитента
цифровых удостоверений. Мы сосредоточимся на протоколе **OpenID for Verifiable Credential
Issuance (OpenID4VCI)** — современном стандарте, который определяет, как пользователи
могут получать удостоверения от эмитента и безопасно хранить их в своих цифровых
кошельках.

В результате мы получим работающее приложение на [Next.js](https://www.corbado.com/blog/nextjs-passkeys), которое
сможет:

1. Принимать данные пользователя через простую веб-форму.
2. Генерировать безопасное одноразовое предложение удостоверения (credential offer).
3. Отображать предложение в виде QR-кода, который пользователь может отсканировать своим
   мобильным кошельком.
4. Выпускать криптографически подписанное удостоверение, которое пользователь может
   сохранить и предъявлять для проверки.

### 1.1. Разбираемся в терминологии: цифровые и верифицируемые удостоверения

Прежде чем продолжить, важно прояснить различие между двумя связанными, но разными
понятиями:

- **Цифровые удостоверения (общий термин):** Это широкая категория, которая охватывает
  любую цифровую форму удостоверений, сертификатов или аттестаций. Это могут быть простые
  цифровые сертификаты, базовые цифровые значки или любые удостоверения, хранящиеся в
  электронном виде, которые могут иметь или не иметь криптографических средств защиты.

- **Верифицируемые удостоверения (VC — стандарт W3C):** Это особый тип цифровых
  удостоверений, который соответствует стандарту W3C
  [Verifiable Credentials](https://www.corbado.com/glossary/microcredentials) Data Model. Верифицируемые
  удостоверения — это криптографически подписанные, защищенные от подделки и сохраняющие
  конфиденциальность удостоверения, которые можно проверить независимо. Они включают в
  себя особые технические требования, такие как:
    - Криптографические подписи для подлинности и целостности
    - Стандартизированная модель данных и форматы
    - Механизмы представления, сохраняющие конфиденциальность
    - Совместимые протоколы верификации

**В этом руководстве мы создаем именно эмитент верифицируемых удостоверений**, который
соответствует стандарту W3C, а не просто любую систему цифровых удостоверений. Протокол
[OpenID4VCI](https://www.corbado.com/glossary/openid4vci), который мы используем, специально разработан для
выпуска верифицируемых удостоверений, а формат JWT-VC, который мы будем реализовывать,
является совместимым со стандартом W3C форматом для верифицируемых удостоверений.

### 1.2. Как это работает

Магия цифровых удостоверений заключается в простой, но мощной модели **«треугольника
доверия»**, в которой участвуют три ключевых игрока:

- **Эмитент:** Доверенная организация (например,
  [государственное учреждение](https://www.corbado.com/passkeys-for-public-sector), университет или банк),
  которая криптографически подписывает и выдает удостоверение пользователю. **Именно эту
  роль мы и создаем в данном руководстве.**
- **Держатель:** Пользователь, который получает удостоверение и безопасно хранит его в
  личном цифровом кошельке на своем устройстве.
- **Верификатор:** Приложение или сервис, которому необходимо проверить удостоверение
  пользователя.

![Экосистема верифицируемых удостоверений W3C](https://www.w3.org/TR/vc-data-model/diagrams/ecosystem.svg)

Процесс выпуска — это первый шаг в этой экосистеме. Эмитент проверяет информацию
пользователя и предоставляет ему удостоверение. Как только держатель получает это
удостоверение в свой кошелек, он может предъявить его верификатору, чтобы подтвердить свою
личность или заявления, замыкая треугольник.

Вот краткий обзор работы готового приложения:

**Шаг 1: Ввод данных пользователя** Пользователь заполняет форму со своими личными
данными, чтобы запросить новое удостоверение.
![Форма ввода данных пользователя](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_1_0733a9e1da.png)

**Шаг 2: Генерация предложения удостоверения** Приложение генерирует безопасное
предложение удостоверения, отображаемое в виде QR-кода и предварительно авторизованного
кода.
![QR-код предложения удостоверения](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_2_3f1881c473.png)

**Шаг 3: Взаимодействие с кошельком** Пользователь сканирует QR-код совместимым кошельком
(например, Sphereon [Wallet](https://www.corbado.com/blog/digital-wallet-assurance)) и вводит PIN-код для
авторизации выпуска.
![Предложение удостоверения в кошельке](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_3_b80d689dfe.png)
![Ввод PIN-кода](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_4_ca8bad8d11.png)

**Шаг 4: Удостоверение выпущено** Кошелек получает и сохраняет только что выпущенное
цифровое удостоверение, готовое к использованию в будущем.
![Подтверждение данных удостоверения](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_5_55b8150597.png)
![Удостоверение добавлено](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_6_7f5ac5745d.png)

## 2. Предварительные требования для создания эмитента

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

### 2.1. Выбор протоколов

Наш эмитент построен на наборе открытых стандартов, которые обеспечивают совместимость
между кошельками и сервисами выпуска. В этом руководстве мы сосредоточимся на следующих:

| Стандарт / Протокол                                               | Описание                                                                                                                                                                                                                                                         |
| :---------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **OpenID4VCI**                                                    | **OpenID for Verifiable Credential Issuance.** Это основной протокол, который мы будем использовать. Он определяет стандартный процесс, с помощью которого пользователь (через свой кошелек) может запросить и получить удостоверение от эмитента.               |
| **[JWT-VC](https://www.w3.org/TR/vc-data-model/#json-web-token)** | **Верифицируемые удостоверения на основе JWT.** Формат удостоверения, которое мы будем выпускать. Это стандарт W3C, который кодирует верифицируемые удостоверения как JSON Web Tokens (JWT), делая их компактными и удобными для использования в вебе.           |
| **[ISO mDoc](https://www.iso.org/standard/69084.html)**           | **ISO/IEC 18013-5.** Международный стандарт для мобильных водительских удостоверений (mDL). Хотя мы выпускаем JWT-VC, _заявления_ (claims) внутри него структурированы так, чтобы быть совместимыми с моделью данных mDoc (например, `eu.europa.ec.eudi.pid.1`). |
| **OAuth 2.0**                                                     | Базовая платформа авторизации, используемая OpenID4VCI. Мы реализуем поток `pre-authorized_code`, который является специфическим типом предоставления прав, разработанным для безопасного и удобного выпуска удостоверений.                                      |

#### 2.1.1. Потоки авторизации: Pre-Authorized и Authorization Code

[OpenID4VCI](https://www.corbado.com/glossary/openid4vci) поддерживает два основных потока авторизации для
выпуска удостоверений:

1. **Поток с предварительно авторизованным кодом (Pre-Authorized Code Flow):** В этом
   потоке эмитент генерирует кратковременный, одноразовый код (`pre-authorized_code`),
   который сразу же доступен пользователю. Кошелек пользователя может затем обменять этот
   код непосредственно на удостоверение. Этот поток идеально подходит для сценариев, когда
   пользователь уже аутентифицирован и находится на веб-сайте эмитента, поскольку он
   обеспечивает бесшовный, мгновенный выпуск без перенаправлений.

2. **Поток с кодом авторизации (Authorization Code Flow):** Это стандартный поток
   [OAuth 2.0](https://www.corbado.com/glossary/oauth2), при котором пользователь перенаправляется на сервер
   авторизации для предоставления согласия. После одобрения сервер отправляет
   `authorization_code` обратно на зарегистрированный `redirect_uri`. Этот поток больше
   подходит для сторонних приложений, которые инициируют процесс выпуска от имени
   пользователя.

**В этом руководстве мы будем использовать поток `pre-authorized_code`.** Мы выбрали этот
подход, потому что он проще и обеспечивает более прямой пользовательский опыт для нашего
конкретного случая: пользователь напрямую запрашивает удостоверение с собственного
веб-сайта эмитента. Это устраняет необходимость в сложных перенаправлениях и регистрации
клиента, делая основную логику выпуска проще для понимания и реализации.

Эта комбинация стандартов позволяет нам создать эмитент, совместимый с широким спектром
цифровых кошельков и обеспечивающий безопасный, стандартизированный процесс для
пользователя.

### 2.2. Выбор технологического стека

Для создания нашего эмитента мы будем использовать тот же надежный и современный
технологический стек, который мы использовали для верификатора, обеспечивая
последовательный и высококачественный опыт для разработчиков.

#### 2.2.1. Язык: TypeScript

Мы будем использовать **TypeScript** как для фронтенда, так и для бэкенда. Его статическая
типизация неоценима в критически важном для безопасности приложении, таком как эмитент,
поскольку она помогает предотвратить распространенные ошибки и улучшает общее качество и
поддерживаемость кода.

#### 2.2.2. Фреймворк: Next.js

**Next.js** — наш выбор фреймворка, потому что он обеспечивает бесшовный, интегрированный
опыт для создания [full-stack](https://www.corbado.com/ru/blog/crud-prilozhenie-react-express-mysql) приложений.

- **Для фронтенда:** Мы будем использовать [Next.js](https://www.corbado.com/blog/nextjs-passkeys) с
  [React](https://www.corbado.com/blog/react-passkeys) для создания пользовательского интерфейса, где
  пользователи могут вводить свои данные для запроса удостоверения.
- **Для бэкенда:** Мы воспользуемся **Next.js API Routes** для создания серверных
  эндпоинтов, которые обрабатывают поток [OpenID4VCI](https://www.corbado.com/glossary/openid4vci), от генерации
  предложений удостоверений до выпуска окончательного подписанного удостоверения.

#### 2.2.3. Ключевые библиотеки

Наша реализация будет опираться на несколько ключевых библиотек для решения конкретных
задач:

- **next**, **react** и **react-dom**: Основные библиотеки для нашего приложения
  [Next.js](https://www.corbado.com/blog/nextjs-passkeys).
- **mysql2**: Клиент [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) для
  [Node.js](https://www.corbado.com/blog/nodejs-passkeys), используемый для хранения кодов авторизации и данных
  сессий.
- **uuid**: Библиотека для генерации уникальных идентификаторов, которую мы будем
  использовать для создания значений `pre-authorized_code`.
- **jose**: Надежная библиотека для работы с JSON Web Signatures (JWS), которую мы будем
  использовать для криптографической подписи выпускаемых нами удостоверений.

### 2.3. Установите тестовый кошелек

Для тестирования вашего эмитента вам понадобится мобильный кошелек, поддерживающий
протокол OpenID4VCI. Для этого руководства мы рекомендуем **Sphereon Wallet**, который
доступен как для [Android](https://www.corbado.com/blog/how-to-enable-passkeys-android), так и для
[iOS](https://www.corbado.com/blog/how-to-enable-passkeys-ios).

**Как установить Sphereon Wallet:**

1. **Загрузите кошелек** из
   [Google Play Store](https://play.google.com/store/apps/details?id=com.sphereon.ssi.wallet)
   или [Apple App Store](https://apps.apple.com/us/app/sphereon-wallet/id1661096796).
2. Установите приложение на свое мобильное устройство.
3. После установки кошелек готов к приему предложений удостоверений путем сканирования
   QR-кода.

### 2.4. Знания в области криптографии

Выпуск удостоверения — это критически важная для безопасности операция, которая опирается
на фундаментальные криптографические концепции для обеспечения доверия и подлинности.

#### 2.4.1. Цифровые подписи

По своей сути, верифицируемое удостоверение — это набор заявлений, который был **подписан
цифровой подписью** эмитента. Эта подпись предоставляет две гарантии:

- **Подлинность:** Она доказывает, что удостоверение было создано легитимным эмитентом.
- **Целостность:** Она доказывает, что удостоверение не было изменено с момента его
  выпуска.

#### 2.4.2. Криптография с открытым/закрытым ключом

Цифровые подписи создаются с использованием криптографии с открытым и закрытым ключами.
Вот как это работает:

1. **У эмитента есть пара ключей:** **закрытый ключ**, который хранится в секрете и
   безопасности, и соответствующий **открытый ключ**, который делается общедоступным.
2. **Подписание:** Когда эмитент создает удостоверение, он использует свой **закрытый
   ключ** для генерации уникальной цифровой подписи для данных удостоверения.
3. **Верификация:** Верификатор может позже использовать **открытый ключ** эмитента для
   проверки подписи. Если проверка проходит успешно, верификатор знает, что удостоверение
   подлинное и не было изменено.

В нашей реализации мы сгенерируем пару ключей на эллиптических кривых (EC) и будем
использовать алгоритм `ES256` для подписи JWT-VC. Открытый ключ встроен в DID эмитента
(`did:web`), что позволяет любому верификатору обнаружить его и проверить подпись
удостоверения. **Примечание:** Заявление `aud` (audience) намеренно опущено в наших JWT,
поскольку удостоверение предназначено для общего использования и не привязано к
конкретному кошельку. Если вы хотите ограничить использование определенной аудиторией,
включите заявление `aud` и установите его соответствующим образом.

## 3. Обзор архитектуры

Наше приложение-эмитент построено как
[full-stack](https://www.corbado.com/ru/blog/crud-prilozhenie-react-express-mysql) проект на Next.js с четким
разделением между логикой фронтенда и бэкенда. Эта архитектура позволяет нам создать
бесшовный пользовательский опыт, обрабатывая все критически важные для безопасности
операции на сервере. **Важно:** Включенные в SQL таблицы `verification_sessions` и
`verified_credentials` не требуются для этого эмитента, но добавлены для полноты картины.

- **Фронтенд (`src/app/issue/page.tsx`):** Одна страница на [React](https://www.corbado.com/blog/react-passkeys),
  которая позволяет пользователям вводить свои данные для запроса удостоверения. Она
  делает API-вызовы на наш бэкенд для инициации процесса выпуска.
- **Бэкенд API Routes (`src/app/api/issue/...`):** Набор серверных эндпоинтов, которые
  реализуют протокол OpenID4VCI.
    - `/.well-known/openid-credential-issuer`: Публичный эндпоинт метаданных. Это первый
      URL, который кошелек проверит, чтобы узнать о возможностях эмитента, включая его
      сервер авторизации, эндпоинт токенов, эндпоинт удостоверений и типы предлагаемых
      удостоверений.
    - `/.well-known/openid-configuration`: Стандартный эндпоинт обнаружения OpenID
      Connect. Хотя он тесно связан с предыдущим, этот эндпоинт предоставляет более
      широкую конфигурацию, связанную с OIDC, и часто требуется для совместимости со
      стандартными клиентами OpenID.
    - `/.well-known/did.json`: DID-документ нашего эмитента. При использовании метода
      `did:web` этот файл используется для публикации открытых ключей эмитента, которые
      верификаторы могут использовать для проверки подписей выпускаемых им удостоверений.
    - `authorize/route.ts`: Создает `pre-authorized_code` и предложение удостоверения.
    - `token/route.ts`: Обменивает `pre-authorized_code` на токен доступа.
    - `credential/route.ts`: Выпускает окончательное, криптографически подписанное JWT-VC.
    - `schemas/pid/route.ts`: Предоставляет JSON-схему для удостоверения PID. Это
      позволяет любому потребителю удостоверения понять его структуру и типы данных.
- **Библиотека (`src/lib/`):**
    - `database.ts`: Управляет всеми взаимодействиями с базой данных, такими как хранение
      кодов авторизации и ключей эмитента.
    - `crypto.ts`: Обрабатывает все криптографические операции, включая генерацию ключей и
      подписание JWT.

Вот диаграмма, иллюстрирующая процесс выпуска:

![Схема процесса выпуска цифрового удостоверения](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/Mermaid_Chart_Create_complex_visual_diagrams_with_text_A_smarter_way_of_creating_diagrams_2025_07_29_145228_d28fd13731.svg)

## 4. Создание эмитента

Теперь, когда у нас есть четкое понимание стандартов, протоколов и архитектуры, мы можем
приступить к созданию нашего эмитента.

> **Следуйте инструкциям или используйте готовый код**
>
> Сейчас мы пошагово пройдем через настройку и реализацию кода. Если вы предпочитаете
> сразу перейти к готовому продукту, вы можете склонировать полный проект из нашего
> репозитория на GitHub и запустить его локально.
>
> ```bash
> git clone https://github.com/corbado/digital-credentials-example.git
> ```

### 4.1. Настройка проекта

Сначала мы инициализируем новый проект Next.js, установим необходимые зависимости и
запустим нашу базу данных.

#### 4.1.1. Инициализация приложения Next.js

Откройте терминал, перейдите в каталог, где вы хотите создать свой проект, и выполните
следующую команду. Мы используем App Router, TypeScript и Tailwind CSS для этого проекта.

```bash
npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm
```

Эта команда создает новый скелет приложения Next.js в вашем текущем каталоге.

#### 4.1.2. Установка зависимостей

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

```bash
npm install jose mysql2 uuid @types/uuid
```

Эта команда устанавливает:

- `jose`: для подписи и проверки JSON Web Tokens (JWT).
- `mysql2`: клиент [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) для нашей базы данных.
- `uuid`: для генерации уникальных строк вызова (challenge strings).
- `@types/uuid`: типы TypeScript для библиотеки `uuid`.

#### 4.1.3. Запуск базы данных

Нашему бэкенду требуется база данных [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) для
хранения кодов авторизации, сессий выпуска и ключей эмитента. Мы включили файл
`docker-compose.yml`, чтобы упростить этот процесс.

Если вы склонировали репозиторий, вы можете просто выполнить `docker-compose up -d`. Если
вы создаете проект с нуля, создайте файл с именем `docker-compose.yml` со следующим
содержимым:

```yaml
services:
    mysql:
        image: mysql:8.0
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: rootpassword
            MYSQL_DATABASE: digital_credentials
            MYSQL_USER: app_user
            MYSQL_PASSWORD: app_password
        ports:
            - "3306:3306"
        volumes:
            - mysql_data:/var/lib/mysql
            - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql
        healthcheck:
            test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
            timeout: 20s
            retries: 10

volumes:
    mysql_data:
```

Эта настройка Docker Compose также требует скрипта инициализации SQL. Создайте каталог с
именем `sql` и внутри него файл с именем `init.sql` со следующим содержимым для настройки
необходимых таблиц как для верификатора, так и для эмитента:

```sql
-- Create database if not exists
CREATE DATABASE IF NOT EXISTS digital_credentials;
USE digital_credentials;

-- Table for storing challenges
CREATE TABLE IF NOT EXISTS challenges (
    id VARCHAR(36) PRIMARY KEY,
    challenge VARCHAR(255) NOT NULL UNIQUE,
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    used BOOLEAN DEFAULT FALSE,
    INDEX idx_challenge (challenge),
    INDEX idx_expires_at (expires_at)
);

-- Table for storing verification sessions
CREATE TABLE IF NOT EXISTS verification_sessions (
    id VARCHAR(36) PRIMARY KEY,
    challenge_id VARCHAR(36),
    status ENUM('pending', 'verified', 'failed', 'expired') DEFAULT 'pending',
    presentation_data JSON,
    verified_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE,
    INDEX idx_challenge_id (challenge_id),
    INDEX idx_status (status)
);

-- Table for storing verified credentials data (optional)
CREATE TABLE IF NOT EXISTS verified_credentials (
    id VARCHAR(36) PRIMARY KEY,
    session_id VARCHAR(36),
    credential_type VARCHAR(255),
    issuer VARCHAR(255),
    subject VARCHAR(255),
    claims JSON,
    verified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (session_id) REFERENCES verification_sessions(id) ON DELETE CASCADE,
    INDEX idx_session_id (session_id),
    INDEX idx_credential_type (credential_type)
);

-- ISSUER TABLES

-- Table for storing authorization codes in OpenID4VCI flow
CREATE TABLE IF NOT EXISTS authorization_codes (
    id VARCHAR(36) PRIMARY KEY,
    code VARCHAR(255) NOT NULL UNIQUE,
    client_id VARCHAR(255),
    scope VARCHAR(255),
    code_challenge VARCHAR(255),
    code_challenge_method VARCHAR(50),
    redirect_uri TEXT,
    user_pin VARCHAR(10),
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    used BOOLEAN DEFAULT FALSE,
    INDEX idx_code (code),
    INDEX idx_expires_at (expires_at)
);

-- Table for storing issuance sessions
CREATE TABLE IF NOT EXISTS issuance_sessions (
    id VARCHAR(36) PRIMARY KEY,
    authorization_code_id VARCHAR(36),
    access_token VARCHAR(255),
    token_type VARCHAR(50) DEFAULT 'Bearer',
    expires_in INT DEFAULT 3600,
    c_nonce VARCHAR(255),
    c_nonce_expires_at TIMESTAMP,
    status ENUM('pending', 'authorized', 'credential_issued', 'expired', 'failed') DEFAULT 'pending',
    user_data JSON,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (authorization_code_id) REFERENCES authorization_codes(id) ON DELETE CASCADE,
    INDEX idx_access_token (access_token),
    INDEX idx_c_nonce (c_nonce),
    INDEX idx_status (status)
);

-- Table for storing issued credentials
CREATE TABLE IF NOT EXISTS issued_credentials (
    id VARCHAR(36) PRIMARY KEY,
    session_id VARCHAR(36),
    credential_id VARCHAR(255),
    credential_type VARCHAR(255) DEFAULT 'jwt_vc',
    doctype VARCHAR(255) DEFAULT 'eu.europa.ec.eudi.pid.1',
    credential_data LONGTEXT, -- Base64 encoded mDoc
    credential_claims JSON,
    issuer_did VARCHAR(255),
    subject_id VARCHAR(255),
    issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMP,
    revoked BOOLEAN DEFAULT FALSE,
    revoked_at TIMESTAMP NULL,
    FOREIGN KEY (session_id) REFERENCES issuance_sessions(id) ON DELETE CASCADE,
    INDEX idx_credential_id (credential_id),
    INDEX idx_session_id (session_id),
    INDEX idx_doctype (doctype),
    INDEX idx_subject_id (subject_id),
    INDEX idx_issued_at (issued_at)
);

-- Table for storing issuer keys (simplified for demo)
CREATE TABLE IF NOT EXISTS issuer_keys (
    id VARCHAR(36) PRIMARY KEY,
    key_id VARCHAR(255) NOT NULL UNIQUE,
    key_type VARCHAR(50) NOT NULL, -- 'EC', 'RSA'
    algorithm VARCHAR(50) NOT NULL, -- 'ES256', 'RS256', etc.
    public_key TEXT NOT NULL, -- JWK format
    private_key TEXT NOT NULL, -- JWK format (encrypted in production)
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_key_id (key_id),
    INDEX idx_is_active (is_active)
);
```

Когда оба файла будут на месте, откройте терминал в корне проекта и выполните:

```bash
docker-compose up -d
```

Эта команда запустит контейнер MySQL в фоновом режиме, готовый к использованию нашим
приложением.

### 4.2. Реализация общих библиотек

Прежде чем создавать API-эндпоинты, давайте создадим общие библиотеки, которые будут
обрабатывать основную бизнес-логику. Такой подход позволяет нам держать наши API-маршруты
чистыми и сфокусированными на обработке HTTP-запросов, в то время как сложная работа
делегируется этим модулям.

#### 4.2.1. Библиотека базы данных (`src/lib/database.ts`)

Этот файл является единственным источником истины для всех взаимодействий с базой данных.
Он использует библиотеку `mysql2` для подключения к нашему контейнеру MySQL и
предоставляет набор экспортируемых функций для создания, чтения и обновления записей в
наших таблицах. Этот уровень абстракции делает наш код более модульным и простым в
обслуживании.

Создайте файл `src/lib/database.ts` со следующим содержимым:

```typescript
// src/lib/database.ts
import mysql from "mysql2/promise";

// Database connection configuration
const dbConfig = {
    host: process.env.DATABASE_HOST || "localhost",
    port: parseInt(process.env.DATABASE_PORT || "3306"),
    user: process.env.DATABASE_USER || "app_user",
    password: process.env.DATABASE_PASSWORD || "app_password",
    database: process.env.DATABASE_NAME || "digital_credentials",
    timezone: "+00:00",
};

let connection: mysql.Connection | null = null;

export async function getConnection(): Promise<mysql.Connection> {
    if (!connection) {
        connection = await mysql.createConnection(dbConfig);
    }
    return connection;
}

// Data-Access-Object (DAO) functions for each table
// ... (e.g., createChallenge, getChallenge, createAuthorizationCode, etc.)
```

> **Примечание:** Для краткости полный список функций DAO был опущен. Вы можете найти
> полный код в
> [репозитории проекта](https://github.com/corbado/digital-credentials-example/blob/main/src/lib/database.ts).
> Этот файл включает функции для управления challenges, сессиями верификации, кодами
> авторизации, сессиями выпуска и ключами эмитента.

#### 4.2.2. Криптографическая библиотека (`src/lib/crypto.ts`)

Этот файл обрабатывает все критически важные для безопасности криптографические операции.
Он использует библиотеку `jose` для генерации пар ключей и подписи JSON Web Tokens (JWT).

**Генерация ключей** Функция `generateIssuerKeyPair` создает новую пару ключей на
эллиптических кривых, которая будет использоваться для подписи удостоверений. Открытый
ключ экспортируется в формате JSON Web Key (JWK), чтобы его можно было опубликовать в
нашем документе `did.json`.

```typescript
// src/lib/crypto.ts
import { generateKeyPair, exportJWK, SignJWT } from "jose";

export async function generateIssuerKeyPair(keyId: string, issuerDid: string) {
    const { publicKey, privateKey } = await generateKeyPair("ES256", {
        crv: "P-256",
        extractable: true,
    });

    const publicKeyJWK = await exportJWK(publicKey);
    publicKeyJWK.kid = keyId; // Assign a unique key ID

    // ... (private key export and other setup)

    return { publicKey, privateKey, publicKeyJWK /* ... */ };
}
```

**Создание JWT-удостоверения** Функция `createJWTVerifiableCredential` является ядром
процесса выпуска. Она принимает заявления пользователя, пару ключей эмитента и другие
метаданные и использует их для создания подписанного JWT-VC.

```typescript
// src/lib/crypto.ts

export async function createJWTVerifiableCredential(
    claims: MDocClaims,
    issuerKeyPair: IssuerKeyPair,
    subjectId: string,
    audience: string,
): Promise<string> {
    const now = Math.floor(Date.now() / 1000);
    const oneYear = 365 * 24 * 60 * 60;

    const vcPayload = {
        // DID эмитента
        iss: issuerKeyPair.issuerDid,
        // DID субъекта (держателя)
        sub: subjectId,
        // Время выпуска удостоверения (iat) и время истечения срока его действия (exp)
        iat: now,
        exp: now + oneYear,
        // Модель данных верифицируемого удостоверения
        vc: {
            "@context": [
                "https://www.w3.org/2018/credentials/v1",
                "https://europa.eu/eudi/pid/v1",
            ],
            type: ["VerifiableCredential", "eu.europa.ec.eudi.pid.1"],
            issuer: issuerKeyPair.issuerDid,
            issuanceDate: new Date(now * 1000).toISOString(),
            credentialSubject: {
                id: subjectId,
                ...claims,
            },
        },
    };

    // Подписываем полезную нагрузку закрытым ключом эмитента
    return await new SignJWT(vcPayload)
        .setProtectedHeader({
            alg: issuerKeyPair.algorithm,
            kid: issuerKeyPair.keyId,
            typ: "JWT",
        })
        .sign(issuerKeyPair.privateKey);
}
```

Эта функция конструирует полезную нагрузку JWT в соответствии с моделью данных W3C
[Verifiable Credentials](https://www.corbado.com/glossary/microcredentials) и подписывает ее закрытым ключом
эмитента, создавая безопасное и верифицируемое удостоверение.

### 4.2. Архитектурный обзор приложения Next.js

Наше приложение Next.js структурировано таким образом, чтобы разделить обязанности между
фронтендом и бэкендом, хотя они и являются частью одного проекта. Это достигается за счет
использования App Router как для страниц UI, так и для API-эндпоинтов.

- **Фронтенд (`src/app/issue/page.tsx`):** Один компонент страницы
  [React](https://www.corbado.com/blog/react-passkeys), который определяет UI для маршрута `/issue`. Он
  обрабатывает ввод пользователя и взаимодействует с нашим бэкенд-API.

- **Бэкенд API Routes (`src/app/api/...`):**
    - **Обнаружение (`.well-known/.../route.ts`):** Эти маршруты предоставляют публичные
      эндпоинты метаданных, которые позволяют кошелькам и другим клиентам обнаруживать
      возможности эмитента и его открытые ключи.
    - **Выпуск (`issue/.../route.ts`):** Эти эндпоинты реализуют основную логику
      OpenID4VCI, включая создание предложений удостоверений, выпуск токенов и подписание
      окончательного удостоверения.
    - **Схема (`schemas/pid/route.ts`):** Этот маршрут предоставляет JSON-схему для
      удостоверения, определяя его структуру.

- **Библиотека (`src/lib/`):** Этот каталог содержит переиспользуемую логику, общую для
  всего бэкенда.
    - `database.ts`: Управляет всеми взаимодействиями с базой данных, абстрагируя
      SQL-запросы.
    - `crypto.ts`: Обрабатывает все криптографические операции, такие как генерация ключей
      и подписание JWT.

Такое четкое разделение делает приложение модульным и более простым в обслуживании.

**Примечание:** Функция `generateIssuerDid()` должна возвращать действительный `did:web`,
соответствующий вашему домену эмитента. При развертывании файл `.well-known/did.json`
должен обслуживаться по протоколу HTTPS на этом домене, чтобы верификаторы могли проверять
удостоверения.

![Архитектурный обзор приложения Next.js](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/Mermaid_Chart_Create_complex_visual_diagrams_with_text_A_smarter_way_of_creating_diagrams_2025_07_29_151549_6a0aca6477.svg)

### 4.3. Создание фронтенда

Наш фронтенд представляет собой одну страницу на React, которая предоставляет простую
форму для запроса нового цифрового удостоверения пользователями. Его обязанности:

- Собирать данные пользователя (имя, дата рождения и т. д.).
- Отправлять эти данные на наш бэкенд для создания предложения удостоверения.
- Отображать полученный QR-код и PIN-код для сканирования пользователем с помощью своего
  кошелька.

Основная логика обрабатывается в функции `handleSubmit`, которая срабатывает, когда
пользователь отправляет форму.

```typescript
// src/app/issue/page.tsx

const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    setCredentialOffer(null);

    try {
        // 1. Проверка обязательных полей
        if (!userData.given_name || !userData.family_name || !userData.birth_date) {
            throw new Error("Please fill in all required fields");
        }

        // 2. Запрос предложения удостоверения с бэкенда
        const response = await fetch("/api/issue/authorize", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                user_data: userData,
            }),
        });

        if (!response.ok) {
            const errorData = await response.json();
            throw new Error(
                errorData.error_description || "Failed to create credential offer",
            );
        }

        // 3. Установка предложения удостоверения в состояние для отображения QR-кода
        const result = await response.json();
        setCredentialOffer(result);
    } catch (err) {
        const errorMessage = (err as Error).message || "Unknown error occurred";
        setError(errorMessage);
    } finally {
        setLoading(false);
    }
};
```

Эта функция выполняет три ключевых действия:

1. **Проверяет данные формы**, чтобы убедиться, что все обязательные поля заполнены.
2. **Отправляет `POST`-запрос** на наш эндпоинт `/api/issue/authorize` с данными
   пользователя.
3. **Обновляет состояние компонента** с предложением удостоверения, полученным с бэкенда,
   что заставляет UI отобразить QR-код и код транзакции.

Остальная часть файла содержит стандартный код React для рендеринга формы и отображения
QR-кода. Вы можете просмотреть полный файл в
[репозитории проекта](https://github.com/corbado/digital-credentials-example/blob/main/src/app/issue/page.tsx).

### 4.4. Настройка окружения и обнаружения

Прежде чем мы создадим бэкенд-API, нам нужно настроить наше окружение и эндпоинты
обнаружения. Эти файлы `.well-known` имеют решающее значение для того, чтобы кошельки
могли найти нашего эмитента и понять, как с ним взаимодействовать.

#### 4.4.1. Создание файла окружения

Создайте файл с именем `.env.local` в корне вашего проекта и добавьте следующую строку.
Этот URL должен быть общедоступным, чтобы мобильный кошелек мог его достичь. Для локальной
разработки вы можете использовать сервис туннелирования, такой как
[ngrok](https://www.corbado.com/blog/multi-device-passkey-login-corbado-ngrok), чтобы открыть доступ к вашему
`localhost`.

```
NEXT_PUBLIC_BASE_URL=http://localhost:3000
```

#### 4.4.2. Реализация эндпоинтов обнаружения

Кошельки обнаруживают возможности эмитента, запрашивая стандартные URL-адреса
`.well-known`. Нам нужно создать три таких эндпоинта.

**1. Метаданные эмитента (`/.well-known/openid-credential-issuer`)**

Это основной файл обнаружения для OpenID4VCI. Он сообщает кошельку все, что ему нужно
знать об эмитенте, включая его эндпоинты, типы предлагаемых удостоверений и поддерживаемые
криптографические алгоритмы.

Создайте файл `src/app/.well-known/openid-credential-issuer/route.ts`:

```typescript
// src/app/.well-known/openid-credential-issuer/route.ts
import { NextResponse } from "next/server";

export async function GET() {
    const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";

    const issuerMetadata = {
        // Уникальный идентификатор эмитента.
        issuer: baseUrl,
        // URL сервера авторизации. Для простоты наш эмитент является своим собственным сервером авторизации.
        authorization_servers: [baseUrl],
        // URL эмитента удостоверений.
        credential_issuer: baseUrl,
        // Эндпоинт, на который кошелек отправит POST-запрос для получения фактического удостоверения.
        credential_endpoint: `${baseUrl}/api/issue/credential`,
        // Эндпоинт, где кошелек обменивает код авторизации на токен доступа.
        token_endpoint: `${baseUrl}/api/issue/token`,
        // Эндпоинт для потока авторизации (не используется в нашем потоке с предварительной авторизацией, но рекомендуется его включить).
        authorization_endpoint: `${baseUrl}/api/issue/authorize`,
        // Указывает на поддержку потока с предварительно авторизованным кодом без требования аутентификации клиента.
        pre_authorized_grant_anonymous_access_supported: true,
        // Человекочитаемая информация об эмитенте.
        display: [
            {
                name: "Corbado Credentials Issuer",
                locale: "en-US",
            },
        ],
        // Список типов удостоверений, которые может выпускать этот эмитент.
        credential_configurations_supported: {
            "eu.europa.ec.eudi.pid.1": {
                // Формат удостоверения (например, jwt_vc, mso_mdoc).
                format: "jwt_vc",
                // Конкретный тип документа, соответствующий стандартам ISO mDoc.
                doctype: "eu.europa.ec.eudi.pid.1",
                // Область OAuth 2.0, связанная с этим типом удостоверения.
                scope: "eu.europa.ec.eudi.pid.1",
                // Методы, которые кошелек может использовать для подтверждения владения своим ключом.
                cryptographic_binding_methods_supported: ["jwk"],
                // Алгоритмы подписи, поддерживаемые эмитентом для этого удостоверения.
                credential_signing_alg_values_supported: ["ES256"],
                // Типы подтверждения владения, которые может использовать кошелек.
                proof_types_supported: {
                    jwt: {
                        proof_signing_alg_values_supported: ["ES256", "ES384", "ES512"],
                    },
                },
                // Свойства отображения для удостоверения.
                display: [
                    {
                        name: "Corbado Credential Issuer",
                        locale: "en-US",
                        logo: {
                            uri: `${baseUrl}/logo.png`,
                            alt_text: "EU Digital Identity",
                        },
                        background_color: "#003399",
                        text_color: "#FFFFFF",
                    },
                ],
                // Список заявлений (атрибутов) в удостоверении.
                claims: {
                    "eu.europa.ec.eudi.pid.1": {
                        given_name: {
                            mandatory: true,
                            display: [{ name: "Given Name", locale: "en-US" }],
                        },
                        family_name: {
                            mandatory: true,
                            display: [{ name: "Family Name", locale: "en-US" }],
                        },
                        birth_date: {
                            mandatory: true,
                            display: [{ name: "Date of Birth", locale: "en-US" }],
                        },
                    },
                },
            },
        },
        // Методы аутентификации, поддерживаемые эндпоинтом токенов. 'none' означает публичный клиент.
        token_endpoint_auth_methods_supported: ["none"],
        // Поддерживаемые методы вызова PKCE.
        code_challenge_methods_supported: ["S256"],
        // Типы предоставления прав OAuth 2.0, поддерживаемые эмитентом.
        grant_types_supported: [
            "authorization_code",
            "urn:ietf:params:oauth:grant-type:pre-authorized_code",
        ],
    };

    return NextResponse.json(issuerMetadata, {
        headers: {
            "Content-Type": "application/json",
            "Cache-Control": "no-cache, no-store, must-revalidate",
            Pragma: "no-cache",
            Expires: "0",
        },
    });
}
```

**2. Конфигурация OpenID (`/.well-known/openid-configuration`)**

Это стандартный документ обнаружения OIDC, который предоставляет более широкий набор
деталей конфигурации.

Создайте файл `src/app/.well-known/openid-configuration/route.ts`:

```typescript
// src/app/.well-known/openid-configuration/route.ts
import { NextResponse } from "next/server";

export async function GET() {
    const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";

    const openidConfiguration = {
        // Уникальный идентификатор эмитента.
        credential_issuer: baseUrl,
        // Эндпоинт, на который кошелек отправит POST-запрос для получения фактического удостоверения.
        credential_endpoint: `${baseUrl}/api/issue/credential`,
        // Эндпоинт для потока авторизации.
        authorization_endpoint: `${baseUrl}/api/issue/authorize`,
        // Эндпоинт, где кошелек обменивает код авторизации на токен доступа.
        token_endpoint: `${baseUrl}/api/issue/token`,
        // Список типов удостоверений, которые может выпускать этот эмитент.
        credential_configurations_supported: {
            "eu.europa.ec.eudi.pid.1": {
                format: "jwt_vc",
                scope: "eu.europa.ec.eudi.pid.1",
                cryptographic_binding_methods_supported: ["jwk"],
                credential_signing_alg_values_supported: ["ES256", "ES384", "ES512"],
                proof_types_supported: {
                    jwt: {
                        proof_signing_alg_values_supported: ["ES256", "ES384", "ES512"],
                    },
                },
            },
        },
        // Типы предоставления прав OAuth 2.0, поддерживаемые эмитентом.
        grant_types_supported: [
            "authorization_code",
            "urn:ietf:params:oauth:grant-type:pre-authorized_code",
        ],
        // Указывает на поддержку потока с предварительно авторизованным кодом.
        pre_authorized_grant_anonymous_access_supported: true,
        // Поддерживаемые методы вызова PKCE.
        code_challenge_methods_supported: ["S256"],
        // Методы аутентификации, поддерживаемые эндпоинтом токенов.
        token_endpoint_auth_methods_supported: ["none"],
        // Области OAuth 2.0, поддерживаемые эмитентом.
        scopes_supported: ["eu.europa.ec.eudi.pid.1"],
    };

    return NextResponse.json(openidConfiguration, {
        headers: {
            "Content-Type": "application/json",
            "Cache-Control": "no-cache, no-store, must-revalidate",
            Pragma: "no-cache",
            Expires: "0",
        },
    });
}
```

**3. DID-документ (`/.well-known/did.json`)**

Этот файл публикует открытый ключ эмитента с использованием метода `did:web`, позволяя
любому проверить подпись выданных им удостоверений.

Создайте файл `src/app/.well-known/did.json/route.ts`:

```typescript
// src/app/.well-known/did.json/route.ts
import { NextResponse } from "next/server";
import { getActiveIssuerKey } from "../../../lib/database";
import { generateIssuerDid } from "../../../lib/crypto";

export async function GET() {
    const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
    const issuerKey = await getActiveIssuerKey();

    if (!issuerKey) {
        return NextResponse.json(
            { error: "No active issuer key found" },
            { status: 404 },
        );
    }

    const publicKeyJWK = JSON.parse(issuerKey.public_key);
    const didId = generateIssuerDid();
    const didDocument = {
        // Контекст определяет словарь, используемый в документе.
        "@context": [
            "https://www.w3.org/ns/did/v1",
            "https://w3id.org/security/suites/jws-2020/v1",
        ],
        // DID URI, который является уникальным идентификатором эмитента.
        id: didId,
        // Контроллер DID, то есть сущность, которая контролирует DID. Здесь это сам эмитент.
        controller: didId,
        // Список открытых ключей, которые можно использовать для проверки подписей от эмитента.
        verificationMethod: [
            {
                // Уникальный идентификатор ключа в рамках DID.
                id: `${didId}#${issuerKey.key_id}`,
                // Тип ключа.
                type: "JsonWebKey2020",
                // DID контроллера ключа.
                controller: didId,
                // Открытый ключ в формате JWK.
                publicKeyJwk: publicKeyJWK,
            },
        ],
        // Указывает, какие ключи можно использовать для аутентификации (доказательства контроля над DID).
        authentication: [`${didId}#${issuerKey.key_id}`],
        // Указывает, какие ключи можно использовать для создания верифицируемых удостоверений.
        assertionMethod: [`${didId}#${issuerKey.key_id}`],
        // Список сервисов, предоставляемых субъектом DID, таких как эндпоинт эмитента.
        service: [
            {
                id: `${didId}#openid-credential-issuer`,
                type: "OpenIDCredentialIssuer",
                serviceEndpoint: `${baseUrl}/.well-known/openid-credential-issuer`,
            },
        ],
    };

    return NextResponse.json(didDocument, {
        headers: {
            "Content-Type": "application/did+json",
            "Cache-Control": "no-cache, no-store, must-revalidate",
            Pragma: "no-cache",
            Expires: "0",
        },
    });
}
```

> **Почему нет кеширования?** Вы заметите, что все три этих эндпоинта возвращают
> заголовки, которые агрессивно предотвращают кеширование (`Cache-Control: no-cache`,
> `Pragma: no-cache`, `Expires: 0`). Это критически важная практика безопасности для
> документов обнаружения. Конфигурации эмитента могут меняться — например, может быть
> произведена ротация криптографического ключа. Если бы кошелек или клиент закешировал
> старую версию файла `did.json` или `openid-credential-issuer`, он не смог бы проверить
> новые удостоверения или взаимодействовать с обновленными эндпоинтами. Заставляя клиентов
> каждый раз запрашивать свежую копию, мы гарантируем, что у них всегда будет самая
> актуальная информация.

#### 4.4.3. Реализация эндпоинта схемы удостоверения

Последний элемент нашей публичной [инфраструктуры](https://www.corbado.com/passkeys-for-critical-infrastructure)
— это эндпоинт схемы удостоверения. Этот маршрут предоставляет JSON-схему, которая
формально определяет структуру, типы данных и ограничения удостоверения PID, которое мы
выпускаем. Кошельки и верификаторы могут использовать эту схему для проверки содержимого
удостоверения.

Создайте файл `src/app/api/schemas/pid/route.ts` со следующим содержимым:

```typescript
// src/app/api/schemas/pid/route.ts
import { NextResponse } from "next/server";

export async function GET() {
    const schema = {
        $schema: "https://json-schema.org/draft/2020-12/schema",
        $id: "https://example.com/schemas/pid", // Замените на ваш реальный домен
        title: "PID Credential",
        description:
            "A schema for a Verifiable Credential representing a Personal Identification Document (PID).",
        type: "object",
        properties: {
            credentialSubject: {
                type: "object",
                properties: {
                    given_name: { type: "string" },
                    family_name: { type: "string" },
                    birth_date: { type: "string", format: "date" },
                    // ... другие свойства субъекта удостоверения
                },
                required: ["given_name", "family_name", "birth_date"],
            },
            // ... другие свойства верхнего уровня верифицируемого удостоверения
        },
    };

    return NextResponse.json(schema, {
        headers: {
            "Content-Type": "application/schema+json",
            "Access-Control-Allow-Origin": "*", // Разрешить кросс-доменные запросы
        },
    });
}
```

> **Примечание:** JSON-схема для удостоверения PID может быть довольно большой и
> детализированной. Для краткости полная схема была усечена. Вы можете найти полный файл в
> [репозитории проекта](https://github.com/corbado/digital-credentials-example/blob/main/src/app/api/schemas/pid/route.ts).

### 4.5. Создание бэкенд-эндпоинтов

Имея готовый фронтенд, нам теперь нужна серверная логика для обработки потока OpenID4VCI.
Мы начнем с первого эндпоинта, который вызывает фронтенд: `/api/issue/authorize`.

#### 4.5.1. `/api/issue/authorize`: Создание предложения удостоверения

Этот эндпоинт отвечает за получение данных пользователя, генерацию безопасного
одноразового кода и конструирование `credential_offer`, который сможет понять кошелек
пользователя.

Вот основная логика:

```typescript
// src/app/api/issue/authorize/route.ts
import { NextRequest, NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";
import { createAuthorizationCode } from "@/lib/database";

export async function POST(request: NextRequest) {
    try {
        const body = await request.json();
        const { user_data } = body;

        // 1. Проверка данных пользователя
        if (
            !user_data ||
            !user_data.given_name ||
            !user_data.family_name ||
            !user_data.birth_date
        ) {
            return NextResponse.json({ error: "missing_user_data" }, { status: 400 });
        }

        // 2. Генерация предварительно авторизованного кода и PIN-кода
        const code = uuidv4();
        const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 минут
        const txCode = Math.floor(1000 + Math.random() * 9000).toString(); // 4-значный PIN

        // 3. Хранение кода и данных пользователя
        await createAuthorizationCode(uuidv4(), code, expiresAt);
        // Примечание: здесь используется хранилище в памяти только для демонстрационных целей.
        // В продакшене данные следует безопасно хранить в базе данных с правильным сроком действия.
        if (!(global as any).userDataStore) (global as any).userDataStore = new Map();
        (global as any).userDataStore.set(code, user_data);
        if (!(global as any).txCodeStore) (global as any).txCodeStore = new Map();
        (global as any).txCodeStore.set(code, txCode);

        // 4. Создание объекта предложения удостоверения
        const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
        const credentialOffer = {
            // Идентификатор эмитента, который является его базовым URL.
            credential_issuer: baseUrl,
            // Массив типов удостоверений, которые предлагает эмитент.
            credential_configuration_ids: ["eu.europa.ec.eudi.pid.1"],
            // Указывает типы предоставления прав, которые может использовать кошелек.
            grants: {
                // Мы используем поток с предварительно авторизованным кодом.
                "urn:ietf:params:oauth:grant-type:pre-authorized_code": {
                    // Одноразовый код, который кошелек обменяет на токен.
                    "pre-authorized_code": code,
                    // Указывает, что пользователь должен ввести PIN-код (tx_code) для использования кода.
                    user_pin_required: true,
                },
            },
        };

        // 5. Создание полного URI предложения удостоверения (глубокая ссылка для кошельков)
        const credentialOfferUri = `openid-credential-offer://?credential_offer=${encodeURIComponent(
            JSON.stringify(credentialOffer),
        )}`;

        // Окончательный ответ фронтенду.
        return NextResponse.json({
            // Глубокая ссылка для QR-кода.
            credential_offer_uri: credentialOfferUri,
            // Необработанный предварительно авторизованный код для отображения или ручного ввода.
            pre_authorized_code: code,
            // 4-значный PIN, который пользователь должен ввести в своем кошельке.
            tx_code: txCode,
        });
    } catch (error) {
        console.error("Authorization error:", error);
        return NextResponse.json({ error: "server_error" }, { status: 500 });
    }
}
```

Ключевые шаги в этом эндпоинте:

1. **Проверка данных:** Сначала он убеждается, что необходимые данные пользователя
   присутствуют.
2. **Генерация кодов:** Он создает уникальный `pre-authorized_code` (UUID) и 4-значный
   `tx_code` (PIN) для дополнительного уровня безопасности.
3. **Сохранение данных:** `pre-authorized_code` сохраняется в базе данных с коротким
   сроком действия. Данные пользователя и PIN-код хранятся в памяти, связанные с кодом.
4. **Построение предложения:** Он конструирует объект `credential_offer` в соответствии со
   спецификацией OpenID4VCI. Этот объект сообщает кошельку, где находится эмитент, какие
   удостоверения он предлагает и какой код нужен для их получения.
5. **Возврат URI:** Наконец, он создает URI глубокой ссылки
   (`openid-credential-offer://...`) и возвращает его фронтенду вместе с `tx_code` для
   отображения пользователю.

#### 4.5.2. `/api/issue/token`: Обмен кода на токен

Как только пользователь сканирует QR-код и вводит свой PIN-код, кошелек делает
`POST`-запрос на этот эндпоинт. Его задача — проверить `pre-authorized_code` и `user_pin`
(PIN) и, если они действительны, выпустить кратковременный токен доступа.

Создайте файл `src/app/api/issue/token/route.ts` со следующим содержимым:

```typescript
// src/app/api/issue/token/route.ts
import { NextRequest, NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";
import {
    getAuthorizationCode,
    markAuthorizationCodeAsUsed,
    createIssuanceSession,
} from "@/lib/database";

export async function POST(request: NextRequest) {
    try {
        const formData = await request.formData();
        const grant_type = formData.get("grant_type") as string;
        const code = formData.get("pre-authorized_code") as string;
        const user_pin = formData.get("user_pin") as string;

        // 1. Проверка типа предоставления прав
        if (grant_type !== "urn:ietf:params:oauth:grant-type:pre-authorized_code") {
            return NextResponse.json(
                { error: "unsupported_grant_type" },
                { status: 400 },
            );
        }

        // 2. Проверка предварительно авторизованного кода
        const authCode = await getAuthorizationCode(code);
        if (!authCode) {
            return NextResponse.json(
                {
                    error: "invalid_grant",
                    error_description: "Invalid or expired code",
                },
                { status: 400 },
            );
        }

        // 3. Проверка PIN-кода (tx_code)
        const expectedTxCode = (global as any).txCodeStore?.get(code);
        if (expectedTxCode !== user_pin) {
            return NextResponse.json(
                { error: "invalid_grant", error_description: "Invalid PIN" },
                { status: 400 },
            );
        }

        // 4. Генерация токена доступа и c_nonce
        const accessToken = uuidv4();
        const cNonce = uuidv4();
        const cNonceExpiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 минут

        // 5. Создание новой сессии выпуска
        const userData = (global as any).userDataStore?.get(code);
        await createIssuanceSession(
            uuidv4(),
            authCode.id,
            accessToken,
            cNonce,
            cNonceExpiresAt,
            userData,
        );

        // 6. Пометка кода как использованного и очистка временных данных
        await markAuthorizationCodeAsUsed(code);
        (global as any).txCodeStore?.delete(code);
        (global as any).userDataStore?.delete(code);

        // 7. Возврат ответа с токеном доступа
        return NextResponse.json({
            access_token: accessToken,
            token_type: "Bearer",
            expires_in: 3600, // 1 час
            c_nonce: cNonce,
            c_nonce_expires_in: 300, // 5 минут
        });
    } catch (error) {
        console.error("Token endpoint error:", error);
        return NextResponse.json({ error: "server_error" }, { status: 500 });
    }
}
```

Ключевые шаги в этом эндпоинте:

1. **Проверка типа предоставления прав:** Он гарантирует, что кошелек использует
   правильный тип `pre-authorized_code`.
2. **Проверка кода:** Он проверяет, что `pre-authorized_code` существует в базе данных, не
   истек и не был использован ранее.
3. **Проверка PIN-кода:** Он сравнивает `user_pin` из кошелька с `tx_code`, который мы
   сохранили ранее, чтобы убедиться, что пользователь авторизовал транзакцию.
4. **Генерация токенов:** Он создает безопасный `access_token` и `c_nonce` (credential
   nonce) — одноразовое значение для предотвращения атак повторного воспроизведения на
   эндпоинте удостоверений.
5. **Создание сессии:** Он создает новую запись `issuance_sessions` в базе данных,
   связывая токен доступа с данными пользователя.
6. **Пометка кода как использованного:** Чтобы предотвратить повторное использование того
   же предложения, он помечает `pre-authorized_code` как использованный.
7. **Возврат токена:** Он возвращает `access_token` и `c_nonce` кошельку.

#### 4.5.3. `/api/issue/credential`: Выпуск подписанного удостоверения

Это последний и самый важный эндпоинт. Кошелек использует токен доступа, полученный с
эндпоинта `/token`, чтобы сделать аутентифицированный `POST`-запрос на этот маршрут.
Задача этого эндпоинта — выполнить окончательную проверку, создать криптографически
подписанное удостоверение и вернуть его кошельку.

Создайте файл `src/app/api/issue/credential/route.ts` со следующим содержимым:

```typescript
// src/app/api/issue/credential/route.ts
import { NextRequest, NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";
import {
    getIssuanceSessionByToken,
    updateIssuanceSession,
    createIssuedCredential,
    getActiveIssuerKey,
} from "@/lib/database";
import {
    createJWTVerifiableCredential,
    importIssuerKeyPair,
    generateIssuerDid,
} from "@/lib/crypto";

export async function POST(request: NextRequest) {
    try {
        // 1. Проверка Bearer токена
        const authHeader = request.headers.get("authorization");
        const accessToken = authHeader?.substring(7);
        const session = await getIssuanceSessionByToken(accessToken);

        if (!session) {
            return NextResponse.json({ error: "invalid_token" }, { status: 401 });
        }

        // 2. Получение данных пользователя из сессии
        const userData = session.user_data;
        if (!userData) {
            return NextResponse.json({ error: "missing_user_data" }, { status: 400 });
        }

        // 3. Получение активного ключа эмитента
        const issuerKey = await getActiveIssuerKey();
        if (!issuerKey) {
            // В реальном приложении у вас была бы более надежная система управления ключами.
            // Для этого демо мы можем сгенерировать ключ на лету, если он не существует.
            // Эта часть опущена для краткости, но есть в репозитории.
            return NextResponse.json(
                {
                    error: "server_error",
                    error_description: "Failed to get issuer key",
                },
                { status: 500 },
            );
        }

        // 4. Создание JWT-VC
        const issuerDid = generateIssuerDid();
        const keyPair = await importIssuerKeyPair(
            issuerKey.key_id,
            issuerKey.public_key,
            issuerKey.private_key,
            issuerDid,
        );
        const subjectId = `did:example:${uuidv4()}`;
        const credentialData = await createJWTVerifiableCredential(
            userData,
            keyPair,
            subjectId,
            process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000",
        );

        // 5. Сохранение выпущенного удостоверения в базе данных
        await createIssuedCredential(/* ... credential details ... */);
        await updateIssuanceSession(session.id, "credential_issued");

        // 6. Возврат подписанного удостоверения
        return NextResponse.json({
            format: "jwt_vc",
            credential: credentialData,
            c_nonce: uuidv4(), // Новый nonce для последующих запросов
            c_nonce_expires_in: 300,
        });
    } catch (error) {
        console.error("Credential endpoint error:", error);
        return NextResponse.json({ error: "server_error" }, { status: 500 });
    }
}
```

Ключевые шаги в этом эндпоинте:

1. **Проверка токена:** Он проверяет наличие действительного `Bearer` токена в заголовке
   `Authorization` и использует его для поиска активной сессии выпуска.
2. **Получение данных пользователя:** Он извлекает данные заявлений пользователя, которые
   были сохранены в сессии при создании токена.
3. **Загрузка ключа эмитента:** Он загружает активный ключ подписи эмитента из базы
   данных. В реальном сценарии это управлялось бы безопасной системой управления ключами.
4. **Создание удостоверения:** Он вызывает наш хелпер `createJWTVerifiableCredential` из
   `src/lib/crypto.ts` для конструирования и подписи JWT-VC.
5. **Логирование выпуска:** Он сохраняет запись о выпущенном удостоверении в базе данных
   для целей аудита и отзыва.
6. **Возврат удостоверения:** Он возвращает подписанное удостоверение кошельку в
   JSON-ответе. Кошелек затем несет ответственность за его безопасное хранение.

## 5. Запуск эмитента и следующие шаги

Теперь у вас есть полная, сквозная реализация эмитента цифровых удостоверений. Вот как
запустить его локально и что нужно учесть, чтобы превратить его из прототипа в готовое к
продакшену приложение.

### 5.1. Как запустить пример

1. **Склонируйте репозиторий:**

    ```bash
    git clone https://github.com/corbado/digital-credentials-example.git
    cd digital-credentials-example
    ```

2. **Установите зависимости:**

    ```bash
    npm install
    ```

3. **Запустите базу данных:** Убедитесь, что Docker запущен, затем запустите контейнер
   MySQL:

    ```bash
    docker-compose up -d
    ```

4. **Настройте окружение и запустите туннель:** Это самый важный шаг для локального
   тестирования. Поскольку вашему мобильному кошельку необходимо подключаться к вашей
   машине разработки через интернет, вы должны предоставить вашему локальному серверу
   публичный HTTPS URL. Мы будем использовать для этого `ngrok`.

    a. **Запустите ngrok:**

    ```bash
    ngrok http 3000
    ```

    b. **Скопируйте HTTPS URL** из вывода
    [ngrok](https://www.corbado.com/blog/multi-device-passkey-login-corbado-ngrok) (например,
    `https://random-string.ngrok.io`). c. **Создайте файл `.env.local`** и установите URL:

    ```
    NEXT_PUBLIC_BASE_URL=https://<your-ngrok-url>
    ```

5. **Запустите приложение:**

    ```bash
    npm run dev
    ```

    Откройте браузер по адресу `http://localhost:3000/issue`. Теперь вы можете заполнить
    форму, и сгенерированный QR-код будет правильно указывать на ваш публичный URL
    [ngrok](https://www.corbado.com/blog/multi-device-passkey-login-corbado-ngrok), позволяя вашему мобильному
    кошельку подключиться и получить удостоверение.

### 5.2. Важность HTTPS и `ngrok`

Протоколы цифровых удостоверений создаются с приоритетом на безопасность. По этой причине
кошельки почти всегда отказываются подключаться к эмитенту через небезопасное соединение
(`http://`). Весь процесс основан на безопасном соединении **HTTPS**, которое
обеспечивается **SSL-сертификатом**.

Сервис туннелирования, такой как `ngrok`, решает обе проблемы, создавая безопасный,
общедоступный HTTPS URL (с действительным SSL-сертификатом), который перенаправляет весь
трафик на ваш локальный сервер разработки. Кошельки требуют HTTPS и отказываются
подключаться к небезопасным (`http://`) эндпоинтам. Это незаменимый инструмент для
тестирования любого веб-сервиса, которому необходимо взаимодействовать с мобильными
устройствами или внешними веб-хуками.

### 5.3. Что выходит за рамки этого руководства

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

- **Безопасность на уровне продакшена:** Этот эмитент предназначен для образовательных
  целей. Продакшн-система потребовала бы безопасной системы управления ключами (KMS)
  вместо хранения ключей в базе данных, надежной обработки ошибок, ограничения скорости
  запросов и всестороннего аудита.
- **Отзыв удостоверений:** В этом руководстве не реализован механизм отзыва выпущенных
  удостоверений. Хотя схема включает флаг `revoked` для будущего использования, логика
  отзыва здесь не предоставлена.
- **Поток с кодом авторизации:** Мы сосредоточились исключительно на потоке
  `pre-authorized_code`. Полная реализация потока `authorization_code` потребовала бы
  экрана согласия пользователя и более сложной логики [OAuth 2.0](https://www.corbado.com/glossary/oauth2).
- **Управление пользователями:** Руководство не включает аутентификацию или управление
  пользователями для самого эмитента. Предполагается, что пользователь уже
  аутентифицирован и авторизован для получения удостоверения.

## 6. Заключение

Вот и всё! С помощью нескольких страниц кода мы получили полный, сквозной эмитент цифровых
удостоверений, который:

1. Предоставляет удобный фронтенд для запроса удостоверений.
2. Реализует полный поток OpenID4VCI `pre-authorized_code`.
3. Предоставляет все необходимые эндпоинты обнаружения для совместимости с кошельками.
4. Генерирует и подписывает безопасное, соответствующее стандартам JWT-верифицируемое
   удостоверение.

Хотя это руководство предоставляет прочную основу, готовый к продакшену эмитент потребует
дополнительных функций, таких как надежное управление ключами, постоянное хранилище вместо
хранения в памяти, отзыв удостоверений и всестороннее усиление безопасности. Совместимость
с кошельками также может различаться; для тестирования рекомендуется Sphereon
[Wallet](https://www.corbado.com/blog/digital-wallet-assurance), но другие кошельки могут не поддерживать поток с
предварительной авторизацией в том виде, в котором он здесь реализован. Однако основные
строительные блоки и схема взаимодействия останутся прежними. Следуя этим шаблонам, вы
можете создать безопасный и совместимый эмитент для любого типа цифровых удостоверений.

## 7. Ресурсы

Вот некоторые из ключевых ресурсов, спецификаций и инструментов, использованных или
упомянутых в этом руководстве:

- **Репозиторий проекта:**
    - [Полный исходный код на GitHub](https://github.com/corbado/digital-credentials-example)

- **Ключевые спецификации:**
    - [OpenID for Verifiable Credential Issuance (OpenID4VCI)](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html):
      Основной протокол выпуска.
    - [W3C Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/):
      Фундаментальный стандарт для VC.
    - [Метод `did:web`](https://w3c-ccg.github.io/did-method-web/): Метод DID,
      используемый для открытого ключа нашего эмитента.

- **Инструменты:**
    - [Sphereon Wallet](https://sphereon.com/wallet/): Тестовый кошелек, использованный в
      этом руководстве.
    - ngrok: Для создания безопасного туннеля к вашей локальной среде разработки.

- **Библиотеки:**
    - Next.js: Фреймворк React для создания фронтенда и бэкенда.
    - [jose](https://github.com/panva/jose): Для создания и подписи JSON Web Tokens (JWT).
    - [mysql2](https://github.com/sidorares/node-mysql2): Клиент MySQL для
      [Node.js](https://www.corbado.com/blog/nodejs-passkeys).
