Enterprise édition
SaaS — Référence détaillée
En un coup d’œil
Section intitulée « En un coup d’œil »Le module Enterprise SaaS fournit les briques multi-locataires d’un service basé sur NextPDF.
TenantContextest un objet-valeur d’identité immuable, résolu uniquement à partir du contexte authentifié.ApiKeyGeneratoretApiKeyAuthenticatorémettent et valident des clés API préfixées, dotées d’une somme de contrôle et stockées sous forme de hachage.QuotaCheckercontrôle les requêtes au regard des quotas par locataire : avertissement à 80 %, rejet à 100 %, refus fail-closed quand l’usage est inconnu.SidecarJwtMinterémet des jetons de service HS256 à courte durée de vie pour les appels inter-composants.UsageMeteretStripeMeteringSyncerextraient les événements d’usage et les synchronisent vers le fournisseur de facturation avec une idempotence déterministe.
Disponibilité et licence
Section intitulée « Disponibilité et licence »Cette fonctionnalité est fournie dans NextPDF Enterprise (nextpdf/enterprise) et s’active avec une enveloppe de licence de niveau Enterprise. Un déploiement dépourvu de ce droit ne charge pas les classes de la fonctionnalité. Compare les éditions et obtiens une licence.
La surface SaaS est une fonctionnalité Enterprise de base ; aucun indicateur par fonctionnalité distinct n’existe. NextPDF Core (Apache-2.0) et NextPDF Pro n’ont aucun modèle de location, de clé API ou de quota ; cette fonctionnalité n’a pas d’équivalent de niveau inférieur.
composer require nextpdf/enterprise:^3Surface d’API publique
Section intitulée « Surface d’API publique »Tous les symboles résident sous NextPDF\Enterprise\SaaS.
| Symbole | Paramètres | Comportement par défaut | Renvoie | Lève ou échoue avec | Notes |
|---|---|---|---|---|---|
TenantContext | string $tenantId, string $source, array $scopes = ['read'] | Objet-valeur d’identité immuable | objet-valeur | Rien | Sources : jwt, mtls, api_key ; hasScope() / hasAnyScope() testent les portées |
TenantContext::singleTenant() | aucun | Locataire default fixe avec read, write, admin | TenantContext | Rien | Déploiements mono-locataire |
ApiKeyAuthenticator::authenticate() | string $rawKey | Validation en six étapes, puis résolution du contexte | TenantContext | ApiKeyAuthenticationException (HTTP 401) | Le source du contexte est api_key ; les portées sont copiées depuis l’enregistrement de la clé |
ApiKeyAuthenticator::requireScope() | TenantContext $context, ApiKeyScope $requiredScope | Assertion de portée explicite | void | ApiKeyAuthenticationException::insufficientScope() (HTTP 403) | L’application des portées est une étape distincte et explicite |
ApiKeyGenerator::generateLive() / ::generateTest() | aucun | Nouvelle clé : préfixe, corps base62 de 32 caractères (entropie de 192 bits), somme de contrôle de 4 caractères | array{key, hash, prefix} | Rien | Préfixes npf_live_ / npf_test_ ; hash est le condensé de stockage |
ApiKeyGenerator::validateChecksum() | string $key | Vérification de forme du préfixe, de la longueur et de la somme de contrôle CRC32 | bool | Rien | Garde-fou anti-faute de frappe avant toute consultation du datastore ; ce n’est pas un contrôle de sécurité |
ApiKeyGenerator::hashKey() (static) | string $key | Condensé hexadécimal SHA-256 de la clé brute | string | Rien | La seule représentation stockée d’une clé |
ApiKeyGenerator::isLiveKey() / ::isTestKey() | string $key | Inspection du préfixe | bool | Rien | Environnement visible sans consultation |
ApiKey | id, locataire, hachage de clé, préfixe d’affichage, masque de portée, instants de création/expiration/révocation | Enregistrement de clé stocké ; le texte en clair n’est jamais persisté | objet-valeur | Rien | isActive(), isRevoked(), isExpired(), scopeNames() |
ApiKeyScope | énumération typée : Read = 1, Write = 2, Admin = 4 | Modèle de portée par masque de bits | énumération | Rien | maskFromNames(), fromName(), fullAccess() ; les noms inconnus sont ignorés par le constructeur de masque |
ApiKeyRepositoryInterface | — | Contrat de stockage ; persistance par hachage uniquement | — | Défini par l’implémentation | findByHash(), findActiveByTenant(), store(), revoke() |
SidecarJwtMinter::__construct() | string $secret, émetteur, audience, int $ttlSeconds = 300 | Rejette à la construction un secret de signature inférieur à 16 octets | instance | InvalidArgumentException | Plancher de robustesse de clé de 128 bits ; 32 octets aléatoires ou plus recommandés |
SidecarJwtMinter::mint() | TenantContext $tenant | JWT HS256 avec iss, aud, sub, scope, tenant_id, iat, exp, jti | string | JsonException en cas d’échec d’encodage des revendications | Durée de vie par défaut de cinq minutes ; jti correspond à 16 octets aléatoires, encodés en hexadécimal |
QuotaChecker::check() | TenantContext $tenant, TenantQuota $quota | Lit l’usage courant ; avertit à 80 % ; rejette à 100 % ; refuse quand l’usage est inconnu | array{allowed: bool, warning_percentage: float|null} | QuotaExceededException, QuotaUnavailableException | Rappel d’alerte invoqué aux deux seuils |
TenantQuota | float $maxCuPerPeriod, collections, octets de stockage, tâches concurrentes | Limites par période ; constante de seuil souple de 80 % | objet-valeur | Rien | Valeurs par défaut de fromConfig() : 10,000 CU, 100 collections, 10 Go, 10 tâches |
QuotaExceededException::toErrorEnvelope() | aucun | Enveloppe d’erreur SPEC-QUOTA-001 | array | — | HTTP 402, non rejouable ; porte l’usage courant, la limite et l’instant de réinitialisation |
QuotaUnavailableException::toErrorEnvelope() | aucun | Enveloppe d’erreur SPEC-QUOTA-503 | array | — | HTTP 503, rejouable ; motif usage_undeterminable |
UsageMeter::pullUsage() | array<string, int> $watermarks | Interroge chaque hôte source d’usage configuré depuis son curseur | array{events, instance_id} | UsageMeterException quand tous les hôtes sont injoignables | Panne partielle tolérée ; les hôtes injoignables sont journalisés et ignorés |
UsageMeter::getCurrentUsage() | string $tenantId | Usage en unités de calcul de la période courante | float | UsageMeterException quand l’usage est indéterminable | Un zéro analysable fait foi ; un usage inconnu lève une exception |
StripeMeteringSyncer::sync() | array<string, int> $watermarks | Un cycle extraction, transformation, envoi | array{watermarks, sent, failed} | Rien ; les échecs d’envoi sont routés vers le rappel DLQ | Un échec d’extraction renvoie un cycle sans effet préservant le curseur |
StripeAdapter::sendMeterEvent() | MeterEvent $event | POST vers le fournisseur avec un en-tête d’idempotence | void | StripeSyncException | HTTP 429 et 5xx rejouables ; les autres 4xx non rejouables |
StripeAdapter::sendBatch() | list<MeterEvent> $events | Envoie chaque événement ; collecte les échecs | list<StripeSyncException> | Rien | Une liste vide signifie que tous les événements ont réussi |
MeterEvent | nom de compteur, locataire, valeur, clé d’idempotence, horodatage | Objet-valeur d’événement de compteur immuable | objet-valeur | Rien | toStripePayload() sérialise la charge utile du fournisseur |
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 {}}Contrat de comportement
Section intitulée « Contrat de comportement »- Identité du locataire. Un contexte de locataire est immuable : identifiant de locataire, source de résolution, portées. L’identité est résolue uniquement à partir du contexte authentifié (
jwt,mtls,api_key) — jamais depuis un en-tête ou un paramètre de requête fourni par le client. Un déploiement mono-locataire utilise le contextedefaultfixe avec toutes les portées. - Ordre d’authentification. L’authentification par clé API se déroule dans un ordre fixe : somme de contrôle, hachage SHA-256, consultation du dépôt, vérification de révocation, vérification d’expiration, résolution du contexte. Les clés inconnues, révoquées et expirées sont trois résultats distincts, tous HTTP 401 ; une portée insuffisante est HTTP 403.
- Secret des clés. La clé brute n’est jamais stockée ni journalisée ; seul son condensé SHA-256 est persisté et consulté. L’authentificateur n’effectue lui-même aucune comparaison de secret octet par octet ; la consultation de condensé à temps constant relève du contrat de l’implémentation du dépôt.
- Seuils de quota. Au seuil souple de 80 %, la requête se poursuit, le pourcentage d’avertissement est renvoyé et le rappel d’alerte se déclenche. Au seuil dur de 100 %, la requête est rejetée avec
SPEC-QUOTA-001(HTTP 402) portant l’instant de réinitialisation — le premier jour du mois suivant, minuit UTC. - Quota fail-closed. Un usage indéterminable refuse la requête avec
SPEC-QUOTA-503(HTTP 503, rejouable). Un usage inconnu n’est jamais traité comme zéro. Un usage réellement nul et analysable fait foi et admet la requête. - Déduplication des alertes. Le vérificateur ne déduplique pas les alertes ; la déduplication par période relève de la responsabilité du rappel.
- Synchronisation du métrage. Le cycle est planifié, jamais sur le chemin de la requête. Il reprend à partir des filigranes par source et fait avancer chaque curseur jusqu’à l’identité du dernier événement envoyé avec succès. La clé d’idempotence est déterministe — locataire, période, identité de l’événement — de sorte qu’un événement renvoyé est fusionné par la déduplication côté fournisseur.
- Échec d’extraction. Une extraction échouée renvoie un cycle sans effet (
sentà 0,failedà 0) qui préserve les filigranes ; le cycle suivant réessaie la même fenêtre au lieu de la sauter. - Jetons de service. Les jetons sont en HS256 avec un secret partagé et portent
iss,aud,sub,scope,tenant_id,iat,expet unjtiunique. La durée de vie par défaut est de cinq minutes. La construction rejette un secret inférieur à 16 octets, fail-closed.
Cas limites et modes de défaillance
Section intitulée « Cas limites et modes de défaillance »- Une clé malformée échoue à la somme de contrôle et est rejetée avant tout accès au datastore. Une clé bien formée mais inconnue est rejetée après consultation. Les deux se manifestent comme le résultat clé-invalide.
- Les clés inconnues, révoquées et expirées utilisent des fabriques d’exceptions distinctes ; l’indicateur
keyExpiredn’est vrai que pour le résultat expiré. Fais-les correspondre à des réponses client distinctes. QuotaChecker::check()ne renvoie qu’en cas d’admission ; leallowedrenvoyé est toujourstrue. Le rejet et l’indisponibilité sont des résultats exceptionnels.TenantQuota::usagePercentage()renvoie0.0pour un quota non positif ;fromConfig()substitue des valeurs par défaut aux valeurs absentes et borne les limites entières à au moins 1.- Les filigranes sont par source ; un filigrane absent démarre au début du flux de cette source (curseur
0). Un déploiement multi-source maintient des filigranes indépendants. - La transformation ignore les événements non-tableau, les événements dont l’opération ou le locataire est absent ou vide, une valeur non positive, ou une opération non mappée — sans faire échouer le cycle. Un événement dépourvu d’identité entière positive exploitable est refusé avec un avertissement : une clé de repli aléatoire déjouerait la déduplication côté fournisseur et pourrait facturer deux fois le locataire.
- Dix échecs d’envoi consécutifs escaladent vers une entrée de journal critique ; le compteur se réinitialise à tout envoi réussi. Chaque événement échoué atteint néanmoins le rappel de file d’attente de lettres mortes.
- Un corps JSON malformé provenant d’un hôte source d’usage produit une liste d’événements vide, pas un échec de cycle.
pullUsage()ne lève une exception que lorsque tous les hôtes configurés sont injoignables.
Comportement en mode FIPS
Section intitulée « Comportement en mode FIPS »- Les primitives de condensé et de MAC sont SHA-256 et HMAC-SHA256 via le fournisseur cryptographique PHP de l’hôte. Une build contrainte par FIPS échoue en fermeture sur un algorithme non approuvé plutôt que de se rabattre ; la couche SaaS n’ajoute aucune politique cryptographique propre.
- Les corps de clés et les identifiants de jetons proviennent du CSPRNG (
random_int(),random_bytes()). - La somme de contrôle CRC32 n’est pas un contrôle cryptographique et n’est pas affectée par le mode FIPS.
Conformité
Section intitulée « Conformité »Les affirmations ci-dessous décrivent la fonctionnalité au regard des clauses citées. Ce ne sont pas des revendications de certification ; NextPDF ne détient aucune certification pour ce module.
| Comportement | Référence |
|---|---|
Sémantique not-after de exp du jeton de service | RFC 7519 §4.1.4 |
| Sérialisation compacte JWS du jeton de service | RFC 7515 §3.1 |
| Plancher de secret HS256 de 16 octets ; pas de mots de passe mémorisables comme clés MAC | RFC 8725 §3.5 (threat: §2.2) |
| Contrat de temps constant pour la consultation de condensé du dépôt | OWASP ASVS 5.0 §11.2.4 |
| Condensé de stockage de clé API SHA-256 | FIPS 180-4 (code-declared) |
Les citations RFC 8725 et OWASP ASVS 5.0 sont vérifiées par RAG ; les identifiants de référence complets sont consignés dans le frontmatter de cette page. Les références FIPS 180-4, FIPS 198-1 et BSI TR-02102-1 sont déclarées dans le code source du produit (hash('sha256', …) et le plancher de clé documenté de l’émetteur de jetons) ; elles n’ont pas été extraites du corpus RAG pour cette page. L’exigence de temps constant de l’ASVS §11.2.4 lie l’implémentation de dépôt fournie par l’opérateur, pas la classe d’authentificateur elle-même.
Notes de développement
Section intitulée « Notes de développement »- Fournis des implémentations durables de
ApiKeyRepositoryInterfaceetStripeAdapterInterface; le paquet livre les contrats et un client fournisseur PSR-18, pas la persistance. - Les dépendances sont uniquement des abstractions PSR : journaliseur PSR-3, client HTTP PSR-18, fabriques de requêtes et de flux PSR-17. Aucun SDK de fournisseur n’est requis.
- Exécute la synchronisation du métrage comme une tâche planifiée. Persiste durablement les filigranes renvoyés après chaque cycle.
- Expose le pourcentage d’avertissement de quota aux clients, par exemple sous forme d’en-tête d’avertissement, et déduplique les alertes de quota par période dans le rappel.
- Fournis le secret de l’émetteur de jetons depuis la configuration sous forme de valeur aléatoire à haute entropie ; 32 octets aléatoires ou plus sont recommandés. Ne le dérive jamais d’un mot de passe.
- Les préfixes de clé rendent l’environnement visible sans consultation ; les clés de bac à sable et de production n’entrent jamais en collision car le préfixe participe au condensé stocké.
- Le détail des mécanismes internes reste dans la documentation interne du dépôt source et sort du périmètre de ce manuel.
Périmètre de publication
Section intitulée « Périmètre de publication »Cette page ne documente que le comportement observable de l’extérieur et la surface d’API publique prise en charge. Les chemins de namespace internes, les classes utilitaires, les tables de mécanismes, les noms de fichiers de runbook et les préfixes de tickets sortent du périmètre.