Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ef054a7
feat(storage): add transactional vault schema migrations
Dzejkop Oct 27, 2026
b27028e
feat(storage): store PCPs in a dedicated vault table
Dzejkop Oct 27, 2026
4e20143
fix(storage): commit PCP upserts explicitly
Dzejkop Oct 27, 2026
5074555
fix(storage): reject PCP paths containing NUL
Dzejkop Oct 27, 2026
23773a2
perf(storage): store PCP data after its metadata
Dzejkop Oct 27, 2026
58881af
test(storage): format the v1 schema fixture as plain SQL
Dzejkop Oct 27, 2026
6009949
Merge branch 'dzejkop/vault-schema-migrations' into dzejkop/pcp-storage
Dzejkop Oct 27, 2026
a00e43f
refactor(storage): make PCP ids and kinds opaque to WalletKit
Dzejkop Oct 27, 2026
3b243b0
docs: clarify PCP API availability and storage lifecycle
Dzejkop Oct 27, 2026
ef5cd35
fix(storage): align vault schema checks with cache migrations
Dzejkop Oct 28, 2026
dd91fb2
Merge branch 'dzejkop/vault-schema-migrations' into dzejkop/pcp-storage
Dzejkop Oct 28, 2026
4ba4918
fix(storage): check PCP table in current vault schemas
Dzejkop Oct 28, 2026
cf8773e
refactor(storage): version vault schema in singleton metadata
Dzejkop Oct 28, 2026
dc1a3cd
refactor(storage): inherit singleton vault metadata versioning
Dzejkop Oct 28, 2026
f37fcbc
style(storage): expect the migration count cast lint
Dzejkop Oct 28, 2026
629fae5
Merge branch 'dzejkop/vault-schema-migrations' into dzejkop/pcp-storage
Dzejkop Oct 28, 2026
9728504
docs(storage): clarify vault schema initialization
Dzejkop Oct 28, 2026
c01e533
docs(storage): inherit clearer schema initialization docs
Dzejkop Oct 28, 2026
9ba89f5
refactor(storage): separate vault initialization from version reads
Dzejkop Oct 28, 2026
98082a6
refactor(storage): inherit explicit vault schema initialization
Dzejkop Oct 28, 2026
f927c14
docs(storage): explain why PCPs are excluded from credential backups
Dzejkop Oct 28, 2026
724b86f
refactor(storage): drop PCP checksums
Dzejkop Oct 28, 2026
4e32d2e
refactor(storage): expose PCPs through PcpVault
Dzejkop Oct 28, 2026
2a099ed
refactor(storage): move PcpVault to its own module, namespace PCP SQL
Dzejkop Oct 28, 2026
85efc9f
Merge remote-tracking branch 'origin/dzejkop/vault-version' into dzej…
Dzejkop Oct 29, 2026
739eda4
Merge remote-tracking branch 'origin/dzejkop/vault-version' into dzej…
Dzejkop Oct 29, 2026
e1f1834
test(storage): name the v2 to v3 upgrade test
Dzejkop Oct 29, 2026
51c8bce
Merge remote-tracking branch 'origin/dzejkop/vault-version' into dzej…
Dzejkop Oct 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
72 changes: 69 additions & 3 deletions crates/walletkit-core/src/storage/credential_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ impl CredentialStore {
Ok(added)
}

/// Permanently destroys all credential storage data.
/// Permanently destroys all credential storage data, including sealed PCPs.
///
/// Intended for use when the user logs out or deletes their account.
///
Expand Down Expand Up @@ -486,8 +486,9 @@ impl CredentialStore {
inner.import_vault_from_file(&path)
}

/// Registers a listener that is called after every successful vault
/// mutation (store, delete, purge).
/// Registers a listener for successful credential mutations (store, delete,
/// merge). PCP changes do not affect credential backups or notify this
/// listener.
///
/// Only one listener can be active at a time — calling this replaces any
/// previously registered listener. The previous delivery thread shuts down
Expand Down Expand Up @@ -659,6 +660,14 @@ impl CredentialStore {
.map_err(|_| StorageError::Lock("storage mutex poisoned".to_string()))
}

/// Runs `f` on the initialized vault while holding the storage mutex.
pub(super) fn with_vault<T>(
&self,
f: impl FnOnce(&CredentialVault) -> StorageResult<T>,
) -> StorageResult<T> {
f(&self.lock_inner()?.state()?.vault)
}

/// Retrieves a full credential including raw bytes by issuer schema ID.
///
/// Returns the most recent non-expired credential matching the issuer schema ID.
Expand Down Expand Up @@ -1159,6 +1168,63 @@ mod tests {

use std::sync::atomic::{AtomicU32, Ordering};

#[test]
#[cfg(not(target_arch = "wasm32"))]
fn pcp_vault_requires_initialization_and_never_notifies_credential_backup_listener()
{
let dir = tempfile::tempdir().unwrap();
let store = Arc::new(
CredentialStore::from_provider(&InMemoryStorageProvider::new(dir.path()))
.unwrap(),
);
let pcps = Arc::clone(&store).pcp_vault();
assert!(matches!(
pcps.put("package", "orb_pkg", b"sealed", 100),
Err(StorageError::NotInitialized)
));
assert!(matches!(
pcps.get("package"),
Err(StorageError::NotInitialized)
));
assert!(matches!(pcps.list(None), Err(StorageError::NotInitialized)));
assert!(matches!(
pcps.delete("package"),
Err(StorageError::NotInitialized)
));
store.init(42, 100).unwrap();
let (tx, rx) = mpsc::sync_channel(1);
*store.vault_changed_tx.lock().unwrap() = Some(tx);
let before = store.export_vault_for_backup().unwrap();
pcps.put("package", "orb_pkg", b"sealed", 100).unwrap();
assert!(pcps.get("missing").unwrap().is_none());
assert_eq!(pcps.get("package").unwrap().unwrap().data, b"sealed");
assert_eq!(pcps.list(Some("orb_pkg".to_owned())).unwrap().len(), 1);
assert!(matches!(
pcps.put("package", "face_pkg", b"wrong", 100),
Err(StorageError::PcpKindMismatch)
));
assert_eq!(store.export_vault_for_backup().unwrap(), before);
store.import_vault_from_backup(&before).unwrap();
assert_eq!(store.merge_vault_from_backup(&before).unwrap(), 0);
assert_eq!(pcps.get("package").unwrap().unwrap().data, b"sealed");
assert!(pcps.delete("package").unwrap());
assert!(!pcps.delete("package").unwrap());
assert!(matches!(rx.try_recv(), Err(mpsc::TryRecvError::Empty)));
let credential: Credential = world_id_core::Credential::new()
.issuer_schema_id(10)
.genesis_issued_at(100)
.into();
store
.store_credential(&credential, &FieldElement::from(7_u64), 1000, None, 100)
.unwrap();
rx.try_recv().unwrap();
store.destroy_storage().unwrap();
assert!(matches!(
pcps.get("package"),
Err(StorageError::NotInitialized)
));
}

fn supplied_keys(byte: u8) -> Arc<StorageKeys> {
Arc::new(StorageKeys::from_bytes(vec![byte; 32]).expect("32-byte key"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! [`walletkit_db`].

mod merge;
mod pcp;
mod schema;
#[cfg(test)]
mod tests;
Expand Down
Loading
Loading