Pular para o conteúdo
getnextpdf.com

Enterprise edição

SaaS — Referência Profunda

O módulo Enterprise SaaS fornece os blocos de construção multilocatários para um serviço baseado em NextPDF.

  • TenantContext é um objeto de valor de identidade imutável, resolvido apenas a partir do contexto autenticado.
  • ApiKeyGenerator e ApiKeyAuthenticator emitem e validam chaves de API com prefixo, checksum e armazenadas por hash.
  • QuotaChecker controla as requisições contra as cotas por locatário: avisa em 80%, rejeita em 100%, nega de forma segura (fail-closed) quando o uso é desconhecido.
  • SidecarJwtMinter gera tokens de serviço HS256 de vida curta para chamadas entre componentes.
  • UsageMeter e StripeMeteringSyncer puxam eventos de uso e os sincronizam com o provedor de cobrança com idempotência determinística.

Esta capacidade vem no NextPDF Enterprise (nextpdf/enterprise) e é ativada por um envelope de licença de nível Enterprise. Uma implantação sem esse direito de uso não carrega as classes da capacidade. Compare edições e obtenha uma licença.

A superfície SaaS é uma capacidade base do Enterprise; não existe nenhum sinalizador por recurso separado. NextPDF Core (Apache-2.0) e NextPDF Pro não têm nenhum modelo de locação, chave de API ou cota; esta capacidade não tem equivalente em um nível inferior.

Terminal window
composer require nextpdf/enterprise:^3

Todos os símbolos residem sob NextPDF\Enterprise\SaaS.

SímboloParâmetrosComportamento padrãoRetornaLança ou falha comNotas
TenantContextstring $tenantId, string $source, array $scopes = ['read']Objeto de valor de identidade imutávelobjeto de valorNadaFontes: jwt, mtls, api_key; hasScope() / hasAnyScope() testam escopos
TenantContext::singleTenant()nenhumLocatário default fixo com read, write, adminTenantContextNadaImplantações de locatário único
ApiKeyAuthenticator::authenticate()string $rawKeyValidação em seis etapas, depois resolução de contextoTenantContextApiKeyAuthenticationException (HTTP 401)O source do contexto é api_key; escopos copiados do registro da chave
ApiKeyAuthenticator::requireScope()TenantContext $context, ApiKeyScope $requiredScopeAsserção explícita de escopovoidApiKeyAuthenticationException::insufficientScope() (HTTP 403)A imposição de escopo é uma etapa explícita separada
ApiKeyGenerator::generateLive() / ::generateTest()nenhumNova chave: prefixo, corpo base62 de 32 caracteres (entropia de 192 bits), checksum de 4 caracteresarray{key, hash, prefix}NadaPrefixos npf_live_ / npf_test_; hash é o resumo de armazenamento
ApiKeyGenerator::validateChecksum()string $keyVerificação de formato de prefixo, comprimento e checksum CRC32boolNadaProteção contra erro de digitação antes de qualquer consulta ao datastore; não é um controle de segurança
ApiKeyGenerator::hashKey() (estático)string $keyResumo hexadecimal SHA-256 da chave em texto clarostringNadaA única representação armazenada de uma chave
ApiKeyGenerator::isLiveKey() / ::isTestKey()string $keyInspeção de prefixoboolNadaAmbiente visível sem uma consulta
ApiKeyid, locatário, hash da chave, prefixo de exibição, máscara de escopo, instantes de criação/expiração/revogaçãoRegistro de chave armazenado; texto claro nunca persistidoobjeto de valorNadaisActive(), isRevoked(), isExpired(), scopeNames()
ApiKeyScopeenum tipado: Read = 1, Write = 2, Admin = 4Modelo de escopo por máscara de bitsenumNadamaskFromNames(), fromName(), fullAccess(); nomes desconhecidos são ignorados pelo construtor de máscara
ApiKeyRepositoryInterfaceContrato de armazenamento; persistência apenas por hashDefinido pela implementaçãofindByHash(), findActiveByTenant(), store(), revoke()
SidecarJwtMinter::__construct()string $secret, issuer, audience, int $ttlSeconds = 300Rejeita um segredo de assinatura com menos de 16 bytes na construçãoinstânciaInvalidArgumentExceptionPiso de força de chave de 128 bits; recomendam-se 32 ou mais bytes aleatórios
SidecarJwtMinter::mint()TenantContext $tenantJWT HS256 com iss, aud, sub, scope, tenant_id, iat, exp, jtistringJsonException em falha de codificação de claimVida útil padrão de cinco minutos; jti é 16 bytes aleatórios, codificados em hexadecimal
QuotaChecker::check()TenantContext $tenant, TenantQuota $quotaLê o uso atual; avisa em 80%; rejeita em 100%; nega quando o uso é desconhecidoarray{allowed: bool, warning_percentage: float|null}QuotaExceededException, QuotaUnavailableExceptionCallback de alerta invocado em ambos os limiares
TenantQuotafloat $maxCuPerPeriod, coleções, bytes de armazenamento, jobs concorrentesLimites por período; constante de limiar flexível de 80%objeto de valorNadaPadrões de fromConfig(): 10,000 CU, 100 coleções, 10 GB, 10 jobs
QuotaExceededException::toErrorEnvelope()nenhumEnvelope de erro SPEC-QUOTA-001arrayHTTP 402, não repetível; carrega o valor atual, o limite e o instante de redefinição
QuotaUnavailableException::toErrorEnvelope()nenhumEnvelope de erro SPEC-QUOTA-503arrayHTTP 503, repetível; motivo usage_undeterminable
UsageMeter::pullUsage()array<string, int> $watermarksConsulta cada host de fonte de uso configurado a partir do seu cursorarray{events, instance_id}UsageMeterException quando todos os hosts estão inacessíveisInterrupção parcial tolerada; hosts inacessíveis registrados em log e ignorados
UsageMeter::getCurrentUsage()string $tenantIdUso de unidades de computação do período atualfloatUsageMeterException quando o uso é indeterminávelUm zero analisável é autoritativo; uso desconhecido lança exceção
StripeMeteringSyncer::sync()array<string, int> $watermarksUm ciclo de pull, transformação e envioarray{watermarks, sent, failed}Nada; falhas de envio são roteadas para o callback de DLQUma falha de pull retorna um ciclo no-op que preserva o cursor
StripeAdapter::sendMeterEvent()MeterEvent $eventPOST ao provedor com um cabeçalho de idempotênciavoidStripeSyncExceptionHTTP 429 e 5xx repetíveis; outros 4xx não repetíveis
StripeAdapter::sendBatch()list<MeterEvent> $eventsEnvia cada evento; coleta falhaslist<StripeSyncException>NadaUma lista vazia significa que todos os eventos tiveram sucesso
MeterEventnome do medidor, locatário, valor, chave de idempotência, timestampObjeto de valor de evento de medidor imutávelobjeto de valorNadatoStripePayload() serializa o payload do provedor
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 {}
}
  • Identidade do locatário. Um contexto de locatário é imutável: identificador do locatário, fonte de resolução, escopos. A identidade é resolvida apenas a partir do contexto autenticado (jwt, mtls, api_key) — nunca a partir de um cabeçalho ou parâmetro de consulta fornecido pelo cliente. Uma implantação de locatário único usa o contexto default fixo com escopos completos.
  • Ordem de autenticação. A autenticação por chave de API prossegue em uma ordem fixa: checksum, hash SHA-256, consulta no repositório, verificação de revogação, verificação de expiração, resolução de contexto. Chaves desconhecidas, revogadas e expiradas são três resultados distintos, todos HTTP 401; escopo insuficiente é HTTP 403.
  • Sigilo da chave. A chave em texto claro nunca é armazenada nem registrada em log; apenas o seu resumo SHA-256 é persistido e consultado. O autenticador não realiza nenhuma comparação byte a byte do segredo por conta própria; a consulta de resumo em tempo constante é o contrato da implementação do repositório.
  • Limiares de cota. No limite flexível de 80% a requisição prossegue, a porcentagem de aviso é retornada e o callback de alerta dispara. No limite rígido de 100% a requisição é rejeitada com SPEC-QUOTA-001 (HTTP 402) carregando o instante de redefinição — o primeiro dia do mês seguinte, meia-noite UTC.
  • Cota fail-closed. Uso indeterminável nega a requisição com SPEC-QUOTA-503 (HTTP 503, repetível). Uso desconhecido nunca é tratado como zero. Um uso zero genuíno e analisável é autoritativo e admite.
  • Deduplicação de alertas. O verificador não deduplica alertas; a deduplicação por período é responsabilidade do callback.
  • Sincronização de medição. O ciclo é agendado, nunca no caminho da requisição. Ele retoma a partir de marcas d’água por fonte e avança cada cursor até a maior identidade de evento enviada com sucesso. A chave de idempotência é determinística — locatário, período, identidade do evento — de modo que um evento reenviado colapsa na deduplicação do provedor.
  • Falha de pull. Um pull com falha retorna um ciclo no-op (sent 0, failed 0) que preserva as marcas d’água; o próximo ciclo tenta novamente a mesma janela em vez de pulá-la.
  • Tokens de serviço. Os tokens são HS256 com um segredo compartilhado e carregam iss, aud, sub, scope, tenant_id, iat, exp e um jti único. A vida útil padrão é de cinco minutos. A construção rejeita um segredo com menos de 16 bytes, de forma fail-closed.
  • Uma chave malformada falha no checksum e é rejeitada antes de qualquer acesso ao datastore. Uma chave bem formada mas desconhecida é rejeitada após a consulta. Ambas surgem como o resultado de chave inválida.
  • Chaves desconhecidas, revogadas e expiradas usam fábricas de exceção distintas; o sinalizador keyExpired é verdadeiro apenas no resultado de expiração. Mapeie-as para respostas de cliente distintas.
  • QuotaChecker::check() retorna apenas na admissão; o allowed retornado é sempre true. Rejeição e indisponibilidade são resultados excepcionais.
  • TenantQuota::usagePercentage() retorna 0.0 para uma cota não positiva; fromConfig() substitui valores ausentes por padrões e limita os limites inteiros a pelo menos 1.
  • As marcas d’água são por fonte; uma marca d’água ausente parte do início do fluxo daquela fonte (cursor 0). Uma implantação com múltiplas fontes mantém marcas d’água independentes.
  • A transformação ignora eventos que não são array, eventos com uma operação ou locatário ausente ou vazio, um valor não positivo, ou uma operação não mapeada — sem fazer o ciclo falhar. Um evento sem uma identidade inteira positiva utilizável é recusado com um aviso: uma chave de fallback aleatória anularia a deduplicação do lado do provedor e poderia cobrar em dobro o locatário.
  • Dez falhas de envio consecutivas escalam para uma entrada de log crítica; o contador reinicia em qualquer envio bem-sucedido. Todo evento com falha ainda alcança o callback de dead-letter.
  • Um corpo JSON malformado de um host de fonte de uso produz uma lista de eventos vazia, não uma falha de ciclo. pullUsage() lança exceção apenas quando todos os hosts configurados estão inacessíveis.
  • As primitivas de resumo e MAC são SHA-256 e HMAC-SHA256 através do provedor de criptografia PHP do host. Um build com restrição FIPS falha de forma segura em um algoritmo não aprovado em vez de fazer downgrade; a camada SaaS não acrescenta nenhuma política criptográfica própria.
  • Os corpos de chave e os identificadores de token vêm do CSPRNG (random_int(), random_bytes()).
  • O checksum CRC32 não é um controle criptográfico e não é afetado pelo modo FIPS.

As declarações abaixo descrevem a capacidade em relação às cláusulas citadas. Elas não são reivindicações de certificação; o NextPDF não detém nenhuma certificação para este módulo.

ComportamentoReferência
Semântica not-after do exp do token de serviçoRFC 7519 §4.1.4
Serialização compacta JWS do token de serviçoRFC 7515 §3.1
Piso de segredo HS256 de 16 bytes; nenhuma senha memorizável por humanos como chave MACRFC 8725 §3.5 (ameaça: §2.2)
Contrato de consulta de resumo em tempo constante do repositórioOWASP ASVS 5.0 §11.2.4
Resumo de armazenamento da chave de API SHA-256FIPS 180-4 (declarado em código)

As citações de RFC 8725 e OWASP ASVS 5.0 são verificadas por RAG; os identificadores de referência completos estão registrados no frontmatter desta página. As referências FIPS 180-4, FIPS 198-1 e BSI TR-02102-1 são declaradas em código na fonte do produto (hash('sha256', …) e o piso de chave documentado do minter); elas não foram recuperadas do corpus RAG para esta página. O requisito de tempo constante da ASVS §11.2.4 vincula a implementação do repositório que o operador fornece, não a própria classe do autenticador.

  • Forneça implementações duráveis de ApiKeyRepositoryInterface e StripeAdapterInterface; o pacote entrega os contratos e um cliente de provedor PSR-18, não a persistência.
  • As dependências são apenas abstrações PSR: logger PSR-3, cliente HTTP PSR-18, fábricas de requisição e stream PSR-17. Nenhum SDK de provedor é exigido.
  • Execute a sincronização de medição como um job agendado. Persista as marcas d’água retornadas de forma durável após cada ciclo.
  • Apresente a porcentagem de aviso de cota aos clientes, por exemplo como um cabeçalho de aviso, e deduplique os alertas de cota por período no callback.
  • Forneça o segredo do minter de tokens a partir da configuração como um valor aleatório de alta entropia; recomendam-se 32 ou mais bytes aleatórios. Nunca o derive de uma senha.
  • Os prefixos de chave tornam o ambiente visível sem uma consulta; chaves de sandbox e de produção nunca colidem porque o prefixo participa do resumo armazenado.
  • Detalhes internos de mecanismo permanecem na documentação interna do repositório de origem e estão fora do escopo deste manual.

Esta página documenta apenas o comportamento observável externamente e a superfície pública suportada da API. Caminhos de namespace internos, classes auxiliares, tabelas de mecanismo, nomes de arquivo de runbook e prefixos de tíquete estão fora do escopo.