Skip to content
getnextpdf.com

Enterprise edition

Licensing — Deep Reference

This page is the deep reference for the NextPDF Enterprise licensing surface — the module that resolves every other Enterprise capability. It covers the entitlement state model (EntitlementEvaluator, EntitlementStatus, EntitlementResult), enforced capability gating (FeatureGate, CapabilityCode, CapabilityRegistry), the two-timer channel enforcement machine (ChannelEnforcementPolicy, OnlineCheckScheduler), and the signed online client (LicenseClient). Ordinary entitlement evaluation needs no network. The online client exists for explicit activate, heartbeat, deactivate, verify-online, and renewal-probe operations. Internal mechanism detail stays in the source repository’s internal documentation and is out of scope for this manual.

This capability ships in NextPDF Enterprise (nextpdf/enterprise) and activates with an Enterprise-tier license envelope. A deployment without that entitlement does not load the capability’s classes. Compare editions and get a license.

Licensing is a base Enterprise surface. It is always present once the Enterprise package is installed next to the Core package, because it is the surface that resolves everything else. NextPDF Pro has no separate entitlement surface; the state model, enforcement machine, and online client ship in nextpdf/enterprise only. NextPDF Core (Apache-2.0) has no entitlement model at all — a host that needs unlicensed Apache-2.0 behavior uses the Core engine directly, not the Enterprise pipeline.

All types live under NextPDF\Enterprise\Licensing. The enforcement machine sits in the Channel sub-namespace, the runtime composition in Runtime, and the signed-status types in Online.

SymbolParametersDefault behaviorReturnsThrows or fails withNotes
EntitlementEvaluator::evaluate()?LicenseKey $license, ?DateTimeImmutable $now = nullMaps a verified license (or null) to the full entitlement decisionEntitlementResultNoneA null license yields the fail-closed no-license result
EntitlementEvaluator::evaluateEnforced()?LicenseKey $license, EnforcementOutcome $outcomeConverts a pre-computed enforcement outcome into an entitlement resultEntitlementResultNoneThe effective edition comes from the outcome; every enforcement downgrade lands on Core
EntitlementEvaluator::resolveBrandingMode(), ::resolveBrandingStrategy()?LicenseKey $license, ?DateTimeImmutable $now = nullResolves the branding decision for generated outputBrandingMode / BrandingStrategyNoneSole branding authority; only the evaluation channel watermarks
EntitlementResultReadonly value objectCarries status, edition, channel, branding mode, runtime/update/support flags, and an optional warningNoneHelpers: isFullyActive(), isExpired(), isDowngradedToCore(), shouldApplyEvaluationBranding()
EntitlementStatusString-backed enumActive, GracePeriod, DowngradedToCore, EvaluationExpired, NoLicenseNonePerpetualFallback is retained but deprecated; kept only so pre-6.0 consumers still compile
FeatureGate::hasCapability()CapabilityCode $capabilityChecks the capability against the enforced effective edition and pack stateboolNoneFail-closed to core on any resolver error
FeatureGate::requireCapability()CapabilityCode $capabilityPasses silently or throws with structured upsell contextvoidSpectrumAuthenticationException SPEC-LIC-001 (not licensed) or SPEC-LIC-002 (pack expired)The context carries required_capability, required_pack, current_packs, upgrade_info_url
FeatureGate::isFeatureEnabled(), ::requireFeature()string $featureLegacy feature-identifier checkbool / voidSpectrumAuthenticationException SPEC-AUTH-007 (require path)Legacy compatibility; prefer capability checks
FeatureGate accessorsNoneLicense introspectionVariesNonecurrentEdition(), isLicenseValid(), isInGracePeriod(), getActivePacks(), trialPolicy()
CapabilityCodeString-backed enumCanonical capability identifiers (core.*, pro.*, enterprise.*, pack.*)NoneisPack(), packName(), minimumEdition(), forEdition()
CapabilityRegistryLicenseKey, route patternsRoute-to-capability resolution and pack lookup before dispatchVariesNoneregisterDefaultRoutes() covers the documented service routes
LicenseKeyReadonly value objectParsed, verified license claimsNoneisExpired(), isInGracePeriod(), isFullyExpired(), hasFeature(), hasCapability(), activePackNames(); maxSlots is contractual only
LicenseValidator::buildVerifiedLicenseKey()array $dataBuilds a typed LicenseKey from an already-verified payloadLicenseKeySpectrumAuthenticationException SPEC-AUTH-001 on schema violationSupports schema 2.0, 1.0, and legacy payloads
PackDefinition / PackRegistryReadonly value objectsAdd-on pack state with independent expiry and graceVariesNoneisActive(), isFullyExpired(), activePacks(), activeCapabilities()
TrialPolicybool $enabled, int $maxPagesPerSecond = 2, watermark fieldsTrial throughput-cap policyNonefromLicenseKey(), disabled(), isThrottled(); branding authority stays with the evaluator
ChannelEnforcementPolicy::enforce()LicenseKey $license, LocalState $state, int $systemNow, bool $revoked = falseRuns the two-timer machine plus revocation and trial re-labellingEnforcementOutcomeNoneRevocation overrides every timer and trial state
OnlineCheckScheduler::evaluate(), ::recordSuccessfulOnlineCheck()License, local state, timestampsTwo-timer evaluation; lease renewal from verified server timeEnforcementOutcome / LocalStateNoneONLINE_GRACE_SECONDS grants 48 hours of liveness grace past the lease
EnforcementDecision / EnforcementReason / EnforcementOutcomeEnums + readonly value objectActive, Grace, DowngradedToCore plus the precise reasonNoneEach reason maps deterministically to one decision
Runtime\EntitlementResolver::currentEntitlement()NoneInterface: the enforced entitlement the runtime servesEntitlementResultImplementation-definedEnforcedEntitlementResolver composes cache, state, policy, and evaluator, fail-closed to Core
LicenseStateCache::get(), ::getOrThrow()bool $forceRefresh = falseVerified-license cache backing the gate?LicenseKeygetOrThrow(): SpectrumAuthenticationException SPEC-AUTH-008; LicenseConfigurationException on invalid wiringinvalidate(), isCacheValid()
LicenseClient::activate(), ::heartbeat(), ::deactivate(), ::verifyOnline()string $licenseJws, string $deploymentId, ?string $machineFingerprintHash = null, ?string $expectedLicenseId = null, ?string $clientNonce = nullOne signed-status exchange; the response is verified against the pinned root and bound to the sent nonceStatusResponseLicenseClientException (transport, non-200, bad supplied nonce); SpectrumAuthenticationException (forged, tampered, stale, wrong-nonce, wrong-license, wrong-type response)Only the salted fingerprint hash is ever sent, never a raw fingerprint
LicenseClient::checkRenewal()string $licenseId, string $customerIdRenewal-availability probe; 404 is the normal “none available” answerRenewal array (available, download_url, expires_at)LicenseClientException (unreachable, non-200/404, or a policy-rejected download_url)The unsigned body’s URL is SSRF-screened before it can escape
LicenseClient::__construct()See signature fenceValidates the transport, URL scheme, and pin set at constructionLicenseConfigurationException (non-policy-enforcing transport, disallowed scheme, plain HTTP without pins, malformed pin set)getPinSet() exposes the active pin set for rotation tooling
Online\StatusResponseReadonly value objectThe verified signed statusNoneisLive() is false on revoked / expired / released; carries leaseExpiresAt, slot counts, and an entitlement snapshot
LicenseChannelMode / OnlineMode / OnlinePolicyEnums + readonly value objectChannel and online-policy claimsNonesigned_source vs ioncube; online mode required / optional / disabled with intervalDays and leaseDays
LicenseEditionString-backed enumCore, Pro, EnterpriseNonesatisfies() implements the tier hierarchy
public function __construct(private ?EvaluationBrandingConfig $evaluationBrandingConfig = null)
public function evaluate(?LicenseKey $license, ?DateTimeImmutable $now = null): EntitlementResult
public function evaluateEnforced(?LicenseKey $license, EnforcementOutcome $outcome): EntitlementResult
public function resolveBrandingMode(?LicenseKey $license, ?DateTimeImmutable $now = null): BrandingMode
public function resolveBrandingStrategy(?LicenseKey $license, ?DateTimeImmutable $now = null): BrandingStrategy
public function __construct(private readonly LicenseStateCache $licenseCache, private readonly EntitlementResolver $entitlementResolver, private readonly string $upgradeInfoUrl = self::DEFAULT_UPGRADE_URL)
public function isFeatureEnabled(string $feature): bool
public function requireFeature(string $feature): void
public function currentEdition(): ?string
public function isLicenseValid(): bool
public function isInGracePeriod(): bool
public function hasCapability(CapabilityCode $capability): bool
public function requireCapability(CapabilityCode $capability): void
public function getActivePacks(): array
public function trialPolicy(): TrialPolicy
public function __construct(private readonly ClientInterface $httpClient, private readonly RequestFactoryInterface $requestFactory, private readonly StreamFactoryInterface $streamFactory, private readonly StatusX5cValidator $statusValidator, private readonly string $baseUrl = self::DEFAULT_BASE_URL, #[SensitiveParameter] private readonly ?string $apiKey = null, array $pinnedPublicKeys = [], array $backupPins = [], private readonly OutboundHttpPolicy $policy = new OutboundHttpPolicy(allowedSchemes: ['https'], connectTimeoutSeconds: 10, readTimeoutSeconds: 30, maxResponseBytes: 10_485_760, followRedirects: false, allowPrivateTargets: false, retryAttempts: 0, retryBaseBackoffMs: 250))
public function activate(string $licenseJws, string $deploymentId, ?string $machineFingerprintHash = null, ?string $expectedLicenseId = null, ?string $clientNonce = null): StatusResponse
public function heartbeat(string $licenseJws, string $deploymentId, ?string $machineFingerprintHash = null, ?string $expectedLicenseId = null, ?string $clientNonce = null): StatusResponse
public function deactivate(string $licenseJws, string $deploymentId, ?string $machineFingerprintHash = null, ?string $expectedLicenseId = null, ?string $clientNonce = null): StatusResponse
public function verifyOnline(string $licenseJws, string $deploymentId, ?string $machineFingerprintHash = null, ?string $expectedLicenseId = null, ?string $clientNonce = null): StatusResponse
public function getPinSet(): ?PinSet
public function checkRenewal(string $licenseId, string $customerId): array

Entitlement evaluation produces one of five current states, each with a fixed runtime consequence:

StatusRuntimeEffective capabilitiesUpdates / supportBranding
Activeallowedentitled edition + active packsyes (paid); no (evaluation)none (paid); watermark (evaluation)
GracePeriodallowedentitled edition + active packsyesnone (paid)
DowngradedToCoreallowedCore onlynonone (paid); watermark (trial)
EvaluationExpiredrestrictednonenowatermark
NoLicensedisabled (fail-closed)nonenowatermark
  • Paid expiry never disables the installed runtime. runtimeAllowed stays true for every paid state. Past the grace window the effective edition downgrades to Core: Core capabilities keep running indefinitely, all Pro/Enterprise/pack capabilities gate off, and updates and support are revoked. The deprecated EntitlementStatus::PerpetualFallback case names the pre-6.0 expiry policy and exists only for compile compatibility.
  • Not every state ends in Core execution. Grace keeps the full entitled edition with a dated renewal warning. Evaluation expiry restricts the runtime and keeps the watermark. The no-license state disables Enterprise paths fail-closed and visibly watermarks any legacy output, so it can never pass as paid execution.
  • Fixed resolution order. A fully expired license resolves no capability. Pack capabilities defer to the pack’s own active state and independent expiry. Edition capabilities resolve against the enforced effective edition on the strict hierarchy Enterprise ⊇ Pro ⊇ Core. Raw LicenseKey::$edition is never consulted for gating.
  • Two timers; the most restrictive wins. The entitlement timer (both channels) drives Active, Grace, and the past-grace Core downgrade. The online-liveness timer applies only to the ioncube channel with online mode required: the lease, plus a 48-hour liveness grace, must be live. Offline past the lease, premium pauses at the Core tier; a later verified online check re-establishes the lease and premium resumes. The signed_source channel — and any non-required online mode — has no liveness timer and remains offline and air-gap capable.
  • Anti-rollback is deterministic. Enforcement evaluates against a monotonic floor: the effective time is the maximum of the system clock and the last verified time, so a backward clock change grants no extra time. A rollback exceeding the skew tolerance and the remaining grace forces the Core downgrade.
  • Leases advance on trusted time only. A successful online check records the verified, signed server time, never the local clock, so a local forward-clock jump cannot inflate the lease.
  • Branding has one authority. Only the evaluation channel triggers the watermark. A paid license resolves to no branding in every state, including the downgraded ones.
  • Trial is a policy layer on the same timers. Throughput is capped (default 2 pages/second), the evaluation watermark applies, and expiry is strict with no grace. Only the reported reason changes; the decision and effective edition follow the paid machine.
  • The online contract is signed end to end. Each request carries a CSPRNG client nonce. Each response is a compact EdDSA JWS of media type application/nextpdf-status+jwt, verified through an x5c chain to a compiled-in pinned root and bound to the sent nonce, the installed license id, and the invoked endpoint. The legacy unsigned envelope transport is retired; exactly one trust path remains. Configured SPKI pins travel as the X-NextPDF-Pinned-Public-Key header, and a pin set must include a backup pin.
  • No runtime seat enforcement. maxSlots is contractual only — no telemetry and no mandatory phone-home for ordinary operation.
  • A capability check on a fully expired license short-circuits to “unavailable” before any pack or edition logic runs.
  • An add-on pack can expire while the base license is active. The denial then reads pack expired (SPEC-LIC-002), not not-licensed (SPEC-LIC-001), and lists the currently active packs.
  • A local enforcement state file that is present but unreadable or unparseable is a tampering signal. The machine fails closed to the Core downgrade with EnforcementReason::CorruptState. A legitimately absent file is not corrupt and proceeds normally.
  • A clock rollback below the monotonic floor that exceeds the skew tolerance (default 120 seconds) and the grace remaining forces the Core downgrade deterministically.
  • A lapsed liveness lease downgrades an otherwise-Active ioncube license; entitlement expiry downgrades regardless of lease state.
  • evaluate(null) is fail-closed: the runtime is disallowed, the watermark applies, and the warning names the pricing URL. Hosts that need unlicensed Core behavior must construct the Core engine directly.
  • An expired evaluation license has no grace and no fallback; the runtime is restricted and the watermark stays.
  • checkRenewal() treats HTTP 404 as the normal “none available” answer. Any other non-200 raises LicenseClientException, and a download_url that violates the outbound policy fails closed before it can escape the method.
  • A forged, tampered, stale, wrong-nonce, or wrong-license status response never yields a StatusResponse; verification throws SpectrumAuthenticationException. A signed but revoked, expired, or released status verifies and surfaces as a downgrade signal (isLive() is false).
  • A status response minted for a different exchange is rejected: the verified response_type must match the invoked endpoint.
  • FeatureGate fails closed on any resolver error: the effective edition degrades to core and no premium capability is granted.
  • EnforcedEntitlementResolver is fail-closed end to end: any exception in its chain yields the no-license (Core) result and a PSR-3 error log, never a premium grant.

The licensing layer adds no cryptographic policy of its own. License and status signatures are Ed25519 verifications performed by the host crypto provider. In a FIPS-constrained build, an algorithm the provider does not approve fails at the cryptographic boundary with an explicit error; nothing downgrades silently.

ClaimStandardClause
License files and status responses are compact-serialization JWS, verified fail-closed.RFC 7515§3.1
License and status signatures are Ed25519.RFC 8032§5.1
The x5c chain in a signed response validates back to a compiled-in pinned trust anchor.RFC 5280§6.1
A configured SPKI pin set must include a backup pin for a not-yet-deployed key.RFC 7469§4.3
Verified claims (iss, aud, exp, nbf, iat) are registered claim names.RFC 7519§4.1

The module aligns its behavior with the cited clauses as a capability; acceptance of a license or status envelope is decided by NextPDF’s own pinned-root verifier.

  • Construct LicenseClient with a policy-enforcing transport (canonically SecurityAwareHttpClient). A raw PSR-18 client is rejected at construction so license traffic can never be sent without the anti-SSRF, no-redirect, byte-cap, and timeout controls.
  • A caller-supplied client nonce must decode to at least 16 bytes of entropy. The source grounds this bound in RFC 8555 §6.5; that clause is outside the cited corpus, so the bound is stated here from product source only.
  • Gate features through FeatureGate; never branch on raw LicenseKey::$edition. The gate consults only the enforced effective edition and fails closed to core.
  • Do not switch on the deprecated EntitlementStatus::PerpetualFallback in new code. Test isDowngradedToCore() or match DowngradedToCore instead.
  • Treat a capability denial as a remediation payload. Surface its structured context and upgrade URL to the caller rather than collapsing it into a generic error.
  • This module concerns cryptographic functionality; treat it as security-sensitive in your own review. Capacity and seat terms are governed by the license agreement, not by runtime enforcement. This reference is not a legal opinion; consult your own compliance and legal advisers.

This page documents externally observable behavior and the supported public API surface only. Internal namespace paths, helper classes, mechanism tables, runbook filenames, and ticket prefixes are out of scope.