import base64 import hashlib import hmac import json import time from typing import Any, Dict, Optional from core.settings import WORKSPACE_PREVIEW_SIGNING_SECRET, WORKSPACE_PREVIEW_TOKEN_TTL_SECONDS HTML_PREVIEW_EXTENSIONS = {".html", ".htm"} def _b64url_encode(raw: bytes) -> str: return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=") def _b64url_decode(raw: str) -> bytes: padding = "=" * (-len(raw) % 4) return base64.urlsafe_b64decode(f"{raw}{padding}".encode("ascii")) def normalize_workspace_preview_path(path: str) -> str: return "/".join(part for part in str(path or "").strip().replace("\\", "/").split("/") if part) def is_html_preview_path(path: str) -> bool: normalized = normalize_workspace_preview_path(path).lower() return any(normalized.endswith(ext) for ext in HTML_PREVIEW_EXTENSIONS) def create_workspace_preview_token(bot_id: str, path: str, ttl_seconds: Optional[int] = None) -> Dict[str, Any]: normalized_bot_id = str(bot_id or "").strip() normalized_path = normalize_workspace_preview_path(path) ttl = max(60, min(int(ttl_seconds or WORKSPACE_PREVIEW_TOKEN_TTL_SECONDS), 86400)) expires_at = int(time.time()) + ttl payload = { "bot_id": normalized_bot_id, "entry_path": normalized_path, "exp": expires_at, "kind": "workspace-preview-session", } payload_json = json.dumps(payload, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode("utf-8") body = _b64url_encode(payload_json) signature = hmac.new( WORKSPACE_PREVIEW_SIGNING_SECRET.encode("utf-8"), body.encode("ascii"), hashlib.sha256, ).digest() return { "token": f"{body}.{_b64url_encode(signature)}", "expires_at": expires_at, "ttl_seconds": ttl, } def resolve_workspace_preview_token(token: str) -> Optional[Dict[str, Any]]: raw_token = str(token or "").strip() if not raw_token or "." not in raw_token: return None body, signature_raw = raw_token.split(".", 1) expected_signature = hmac.new( WORKSPACE_PREVIEW_SIGNING_SECRET.encode("utf-8"), body.encode("ascii"), hashlib.sha256, ).digest() try: provided_signature = _b64url_decode(signature_raw) except Exception: return None if not hmac.compare_digest(expected_signature, provided_signature): return None try: payload = json.loads(_b64url_decode(body).decode("utf-8")) except Exception: return None if not isinstance(payload, dict): return None if str(payload.get("kind") or "") != "workspace-preview-session": return None bot_id = str(payload.get("bot_id") or "").strip() entry_path = normalize_workspace_preview_path(str(payload.get("entry_path") or "")) if not bot_id or not is_html_preview_path(entry_path): return None try: expires_at = int(payload.get("exp") or 0) except Exception: return None if expires_at < int(time.time()): return None return { "bot_id": bot_id, "entry_path": entry_path, "expires_at": expires_at, }