Aller au contenu
getnextpdf.com

Enterprise édition

SaaS — Référence détaillée

Le module Enterprise SaaS fournit les briques multi-locataires d’un service basé sur NextPDF.

  • TenantContext est un objet-valeur d’identité immuable, résolu uniquement à partir du contexte authentifié.
  • ApiKeyGenerator et ApiKeyAuthenticator é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.
  • QuotaChecker contrô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.
  • UsageMeter et StripeMeteringSyncer extraient les événements d’usage et les synchronisent vers le fournisseur de facturation avec une idempotence déterministe.

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.

Fenêtre de terminal
composer require nextpdf/enterprise:^3

Tous les symboles résident sous NextPDF\Enterprise\SaaS.

SymboleParamètresComportement par défautRenvoieLève ou échoue avecNotes
TenantContextstring $tenantId, string $source, array $scopes = ['read']Objet-valeur d’identité immuableobjet-valeurRienSources : jwt, mtls, api_key ; hasScope() / hasAnyScope() testent les portées
TenantContext::singleTenant()aucunLocataire default fixe avec read, write, adminTenantContextRienDéploiements mono-locataire
ApiKeyAuthenticator::authenticate()string $rawKeyValidation en six étapes, puis résolution du contexteTenantContextApiKeyAuthenticationException (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 $requiredScopeAssertion de portée explicitevoidApiKeyAuthenticationException::insufficientScope() (HTTP 403)L’application des portées est une étape distincte et explicite
ApiKeyGenerator::generateLive() / ::generateTest()aucunNouvelle clé : préfixe, corps base62 de 32 caractères (entropie de 192 bits), somme de contrôle de 4 caractèresarray{key, hash, prefix}RienPréfixes npf_live_ / npf_test_ ; hash est le condensé de stockage
ApiKeyGenerator::validateChecksum()string $keyVérification de forme du préfixe, de la longueur et de la somme de contrôle CRC32boolRienGarde-fou anti-faute de frappe avant toute consultation du datastore ; ce n’est pas un contrôle de sécurité
ApiKeyGenerator::hashKey() (static)string $keyCondensé hexadécimal SHA-256 de la clé brutestringRienLa seule représentation stockée d’une clé
ApiKeyGenerator::isLiveKey() / ::isTestKey()string $keyInspection du préfixeboolRienEnvironnement visible sans consultation
ApiKeyid, locataire, hachage de clé, préfixe d’affichage, masque de portée, instants de création/expiration/révocationEnregistrement de clé stocké ; le texte en clair n’est jamais persistéobjet-valeurRienisActive(), isRevoked(), isExpired(), scopeNames()
ApiKeyScopeénumération typée : Read = 1, Write = 2, Admin = 4Modèle de portée par masque de bitsénumérationRienmaskFromNames(), fromName(), fullAccess() ; les noms inconnus sont ignorés par le constructeur de masque
ApiKeyRepositoryInterfaceContrat de stockage ; persistance par hachage uniquementDéfini par l’implémentationfindByHash(), findActiveByTenant(), store(), revoke()
SidecarJwtMinter::__construct()string $secret, émetteur, audience, int $ttlSeconds = 300Rejette à la construction un secret de signature inférieur à 16 octetsinstanceInvalidArgumentExceptionPlancher de robustesse de clé de 128 bits ; 32 octets aléatoires ou plus recommandés
SidecarJwtMinter::mint()TenantContext $tenantJWT HS256 avec iss, aud, sub, scope, tenant_id, iat, exp, jtistringJsonException en cas d’échec d’encodage des revendicationsDuré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 $quotaLit l’usage courant ; avertit à 80 % ; rejette à 100 % ; refuse quand l’usage est inconnuarray{allowed: bool, warning_percentage: float|null}QuotaExceededException, QuotaUnavailableExceptionRappel d’alerte invoqué aux deux seuils
TenantQuotafloat $maxCuPerPeriod, collections, octets de stockage, tâches concurrentesLimites par période ; constante de seuil souple de 80 %objet-valeurRienValeurs par défaut de fromConfig() : 10,000 CU, 100 collections, 10 Go, 10 tâches
QuotaExceededException::toErrorEnvelope()aucunEnveloppe d’erreur SPEC-QUOTA-001arrayHTTP 402, non rejouable ; porte l’usage courant, la limite et l’instant de réinitialisation
QuotaUnavailableException::toErrorEnvelope()aucunEnveloppe d’erreur SPEC-QUOTA-503arrayHTTP 503, rejouable ; motif usage_undeterminable
UsageMeter::pullUsage()array<string, int> $watermarksInterroge chaque hôte source d’usage configuré depuis son curseurarray{events, instance_id}UsageMeterException quand tous les hôtes sont injoignablesPanne partielle tolérée ; les hôtes injoignables sont journalisés et ignorés
UsageMeter::getCurrentUsage()string $tenantIdUsage en unités de calcul de la période courantefloatUsageMeterException quand l’usage est indéterminableUn zéro analysable fait foi ; un usage inconnu lève une exception
StripeMeteringSyncer::sync()array<string, int> $watermarksUn cycle extraction, transformation, envoiarray{watermarks, sent, failed}Rien ; les échecs d’envoi sont routés vers le rappel DLQUn échec d’extraction renvoie un cycle sans effet préservant le curseur
StripeAdapter::sendMeterEvent()MeterEvent $eventPOST vers le fournisseur avec un en-tête d’idempotencevoidStripeSyncExceptionHTTP 429 et 5xx rejouables ; les autres 4xx non rejouables
StripeAdapter::sendBatch()list<MeterEvent> $eventsEnvoie chaque événement ; collecte les échecslist<StripeSyncException>RienUne liste vide signifie que tous les événements ont réussi
MeterEventnom de compteur, locataire, valeur, clé d’idempotence, horodatageObjet-valeur d’événement de compteur immuableobjet-valeurRientoStripePayload() 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 {}
}
  • 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 contexte default fixe 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, exp et un jti unique. La durée de vie par défaut est de cinq minutes. La construction rejette un secret inférieur à 16 octets, fail-closed.
  • 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 keyExpired n’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 ; le allowed renvoyé est toujours true. Le rejet et l’indisponibilité sont des résultats exceptionnels.
  • TenantQuota::usagePercentage() renvoie 0.0 pour 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.
  • 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.

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.

ComportementRéférence
Sémantique not-after de exp du jeton de serviceRFC 7519 §4.1.4
Sérialisation compacte JWS du jeton de serviceRFC 7515 §3.1
Plancher de secret HS256 de 16 octets ; pas de mots de passe mémorisables comme clés MACRFC 8725 §3.5 (threat: §2.2)
Contrat de temps constant pour la consultation de condensé du dépôtOWASP ASVS 5.0 §11.2.4
Condensé de stockage de clé API SHA-256FIPS 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.

  • Fournis des implémentations durables de ApiKeyRepositoryInterface et StripeAdapterInterface ; 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.

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.