Enterprise Edition
SaaS — Ausführliche Referenz
Auf einen Blick
Abschnitt betitelt „Auf einen Blick“Das Enterprise-SaaS-Modul liefert die mandantenfähigen Bausteine für einen NextPDF-basierten Dienst.
TenantContextist ein unveränderliches Identitäts-Wertobjekt, das ausschließlich aus authentifiziertem Kontext aufgelöst wird.ApiKeyGeneratorundApiKeyAuthenticatorstellen präfixierte, prüfsummenbehaftete, hash-gespeicherte API-Keys aus und validieren sie.QuotaCheckerreglementiert Anfragen anhand mandantenspezifischer Kontingente: Warnung bei 80 %, Ablehnung bei 100 %, Fail-closed-Verweigerung, wenn die Nutzung unbekannt ist.SidecarJwtMinterprägt kurzlebige HS256-Service-Tokens für Aufrufe zwischen Komponenten.UsageMeterundStripeMeteringSyncerziehen Nutzungsereignisse und synchronisieren sie mit deterministischer Idempotenz zum Abrechnungsanbieter.
Verfügbarkeit & Lizenzierung
Abschnitt betitelt „Verfügbarkeit & Lizenzierung“Diese Funktion wird in NextPDF Enterprise (nextpdf/enterprise) ausgeliefert und aktiviert sich mit einer Lizenz-Envelope der Enterprise-Stufe. Ein Deployment ohne diese Berechtigung lädt die Klassen der Funktion nicht. Editionen vergleichen und eine Lizenz erwerben.
Die SaaS-Oberfläche ist eine Enterprise-Basisfunktion; ein separates Feature-Flag existiert nicht. NextPDF Core (Apache-2.0) und NextPDF Pro haben kein Mandanten-, API-Key- oder Kontingentmodell; für diese Funktion gibt es kein Äquivalent in einer niedrigeren Stufe.
composer require nextpdf/enterprise:^3Öffentliche API-Oberfläche
Abschnitt betitelt „Öffentliche API-Oberfläche“Alle Symbole liegen unter NextPDF\Enterprise\SaaS.
| Symbol | Parameter | Standardverhalten | Rückgabe | Wirft oder scheitert mit | Hinweise |
|---|---|---|---|---|---|
TenantContext | string $tenantId, string $source, array $scopes = ['read'] | Unveränderliches Identitäts-Wertobjekt | Wertobjekt | Nichts | Quellen: jwt, mtls, api_key; hasScope() / hasAnyScope() prüfen Scopes |
TenantContext::singleTenant() | keine | Fester default-Mandant mit read, write, admin | TenantContext | Nichts | Einzelmandanten-Deployments |
ApiKeyAuthenticator::authenticate() | string $rawKey | Sechsstufige Validierung, dann Kontextauflösung | TenantContext | ApiKeyAuthenticationException (HTTP 401) | Kontext-source ist api_key; Scopes werden aus dem Key-Datensatz kopiert |
ApiKeyAuthenticator::requireScope() | TenantContext $context, ApiKeyScope $requiredScope | Explizite Scope-Zusicherung | void | ApiKeyAuthenticationException::insufficientScope() (HTTP 403) | Die Scope-Durchsetzung ist ein separater, expliziter Schritt |
ApiKeyGenerator::generateLive() / ::generateTest() | keine | Neuer Key: Präfix, 32-Zeichen-base62-Körper (192-Bit-Entropie), 4-Zeichen-Prüfsumme | array{key, hash, prefix} | Nichts | Präfixe npf_live_ / npf_test_; hash ist der Speicher-Digest |
ApiKeyGenerator::validateChecksum() | string $key | Prüfung von Präfix, Länge und CRC32-Prüfsummenform | bool | Nichts | Tippfehlerschutz vor jedem Datenspeicher-Lookup; keine Sicherheitskontrolle |
ApiKeyGenerator::hashKey() (static) | string $key | SHA-256-Hex-Digest des Roh-Keys | string | Nichts | Die einzige gespeicherte Repräsentation eines Keys |
ApiKeyGenerator::isLiveKey() / ::isTestKey() | string $key | Präfixprüfung | bool | Nichts | Umgebung ohne Lookup sichtbar |
ApiKey | ID, Mandant, Key-Hash, Anzeige-Präfix, Scope-Maske, Erstellungs-/Ablauf-/Widerrufszeitpunkte | Gespeicherter Key-Datensatz; Klartext wird nie persistiert | Wertobjekt | Nichts | isActive(), isRevoked(), isExpired(), scopeNames() |
ApiKeyScope | Backed-Enum: Read = 1, Write = 2, Admin = 4 | Bitmasken-Scope-Modell | Enum | Nichts | maskFromNames(), fromName(), fullAccess(); unbekannte Namen werden vom Maskenersteller ignoriert |
ApiKeyRepositoryInterface | — | Speichervertrag; ausschließlich Hash-Persistenz | — | Implementierungsabhängig | findByHash(), findActiveByTenant(), store(), revoke() |
SidecarJwtMinter::__construct() | string $secret, Issuer, Audience, int $ttlSeconds = 300 | Weist bei der Konstruktion ein Signatur-Secret unter 16 Byte zurück | Instanz | InvalidArgumentException | 128-Bit-Mindestschlüsselstärke; 32 oder mehr Zufallsbyte empfohlen |
SidecarJwtMinter::mint() | TenantContext $tenant | HS256-JWT mit iss, aud, sub, scope, tenant_id, iat, exp, jti | string | JsonException bei Fehler in der Claim-Kodierung | Fünf Minuten Standardlebensdauer; jti besteht aus 16 Zufallsbyte, hex-kodiert |
QuotaChecker::check() | TenantContext $tenant, TenantQuota $quota | Liest die aktuelle Nutzung; warnt bei 80 %; lehnt bei 100 % ab; verweigert, wenn die Nutzung unbekannt ist | array{allowed: bool, warning_percentage: float|null} | QuotaExceededException, QuotaUnavailableException | Alert-Callback wird an beiden Schwellen aufgerufen |
TenantQuota | float $maxCuPerPeriod, Collections, Speicher-Byte, gleichzeitige Jobs | Limits pro Periode; 80-%-Soft-Schwellenkonstante | Wertobjekt | Nichts | fromConfig()-Standardwerte: 10,000 CU, 100 Collections, 10 GB, 10 Jobs |
QuotaExceededException::toErrorEnvelope() | keine | SPEC-QUOTA-001-Fehler-Envelope | array | — | HTTP 402, nicht wiederholbar; enthält aktuellen Wert, Limit und Reset-Zeitpunkt |
QuotaUnavailableException::toErrorEnvelope() | keine | SPEC-QUOTA-503-Fehler-Envelope | array | — | HTTP 503, wiederholbar; Grund usage_undeterminable |
UsageMeter::pullUsage() | array<string, int> $watermarks | Fragt jeden konfigurierten Usage-Source-Host ab seinem Cursor ab | array{events, instance_id} | UsageMeterException, wenn jeder Host unerreichbar ist | Teilausfall toleriert; unerreichbare Hosts werden protokolliert und übersprungen |
UsageMeter::getCurrentUsage() | string $tenantId | Compute-Unit-Nutzung der aktuellen Periode | float | UsageMeterException, wenn die Nutzung unbestimmbar ist | Eine parsbare Null ist maßgeblich; unbekannte Nutzung wirft |
StripeMeteringSyncer::sync() | array<string, int> $watermarks | Ein Pull-, Transform-, Send-Zyklus | array{watermarks, sent, failed} | Nichts; Sendefehler werden an den DLQ-Callback geleitet | Ein Pull-Fehler liefert einen No-op-Zyklus, der den Cursor bewahrt |
StripeAdapter::sendMeterEvent() | MeterEvent $event | POST an den Provider mit einem Idempotenz-Header | void | StripeSyncException | HTTP 429 und 5xx wiederholbar; andere 4xx nicht wiederholbar |
StripeAdapter::sendBatch() | list<MeterEvent> $events | Sendet jedes Event; sammelt Fehler | list<StripeSyncException> | Nichts | Eine leere Liste bedeutet, dass jedes Event erfolgreich war |
MeterEvent | Meter-Name, Mandant, Wert, Idempotenzschlüssel, Zeitstempel | Unveränderliches Meter-Event-Wertobjekt | Wertobjekt | Nichts | toStripePayload() serialisiert die 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 {}}Verhaltensvertrag
Abschnitt betitelt „Verhaltensvertrag“- Mandantenidentität. Ein Mandantenkontext ist unveränderlich: Mandantenkennung, Auflösungsquelle, Scopes. Die Identität wird ausschließlich aus authentifiziertem Kontext aufgelöst (
jwt,mtls,api_key) — niemals aus einem vom Client gelieferten Header oder Query-Parameter. Ein Einzelmandanten-Deployment verwendet den festendefault-Kontext mit vollen Scopes. - Authentifizierungsreihenfolge. Die API-Key-Authentifizierung läuft in fester Reihenfolge ab: Prüfsumme, SHA-256-Hash, Repository-Lookup, Widerrufsprüfung, Ablaufprüfung, Kontextauflösung. Unbekannte, widerrufene und abgelaufene Keys sind drei verschiedene Ergebnisse, alle HTTP 401; unzureichender Scope ist HTTP 403.
- Key-Geheimhaltung. Der Roh-Key wird nie gespeichert oder protokolliert; nur sein SHA-256-Digest wird persistiert und nachgeschlagen. Der Authenticator führt selbst keinen byteweisen Geheimnisvergleich durch; der konstantzeitige Digest-Lookup ist Vertragsbestandteil der Repository-Implementierung.
- Kontingentschwellen. Beim 80-%-Soft-Limit läuft die Anfrage weiter, der Warnprozentsatz wird zurückgegeben und der Alert-Callback wird ausgelöst. Beim 100-%-Hard-Limit wird die Anfrage mit
SPEC-QUOTA-001(HTTP 402) abgelehnt und trägt den Reset-Zeitpunkt — den ersten Tag des Folgemonats, Mitternacht UTC. - Kontingent Fail-closed. Unbestimmbare Nutzung verweigert die Anfrage mit
SPEC-QUOTA-503(HTTP 503, wiederholbar). Unbekannte Nutzung wird nie als Null behandelt. Eine echte, parsbare Nullnutzung ist maßgeblich und lässt zu. - Alert-Deduplizierung. Der Checker dedupliziert keine Alerts; die Deduplizierung pro Periode liegt in der Verantwortung des Callbacks.
- Metering-Synchronisation. Der Zyklus ist geplant, niemals auf dem Anfragepfad. Er setzt bei quellenspezifischen Watermarks wieder ein und rückt jeden Cursor auf die höchste erfolgreich gesendete Ereignisidentität vor. Der Idempotenzschlüssel ist deterministisch — Mandant, Periode, Ereignisidentität —, sodass ein erneut gesendetes Event bei der Deduplizierung des Providers kollabiert.
- Pull-Fehler. Ein fehlgeschlagener Pull liefert einen No-op-Zyklus (
sent0,failed0), der die Watermarks bewahrt; der nächste Zyklus wiederholt dasselbe Fenster, anstatt es zu überspringen. - Service-Tokens. Tokens sind HS256 mit einem gemeinsamen Secret und tragen
iss,aud,sub,scope,tenant_id,iat,expsowie ein eindeutigesjti. Die Standardlebensdauer beträgt fünf Minuten. Die Konstruktion weist ein Secret unter 16 Byte zurück, fail-closed.
Grenzfälle & Fehlermodi
Abschnitt betitelt „Grenzfälle & Fehlermodi“- Ein fehlerhaft geformter Key scheitert an der Prüfsumme und wird vor jedem Datenspeicherzugriff abgelehnt. Ein wohlgeformter, aber unbekannter Key wird nach dem Lookup abgelehnt. Beide erscheinen als Ungültiger-Key-Ergebnis.
- Unbekannte, widerrufene und abgelaufene Keys verwenden verschiedene Exception-Factories; das Flag
keyExpiredist nur beim Ablauf-Ergebnis wahr. Bilden Sie sie auf verschiedene Client-Antworten ab. QuotaChecker::check()kehrt nur bei Zulassung zurück; das zurückgegebeneallowedist immertrue. Ablehnung und Nichtverfügbarkeit sind Ausnahmeergebnisse.TenantQuota::usagePercentage()liefert0.0für ein nicht-positives Kontingent;fromConfig()ersetzt fehlende Werte durch Standardwerte und begrenzt Ganzzahllimits auf mindestens 1.- Watermarks sind quellenspezifisch; ein fehlendes Watermark beginnt am Anfang des Streams dieser Quelle (Cursor
0). Ein Multi-Source-Deployment führt unabhängige Watermarks. - Die Transformation überspringt Nicht-Array-Events, Events mit fehlender oder leerer Operation oder fehlendem Mandanten, mit nicht-positivem Wert oder einer nicht zugeordneten Operation — ohne den Zyklus scheitern zu lassen. Ein Event ohne verwendbare positive Ganzzahlidentität wird mit einer Warnung abgelehnt: Ein zufälliger Fallback-Schlüssel würde die providerseitige Deduplizierung aushebeln und könnte den Mandanten doppelt berechnen.
- Zehn aufeinanderfolgende Sendefehler eskalieren zu einem kritischen Log-Eintrag; der Zähler wird bei jedem erfolgreichen Senden zurückgesetzt. Jedes fehlgeschlagene Event erreicht dennoch den Dead-Letter-Callback.
- Ein fehlerhafter JSON-Body von einem Usage-Source-Host ergibt eine leere Ereignisliste, keinen Zyklusfehler.
pullUsage()wirft nur, wenn jeder konfigurierte Host unerreichbar ist.
Verhalten im FIPS-Modus
Abschnitt betitelt „Verhalten im FIPS-Modus“- Digest- und MAC-Primitive sind SHA-256 und HMAC-SHA256 über den PHP-Krypto-Provider des Hosts. Ein FIPS-beschränkter Build scheitert bei einem nicht zugelassenen Algorithmus fail-closed, statt herabzustufen; die SaaS-Schicht fügt keine eigene Kryptografie-Richtlinie hinzu.
- Key-Körper und Token-Bezeichner stammen aus dem CSPRNG (
random_int(),random_bytes()). - Die CRC32-Prüfsumme ist keine kryptografische Kontrolle und wird vom FIPS-Modus nicht beeinflusst.
Konformität
Abschnitt betitelt „Konformität“Die folgenden Aussagen beschreiben die Fähigkeit gegenüber den zitierten Klauseln. Sie sind keine Zertifizierungsansprüche; NextPDF besitzt keine Zertifizierung für dieses Modul.
| Verhalten | Referenz |
|---|---|
Service-Token-exp-Not-After-Semantik | RFC 7519 §4.1.4 |
| Service-Token-JWS-Compact-Serialisierung | RFC 7515 §3.1 |
| 16-Byte-Mindest-HS256-Secret; keine menschlich merkbaren Passwörter als MAC-Schlüssel | RFC 8725 §3.5 (Bedrohung: §2.2) |
| Konstantzeit-Vertrag für Repository-Digest-Lookup | OWASP ASVS 5.0 §11.2.4 |
| API-Key-Speicher-Digest SHA-256 | FIPS 180-4 (im Code deklariert) |
Die Zitate zu RFC 8725 und OWASP ASVS 5.0 sind RAG-verifiziert; die vollständigen Referenzkennungen sind im Frontmatter dieser Seite hinterlegt. Die Referenzen zu FIPS 180-4, FIPS 198-1 und BSI TR-02102-1 sind im Produktquellcode deklariert (hash('sha256', …) und die dokumentierte Schlüsseluntergrenze des Minters); sie wurden für diese Seite nicht aus dem RAG-Korpus abgerufen. Die Konstantzeit-Anforderung von ASVS §11.2.4 bindet die vom Betreiber bereitgestellte Repository-Implementierung, nicht die Authenticator-Klasse selbst.
Entwicklungshinweise
Abschnitt betitelt „Entwicklungshinweise“- Stellen Sie dauerhafte Implementierungen von
ApiKeyRepositoryInterfaceundStripeAdapterInterfacebereit; das Paket liefert die Verträge und einen PSR-18-Provider-Client, keine Persistenz. - Abhängigkeiten sind ausschließlich PSR-Abstraktionen: PSR-3-Logger, PSR-18-HTTP-Client, PSR-17-Request- und Stream-Factories. Kein Provider-SDK ist erforderlich.
- Führen Sie die Metering-Synchronisation als geplanten Job aus. Persistieren Sie die zurückgegebenen Watermarks nach jedem Zyklus dauerhaft.
- Geben Sie den Kontingent-Warnprozentsatz an Clients weiter, etwa als Warn-Header, und deduplizieren Sie Kontingent-Alerts pro Periode im Callback.
- Liefern Sie das Token-Minter-Secret aus der Konfiguration als hochentropischen Zufallswert; 32 oder mehr Zufallsbyte werden empfohlen. Leiten Sie es niemals aus einem Passwort ab.
- Key-Präfixe machen die Umgebung ohne Lookup sichtbar; Sandbox- und Produktions-Keys kollidieren nie, weil das Präfix in den gespeicherten Digest eingeht.
- Interne Mechanismusdetails verbleiben in der internen Dokumentation des Quell-Repositorys und liegen außerhalb des Geltungsbereichs dieses Handbuchs.
Veröffentlichungsgrenze
Abschnitt betitelt „Veröffentlichungsgrenze“Diese Seite dokumentiert ausschließlich extern beobachtbares Verhalten und die unterstützte öffentliche API-Oberfläche. Interne Namespace-Pfade, Hilfsklassen, Mechanismustabellen, Runbook-Dateinamen und Ticket-Präfixe liegen außerhalb des Geltungsbereichs.