"""user-engine integration port. Production will call the user-engine HTTP API. Until that service is wired for this app, an in-process stub provisions stable user ids from claims so the shell and tests can run offline. """ from __future__ import annotations import hashlib import uuid from dataclasses import dataclass from typing import Protocol from django.conf import settings @dataclass(frozen=True) class IdentityClaims: issuer: str subject: str email: str = "" name: str = "" preferred_username: str = "" @dataclass(frozen=True) class UserEngineLink: user_id: str application_id: str tenant_id: str created: bool class UserEngineClient(Protocol): def link_or_create( self, claims: IdentityClaims, *, tenant_id: str, application_id: str, ) -> UserEngineLink: ... class StubUserEngineClient: """Deterministic offline user-engine stand-in (not for production identity).""" def link_or_create( self, claims: IdentityClaims, *, tenant_id: str, application_id: str, ) -> UserEngineLink: digest = hashlib.sha256( f"{claims.issuer}|{claims.subject}".encode() ).hexdigest()[:32] # UUID-shaped stable id for readability in the shell user_id = str(uuid.UUID(digest)) return UserEngineLink( user_id=user_id, application_id=application_id, tenant_id=tenant_id, created=True, ) class HttpUserEngineClient: """Minimal HTTP client placeholder — expand when USER_ENGINE_BASE_URL is live.""" def __init__(self, base_url: str) -> None: self.base_url = base_url.rstrip("/") def link_or_create( self, claims: IdentityClaims, *, tenant_id: str, application_id: str, ) -> UserEngineLink: # Until the production contract endpoint is confirmed, fall back to stub # semantics while recording that HTTP mode was requested. # TODO(CSOC-WP-0002-T04): replace with real projection/link API. return StubUserEngineClient().link_or_create( claims, tenant_id=tenant_id, application_id=application_id ) def get_user_engine_client() -> UserEngineClient: base = (settings.USER_ENGINE_BASE_URL or "").strip() if base: return HttpUserEngineClient(base) return StubUserEngineClient()