Salta ai contenuti
getnextpdf.com

Enterprise edizione

SaaS — Riferimento approfondito

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.
  • ApiKeyGenerator e ApiKeyAuthenticator emettono e validano API key con prefisso, checksum e archiviate come hash.
  • QuotaChecker filtra le richieste rispetto alle quote per-tenant: avviso all’80%, rifiuto al 100%, negazione fail-closed quando l’utilizzo è sconosciuto.
  • SidecarJwtMinter conia token di servizio HS256 a breve durata per le chiamate inter-componente.
  • UsageMeter e StripeMeteringSyncer estraggono gli eventi di utilizzo e li sincronizzano con il provider di fatturazione con idempotenza deterministica.

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.

Terminal window
composer require nextpdf/enterprise:^3

Tutti i simboli risiedono sotto NextPDF\Enterprise\SaaS.

SimboloParametriComportamento predefinitoRestituisceSolleva o fallisce conNote
TenantContextstring $tenantId, string $source, array $scopes = ['read']Value object di identità immutabilevalue objectNullaSorgenti: jwt, mtls, api_key; hasScope() / hasAnyScope() verificano gli scope
TenantContext::singleTenant()nessunoTenant default fisso con read, write, adminTenantContextNullaDistribuzioni single-tenant
ApiKeyAuthenticator::authenticate()string $rawKeyValidazione in sei passi, poi risoluzione del contestoTenantContextApiKeyAuthenticationException (HTTP 401)Il source del contesto è api_key; gli scope sono copiati dal record della chiave
ApiKeyAuthenticator::requireScope()TenantContext $context, ApiKeyScope $requiredScopeAsserzione esplicita dello scopevoidApiKeyAuthenticationException::insufficientScope() (HTTP 403)L’applicazione dello scope è un passo separato ed esplicito
ApiKeyGenerator::generateLive() / ::generateTest()nessunoNuova chiave: prefisso, corpo base62 di 32 caratteri (entropia a 192 bit), checksum di 4 caratteriarray{key, hash, prefix}NullaPrefissi npf_live_ / npf_test_; hash è il digest di archiviazione
ApiKeyGenerator::validateChecksum()string $keyControllo di forma su prefisso, lunghezza e checksum CRC32boolNullaProtezione dagli errori di battitura prima di qualsiasi lookup sul datastore; non è un controllo di sicurezza
ApiKeyGenerator::hashKey() (statico)string $keyDigest esadecimale SHA-256 della chiave in chiarostringNullaL’unica rappresentazione archiviata di una chiave
ApiKeyGenerator::isLiveKey() / ::isTestKey()string $keyIspezione del prefissoboolNullaAmbiente visibile senza un lookup
ApiKeyid, tenant, hash della chiave, prefisso di visualizzazione, maschera degli scope, istanti di creazione/scadenza/revocaRecord di chiave archiviato; il testo in chiaro non è mai persistitovalue objectNullaisActive(), isRevoked(), isExpired(), scopeNames()
ApiKeyScopebacked enum: Read = 1, Write = 2, Admin = 4Modello di scope a bitmaskenumNullamaskFromNames(), fromName(), fullAccess(); i nomi sconosciuti vengono ignorati dal costruttore della maschera
ApiKeyRepositoryInterfaceContratto di archiviazione; persistenza solo per hashDefinito dall’implementazionefindByHash(), findActiveByTenant(), store(), revoke()
SidecarJwtMinter::__construct()string $secret, issuer, audience, int $ttlSeconds = 300Rifiuta un secret di firma inferiore a 16 byte alla costruzioneistanzaInvalidArgumentExceptionSoglia minima di robustezza della chiave a 128 bit; consigliati 32 o più byte casuali
SidecarJwtMinter::mint()TenantContext $tenantJWT HS256 con iss, aud, sub, scope, tenant_id, iat, exp, jtistringJsonException in caso di fallimento della codifica dei claimDurata predefinita di cinque minuti; jti è composto da 16 byte casuali, codificati in esadecimale
QuotaChecker::check()TenantContext $tenant, TenantQuota $quotaLegge l’utilizzo corrente; avvisa all’80%; rifiuta al 100%; nega quando l’utilizzo è sconosciutoarray{allowed: bool, warning_percentage: float|null}QuotaExceededException, QuotaUnavailableExceptionLa callback di alert è invocata a entrambe le soglie
TenantQuotafloat $maxCuPerPeriod, collezioni, byte di storage, job concorrentiLimiti per-periodo; costante di soglia soft all’80%value objectNullaValori predefiniti di fromConfig(): 10,000 CU, 100 collezioni, 10 GB, 10 job
QuotaExceededException::toErrorEnvelope()nessunoEnvelope di errore SPEC-QUOTA-001arrayHTTP 402, non ritentabile; trasporta l’utilizzo corrente, il limite e l’istante di reset
QuotaUnavailableException::toErrorEnvelope()nessunoEnvelope di errore SPEC-QUOTA-503arrayHTTP 503, ritentabile; motivo usage_undeterminable
UsageMeter::pullUsage()array<string, int> $watermarksInterroga ogni host sorgente di utilizzo configurato a partire dal suo cursorearray{events, instance_id}UsageMeterException quando ogni host è irraggiungibileInterruzione parziale tollerata; gli host irraggiungibili vengono registrati e saltati
UsageMeter::getCurrentUsage()string $tenantIdUtilizzo di compute-unit del periodo correntefloatUsageMeterException quando l’utilizzo non è determinabileUno zero analizzabile è autorevole; un utilizzo sconosciuto solleva un’eccezione
StripeMeteringSyncer::sync()array<string, int> $watermarksUn ciclo di pull, trasformazione e invioarray{watermarks, sent, failed}Nulla; i fallimenti di invio vengono instradati alla callback DLQUn fallimento di pull restituisce un ciclo no-op che preserva il cursore
StripeAdapter::sendMeterEvent()MeterEvent $eventPOST al provider con un header di idempotenzavoidStripeSyncExceptionHTTP 429 e 5xx ritentabili; gli altri 4xx non ritentabili
StripeAdapter::sendBatch()list<MeterEvent> $eventsInvia ogni evento; raccoglie i fallimentilist<StripeSyncException>NullaUna lista vuota significa che ogni evento è andato a buon fine
MeterEventnome del meter, tenant, valore, chiave di idempotenza, timestampValue object di meter-event immutabilevalue objectNullatoStripePayload() 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 {}
}
  • 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 contesto default fisso 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 (sent 0, failed 0) 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, exp e un jti univoco. La durata predefinita è di cinque minuti. La costruzione rifiuta un secret inferiore a 16 byte, in modalità fail-closed.
  • 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 valore allowed restituito è sempre true. Il rifiuto e l’indisponibilità sono esiti eccezionali.
  • TenantQuota::usagePercentage() restituisce 0.0 per 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.
  • 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.

Le affermazioni che seguono descrivono la capacità rispetto alle clausole citate. Non sono dichiarazioni di certificazione; NextPDF non detiene alcuna certificazione per questo modulo.

ComportamentoRiferimento
Semantica not-after di exp del token di servizioRFC 7519 §4.1.4
Serializzazione compatta JWS del token di servizioRFC 7515 §3.1
Soglia minima di 16 byte per il secret HS256; nessuna password memorizzabile da un umano come chiave MACRFC 8725 §3.5 (minaccia: §2.2)
Contratto a tempo costante del lookup del digest nel repositoryOWASP ASVS 5.0 §11.2.4
Digest di archiviazione delle API key SHA-256FIPS 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.

  • Fornire implementazioni durevoli di ApiKeyRepositoryInterface e StripeAdapterInterface; 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.

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.