v0.3.3: Rate limiting, IPv6 fix, schema versioning, video preload, engagement propagation

Security & stability:
- Incoming auth-fail rate limiting per source IP (3 attempts, then exponential backoff)
- Schema versioning via PRAGMA user_version with migration framework

Networking:
- IPv6 http_addr fix: advertise actual public IPv6 instead of 0.0.0.0
- N2/N3 TTL reduced from 7 days to 5 hours
- Full N1/N2 state re-broadcast every 4 hours
- Bootstrap isolation recovery: 24h check with sticky N1 advertising
- Bidirectional engagement propagation (upstream + downstream)
- Auto downstream registration on pull sync and push notification
- post_upstream table for CDN tree traversal

Media & UI:
- Video preload="auto" for share links and in-app blob URLs
- Following: Online/Offline split with last-seen timestamps
- DMs filtered from My Posts tab
- Image lightbox, audio player, file attachments with download prompt
- Share link unroutable address filtering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-03-16 18:37:24 -04:00
parent e6f55fb1d6
commit 8fad30cf95
8 changed files with 136 additions and 17 deletions

View file

@ -28,16 +28,72 @@ pub struct Storage {
conn: Connection,
}
/// Current schema version. Bump this when making schema or data changes
/// that require migration. Old databases with a lower version will be migrated.
/// If the gap is too large (major version mismatch), the DB is reset instead.
const SCHEMA_VERSION: u32 = 2;
/// Minimum schema version we can migrate from. Anything older gets a full reset.
const MIN_MIGRATABLE_VERSION: u32 = 1;
impl Storage {
pub fn open(path: impl AsRef<Path>) -> anyhow::Result<Self> {
let conn = Connection::open(path)?;
let conn = Connection::open(path.as_ref())?;
conn.execute_batch("PRAGMA journal_mode=WAL;")?;
// Check schema version
let db_version: u32 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
if db_version > 0 && db_version < MIN_MIGRATABLE_VERSION {
// Too old to migrate — reset the database
tracing::warn!(
db_version, current = SCHEMA_VERSION,
"Database schema too old to migrate, resetting"
);
drop(conn);
std::fs::remove_file(path.as_ref())?;
let conn = Connection::open(path.as_ref())?;
conn.execute_batch("PRAGMA journal_mode=WAL;")?;
let storage = Self { conn };
storage.init_tables()?;
storage.set_schema_version(SCHEMA_VERSION)?;
return Ok(storage);
}
let storage = Self { conn };
storage.init_tables()?;
storage.migrate()?;
if db_version < SCHEMA_VERSION {
// Run version-specific data migrations
storage.migrate_data(db_version)?;
storage.set_schema_version(SCHEMA_VERSION)?;
tracing::info!(from = db_version, to = SCHEMA_VERSION, "Database schema upgraded");
}
Ok(storage)
}
fn set_schema_version(&self, version: u32) -> anyhow::Result<()> {
self.conn.pragma_update(None, "user_version", version)?;
Ok(())
}
/// Data migrations that run once when upgrading between schema versions.
fn migrate_data(&self, from_version: u32) -> anyhow::Result<()> {
if from_version < 2 {
// v1 → v2: Clear stale N2/N3 entries and mesh_peers that may prevent
// bootstrap reconnection. The node will rediscover peers on next startup.
tracing::info!("Schema v2: clearing stale network state for fresh discovery");
let _ = self.conn.execute_batch(
"DELETE FROM reachable_n2;
DELETE FROM reachable_n3;
DELETE FROM mesh_peers;"
);
}
Ok(())
}
fn init_tables(&self) -> anyhow::Result<()> {
self.conn.execute_batch(
"CREATE TABLE IF NOT EXISTS posts (