this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add purge command to delete all Opake data from PDS

Enumerates and deletes all records across all 7 app.opake.* collections.
Supports --dry-run for preview, --force to skip passphrase confirmation
and auto-remove local identity. [CL-196]

+200
+1
CHANGELOG.md
··· 12 12 - Remove bearer token authentication fallback from AppView [#26](https://issues.opake.app/issues/26.html)s 13 13 14 14 ### Added 15 + - Add purge command to delete all Opake data from PDS [#196](https://issues.opake.app/issues/196.html) 15 16 - Add metadata CLI command for rename, tag, and description management [#190](https://issues.opake.app/issues/190.html) 16 17 - Consolidate DNS and transport into opake-core, unify handle resolution [#185](https://issues.opake.app/issues/185.html) 17 18 - Build web login, callback, setup, and recover routes [#173](https://issues.opake.app/issues/173.html)
+9
README.md
··· 138 138 opake pair request # on the NEW device (polls for approval) 139 139 opake pair approve # on the EXISTING device (select + approve) 140 140 141 + # delete all Opake data from PDS (see what would go) 142 + opake purge --dry-run 143 + 144 + # delete everything (prompts for confirmation phrase) 145 + opake purge 146 + 147 + # skip confirmation and also remove local identity 148 + opake purge --force 149 + 141 150 # remove an account (defaults to only account if just one) 142 151 opake logout 143 152 opake logout bob.other.com
+1
crates/opake-cli/src/commands/mod.rs
··· 10 10 pub mod mkdir; 11 11 pub mod move_cmd; 12 12 pub mod pair; 13 + pub mod purge; 13 14 pub mod resolve; 14 15 pub mod revoke; 15 16 pub mod rm;
+186
crates/opake-cli/src/commands/purge.rs
··· 1 + use anyhow::{Context, Result}; 2 + use clap::Args; 3 + use opake_core::atproto; 4 + use opake_core::client::{Session, Transport, XrpcClient}; 5 + use opake_core::directories::DIRECTORY_COLLECTION; 6 + use opake_core::documents::DOCUMENT_COLLECTION; 7 + use opake_core::keyrings::KEYRING_COLLECTION; 8 + use opake_core::records::{ 9 + PAIR_REQUEST_COLLECTION, PAIR_RESPONSE_COLLECTION, PUBLIC_KEY_COLLECTION, 10 + }; 11 + use opake_core::sharing::GRANT_COLLECTION; 12 + 13 + use crate::commands::Execute; 14 + use crate::session::{self, CommandContext}; 15 + 16 + const CONFIRMATION_PHRASE: &str = "I want to delete all my Opake data"; 17 + 18 + /// All Opake collections in deletion order — dependents before parents. 19 + const COLLECTIONS: &[&str] = &[ 20 + GRANT_COLLECTION, 21 + PAIR_RESPONSE_COLLECTION, 22 + PAIR_REQUEST_COLLECTION, 23 + KEYRING_COLLECTION, 24 + DOCUMENT_COLLECTION, 25 + DIRECTORY_COLLECTION, 26 + PUBLIC_KEY_COLLECTION, 27 + ]; 28 + 29 + /// Delete all Opake data from the PDS 30 + #[derive(Args)] 31 + pub struct PurgeCommand { 32 + /// Show what would be deleted without deleting anything 33 + #[arg(long)] 34 + dry_run: bool, 35 + 36 + /// Skip confirmation prompt 37 + #[arg(long)] 38 + force: bool, 39 + } 40 + 41 + /// Paginate through a collection and collect all record keys. 42 + /// 43 + /// Unlike `list_collection`, this doesn't parse or version-check records — 44 + /// purge wants to delete everything regardless of schema version. 45 + async fn collect_rkeys( 46 + client: &mut XrpcClient<impl Transport>, 47 + collection: &str, 48 + ) -> Result<Vec<String>> { 49 + let mut rkeys = Vec::new(); 50 + let mut cursor: Option<String> = None; 51 + 52 + loop { 53 + let page = client 54 + .list_records(collection, Some(100), cursor.as_deref()) 55 + .await?; 56 + 57 + for record in &page.records { 58 + let at_uri = atproto::parse_at_uri(&record.uri)?; 59 + rkeys.push(at_uri.rkey); 60 + } 61 + 62 + match page.cursor { 63 + Some(c) => cursor = Some(c), 64 + None => break, 65 + } 66 + } 67 + 68 + Ok(rkeys) 69 + } 70 + 71 + /// Prompt the user to type the exact confirmation phrase. 72 + fn require_confirmation() -> Result<()> { 73 + println!(); 74 + println!("WARNING: This will permanently delete ALL Opake data from your PDS."); 75 + println!("All encrypted files, keys, grants, and directories will be gone."); 76 + println!("This action is irreversible."); 77 + println!(); 78 + println!("Type exactly: {CONFIRMATION_PHRASE}"); 79 + print!("> "); 80 + std::io::Write::flush(&mut std::io::stdout())?; 81 + 82 + let mut input = String::new(); 83 + std::io::stdin().read_line(&mut input)?; 84 + 85 + if input.trim() != CONFIRMATION_PHRASE { 86 + anyhow::bail!("Purge cancelled."); 87 + } 88 + 89 + Ok(()) 90 + } 91 + 92 + /// Ask whether to delete local identity and session files. 93 + fn confirm_local_cleanup(force: bool) -> Result<bool> { 94 + if force { 95 + return Ok(true); 96 + } 97 + 98 + eprint!("Delete local identity and session? [y/N] "); 99 + let mut answer = String::new(); 100 + std::io::stdin() 101 + .read_line(&mut answer) 102 + .context("failed to read confirmation")?; 103 + 104 + Ok(answer.trim().eq_ignore_ascii_case("y")) 105 + } 106 + 107 + impl Execute for PurgeCommand { 108 + async fn execute(self, ctx: &CommandContext) -> Result<Option<Session>> { 109 + let mut client = session::load_client(&ctx.storage, &ctx.did)?; 110 + 111 + // Enumerate all records across all collections. 112 + let mut inventory: Vec<(&str, Vec<String>)> = Vec::new(); 113 + let mut total = 0usize; 114 + 115 + for &collection in COLLECTIONS { 116 + let rkeys = collect_rkeys(&mut client, collection).await?; 117 + total += rkeys.len(); 118 + inventory.push((collection, rkeys)); 119 + } 120 + 121 + // Nothing to do. 122 + if total == 0 { 123 + println!("No Opake records found on PDS."); 124 + return Ok(session::refreshed_session(&client)); 125 + } 126 + 127 + // Print inventory (always, not just dry-run — useful context before confirmation). 128 + let max_name_len = inventory 129 + .iter() 130 + .map(|(name, _)| name.len()) 131 + .max() 132 + .unwrap_or(0); 133 + 134 + for &(collection, ref rkeys) in &inventory { 135 + let noun = if rkeys.len() == 1 { 136 + "record" 137 + } else { 138 + "records" 139 + }; 140 + println!( 141 + " {:<width$} {:>3} {noun}", 142 + collection, 143 + rkeys.len(), 144 + width = max_name_len, 145 + ); 146 + } 147 + 148 + println!(); 149 + 150 + if self.dry_run { 151 + let noun = if total == 1 { "record" } else { "records" }; 152 + println!("Total: {total} {noun} would be deleted."); 153 + return Ok(session::refreshed_session(&client)); 154 + } 155 + 156 + // Confirmation gate. 157 + if !self.force { 158 + require_confirmation()?; 159 + } 160 + 161 + // Delete everything. 162 + for (collection, rkeys) in &inventory { 163 + for rkey in rkeys { 164 + client.delete_record(collection, rkey).await?; 165 + } 166 + if !rkeys.is_empty() { 167 + println!("deleted {} {collection}", rkeys.len()); 168 + } 169 + } 170 + 171 + println!(); 172 + println!("Purged {total} records from PDS."); 173 + 174 + // Local cleanup. 175 + if confirm_local_cleanup(self.force)? { 176 + // Session will be invalid after remove_account, so grab any refresh first. 177 + let refreshed = session::refreshed_session(&client); 178 + ctx.storage.remove_account(&ctx.did)?; 179 + println!("Removed local identity and session for {}.", ctx.did); 180 + // Don't persist session — the account dir is gone. 181 + return Ok(refreshed.and(None)); 182 + } 183 + 184 + Ok(session::refreshed_session(&client)) 185 + } 186 + }
+3
crates/opake-cli/src/main.rs
··· 53 53 Revoke(commands::revoke::RevokeCommand), 54 54 Keyring(commands::keyring::KeyringCommand), 55 55 Pair(commands::pair::PairCommand), 56 + /// Delete all Opake data from the PDS 57 + Purge(commands::purge::PurgeCommand), 56 58 Tree(commands::tree::TreeCommand), 57 59 } 58 60 ··· 121 123 Command::Revoke(cmd) => run_with_context(&storage, as_flag.as_deref(), cmd).await?, 122 124 Command::Keyring(cmd) => run_with_context(&storage, as_flag.as_deref(), cmd).await?, 123 125 Command::Pair(cmd) => run_with_context(&storage, as_flag.as_deref(), cmd).await?, 126 + Command::Purge(cmd) => run_with_context(&storage, as_flag.as_deref(), cmd).await?, 124 127 Command::Tree(cmd) => run_with_context(&storage, as_flag.as_deref(), cmd).await?, 125 128 } 126 129