Ir al contenido
getnextpdf.com

Enterprise edición

SaaS — Referencia detallada

El módulo SaaS de Enterprise aporta los componentes multiinquilino para un servicio basado en NextPDF.

  • TenantContext es un objeto de valor de identidad inmutable, resuelto únicamente a partir del contexto autenticado.
  • ApiKeyGenerator y ApiKeyAuthenticator emiten y validan claves de API con prefijo, suma de comprobación y almacenamiento por hash.
  • QuotaChecker regula las solicitudes frente a las cuotas por inquilino: avisa al 80 %, rechaza al 100 % y deniega en modo de fallo seguro cuando se desconoce el consumo.
  • SidecarJwtMinter acuña tokens de servicio HS256 de corta duración para las llamadas entre componentes.
  • UsageMeter y StripeMeteringSyncer extraen los eventos de consumo y los sincronizan con el proveedor de facturación con idempotencia determinista.

Esta capacidad se incluye en NextPDF Enterprise (nextpdf/enterprise) y se activa con un sobre de licencia de nivel Enterprise. Una implementación sin ese derecho no carga las clases de la capacidad. Comparar ediciones y obtener una licencia.

La superficie SaaS es una capacidad base de Enterprise; no existe ningún indicador por función independiente. NextPDF Core (Apache-2.0) y NextPDF Pro no tienen modelo de inquilinos, claves de API ni cuotas; esta capacidad no tiene equivalente en niveles inferiores.

Ventana de terminal
composer require nextpdf/enterprise:^3

Todos los símbolos residen bajo NextPDF\Enterprise\SaaS.

SímboloParámetrosComportamiento por defectoDevuelveLanza o falla conNotas
TenantContextstring $tenantId, string $source, array $scopes = ['read']Objeto de valor de identidad inmutableobjeto de valorNadaFuentes: jwt, mtls, api_key; hasScope() / hasAnyScope() comprueban ámbitos
TenantContext::singleTenant()ningunoInquilino fijo default con read, write, adminTenantContextNadaImplementaciones de un solo inquilino
ApiKeyAuthenticator::authenticate()string $rawKeyValidación en seis pasos y, después, resolución del contextoTenantContextApiKeyAuthenticationException (HTTP 401)El source del contexto es api_key; los ámbitos se copian del registro de la clave
ApiKeyAuthenticator::requireScope()TenantContext $context, ApiKeyScope $requiredScopeAserción explícita de ámbitovoidApiKeyAuthenticationException::insufficientScope() (HTTP 403)La aplicación de ámbitos es un paso independiente y explícito
ApiKeyGenerator::generateLive() / ::generateTest()ningunoClave nueva: prefijo, cuerpo base62 de 32 caracteres (192 bits de entropía), suma de comprobación de 4 caracteresarray{key, hash, prefix}NadaPrefijos npf_live_ / npf_test_; hash es el resumen de almacenamiento
ApiKeyGenerator::validateChecksum()string $keyComprobación de la forma del prefijo, la longitud y la suma de comprobación CRC32boolNadaProtección contra erratas previa a cualquier consulta al almacén; no es un control de seguridad
ApiKeyGenerator::hashKey() (estático)string $keyResumen hexadecimal SHA-256 de la clave en brutostringNadaLa única representación almacenada de una clave
ApiKeyGenerator::isLiveKey() / ::isTestKey()string $keyInspección del prefijoboolNadaEntorno visible sin consulta
ApiKeyid, inquilino, hash de la clave, prefijo visible, máscara de ámbito, instantes de creación/expiración/revocaciónRegistro de clave almacenado; el texto plano nunca se persisteobjeto de valorNadaisActive(), isRevoked(), isExpired(), scopeNames()
ApiKeyScopeenum respaldado: Read = 1, Write = 2, Admin = 4Modelo de ámbito por máscara de bitsenumNadamaskFromNames(), fromName(), fullAccess(); el constructor de la máscara ignora los nombres desconocidos
ApiKeyRepositoryInterfaceContrato de almacenamiento; persistencia solo por hashDefinido por la implementaciónfindByHash(), findActiveByTenant(), store(), revoke()
SidecarJwtMinter::__construct()string $secret, emisor, audiencia, int $ttlSeconds = 300Rechaza un secreto de firma de menos de 16 bytes en la construccióninstanciaInvalidArgumentExceptionUmbral mínimo de solidez de clave de 128 bits; se recomiendan 32 bytes aleatorios o más
SidecarJwtMinter::mint()TenantContext $tenantJWT HS256 con iss, aud, sub, scope, tenant_id, iat, exp, jtistringJsonException si falla la codificación de las reivindicacionesDuración por defecto de cinco minutos; jti son 16 bytes aleatorios codificados en hexadecimal
QuotaChecker::check()TenantContext $tenant, TenantQuota $quotaLee el consumo actual; avisa al 80 %; rechaza al 100 %; deniega cuando se desconoce el consumoarray{allowed: bool, warning_percentage: float|null}QuotaExceededException, QuotaUnavailableExceptionEl callback de alerta se invoca en ambos umbrales
TenantQuotafloat $maxCuPerPeriod, colecciones, bytes de almacenamiento, trabajos concurrentesLímites por periodo; constante de umbral flexible del 80 %objeto de valorNadaValores por defecto de fromConfig(): 10 000 CU, 100 colecciones, 10 GB, 10 trabajos
QuotaExceededException::toErrorEnvelope()ningunoSobre de error SPEC-QUOTA-001arrayHTTP 402, no reintentable; incluye el consumo actual, el límite y el instante de reinicio
QuotaUnavailableException::toErrorEnvelope()ningunoSobre de error SPEC-QUOTA-503arrayHTTP 503, reintentable; motivo usage_undeterminable
UsageMeter::pullUsage()array<string, int> $watermarksConsulta cada host de fuente de consumo configurado desde su cursorarray{events, instance_id}UsageMeterException cuando todos los hosts son inalcanzablesSe tolera una interrupción parcial; los hosts inalcanzables se registran y se omiten
UsageMeter::getCurrentUsage()string $tenantIdConsumo de unidades de cómputo del periodo actualfloatUsageMeterException cuando el consumo es indeterminableUn cero analizable es autoritativo; un consumo desconocido lanza excepción
StripeMeteringSyncer::sync()array<string, int> $watermarksUn ciclo de extracción, transformación y envíoarray{watermarks, sent, failed}Nada; los fallos de envío se enrutan al callback de la DLQUn fallo de extracción devuelve un ciclo sin efecto que conserva el cursor
StripeAdapter::sendMeterEvent()MeterEvent $eventPOST al proveedor con una cabecera de idempotenciavoidStripeSyncExceptionHTTP 429 y 5xx reintentables; otros 4xx no reintentables
StripeAdapter::sendBatch()list<MeterEvent> $eventsEnvía cada evento; recopila los falloslist<StripeSyncException>NadaUna lista vacía significa que todos los eventos se enviaron correctamente
MeterEventnombre del medidor, inquilino, valor, clave de idempotencia, marca de tiempoObjeto de valor de evento de medición inmutableobjeto de valorNadatoStripePayload() serializa la carga útil del proveedor
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 {}
}
  • Identidad de inquilino. Un contexto de inquilino es inmutable: identificador de inquilino, fuente de resolución, ámbitos. La identidad se resuelve únicamente a partir del contexto autenticado (jwt, mtls, api_key), nunca a partir de una cabecera o un parámetro de consulta proporcionados por el cliente. Una implementación de un solo inquilino utiliza el contexto fijo default con todos los ámbitos.
  • Orden de autenticación. La autenticación por clave de API se realiza en un orden fijo: suma de comprobación, hash SHA-256, consulta al repositorio, comprobación de revocación, comprobación de expiración, resolución del contexto. Las claves desconocidas, revocadas y expiradas son tres resultados distintos, todos HTTP 401; un ámbito insuficiente es HTTP 403.
  • Secreto de la clave. La clave en bruto nunca se almacena ni se registra; solo se persiste y se consulta su resumen SHA-256. El autenticador no realiza por sí mismo ninguna comparación byte a byte del secreto; la consulta de resumen en tiempo constante es el contrato de la implementación del repositorio.
  • Umbrales de cuota. En el límite flexible del 80 % la solicitud continúa, se devuelve el porcentaje de aviso y se dispara el callback de alerta. En el límite estricto del 100 % la solicitud se rechaza con SPEC-QUOTA-001 (HTTP 402), que incluye el instante de reinicio: el primer día del mes siguiente, medianoche UTC.
  • Fallo seguro de cuota. Un consumo indeterminable deniega la solicitud con SPEC-QUOTA-503 (HTTP 503, reintentable). Un consumo desconocido nunca se trata como cero. Un consumo genuino de cero, analizable, es autoritativo y admite la solicitud.
  • Desduplicación de alertas. El verificador no desduplica las alertas; la desduplicación por periodo es responsabilidad del callback.
  • Sincronización de medición. El ciclo se programa, nunca está en la ruta de la solicitud. Se reanuda desde las marcas de agua por fuente y avanza cada cursor hasta la identidad de evento enviada correctamente más alta. La clave de idempotencia es determinista —inquilino, periodo, identidad de evento—, de modo que un evento reenviado se colapsa en la desduplicación del proveedor.
  • Fallo de extracción. Una extracción fallida devuelve un ciclo sin efecto (sent 0, failed 0) que conserva las marcas de agua; el ciclo siguiente reintenta la misma ventana en lugar de omitirla.
  • Tokens de servicio. Los tokens son HS256 con un secreto compartido y contienen iss, aud, sub, scope, tenant_id, iat, exp y un jti único. La duración por defecto es de cinco minutos. La construcción rechaza un secreto de menos de 16 bytes, en modo de fallo seguro.
  • Una clave malformada no supera la suma de comprobación y se rechaza antes de cualquier acceso al almacén. Una clave bien formada pero desconocida se rechaza tras la consulta. Ambas se manifiestan como el resultado de clave no válida.
  • Las claves desconocidas, revocadas y expiradas usan fábricas de excepciones distintas; el indicador keyExpired solo es verdadero en el resultado de expiración. Asígnalas a respuestas de cliente distintas.
  • QuotaChecker::check() solo retorna en caso de admisión; el allowed devuelto es siempre true. El rechazo y la indisponibilidad son resultados excepcionales.
  • TenantQuota::usagePercentage() devuelve 0.0 para una cuota no positiva; fromConfig() sustituye los valores ausentes por los valores por defecto y ajusta los límites enteros a un mínimo de 1.
  • Las marcas de agua son por fuente; una marca de agua ausente parte desde el inicio del flujo de esa fuente (cursor 0). Una implementación de múltiples fuentes mantiene marcas de agua independientes.
  • La transformación omite los eventos que no son arrays, los eventos sin operación o inquilino o con estos vacíos, con un valor no positivo o con una operación no mapeada, sin hacer fallar el ciclo. Un evento que carece de una identidad de entero positivo utilizable se rechaza con un aviso: una clave de reserva aleatoria anularía la desduplicación del lado del proveedor y podría facturar dos veces al inquilino.
  • Diez fallos de envío consecutivos escalan a una entrada de log crítica; el contador se reinicia con cualquier envío correcto. Cada evento fallido llega igualmente al callback de la cola de mensajes fallidos.
  • Un cuerpo JSON malformado de un host de fuente de consumo produce una lista de eventos vacía, no un fallo de ciclo. pullUsage() solo lanza excepción cuando todos los hosts configurados son inalcanzables.
  • Las primitivas de resumen y MAC son SHA-256 y HMAC-SHA256 a través del proveedor criptográfico de PHP del host. Una compilación restringida a FIPS falla de forma segura ante un algoritmo no aprobado en lugar de degradarse; la capa SaaS no añade ninguna política criptográfica propia.
  • Los cuerpos de las claves y los identificadores de token provienen del CSPRNG (random_int(), random_bytes()).
  • La suma de comprobación CRC32 no es un control criptográfico y no se ve afectada por el modo FIPS.

Las afirmaciones siguientes describen la capacidad frente a las cláusulas citadas. No son declaraciones de certificación; NextPDF no posee ninguna certificación para este módulo.

ComportamientoReferencia
Semántica de no-posterior del exp del token de servicioRFC 7519 §4.1.4
Serialización compacta JWS del token de servicioRFC 7515 §3.1
Umbral mínimo de 16 bytes para el secreto HS256; sin contraseñas memorizables por una persona como claves MACRFC 8725 §3.5 (amenaza: §2.2)
Contrato de tiempo constante para la consulta de resumen del repositorioOWASP ASVS 5.0 §11.2.4
Resumen SHA-256 de almacenamiento de la clave de APIFIPS 180-4 (declarado en el código)

Las citas de RFC 8725 y OWASP ASVS 5.0 están verificadas por RAG; los identificadores de referencia completos se registran en el frontmatter de esta página. Las referencias FIPS 180-4, FIPS 198-1 y BSI TR-02102-1 están declaradas en el código fuente del producto (hash('sha256', …) y el umbral mínimo de clave documentado del acuñador); no se recuperaron del corpus RAG para esta página. El requisito de tiempo constante de ASVS §11.2.4 vincula la implementación del repositorio que proporciona el operador, no la propia clase autenticadora.

  • Proporciona implementaciones duraderas de ApiKeyRepositoryInterface y StripeAdapterInterface; el paquete incluye los contratos y un cliente de proveedor PSR-18, no la persistencia.
  • Las dependencias son únicamente abstracciones PSR: logger PSR-3, cliente HTTP PSR-18, fábricas de solicitud y de flujo PSR-17. No se requiere ningún SDK de proveedor.
  • Ejecuta la sincronización de medición como un trabajo programado. Persiste de forma duradera las marcas de agua devueltas tras cada ciclo.
  • Expón el porcentaje de aviso de cuota a los clientes, por ejemplo como una cabecera de aviso, y desduplica las alertas de cuota por periodo en el callback.
  • Suministra el secreto del acuñador de tokens desde la configuración como un valor aleatorio de alta entropía; se recomiendan 32 bytes aleatorios o más. Nunca lo derives de una contraseña.
  • Los prefijos de clave hacen visible el entorno sin consulta; las claves de sandbox y de producción nunca colisionan porque el prefijo participa en el resumen almacenado.
  • El detalle de los mecanismos internos permanece en la documentación interna del repositorio de código fuente y queda fuera del alcance de este manual.

Esta página documenta únicamente el comportamiento observable externamente y la superficie de API pública admitida. Las rutas de espacios de nombres internos, las clases auxiliares, las tablas de mecanismos, los nombres de archivo de runbook y los prefijos de tickets quedan fuera de alcance.