stylobridge

1.0.0

Air-gapped MCP document converter — Markdown/PDF to branded DOCX with audit logging

License
Unknown license
Published
March 21, 2026
21h ago
Package Registry
README badge Customize →
License Sources
SourceLicenseClass
Licensie (detected)
Pending-
PyPI (reported)
Not reported-

License detection is still in progress for this version.

Loading dependencies…
License File
"""JWT license validation for StyloBridge Enterprise.

Uses RS256 asymmetric signing with audience claim to prevent
cross-app token replay attacks.
"""

import os
from pathlib import Path

import jwt

from src.schemas.license_claims import LicenseClaims


class LicenseError(Exception):
    """Raised when license validation fails."""


# Load public key once at module level
_PUBLIC_KEY: str | None = None


def _get_public_key() -> str:
    """Load and cache the RSA public key."""
    global _PUBLIC_KEY
    if _PUBLIC_KEY is not None:
        return _PUBLIC_KEY

    # Try environment variable first, then file
    env_key = os.getenv("STYLOBRIDGE_PUBLIC_KEY")
    if env_key:
        _PUBLIC_KEY = env_key
        return _PUBLIC_KEY

    key_path = Path(__file__).parent / "public_key.pem"
    if key_path.exists():
        if key_path.is_symlink():
            raise LicenseError("Public key file must not be a symlink")
        _PUBLIC_KEY = key_path.read_text()
        return _PUBLIC_KEY

    raise LicenseError("No public key found. Set STYLOBRIDGE_PUBLIC_KEY or place src/public_key.pem")


def validate_license(token: str) -> dict:
    """Validate a StyloBridge JWT license token.

    Enforcement logic:
      1. Validate RS256 signature (rejects tampered/forged tokens)
      2. Check expiration date (rejects expired licenses)
      3. Return claims including 'tier' for feature gating

    Tier-based access (enforced in main.py):
      - "standard"   → convert_to_docx (no templates, no audit log)
      - "enterprise" → all tools + corporate templates + audit logging

    The 'seats' claim is metadata for billing/invoicing —
    not enforced at runtime.

    Args:
        token: The JWT token string.

    Returns:
        Decoded claims as a dict.

    Raises:
        LicenseError: If the token is invalid, expired, or has wrong claims.
    """
    if not token:
        raise LicenseError("No license token provided")

    public_key = _get_public_key()

    # Step 1: Validate signature (RS256)
    # Step 2: Check expiration (built into jwt.decode)
    try:
        payload = jwt.decode(
            token,
            public_key,
            algorithms=["RS256"],
            issuer="stylobridge",
            audience="stylobridge",
        )
    except jwt.ExpiredSignatureError:
        raise LicenseError("License has expired")
    except jwt.InvalidIssuerError:
        raise LicenseError("Invalid license issuer")
    except jwt.InvalidAudienceError:
        raise LicenseError("Invalid license audience (possible cross-app token replay)")
    except jwt.InvalidSignatureError:
        raise LicenseError("Invalid license signature (token may be tampered)")
    except jwt.DecodeError:
        raise LicenseError("Failed to decode license token")

    # Step 3: Validate structure (including tier)
    try:
        LicenseClaims(**payload)
    except Exception:
        raise LicenseError("License token has invalid or missing claims")

    return payload
Versions
1 version
VersionLicensePublishedStatus
1.0.0 Latest Viewing-Mar 21, 2026 Pending