feat: add typedoc documentation

This commit is contained in:
Sergey Krylov 2026-03-04 06:37:06 +03:00
parent b36d1cfc08
commit d59357f6c9
9 changed files with 433 additions and 5 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ pnpm-lock.yaml
.DS_Store
.log
.vscode/
docs/

198
README.md Normal file
View File

@ -0,0 +1,198 @@
# @teacinema/contracts
npm-пакет с контрактами для межсервисного взаимодействия: gRPC (протобафы) и события (message broker).
## Установка
```bash
yarn add @teacinema/contracts
```
---
## gRPC сервисы
Для подключения gRPC клиента используй `PROTO_PATHS` и соответствующее имя пакета.
| Сервис | Пакет proto | Описание |
|---|---|---|
| `AuthService` | `auth.v1` | Аутентификация (OTP, токены, Telegram) |
| `AccountService` | `account.v1` | Управление аккаунтом (email, телефон) |
| `UsersService` | `users.v1` | Профили пользователей |
### AuthService — `auth.v1`
| Метод | Запрос | Ответ | Описание |
|---|---|---|---|
| `SendOtp` | `SendOtpRequest` | `SendOtpResponse` | Отправить OTP-код |
| `VerifyOtp` | `VerifyOtpRequest` | `VerifyOtpResponse` | Проверить OTP, получить токены |
| `Refresh` | `RefreshRequest` | `RefreshResponse` | Обновить access/refresh токены |
| `TelegramInit` | `Empty` | `TelegramInitResponse` | Получить ссылку для Telegram-авторизации |
| `TelegramVerify` | `TelegramVerifyRequest` | `TelegramVerifyResponse` | Верифицировать данные от Telegram |
| `TelegramComplete` | `TelegramCompleteRequest` | `TelegramCompleteResponse` | Привязать телефон к Telegram-сессии |
| `TelegramConsume` | `TelegramConsumeRequest` | `TelegramConsumeResponse` | Получить токены из Telegram-сессии |
### AccountService — `account.v1`
| Метод | Запрос | Ответ | Описание |
|---|---|---|---|
| `GetAccount` | `GetAccountRequest` | `GetAccountResponse` | Получить данные аккаунта по ID |
| `InitEmailChange` | `InitEmailChangeRequest` | `InitEmailChangeResponse` | Инициировать смену email |
| `ConfirmEmailChange` | `ConfirmEmailChangeRequest` | `ConfirmEmailChangeResponse` | Подтвердить смену email |
| `InitPhoneChange` | `InitPhoneChangeRequest` | `InitPhoneChangeResponse` | Инициировать смену телефона |
| `ConfirmPhoneChange` | `ConfirmPhoneChangeRequest` | `ConfirmPhoneChangeResponse` | Подтвердить смену телефона |
### UsersService — `users.v1`
| Метод | Запрос | Ответ | Описание |
|---|---|---|---|
| `GetMe` | `GetMeRequest` | `GetMeResponse` | Получить профиль пользователя по ID |
| `CreateUser` | `CreateUserRequest` | `CreateUserResponse` | Создать нового пользователя |
---
## Использование в NestJS
### 1. Регистрация gRPC клиента
```ts
import { ClientsModule, Transport } from '@nestjs/microservices';
import { PROTO_PATHS } from '@teacinema/contracts';
@Module({
imports: [
ClientsModule.register([
{
name: 'AUTH_SERVICE',
transport: Transport.GRPC,
options: {
protoPath: PROTO_PATHS.AUTH,
package: 'auth.v1',
url: 'localhost:5000',
},
},
{
name: 'ACCOUNT_SERVICE',
transport: Transport.GRPC,
options: {
protoPath: PROTO_PATHS.ACCOUNT,
package: 'account.v1',
url: 'localhost:5001',
},
},
{
name: 'USERS_SERVICE',
transport: Transport.GRPC,
options: {
protoPath: PROTO_PATHS.USERS,
package: 'users.v1',
url: 'localhost:5002',
},
},
]),
],
})
export class AppModule {}
```
### 2. Использование gRPC клиента
Типы клиентов генерируются из proto-файлов и находятся в `gen/`:
```ts
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { AuthServiceClient } from '@teacinema/contracts/gen/auth';
@Injectable()
export class AuthProxy implements OnModuleInit {
private authService: AuthServiceClient;
constructor(@Inject('AUTH_SERVICE') private client: ClientGrpc) {}
onModuleInit() {
this.authService = this.client.getService<AuthServiceClient>('AuthService');
}
sendOtp(identifier: string, type: string) {
return this.authService.sendOtp({ identifier, type });
}
}
```
### 3. Реализация gRPC сервера
```ts
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { AuthServiceController, AuthServiceControllerMethods } from '@teacinema/contracts/gen/auth';
@Controller()
@AuthServiceControllerMethods()
export class AuthController implements AuthServiceController {
@GrpcMethod('AuthService', 'SendOtp')
sendOtp(request: SendOtpRequest): SendOtpResponse {
// ...
}
}
```
---
## События (Message Broker)
Интерфейсы событий для типизированного обмена через брокер сообщений (RabbitMQ, Kafka и др.).
| Интерфейс | Источник | Описание |
|---|---|---|
| `OtpRequestedEvent` | auth-service | Запрос на отправку OTP-кода |
| `EmailChangedEvent` | account-service | Инициация смены email |
| `PhoneChangedEvent` | account-service | Инициация смены телефона |
### Публикация события
```ts
import { OtpRequestedEvent } from '@teacinema/contracts';
client.emit<OtpRequestedEvent>('auth.otp_requested', {
identifier: '+79001234567',
code: '123456',
type: 'phone',
});
```
### Подписка на событие
```ts
import { EventPattern } from '@nestjs/microservices';
import { OtpRequestedEvent } from '@teacinema/contracts';
@Controller()
export class NotificationController {
@EventPattern('auth.otp_requested')
handleOtpRequested(data: OtpRequestedEvent) {
// отправить SMS или email с кодом
}
}
```
---
## Обновление типов из proto
После изменения `.proto` файлов — пересгенерировать TypeScript типы:
```bash
yarn generate
```
---
## Документация
Сгенерировать HTML-документацию и открыть в браузере:
```bash
yarn docs
open docs/index.html
```

View File

@ -4,7 +4,8 @@
"description": "Protocol Buffers contracts for microservices",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"generate": "npx protoc -I ./proto ./proto/*.proto --ts_proto_out=./gen --ts_proto_opt=nestJs=true,package=omit"
"generate": "npx protoc -I ./proto ./proto/*.proto --ts_proto_out=./gen --ts_proto_opt=nestJs=true,package=omit",
"docs": "typedoc"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@ -24,6 +25,7 @@
},
"devDependencies": {
"@types/node": "^25.0.10",
"typedoc": "^0.28.17",
"typescript": "^5.9.3"
}
}

View File

@ -1,4 +1,18 @@
/**
* Событие инициации смены email. Публикуется сервисом аккаунтов
* подписчик отправляет письмо с кодом подтверждения.
*
* @example
* ```ts
* client.emit<EmailChangedEvent>('account.email_change_initiated', {
* email: 'user@example.com',
* code: '123456',
* });
* ```
*/
export interface EmailChangedEvent {
/** Новый email пользователя */
email: string
/** Код подтверждения */
code: string
}

View File

@ -1,4 +1,18 @@
/**
* Событие инициации смены номера телефона. Публикуется сервисом аккаунтов
* подписчик отправляет SMS с кодом подтверждения.
*
* @example
* ```ts
* client.emit<PhoneChangedEvent>('account.phone_change_initiated', {
* phone: '+79001234567',
* code: '123456',
* });
* ```
*/
export interface PhoneChangedEvent {
/** Новый номер телефона пользователя */
phone: string
/** Код подтверждения */
code: string
}

View File

@ -1,5 +1,27 @@
/**
* Событие запроса OTP-кода. Публикуется сервисом аутентификации
* подписчик (например, notification-service) доставляет код пользователю.
*
* @example Публикация события
* ```ts
* client.emit<OtpRequestedEvent>('auth.otp_requested', {
* identifier: '+79001234567',
* code: '123456',
* type: 'phone',
* });
* ```
*
* @example Подписка на событие
* ```ts
* @EventPattern('auth.otp_requested')
* handle(data: OtpRequestedEvent) { ... }
* ```
*/
export interface OtpRequestedEvent {
/** Email или номер телефона получателя */
identifier: string
/** OTP-код для отправки */
code: string
/** Тип идентификатора */
type: 'email' | 'phone'
}

View File

@ -1,7 +1,31 @@
import { join } from 'node:path';
/**
* Абсолютные пути к `.proto` файлам.
*
* Используются при регистрации gRPC клиентов и серверов в NestJS.
*
* @example Регистрация gRPC клиента
* ```ts
* import { ClientsModule, Transport } from '@nestjs/microservices';
* import { PROTO_PATHS } from '@teacinema/contracts';
*
* ClientsModule.register([{
* name: 'AUTH_SERVICE',
* transport: Transport.GRPC,
* options: {
* protoPath: PROTO_PATHS.AUTH,
* package: 'auth.v1',
* url: 'localhost:5000',
* },
* }])
* ```
*/
export const PROTO_PATHS = {
/** Путь к `auth.proto` (пакет `auth.v1`) */
AUTH: join(__dirname, '../../proto/auth.proto'),
/** Путь к `account.proto` (пакет `account.v1`) */
ACCOUNT: join(__dirname, '../../proto/account.proto'),
/** Путь к `users.proto` (пакет `users.v1`) */
USERS: join(__dirname, '../../proto/users.proto'),
} as const

11
typedoc.json Normal file
View File

@ -0,0 +1,11 @@
{
"$schema": "https://typedoc.org/schema.json",
"entryPoints": ["src/index.ts"],
"out": "docs",
"name": "@teacinema/contracts",
"readme": "README.md",
"includeVersion": true,
"categorizeByGroup": true,
"tsconfig": "tsconfig.json",
"skipErrorChecking": true
}

142
yarn.lock
View File

@ -7,6 +7,17 @@
resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-2.10.2.tgz#d7c063301f2a33095fc202f06bf3cce0c138dfcd"
integrity sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==
"@gerrit0/mini-shiki@^3.17.0":
version "3.23.0"
resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-3.23.0.tgz#d9414f3080b88303b18f3a311846e37e424d800c"
integrity sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==
dependencies:
"@shikijs/engine-oniguruma" "^3.23.0"
"@shikijs/langs" "^3.23.0"
"@shikijs/themes" "^3.23.0"
"@shikijs/types" "^3.23.0"
"@shikijs/vscode-textmate" "^10.0.2"
"@nestjs/microservices@^11.1.12":
version "11.1.12"
resolved "https://registry.yarnpkg.com/@nestjs/microservices/-/microservices-11.1.12.tgz#05c497eb5ddcae09ae8ac0d3aa1bcd0b7336816b"
@ -15,6 +26,48 @@
iterare "1.2.1"
tslib "2.8.1"
"@shikijs/engine-oniguruma@^3.23.0":
version "3.23.0"
resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz#789421048d66ac1b33613169d6d18b9cc6e340ed"
integrity sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==
dependencies:
"@shikijs/types" "3.23.0"
"@shikijs/vscode-textmate" "^10.0.2"
"@shikijs/langs@^3.23.0":
version "3.23.0"
resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-3.23.0.tgz#00959d8b16c7f671221ae79b3ad8cde7e6a5c112"
integrity sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==
dependencies:
"@shikijs/types" "3.23.0"
"@shikijs/themes@^3.23.0":
version "3.23.0"
resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-3.23.0.tgz#fd96ca5ad52639057995bc2093682884e1846f27"
integrity sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==
dependencies:
"@shikijs/types" "3.23.0"
"@shikijs/types@3.23.0", "@shikijs/types@^3.23.0":
version "3.23.0"
resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-3.23.0.tgz#d441571a058641926018ae3de99866f39e5bbdf2"
integrity sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==
dependencies:
"@shikijs/vscode-textmate" "^10.0.2"
"@types/hast" "^3.0.4"
"@shikijs/vscode-textmate@^10.0.2":
version "10.0.2"
resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224"
integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==
"@types/hast@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa"
integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==
dependencies:
"@types/unist" "*"
"@types/node@^25.0.10":
version "25.0.10"
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.10.tgz#4864459c3c9459376b8b75fd051315071c8213e7"
@ -22,6 +75,28 @@
dependencies:
undici-types "~7.16.0"
"@types/unist@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c"
integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
brace-expansion@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
dependencies:
balanced-match "^1.0.0"
case-anything@^2.1.13:
version "2.1.13"
resolved "https://registry.yarnpkg.com/case-anything/-/case-anything-2.1.13.tgz#0cdc16278cb29a7fcdeb072400da3f342ba329e9"
@ -39,16 +114,62 @@ dprint-node@^1.0.8:
dependencies:
detect-libc "^1.0.3"
entities@^4.4.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
iterare@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.2.1.tgz#139c400ff7363690e33abffa33cbba8920f00042"
integrity sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==
linkify-it@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
dependencies:
uc.micro "^2.0.0"
lunr@^2.3.9:
version "2.3.9"
resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1"
integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==
markdown-it@^14.1.0:
version "14.1.1"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.1.tgz#856f90b66fc39ae70affd25c1b18b581d7deee1f"
integrity sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==
dependencies:
argparse "^2.0.1"
entities "^4.4.0"
linkify-it "^5.0.0"
mdurl "^2.0.0"
punycode.js "^2.3.1"
uc.micro "^2.1.0"
mdurl@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
minimatch@^9.0.5:
version "9.0.9"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e"
integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==
dependencies:
brace-expansion "^2.0.2"
protoc@33.4.0:
version "33.4.0"
resolved "https://registry.yarnpkg.com/protoc/-/protoc-33.4.0.tgz#4a288b59f5f50b9a2c4bbf345276357e59dd6f10"
integrity sha512-dC1SJKbn4f60onsAMrfiPMygfSz4w/tPGHtPCqMSa5CLfHocJXkHjsvXeuuhjSPAJ8uUc8rfUvs0Hy9nmKIOhA==
punycode.js@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
rxjs@^7.8.2:
version "7.8.2"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b"
@ -85,12 +206,33 @@ tslib@2.8.1, tslib@^2.1.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
typedoc@^0.28.17:
version "0.28.17"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.28.17.tgz#eab7c6649494d0a796e0b2fd2c9a5aea41b0a781"
integrity sha512-ZkJ2G7mZrbxrKxinTQMjFqsCoYY6a5Luwv2GKbTnBCEgV2ihYm5CflA9JnJAwH0pZWavqfYxmDkFHPt4yx2oDQ==
dependencies:
"@gerrit0/mini-shiki" "^3.17.0"
lunr "^2.3.9"
markdown-it "^14.1.0"
minimatch "^9.0.5"
yaml "^2.8.1"
typescript@^5.9.3:
version "5.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
uc.micro@^2.0.0, uc.micro@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"
integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
undici-types@~7.16.0:
version "7.16.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46"
integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==
yaml@^2.8.1:
version "2.8.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.2.tgz#5694f25eca0ce9c3e7a9d9e00ce0ddabbd9e35c5"
integrity sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==