diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c786f79..f5e889b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -72,7 +72,7 @@ Cargo workspace with 12 crates: ## CI / Infrastructure Dependency -**Branch protection for this repo is managed via Terraform in `kafkade/github-infra` (`repo_toku.tf`).** The `required_status_checks` list must match the job names in `.github/workflows/ci.yml`. If you rename, add, or remove CI jobs that are used as merge gates (currently `Validate`), the corresponding IaC config must be updated or PRs will be permanently blocked. Always flag this when proposing workflow changes. +**Branch protection for this repo is managed via Terraform in `kafkade/github-infra` (`repo_toku.tf`).** The `required_status_checks` list must match the job names in `.github/workflows/validate.yml`. If you rename, add, or remove CI jobs that are used as merge gates (currently `Validate`), the corresponding IaC config must be updated or PRs will be permanently blocked. Always flag this when proposing workflow changes. ## Releasing diff --git a/CHANGELOG.md b/CHANGELOG.md index 166b31a..9733c33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Sync error messages now point at the right command. When sync isn't set up, when your + device has no auth token, after `toku sync disable`, or when a library key hasn't been + unlocked, Toku now tells you to run `toku sync signup`, `toku sync login`, or + `toku sync enroll` — whichever actually fits your situation — instead of the deprecated + `toku sync init`. The web dashboard's sync page says the same thing. `toku sync init` + still works and still prints its deprecation notice; only the guidance text changed (#221) + - Sync now propagates ordinary edits incrementally. Every create/update/delete on a syncable entity (books, reading sessions, progress, and tags) now stages a sync op atomically with the write, in a single choke-point in the persistence layer, so all diff --git a/ROADMAP.md b/ROADMAP.md index 7153625..9178d7a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -453,7 +453,7 @@ Rejected alternatives: | JSON (full) | Backup, programmatic access | **MVP** | 🟢 | | CSV | Spreadsheet users | **MVP** | 🟢 | | Markdown | Blog posts, reading lists | Phase 2 | 🟢 | -| ZIP backup (canonical) | Full backup/migration | ✅ Shipped (#200) | 🟡 | +| ZIP backup (canonical) | Full backup/migration | Phase 2 ✅ (#200) | 🟡 | | BibTeX | Academic citations | Phase 3 | 🟡 | | OPDS | E-reader catalog | Phase 6 | 🟢 | | HTML | Static reading list site | Phase 3 | 🟡 | @@ -767,13 +767,18 @@ File management lives in a new `toku-files` crate that depends on `toku-core` an - Apple Books: annotations in `~/Library/Containers/com.apple.iBooksX/` `[Validation Required]`. - Phase: Post-1.0 research. High value but each reader is a separate integration effort. -### 7.5 — Self-Hosted Sync Server 🔴 - -- Deferred to Phase 7. See ADR-010 for the current architecture decision. -- Immich-style self-hostable Docker image with first-run admin onboarding, real user - accounts, and 1Password-style two-secret SRP authentication. -- Mandatory zero-knowledge E2E encryption — no plaintext mode for hosted sync. -- Axum server with REST API (op-log wire protocol from ADR-008 retained). +### 7.5 — Self-Hosted Sync Server 🟡 + +- **Shipped (Phase 7)**: the `toku-sync` Axum server with a REST API (op-log wire protocol from + ADR-008 retained), mandatory zero-knowledge E2E encryption — no plaintext mode for hosted sync — + and 1Password-style two-secret SRP authentication (ADR-010). Immich-style self-hostable Docker + image (`ghcr.io/kafkade/toku-sync`) with first-run admin onboarding and real user accounts, + op-log emission on every mutation (#194), and new-device bootstrap (#199). +- Optional managed-tier controls for operators running a shared instance — per-user quotas, + per-user rate limiting, admin-scoped encrypted backup, and signup email verification — are off + by default and preserve the zero-knowledge guarantee (#206). +- **Remaining (why still 🟡)**: genuine multi-device client integration — the iOS, macOS, web, and + Windows apps actually syncing between each other end-to-end (the CLI already does). - cr-sqlite kept as a research alternative if it matures for iOS/WASM. --- diff --git a/crates/toku-cli/src/main.rs b/crates/toku-cli/src/main.rs index 0e2d664..7ab7dbf 100644 --- a/crates/toku-cli/src/main.rs +++ b/crates/toku-cli/src/main.rs @@ -4579,16 +4579,18 @@ fn cmd_sync(data_dir: &Path, action: SyncAction, output_format: &OutputFormat) - /// Helper: get sync config or error with a helpful message. fn require_sync(config: &toku_core::TokuConfig) -> Result<&toku_core::SyncConfig> { - config - .sync - .as_ref() - .ok_or_else(|| anyhow::anyhow!("sync is not configured. Run `toku sync init` first.")) + config.sync.as_ref().ok_or_else(|| { + anyhow::anyhow!( + "sync is not configured. Run `toku sync signup` (new account) or \ + `toku sync enroll` (existing account) first." + ) + }) } /// Helper: get auth token for the configured server. fn require_token(token_store: &sync::token_store::TokenStore, server: &str) -> Result { token_store.load(server)?.ok_or_else(|| { - anyhow::anyhow!("no auth token found for {server}. Run `toku sync init` first.") + anyhow::anyhow!("no auth token found for {server}. Run `toku sync login` first.") }) } @@ -5250,7 +5252,9 @@ fn cmd_sync(data_dir: &Path, action: SyncAction, output_format: &OutputFormat) - } _ => { eprintln!("Sync disabled. Local data preserved."); - eprintln!(" Run `toku sync init` to re-enable."); + eprintln!( + " Run `toku sync enroll` to re-enable (or `toku sync signup` for a new account)." + ); } } Ok(()) @@ -5442,7 +5446,9 @@ fn cmd_sync(data_dir: &Path, action: SyncAction, output_format: &OutputFormat) - let client = sync::client::SyncClient::new(server)?; let device = sync_repo.get_device()?.ok_or_else(|| { - anyhow::anyhow!("no device identity found. Run `toku sync init` first.") + anyhow::anyhow!( + "no device identity found. Run `toku sync signup` or `toku sync enroll` first." + ) })?; let mut clock = toku_core::HybridClock::new(&device.device_id); let hlc_str = clock.now().to_canonical(); @@ -5452,7 +5458,8 @@ fn cmd_sync(data_dir: &Path, action: SyncAction, output_format: &OutputFormat) - let key_bytes = token_store.load_sync_key(server)?.ok_or_else(|| { anyhow::anyhow!( "hosted sync requires client-side encryption but no key is configured.\n\ - Run `toku sync init --passphrase` to enroll this device with encryption." + Run `toku sync login` to unlock your library key with your password and \ + Secret Key, or `toku sync enroll` to enroll this device into your account." ) })?; let key = toku_core::SyncKey::from_exported_bytes(&key_bytes) diff --git a/crates/toku-sync-client/src/orchestrator.rs b/crates/toku-sync-client/src/orchestrator.rs index e610f6d..cd38d0a 100644 --- a/crates/toku-sync-client/src/orchestrator.rs +++ b/crates/toku-sync-client/src/orchestrator.rs @@ -188,16 +188,18 @@ fn open_db(data_dir: &Path) -> anyhow::Result { } fn require_sync(config: &toku_core::TokuConfig) -> anyhow::Result<&toku_core::SyncConfig> { - config - .sync - .as_ref() - .ok_or_else(|| anyhow::anyhow!("sync is not configured. Run sync init first.")) + config.sync.as_ref().ok_or_else(|| { + anyhow::anyhow!( + "sync is not configured. Run `toku sync signup` (new account) or \ + `toku sync enroll` (existing account) first." + ) + }) } fn require_token(token_store: &TokenStore, server: &str) -> anyhow::Result { - token_store - .load(server)? - .ok_or_else(|| anyhow::anyhow!("no auth token found for {server}. Run sync init first.")) + token_store.load(server)?.ok_or_else(|| { + anyhow::anyhow!("no auth token found for {server}. Run `toku sync login` first.") + }) } /// Load the client-side encryption key when encryption is enabled for this @@ -407,7 +409,8 @@ pub fn init( // actionable error instead of registering an unusable device. Err(anyhow::anyhow!( "hosted sync requires client-side encryption: a passphrase is mandatory.\n\ - Re-run with `toku sync init --passphrase` (you will be prompted securely).\n\ + Run `toku sync signup` (new account) or `toku sync enroll` (existing account) \ + to set up encryption.\n\ Local-only, single-device usage needs no sync and no passphrase." )) } @@ -444,7 +447,8 @@ pub fn push(data_dir: &Path) -> anyhow::Result { let key = load_encryption_key(&token_store, server, sync_config)?.ok_or_else(|| { anyhow::anyhow!( "hosted sync requires client-side encryption but no key is configured.\n\ - Re-run `toku sync init --passphrase` to enroll this device with encryption." + Run `toku sync login` to unlock your library key with your password and Secret Key, \ + or `toku sync enroll` to enroll this device into your account." ) })?; let wire_ops: Vec = unpushed @@ -593,7 +597,8 @@ pub fn bootstrap(data_dir: &Path, reset_cursor: bool) -> anyhow::Result Markup { @if !o.configured { div.conflict-empty { p { "Sync is not configured on this device." } - p.muted { "Set up sync from the command line (" code { "toku sync init" } ") or a native app. The web dashboard is a read-only companion." } + p.muted { "Set up sync from the command line (" code { "toku sync signup" } " or " code { "toku sync enroll" } ") or a native app. The web dashboard is a read-only companion." } } } @else { div.sync-card {