Enterprise edición
SaaS — Referencia detallada
De un vistazo
Sección titulada «De un vistazo»El módulo SaaS de Enterprise aporta los componentes multiinquilino para un servicio basado en NextPDF.
TenantContextes un objeto de valor de identidad inmutable, resuelto únicamente a partir del contexto autenticado.ApiKeyGeneratoryApiKeyAuthenticatoremiten y validan claves de API con prefijo, suma de comprobación y almacenamiento por hash.QuotaCheckerregula 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.SidecarJwtMinteracuña tokens de servicio HS256 de corta duración para las llamadas entre componentes.UsageMeteryStripeMeteringSyncerextraen los eventos de consumo y los sincronizan con el proveedor de facturación con idempotencia determinista.
Disponibilidad y licencias
Sección titulada «Disponibilidad y licencias»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.
composer require nextpdf/enterprise:^3Superficie de API pública
Sección titulada «Superficie de API pública»Todos los símbolos residen bajo NextPDF\Enterprise\SaaS.
| Símbolo | Parámetros | Comportamiento por defecto | Devuelve | Lanza o falla con | Notas |
|---|---|---|---|---|---|
TenantContext | string $tenantId, string $source, array $scopes = ['read'] | Objeto de valor de identidad inmutable | objeto de valor | Nada | Fuentes: jwt, mtls, api_key; hasScope() / hasAnyScope() comprueban ámbitos |
TenantContext::singleTenant() | ninguno | Inquilino fijo default con read, write, admin | TenantContext | Nada | Implementaciones de un solo inquilino |
ApiKeyAuthenticator::authenticate() | string $rawKey | Validación en seis pasos y, después, resolución del contexto | TenantContext | ApiKeyAuthenticationException (HTTP 401) | El source del contexto es api_key; los ámbitos se copian del registro de la clave |
ApiKeyAuthenticator::requireScope() | TenantContext $context, ApiKeyScope $requiredScope | Aserción explícita de ámbito | void | ApiKeyAuthenticationException::insufficientScope() (HTTP 403) | La aplicación de ámbitos es un paso independiente y explícito |
ApiKeyGenerator::generateLive() / ::generateTest() | ninguno | Clave nueva: prefijo, cuerpo base62 de 32 caracteres (192 bits de entropía), suma de comprobación de 4 caracteres | array{key, hash, prefix} | Nada | Prefijos npf_live_ / npf_test_; hash es el resumen de almacenamiento |
ApiKeyGenerator::validateChecksum() | string $key | Comprobación de la forma del prefijo, la longitud y la suma de comprobación CRC32 | bool | Nada | Protección contra erratas previa a cualquier consulta al almacén; no es un control de seguridad |
ApiKeyGenerator::hashKey() (estático) | string $key | Resumen hexadecimal SHA-256 de la clave en bruto | string | Nada | La única representación almacenada de una clave |
ApiKeyGenerator::isLiveKey() / ::isTestKey() | string $key | Inspección del prefijo | bool | Nada | Entorno visible sin consulta |
ApiKey | id, inquilino, hash de la clave, prefijo visible, máscara de ámbito, instantes de creación/expiración/revocación | Registro de clave almacenado; el texto plano nunca se persiste | objeto de valor | Nada | isActive(), isRevoked(), isExpired(), scopeNames() |
ApiKeyScope | enum respaldado: Read = 1, Write = 2, Admin = 4 | Modelo de ámbito por máscara de bits | enum | Nada | maskFromNames(), fromName(), fullAccess(); el constructor de la máscara ignora los nombres desconocidos |
ApiKeyRepositoryInterface | — | Contrato de almacenamiento; persistencia solo por hash | — | Definido por la implementación | findByHash(), findActiveByTenant(), store(), revoke() |
SidecarJwtMinter::__construct() | string $secret, emisor, audiencia, int $ttlSeconds = 300 | Rechaza un secreto de firma de menos de 16 bytes en la construcción | instancia | InvalidArgumentException | Umbral mínimo de solidez de clave de 128 bits; se recomiendan 32 bytes aleatorios o más |
SidecarJwtMinter::mint() | TenantContext $tenant | JWT HS256 con iss, aud, sub, scope, tenant_id, iat, exp, jti | string | JsonException si falla la codificación de las reivindicaciones | Duración por defecto de cinco minutos; jti son 16 bytes aleatorios codificados en hexadecimal |
QuotaChecker::check() | TenantContext $tenant, TenantQuota $quota | Lee el consumo actual; avisa al 80 %; rechaza al 100 %; deniega cuando se desconoce el consumo | array{allowed: bool, warning_percentage: float|null} | QuotaExceededException, QuotaUnavailableException | El callback de alerta se invoca en ambos umbrales |
TenantQuota | float $maxCuPerPeriod, colecciones, bytes de almacenamiento, trabajos concurrentes | Límites por periodo; constante de umbral flexible del 80 % | objeto de valor | Nada | Valores por defecto de fromConfig(): 10 000 CU, 100 colecciones, 10 GB, 10 trabajos |
QuotaExceededException::toErrorEnvelope() | ninguno | Sobre de error SPEC-QUOTA-001 | array | — | HTTP 402, no reintentable; incluye el consumo actual, el límite y el instante de reinicio |
QuotaUnavailableException::toErrorEnvelope() | ninguno | Sobre de error SPEC-QUOTA-503 | array | — | HTTP 503, reintentable; motivo usage_undeterminable |
UsageMeter::pullUsage() | array<string, int> $watermarks | Consulta cada host de fuente de consumo configurado desde su cursor | array{events, instance_id} | UsageMeterException cuando todos los hosts son inalcanzables | Se tolera una interrupción parcial; los hosts inalcanzables se registran y se omiten |
UsageMeter::getCurrentUsage() | string $tenantId | Consumo de unidades de cómputo del periodo actual | float | UsageMeterException cuando el consumo es indeterminable | Un cero analizable es autoritativo; un consumo desconocido lanza excepción |
StripeMeteringSyncer::sync() | array<string, int> $watermarks | Un ciclo de extracción, transformación y envío | array{watermarks, sent, failed} | Nada; los fallos de envío se enrutan al callback de la DLQ | Un fallo de extracción devuelve un ciclo sin efecto que conserva el cursor |
StripeAdapter::sendMeterEvent() | MeterEvent $event | POST al proveedor con una cabecera de idempotencia | void | StripeSyncException | HTTP 429 y 5xx reintentables; otros 4xx no reintentables |
StripeAdapter::sendBatch() | list<MeterEvent> $events | Envía cada evento; recopila los fallos | list<StripeSyncException> | Nada | Una lista vacía significa que todos los eventos se enviaron correctamente |
MeterEvent | nombre del medidor, inquilino, valor, clave de idempotencia, marca de tiempo | Objeto de valor de evento de medición inmutable | objeto de valor | Nada | toStripePayload() 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 {}}Contrato de comportamiento
Sección titulada «Contrato de comportamiento»- 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 fijodefaultcon 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 (
sent0,failed0) 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,expy unjtiú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.
Casos límite y modos de fallo
Sección titulada «Casos límite y modos de fallo»- 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
keyExpiredsolo es verdadero en el resultado de expiración. Asígnalas a respuestas de cliente distintas. QuotaChecker::check()solo retorna en caso de admisión; elalloweddevuelto es siempretrue. El rechazo y la indisponibilidad son resultados excepcionales.TenantQuota::usagePercentage()devuelve0.0para 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.
Comportamiento en modo FIPS
Sección titulada «Comportamiento en modo FIPS»- 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.
Conformidad
Sección titulada «Conformidad»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.
| Comportamiento | Referencia |
|---|---|
Semántica de no-posterior del exp del token de servicio | RFC 7519 §4.1.4 |
| Serialización compacta JWS del token de servicio | RFC 7515 §3.1 |
| Umbral mínimo de 16 bytes para el secreto HS256; sin contraseñas memorizables por una persona como claves MAC | RFC 8725 §3.5 (amenaza: §2.2) |
| Contrato de tiempo constante para la consulta de resumen del repositorio | OWASP ASVS 5.0 §11.2.4 |
| Resumen SHA-256 de almacenamiento de la clave de API | FIPS 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.
Notas de desarrollo
Sección titulada «Notas de desarrollo»- Proporciona implementaciones duraderas de
ApiKeyRepositoryInterfaceyStripeAdapterInterface; 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.
Límite de publicación
Sección titulada «Límite de publicación»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.