Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 13 additions & 8 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | 🟡 |
Expand Down Expand Up @@ -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.

---
Expand Down
23 changes: 15 additions & 8 deletions crates/toku-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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.")
})
}

Expand Down Expand Up @@ -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(())
Expand Down Expand Up @@ -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();
Expand All @@ -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)
Expand Down
25 changes: 15 additions & 10 deletions crates/toku-sync-client/src/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,18 @@ fn open_db(data_dir: &Path) -> anyhow::Result<Database> {
}

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<String> {
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
Expand Down Expand Up @@ -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."
))
}
Expand Down Expand Up @@ -444,7 +447,8 @@ pub fn push(data_dir: &Path) -> anyhow::Result<PushOutcome> {
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<WireOp> = unpushed
Expand Down Expand Up @@ -593,7 +597,8 @@ pub fn bootstrap(data_dir: &Path, reset_cursor: bool) -> anyhow::Result<Bootstra
let key = load_encryption_key(&token_store, server, sync_config)?.ok_or_else(|| {
anyhow::anyhow!(
"downloaded an encrypted snapshot 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 envelope: toku_core::EncryptedEnvelope = serde_json::from_str(&snap.snapshot_json)
Expand Down
2 changes: 1 addition & 1 deletion crates/toku-web/src/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ pub fn sync_page(o: &SyncOverview) -> 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 {
Expand Down
Loading