Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions rust/crates/du-db/src/ibd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,13 @@ pub struct SuggestionReport {
pub suggestions_written: u64,
}

/// A ranked suggestion for a sample (the reader's row).
/// A ranked suggestion for a sample (the reader's row). `target_sample_guid` is the reader's
/// **own** sample the candidate was matched against — the caller already owns it, so returning
/// it reveals nothing new, and the Edge needs it as the `claimed_sample` of an
/// [`messages::attest`] report (which [`record_attestation`] gates on ownership).
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct SuggestionView {
pub target_sample_guid: Uuid,
pub suggested_sample_guid: Uuid,
pub suggestion_type: String,
pub score: Option<f64>,
Expand All @@ -89,7 +93,7 @@ pub struct SuggestionView {
/// Serve a sample's ranked active candidates (used by the eventual consent-gated API).
pub async fn suggestions_for(pool: &PgPool, sample_guid: Uuid, limit: i64) -> Result<Vec<SuggestionView>, DbError> {
Ok(sqlx::query_as(
"SELECT suggested_sample_guid, suggestion_type, score, metadata \
"SELECT target_sample_guid, suggested_sample_guid, suggestion_type, score, metadata \
FROM ibd.match_suggestion \
WHERE target_sample_guid = $1 AND status = 'ACTIVE' \
ORDER BY score DESC NULLS LAST LIMIT $2",
Expand Down Expand Up @@ -133,7 +137,7 @@ pub mod messages {
/// a counterpart DID (identity reveal stays Edge-to-Edge over D1 consent).
pub async fn suggestions_for_did(pool: &PgPool, did: &str, limit: i64) -> Result<Vec<SuggestionView>, DbError> {
Ok(sqlx::query_as(
"SELECT ms.suggested_sample_guid, ms.suggestion_type, ms.score, ms.metadata \
"SELECT ms.target_sample_guid, ms.suggested_sample_guid, ms.suggestion_type, ms.score, ms.metadata \
FROM ibd.match_suggestion ms \
JOIN core.biosample b ON b.sample_guid = ms.target_sample_guid \
WHERE b.atproto->>'repo_did' = $1 AND ms.status = 'ACTIVE' \
Expand Down
3 changes: 3 additions & 0 deletions rust/crates/du-db/tests/ibd_suggestions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ async fn suggestions_scoped_by_owner_did() {
let mine = ibd::suggestions_for_did(&pool, "did:ex:owner", 50).await.unwrap();
assert_eq!(mine.len(), 1);
assert_eq!(mine[0].suggested_sample_guid, suggested);
// The row also names the caller's OWN sample — the Edge attests with it as `claimed_sample`,
// and it is the only way a self-publishing client learns its server-side sample guid.
assert_eq!(mine[0].target_sample_guid, target);
assert!(ibd::suggestions_for_did(&pool, "did:ex:counterpart", 50).await.unwrap().is_empty());

// Introduce authorization: true only for the owner's genuine candidate.
Expand Down
3 changes: 3 additions & 0 deletions rust/crates/du-web/src/routes/ibd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ async fn suggestions(State(st): State<AppState>, Query(q): Query<SuggestionsQuer
.await?
.into_iter()
.map(|s| json!({
// The caller's own sample the candidate was ranked against — it already owns this,
// and the Edge needs it to attest a completed comparison (`/ibd/attest`).
"target_sample_guid": s.target_sample_guid,
"suggested_sample_guid": s.suggested_sample_guid,
"suggestion_type": s.suggestion_type,
"score": s.score,
Expand Down
Loading