Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
a84a32f
Add owner deletion admission control plane
TheSentinel454 Sep 22, 2026
acf5612
Fence archived community owner rotation
TheSentinel454 Sep 23, 2026
4432b57
Allow privileged abort at the reversible deletion boundary
TheSentinel454 Sep 25, 2026
3edd594
Reject owner deletion rows that omit provenance
TheSentinel454 Sep 25, 2026
2abeed3
Reach each owner-delete rejection guard separately
TheSentinel454 Sep 25, 2026
6d82d2b
Add typed deletion drain operator CronJob
TheSentinel454 Sep 25, 2026
1cef1ed
Cover operator-bound signing and replay on owner delete
TheSentinel454 Sep 25, 2026
99c26f9
Automate owner deletion preparation
TheSentinel454 Sep 25, 2026
d9c153f
Reject reserved deletion job pod labels
TheSentinel454 Sep 25, 2026
8ebe3b8
Record that owner deletion consent is an operator assertion
TheSentinel454 Sep 25, 2026
98f4e91
Harden owner deletion preparation coverage
TheSentinel454 Sep 25, 2026
494d574
Correct the deletion drain runbook's operational claims
TheSentinel454 Sep 25, 2026
e5b3d46
Serialize owner convergence before deletion abort
TheSentinel454 Sep 25, 2026
b02926e
Test blocked owner deletion preparation guards
Sep 28, 2026
e6714ec
Cover deletion drain chart contracts
TheSentinel454 Sep 28, 2026
1037263
Bound operator CronJob names
TheSentinel454 Sep 28, 2026
7e3e142
Clarify deletion job workload identity
TheSentinel454 Sep 28, 2026
dfd5db4
Scope video menu probes to emitted messages
TheSentinel454 Sep 28, 2026
fcd831e
Renumber owner deletion admission migration
codex Sep 28, 2026
c237fd4
Merge main and reconcile owner deletion migration assertions
Sep 28, 2026
1f1b5f4
docs: clarify community deletion abort boundary
Sep 28, 2026
8956cf8
Merge #7818 dependency refresh into #7827
codex Sep 28, 2026
b689026
Merge refreshed #7827 dependency into #7830
codex Sep 28, 2026
9f6f720
Merge main readiness changes and retain test credential annotation
Sep 28, 2026
464a4e6
Merge refreshed admission base into operator cron
Sep 28, 2026
53c78cd
Merge refreshed operator cron base into owner preparation
Sep 28, 2026
dc0a125
test(deletion): synchronize lease-loss cancellation
codex Sep 28, 2026
e56f4d4
Add typed deletion drain operator CronJob
TheSentinel454 Sep 25, 2026
0731205
Reject reserved deletion job pod labels
TheSentinel454 Sep 25, 2026
5c3af3b
Correct the deletion drain runbook's operational claims
TheSentinel454 Sep 25, 2026
d791f86
Cover deletion drain chart contracts
TheSentinel454 Sep 28, 2026
c0a392a
Bound operator CronJob names
TheSentinel454 Sep 28, 2026
cdef026
Clarify deletion job workload identity
TheSentinel454 Sep 28, 2026
38db86b
Scope video menu probes to emitted messages
TheSentinel454 Sep 28, 2026
9c175de
Merge rebased operator CronJob base without changing owner preparation
Sep 28, 2026
131ef21
test(desktop): await channel head before scroll summary check
codex Sep 28, 2026
c18281b
Merge commit '131ef21a253367db5e092fd6a392398e4b6eae93' into elrond/p…
Sep 28, 2026
5650ae7
test(desktop): record transient snapshot before fallback
codex Sep 28, 2026
d8c920c
Merge sidebar snapshot test repair from operator cron base
Sep 28, 2026
7a26420
Merge main and retain upstream scroll readiness fix
Sep 28, 2026
3c8def0
Merge main-refreshed operator cron base
Sep 28, 2026
e024135
feat: add owner deletion receipt and quota reservation
codex Sep 28, 2026
0720b0a
feat(desktop): add hosted community deletion flow
codex Sep 28, 2026
3a7f9ab
fix: route deletion receipts through writer acquisition
codex Sep 28, 2026
0f733da
fix(desktop): retain ambiguous deletion recovery
codex Sep 28, 2026
a1c9794
fix: route deletion receipts through writer acquisition
codex Sep 28, 2026
b50d7f8
docs: describe authoritative owner quota projection
codex Sep 28, 2026
0a4717d
test: advance migration lock expectation to 0053
codex Sep 28, 2026
340ca56
test: cover aborted owner quota reservation
codex Sep 28, 2026
546b686
test: abort quota fixture through store contract
codex Sep 28, 2026
d385c1f
docs: describe authoritative owner quota projection
codex Sep 28, 2026
2b35ca0
test: advance migration lock expectation to 0053
codex Sep 28, 2026
96692c4
test: cover aborted owner quota reservation
codex Sep 28, 2026
9485cdf
test: abort quota fixture through store contract
codex Sep 28, 2026
f79fa81
Merge main into owner deletion auto-prepare
codex Sep 28, 2026
c5edf47
Merge current main into owner deletion foundation
codex Sep 29, 2026
99fa5ae
chore: reserve migration 0054 for deletion quota
codex Sep 29, 2026
3b94325
Merge owner deletion foundation and current main
codex Sep 29, 2026
bd95c4a
fix(relay): return stable deletion lifecycle conflicts
codex Sep 29, 2026
a77fc33
Merge reconciled Relay into Desktop deletion flow
codex Sep 29, 2026
b925d6a
fix community deletion recovery fencing
codex Sep 29, 2026
0d7798c
fix deletion admission and quota lifecycle proof
codex Sep 29, 2026
4ba7f3f
merge relay deletion review corrections
codex Sep 29, 2026
225a11c
Merge merged owner preparation foundation into relay deletion draft
codex Sep 29, 2026
2cc6c4e
Merge reconciled relay into published Desktop deletion draft
codex Sep 29, 2026
529064b
Fence hosted deletion recovery by request and account generation
codex Sep 29, 2026
37e9b68
fix(db): restore owner deletion approval authority fence
codex Sep 29, 2026
944142f
Merge owner preparation authority repair into Desktop deletion draft
codex Sep 29, 2026
5071879
Merge current main into owner deletion relay draft
codex Sep 29, 2026
7400268
refactor(relay): make owner delete the idempotent recovery call; add …
Sep 29, 2026
c35c74f
fix(db): make the lifetime owner cap absolute and rustfmt the quota test
Sep 29, 2026
63623fe
docs(relay): state limit_reached for both owner caps and pin the can_…
Sep 29, 2026
59375c0
test(relay): pin that a changed acknowledgement version replay is uns…
Sep 29, 2026
bb59f0e
docs(relay): note ack-version dependency of resend recovery
codex Sep 29, 2026
e21151f
docs(relay): ack version is a code constant that also gates execution
codex Sep 29, 2026
5f5d6a1
Merge updated relay deletion dependency into desktop draft
codex Sep 29, 2026
4a90914
Replace desktop deletion receipts with explicit same-request checks
codex Sep 29, 2026
0736b70
Hide quota-blocked hosted Create and simplify native command checks
codex Sep 29, 2026
2224c12
Retain deletion recovery when native error status is missing
codex Sep 29, 2026
c3b4891
Restore same-request deletion recovery without owner-list gate
codex Sep 29, 2026
467e34a
Merge origin/main into desktop deletion draft
codex Sep 30, 2026
047f7c2
fix(desktop): retain hosted deletion across owner switches
codex Sep 30, 2026
75c7612
fix(desktop): guard hosted deletion across identities
codex Sep 30, 2026
20e7015
fix(desktop): reconcile accepted deletion on refresh
codex Sep 30, 2026
e5f56aa
refactor(desktop): keep deletion refresh within file-size policy
codex Sep 30, 2026
e2d23ca
fix(desktop): retain confirmed aborts across refresh failures
codex Sep 30, 2026
c8bf273
fix(desktop): preserve account refresh across identity mismatch
codex Sep 30, 2026
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
203 changes: 182 additions & 21 deletions desktop/src-tauri/src/builderlab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use axum::{
routing::get,
Router,
};
use futures_util::StreamExt;
use serde::{Deserialize, Serialize};
use tauri_plugin_opener::OpenerExt;
use tokio::{net::TcpListener, sync::oneshot};
Expand All @@ -20,6 +21,7 @@ const BB_SESSION_CREDENTIAL_HEADER: &str = "X-BB-Session-Credential";
// or challenge/verify fail with `invalid_origin`. It also seeds the challenge
// body's `origin` field so both agree.
const BUILDERLAB_ORIGIN: &str = "https://app.builderlab.xyz";
const BUILDERLAB_JSON_RESPONSE_LIMIT: usize = 64 * 1024;
const AUTH_COMPLETE_HTML: &str = r#"<!doctype html>
<html lang="en">
<head>
Expand Down Expand Up @@ -166,13 +168,25 @@ pub(crate) struct BuilderlabAuthInfo {
expires_at: String,
email: Option<String>,
name: Option<String>,
can_delete_buzz_communities: bool,
}

#[derive(Debug, Deserialize)]
struct AuthMeResponse {
email: Option<String>,
name: Option<String>,
expires_at: String,
#[serde(default)]
capabilities: serde_json::Value,
}

impl AuthMeResponse {
fn can_delete_buzz_communities(&self) -> bool {
self.capabilities
.get("can_delete_buzz_communities")
.and_then(serde_json::Value::as_bool)
== Some(true)
}
}

struct CallbackState {
Expand Down Expand Up @@ -343,10 +357,12 @@ pub(crate) async fn start_builderlab_login(
if exchanged.expires_at != me.expires_at {
return Err("Builderlab session expiry did not match code exchange".to_owned());
}
let can_delete_buzz_communities = me.can_delete_buzz_communities();
let info = BuilderlabAuthInfo {
expires_at: me.expires_at.clone(),
email: me.email,
name: me.name,
can_delete_buzz_communities,
};
{
let mut pending = login.0.lock().map_err(|error| error.to_string())?;
Expand Down Expand Up @@ -379,11 +395,15 @@ pub(crate) async fn get_builderlab_auth(
return Ok(None);
};
match authenticated_user(&app_state.http_client, &credential).await {
Ok(me) => Ok(Some(BuilderlabAuthInfo {
expires_at: me.expires_at,
email: me.email,
name: me.name,
})),
Ok(me) => {
let can_delete_buzz_communities = me.can_delete_buzz_communities();
Ok(Some(BuilderlabAuthInfo {
expires_at: me.expires_at,
email: me.email,
name: me.name,
can_delete_buzz_communities,
}))
}
Err(error) => {
*session
.0
Expand Down Expand Up @@ -421,13 +441,34 @@ struct NostrIdentityChallenge {
expires_at: String,
}

async fn authenticated_json(
#[derive(Debug, Serialize)]
pub(crate) struct AuthenticatedJsonResponse {
http_status: u16,
body: serde_json::Value,
}

fn authenticated_json_response_from_parts(
status: reqwest::StatusCode,
bytes: &[u8],
) -> Result<AuthenticatedJsonResponse, String> {
let body: serde_json::Value = serde_json::from_slice(bytes)
.map_err(|error| format!("invalid Builderlab response: {error}"))?;
if !status.is_success() && body.get("error").is_none() {
return Err(format!("Builderlab request failed (HTTP {status})."));
}
Ok(AuthenticatedJsonResponse {
http_status: status.as_u16(),
body,
})
}

async fn authenticated_json_with_status(
client: &reqwest::Client,
session: &BuilderlabSession,
method: reqwest::Method,
path: &str,
body: serde_json::Value,
) -> Result<serde_json::Value, String> {
) -> Result<AuthenticatedJsonResponse, String> {
let credential = session
.0
.lock()
Expand All @@ -445,22 +486,34 @@ async fn authenticated_json(
.await
.map_err(|error| format!("Builderlab request failed: {error}"))?;
let status = response.status();
let value: serde_json::Value = response
.json()
.await
.map_err(|error| format!("invalid Builderlab response: {error}"))?;
if !status.is_success() {
// Builderlab error responses carry a structured `{ error: { code,
// message, setup_needed, ... } }` body. Pass those through as `Ok` so the
// frontend's typed handling and friendly per-code messages apply, instead
// of surfacing a raw JSON blob. Only fall back to a plain string when the
// body isn't the expected shape.
if value.get("error").is_some() {
return Ok(value);
let mut bytes = Vec::new();
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|error| format!("Builderlab response failed: {error}"))?;
if bytes.len().saturating_add(chunk.len()) > BUILDERLAB_JSON_RESPONSE_LIMIT {
return Err("Builderlab response exceeded the size limit".to_owned());
}
return Err(format!("Builderlab request failed (HTTP {status})."));
bytes.extend_from_slice(&chunk);
}
Ok(value)
// Builderlab error responses carry a structured `{ error: { code,
// message, setup_needed, ... } }` body. Preserve those as a typed result so
// the deletion classifier can bind the body to reqwest's actual status.
// Other callers continue to receive only the body through authenticated_json.
authenticated_json_response_from_parts(status, &bytes)
}

async fn authenticated_json(
client: &reqwest::Client,
session: &BuilderlabSession,
method: reqwest::Method,
path: &str,
body: serde_json::Value,
) -> Result<serde_json::Value, String> {
Ok(
authenticated_json_with_status(client, session, method, path, body)
.await?
.body,
)
}

#[tauri::command]
Expand Down Expand Up @@ -640,6 +693,39 @@ pub(crate) async fn transfer_builderlab_community(
.await
}

fn community_deletion_body(
community_id: String,
host: String,
request_id: String,
acknowledgement_version: i32,
) -> serde_json::Value {
serde_json::json!({
"community_id": community_id,
"host": host,
"request_id": request_id,
"acknowledgement_version": acknowledgement_version,
})
}

#[tauri::command]
pub(crate) async fn delete_builderlab_community(
community_id: String,
host: String,
request_id: String,
acknowledgement_version: i32,
app_state: tauri::State<'_, crate::app_state::AppState>,
session: tauri::State<'_, BuilderlabSession>,
) -> Result<AuthenticatedJsonResponse, String> {
authenticated_json_with_status(
&app_state.http_client,
&session,
reqwest::Method::POST,
"/v1/buzz/communities/delete",
community_deletion_body(community_id, host, request_id, acknowledgement_version),
)
.await
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -684,4 +770,79 @@ mod tests {
);
assert!(!query.contains_key("screen_hint"));
}

#[test]
fn deletion_capability_requires_literal_true() {
for (capabilities, expected) in [
(serde_json::json!({}), false),
(
serde_json::json!({ "can_delete_buzz_communities": "true" }),
false,
),
(
serde_json::json!({ "can_delete_buzz_communities": false }),
false,
),
(
serde_json::json!({ "can_delete_buzz_communities": true }),
true,
),
] {
let response: AuthMeResponse = serde_json::from_value(serde_json::json!({
"expires_at": "2099-01-01T00:00:00Z",
"capabilities": capabilities,
}))
.expect("auth-me fixture");
assert_eq!(response.can_delete_buzz_communities(), expected);
}
}

#[test]
fn community_deletion_commands_share_the_exact_public_tuple() {
let body = community_deletion_body(
"2f6c6a10-6513-45a6-9605-4694333d8feb".to_owned(),
"Exact-Host.communities.buzz.xyz".to_owned(),
"2e1b354d-6f7c-44e8-8928-cf743c77bbbc".to_owned(),
1,
);
assert_eq!(
body,
serde_json::json!({
"community_id": "2f6c6a10-6513-45a6-9605-4694333d8feb",
"host": "Exact-Host.communities.buzz.xyz",
"request_id": "2e1b354d-6f7c-44e8-8928-cf743c77bbbc",
"acknowledgement_version": 1,
})
);
let path = "/v1/buzz/communities/delete";
let url = api_url(path).expect("deletion URL");
assert_eq!(url.origin().ascii_serialization(), BUILDERLAB_ORIGIN);
assert_eq!(url.path(), format!("/api/goose{path}"));
}

#[test]
fn deletion_transport_uses_the_native_status_not_a_body_claim() {
let response = authenticated_json_response_from_parts(
reqwest::StatusCode::SERVICE_UNAVAILABLE,
br#"{"http_status":202,"error":{"code":"relay_unavailable"}}"#,
)
.expect("structured response");

assert_eq!(response.http_status, 503);
assert_eq!(response.body["http_status"], 202);
}

#[test]
fn community_deletion_commands_are_registered_on_the_native_boundary() {
let lib = include_str!("lib.rs");
let command = "delete_builderlab_community,";
assert_eq!(
lib.matches(command).count(),
1,
"{command} must be registered exactly once"
);
assert!(!lib.contains("get_builderlab_community_deletion_receipt"));
let source = include_str!("builderlab.rs");
assert!(source.contains(".header(reqwest::header::ORIGIN, BUILDERLAB_ORIGIN)"));
}
}
1 change: 1 addition & 0 deletions desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ pub fn run() {
archive_builderlab_community,
unarchive_builderlab_community,
transfer_builderlab_community,
delete_builderlab_community,
title_bar_double_click,
get_identity,
get_nsec,
Expand Down
Loading
Loading