Enterprise editie
SaaS — Diepe referentie
In één oogopslag
Sectie met titel “In één oogopslag”De Enterprise SaaS-module levert de multi-tenant bouwstenen voor een op NextPDF gebaseerde service.
TenantContextis een onveranderlijk identity-value-object dat uitsluitend uit geauthenticeerde context wordt geresolveerd.ApiKeyGeneratorenApiKeyAuthenticatorgeven API-keys met prefix, checksum en hash-opslag uit en valideren ze.QuotaCheckerbewaakt verzoeken tegen quota per tenant: waarschuwen bij 80%, afwijzen bij 100%, fail-closed weigeren wanneer het verbruik onbekend is.SidecarJwtMintergenereert kortlevende HS256-servicetokens voor aanroepen tussen componenten.UsageMeterenStripeMeteringSyncerhalen verbruiksgebeurtenissen op en synchroniseren ze met deterministische idempotentie naar de billing provider.
Beschikbaarheid en licenties
Sectie met titel “Beschikbaarheid en licenties”Deze mogelijkheid wordt geleverd in NextPDF Enterprise (nextpdf/enterprise) en wordt geactiveerd met een licentie-envelop op Enterprise-niveau. Een implementatie zonder dat recht laadt de klassen van de mogelijkheid niet. Vergelijk edities en vraag een licentie aan.
Het SaaS-oppervlak is een basismogelijkheid van Enterprise; er bestaat geen aparte vlag per functie. NextPDF Core (Apache-2.0) en NextPDF Pro hebben geen tenancy-, API-key- of quotamodel; deze mogelijkheid heeft geen equivalent op een lager niveau.
composer require nextpdf/enterprise:^3Publiek API-oppervlak
Sectie met titel “Publiek API-oppervlak”Alle symbolen bevinden zich onder NextPDF\Enterprise\SaaS.
| Symbool | Parameters | Standaardgedrag | Retourneert | Werpt of faalt met | Opmerkingen |
|---|---|---|---|---|---|
TenantContext | string $tenantId, string $source, array $scopes = ['read'] | Onveranderlijk identity-value-object | value-object | Niets | Bronnen: jwt, mtls, api_key; hasScope() / hasAnyScope() testen scopes |
TenantContext::singleTenant() | geen | Vaste default-tenant met read, write, admin | TenantContext | Niets | Single-tenant-implementaties |
ApiKeyAuthenticator::authenticate() | string $rawKey | Validatie in zes stappen, daarna contextresolutie | TenantContext | ApiKeyAuthenticationException (HTTP 401) | Context-source is api_key; scopes gekopieerd uit het key-record |
ApiKeyAuthenticator::requireScope() | TenantContext $context, ApiKeyScope $requiredScope | Expliciete scope-assertie | void | ApiKeyAuthenticationException::insufficientScope() (HTTP 403) | Scope-handhaving is een aparte, expliciete stap |
ApiKeyGenerator::generateLive() / ::generateTest() | geen | Nieuwe key: prefix, base62-body van 32 tekens (192-bit entropie), checksum van 4 tekens | array{key, hash, prefix} | Niets | Prefixen npf_live_ / npf_test_; hash is de opslagdigest |
ApiKeyGenerator::validateChecksum() | string $key | Vormcontrole op prefix, lengte en CRC32-checksum | bool | Niets | Typefoutbescherming vóór elke datastore-lookup; geen beveiligingscontrole |
ApiKeyGenerator::hashKey() (static) | string $key | SHA-256-hexdigest van de ruwe key | string | Niets | De enige opgeslagen representatie van een key |
ApiKeyGenerator::isLiveKey() / ::isTestKey() | string $key | Prefixinspectie | bool | Niets | Omgeving zichtbaar zonder lookup |
ApiKey | id, tenant, key-hash, display-prefix, scope-mask, aangemaakt-/verval-/intrekmomenten | Opgeslagen key-record; plaintext wordt nooit bewaard | value-object | Niets | isActive(), isRevoked(), isExpired(), scopeNames() |
ApiKeyScope | backed enum: Read = 1, Write = 2, Admin = 4 | Bitmask-scopemodel | enum | Niets | maskFromNames(), fromName(), fullAccess(); onbekende namen worden door de mask-builder genegeerd |
ApiKeyRepositoryInterface | — | Opslagcontract; alleen hash-persistentie | — | Implementatie-afhankelijk | findByHash(), findActiveByTenant(), store(), revoke() |
SidecarJwtMinter::__construct() | string $secret, issuer, audience, int $ttlSeconds = 300 | Weigert een signing-secret onder 16 bytes bij constructie | instance | InvalidArgumentException | Minimale key-sterkte van 128-bit; 32 of meer willekeurige bytes aanbevolen |
SidecarJwtMinter::mint() | TenantContext $tenant | HS256-JWT met iss, aud, sub, scope, tenant_id, iat, exp, jti | string | JsonException bij fout in claim-encoding | Standaard levensduur van vijf minuten; jti is 16 willekeurige bytes, hex-gecodeerd |
QuotaChecker::check() | TenantContext $tenant, TenantQuota $quota | Leest het huidige verbruik; waarschuwt bij 80%; wijst af bij 100%; weigert wanneer het verbruik onbekend is | array{allowed: bool, warning_percentage: float|null} | QuotaExceededException, QuotaUnavailableException | Alert-callback aangeroepen bij beide drempels |
TenantQuota | float $maxCuPerPeriod, collections, storage bytes, concurrent jobs | Limieten per periode; softdrempelconstante van 80% | value-object | Niets | fromConfig()-standaarden: 10,000 CU, 100 collections, 10 GB, 10 jobs |
QuotaExceededException::toErrorEnvelope() | geen | SPEC-QUOTA-001-foutenvelop | array | — | HTTP 402, niet herhaalbaar; draagt huidige waarde, limiet en resetmoment |
QuotaUnavailableException::toErrorEnvelope() | geen | SPEC-QUOTA-503-foutenvelop | array | — | HTTP 503, herhaalbaar; reden usage_undeterminable |
UsageMeter::pullUsage() | array<string, int> $watermarks | Pollt elke geconfigureerde usage-source-host vanaf zijn cursor | array{events, instance_id} | UsageMeterException wanneer elke host onbereikbaar is | Gedeeltelijke uitval wordt getolereerd; onbereikbare hosts worden gelogd en overgeslagen |
UsageMeter::getCurrentUsage() | string $tenantId | Compute-unit-verbruik van de huidige periode | float | UsageMeterException wanneer het verbruik onbepaalbaar is | Een parseerbare nul is gezaghebbend; onbekend verbruik werpt een exception |
StripeMeteringSyncer::sync() | array<string, int> $watermarks | Eén cyclus van pull, transform en send | array{watermarks, sent, failed} | Niets; send-fouten worden naar de DLQ-callback gerouteerd | Een pull-fout retourneert een no-op-cyclus die de cursor behoudt |
StripeAdapter::sendMeterEvent() | MeterEvent $event | POST naar de provider met een idempotency-header | void | StripeSyncException | HTTP 429 en 5xx herhaalbaar; overige 4xx niet herhaalbaar |
StripeAdapter::sendBatch() | list<MeterEvent> $events | Verstuurt elke event; verzamelt fouten | list<StripeSyncException> | Niets | Een lege lijst betekent dat elke event is geslaagd |
MeterEvent | meter-naam, tenant, waarde, idempotency-key, timestamp | Onveranderlijk meter-event-value-object | value-object | Niets | toStripePayload() serialiseert de provider-payload |
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 {}}Gedragscontract
Sectie met titel “Gedragscontract”- Tenantidentiteit. Een tenantcontext is onveranderlijk: tenant-identificator, resolutiebron, scopes. Identiteit wordt uitsluitend uit geauthenticeerde context geresolveerd (
jwt,mtls,api_key) — nooit uit een door de client aangeleverde header of queryparameter. Een single-tenant-implementatie gebruikt de vastedefault-context met volledige scopes. - Authenticatievolgorde. API-key-authenticatie verloopt in een vaste volgorde: checksum, SHA-256-hash, repository-lookup, intrekkingscontrole, vervalcontrole, contextresolutie. Onbekende, ingetrokken en verlopen keys zijn drie afzonderlijke uitkomsten, alle HTTP 401; onvoldoende scope is HTTP 403.
- Key-geheimhouding. De ruwe key wordt nooit opgeslagen of gelogd; alleen de SHA-256-digest ervan wordt bewaard en opgezocht. De authenticator voert zelf geen byte-gewijze secret-vergelijking uit; een timing-safe digest-lookup is het contract van de repository-implementatie.
- Quotadrempels. Bij de softlimiet van 80% gaat het verzoek door, wordt het waarschuwingspercentage geretourneerd en vuurt de alert-callback. Bij de hardlimiet van 100% wordt het verzoek afgewezen met
SPEC-QUOTA-001(HTTP 402), die het resetmoment draagt — de eerste dag van de volgende maand, middernacht UTC. - Quota fail-closed. Onbepaalbaar verbruik wijst het verzoek af met
SPEC-QUOTA-503(HTTP 503, herhaalbaar). Onbekend verbruik wordt nooit als nul behandeld. Een echt, parseerbaar nulverbruik is gezaghebbend en wordt toegelaten. - Alert-deduplicatie. De checker dedupliceert alerts niet; deduplicatie per periode is de verantwoordelijkheid van de callback.
- Metering-sync. De cyclus is gepland, nooit op het verzoekpad. Hij hervat vanaf watermarks per bron en schuift elke cursor door naar de hoogste succesvol verstuurde gebeurtenisidentiteit. De idempotency-key is deterministisch — tenant, periode, gebeurtenisidentiteit — zodat een opnieuw verstuurde gebeurtenis samenvalt door de deduplicatie aan providerzijde.
- Pull-fout. Een mislukte pull retourneert een no-op-cyclus (
sent0,failed0) die de watermarks behoudt; de volgende cyclus probeert hetzelfde venster opnieuw in plaats van het over te slaan. - Servicetokens. Tokens zijn HS256 met een gedeeld secret en dragen
iss,aud,sub,scope,tenant_id,iat,expen een uniekjti. De standaard levensduur is vijf minuten. De constructie weigert een secret onder 16 bytes, fail-closed.
Randgevallen en foutmodi
Sectie met titel “Randgevallen en foutmodi”- Een misvormde key faalt de checksum en wordt afgewezen vóór elke datastore-toegang. Een goedgevormde maar onbekende key wordt afgewezen na lookup. Beide verschijnen als de ongeldige-key-uitkomst.
- Onbekende, ingetrokken en verlopen keys gebruiken afzonderlijke exception-factory’s; de
keyExpired-vlag is alleen waar bij de verlopen-uitkomst. Map ze naar afzonderlijke clientantwoorden. QuotaChecker::check()keert alleen terug bij toelating; de geretourneerdeallowedis altijdtrue. Afwijzing en onbeschikbaarheid zijn exceptionele uitkomsten.TenantQuota::usagePercentage()retourneert0.0bij een niet-positief quotum;fromConfig()vult standaarden in voor ontbrekende waarden en klemt integer-limieten tot minimaal 1.- Watermarks zijn per bron; een ontbrekende watermark start vanaf het begin van de stream van die bron (cursor
0). Een multi-source-implementatie houdt onafhankelijke watermarks bij. - De transform slaat niet-array-gebeurtenissen over, gebeurtenissen met een ontbrekende of lege operatie of tenant, een niet-positieve waarde, of een ongemapte operatie — zonder de cyclus te laten falen. Een gebeurtenis zonder bruikbare positieve integer-identiteit wordt geweigerd met een waarschuwing: een willekeurige fallback-key zou de deduplicatie aan providerzijde ondermijnen en de tenant dubbel kunnen factureren.
- Tien opeenvolgende send-fouten escaleren naar een kritieke logregel; de teller reset bij elke geslaagde send. Elke mislukte gebeurtenis bereikt nog steeds de dead-letter-callback.
- Een misvormde JSON-body van een usage-source-host levert een lege gebeurtenislijst op, geen cyclusfout.
pullUsage()werpt alleen wanneer elke geconfigureerde host onbereikbaar is.
FIPS-modusgedrag
Sectie met titel “FIPS-modusgedrag”- Digest- en MAC-primitieven zijn SHA-256 en HMAC-SHA256 via de crypto-provider van de host-PHP. Een FIPS-beperkte build gaat fail-closed bij een niet-goedgekeurd algoritme in plaats van te degraderen; de SaaS-laag voegt geen eigen cryptografisch beleid toe.
- Key-bodies en token-identificatoren komen uit de CSPRNG (
random_int(),random_bytes()). - De CRC32-checksum is geen cryptografische controle en wordt niet beïnvloed door de FIPS-modus.
Conformiteit
Sectie met titel “Conformiteit”De onderstaande uitspraken beschrijven de mogelijkheid ten opzichte van de geciteerde clausules. Het zijn geen certificeringsclaims; NextPDF houdt geen certificering voor deze module.
| Gedrag | Referentie |
|---|---|
Not-after-semantiek van service-token exp | RFC 7519 §4.1.4 |
| JWS compact serialization van service-token | RFC 7515 §3.1 |
| Minimale HS256-secret van 16 bytes; geen door mensen te onthouden wachtwoorden als MAC-keys | RFC 8725 §3.5 (threat: §2.2) |
| Constant-time-contract voor repository-digest-lookup | OWASP ASVS 5.0 §11.2.4 |
| Opslagdigest van API-key SHA-256 | FIPS 180-4 (code-declared) |
De citaten van RFC 8725 en OWASP ASVS 5.0 zijn RAG-verified; volledige reference-identifiers zijn vastgelegd in de frontmatter van deze pagina. De referenties naar FIPS 180-4, FIPS 198-1 en BSI TR-02102-1 zijn code-declared in de productbron (hash('sha256', …) en de gedocumenteerde key-ondergrens van de minter); ze zijn voor deze pagina niet uit het RAG-corpus opgehaald. De constant-time-eis van ASVS §11.2.4 bindt de repository-implementatie die de operator levert, niet de authenticator-klasse zelf.
Ontwikkelingsnotities
Sectie met titel “Ontwikkelingsnotities”- Lever duurzame implementaties van
ApiKeyRepositoryInterfaceenStripeAdapterInterface; het pakket levert de contracten en een PSR-18-provider-client, geen persistentie. - Afhankelijkheden zijn uitsluitend PSR-abstracties: PSR-3-logger, PSR-18-HTTP-client, PSR-17-request- en -stream-factory’s. Er is geen provider-SDK vereist.
- Draai de metering-sync als een geplande job. Bewaar de geretourneerde watermarks duurzaam na elke cyclus.
- Toon het quota-waarschuwingspercentage aan clients, bijvoorbeeld als waarschuwingsheader, en dedupliceer quota-alerts per periode in de callback.
- Lever het secret van de token-minter vanuit configuratie als een willekeurige waarde met hoge entropie; 32 of meer willekeurige bytes wordt aanbevolen. Leid het nooit af uit een wachtwoord.
- Key-prefixen maken de omgeving zichtbaar zonder lookup; sandbox- en production-keys botsen nooit omdat de prefix deel uitmaakt van de opgeslagen digest.
- Interne mechanismedetails blijven in de interne documentatie van de bronrepository en vallen buiten de scope van deze handleiding.
Publicatiegrens
Sectie met titel “Publicatiegrens”Deze pagina documenteert alleen extern waarneembaar gedrag en het ondersteunde publieke API-oppervlak. Interne namespace-paden, helper-klassen, mechanismetabellen, runbook-bestandsnamen en ticketprefixen vallen buiten de scope.