Enterprise edizione
SaaS — Riferimento approfondito
In breve
Sezione intitolata “In breve”Il modulo Enterprise SaaS fornisce i blocchi costitutivi multi-tenant per un servizio basato su NextPDF.
TenantContextè un value object di identità immutabile, risolto esclusivamente dal contesto autenticato.ApiKeyGeneratoreApiKeyAuthenticatoremettono e validano API key con prefisso, checksum e archiviate come hash.QuotaCheckerfiltra le richieste rispetto alle quote per-tenant: avviso all’80%, rifiuto al 100%, negazione fail-closed quando l’utilizzo è sconosciuto.SidecarJwtMinterconia token di servizio HS256 a breve durata per le chiamate inter-componente.UsageMetereStripeMeteringSyncerestraggono gli eventi di utilizzo e li sincronizzano con il provider di fatturazione con idempotenza deterministica.
Disponibilità e licenza
Sezione intitolata “Disponibilità e licenza”Questa capacità è inclusa in NextPDF Enterprise (nextpdf/enterprise) e si attiva con un envelope di licenza di tier Enterprise. Una distribuzione priva di tale entitlement non carica le classi della capacità. Confronta le edizioni e ottieni una licenza.
La superficie SaaS è una capacità Enterprise di base; non esiste un flag per-funzionalità separato. NextPDF Core (Apache-2.0) e NextPDF Pro non hanno alcun modello di tenancy, API key o quota; questa capacità non ha equivalente a un tier inferiore.
composer require nextpdf/enterprise:^3Superficie API pubblica
Sezione intitolata “Superficie API pubblica”Tutti i simboli risiedono sotto NextPDF\Enterprise\SaaS.
| Simbolo | Parametri | Comportamento predefinito | Restituisce | Solleva o fallisce con | Note |
|---|---|---|---|---|---|
TenantContext | string $tenantId, string $source, array $scopes = ['read'] | Value object di identità immutabile | value object | Nulla | Sorgenti: jwt, mtls, api_key; hasScope() / hasAnyScope() verificano gli scope |
TenantContext::singleTenant() | nessuno | Tenant default fisso con read, write, admin | TenantContext | Nulla | Distribuzioni single-tenant |
ApiKeyAuthenticator::authenticate() | string $rawKey | Validazione in sei passi, poi risoluzione del contesto | TenantContext | ApiKeyAuthenticationException (HTTP 401) | Il source del contesto è api_key; gli scope sono copiati dal record della chiave |
ApiKeyAuthenticator::requireScope() | TenantContext $context, ApiKeyScope $requiredScope | Asserzione esplicita dello scope | void | ApiKeyAuthenticationException::insufficientScope() (HTTP 403) | L’applicazione dello scope è un passo separato ed esplicito |
ApiKeyGenerator::generateLive() / ::generateTest() | nessuno | Nuova chiave: prefisso, corpo base62 di 32 caratteri (entropia a 192 bit), checksum di 4 caratteri | array{key, hash, prefix} | Nulla | Prefissi npf_live_ / npf_test_; hash è il digest di archiviazione |
ApiKeyGenerator::validateChecksum() | string $key | Controllo di forma su prefisso, lunghezza e checksum CRC32 | bool | Nulla | Protezione dagli errori di battitura prima di qualsiasi lookup sul datastore; non è un controllo di sicurezza |
ApiKeyGenerator::hashKey() (statico) | string $key | Digest esadecimale SHA-256 della chiave in chiaro | string | Nulla | L’unica rappresentazione archiviata di una chiave |
ApiKeyGenerator::isLiveKey() / ::isTestKey() | string $key | Ispezione del prefisso | bool | Nulla | Ambiente visibile senza un lookup |
ApiKey | id, tenant, hash della chiave, prefisso di visualizzazione, maschera degli scope, istanti di creazione/scadenza/revoca | Record di chiave archiviato; il testo in chiaro non è mai persistito | value object | Nulla | isActive(), isRevoked(), isExpired(), scopeNames() |
ApiKeyScope | backed enum: Read = 1, Write = 2, Admin = 4 | Modello di scope a bitmask | enum | Nulla | maskFromNames(), fromName(), fullAccess(); i nomi sconosciuti vengono ignorati dal costruttore della maschera |
ApiKeyRepositoryInterface | — | Contratto di archiviazione; persistenza solo per hash | — | Definito dall’implementazione | findByHash(), findActiveByTenant(), store(), revoke() |
SidecarJwtMinter::__construct() | string $secret, issuer, audience, int $ttlSeconds = 300 | Rifiuta un secret di firma inferiore a 16 byte alla costruzione | istanza | InvalidArgumentException | Soglia minima di robustezza della chiave a 128 bit; consigliati 32 o più byte casuali |
SidecarJwtMinter::mint() | TenantContext $tenant | JWT HS256 con iss, aud, sub, scope, tenant_id, iat, exp, jti | string | JsonException in caso di fallimento della codifica dei claim | Durata predefinita di cinque minuti; jti è composto da 16 byte casuali, codificati in esadecimale |
QuotaChecker::check() | TenantContext $tenant, TenantQuota $quota | Legge l’utilizzo corrente; avvisa all’80%; rifiuta al 100%; nega quando l’utilizzo è sconosciuto | array{allowed: bool, warning_percentage: float|null} | QuotaExceededException, QuotaUnavailableException | La callback di alert è invocata a entrambe le soglie |
TenantQuota | float $maxCuPerPeriod, collezioni, byte di storage, job concorrenti | Limiti per-periodo; costante di soglia soft all’80% | value object | Nulla | Valori predefiniti di fromConfig(): 10,000 CU, 100 collezioni, 10 GB, 10 job |
QuotaExceededException::toErrorEnvelope() | nessuno | Envelope di errore SPEC-QUOTA-001 | array | — | HTTP 402, non ritentabile; trasporta l’utilizzo corrente, il limite e l’istante di reset |
QuotaUnavailableException::toErrorEnvelope() | nessuno | Envelope di errore SPEC-QUOTA-503 | array | — | HTTP 503, ritentabile; motivo usage_undeterminable |
UsageMeter::pullUsage() | array<string, int> $watermarks | Interroga ogni host sorgente di utilizzo configurato a partire dal suo cursore | array{events, instance_id} | UsageMeterException quando ogni host è irraggiungibile | Interruzione parziale tollerata; gli host irraggiungibili vengono registrati e saltati |
UsageMeter::getCurrentUsage() | string $tenantId | Utilizzo di compute-unit del periodo corrente | float | UsageMeterException quando l’utilizzo non è determinabile | Uno zero analizzabile è autorevole; un utilizzo sconosciuto solleva un’eccezione |
StripeMeteringSyncer::sync() | array<string, int> $watermarks | Un ciclo di pull, trasformazione e invio | array{watermarks, sent, failed} | Nulla; i fallimenti di invio vengono instradati alla callback DLQ | Un fallimento di pull restituisce un ciclo no-op che preserva il cursore |
StripeAdapter::sendMeterEvent() | MeterEvent $event | POST al provider con un header di idempotenza | void | StripeSyncException | HTTP 429 e 5xx ritentabili; gli altri 4xx non ritentabili |
StripeAdapter::sendBatch() | list<MeterEvent> $events | Invia ogni evento; raccoglie i fallimenti | list<StripeSyncException> | Nulla | Una lista vuota significa che ogni evento è andato a buon fine |
MeterEvent | nome del meter, tenant, valore, chiave di idempotenza, timestamp | Value object di meter-event immutabile | value object | Nulla | toStripePayload() serializza il payload del provider |
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 {}}Contratto di comportamento
Sezione intitolata “Contratto di comportamento”- Identità del tenant. Un contesto tenant è immutabile: identificatore del tenant, sorgente di risoluzione, scope. L’identità è risolta esclusivamente dal contesto autenticato (
jwt,mtls,api_key) — mai da un header o da un parametro di query forniti dal client. Una distribuzione single-tenant usa il contestodefaultfisso con scope completi. - Ordine di autenticazione. L’autenticazione tramite API key procede in un ordine fisso: checksum, hash SHA-256, lookup nel repository, controllo di revoca, controllo di scadenza, risoluzione del contesto. Le chiavi sconosciute, revocate e scadute sono tre esiti distinti, tutti HTTP 401; uno scope insufficiente è HTTP 403.
- Segretezza della chiave. La chiave in chiaro non viene mai archiviata né registrata; solo il suo digest SHA-256 viene persistito e usato per il lookup. L’autenticatore non esegue di per sé alcun confronto byte per byte del secret; il lookup del digest a tempo costante è il contratto dell’implementazione del repository.
- Soglie quota. Al soft limit dell’80% la richiesta prosegue, viene restituita la percentuale di avviso e scatta la callback di alert. All’hard limit del 100% la richiesta è rifiutata con
SPEC-QUOTA-001(HTTP 402) che trasporta l’istante di reset — il primo giorno del mese successivo, mezzanotte UTC. - Quota fail-closed. Un utilizzo non determinabile nega la richiesta con
SPEC-QUOTA-503(HTTP 503, ritentabile). Un utilizzo sconosciuto non viene mai trattato come zero. Un utilizzo pari a zero genuino e analizzabile è autorevole e ammette la richiesta. - Deduplicazione degli alert. Il checker non deduplica gli alert; la deduplicazione per-periodo è responsabilità della callback.
- Sincronizzazione del metering. Il ciclo è pianificato, mai sul percorso della richiesta. Riprende dai watermark per-sorgente e avanza ogni cursore fino all’identità di evento inviata con successo più alta. La chiave di idempotenza è deterministica — tenant, periodo, identità dell’evento — così che un evento rispedito collassi sulla deduplicazione del provider.
- Fallimento di pull. Un pull fallito restituisce un ciclo no-op (
sent0,failed0) che preserva i watermark; il ciclo successivo ritenta la stessa finestra anziché saltarla. - Token di servizio. I token sono HS256 con un secret condiviso e trasportano
iss,aud,sub,scope,tenant_id,iat,expe unjtiunivoco. La durata predefinita è di cinque minuti. La costruzione rifiuta un secret inferiore a 16 byte, in modalità fail-closed.
Casi limite e modalità di guasto
Sezione intitolata “Casi limite e modalità di guasto”- Una chiave malformata fallisce il checksum ed è rifiutata prima di qualsiasi accesso al datastore. Una chiave ben formata ma sconosciuta è rifiutata dopo il lookup. Entrambe si manifestano come esito di chiave non valida.
- Le chiavi sconosciute, revocate e scadute usano factory di eccezione distinte; il flag
keyExpiredè vero solo nell’esito di chiave scaduta. Mapparle su risposte client distinte. QuotaChecker::check()restituisce solo in caso di ammissione; il valoreallowedrestituito è sempretrue. Il rifiuto e l’indisponibilità sono esiti eccezionali.TenantQuota::usagePercentage()restituisce0.0per una quota non positiva;fromConfig()sostituisce i valori assenti con quelli predefiniti e limita i valori interi ad almeno 1.- I watermark sono per-sorgente; un watermark mancante riparte dall’inizio dello stream di quella sorgente (cursore
0). Una distribuzione multi-sorgente mantiene watermark indipendenti. - La trasformazione salta gli eventi non-array, gli eventi con operazione o tenant mancanti o vuoti, con un valore non positivo o con un’operazione non mappata — senza far fallire il ciclo. Un evento privo di un’identità intera positiva utilizzabile è rifiutato con un avviso: una chiave di fallback casuale vanificherebbe la deduplicazione lato provider e potrebbe fatturare due volte il tenant.
- Dieci fallimenti di invio consecutivi escalano a una voce di log critica; il contatore si azzera a ogni invio riuscito. Ogni evento fallito raggiunge comunque la callback dead-letter.
- Un corpo JSON malformato da un host sorgente di utilizzo produce una lista di eventi vuota, non un fallimento del ciclo.
pullUsage()solleva un’eccezione solo quando ogni host configurato è irraggiungibile.
Comportamento in modalità FIPS
Sezione intitolata “Comportamento in modalità FIPS”- Le primitive di digest e MAC sono SHA-256 e HMAC-SHA256 attraverso il provider crittografico PHP dell’host. Una build con vincoli FIPS fallisce in modo chiuso su un algoritmo non approvato anziché degradare; lo strato SaaS non aggiunge alcuna politica crittografica propria.
- I corpi delle chiavi e gli identificatori dei token provengono dal CSPRNG (
random_int(),random_bytes()). - Il checksum CRC32 non è un controllo crittografico e non è influenzato dalla modalità FIPS.
Conformità
Sezione intitolata “Conformità”Le affermazioni che seguono descrivono la capacità rispetto alle clausole citate. Non sono dichiarazioni di certificazione; NextPDF non detiene alcuna certificazione per questo modulo.
| Comportamento | Riferimento |
|---|---|
Semantica not-after di exp del token di servizio | RFC 7519 §4.1.4 |
| Serializzazione compatta JWS del token di servizio | RFC 7515 §3.1 |
| Soglia minima di 16 byte per il secret HS256; nessuna password memorizzabile da un umano come chiave MAC | RFC 8725 §3.5 (minaccia: §2.2) |
| Contratto a tempo costante del lookup del digest nel repository | OWASP ASVS 5.0 §11.2.4 |
| Digest di archiviazione delle API key SHA-256 | FIPS 180-4 (dichiarato nel codice) |
Le citazioni RFC 8725 e OWASP ASVS 5.0 sono verificate tramite RAG; gli identificatori di riferimento completi sono registrati nel frontmatter di questa pagina. I riferimenti FIPS 180-4, FIPS 198-1 e BSI TR-02102-1 sono dichiarati nel codice nel sorgente del prodotto (hash('sha256', …) e la soglia minima documentata della chiave del minter); non sono stati recuperati dal corpus RAG per questa pagina. Il requisito di tempo costante di ASVS §11.2.4 vincola l’implementazione del repository fornita dall’operatore, non la classe autenticatore stessa.
Note di sviluppo
Sezione intitolata “Note di sviluppo”- Fornire implementazioni durevoli di
ApiKeyRepositoryInterfaceeStripeAdapterInterface; il pacchetto include i contratti e un client provider PSR-18, non la persistenza. - Le dipendenze sono solo astrazioni PSR: logger PSR-3, client HTTP PSR-18, factory di request e stream PSR-17. Non è richiesto alcun SDK del provider.
- Eseguire la sincronizzazione del metering come job pianificato. Persistere in modo durevole i watermark restituiti dopo ogni ciclo.
- Esporre la percentuale di avviso della quota ai client, per esempio come header di avviso, e deduplicare gli alert di quota per periodo nella callback.
- Fornire il secret del token-minter dalla configurazione come valore casuale ad alta entropia; sono consigliati 32 o più byte casuali. Non derivarlo mai da una password.
- I prefissi delle chiavi rendono l’ambiente visibile senza un lookup; le chiavi sandbox e production non collidono mai perché il prefisso partecipa al digest archiviato.
- I dettagli dei meccanismi interni restano nella documentazione interna del repository sorgente e sono fuori ambito per questo manuale.
Confine di pubblicazione
Sezione intitolata “Confine di pubblicazione”Questa pagina documenta solo il comportamento osservabile dall’esterno e la superficie API pubblica supportata. I percorsi di namespace interni, le classi helper, le tabelle dei meccanismi, i nomi di file di runbook e i prefissi dei ticket sono fuori ambito.