use remora_domain::OpaqueId; use remora_durable::DurableError; use sqlx::PgPool; use time::OffsetDateTime; use crate::error::IdentityError; use crate::principal::{ExternalIdentity, Principal, principal_id_for}; #[derive(Clone, Debug, Eq, PartialEq)] pub struct PrincipalRecord { pub principal_id: OpaqueId, pub issuer: String, pub subject: String, pub created_at: OffsetDateTime, } /// Resolve or create the durable principal mapping for an external identity. pub async fn resolve_principal( pool: &PgPool, external: ExternalIdentity, ) -> Result { let principal_id = principal_id_for(&external.issuer, &external.subject)?; sqlx::query( "INSERT INTO principal_identities (principal_id, issuer, subject) VALUES ($1, $2, $3) ON CONFLICT (issuer, subject) DO NOTHING", ) .bind(principal_id.as_str()) .bind(&external.issuer) .bind(&external.subject) .execute(pool) .await .map_err(DurableError::from)?; let record = load_by_external(pool, &external.issuer, &external.subject) .await? .ok_or_else(|| { IdentityError::Config("principal insert succeeded but row is missing".into()) })?; // Prefer the stored id so a future non-deterministic scheme remains stable. Ok(Principal::from_external(record.principal_id, external)) } pub async fn load_by_id( pool: &PgPool, principal_id: &OpaqueId, ) -> Result, IdentityError> { let row = sqlx::query_as::<_, PrincipalRow>( "SELECT principal_id, issuer, subject, created_at FROM principal_identities WHERE principal_id = $1", ) .bind(principal_id.as_str()) .fetch_optional(pool) .await .map_err(DurableError::from)?; row.map(PrincipalRow::into_record).transpose() } pub async fn load_by_external( pool: &PgPool, issuer: &str, subject: &str, ) -> Result, IdentityError> { let row = sqlx::query_as::<_, PrincipalRow>( "SELECT principal_id, issuer, subject, created_at FROM principal_identities WHERE issuer = $1 AND subject = $2", ) .bind(issuer) .bind(subject) .fetch_optional(pool) .await .map_err(DurableError::from)?; row.map(PrincipalRow::into_record).transpose() } #[derive(sqlx::FromRow)] struct PrincipalRow { principal_id: String, issuer: String, subject: String, created_at: OffsetDateTime, } impl PrincipalRow { fn into_record(self) -> Result { Ok(PrincipalRecord { principal_id: OpaqueId::parse(self.principal_id)?, issuer: self.issuer, subject: self.subject, created_at: self.created_at, }) } }