from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from scribe.models import Base from scribe.models.base import TimestampMixin, iso class ForgeConnection(Base, TimestampMixin): """One user's read-only credential for one git forge host. The keyring model: a user owns a set of connections and every server-side forge read for a project runs on the PROJECT OWNER's set, resolved by the repo's host. One row per (user, host) — the repo's host picks the connection deterministically, so there is no "default forge" pointer to maintain or tie-break. `host` is derived from `base_url` at write time and stored because it is the lookup key; the service layer keeps the two in step. The token is a secret: it is stored sealed (services/secrets.py — read it through services.forge_connections.connection_token), to_dict never includes it, and no route may return it. """ __tablename__ = "forge_connections" __table_args__ = ( UniqueConstraint("user_id", "host", name="uq_forge_connections_user_host"), ) id: Mapped[int] = mapped_column(primary_key=True) user_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False ) kind: Mapped[str] = mapped_column(Text, nullable=False) base_url: Mapped[str] = mapped_column(Text, nullable=False) host: Mapped[str] = mapped_column(Text, nullable=False) token_sealed: Mapped[str] = mapped_column(Text, nullable=False) def to_dict(self) -> dict: return { "id": self.id, "user_id": self.user_id, "kind": self.kind, "base_url": self.base_url, "host": self.host, "created_at": iso(self.created_at), "updated_at": iso(self.updated_at), }