feat: wire license.effective_tier into tiers.py; add dev_override priority

This commit is contained in:
pyr0ball 2026-02-25 23:05:55 -08:00
parent bf2d0f81c7
commit 58ebd57c49
3 changed files with 99 additions and 0 deletions

1
.gitignore vendored
View file

@ -29,3 +29,4 @@ scrapers/.debug/
scrapers/raw_scrapes/ scrapers/raw_scrapes/
compose.override.yml compose.override.yml
config/license.json

View file

@ -65,3 +65,32 @@ def tier_label(feature: str) -> str:
if required is None: if required is None:
return "" return ""
return "🔒 Paid" if required == "paid" else "⭐ Premium" return "🔒 Paid" if required == "paid" else "⭐ Premium"
def effective_tier(
profile=None,
license_path=None,
public_key_path=None,
) -> str:
"""Return the effective tier for this installation.
Priority:
1. profile.dev_tier_override (developer mode override)
2. License JWT verification (offline RS256 check)
3. "free" (fallback)
license_path and public_key_path default to production paths when None.
Pass explicit paths in tests to avoid touching real files.
"""
if profile and getattr(profile, "dev_tier_override", None):
return profile.dev_tier_override
from scripts.license import effective_tier as _license_tier
from pathlib import Path as _Path
kwargs = {}
if license_path is not None:
kwargs["license_path"] = _Path(license_path)
if public_key_path is not None:
kwargs["public_key_path"] = _Path(public_key_path)
return _license_tier(**kwargs)

View file

@ -0,0 +1,69 @@
import json
import pytest
from pathlib import Path
from datetime import datetime, timedelta, timezone
from unittest.mock import patch
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import jwt as pyjwt
@pytest.fixture()
def license_env(tmp_path):
"""Returns (private_pem, public_path, license_path) for tier integration tests."""
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
public_pem = private_key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
public_path = tmp_path / "public.pem"
public_path.write_bytes(public_pem)
license_path = tmp_path / "license.json"
return private_pem, public_path, license_path
def _write_jwt_license(license_path, private_pem, tier="paid", days=30):
now = datetime.now(timezone.utc)
token = pyjwt.encode({
"sub": "CFG-PRNG-TEST", "product": "peregrine", "tier": tier,
"iat": now, "exp": now + timedelta(days=days),
}, private_pem, algorithm="RS256")
license_path.write_text(json.dumps({"jwt": token, "grace_until": None}))
def test_effective_tier_free_without_license(tmp_path):
from app.wizard.tiers import effective_tier
tier = effective_tier(
profile=None,
license_path=tmp_path / "missing.json",
public_key_path=tmp_path / "key.pem",
)
assert tier == "free"
def test_effective_tier_paid_with_valid_license(license_env):
private_pem, public_path, license_path = license_env
_write_jwt_license(license_path, private_pem, tier="paid")
from app.wizard.tiers import effective_tier
tier = effective_tier(profile=None, license_path=license_path,
public_key_path=public_path)
assert tier == "paid"
def test_effective_tier_dev_override_takes_precedence(license_env):
"""dev_tier_override wins even when a valid license is present."""
private_pem, public_path, license_path = license_env
_write_jwt_license(license_path, private_pem, tier="paid")
class FakeProfile:
dev_tier_override = "premium"
from app.wizard.tiers import effective_tier
tier = effective_tier(profile=FakeProfile(), license_path=license_path,
public_key_path=public_path)
assert tier == "premium"