Phase 4 (0.6.3-beta): posting-key / network-key split (plumbing)

Decouple the signing identity from the network identity. This phase
ships the plumbing only — every device still has exactly one posting
identity, copied from the network key on first 0.6.3 launch so all
existing signed content keeps verifying. Phase 5 builds the
multi-persona UX on top.

Types:
- New PostingIdentity struct: { node_id, secret_seed, display_name,
  created_at }

Storage:
- New posting_identities(node_id, secret_seed, display_name,
  created_at) table
- Methods: upsert / get / list / delete posting identities;
  get/set default posting id (stored in settings)
- seed_posting_identity_from_network: idempotent migration inserts
  the network key as the single posting identity and sets it default
  on first 0.6.3 launch

Node:
- default_posting_id + default_posting_secret fields populated on
  startup via the migration
- All content signing / encryption / key wrapping now uses
  default_posting_secret; the old Node.secret_seed field is gone
  (iroh holds the network secret internally)
- author field on all locally-created content is now
  default_posting_id (equal to node_id for upgraders until Phase 5
  introduces separate personas)
- Auto-follow-self covers both network_id and default_posting_id
  (same in 0.6.3, may diverge in 0.6.4+)

Export/import:
- Bundle now includes posting_identities.json in
  IdentityOnly / PostsWithIdentity / Everything scopes
- restore_posting_identities(zip, storage) reads and upserts on
  import

Smoke-tested:
- Fresh 0.6.3 install: posting_identities seeded from network key;
  default set; new post's author = default_posting_id = network_id
- Two-node pull sync: B pulls A's post, signature verifies across
  the wire
- 111 core tests pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-04-21 22:38:12 -04:00
parent 975e7b9bfe
commit ce4b989b17
5 changed files with 287 additions and 50 deletions

View file

@ -15,7 +15,40 @@ use crate::blob::BlobStore;
use crate::content::compute_post_id;
use crate::export::{ExportManifest, ExportedPost};
use crate::storage::StoragePool;
use crate::types::{Attachment, NodeId, Post, PostVisibility};
use crate::types::{Attachment, NodeId, Post, PostVisibility, PostingIdentity};
/// Extract posting_identities.json from an export ZIP and upsert each entry
/// into storage. Called during import so multi-persona users restore all
/// their posting keys. Idempotent — INSERT OR IGNORE on conflict. No-op if
/// the file is missing (pre-0.6.3 bundle).
pub async fn restore_posting_identities(
zip_path: &Path,
storage: &StoragePool,
) -> anyhow::Result<usize> {
let zip_path = zip_path.to_path_buf();
let identities: Vec<PostingIdentity> = tokio::task::spawn_blocking(move || -> anyhow::Result<Vec<PostingIdentity>> {
let file = std::fs::File::open(&zip_path)?;
let mut archive = zip::ZipArchive::new(file)?;
let buf = {
let mut entry = match archive.by_name("itsgoin-export/posting_identities.json") {
Ok(e) => e,
Err(_) => return Ok(Vec::new()),
};
let mut s = String::new();
entry.read_to_string(&mut s)?;
s
};
Ok(serde_json::from_str(&buf).unwrap_or_default())
}).await??;
let s = storage.get().await;
let mut restored = 0usize;
for id in &identities {
s.upsert_posting_identity(id)?;
restored += 1;
}
Ok(restored)
}
/// What to do with the imported data.
#[derive(Debug, Clone, Serialize, Deserialize)]