Enterprise edição
SaaS — Referência Profunda
Visão geral
Seção intitulada “Visão geral”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.ApiKeyGeneratoreApiKeyAuthenticatoremitem e validam chaves de API com prefixo, checksum e armazenadas por hash.QuotaCheckercontrola 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.SidecarJwtMintergera tokens de serviço HS256 de vida curta para chamadas entre componentes.UsageMetereStripeMeteringSyncerpuxam eventos de uso e os sincronizam com o provedor de cobrança com idempotência determinística.
Disponibilidade e licenciamento
Seção intitulada “Disponibilidade e licenciamento”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.
composer require nextpdf/enterprise:^3Superfície pública da API
Seção intitulada “Superfície pública da API”Todos os símbolos residem sob NextPDF\Enterprise\SaaS.
| Símbolo | Parâmetros | Comportamento padrão | Retorna | Lança ou falha com | Notas |
|---|---|---|---|---|---|
TenantContext | string $tenantId, string $source, array $scopes = ['read'] | Objeto de valor de identidade imutável | objeto de valor | Nada | Fontes: jwt, mtls, api_key; hasScope() / hasAnyScope() testam escopos |
TenantContext::singleTenant() | nenhum | Locatário default fixo com read, write, admin | TenantContext | Nada | Implantações de locatário único |
ApiKeyAuthenticator::authenticate() | string $rawKey | Validação em seis etapas, depois resolução de contexto | TenantContext | ApiKeyAuthenticationException (HTTP 401) | O source do contexto é api_key; escopos copiados do registro da chave |
ApiKeyAuthenticator::requireScope() | TenantContext $context, ApiKeyScope $requiredScope | Asserção explícita de escopo | void | ApiKeyAuthenticationException::insufficientScope() (HTTP 403) | A imposição de escopo é uma etapa explícita separada |
ApiKeyGenerator::generateLive() / ::generateTest() | nenhum | Nova chave: prefixo, corpo base62 de 32 caracteres (entropia de 192 bits), checksum de 4 caracteres | array{key, hash, prefix} | Nada | Prefixos npf_live_ / npf_test_; hash é o resumo de armazenamento |
ApiKeyGenerator::validateChecksum() | string $key | Verificação de formato de prefixo, comprimento e checksum CRC32 | bool | Nada | Proteção contra erro de digitação antes de qualquer consulta ao datastore; não é um controle de segurança |
ApiKeyGenerator::hashKey() (estático) | string $key | Resumo hexadecimal SHA-256 da chave em texto claro | string | Nada | A única representação armazenada de uma chave |
ApiKeyGenerator::isLiveKey() / ::isTestKey() | string $key | Inspeção de prefixo | bool | Nada | Ambiente visível sem uma consulta |
ApiKey | id, locatário, hash da chave, prefixo de exibição, máscara de escopo, instantes de criação/expiração/revogação | Registro de chave armazenado; texto claro nunca persistido | objeto de valor | Nada | isActive(), isRevoked(), isExpired(), scopeNames() |
ApiKeyScope | enum tipado: Read = 1, Write = 2, Admin = 4 | Modelo de escopo por máscara de bits | enum | Nada | maskFromNames(), fromName(), fullAccess(); nomes desconhecidos são ignorados pelo construtor de máscara |
ApiKeyRepositoryInterface | — | Contrato de armazenamento; persistência apenas por hash | — | Definido pela implementação | findByHash(), findActiveByTenant(), store(), revoke() |
SidecarJwtMinter::__construct() | string $secret, issuer, audience, int $ttlSeconds = 300 | Rejeita um segredo de assinatura com menos de 16 bytes na construção | instância | InvalidArgumentException | Piso de força de chave de 128 bits; recomendam-se 32 ou mais bytes aleatórios |
SidecarJwtMinter::mint() | TenantContext $tenant | JWT HS256 com iss, aud, sub, scope, tenant_id, iat, exp, jti | string | JsonException em falha de codificação de claim | Vida útil padrão de cinco minutos; jti é 16 bytes aleatórios, codificados em hexadecimal |
QuotaChecker::check() | TenantContext $tenant, TenantQuota $quota | Lê o uso atual; avisa em 80%; rejeita em 100%; nega quando o uso é desconhecido | array{allowed: bool, warning_percentage: float|null} | QuotaExceededException, QuotaUnavailableException | Callback de alerta invocado em ambos os limiares |
TenantQuota | float $maxCuPerPeriod, coleções, bytes de armazenamento, jobs concorrentes | Limites por período; constante de limiar flexível de 80% | objeto de valor | Nada | Padrões de fromConfig(): 10,000 CU, 100 coleções, 10 GB, 10 jobs |
QuotaExceededException::toErrorEnvelope() | nenhum | Envelope de erro SPEC-QUOTA-001 | array | — | HTTP 402, não repetível; carrega o valor atual, o limite e o instante de redefinição |
QuotaUnavailableException::toErrorEnvelope() | nenhum | Envelope de erro SPEC-QUOTA-503 | array | — | HTTP 503, repetível; motivo usage_undeterminable |
UsageMeter::pullUsage() | array<string, int> $watermarks | Consulta cada host de fonte de uso configurado a partir do seu cursor | array{events, instance_id} | UsageMeterException quando todos os hosts estão inacessíveis | Interrupção parcial tolerada; hosts inacessíveis registrados em log e ignorados |
UsageMeter::getCurrentUsage() | string $tenantId | Uso de unidades de computação do período atual | float | UsageMeterException quando o uso é indeterminável | Um zero analisável é autoritativo; uso desconhecido lança exceção |
StripeMeteringSyncer::sync() | array<string, int> $watermarks | Um ciclo de pull, transformação e envio | array{watermarks, sent, failed} | Nada; falhas de envio são roteadas para o callback de DLQ | Uma falha de pull retorna um ciclo no-op que preserva o cursor |
StripeAdapter::sendMeterEvent() | MeterEvent $event | POST ao provedor com um cabeçalho de idempotência | void | StripeSyncException | HTTP 429 e 5xx repetíveis; outros 4xx não repetíveis |
StripeAdapter::sendBatch() | list<MeterEvent> $events | Envia cada evento; coleta falhas | list<StripeSyncException> | Nada | Uma lista vazia significa que todos os eventos tiveram sucesso |
MeterEvent | nome do medidor, locatário, valor, chave de idempotência, timestamp | Objeto de valor de evento de medidor imutável | objeto de valor | Nada | toStripePayload() 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 {}}Contrato de comportamento
Seção intitulada “Contrato de comportamento”- 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 contextodefaultfixo 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 (
sent0,failed0) 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,expe umjtiúnico. A vida útil padrão é de cinco minutos. A construção rejeita um segredo com menos de 16 bytes, de forma fail-closed.
Casos extremos e modos de falha
Seção intitulada “Casos extremos e modos de falha”- 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; oallowedretornado é sempretrue. Rejeição e indisponibilidade são resultados excepcionais.TenantQuota::usagePercentage()retorna0.0para 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.
Comportamento em modo FIPS
Seção intitulada “Comportamento em modo FIPS”- 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.
Conformidade
Seção intitulada “Conformidade”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.
| Comportamento | Referência |
|---|---|
Semântica not-after do exp do token de serviço | RFC 7519 §4.1.4 |
| Serialização compacta JWS do token de serviço | RFC 7515 §3.1 |
| Piso de segredo HS256 de 16 bytes; nenhuma senha memorizável por humanos como chave MAC | RFC 8725 §3.5 (ameaça: §2.2) |
| Contrato de consulta de resumo em tempo constante do repositório | OWASP ASVS 5.0 §11.2.4 |
| Resumo de armazenamento da chave de API SHA-256 | FIPS 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.
Notas de desenvolvimento
Seção intitulada “Notas de desenvolvimento”- Forneça implementações duráveis de
ApiKeyRepositoryInterfaceeStripeAdapterInterface; 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.
Limite de publicação
Seção intitulada “Limite de publicação”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.