Перейти к содержимому
getnextpdf.com

Enterprise редакция

SaaS — глубокий справочник

Модуль Enterprise SaaS предоставляет многоарендные строительные блоки для сервиса на базе NextPDF.

  • TenantContext — неизменяемый value-объект идентичности, разрешаемый только из аутентифицированного контекста.
  • ApiKeyGenerator и ApiKeyAuthenticator выпускают и проверяют API-ключи с префиксом, контрольной суммой и хранением в виде хеша.
  • QuotaChecker ограничивает запросы по квотам на каждого арендатора: предупреждение на 80%, отклонение на 100%, защищённый отказ при неизвестном использовании.
  • SidecarJwtMinter выпускает краткосрочные служебные токены HS256 для вызовов между компонентами.
  • UsageMeter и StripeMeteringSyncer получают события использования и синхронизируют их с поставщиком биллинга с детерминированной идемпотентностью.

Эта возможность поставляется в NextPDF Enterprise (nextpdf/enterprise) и активируется лицензионным конвертом уровня Enterprise. Развёртывание без этого права не загружает классы возможности. Сравните редакции и получите лицензию.

Поверхность SaaS — базовая возможность Enterprise; отдельного флага на каждую функцию нет. У NextPDF Core (Apache-2.0) и NextPDF Pro нет модели арендности, API-ключей или квот; у этой возможности нет эквивалента в более низком уровне.

Окно терминала
composer require nextpdf/enterprise:^3

Все символы находятся в пространстве NextPDF\Enterprise\SaaS.

СимволПараметрыПоведение по умолчаниюВозвращаетБросает или завершается сПримечания
TenantContextstring $tenantId, string $source, array $scopes = ['read']Неизменяемый value-объект идентичностиvalue-объектНичегоИсточники: jwt, mtls, api_key; hasScope() / hasAnyScope() проверяют области
TenantContext::singleTenant()нетФиксированный арендатор default с read, write, adminTenantContextНичегоРазвёртывания с одним арендатором
ApiKeyAuthenticator::authenticate()string $rawKeyШестишаговая проверка, затем разрешение контекстаTenantContextApiKeyAuthenticationException (HTTP 401)Контекст sourceapi_key; области копируются из записи ключа
ApiKeyAuthenticator::requireScope()TenantContext $context, ApiKeyScope $requiredScopeЯвное утверждение областиvoidApiKeyAuthenticationException::insufficientScope() (HTTP 403)Проверка области — отдельный явный шаг
ApiKeyGenerator::generateLive() / ::generateTest()нетНовый ключ: префикс, тело из 32 символов base62 (энтропия 192 бита), контрольная сумма из 4 символовarray{key, hash, prefix}НичегоПрефиксы npf_live_ / npf_test_; hash — дайджест для хранения
ApiKeyGenerator::validateChecksum()string $keyПроверка формы префикса, длины и контрольной суммы CRC32boolНичегоОтбраковка опечаток до любого обращения к хранилищу данных; не средство безопасности
ApiKeyGenerator::hashKey() (статический)string $keyШестнадцатеричный дайджест SHA-256 от сырого ключаstringНичегоЕдинственное сохраняемое представление ключа
ApiKeyGenerator::isLiveKey() / ::isTestKey()string $keyПроверка префиксаboolНичегоСреда видна без поиска
ApiKeyid, арендатор, хеш ключа, отображаемый префикс, маска областей, моменты создания/истечения/отзываСохранённая запись ключа; открытый текст никогда не сохраняетсяvalue-объектНичегоisActive(), isRevoked(), isExpired(), scopeNames()
ApiKeyScopeперечисление (backed enum): Read = 1, Write = 2, Admin = 4Модель областей на основе битовой маскиперечислениеНичегоmaskFromNames(), fromName(), fullAccess(); неизвестные имена игнорируются построителем маски
ApiKeyRepositoryInterfaceКонтракт хранения; сохранение только хешаОпределяется реализациейfindByHash(), findActiveByTenant(), store(), revoke()
SidecarJwtMinter::__construct()string $secret, издатель, аудитория, int $ttlSeconds = 300Отклоняет подписывающий секрет короче 16 байт при конструированииэкземплярInvalidArgumentExceptionНижний порог стойкости ключа 128 бит; рекомендуется 32 и более случайных байт
SidecarJwtMinter::mint()TenantContext $tenantHS256 JWT с iss, aud, sub, scope, tenant_id, iat, exp, jtistringJsonException при сбое кодирования утвержденийВремя жизни по умолчанию пять минут; jti — 16 случайных байт в шестнадцатеричном виде
QuotaChecker::check()TenantContext $tenant, TenantQuota $quotaЧитает текущее использование; предупреждает на 80%; отклоняет на 100%; отказывает при неизвестном использованииarray{allowed: bool, warning_percentage: float|null}QuotaExceededException, QuotaUnavailableExceptionКоллбэк оповещения вызывается на обоих порогах
TenantQuotafloat $maxCuPerPeriod, коллекции, байты хранилища, параллельные заданияЛимиты на период; константа мягкого порога 80%value-объектНичегоЗначения по умолчанию fromConfig(): 10,000 CU, 100 коллекций, 10 GB, 10 заданий
QuotaExceededException::toErrorEnvelope()нетКонверт ошибки SPEC-QUOTA-001arrayHTTP 402, без повтора; несёт текущее значение, лимит и момент сброса
QuotaUnavailableException::toErrorEnvelope()нетКонверт ошибки SPEC-QUOTA-503arrayHTTP 503, с повтором; причина usage_undeterminable
UsageMeter::pullUsage()array<string, int> $watermarksОпрашивает каждый настроенный хост-источник использования от его курсораarray{events, instance_id}UsageMeterException, когда недоступны все хостыЧастичный сбой допустим; недоступные хосты журналируются и пропускаются
UsageMeter::getCurrentUsage()string $tenantIdИспользование вычислительных единиц за текущий периодfloatUsageMeterException, когда использование неопределимоРазбираемый ноль авторитетен; неизвестное использование бросает исключение
StripeMeteringSyncer::sync()array<string, int> $watermarksОдин цикл: получение, преобразование, отправкаarray{watermarks, sent, failed}Ничего; сбои отправки направляются в коллбэк DLQСбой получения возвращает холостой цикл, сохраняющий курсор
StripeAdapter::sendMeterEvent()MeterEvent $eventPOST-запрос поставщику с заголовком идемпотентностиvoidStripeSyncExceptionHTTP 429 и 5xx — с повтором; прочие 4xx — без повтора
StripeAdapter::sendBatch()list<MeterEvent> $eventsОтправляет каждое событие; собирает сбоиlist<StripeSyncException>НичегоПустой список означает, что все события успешны
MeterEventимя счётчика, арендатор, значение, ключ идемпотентности, временная меткаНеизменяемый value-объект события счётчикаvalue-объектНичегоtoStripePayload() сериализует полезную нагрузку поставщика
final readonly class ApiKeyAuthenticator
{
public function __construct(
private ApiKeyRepositoryInterface $repository,
private ApiKeyGenerator $generator,
private LoggerInterface $logger,
) {}
public function authenticate(string $rawKey): TenantContext {}
public function requireScope(TenantContext $context, ApiKeyScope $requiredScope): void {}
}
final class QuotaChecker
{
public function __construct(
private readonly UsageMeterInterface $usageMeter,
private readonly LoggerInterface $logger,
private readonly Closure $quotaAlertCallback,
) {}
/** @return array{allowed: bool, warning_percentage: float|null} */
public function check(TenantContext $tenant, TenantQuota $quota): array {}
}
interface UsageMeterInterface
{
/** @return array<string, mixed> */
public function pullUsage(array $watermarks): array;
public function getCurrentUsage(string $tenantId): float;
}
final class StripeMeteringSyncer
{
public function __construct(
private readonly UsageMeterInterface $usageMeter,
private readonly StripeAdapterInterface $stripeAdapter,
private readonly LoggerInterface $logger,
private readonly Closure $dlqCallback,
) {}
/** @return array{watermarks: array<string, int>, sent: int, failed: int} */
public function sync(array $watermarks): array {}
}
final readonly class SidecarJwtMinter
{
public function __construct(
private string $secret,
private string $issuer = 'nextpdf-enterprise',
private string $audience = 'nextpdf-spectrum',
private int $ttlSeconds = self::DEFAULT_TTL_SECONDS,
) {}
public function mint(TenantContext $tenant): string {}
}
  • Идентичность арендатора. Контекст арендатора неизменяем: идентификатор арендатора, источник разрешения, области. Идентичность разрешается только из аутентифицированного контекста (jwt, mtls, api_key) — никогда из заголовка или параметра запроса, предоставленного клиентом. Развёртывание с одним арендатором использует фиксированный контекст default с полными областями.
  • Порядок аутентификации. Аутентификация по API-ключу выполняется в фиксированном порядке: контрольная сумма, хеш SHA-256, поиск в репозитории, проверка отзыва, проверка срока действия, разрешение контекста. Неизвестный, отозванный и истёкший ключи — три отдельных результата, все HTTP 401; недостаточная область — HTTP 403.
  • Секретность ключа. Сырой ключ никогда не сохраняется и не журналируется; сохраняется и ищется только его дайджест SHA-256. Аутентификатор сам не выполняет побайтового сравнения секрета; поиск дайджеста за константное время — контракт реализации репозитория.
  • Пороги квоты. На мягком лимите 80% запрос продолжается, возвращается процент предупреждения и срабатывает коллбэк оповещения. На жёстком лимите 100% запрос отклоняется с SPEC-QUOTA-001 (HTTP 402), несущим момент сброса — первый день следующего месяца, полночь по UTC.
  • Защищённый отказ квоты. Неопределимое использование отклоняет запрос с SPEC-QUOTA-503 (HTTP 503, с повтором). Неизвестное использование никогда не считается нулём. Настоящее, разбираемое нулевое использование авторитетно и допускает запрос.
  • Дедупликация оповещений. Контролёр не дедуплицирует оповещения; дедупликация за период — ответственность коллбэка.
  • Синхронизация учёта. Цикл запланирован, никогда не на пути запроса. Он возобновляется с отметок для каждого источника и продвигает каждый курсор к идентичности самого позднего успешно отправленного события. Ключ идемпотентности детерминирован — арендатор, период, идентичность события — поэтому повторно отправленное событие схлопывается при дедупликации на стороне поставщика.
  • Сбой получения. Сбойное получение возвращает холостой цикл (sent 0, failed 0), сохраняющий отметки; следующий цикл повторяет то же окно, а не пропускает его.
  • Служебные токены. Токены — HS256 с общим секретом, несут iss, aud, sub, scope, tenant_id, iat, exp и уникальный jti. Время жизни по умолчанию — пять минут. Конструирование отклоняет секрет короче 16 байт, защищённым отказом.
  • Некорректный ключ не проходит контрольную сумму и отклоняется до любого обращения к хранилищу данных. Корректный, но неизвестный ключ отклоняется после поиска. Оба проявляются как результат недействительного ключа.
  • Неизвестный, отозванный и истёкший ключи используют отдельные фабрики исключений; флаг keyExpired истинен только для результата истечения. Сопоставьте их с отдельными ответами клиенту.
  • QuotaChecker::check() возвращает значение только при допуске; возвращаемый allowed всегда true. Отклонение и недоступность — исключительные результаты.
  • TenantQuota::usagePercentage() возвращает 0.0 для неположительной квоты; fromConfig() подставляет значения по умолчанию для отсутствующих величин и ограничивает целочисленные лимиты снизу значением 1.
  • Отметки существуют для каждого источника; отсутствующая отметка начинает с начала потока этого источника (курсор 0). Многоисточниковое развёртывание поддерживает независимые отметки.
  • Преобразование пропускает события, не являющиеся массивом, события с отсутствующей или пустой операцией либо арендатором, неположительным значением или несопоставленной операцией — не приводя к сбою цикла. Событие без пригодной положительной целочисленной идентичности отклоняется с предупреждением: случайный запасной ключ свёл бы на нет дедупликацию на стороне поставщика и мог бы привести к двойному выставлению счёта арендатору.
  • Десять последовательных сбоев отправки эскалируют до критической записи в журнале; счётчик сбрасывается при любой успешной отправке. Каждое сбойное событие всё равно достигает коллбэка недоставленных сообщений.
  • Некорректное тело JSON от хоста-источника использования даёт пустой список событий, а не сбой цикла. pullUsage() бросает исключение только когда недоступны все настроенные хосты.
  • Примитивы дайджеста и MAC — SHA-256 и HMAC-SHA256 через криптопровайдер PHP хоста. Сборка с ограничениями FIPS завершается защищённым отказом на неодобренном алгоритме, а не понижает уровень; слой SaaS не добавляет собственной криптографической политики.
  • Тела ключей и идентификаторы токенов берутся из CSPRNG (random_int(), random_bytes()).
  • Контрольная сумма CRC32 не является криптографическим средством и не зависит от режима FIPS.

Приведённые ниже утверждения описывают возможности относительно цитируемых пунктов. Это не заявления о сертификации; у NextPDF нет сертификации для этого модуля.

ПоведениеСсылка
Семантика not-after exp служебного токенаRFC 7519 §4.1.4
Компактная сериализация JWS служебного токенаRFC 7515 §3.1
Нижний порог секрета HS256 16 байт; никаких запоминаемых человеком паролей в качестве MAC-ключейRFC 8725 §3.5 (threat: §2.2)
Контракт поиска дайджеста в репозитории за константное времяOWASP ASVS 5.0 §11.2.4
Дайджест хранения API-ключа SHA-256FIPS 180-4 (code-declared)

Цитаты RFC 8725 и OWASP ASVS 5.0 проверены по RAG; полные идентификаторы ссылок записаны во frontmatter этой страницы. Ссылки FIPS 180-4, FIPS 198-1 и BSI TR-02102-1 объявлены в коде исходников продукта (hash('sha256', …) и задокументированный нижний порог ключа в минтере); они не извлекались из корпуса RAG для этой страницы. Требование константного времени ASVS §11.2.4 связывает реализацию репозитория, которую предоставляет оператор, а не сам класс аутентификатора.

  • Предоставьте надёжные реализации ApiKeyRepositoryInterface и StripeAdapterInterface; пакет поставляет контракты и клиент поставщика PSR-18, а не хранилище.
  • Зависимости — только абстракции PSR: логгер PSR-3, HTTP-клиент PSR-18, фабрики запросов и потоков PSR-17. SDK поставщика не требуется.
  • Запускайте синхронизацию учёта как запланированное задание. Надёжно сохраняйте возвращаемые отметки после каждого цикла.
  • Передавайте клиентам процент предупреждения квоты, например как заголовок предупреждения, и дедуплицируйте оповещения квоты за период в коллбэке.
  • Задавайте секрет минтера токенов из конфигурации как случайное значение с высокой энтропией; рекомендуется 32 и более случайных байт. Никогда не выводите его из пароля.
  • Префиксы ключей делают среду видимой без поиска; ключи sandbox и production никогда не сталкиваются, потому что префикс участвует в сохранённом дайджесте.
  • Детали внутреннего механизма остаются во внутренней документации репозитория исходников и выходят за рамки этого руководства.

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