@opsregistry/adapter-saby
v0.5.0
Published
Saby adapter for universal electronic business document exchange operations.
Readme
@opsregistry/adapter-saby
Адаптер Saby (СБИС) для универсальных операций электронного обмена деловыми документами.
Пакет преобразует контракты @opsregistry/contracts в команды API ЭДО Saby. Потребитель работает с
нормализованными идентификаторами, сторонами, статусами, вложениями и событиями, не распространяя
русские имена полей Saby по своему приложению.
Реализовано
it.identity.authenticate— сервисная авторизация приложения или готовый access token;documents.exchangeDocument.list—СБИС.СписокДокументов;documents.exchangeDocument.get—СБИС.ПрочитатьДокумент;documents.exchangeDocument.listChanges—СБИС.СписокИзменений.documents.exchangeDocument.create— создать черновик черезСБИС.ЗаписатьДокумент;documents.exchangeDocument.send— отправить черновик черезСБИС.ВыполнитьДействие.documents.exchangeDocument.accept— принять входящий документ или выбранные вложения;documents.exchangeDocument.reject— отклонить входящий документ или выбранные вложения.documents.exchangeDocument.requestCancellation— запросить аннулирование;documents.exchangeDocument.acceptCancellation— принять запрос на аннулирование;documents.exchangeDocument.rejectCancellation— отклонить запрос на аннулирование.documents.exchangeDocument.prepareSigning— получить подготовленные хеши вложений;documents.exchangeDocument.completeSigning— загрузить внешние подписи и завершить действие.
Адаптер не формирует электронную подпись сам: это отдельная операция
trust.digitalSignature.create, которую выполняет локальный криптографический провайдер.
Настройка приложения Saby
В Saby создайте внешнее приложение с сервисной авторизацией и минимальными правами на ЭДО. Адаптеру нужны:
app_client_id— ID подключения;app_secret— защищённый ключ;secret_key— сервисный ключ.
Передавайте секреты только через окружение или защищённое хранилище приложения:
import { createAdapter } from '@opsregistry/adapter-saby';
const saby = createAdapter({
appClientId: process.env.SABY_APP_CLIENT_ID!,
appSecret: process.env.SABY_APP_SECRET!,
serviceKey: process.env.SABY_SERVICE_KEY!
});Можно передать уже полученный accessToken вместо трёх параметров приложения. Адаптер получает и
кэширует токен сам, а в команды ЭДО отправляет его только в заголовке X-SBISAccessToken.
Получение документов
Saby требует тип документа для СБИС.СписокДокументов. Это ограничение конкретного провайдера:
универсальный контракт оставляет documentType необязательным, но этот адаптер проверяет его перед
вызовом.
Фильтры по нормализованному status, documentSubtype и внутреннему направлению пока отклоняются
явной ошибкой: соответствующие коды состояний и поддержка полей должны быть подтверждены на живом
кабинете, а молча игнорировать запрошенный фильтр нельзя.
const incoming = await saby.listExchangeDocuments({
documentType: 'ДокОтгрВх',
direction: 'incoming',
dateFrom: '2026-08-01',
dateTo: '2026-08-31',
pageSize: 50
});
const document = await saby.getExchangeDocument({
documentId: incoming.items[0]!.documentId
});nextPageToken непрозрачен: сохраняйте и передавайте его без разбора. Ссылки Saby на вложения и
подписи временные; приложение не должно считать их постоянным архивным адресом.
Синхронизация изменений
const changes = await saby.listExchangeDocumentChanges({
changedAfter: '2026-08-01T00:00:00Z',
pageSize: 50
});
// Для следующего опроса передайте changes.nextCursor без изменения.Внутри cursor сохраняются координаты, необходимые Saby: событие, документ, редакция и время. Публичный контракт не раскрывает и не стандартизует внутреннее устройство курсора.
Создание и отправка
Создание не запускает документооборот. Оно записывает черновик, а clientReference сохраняется в
Редакция.ПримечаниеИС, чтобы связать документ с объектом исходной системы.
const draft = await saby.createExchangeDocument({
clientReference: 'invoice:42',
documentType: 'ДокОтгрИсх',
attachments: [
{
name: 'invoice.pdf',
content: { kind: 'base64', data: pdfBase64 }
}
]
});Отправка отделена намеренно и требует confirmLegalAction: true. Saby сам определяет требования
регламента к подписи. Адаптер поддерживает управляемую Saby подпись и два режима отложенной подписи,
но отклоняет withoutSignature, потому что не может гарантировать отсутствие подписи на переходе.
await saby.sendExchangeDocument({
documentId: draft.document.documentId,
signingMode: 'deferredWithConfirmation',
confirmLegalAction: true
});Saby не документирует ключ идемпотентности для этих команд. Поэтому адаптер отклоняет переданный
idempotencyKey, а не создаёт ложное ощущение защиты от повторной отправки.
Для формализованного вложения Saby требует одновременно передать documentType, documentSubtype,
formatVersion и formatSubversion; частичный набор адаптер отклоняет до запроса.
content.kind=url означает ссылку на объект файлового хранилища Saby, а не произвольный публичный
URL.
Обработка входящих документов
getExchangeDocument возвращает availableActions: нормализованный смысл действия, название Saby,
идентификатор этапа и признаки обязательной подписи или комментария. Для accept и reject адаптер
повторно читает актуальный документ непосредственно перед изменением и выбирает действие нужного
смысла. Если подходящих действий несколько, нужно передать выбранный элемент как action; адаптер
не делает юридически значимый выбор сам.
const incoming = await saby.getExchangeDocument({ documentId });
const acceptAction = incoming.document.availableActions.find(
(action) => action.kind === 'accept'
);
await saby.acceptExchangeDocument({
documentId,
action: acceptAction,
attachmentIds: ['attachment-id'],
signingMode: 'deferredWithConfirmation',
confirmLegalAction: true
});Без attachmentIds обрабатывается весь пакет. При их наличии адаптер включает
СложноеУтверждение и передаёт Saby только выбранные вложения. Отклонение всегда требует reason:
await saby.rejectExchangeDocument({
documentId,
signingMode: 'providerManaged',
reason: 'The document amount does not match the contract',
confirmLegalAction: true
});Режим withoutSignature допускается только когда актуальное действие Saby явно сообщает, что
подпись не требуется. accept и reject, как и send, не поддерживают выдуманную адаптером
идемпотентность и отклоняют idempotencyKey.
Аннулирование
Аннулирование отделено от удаления: это подписываемое соглашение двух сторон. Отправка запроса
переводит документ в cancellationPending, но ещё не делает его аннулированным.
await saby.requestExchangeDocumentCancellation({
documentId,
reason: 'The document was issued by mistake',
signingMode: 'deferredWithConfirmation',
confirmLegalAction: true
});Получатель запроса вызывает acceptExchangeDocumentCancellation или
rejectExchangeDocumentCancellation. Перед ответом адаптер перечитывает актуальный регламент Saby
и выбирает действие Документ аннулирован либо Аннулирование отклонено. Можно передать action
из availableActions, если в регламенте найдено несколько подходящих переходов. Для частичного
аннулирования во всех трёх операциях поддерживается attachmentIds.
Подпись на физическом носителе
Для локальной подписи адаптер не обращается к носителю и никогда не получает его PIN. Сначала
prepareExchangeDocumentSigning вызывает СБИС.ПодготовитьДействие с точной редакцией, этапом,
действием и отпечатком сертификата. Saby формирует служебные вложения и возвращает их хеши.
Приложение передаёт эти хеши локальному провайдеру операции trust.digitalSignature.create.
Провайдер работает с Рутокеном через PKCS#11 или системный криптопровайдер, показывает пользователю
сертификат и запрашивает PIN локально. В адаптер Saby возвращаются только готовые подписи Base64.
completeExchangeDocumentSigning прикладывает их к соответствующим вложениям и вызывает
СБИС.ВыполнитьДействие после confirmLegalAction: true.
Подготовка и завершение должны использовать одну редакцию и одно действие из availableActions.
Повторная подготовка может перегенерировать служебные документы, поэтому подписи от предыдущей
подготовки после этого использовать нельзя.
Стенды и документация
- production API:
https://online.sbis.ru/service/?srv=1; - production OAuth:
https://online.sbis.ru/oauth/service/; - тестовый API доступен на
fix-online.sbis.ruи задаётся черезbaseUrl/authUrl.
Официальная документация:
- https://saby.ru/help/integration/api/edo
- https://saby.ru/help/integration/api/auth/service/
- https://saby.ru/help/integration/api/all_methods/list_doc/
- https://saby.ru/help/integration/api/all_methods/read_doc/
- https://saby.ru/help/integration/api/all_methods/changeslist/
- https://saby.ru/help/integration/api/all_methods/doc
- https://saby.ru/help/integration/api/all_methods/make_doc/
- https://saby.ru/help/integration/api/sequence/ep
- https://saby.ru/help/integration/api/sequence/incom_doc
- https://saby.ru/help/integration/api/sequence/cancellation
- https://saby.ru/help/integration/api/all_methods/develop_doc
Живая проверка production API выполнена 3 сентября 2026 года для сервисной OAuth-авторизации,
documents.exchangeDocument.list и создания неотправленного черновика. Saby требует явно указывать
UTF-8 в Content-Type; транспорт адаптера делает это автоматически. При создании документа ООО без
формализованного XML передавайте в ourOrganization как taxId, так и
registrationReasonCode. Отправка, принятие и отклонение в боевом кабинете намеренно не проверялись,
поскольку это юридически значимые действия.
Ограничения первого релиза
- соответствие редких статусов нормализованной модели уточняется по живым ответам;
dateTimeOffsetпо умолчанию равен+03:00и должен совпадать с часовым поясом кабинета;- временные download URL возвращаются как есть и не скачиваются автоматически;
- адаптер не хранит документы, подписи и секреты;
- боевую отправку следует выполнять только после отдельного подтверждения пользователя.
