Enterprise edition
Licensing — Deep Reference
At a glance
Section titled “At a glance”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.
Availability & licensing
Section titled “Availability & licensing”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.
Public API surface
Section titled “Public API surface”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.
| Symbol | Parameters | Default behavior | Returns | Throws or fails with | Notes |
|---|---|---|---|---|---|
EntitlementEvaluator::evaluate() | ?LicenseKey $license, ?DateTimeImmutable $now = null | Maps a verified license (or null) to the full entitlement decision | EntitlementResult | None | A null license yields the fail-closed no-license result |
EntitlementEvaluator::evaluateEnforced() | ?LicenseKey $license, EnforcementOutcome $outcome | Converts a pre-computed enforcement outcome into an entitlement result | EntitlementResult | None | The effective edition comes from the outcome; every enforcement downgrade lands on Core |
EntitlementEvaluator::resolveBrandingMode(), ::resolveBrandingStrategy() | ?LicenseKey $license, ?DateTimeImmutable $now = null | Resolves the branding decision for generated output | BrandingMode / BrandingStrategy | None | Sole branding authority; only the evaluation channel watermarks |
EntitlementResult | Readonly value object | Carries status, edition, channel, branding mode, runtime/update/support flags, and an optional warning | — | None | Helpers: isFullyActive(), isExpired(), isDowngradedToCore(), shouldApplyEvaluationBranding() |
EntitlementStatus | String-backed enum | Active, GracePeriod, DowngradedToCore, EvaluationExpired, NoLicense | — | None | PerpetualFallback is retained but deprecated; kept only so pre-6.0 consumers still compile |
FeatureGate::hasCapability() | CapabilityCode $capability | Checks the capability against the enforced effective edition and pack state | bool | None | Fail-closed to core on any resolver error |
FeatureGate::requireCapability() | CapabilityCode $capability | Passes silently or throws with structured upsell context | void | SpectrumAuthenticationException 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 $feature | Legacy feature-identifier check | bool / void | SpectrumAuthenticationException SPEC-AUTH-007 (require path) | Legacy compatibility; prefer capability checks |
FeatureGate accessors | None | License introspection | Varies | None | currentEdition(), isLicenseValid(), isInGracePeriod(), getActivePacks(), trialPolicy() |
CapabilityCode | String-backed enum | Canonical capability identifiers (core.*, pro.*, enterprise.*, pack.*) | — | None | isPack(), packName(), minimumEdition(), forEdition() |
CapabilityRegistry | LicenseKey, route patterns | Route-to-capability resolution and pack lookup before dispatch | Varies | None | registerDefaultRoutes() covers the documented service routes |
LicenseKey | Readonly value object | Parsed, verified license claims | — | None | isExpired(), isInGracePeriod(), isFullyExpired(), hasFeature(), hasCapability(), activePackNames(); maxSlots is contractual only |
LicenseValidator::buildVerifiedLicenseKey() | array $data | Builds a typed LicenseKey from an already-verified payload | LicenseKey | SpectrumAuthenticationException SPEC-AUTH-001 on schema violation | Supports schema 2.0, 1.0, and legacy payloads |
PackDefinition / PackRegistry | Readonly value objects | Add-on pack state with independent expiry and grace | Varies | None | isActive(), isFullyExpired(), activePacks(), activeCapabilities() |
TrialPolicy | bool $enabled, int $maxPagesPerSecond = 2, watermark fields | Trial throughput-cap policy | — | None | fromLicenseKey(), disabled(), isThrottled(); branding authority stays with the evaluator |
ChannelEnforcementPolicy::enforce() | LicenseKey $license, LocalState $state, int $systemNow, bool $revoked = false | Runs the two-timer machine plus revocation and trial re-labelling | EnforcementOutcome | None | Revocation overrides every timer and trial state |
OnlineCheckScheduler::evaluate(), ::recordSuccessfulOnlineCheck() | License, local state, timestamps | Two-timer evaluation; lease renewal from verified server time | EnforcementOutcome / LocalState | None | ONLINE_GRACE_SECONDS grants 48 hours of liveness grace past the lease |
EnforcementDecision / EnforcementReason / EnforcementOutcome | Enums + readonly value object | Active, Grace, DowngradedToCore plus the precise reason | — | None | Each reason maps deterministically to one decision |
Runtime\EntitlementResolver::currentEntitlement() | None | Interface: the enforced entitlement the runtime serves | EntitlementResult | Implementation-defined | EnforcedEntitlementResolver composes cache, state, policy, and evaluator, fail-closed to Core |
LicenseStateCache::get(), ::getOrThrow() | bool $forceRefresh = false | Verified-license cache backing the gate | ?LicenseKey | getOrThrow(): SpectrumAuthenticationException SPEC-AUTH-008; LicenseConfigurationException on invalid wiring | invalidate(), isCacheValid() |
LicenseClient::activate(), ::heartbeat(), ::deactivate(), ::verifyOnline() | string $licenseJws, string $deploymentId, ?string $machineFingerprintHash = null, ?string $expectedLicenseId = null, ?string $clientNonce = null | One signed-status exchange; the response is verified against the pinned root and bound to the sent nonce | StatusResponse | LicenseClientException (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 $customerId | Renewal-availability probe; 404 is the normal “none available” answer | Renewal 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 fence | Validates the transport, URL scheme, and pin set at construction | — | LicenseConfigurationException (non-policy-enforcing transport, disallowed scheme, plain HTTP without pins, malformed pin set) | getPinSet() exposes the active pin set for rotation tooling |
Online\StatusResponse | Readonly value object | The verified signed status | — | None | isLive() is false on revoked / expired / released; carries leaseExpiresAt, slot counts, and an entitlement snapshot |
LicenseChannelMode / OnlineMode / OnlinePolicy | Enums + readonly value object | Channel and online-policy claims | — | None | signed_source vs ioncube; online mode required / optional / disabled with intervalDays and leaseDays |
LicenseEdition | String-backed enum | Core, Pro, Enterprise | — | None | satisfies() implements the tier hierarchy |
public function __construct(private ?EvaluationBrandingConfig $evaluationBrandingConfig = null)public function evaluate(?LicenseKey $license, ?DateTimeImmutable $now = null): EntitlementResultpublic function evaluateEnforced(?LicenseKey $license, EnforcementOutcome $outcome): EntitlementResultpublic function resolveBrandingMode(?LicenseKey $license, ?DateTimeImmutable $now = null): BrandingModepublic function resolveBrandingStrategy(?LicenseKey $license, ?DateTimeImmutable $now = null): BrandingStrategypublic function __construct(private readonly LicenseStateCache $licenseCache, private readonly EntitlementResolver $entitlementResolver, private readonly string $upgradeInfoUrl = self::DEFAULT_UPGRADE_URL)public function isFeatureEnabled(string $feature): boolpublic function requireFeature(string $feature): voidpublic function currentEdition(): ?stringpublic function isLicenseValid(): boolpublic function isInGracePeriod(): boolpublic function hasCapability(CapabilityCode $capability): boolpublic function requireCapability(CapabilityCode $capability): voidpublic function getActivePacks(): arraypublic function trialPolicy(): TrialPolicypublic 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): StatusResponsepublic function heartbeat(string $licenseJws, string $deploymentId, ?string $machineFingerprintHash = null, ?string $expectedLicenseId = null, ?string $clientNonce = null): StatusResponsepublic function deactivate(string $licenseJws, string $deploymentId, ?string $machineFingerprintHash = null, ?string $expectedLicenseId = null, ?string $clientNonce = null): StatusResponsepublic function verifyOnline(string $licenseJws, string $deploymentId, ?string $machineFingerprintHash = null, ?string $expectedLicenseId = null, ?string $clientNonce = null): StatusResponsepublic function getPinSet(): ?PinSetpublic function checkRenewal(string $licenseId, string $customerId): arrayBehavior contract
Section titled “Behavior contract”Entitlement evaluation produces one of five current states, each with a fixed runtime consequence:
| Status | Runtime | Effective capabilities | Updates / support | Branding |
|---|---|---|---|---|
Active | allowed | entitled edition + active packs | yes (paid); no (evaluation) | none (paid); watermark (evaluation) |
GracePeriod | allowed | entitled edition + active packs | yes | none (paid) |
DowngradedToCore | allowed | Core only | no | none (paid); watermark (trial) |
EvaluationExpired | restricted | none | no | watermark |
NoLicense | disabled (fail-closed) | none | no | watermark |
- Paid expiry never disables the installed runtime.
runtimeAllowedstays 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 deprecatedEntitlementStatus::PerpetualFallbackcase 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::$editionis 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
ioncubechannel with online moderequired: 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. Thesigned_sourcechannel — 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 theX-NextPDF-Pinned-Public-Keyheader, and a pin set must include a backup pin. - No runtime seat enforcement.
maxSlotsis contractual only — no telemetry and no mandatory phone-home for ordinary operation.
Edge cases & failure modes
Section titled “Edge cases & failure modes”- 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
ioncubelicense; 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 raisesLicenseClientException, and adownload_urlthat 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 throwsSpectrumAuthenticationException. A signed butrevoked,expired, orreleasedstatus verifies and surfaces as a downgrade signal (isLive()is false). - A status response minted for a different exchange is rejected: the verified
response_typemust match the invoked endpoint. FeatureGatefails closed on any resolver error: the effective edition degrades tocoreand no premium capability is granted.EnforcedEntitlementResolveris 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.
FIPS-mode behavior
Section titled “FIPS-mode behavior”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.
Conformance
Section titled “Conformance”| Claim | Standard | Clause |
|---|---|---|
| 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.
Development notes
Section titled “Development notes”- Construct
LicenseClientwith a policy-enforcing transport (canonicallySecurityAwareHttpClient). 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 rawLicenseKey::$edition. The gate consults only the enforced effective edition and fails closed tocore. - Do not switch on the deprecated
EntitlementStatus::PerpetualFallbackin new code. TestisDowngradedToCore()or matchDowngradedToCoreinstead. - 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.
Publication boundary
Section titled “Publication boundary”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.
See also
Section titled “See also”- Licensing — NextPDF Enterprise — the capability page with setup and integration guidance.
- Billing — Deep Reference — plan tiers, overage policies, and rollover.
- SaaS — Deep Reference — the multi-tenant service surface the route map guards.
- Branding — Deep Reference — the branding strategies the evaluator resolves.