Ctrl K
ComunidadGuías…

license.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""
The Team plan, unlocked by a license. What blocks and holds is free and stays free; a license adds what a team needs:

  people     more than one person on the gateway (approvers, viewers, admins)
  phone      approvals on a phone: push notifications (ntfy) and Face ID / fingerprint (passkeys)
  channels   approvals and the weekly report in Slack, Teams and email
  audit      the audit export (JSON, CSV, JSONL) and the evidence pack for an auditor

A license is one line from squidbrake.com: base64url(JSON) "." base64url(Ed25519 signature), checked offline against
PUBLIC_KEY. It goes in SQUIDBRAKE_LICENSE or data/license.key (`squidbrake license set LICENSE`). Until GRACE_UNTIL
everyone keeps these features, so nobody who uses them today is cut off without notice.
"""
from __future__ import annotations

import base64
import json
import os
from datetime import date, datetime, timezone
from pathlib import Path

import signing

PUBLIC_KEY_HEX = "57316a1391b1d078f922eeea02ffa5a5f26098f36b48e2048f3e526b6157fe12"   # its secret half is only on squidbrake.com's server
GRACE_UNTIL = "2026-11-20"
FILE = "license.key"
FEATURES = {"people": "more than one person", "phone": "approvals on a phone",
            "channels": "Slack, Teams and email", "audit": "audit export and the evidence pack",
            "sso": "sign-in with your company's SSO"}
UPGRADE_URL = "https://squidbrake.com/signup?plan=team&from=license"


def _b64(s: str) -> bytes:
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))


def issue(secret: bytes, org: str, expires: str, plan: str = "team") -> str:
    """A license for `org` until `expires` (YYYY-MM-DD). Only squidbrake.com holds the secret."""
    body = json.dumps({"org": org, "plan": plan, "expires": expires}, separators=(",", ":")).encode()
    enc = lambda b: base64.urlsafe_b64encode(b).decode().rstrip("=")
    return enc(body) + "." + enc(signing.ed25519_sign(secret, body))


def check(token: str) -> dict | None:
    """The license's terms if its signature is ours and it hasn't expired, else None."""
    try:
        body, sig = token.strip().split(".")
        raw = _b64(body)
        if not signing.ed25519_verify(bytes.fromhex(PUBLIC_KEY_HEX), raw, _b64(sig)):
            return None
        terms = json.loads(raw)
        if date.fromisoformat(terms["expires"]) < datetime.now(timezone.utc).date():
            return None
        return terms
    except Exception:          # malformed: no license, never a crash
        return None


def current(data_dir: Path) -> dict | None:
    token = os.getenv("SQUIDBRAKE_LICENSE", "")
    if not token:
        try:
            token = (Path(data_dir) / FILE).read_text(encoding="utf-8")
        except OSError:
            return None
    return check(token)


def in_grace() -> bool:
    return datetime.now(timezone.utc).date() < date.fromisoformat(GRACE_UNTIL)


def paid(data_dir: Path) -> bool:
    return in_grace() or current(data_dir) is not None


def status(data_dir: Path) -> dict:
    lic = current(data_dir)
    return {"licensed": lic is not None, "org": lic and lic.get("org"), "expires": lic and lic.get("expires"),
            "grace_until": GRACE_UNTIL, "in_grace": in_grace(), "team_features": paid(data_dir),
            "features": list(FEATURES.values()), "upgrade_url": UPGRADE_URL}


def need(data_dir: Path, feature: str) -> None:
    """Raise 402 with what to do, when a Team feature is used without a license (after the grace period)."""
    if not paid(data_dir):
        from fastapi import HTTPException
        raise HTTPException(402, f"{FEATURES[feature][0].upper()}{FEATURES[feature][1:]} is part of the Team plan. "
                                 f"Start a 14-day trial or get a license: {UPGRADE_URL}")