9ad11a021c
- Add ConfigProfile and ConfigProfileInclude data models with migrations - Implement profile resolver service with ordered includes and merge rules - Add profile CRUD API with validation, compatibility, and cycle detection - Add instance API plumbing for profile selection on create/start/restart - Add resolved profile preview and default resolution APIs - Add frontend config profile API client and management UI - Add launch/restart profile selection UI - Add backend integration and unit tests (31 passing) OpenSpec: add-config-profiles Quality gates: ruff, TypeScript compile, 31 tests passing
72 lines
2.2 KiB
Python
72 lines
2.2 KiB
Python
import hmac
|
|
import hashlib
|
|
import json
|
|
import base64
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Any
|
|
|
|
from src.config import Settings
|
|
|
|
|
|
def _base64url_encode(data: bytes) -> str:
|
|
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
|
|
|
|
def _base64url_decode(data: str) -> bytes:
|
|
padding = 4 - len(data) % 4
|
|
if padding != 4:
|
|
data += "=" * padding
|
|
return base64.urlsafe_b64decode(data)
|
|
|
|
|
|
def create_session_cookie(*, settings: Settings, user_id: str) -> str:
|
|
"""Create a signed session cookie value."""
|
|
payload = {
|
|
"user_id": user_id,
|
|
"exp": int((datetime.now(timezone.utc) + timedelta(hours=settings.session_ttl_hours)).timestamp()),
|
|
}
|
|
|
|
header = _base64url_encode(json.dumps({"alg": "HS256", "typ": "session"}).encode())
|
|
payload_encoded = _base64url_encode(json.dumps(payload).encode())
|
|
message = f"{header}.{payload_encoded}"
|
|
|
|
signature = hmac.new(
|
|
settings.session_secret.encode(),
|
|
message.encode(),
|
|
hashlib.sha256,
|
|
).digest()
|
|
signature_encoded = _base64url_encode(signature)
|
|
|
|
return f"{message}.{signature_encoded}"
|
|
|
|
|
|
def decode_session_cookie(*, settings: Settings, cookie_value: str) -> dict[str, Any]:
|
|
"""Decode and verify a session cookie. Returns payload or raises ValueError."""
|
|
parts = cookie_value.split(".")
|
|
if len(parts) != 3:
|
|
raise ValueError("invalid session format")
|
|
|
|
header, payload_encoded, signature_encoded = parts
|
|
message = f"{header}.{payload_encoded}"
|
|
|
|
# Verify signature
|
|
expected_signature = hmac.new(
|
|
settings.session_secret.encode(),
|
|
message.encode(),
|
|
hashlib.sha256,
|
|
).digest()
|
|
expected_signature_encoded = _base64url_encode(expected_signature)
|
|
|
|
if not hmac.compare_digest(signature_encoded, expected_signature_encoded):
|
|
raise ValueError("invalid session signature")
|
|
|
|
# Decode payload
|
|
payload_bytes = _base64url_decode(payload_encoded)
|
|
payload = json.loads(payload_bytes)
|
|
|
|
# Check expiry
|
|
if payload.get("exp", 0) < int(datetime.now(timezone.utc).timestamp()):
|
|
raise ValueError("session expired")
|
|
|
|
return payload
|